gauges: reconstruct headingPointer -- the rotating compass + heading readout [widget recon 2]
The visible money-shot: a green compass needle that rotates with the mech's facing plus a numeric heading in whole degrees (render-verified: the needle swung and the number went 247 -> 182 as the mech turned). Full class reconstruction from the binary (Make/ctor/dtor/TestInstance/ ShowInstance/BecameActive/Execute). Ghidra dropped the x87 float math feeding Execute's needle endpoints and the ctor's NumericDisplay centering, so it was recovered by disassembling BTL4OPT.EXE with capstone (scratchpad/disas_hp.py): the needle is a radial line from innerRadius(20) to outerRadius(39) at the heading angle, endpoints rounded half-up; the readout is round(360 - deg). Two corrections vs the map: - The header ctor was 12 args; the binary's is 14 -- widened it. Fields @0x98/ @0x9C are the needle's inner/outer RADIUS (Execute multiplies them by sin/cos), not "color/spacing" as the old field names implied; renamed accordingly. - ENGINE-CONVENTION FIX: the binary read the heading from EulerAngles index [0], but the WinTesla EulerAngles(Quaternion) decomposition is ambiguous for a yawing mech (the quaternion double-cover flips it to a pitch=roll=pi branch as yaw sweeps past +/-pi -> the needle spins erratically). Switched to YawPitchRoll, whose yaw-first .yaw is the clean continuous heading (pitch=roll~0). Faithful to the binary's intent (heading), correct for this engine's decomposition. renderer->GetLinkedEntity() resolves the viewpoint mech (a GetViewpointEntity fallback is kept belt-and-braces). Deps (NumericDisplay/GraphicsViewRecord/ GraphicGauge) are real engine classes -> no /FORCE risk; verified no unresolved HeadingPointer externals, no parse desync, combat un-regressed (TARGET DESTROYED, 0 crashes). Details + the disasm technique: docs/GAUGE_COMPOSITE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
547f25e043
commit
e869b00181
+26
-3
@@ -257,9 +257,32 @@ Execute→palette-push runs end-to-end; combat un-regressed (TARGET DESTROYED, 0
|
||||
schematic zones are now tinted by the REAL subsystem `currentTemperature` (stable tint since temp is stable —
|
||||
the visible-animation win is `headingPointer` next). **Method established for every remaining widget.**
|
||||
|
||||
**Next increments (priority order from the map):** `headingPointer` (rotating compass + numeric heading —
|
||||
reconstruct from prose `btl4gaug.cpp:644`, reads owner quaternion directly, no attribute table/connection
|
||||
needed — the visible money-shot); then `cmArmor`/`colorMapperMultiArmor` (armor schematic, needs the
|
||||
**✅ Increment 2 — `headingPointer` (HeadingPointer) DONE — the rotating compass + live heading readout.**
|
||||
The visible money-shot: a green needle that rotates with the mech's facing + a numeric heading in whole
|
||||
degrees (render-verified: needle swung down-left→right and the number went 247°→182° as the mech turned).
|
||||
Full class reconstruction from the binary (Make/ctor/dtor/TestInstance/ShowInstance/BecameActive/Execute) —
|
||||
the Ghidra pseudo-C for Execute@004c5914 + ctor@004c562c had the x87 endpoint math dropped, so it was
|
||||
**recovered by disassembling BTL4OPT.EXE** (capstone, `scratchpad/disas_hp.py`). Key facts learned:
|
||||
- **The header ctor arity was wrong (12 vs the binary's 14).** Widened it; fields `@0x98`/`@0x9C` are the
|
||||
needle's inner/outer RADIUS (Execute does `fild [@0x98] ; fmul sin`), not "color/spacing" as named. The
|
||||
needle is a radial line innerRadius(20)..outerRadius(39) at the heading angle; endpoints round half-up
|
||||
(`(int)(r*trig + 0.5f)` == the binary's `fadd 0.5 ; _ftol`). The readout = `round(360 - normalize(deg))`.
|
||||
- **⭐ ENGINE-CONVENTION FIX (empirical, as the map's data-binding agent flagged): the binary read the
|
||||
heading from `EulerAngles` index [0] (`.pitch`), but the WinTesla MUNGA `EulerAngles(Quaternion)`
|
||||
decomposition is AMBIGUOUS for a yawing mech** — the quaternion double-cover flips it to a `pitch=roll=π`
|
||||
branch as the yaw sweeps past ±π, so `euler.yaw` jumps discontinuously (needle spins erratically). Switched
|
||||
to **`YawPitchRoll`** (yaw applied first → `.yaw` is the clean continuous heading, `pitch=roll≈0`
|
||||
throughout). This is a legitimate port of the binary's INTENT, not a stand-in: the original engine's euler
|
||||
put the yaw in [0]; WinTesla's puts it in a different, ambiguous slot, so read the unambiguous decomposition.
|
||||
- `renderer->GetLinkedEntity()` (Renderer::GetLinkedEntity, RENDERER.h:374) resolves the viewpoint mech
|
||||
(the entitySocket IS wired — the map's NULL-socket worry didn't materialize; a `GetViewpointEntity()`
|
||||
fallback is kept belt-and-braces). Deps (`NumericDisplay`, `GraphicsViewRecord`, `GraphicGauge`) are all
|
||||
real engine classes, so no compounding `/FORCE` risk. Verified: no unresolved `HeadingPointer` externals,
|
||||
no parse desync, combat un-regressed (TARGET DESTROYED, 0 crashes). **TECHNIQUE (reusable): recover an x87
|
||||
float computation Ghidra dropped by PE-parsing + capstone-disassembling the fn (`scratchpad/disas_hp.py`) —
|
||||
read the `fild/fmul/fadd` stream + the `.data` float pool.**
|
||||
|
||||
**Next increments (priority order from the map):** `cmArmor`/`colorMapperMultiArmor` (armor schematic, needs the
|
||||
`ArmorZoneConnection`/`MultiArmorConnection` classes reconstructed); then the attribute-table wave (`vertBar`/
|
||||
`segmentArcRatio` speed/`GeneratorCluster` — need `AttributePointers[]` on Mech/HeatableSubsystem, a separate
|
||||
pass); the XL items (`map`/`vehicleSubSystems`/`PlayerStatus`) last. The POD path needs the FULL 23-entry
|
||||
|
||||
+232
-23
@@ -689,35 +689,244 @@ static void
|
||||
//###########################################################################
|
||||
|
||||
//
|
||||
// @004c562c -- ctor (vtable PTR_FUN_00518ae0). GraphicGauge base, then builds
|
||||
// a GraphicsViewRecord (this+0x29, FUN_0044a5b4) for erase tracking, sets the
|
||||
// port origin, stores colours/spacing, interns + ref-counts the pointer image,
|
||||
// and -- centring on the image's mid-point -- constructs an owned NumericDisplay
|
||||
// (this[0x2F], FUN_004700bc, 0x44 bytes) offset by half the glyph metrics.
|
||||
// half-extent maths: w' = (imageWidth/14)*3 ; centre = (-w'/2, -h/2).
|
||||
// Reconstructed from the binary (raw pseudo-C part_013.c:14560-14791 + a
|
||||
// disassembly of Execute@004c5914 and ctor@004c562c to recover the x87 endpoint
|
||||
// arithmetic Ghidra dropped). The compass is a radial NEEDLE (a thick line from
|
||||
// innerRadius to outerRadius at the heading angle) plus a NumericDisplay showing
|
||||
// the heading in whole degrees.
|
||||
//
|
||||
// @004c573c -- dtor: release the image, delete the NumericDisplay
|
||||
// (FUN_0047018c), tear down the GraphicsViewRecord (FUN_0044a5dc),
|
||||
// GraphicGauge::~GraphicGauge.
|
||||
|
||||
MethodDescription
|
||||
HeadingPointer::methodDescription =
|
||||
{
|
||||
"headingPointer",
|
||||
HeadingPointer::Make,
|
||||
{
|
||||
//
|
||||
// CFG shape (L4GAUGE.CFG:4941):
|
||||
// headingPointer( rate, mode, needleColor, numericFg, eraseColor,
|
||||
// innerRadius, outerRadius, font )
|
||||
// e.g. headingPointer(E, ModeAlwaysActive, 2,1,0, 20,39, helv15.pcc);
|
||||
//
|
||||
{ ParameterDescription::typeRate, NULL }, // rate ID
|
||||
{ ParameterDescription::typeModeMask, NULL }, // mode mask
|
||||
{ ParameterDescription::typeColor, NULL }, // needle colour -> @0x90
|
||||
{ ParameterDescription::typeColor, NULL }, // numeric fg -> NumericDisplay
|
||||
{ ParameterDescription::typeColor, NULL }, // erase/bg -> @0x94 + ND bg
|
||||
{ ParameterDescription::typeInteger, NULL }, // inner radius -> @0x98
|
||||
{ ParameterDescription::typeInteger, NULL }, // outer radius -> @0x9C
|
||||
{ ParameterDescription::typeString, NULL }, // font name
|
||||
PARAMETER_DESCRIPTION_END
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// @004c57d0 -- ShowInstance: prints "HeadingPointer" + the fg/bg colours
|
||||
// (this+0x94 / this+0x90) then chains to GraphicGauge::ShowInstance.
|
||||
// @004c554c -- Make. Allocate + construct the gauge, then probe that the glyph
|
||||
// font exists (== NumericDisplayScalar::Make, L4GAUGE.cpp:626-640); the readout
|
||||
// cannot draw without it.
|
||||
//
|
||||
// @004c58e8 -- BecameActive: previousX = previousY = -999 (0xfffffc19) and
|
||||
// reset the NumericDisplay (FUN_004703f4).
|
||||
Logical
|
||||
HeadingPointer::Make(
|
||||
int display_port_index,
|
||||
Vector2DOf<int> position,
|
||||
Entity * /*entity -- unused*/,
|
||||
GaugeRenderer *gauge_renderer
|
||||
)
|
||||
{
|
||||
ParameterDescription *p = methodDescription.parameterList;
|
||||
|
||||
HeadingPointer *gauge = (HeadingPointer *)operator new(0xc0); // FUN_00402298(0xc0)
|
||||
if (gauge != NULL)
|
||||
{
|
||||
new (gauge) HeadingPointer( // FUN_004c562c
|
||||
p[0].data.rate, p[1].data.modeMask,
|
||||
(L4GaugeRenderer *)gauge_renderer,
|
||||
0, // owner_ID
|
||||
display_port_index, // graphics port number (the runtime port)
|
||||
position.x, position.y,
|
||||
p[2].data.color, // needle colour
|
||||
p[3].data.color, // numeric fg colour
|
||||
p[4].data.color, // erase/bg colour
|
||||
p[5].data.integer, // inner radius
|
||||
p[6].data.integer, // outer radius (tip)
|
||||
p[7].data.string, // font name
|
||||
"HeadingPointer");
|
||||
}
|
||||
|
||||
L4Warehouse *warehouse = (L4Warehouse *)gauge_renderer->warehousePointer;
|
||||
if (warehouse->bitMapBin.Get(p[7].data.string) == NULL) // FUN_00442aec
|
||||
{
|
||||
DebugStream << "HeadingPointer: missing font '" << p[7].data.string << "'\n";
|
||||
return False;
|
||||
}
|
||||
warehouse->bitMapBin.Release(p[7].data.string); // FUN_00442c12
|
||||
return True;
|
||||
}
|
||||
|
||||
//
|
||||
// @004c5914 -- Execute. Resolve the owner's heading: take the renderer owner
|
||||
// entity (Resolve renderer+0x40), extract Euler yaw from its orientation
|
||||
// quaternion (entity+0x10c, FUN_0040954c), negate it, SinCos (FUN_00408328),
|
||||
// and Round the rotated pointer endpoint. When the rounded position changes,
|
||||
// erase the previous drawing (GraphicsViewRecord), re-blit the pointer image
|
||||
// rotated to the new heading (BeginClip/SetColor/MoveTo/LineTo/EndClip), then
|
||||
// draw the numeric heading via NumericDisplay::DrawAt (FUN_00470430), guarding
|
||||
// the readout below _DAT_004c5acc (0.0).
|
||||
// @004c562c -- ctor (vtable PTR_FUN_00518ae0). GraphicGauge base; the embedded
|
||||
// GraphicsViewRecord (previousDrawing) default-constructs as a member (== the
|
||||
// binary's FUN_0044a5b4). Set the port origin, store the colours/radii, hold a
|
||||
// ref to the glyph font, and build an owned NumericDisplay centred on the compass
|
||||
// mid-point (half the 3-digit glyph extent).
|
||||
//
|
||||
// (@004c591a is the same routine re-listed from a frame-pointer-relative
|
||||
// disassembly view; it carries no distinct code.)
|
||||
HeadingPointer::HeadingPointer(
|
||||
GaugeRate rate,
|
||||
ModeMask mode_mask,
|
||||
L4GaugeRenderer *renderer_in,
|
||||
unsigned int owner_ID,
|
||||
int graphics_port_number,
|
||||
int x,
|
||||
int y,
|
||||
int needle_color,
|
||||
int numeric_color,
|
||||
int erase_color,
|
||||
int inner_radius,
|
||||
int outer_radius,
|
||||
const char *font_name,
|
||||
const char *identification_string
|
||||
):
|
||||
GraphicGauge(rate, mode_mask, renderer_in, owner_ID, // FUN_00444818
|
||||
graphics_port_number, identification_string)
|
||||
{
|
||||
localView.SetOrigin(x, y); // this+0x48 vtbl+0x10
|
||||
|
||||
needleColor = needle_color; // @0x90
|
||||
eraseColor = erase_color; // @0x94
|
||||
innerRadius = inner_radius; // @0x98
|
||||
outerRadius = outer_radius; // @0x9C
|
||||
|
||||
// Own a copy of the font name (in Make it is the transient interpreter scratch
|
||||
// buffer, so it MUST be copied -- the binary uses nameCopy/FUN_004700ac).
|
||||
imageName = new char[strlen(font_name) + 1]; // @0xA0
|
||||
strcpy(imageName, font_name);
|
||||
|
||||
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
|
||||
BitMap *font = warehouse->bitMapBin.Get(imageName); // FUN_00442aec -- held for the gauge's life
|
||||
int fontW = (font != NULL) ? font->Data.Size.x : 0; // BitMap+0x0C
|
||||
int fontH = (font != NULL) ? font->Data.Size.y : 0; // BitMap+0x10
|
||||
|
||||
int halfW = ((fontW / 14) * 3) / 2; // 14 = totalDigitsPerFont; centre the 3-digit readout
|
||||
int halfH = fontH / 2;
|
||||
|
||||
numericDisplay = new NumericDisplay( // @0xBC FUN_004700bc
|
||||
warehouse,
|
||||
-halfW, -halfH, // centre on the compass mid-point
|
||||
imageName,
|
||||
3, // number_of_digits
|
||||
NumericDisplay::unsignedFormat, // format 0
|
||||
erase_color, // background_color
|
||||
numeric_color); // foreground_color
|
||||
}
|
||||
|
||||
//
|
||||
// @004c573c -- dtor. Release the font ref (keyed on imageName, so before the
|
||||
// free), free imageName, delete the NumericDisplay. The base-dtor chain
|
||||
// (~GraphicsViewRecord on previousDrawing, then ~GraphicGauge) runs implicitly.
|
||||
//
|
||||
HeadingPointer::~HeadingPointer()
|
||||
{
|
||||
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
|
||||
warehouse->bitMapBin.Release(imageName); // FUN_00442c12 -- drop the ctor's Get ref
|
||||
delete[] imageName; // FUN_004022e8
|
||||
imageName = NULL;
|
||||
|
||||
delete numericDisplay; // FUN_0047018c
|
||||
numericDisplay = NULL;
|
||||
}
|
||||
|
||||
Logical
|
||||
HeadingPointer::TestInstance() const
|
||||
{
|
||||
return GraphicGauge::TestInstance();
|
||||
}
|
||||
|
||||
//
|
||||
// @004c57d0 -- ShowInstance (debug dump).
|
||||
//
|
||||
void
|
||||
HeadingPointer::ShowInstance(char *indent)
|
||||
{
|
||||
DebugStream << indent << "HeadingPointer:\n";
|
||||
char deeper[80];
|
||||
strcpy(deeper, indent);
|
||||
strcat(deeper, "...");
|
||||
DebugStream << deeper << "bgColor=" << eraseColor << "\n"; // @0x94 (printed first, per decomp)
|
||||
DebugStream << deeper << "fgColor=" << needleColor << "\n"; // @0x90
|
||||
GraphicGauge::ShowInstance(deeper);
|
||||
}
|
||||
|
||||
//
|
||||
// @004c58e8 -- BecameActive: force a full redraw + reset the readout.
|
||||
//
|
||||
void
|
||||
HeadingPointer::BecameActive()
|
||||
{
|
||||
previousX = -999; // 0xfffffc19 -- guaranteed != any real endpoint
|
||||
previousY = -999;
|
||||
numericDisplay->ForceUpdate(); // FUN_004703f4
|
||||
}
|
||||
|
||||
//
|
||||
// @004c5914 -- Execute. Read the owner mech's heading from its orientation
|
||||
// quaternion, draw the needle as a radial line innerRadius..outerRadius at that
|
||||
// angle (recording the strokes so the next frame can erase them), and update the
|
||||
// numeric readout. Endpoint rounding is half-up: (int)(v + 0.5f), matching the
|
||||
// binary's `fadd 0.5 ; _ftol`.
|
||||
//
|
||||
void
|
||||
HeadingPointer::Execute()
|
||||
{
|
||||
Entity *owner = renderer->GetLinkedEntity(); // FUN_00417ab4(renderer+0x40)
|
||||
if (owner == NULL)
|
||||
{
|
||||
// BRING-UP: if the gauge renderer's entitySocket isn't wired to the viewpoint
|
||||
// mech, fall back to the player so the compass tracks the mech we're driving.
|
||||
// (In practice GetLinkedEntity resolves; this is belt-and-braces.)
|
||||
if (application != NULL)
|
||||
owner = (Entity *)application->GetViewpointEntity();
|
||||
if (owner == NULL)
|
||||
return;
|
||||
}
|
||||
|
||||
// The binary read an EulerAngles component (FUN_0040954c) -- but the WinTesla MUNGA
|
||||
// EulerAngles(Quaternion) decomposition is AMBIGUOUS for a yawing mech: as the yaw
|
||||
// sweeps past +/-pi the quaternion double-cover flips it to a pitch=roll=pi branch,
|
||||
// so euler.yaw jumps discontinuously (the needle spins erratically). YawPitchRoll
|
||||
// applies yaw FIRST, so its .yaw is the clean, continuous heading with pitch=roll~0.
|
||||
YawPitchRoll ypr;
|
||||
ypr = owner->localOrigin.angularPosition; // entity+0x10c (the orientation quaternion)
|
||||
Scalar heading = -(Scalar)ypr.yaw; // negate for the needle (as the binary did)
|
||||
|
||||
SinCosPair sc;
|
||||
sc = Radian(heading); // FUN_00408328 -- sin/cos of the heading
|
||||
|
||||
// Needle endpoints: a radial line innerRadius..outerRadius along the heading.
|
||||
int ax = (int)((Scalar)innerRadius * sc.sine + 0.5f);
|
||||
int ay = (int)((Scalar)innerRadius * sc.cosine + 0.5f);
|
||||
int bx = (int)((Scalar)outerRadius * sc.sine + 0.5f);
|
||||
int by = (int)((Scalar)outerRadius * sc.cosine + 0.5f);
|
||||
|
||||
if (bx != previousX || by != previousY) // redraw only when the tip moves
|
||||
{
|
||||
previousX = bx;
|
||||
previousY = by;
|
||||
previousDrawing.Draw(&localView, eraseColor); // FUN_0044a650 -- erase the last needle
|
||||
previousDrawing.Clear(); // FUN_0044a630
|
||||
localView.AttachRecorder(&previousDrawing); // vtbl+0x64 -- record the new strokes
|
||||
localView.SetColor(needleColor); // vtbl+0x18
|
||||
localView.MoveToAbsolute(ax, ay); // vtbl+0x24
|
||||
localView.DrawThickLineToAbsolute(bx, by); // vtbl+0x38
|
||||
localView.DetachRecorder(); // vtbl+0x68
|
||||
}
|
||||
|
||||
// Numeric heading readout: whole degrees, normalized, displayed as (360 - deg).
|
||||
int deg = (int)((Scalar)ypr.yaw * 57.29578f + 0.5f); // heading -> degrees (0x4c5ac8)
|
||||
if ((Scalar)deg < 0.0f) // _DAT_004c5acc == 0.0f
|
||||
deg = (int)((Scalar)deg + 360.0f);
|
||||
int shown = (int)(360.0f - (Scalar)deg);
|
||||
numericDisplay->Draw(&localView, (Scalar)shown); // FUN_00470430
|
||||
}
|
||||
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -410,12 +410,29 @@
|
||||
public GraphicGauge
|
||||
{
|
||||
public:
|
||||
static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *);
|
||||
// Registered in BTL4MethodDescription[] (btl4grnd.cpp) as "headingPointer".
|
||||
static MethodDescription methodDescription;
|
||||
|
||||
static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *); // @004c554c
|
||||
|
||||
//
|
||||
// The binary ctor (@004c562c) takes 14 args -- the earlier header collapsed
|
||||
// owner_ID/graphics_port_number and merged the colours. Also: fields
|
||||
// @0x98/@0x9C are the needle's inner/outer RADIUS (Execute multiplies them
|
||||
// by sin/cos of the heading), NOT a colour+spacing as the old names implied.
|
||||
//
|
||||
HeadingPointer( // @004c562c
|
||||
GaugeRate, ModeMask, L4GaugeRenderer *, int,
|
||||
int x, int y, const char *font_name,
|
||||
int numeric_color, int background_color, int spacing,
|
||||
const char *image_name, const char *identification_string);
|
||||
GaugeRate, ModeMask, L4GaugeRenderer *,
|
||||
unsigned int owner_ID,
|
||||
int graphics_port_number,
|
||||
int x, int y,
|
||||
int needle_color, // -> @0x90 the drawn needle colour
|
||||
int numeric_color, // -> NumericDisplay foreground (readout)
|
||||
int erase_color, // -> @0x94 erase + NumericDisplay background
|
||||
int inner_radius, // -> @0x98 needle near-end radius
|
||||
int outer_radius, // -> @0x9C needle tip radius (the change key)
|
||||
const char *font_name,
|
||||
const char *identification_string = "HeadingPointer");
|
||||
~HeadingPointer(); // @004c573c
|
||||
|
||||
Logical TestInstance() const;
|
||||
@@ -424,17 +441,17 @@
|
||||
void Execute(); // @004c5914
|
||||
|
||||
protected:
|
||||
int x; // @0x90 this[0x24]
|
||||
int numericColor; // @0x94 this[0x25]
|
||||
int backgroundColor; // @0x98 this[0x26]
|
||||
int spacing; // @0x9C this[0x27]
|
||||
char *imageName; // @0xA0 this[0x28]
|
||||
int needleColor; // @0x90 needle (fg) colour
|
||||
int eraseColor; // @0x94 erase (bg) colour
|
||||
int innerRadius; // @0x98 needle near-end radius
|
||||
int outerRadius; // @0x9C needle tip radius (change key)
|
||||
char *imageName; // @0xA0 interned font name
|
||||
GraphicsViewRecord
|
||||
previousDrawing; // @0xA4 this[0x29]
|
||||
int previousX; // @0xB4 this[0x2D]
|
||||
int previousY; // @0xB8 this[0x2E]
|
||||
previousDrawing; // @0xA4 erase-tracking record
|
||||
int previousX; // @0xB4
|
||||
int previousY; // @0xB8
|
||||
NumericDisplay
|
||||
*numericDisplay; // @0xBC this[0x2F]
|
||||
*numericDisplay; // @0xBC
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ MethodDescription
|
||||
// to the engine base table L4MethodDescription).
|
||||
//
|
||||
&ColorMapperHeat::methodDescription, // "cmHeat" -- heat-driven palette tint
|
||||
&HeadingPointer::methodDescription, // "headingPointer" -- compass needle + heading
|
||||
&BTL4ChainToPrevious
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user