mirror of
https://github.com/dpethes/rerogue.git
synced 2025-06-07 18:58:32 +02:00
terrain viewer: use vertex arrays + glDrawElements.
Vertex normals are fake, needs proper per-vertex normals
This commit is contained in:
parent
2d5c549847
commit
0d8ff0407e
@ -1,21 +1,26 @@
|
|||||||
12B: zeros
|
HMP
|
||||||
4B float: always 0x3f000000 (0.5)
|
|
||||||
4B float: terrain height scale
|
|
||||||
4B float: always 0x3f000000
|
|
||||||
2B int : tile count
|
|
||||||
2B int : ?
|
|
||||||
4B int : offset to tiles
|
|
||||||
4B int : offset to some data?
|
|
||||||
2B int : width in BLK
|
|
||||||
2B int : height in BLK
|
|
||||||
array[width * height] of 2B int: tile indices
|
|
||||||
xB ?
|
|
||||||
tiles
|
|
||||||
{
|
{
|
||||||
2B int: texmap idx (from texture index file)
|
12B: zeros
|
||||||
1B int: ?
|
4B float: always 0x3f000000 (0.5)
|
||||||
1B uint: lo - minimum height in tile (probably for terrain LOD?)
|
4B float: terrain height scale
|
||||||
1B uint: hi - maximum height in tile
|
4B float: always 0x3f000000
|
||||||
array[25] of uint8: - 5x5 heights
|
2B int : tile count
|
||||||
|
2B int : ?
|
||||||
|
4B int : offset to tiles
|
||||||
|
4B int : offset to some data?
|
||||||
|
2B int : width in BLK
|
||||||
|
2B int : height in BLK
|
||||||
|
array[width * height] of 2B int: tile indices
|
||||||
|
xB ?
|
||||||
|
array[tile count] of TILE
|
||||||
|
2B 0x0000
|
||||||
|
}
|
||||||
|
|
||||||
|
TILE [30B]
|
||||||
|
{
|
||||||
|
2B int: texmap idx (from texture index file)
|
||||||
|
1B int: ?
|
||||||
|
1B int: lo - minimum height in tile (probably for terrain LOD? clipping?)
|
||||||
|
1B int: hi - maximum height in tile
|
||||||
|
array[25] of int8: 5x5 height values
|
||||||
}
|
}
|
||||||
2B 0x0000
|
|
||||||
|
@ -20,9 +20,9 @@ type
|
|||||||
TTile = packed record
|
TTile = packed record
|
||||||
texture_index: word;
|
texture_index: word;
|
||||||
unknown_attrib: byte;
|
unknown_attrib: byte;
|
||||||
height_lo: byte;
|
height_lo: shortint;
|
||||||
height_hi: byte;
|
height_hi: shortint;
|
||||||
heights: array[0..24] of byte;
|
heights: array[0..24] of shortint;
|
||||||
end;
|
end;
|
||||||
PTile = ^TTile;
|
PTile = ^TTile;
|
||||||
|
|
||||||
@ -37,12 +37,6 @@ type
|
|||||||
texture_index_map: array of integer;
|
texture_index_map: array of integer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TVertex3f = record
|
|
||||||
x, y, z: single;
|
|
||||||
u, v: single
|
|
||||||
end;
|
|
||||||
PVertex3f = ^TVertex3f;
|
|
||||||
|
|
||||||
{ TWorld }
|
{ TWorld }
|
||||||
|
|
||||||
TWorld = class
|
TWorld = class
|
||||||
@ -55,7 +49,6 @@ type
|
|||||||
|
|
||||||
public
|
public
|
||||||
heightmap: THeightmap;
|
heightmap: THeightmap;
|
||||||
vertex_array: PVertex3f;
|
|
||||||
vertex_count: integer;
|
vertex_count: integer;
|
||||||
|
|
||||||
property TileWidth: word read heightmap.width;
|
property TileWidth: word read heightmap.width;
|
||||||
@ -217,7 +210,7 @@ begin
|
|||||||
heightmap.blk := blk;
|
heightmap.blk := blk;
|
||||||
|
|
||||||
//tiles
|
//tiles
|
||||||
//writeln('tiles: ', tile_count);
|
//writeln('filepos: ', FilePos(f)); writeln('tile pos: ', tile_offset);
|
||||||
Seek(f, tile_offset);
|
Seek(f, tile_offset);
|
||||||
heightmap.tile_count := tile_count;
|
heightmap.tile_count := tile_count;
|
||||||
heightmap.tiles := getmem(tile_count * 30);
|
heightmap.tiles := getmem(tile_count * 30);
|
||||||
@ -241,13 +234,11 @@ end;
|
|||||||
constructor TWorld.Create;
|
constructor TWorld.Create;
|
||||||
begin
|
begin
|
||||||
height_texture := nil;
|
height_texture := nil;
|
||||||
vertex_array := nil;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TWorld.Destroy;
|
destructor TWorld.Destroy;
|
||||||
begin
|
begin
|
||||||
if height_texture <> nil then Freemem(height_texture);
|
if height_texture <> nil then Freemem(height_texture);
|
||||||
if vertex_array <> nil then Freemem(vertex_array);
|
|
||||||
inherited Destroy;
|
inherited Destroy;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ unit terrain_mesh;
|
|||||||
interface
|
interface
|
||||||
|
|
||||||
uses
|
uses
|
||||||
Classes, SysUtils,
|
Classes, SysUtils, matrix,
|
||||||
gl, glext, glu,
|
gl, glext, glu,
|
||||||
rs_world;
|
rs_world, vector_util;
|
||||||
|
|
||||||
type
|
type
|
||||||
TRenderOpts = record
|
TRenderOpts = record
|
||||||
@ -17,17 +17,21 @@ type
|
|||||||
fg_to_draw: integer;
|
fg_to_draw: integer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TTerrainBlock = packed record
|
TTerrainBlock = record
|
||||||
texture_index: integer;
|
texture_index: integer;
|
||||||
vertices: array[0..25] of TVertex3f;
|
vertices: array[0..24] of Tvector3_single; //25*3*4 = 300B
|
||||||
end;
|
normals: array[0..24] of Tvector3_single; //25*3*4 = 300B
|
||||||
|
end; //~600B per block
|
||||||
|
|
||||||
{ TTerrainMesh }
|
{ TTerrainMesh }
|
||||||
TTerrainMesh = class
|
TTerrainMesh = class
|
||||||
private
|
private
|
||||||
terrain: TWorld;
|
terrain: TWorld;
|
||||||
blocks: array of array of TTerrainBlock;
|
blocks: array of array of TTerrainBlock;
|
||||||
|
block_texcoords: array[0..24] of Tvector2_single; //static, 25*2*4 = 200B
|
||||||
|
block_face_indices: array[0..16*2*3 - 1] of byte; //static, 96B
|
||||||
textures_glidx: array of integer;
|
textures_glidx: array of integer;
|
||||||
|
procedure InitBlockStaticData;
|
||||||
procedure TransformTiles;
|
procedure TransformTiles;
|
||||||
public
|
public
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
@ -44,36 +48,60 @@ implementation
|
|||||||
}
|
}
|
||||||
procedure TTerrainMesh.TransformTiles;
|
procedure TTerrainMesh.TransformTiles;
|
||||||
|
|
||||||
//basex/y - offset in vertices
|
//basex/y - position in block units for given dimension (0..block_size-1)
|
||||||
//TODO solve flipped coords
|
//todo fix: the params are flipped.. and so are the calculations
|
||||||
procedure TileToBlock(var blk: TTerrainBlock; var tile: TTile; basex, basey: integer);
|
function TileToBlock(var tile: TTile; basex, basey: integer): TTerrainBlock;
|
||||||
const
|
const
|
||||||
h_scale = 0.5;
|
h_scale = 0.5;
|
||||||
var
|
var
|
||||||
x, y: integer;
|
x, y: integer;
|
||||||
v: TVertex3f;
|
|
||||||
width_half, height_half: integer; //size in vertices
|
|
||||||
v_scale: single;
|
v_scale: single;
|
||||||
begin
|
begin
|
||||||
width_half := terrain.TileWidth * 2;
|
result.texture_index := tile.texture_index;
|
||||||
height_half := terrain.TileHeight * 2;
|
//dim * vertices_per_tile - half_tile_dim * vertices_per_tile
|
||||||
|
basey := basey * 4 - terrain.TileHeight * 2;
|
||||||
|
basex := basex * 4 - terrain.TileWidth * 2;
|
||||||
v_scale := terrain.heightmap.y_scale;
|
v_scale := terrain.heightmap.y_scale;
|
||||||
for y := 0 to 4 do
|
for y := 0 to 4 do
|
||||||
for x := 0 to 4 do begin
|
for x := 0 to 4 do begin
|
||||||
v.x := (-width_half + basex + x) * h_scale;
|
result.vertices[y * 5 + x].init( //x,y,z
|
||||||
v.z := (-height_half + basey + y) * h_scale;
|
(basex + x) * h_scale,
|
||||||
v.v := (1 - x/4);
|
tile.heights[y+x*5] * v_scale * -1,
|
||||||
v.u := y/4;
|
(basey + y) * h_scale
|
||||||
v.y := shortint(tile.heights[y+x*5]) * v_scale;
|
);
|
||||||
v.y := -v.y;
|
end;
|
||||||
|
end;
|
||||||
blk.vertices[y * 5 + x] := v;
|
|
||||||
|
//todo do proper per-vertex normal:
|
||||||
|
//this only calculates face normals and sets them to face's vertices, overwriting the value set
|
||||||
|
//from previous face
|
||||||
|
procedure FakeNormals(var blk: TTerrainBlock);
|
||||||
|
procedure SetTriData(const tri_idx: integer; const i0, i1, i2: byte);
|
||||||
|
var
|
||||||
|
normal: Tvector3_single;
|
||||||
|
begin
|
||||||
|
normal := GetNormal(blk.vertices[i0], blk.vertices[i1], blk.vertices[i2]);
|
||||||
|
blk.normals[i0] := normal;
|
||||||
|
blk.normals[i1] := normal;
|
||||||
|
blk.normals[i2] := normal;
|
||||||
|
end;
|
||||||
|
const
|
||||||
|
VertexStride = 5;
|
||||||
|
var
|
||||||
|
x, y, i, tri_idx: integer;
|
||||||
|
begin
|
||||||
|
tri_idx := 0;
|
||||||
|
for y := 0 to 3 do
|
||||||
|
for x := 0 to 3 do begin
|
||||||
|
i := y * VertexStride + x;
|
||||||
|
SetTriData(tri_idx, i+1, i, i+VertexStride);
|
||||||
|
SetTriData(tri_idx + 1, i+1, i+VertexStride, i+VertexStride+1);
|
||||||
|
tri_idx += 2;
|
||||||
end;
|
end;
|
||||||
blk.texture_index := tile.texture_index;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
var
|
var
|
||||||
x, y, i, tile_idx: integer;
|
x, y, tile_idx: integer;
|
||||||
blk: TTerrainBlock;
|
blk: TTerrainBlock;
|
||||||
tile: TTile;
|
tile: TTile;
|
||||||
begin
|
begin
|
||||||
@ -83,12 +111,46 @@ begin
|
|||||||
tile_idx := terrain.heightmap.blk[y * terrain.TileWidth + x];
|
tile_idx := terrain.heightmap.blk[y * terrain.TileWidth + x];
|
||||||
tile := terrain.heightmap.tiles[tile_idx];
|
tile := terrain.heightmap.tiles[tile_idx];
|
||||||
|
|
||||||
TileToBlock(blk, tile, y * 4, x * 4);
|
blk := TileToBlock(tile, y, x);
|
||||||
|
FakeNormals(blk);
|
||||||
blocks[y, x] := blk;
|
blocks[y, x] := blk;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ InitBlockStaticData
|
||||||
|
Initializes data shared between tiles: face vertex indices, texture coords
|
||||||
|
}
|
||||||
|
procedure TTerrainMesh.InitBlockStaticData;
|
||||||
|
|
||||||
|
procedure SetTriData(const tri_idx: integer; const i0, i1, i2: byte);
|
||||||
|
begin
|
||||||
|
block_face_indices[tri_idx * 3 + 0] := i0;
|
||||||
|
block_face_indices[tri_idx * 3 + 1] := i1;
|
||||||
|
block_face_indices[tri_idx * 3 + 2] := i2;
|
||||||
|
end;
|
||||||
|
|
||||||
|
const
|
||||||
|
VertexStride = 5;
|
||||||
|
var
|
||||||
|
x, y, i, tri_idx: integer;
|
||||||
|
begin
|
||||||
|
tri_idx := 0;
|
||||||
|
//init face indices
|
||||||
|
for y := 0 to 3 do
|
||||||
|
for x := 0 to 3 do begin
|
||||||
|
i := y * VertexStride + x;
|
||||||
|
SetTriData(tri_idx, i+1, i, i+VertexStride);
|
||||||
|
SetTriData(tri_idx+1, i+1, i+VertexStride, i+VertexStride+1);
|
||||||
|
tri_idx += 2;
|
||||||
|
end;
|
||||||
|
//init uv coords
|
||||||
|
for y := 0 to 4 do
|
||||||
|
for x := 0 to 4 do begin
|
||||||
|
block_texcoords[y * 5 + x].init(y/4, 1 - x/4); //u, v
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
destructor TTerrainMesh.Destroy;
|
destructor TTerrainMesh.Destroy;
|
||||||
begin
|
begin
|
||||||
inherited Destroy;
|
inherited Destroy;
|
||||||
@ -100,6 +162,10 @@ begin
|
|||||||
terrain.LoadFromFiles('hmp_0', 'lv_0.text', 'lv_0.tex');
|
terrain.LoadFromFiles('hmp_0', 'lv_0.text', 'lv_0.tex');
|
||||||
//terrain.LoadFromFiles('hmp_1', 'lv_1.text', 'lv_1.tex');
|
//terrain.LoadFromFiles('hmp_1', 'lv_1.text', 'lv_1.tex');
|
||||||
TransformTiles;
|
TransformTiles;
|
||||||
|
InitBlockStaticData;
|
||||||
|
WriteLn(Format('terrain size: %dx%d, tris: %d',
|
||||||
|
[terrain.TileWidth, terrain.TileHeight,
|
||||||
|
terrain.TileWidth * terrain.TileHeight * 4 * 4 * 2]));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
//generate textures. TODO texture atlas?
|
//generate textures. TODO texture atlas?
|
||||||
@ -139,91 +205,42 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
//cross product + normalize
|
|
||||||
function GetNormalv(const v0, v1, v2: TVertex3f): TVertex3f;
|
|
||||||
var
|
|
||||||
a, b: TVertex3f;
|
|
||||||
len: single;
|
|
||||||
begin
|
|
||||||
a.x := v0.x - v1.x;
|
|
||||||
a.y := v0.y - v1.y;
|
|
||||||
a.z := v0.z - v1.z;
|
|
||||||
|
|
||||||
b.x := v1.x - v2.x;
|
|
||||||
b.y := v1.y - v2.y;
|
|
||||||
b.z := v1.z - v2.z;
|
|
||||||
|
|
||||||
result.x := (a.y * b.z) - (a.z * b.y);
|
|
||||||
result.y := (a.z * b.x) - (a.x * b.z);
|
|
||||||
result.z := (a.x * b.y) - (a.y * b.x);
|
|
||||||
|
|
||||||
len := sqrt( sqr(result.x) + sqr(result.y) + sqr(result.z) );
|
|
||||||
if len = 0 then len := 1;
|
|
||||||
|
|
||||||
result.x /= len;
|
|
||||||
result.y /= len;
|
|
||||||
result.z /= len;
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
//draw vertices from each block
|
//draw vertices from each block
|
||||||
procedure TTerrainMesh.DrawGL(opts: TRenderOpts);
|
procedure TTerrainMesh.DrawGL(opts: TRenderOpts);
|
||||||
|
|
||||||
procedure RenderBlock(var blk: TTerrainBlock);
|
|
||||||
procedure RenderTri(i0, i1, i2:integer);
|
|
||||||
var
|
|
||||||
v, n: TVertex3f;
|
|
||||||
begin
|
|
||||||
n := GetNormalv(blk.vertices[i0], blk.vertices[i1], blk.vertices[i2]);
|
|
||||||
v := blk.vertices[i0];
|
|
||||||
glNormal3f(n.x, n.y, n.z);
|
|
||||||
glTexCoord2f(v.u, v.v);
|
|
||||||
glVertex3fv(@v);
|
|
||||||
v := blk.vertices[i1];
|
|
||||||
glTexCoord2f(v.u, v.v);
|
|
||||||
glVertex3fv(@v);
|
|
||||||
v := blk.vertices[i2];
|
|
||||||
glTexCoord2f(v.u, v.v);
|
|
||||||
glVertex3fv(@v);
|
|
||||||
end;
|
|
||||||
var
|
|
||||||
i, x, y, stride: integer;
|
|
||||||
begin
|
|
||||||
stride := 5;
|
|
||||||
glBindTexture(GL_TEXTURE_2D, textures_glidx[blk.texture_index]);
|
|
||||||
|
|
||||||
glBegin(GL_TRIANGLES);
|
|
||||||
//glColor3f(0, 1, 0);
|
|
||||||
for y := 0 to 3 do
|
|
||||||
for x := 0 to 3 do begin
|
|
||||||
//do two triangles
|
|
||||||
i := y * stride + x;
|
|
||||||
RenderTri(i+1, i, i+stride);
|
|
||||||
RenderTri(i+1, i+stride, i+stride+1);
|
|
||||||
end;
|
|
||||||
glEnd;
|
|
||||||
{ //only 2 tris per block
|
|
||||||
glBegin(GL_TRIANGLES);
|
|
||||||
i := y * stride + x;
|
|
||||||
RenderTri(0, 4, 24);
|
|
||||||
RenderTri(24, 20, 0);
|
|
||||||
glEnd;
|
|
||||||
}
|
|
||||||
end;
|
|
||||||
|
|
||||||
var
|
var
|
||||||
x, y: integer;
|
x, y: integer;
|
||||||
blk: TTerrainBlock;
|
blk: TTerrainBlock;
|
||||||
|
last_tex_index: integer;
|
||||||
begin
|
begin
|
||||||
if opts.wireframe then
|
if opts.wireframe then
|
||||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
||||||
else
|
else
|
||||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||||
|
|
||||||
glEnable(GL_TEXTURE_2D);
|
glEnable(GL_TEXTURE_2D);
|
||||||
|
glEnableClientState(GL_VERTEX_ARRAY);
|
||||||
|
glEnableClientState(GL_NORMAL_ARRAY);
|
||||||
|
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||||
|
last_tex_index := -1;
|
||||||
|
|
||||||
for y := 0 to terrain.TileHeight - 1 do
|
for y := 0 to terrain.TileHeight - 1 do
|
||||||
for x := 0 to terrain.TileWidth - 1 do begin
|
for x := 0 to terrain.TileWidth - 1 do begin
|
||||||
blk := blocks[y, x];
|
blk := blocks[y, x];
|
||||||
RenderBlock(blk);
|
//repeated texture binding slows this down a lot (68->30fps)
|
||||||
|
//todo sort by texture?
|
||||||
|
if last_tex_index <> blk.texture_index then begin
|
||||||
|
last_tex_index := blk.texture_index;
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textures_glidx[last_tex_index]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
glVertexPointer(3, GL_FLOAT, sizeof(Tvector3_single), @blk.vertices[0].data[0]);
|
||||||
|
glNormalPointer(GL_FLOAT, sizeof(Tvector3_single), @blk.normals[0].data[0]);
|
||||||
|
glTexCoordPointer(2, GL_FLOAT, sizeof(Tvector2_single), @block_texcoords[0].data[0]);
|
||||||
|
|
||||||
|
if opts.points then
|
||||||
|
glDrawArrays(GL_POINTS, 0, 25)
|
||||||
|
else
|
||||||
|
glDrawElements(GL_TRIANGLES, 16*2*3, GL_UNSIGNED_BYTE, @block_face_indices);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
<PackageName Value="LCL"/>
|
<PackageName Value="LCL"/>
|
||||||
</Item1>
|
</Item1>
|
||||||
</RequiredPackages>
|
</RequiredPackages>
|
||||||
<Units Count="3">
|
<Units Count="4">
|
||||||
<Unit0>
|
<Unit0>
|
||||||
<Filename Value="terrain_viewer.pas"/>
|
<Filename Value="terrain_viewer.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
@ -53,6 +53,11 @@
|
|||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
<UnitName Value="rs_world"/>
|
<UnitName Value="rs_world"/>
|
||||||
</Unit2>
|
</Unit2>
|
||||||
|
<Unit3>
|
||||||
|
<Filename Value="vector_util.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
<UnitName Value="vector_util"/>
|
||||||
|
</Unit3>
|
||||||
</Units>
|
</Units>
|
||||||
</ProjectOptions>
|
</ProjectOptions>
|
||||||
<CompilerOptions>
|
<CompilerOptions>
|
||||||
|
@ -22,7 +22,7 @@ program terrain_viewer;
|
|||||||
uses
|
uses
|
||||||
sysutils, math,
|
sysutils, math,
|
||||||
gl, glu, glext, sdl,
|
gl, glu, glext, sdl,
|
||||||
terrain_mesh, rs_world;
|
terrain_mesh;
|
||||||
|
|
||||||
const
|
const
|
||||||
SCR_W_fscrn = 1024;
|
SCR_W_fscrn = 1024;
|
||||||
|
33
terrain_viewer/vector_util.pas
Normal file
33
terrain_viewer/vector_util.pas
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
unit vector_util;
|
||||||
|
{$mode objfpc}{$H+}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
matrix;
|
||||||
|
|
||||||
|
function GetNormal(v0, v1, v2: Tvector3_single): Tvector3_single;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
//cross product + normalize
|
||||||
|
function GetNormal(v0, v1, v2: Tvector3_single): Tvector3_single;
|
||||||
|
var
|
||||||
|
a, b: Tvector3_single;
|
||||||
|
len: single;
|
||||||
|
begin
|
||||||
|
a := v0 - v1;
|
||||||
|
b := v1 - v2;
|
||||||
|
|
||||||
|
result.data[0] := (a.data[1] * b.data[2]) - (a.data[2] * b.data[1]);
|
||||||
|
result.data[1] := (a.data[2] * b.data[0]) - (a.data[0] * b.data[2]);
|
||||||
|
result.data[2] := (a.data[0] * b.data[1]) - (a.data[1] * b.data[0]);
|
||||||
|
|
||||||
|
len := result.length;
|
||||||
|
if len = 0 then len := 1;
|
||||||
|
|
||||||
|
result /= len;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user