move image loading to assets.zig
This commit is contained in:
parent
9f3c41f991
commit
b73ea021f7
11
build.zig
11
build.zig
@ -29,9 +29,6 @@ pub fn build(b: *std.Build) !void {
|
||||
exe_mod.linkLibrary(tracy_dependency.artifact("tracy"));
|
||||
exe_mod.addImport("tracy", tracy_dependency.module("tracy"));
|
||||
|
||||
const stb_dependency = b.dependency("stb", .{});
|
||||
exe_mod.addIncludePath(stb_dependency.path("."));
|
||||
|
||||
const sokol_c_dependency = b.dependency("sokol_c", .{});
|
||||
exe_mod.addIncludePath(sokol_c_dependency.path("util"));
|
||||
|
||||
@ -41,6 +38,9 @@ pub fn build(b: *std.Build) !void {
|
||||
const tiled_dependency = b.dependency("tiled", .{});
|
||||
exe_mod.addImport("tiled", tiled_dependency.module("tiled"));
|
||||
|
||||
const stb_image_dependency = b.dependency("stb_image", .{});
|
||||
exe_mod.addImport("stb_image", stb_image_dependency.module("stb_image"));
|
||||
|
||||
const sokol_dependency = b.dependency("sokol", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
@ -66,11 +66,6 @@ pub fn build(b: *std.Build) !void {
|
||||
.flags = cflags.items
|
||||
});
|
||||
|
||||
exe_mod.addCSourceFile(.{
|
||||
.file = b.path("src/libs/stb_image.c"),
|
||||
.flags = &.{}
|
||||
});
|
||||
|
||||
if (has_imgui) {
|
||||
if (b.lazyDependency("cimgui", .{
|
||||
.target = target,
|
||||
|
||||
@ -17,10 +17,6 @@
|
||||
.url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
|
||||
.hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
|
||||
},
|
||||
.stb = .{
|
||||
.url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
|
||||
.hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
|
||||
},
|
||||
.sokol_c = .{
|
||||
.url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
|
||||
.hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
|
||||
@ -31,6 +27,9 @@
|
||||
},
|
||||
.tiled = .{
|
||||
.path = "./libs/tiled"
|
||||
},
|
||||
.stb_image = .{
|
||||
.path = "./libs/stb_image"
|
||||
}
|
||||
},
|
||||
.paths = .{
|
||||
|
||||
37
libs/stb_image/build.zig
Normal file
37
libs/stb_image/build.zig
Normal file
@ -0,0 +1,37 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const mod = b.addModule("stb_image", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
});
|
||||
|
||||
const stb_dependency = b.dependency("stb", .{});
|
||||
mod.addIncludePath(stb_dependency.path("."));
|
||||
|
||||
mod.addCSourceFile(.{
|
||||
.file = b.path("src/stb_image_impl.c"),
|
||||
.flags = &.{}
|
||||
});
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "stb_image",
|
||||
.root_module = mod
|
||||
});
|
||||
b.installArtifact(lib);
|
||||
|
||||
{
|
||||
const tests = b.addTest(.{
|
||||
.root_module = mod
|
||||
});
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_tests.step);
|
||||
}
|
||||
}
|
||||
17
libs/stb_image/build.zig.zon
Normal file
17
libs/stb_image/build.zig.zon
Normal file
@ -0,0 +1,17 @@
|
||||
.{
|
||||
.name = .stb_image,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xe5d3607840482046, // Changing this has security and trust implications.
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.stb = .{
|
||||
.url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
|
||||
.hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
30
libs/stb_image/src/root.zig
Normal file
30
libs/stb_image/src/root.zig
Normal file
@ -0,0 +1,30 @@
|
||||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("stb_image.h");
|
||||
});
|
||||
|
||||
const STBImage = @This();
|
||||
|
||||
rgba8_pixels: [*c]u8,
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
pub fn load(png_data: []const u8) !STBImage {
|
||||
var width: c_int = undefined;
|
||||
var height: c_int = undefined;
|
||||
const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4);
|
||||
if (pixels == null) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
return STBImage{
|
||||
.rgba8_pixels = pixels,
|
||||
.width = @intCast(width),
|
||||
.height = @intCast(height)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const STBImage) void {
|
||||
c.stbi_image_free(self.rgba8_pixels);
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
const std = @import("std");
|
||||
const Font = @import("./engine/font.zig");
|
||||
const Gfx = @import("./engine/graphics.zig");
|
||||
const Engine = @import("./engine/root.zig");
|
||||
const Gfx = Engine.Graphics;
|
||||
const Font = Gfx.Font;
|
||||
|
||||
const Math = @import("./engine/math.zig");
|
||||
const Vec2 = Math.Vec2;
|
||||
|
||||
const Assets = @This();
|
||||
|
||||
@ -12,17 +16,48 @@ pub const FontVariant = enum {
|
||||
const Map = std.EnumArray(FontVariant, Font.Id);
|
||||
};
|
||||
|
||||
font_map: FontVariant.Map = .initFill(.invalid),
|
||||
pub const ImageId = enum {
|
||||
tilemap
|
||||
};
|
||||
|
||||
pub const TileId = enum {
|
||||
player,
|
||||
open_door,
|
||||
locked_door
|
||||
};
|
||||
|
||||
font_map: FontVariant.Map,
|
||||
tile_coords: std.EnumArray(TileId, Vec2),
|
||||
|
||||
tilemap: Gfx.Image,
|
||||
tile_size: Vec2,
|
||||
tilemap_size: Vec2,
|
||||
tilemap_view: Gfx.View,
|
||||
|
||||
pub fn init() !Assets {
|
||||
const font_map: FontVariant.Map = .init(.{
|
||||
const tilemap_data = try Gfx.STBImage.load(@embedFile("./assets/kenney-micro-roguelike/colored_tilemap_packed.png"));
|
||||
defer tilemap_data.deinit();
|
||||
|
||||
const tilemap = try Gfx.makeImageWithMipMaps(&.{
|
||||
tilemap_data
|
||||
});
|
||||
const tilemap_view = try Gfx.makeView(tilemap);
|
||||
|
||||
return Assets{
|
||||
.font_map = .init(.{
|
||||
.regular = try Gfx.fonts.add("regular", @embedFile("./assets/roboto-font/Roboto-Regular.ttf")),
|
||||
.italic = try Gfx.fonts.add("italic", @embedFile("./assets/roboto-font/Roboto-Italic.ttf")),
|
||||
.bold = try Gfx.fonts.add("bold", @embedFile("./assets/roboto-font/Roboto-Bold.ttf")),
|
||||
});
|
||||
|
||||
return Assets{
|
||||
.font_map = font_map
|
||||
}),
|
||||
.tile_coords = .init(.{
|
||||
.player = .init(4, 0),
|
||||
.locked_door = .init(7, 2),
|
||||
.open_door = .init(5, 2)
|
||||
}),
|
||||
.tilemap = tilemap,
|
||||
.tilemap_view = tilemap_view,
|
||||
.tile_size = .init(8, 8),
|
||||
.tilemap_size = .init(@floatFromInt(tilemap_data.width), @floatFromInt(tilemap_data.height))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -10,12 +10,15 @@ const imgui = @import("imgui.zig");
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const STBImage = @import("stb_image");
|
||||
pub const Image = sg.Image;
|
||||
pub const View = sg.View;
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("sokol/sokol_gfx.h");
|
||||
@cInclude("sokol/sokol_gl.h");
|
||||
@cInclude("fontstash.h");
|
||||
@cInclude("sokol_fontstash.h");
|
||||
@cInclude("stb_image.h");
|
||||
});
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
@ -64,16 +67,6 @@ const DrawFrame = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const ImageId = enum {
|
||||
tilemap
|
||||
};
|
||||
|
||||
pub const TileId = enum {
|
||||
player,
|
||||
open_door,
|
||||
locked_door
|
||||
};
|
||||
|
||||
pub const Borders = struct {
|
||||
size: f32 = 0,
|
||||
left: Vec4 = rgba(0, 0, 0, 0),
|
||||
@ -114,16 +107,9 @@ pub var circle_quality: f32 = 6;
|
||||
pub var draw_frame: DrawFrame = undefined;
|
||||
var main_pipeline: sgl.Pipeline = .{};
|
||||
|
||||
var image_map: std.EnumArray(ImageId, sg.Image) = .initFill(.{});
|
||||
var image_view_map: std.EnumArray(ImageId, sg.View) = .initFill(.{});
|
||||
|
||||
var linear_sampler: sg.Sampler = .{};
|
||||
var nearest_sampler: sg.Sampler = .{};
|
||||
|
||||
var tile_coords: std.EnumArray(TileId, Vec2) = .initUndefined();
|
||||
const tile_size: Vec2 = .init(8, 8);
|
||||
var tilemap_size: Vec2 = .init(0, 0);
|
||||
|
||||
pub var fonts: Font.Context = undefined;
|
||||
|
||||
const Options = struct {
|
||||
@ -135,63 +121,29 @@ inline fn structCast(T: type, value: anytype) T {
|
||||
return @as(*T, @ptrFromInt(@intFromPtr(&value))).*;
|
||||
}
|
||||
|
||||
const ImageData = struct {
|
||||
rgba8_pixels: [*c]u8,
|
||||
width: u32,
|
||||
height: u32,
|
||||
pub fn makeImageWithMipMaps(image_datas: []const STBImage) !sg.Image {
|
||||
assert(image_datas.len > 0);
|
||||
|
||||
fn load(png_data: []const u8) !ImageData {
|
||||
var width: c_int = undefined;
|
||||
var height: c_int = undefined;
|
||||
const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4);
|
||||
if (pixels == null) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
return ImageData{
|
||||
.rgba8_pixels = pixels,
|
||||
.width = @intCast(width),
|
||||
.height = @intCast(height)
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *const ImageData) void {
|
||||
c.stbi_image_free(self.rgba8_pixels);
|
||||
}
|
||||
};
|
||||
|
||||
fn makeImageWithMipMaps(image_datas: []const ImageData) !sg.Image {
|
||||
var mip_levels_buffer = [_]sg.Range{.{}} ** 16;
|
||||
var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&mip_levels_buffer);
|
||||
|
||||
var image_width: c_int = -1;
|
||||
var image_height: c_int = -1;
|
||||
var data: sg.ImageData = .{};
|
||||
var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&data.mip_levels);
|
||||
|
||||
for (image_datas) |mipmap_image| {
|
||||
if (image_height == -1) {
|
||||
image_width = @intCast(mipmap_image.width);
|
||||
image_height = @intCast(mipmap_image.height);
|
||||
}
|
||||
|
||||
try mip_levels.appendBounded(.{
|
||||
.ptr = mipmap_image.rgba8_pixels,
|
||||
.size = mipmap_image.width * mipmap_image.height * 4
|
||||
});
|
||||
}
|
||||
|
||||
assert(image_width > 0);
|
||||
assert(image_height > 0);
|
||||
|
||||
// TODO: Should error be checked?
|
||||
const image = sg.makeImage(.{
|
||||
.width = image_width,
|
||||
.height = image_height,
|
||||
.width = @intCast(image_datas[0].width),
|
||||
.height = @intCast(image_datas[0].height),
|
||||
.pixel_format = .RGBA8,
|
||||
.usage = .{
|
||||
.immutable = true
|
||||
},
|
||||
.num_mipmaps = @intCast(mip_levels.items.len),
|
||||
.data = sg.ImageData{ .mip_levels = mip_levels_buffer },
|
||||
.data = data
|
||||
});
|
||||
if (image.id == sg.invalid_id) {
|
||||
return error.InvalidImage;
|
||||
@ -200,25 +152,14 @@ fn makeImageWithMipMaps(image_datas: []const ImageData) !sg.Image {
|
||||
return image;
|
||||
}
|
||||
|
||||
fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image {
|
||||
var stbi_images_buffer: [16]ImageData = undefined;
|
||||
var stbi_images: std.ArrayListUnmanaged(ImageData) = .initBuffer(&stbi_images_buffer);
|
||||
defer {
|
||||
for (stbi_images.items) |image| {
|
||||
image.deinit();
|
||||
pub fn makeView(image: sg.Image) !sg.View {
|
||||
const image_view = sg.makeView(.{
|
||||
.texture = .{ .image = image }
|
||||
});
|
||||
if (image_view.id == sg.invalid_id) {
|
||||
return error.InvalidView;
|
||||
}
|
||||
}
|
||||
|
||||
if (image_datas.len > stbi_images.capacity) {
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
|
||||
for (image_datas) |image_data| {
|
||||
const mipmap_image = try ImageData.load(image_data);
|
||||
stbi_images.appendAssumeCapacity(mipmap_image);
|
||||
}
|
||||
|
||||
return try makeImageWithMipMaps(stbi_images.items);
|
||||
return image_view;
|
||||
}
|
||||
|
||||
pub fn init(options: Options) !void {
|
||||
@ -253,6 +194,7 @@ pub fn init(options: Options) !void {
|
||||
|
||||
imgui.setup(options.allocator, .{
|
||||
.logger = structCast(simgui.Logger, options.logger),
|
||||
.no_default_font = true,
|
||||
|
||||
// TODO: Figure out a way to make imgui play nicely with UI
|
||||
// Ideally when mouse is inside a Imgui window, then the imgui cursor should be used.
|
||||
@ -260,37 +202,11 @@ pub fn init(options: Options) !void {
|
||||
.disable_set_mouse_cursor = true
|
||||
});
|
||||
|
||||
// TODO: Move this font loading to assets
|
||||
imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16);
|
||||
|
||||
fonts = try Font.Context.init(512);
|
||||
|
||||
const tilemap = try ImageData.load(@embedFile("../assets/kenney-micro-roguelike/colored_tilemap_packed.png"));
|
||||
defer tilemap.deinit();
|
||||
|
||||
image_map = .init(.{
|
||||
.tilemap = try makeImageWithMipMaps(&.{
|
||||
tilemap
|
||||
})
|
||||
});
|
||||
|
||||
tilemap_size = Vec2.init(@floatFromInt(tilemap.width), @floatFromInt(tilemap.height));
|
||||
|
||||
tile_coords = .init(.{
|
||||
.player = .init(4, 0),
|
||||
.locked_door = .init(7, 2),
|
||||
.open_door = .init(5, 2)
|
||||
});
|
||||
|
||||
var image_iter = image_map.iterator();
|
||||
while (image_iter.next()) |entry| {
|
||||
const image_view = sg.makeView(.{
|
||||
.texture = .{ .image = entry.value.* }
|
||||
});
|
||||
assert(image_view.id != sg.invalid_id);
|
||||
|
||||
image_view_map.set(entry.key, image_view);
|
||||
}
|
||||
|
||||
linear_sampler = sg.makeSampler(.{
|
||||
.min_filter = .LINEAR,
|
||||
.mag_filter = .LINEAR,
|
||||
@ -362,22 +278,21 @@ pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn drawTile(tile_coord: Vec2, pos: Vec2, size: Vec2, tint: Vec4) void {
|
||||
pub fn drawTexturedRectangle(
|
||||
pos: Vec2,
|
||||
size: Vec2,
|
||||
color: Vec4,
|
||||
view: sg.View,
|
||||
view_quad: Rect,
|
||||
) void {
|
||||
sgl.enableTexture();
|
||||
defer sgl.disableTexture();
|
||||
|
||||
sgl.texture(
|
||||
image_view_map.get(.tilemap),
|
||||
view,
|
||||
nearest_sampler
|
||||
);
|
||||
|
||||
var tile_quad = Rect.init(
|
||||
tile_coord.x,
|
||||
tile_coord.y,
|
||||
1,
|
||||
1
|
||||
).multiply(tile_size).divide(tilemap_size);
|
||||
|
||||
const top_left = pos;
|
||||
const top_right = pos.add(.{ .x = size.x, .y = 0 });
|
||||
const bottom_right = pos.add(size);
|
||||
@ -386,42 +301,33 @@ pub fn drawTile(tile_coord: Vec2, pos: Vec2, size: Vec2, tint: Vec4) void {
|
||||
sgl.beginQuads();
|
||||
defer sgl.end();
|
||||
|
||||
sgl.c4f(tint.x, tint.y, tint.z, tint.w);
|
||||
sgl.c4f(color.x, color.y, color.z, color.w);
|
||||
sgl.v2fT2f(
|
||||
top_left.x,
|
||||
top_left.y,
|
||||
tile_quad.left(),
|
||||
tile_quad.top(),
|
||||
view_quad.left(),
|
||||
view_quad.top(),
|
||||
);
|
||||
sgl.v2fT2f(
|
||||
top_right.x,
|
||||
top_right.y,
|
||||
tile_quad.right(),
|
||||
tile_quad.top(),
|
||||
view_quad.right(),
|
||||
view_quad.top(),
|
||||
);
|
||||
sgl.v2fT2f(
|
||||
bottom_right.x,
|
||||
bottom_right.y,
|
||||
tile_quad.right(),
|
||||
tile_quad.bottom(),
|
||||
view_quad.right(),
|
||||
view_quad.bottom(),
|
||||
);
|
||||
sgl.v2fT2f(
|
||||
bottom_left.x,
|
||||
bottom_left.y,
|
||||
tile_quad.left(),
|
||||
tile_quad.bottom(),
|
||||
view_quad.left(),
|
||||
view_quad.bottom(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn drawTileById(tile_id: TileId, pos: Vec2, size: Vec2, tint: Vec4) void {
|
||||
const tile_coord = tile_coords.get(tile_id);
|
||||
drawTile(tile_coord, pos, size, tint);
|
||||
}
|
||||
|
||||
pub fn getTileCoords(tile_id: TileId) Vec2 {
|
||||
return tile_coords.get(tile_id);
|
||||
}
|
||||
|
||||
pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void {
|
||||
// TODO: Don't use line segments
|
||||
drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width);
|
||||
|
||||
@ -4,6 +4,8 @@ const Engine = @import("./engine/root.zig");
|
||||
const Vec2 = Engine.Math.Vec2;
|
||||
const Gfx = Engine.Graphics;
|
||||
|
||||
const Assets = @import("./assets.zig");
|
||||
|
||||
const Entity = @This();
|
||||
|
||||
pub const List = GenerationalArrayList(Entity);
|
||||
@ -26,5 +28,5 @@ locked: bool = false,
|
||||
|
||||
render_tile: ?union(enum) {
|
||||
position: Vec2,
|
||||
id: Gfx.TileId
|
||||
id: Assets.TileId
|
||||
} = null
|
||||
|
||||
25
src/game.zig
25
src/game.zig
@ -7,6 +7,7 @@ const imgui = Engine.imgui;
|
||||
const Gfx = Engine.Graphics;
|
||||
const Vec2 = Engine.Math.Vec2;
|
||||
const Vec4 = Engine.Math.Vec4;
|
||||
const Rect = Engine.Math.Rect;
|
||||
const rgb = Engine.Math.rgb;
|
||||
const rgb_hex = Engine.Math.rgb_hex;
|
||||
|
||||
@ -341,22 +342,21 @@ pub fn getInput(self: *Game, frame: Engine.Frame) Input {
|
||||
}
|
||||
|
||||
fn drawEntity(self: *Game, entity: *Entity) void {
|
||||
_ = self; // autofix
|
||||
if (entity.render_tile) |render_tile| {
|
||||
var tile_coord = switch (render_tile) {
|
||||
.id => |tile_id| Gfx.getTileCoords(tile_id),
|
||||
.id => |tile_id| self.assets.tile_coords.get(tile_id),
|
||||
.position => |position| position
|
||||
};
|
||||
|
||||
if (entity.type == .door) {
|
||||
if (entity.locked) {
|
||||
tile_coord = Gfx.getTileCoords(.locked_door);
|
||||
tile_coord = self.assets.tile_coords.get(.locked_door);
|
||||
} else {
|
||||
tile_coord = Gfx.getTileCoords(.open_door);
|
||||
tile_coord = self.assets.tile_coords.get(.open_door);
|
||||
}
|
||||
}
|
||||
|
||||
Gfx.drawTile(tile_coord, entity.position, .init(1,1), rgb(255, 255, 255));
|
||||
self.drawTile(tile_coord, entity.position);
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,6 +573,21 @@ pub fn tick(self: *Game, frame: Engine.Frame) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn drawTile(self: *Game, tile_coord: Vec2, pos: Vec2) void {
|
||||
Gfx.drawTexturedRectangle(
|
||||
pos,
|
||||
.init(1, 1),
|
||||
Gfx.white,
|
||||
self.assets.tilemap_view,
|
||||
Rect.init(
|
||||
tile_coord.x,
|
||||
tile_coord.y,
|
||||
1,
|
||||
1
|
||||
).multiply(self.assets.tile_size).divide(self.assets.tilemap_size)
|
||||
);
|
||||
}
|
||||
|
||||
pub fn debug(self: *Game) !void {
|
||||
if (!imgui.beginWindow(.{
|
||||
.name = "Debug",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user