#82 peers see a limping mech SKATING: the mech erased its own gimp cell every frame
The binary's one mech+0x40 IS Simulation::simulationState, which rides EVERY
update record header -- so in 1995 a peer's replicant learned the gimp level on
every packet and its gait limped with no gimp-specific replication anywhere.
The port's Mech::PerformAndWatch wrote SetMovementMode(1) every frame ("ground,
non-death, non-airborne"), which erased the level once it was mirrored in for
the warning voice (#78): the wire carried 1, bystanders walked while sliding at
limp speed, and the voice sequence restarted on every damage event (1->4 edge
per tick) instead of announcing once.
- mech4: that per-frame write now writes the AUTHORITATIVE level
(gimped ? 3/4 : 1) via a new alarm-only bridge BTMechGimpAlarmLevel -- which
deliberately never consults the cell it feeds, so a respawn-cleared alarm
cannot re-latch stale gimp out of it.
- BTMechGimpLevel: falls back to the replicated cell, with ONE-CELL precedence
(a fall/death/limbo state wins, so the normal drivers and their death latch
run -- what the binary's single cell enforced structurally).
- Reverted the #78 record guard: on a replicant the master's records are
authoritative, so pinning 3/4 against them would keep a peer limping through
a respawn. Fixed at the writer instead.
- [simstomp] trap now scoped to the watched mech with a module-relative return
address (symcrash-able) -- that is what named the writer.
- Also learned + recorded: zone damage levels replicate only when the EXPLOSION
TABLE's tier is crossed (peer measured at 0.428 vs master 0.857), so peer-side
damage state must never be inferred from them.
Verified two-node (scratchpad/night6/mp_skate.sh): the observer's replicant gets
sim=4 and its gait runs 23 -> 25 (wgr entry -> ggl limp cycle). Field clue that
cracked it: "after respawning a peer DID see the limp" (epilectrik/SAURON).
KB: locomotion.md + gotcha #25.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
98907f45af
commit
e1c15eb806
@@ -322,6 +322,32 @@ trap never fired while the value changed) resets the sim cell between damage eve
|
||||
1 Hz bench harness it restarted the warning before the 1.8 s voice note; sporadic real-play
|
||||
damage is unaffected. Needs a cdb write-watchpoint dig (candidates: a recon raw +0x2c-family
|
||||
write or a struct copy spanning it).
|
||||
### The gimp cell is REPLICATED STATE — peers limped for free in 1995 (#82, fixed 2026-07-29)
|
||||
Field: "peers see a limping mech **skating**" (first-person limp + voice correct; bystanders saw a
|
||||
normal walk sliding at limp speed; a stop/start did not clear it). Root cause chain, all [T1/T2]:
|
||||
- The binary's one `mech+0x40` **is** `Simulation::simulationState`, which
|
||||
`Simulation::Write/ReadUpdateRecord` puts in **every update record header** — so a peer's
|
||||
replicant learned the gimp level on every packet and its self-simulated gait limped with no
|
||||
extra plumbing. There is no gimp-specific replication anywhere in the binary because none is
|
||||
needed.
|
||||
- A replicant NEVER re-derives the level: zone damage reaches it as a DamageZone update record
|
||||
(`Entity::ReadDamageUpdateRecord` → `DamageZone::ReadUpdateRecord` writes `damageLevel` straight
|
||||
from the wire), which never runs `Mech__DamageZone::TakeDamage` — where the leg-threshold
|
||||
evaluation and the `graphicAlarm` 3/4 write live. ⚠ And zone levels replicate only when the
|
||||
**explosion table's** damage tier is crossed (`EXPTBL.cpp:512-530`
|
||||
`CrossedDamageLevelThreshold`) — coarse and lagging (measured: peer at 0.428 while the master
|
||||
was at 0.857). **Never infer damage state on a peer from replicated zone levels.**
|
||||
- The port's own `Mech::PerformAndWatch` wrote `SetMovementMode(1)` **every frame** ("ground,
|
||||
non-death, non-airborne"), which erased the gimp level from that cell once it was mirrored in
|
||||
for the voice — so the wire carried 1 and peers walked. Found with the scoped `[simstomp]` trap
|
||||
(`g_btGimpWatchMech` + module-relative return address → `symcrash.py`).
|
||||
Fix: that per-frame write now writes the authoritative level (`gimped ? 3/4 : 1`) via the
|
||||
alarm-only bridge `BTMechGimpAlarmLevel` (never the cell it feeds, so a respawn-cleared alarm
|
||||
can't re-latch a stale gimp). Verified two-node (`scratchpad/night6/mp_skate.sh`): the observer's
|
||||
replicant receives `sim=4` and its gait runs `23 → 25` (wgr entry → ggl limp cycle). This also
|
||||
made the voice fire exactly once per onset instead of restarting on every damage event, and
|
||||
re-enabled the authentic leg-destroyed→fall gate (`mechdmg` MovementMode 3||4 [T1 task #60]),
|
||||
which had been dead while the cell was pinned at 1.
|
||||
Open: the load-time per-clip callbacks `PTR_LAB_0050d738/744` on the wg clips (uncarved LABs,
|
||||
inert `Recon` placeholders in the port — likely footstep/cycle events, transitions work without
|
||||
them).
|
||||
|
||||
@@ -664,3 +664,28 @@ bugs the LIVE drivers had each been cured of long before:
|
||||
LIVE sibling for the port-glue idioms (mapper access via `MappingMapper()`, alarm→member
|
||||
re-syncs, null guards, `BTEnvOn` gates). The binary-faithful parts transplant cleanly; it is the
|
||||
RECON-side glue that will be missing, because every glue fix landed only where code was running.
|
||||
|
||||
## 25. A SPLIT cell breaks REPLICATION silently — and per-frame writers erase the half you added (2026-07-29)
|
||||
The gimp-level saga's second act (#82). When the port carries one binary cell as TWO members, the
|
||||
damage isn't only cross-TU reads (gotcha #23) — it is **which half rides the wire**:
|
||||
- `Simulation::simulationState` is replicated in EVERY update record header
|
||||
(`Simulation::Write/ReadUpdateRecord`). Any binary state living in `mech+0x40` therefore
|
||||
replicated for free. The port's parallel member (`graphicAlarm`) replicates **nowhere**, so every
|
||||
behavior a peer derives from it silently becomes master-only — visible as "peers see something
|
||||
different" bugs (here: a limping mech that skated on every other pod).
|
||||
- Worse, the engine cell usually already has a **per-frame writer** with a narrower idea of what it
|
||||
means (`Mech::PerformAndWatch`: `SetMovementMode(1)` = "ground, non-death, non-airborne"). The
|
||||
moment you mirror extra semantics into it, that writer erases them 60×/second — and the symptom
|
||||
is not a stuck value but a 1→N→1 oscillation whose edges retrigger anything watching (the
|
||||
warning-voice sequence restarted on every damage tick and never reached its spoken note).
|
||||
**Rules:**
|
||||
1. Before mirroring into an engine cell, `grep` for its per-frame writers and teach them the new
|
||||
value (write `gimped ? 3/4 : 1`, not a blind `1`).
|
||||
2. Read the authority, never the cell you feed — an alarm-only bridge (`BTMechGimpAlarmLevel`)
|
||||
keeps a respawn-cleared alarm from re-latching stale state out of the cell.
|
||||
3. Don't "fix" a stomp by patching incoming records: on a replicant the master's records ARE the
|
||||
authority, and pinning a local value against them makes state stick forever (that draft would
|
||||
have kept peers limping through a respawn). Find the writer instead.
|
||||
**Tool:** scope the trap. `StateIndicator`/`Simulation` state traps drown in subsystem churn —
|
||||
filter to one watched object (`g_btGimpWatchMech`) and print the caller module-relative
|
||||
(`btl4+0x…`) so `tools/symcrash.py` names it. That turned a two-hour guess into one line.
|
||||
|
||||
+12
-7
@@ -225,14 +225,19 @@ public:
|
||||
{Check(this);
|
||||
// #78 DIAG: trap the gimp-cell downgrade (3/4 -> <=1) with the
|
||||
// caller's return address (symbolize via tools/symcrash.py).
|
||||
if (getenv("BT_AUDIO_SPATIAL")) {
|
||||
// #78/#82 DIAG: who resets the GIMP cell? Scoped to the one mech
|
||||
// mechdmg just gimped (g_btGimpWatchMech) -- subsystems are
|
||||
// Simulations too and their 3->0 churn drowned the earlier trap.
|
||||
// ra is printed MODULE-RELATIVE (btl4+0xNNNN) for symcrash.py.
|
||||
{
|
||||
extern void *g_btGimpWatchMech;
|
||||
extern void BTGimpStompTrap(void *sim, unsigned cur,
|
||||
unsigned nw, void *ra);
|
||||
unsigned _cur = simulationState.GetState();
|
||||
if ((_cur == 3 || _cur == 4) && new_state != _cur) {
|
||||
static int s_ds=0; if (s_ds++<40)
|
||||
DEBUG_STREAM << "[simstomp] " << _cur << "->" << new_state
|
||||
<< " this=" << (void*)this
|
||||
<< " ra=" << _ReturnAddress()
|
||||
<< "\n" << std::flush; }
|
||||
if ((void*)this == g_btGimpWatchMech
|
||||
&& (_cur == 3 || _cur == 4) && new_state != _cur)
|
||||
BTGimpStompTrap((void*)this, _cur, new_state,
|
||||
_ReturnAddress());
|
||||
}
|
||||
simulationState.SetState(new_state);}
|
||||
|
||||
|
||||
+15
-22
@@ -2176,21 +2176,14 @@ Logical
|
||||
// airborne 3,4) tracks the master automatically; the case-0 / tail edge
|
||||
// tests below then see the old->current transition.
|
||||
//
|
||||
// #78 GIMP-MONOTONIC RECORD GUARD: the binary's mech+0x40 is one cell, so its
|
||||
// records always carried the live gimp level (3/4). The port's mirror write
|
||||
// (mechdmg) can be STOMPED by a record whose simulationState was captured
|
||||
// pre-gimp -- and the loopback then perpetuates the stale value forever
|
||||
// (write captures the stomped 1, read re-applies it): the "reverse disabled"
|
||||
// voice sequence restarted on every damage tick and never reached the voice
|
||||
// note. Gimp is monotonic within a life, so a record carrying <=1 while the
|
||||
// live state is 3/4 is stale by construction: patch the RECORD field to the
|
||||
// live value before the base read -- SetState then dedupes (no edge, no
|
||||
// restart) and the next WriteUpdateRecord captures 3/4, converging the loop.
|
||||
#define BT_GIMP_SAFE_BASE_READ(msg) \
|
||||
do { unsigned _cur = GetSimulationState(); \
|
||||
if ((_cur == 3 || _cur == 4) && (msg)->simulationState <= 1) \
|
||||
(msg)->simulationState = _cur; \
|
||||
Simulation::ReadUpdateRecord(msg); } while (0)
|
||||
// (#78 history: a "gimp-monotonic record guard" briefly lived here, patching
|
||||
// incoming records that carried a pre-gimp simulationState. It was treating a
|
||||
// symptom -- the real cause was the mech's own per-frame SetMovementMode(1) in
|
||||
// PerformAndWatch erasing the gimp level from that one cell every frame, found
|
||||
// with the [simstomp] trap and fixed at source (mech4.cpp). The guard is gone
|
||||
// because on a REPLICANT it would have been actively harmful: the master's
|
||||
// records are authoritative, so pinning 3/4 against them would have kept a
|
||||
// peer's mech limping forever through a respawn.)
|
||||
|
||||
void
|
||||
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
|
||||
@@ -2296,7 +2289,7 @@ void
|
||||
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
|
||||
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
|
||||
const Time savedLastUpdate = lastUpdate;
|
||||
BT_GIMP_SAFE_BASE_READ(message);
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
if (!s_t2clock) lastUpdate = savedLastUpdate;
|
||||
} // FUN_0041bd34
|
||||
bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x10
|
||||
@@ -2326,7 +2319,7 @@ void
|
||||
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
|
||||
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
|
||||
const Time savedLastUpdate = lastUpdate;
|
||||
BT_GIMP_SAFE_BASE_READ(message);
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
if (!s_t2clock) lastUpdate = savedLastUpdate;
|
||||
}
|
||||
bodyResetLatch = record->legResetLatch; // @0x658 <- rec+0x10
|
||||
@@ -2351,7 +2344,7 @@ void
|
||||
case 4: // orientation + angular-velocity re-sync
|
||||
{
|
||||
Mech__ResyncUpdateRecord *record = (Mech__ResyncUpdateRecord *)message;
|
||||
BT_GIMP_SAFE_BASE_READ(message);
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
creationTime = Now(); // @0x778 -- the dead-reckon ref time
|
||||
{
|
||||
EulerAngles e(Radian(record->eulerX), Radian(record->eulerY),
|
||||
@@ -2390,7 +2383,7 @@ void
|
||||
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
|
||||
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
|
||||
const Time savedLastUpdate = lastUpdate;
|
||||
BT_GIMP_SAFE_BASE_READ(message);
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
if (!s_t2clock) lastUpdate = savedLastUpdate;
|
||||
}
|
||||
heatAlarm.SetLevel(record->heatLevel); // @0x450 <- rec+0x10
|
||||
@@ -2421,7 +2414,7 @@ void
|
||||
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
|
||||
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
|
||||
const Time savedLastUpdate = lastUpdate;
|
||||
BT_GIMP_SAFE_BASE_READ(message);
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
if (!s_t2clock) lastUpdate = savedLastUpdate;
|
||||
}
|
||||
heatAlarm.SetLevel(record->heatLevel);
|
||||
@@ -2454,7 +2447,7 @@ void
|
||||
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
|
||||
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
|
||||
const Time savedLastUpdate = lastUpdate;
|
||||
BT_GIMP_SAFE_BASE_READ(message);
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
if (!s_t2clock) lastUpdate = savedLastUpdate;
|
||||
}
|
||||
heatAlarm.SetLevel(record->heatLevel);
|
||||
@@ -2482,7 +2475,7 @@ void
|
||||
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
|
||||
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
|
||||
const Time savedLastUpdate = lastUpdate;
|
||||
BT_GIMP_SAFE_BASE_READ(message);
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
if (!s_t2clock) lastUpdate = savedLastUpdate;
|
||||
}
|
||||
airborneSelect = record->airborne; // @0x3f4 <- rec+0x10
|
||||
|
||||
@@ -2439,6 +2439,27 @@ void
|
||||
extern int BTMechGimpLevel(void *mech_v);
|
||||
const int replGl = BTMechGimpLevel(this);
|
||||
const bool replGimp = (replGl == 3 || replGl == 4) && hasGimpClips;
|
||||
// #82 probe: what does this REPLICANT actually know? gl, the
|
||||
// zone-derived level, sim cell, and every leg zone's level.
|
||||
if (getenv("BT_REPL_LOG"))
|
||||
{
|
||||
static float s_ga = 0.0f; s_ga += dt;
|
||||
if (s_ga >= 1.0f)
|
||||
{
|
||||
s_ga = 0.0f;
|
||||
DEBUG_STREAM << "[replgimp] ent=" << (int)GetEntityID()
|
||||
<< " gl=" << replGl
|
||||
<< " sim=" << (int)GetSimulationState()
|
||||
<< " hasClips=" << hasGimpClips << " zones:";
|
||||
for (int zi = 0; zi < damageZoneCount; ++zi)
|
||||
{
|
||||
Mech__DamageZone *z = Zone(zi);
|
||||
if (z != 0 && z->damageLevel > 0.0f)
|
||||
DEBUG_STREAM << " " << zi << "=" << z->damageLevel;
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
const Scalar replLegAdv = s_peerLegCh
|
||||
? (replGimp ? AdvanceLegAnimationGimp(dt) // FUN_004a71f4
|
||||
: AdvanceLegAnimation(dt)) // old: re-derived leg SM
|
||||
@@ -3805,12 +3826,24 @@ void
|
||||
// the authentic MaxAcceleration reads directly since the task #4
|
||||
// record-layout fix (madcat: 30 u/s^2); the old floor-25 block
|
||||
// and its one-shot log are retired.
|
||||
// (#78 note: the gimp states live on graphicAlarm, NOT this
|
||||
// engine simulationState cell -- the port carries the
|
||||
// binary's one mech+0x40 as two members; see the split-brain
|
||||
// note in combat-damage.md. This write never touches them.)
|
||||
// #78/#82: this cell IS the binary's one mech+0x40, so it must
|
||||
// carry the GIMP LEVEL too -- writing a blind 1 here erased it
|
||||
// every frame (the old note claiming "this write never touches
|
||||
// them" stopped being true the moment the gimp level was
|
||||
// mirrored in for the warning voice). Two consequences of the
|
||||
// erasure, both field-reported: the "reverse disabled" sequence
|
||||
// restarted on every damage event (1->4 edge per tick), and --
|
||||
// because simulationState rides EVERY update record header --
|
||||
// peers received 1 and their replicant walked instead of
|
||||
// limping (#82, "peers see a limping mech skating"). Write the
|
||||
// authoritative level: gimped -> 3/4, else 1. Alarm-only read
|
||||
// (never the cell we feed), via the mechdmg bridge (gotcha #23).
|
||||
if (!IsMechDestroyed()) // a dead mech keeps its death movementMode
|
||||
SetMovementMode(1); // ground, non-death, non-airborne
|
||||
{
|
||||
extern int BTMechGimpAlarmLevel(void *mech_v);
|
||||
const int gl = BTMechGimpAlarmLevel(this);
|
||||
SetMovementMode((gl == 3 || gl == 4) ? gl : 1); // ground, non-death, non-airborne
|
||||
}
|
||||
// reverseSpeedMax2@0x7a0 is the run-cycle bodyCycleSpeed CLAMP (AdvanceBody
|
||||
// Animation case 12/13); LoadLocomotionClips does not set it -> it reads
|
||||
// 0xCDCDCDCD (-4.3e8) and the clamp clobbers bodyCycleSpeed -> the run cycle
|
||||
|
||||
@@ -71,6 +71,25 @@
|
||||
#endif
|
||||
|
||||
#include <stdio.h> // sscanf (offline streamer)
|
||||
|
||||
// #78/#82 diagnostic scope: the mech whose GIMP cell the [simstomp] trap in
|
||||
// SIMULATE.h should watch (set at each leg-threshold crossing). Definition
|
||||
// lives here so the engine header can extern it without a game dependency.
|
||||
void *g_btGimpWatchMech = 0;
|
||||
|
||||
// The trap body (SIMULATE.h calls it): report WHO reset the gimp cell, with a
|
||||
// module-relative return address for tools/symcrash.py.
|
||||
void BTGimpStompTrap(void *sim, unsigned cur, unsigned nw, void *ra)
|
||||
{
|
||||
static int s_ds = 0;
|
||||
if (s_ds++ >= 40)
|
||||
return;
|
||||
unsigned long base = (unsigned long)GetModuleHandleA(0);
|
||||
char buf[160];
|
||||
sprintf(buf, "[simstomp] %u->%u mech=%p ra=btl4+0x%lx",
|
||||
cur, nw, sim, (unsigned long)ra - base);
|
||||
DEBUG_STREAM << buf << std::endl << std::flush;
|
||||
}
|
||||
#if !defined(RANDOM_HPP)
|
||||
# include <random.hpp> // the engine's global RandomGenerator Random
|
||||
#endif
|
||||
@@ -502,6 +521,7 @@ void
|
||||
//
|
||||
if (damageLevel >= LegHalfStructure && (rightLeg != 0 || leftLeg != 0))
|
||||
{
|
||||
g_btGimpWatchMech = (void *)mech; // scope the #78 simstomp trap
|
||||
unsigned ss = mech->GetSimulationState();
|
||||
if (getenv("BT_AUDIO_SPATIAL")) { static int s_gm=0; if (s_gm++<30)
|
||||
DEBUG_STREAM << "[gimp-sim] ss=" << ss << " -> "
|
||||
@@ -624,13 +644,56 @@ void
|
||||
// that typedef split is audited and unified, every cross-TU read of the
|
||||
// alarm goes through here.
|
||||
//
|
||||
int BTMechGimpLevel(void *mech_v)
|
||||
// #82 REPLICANTS (field: "peers see a limping mech SKATING", 2026-07-29): the
|
||||
// binary's mech+0x40 is ONE cell -- and because that cell IS
|
||||
// Simulation::simulationState, it rides EVERY update record header
|
||||
// (Simulation::Write/ReadUpdateRecord), so a peer's replicant learned the gimp
|
||||
// level for free and its gait machine limped with no extra plumbing.
|
||||
//
|
||||
// The port split the cell (graphicAlarm = where mechdmg writes; engine
|
||||
// simulationState = what replicates). A replicant NEVER re-derives the level:
|
||||
// zone damage reaches it as a DamageZone update record
|
||||
// (Entity::ReadDamageUpdateRecord -> DamageZone::ReadUpdateRecord writes
|
||||
// damageLevel straight from the wire), which never runs
|
||||
// Mech__DamageZone::TakeDamage -- so the threshold evaluation, and with it
|
||||
// graphicAlarm 3/4, only ever happens on the damage-resolving node. Its
|
||||
// self-simulated gait therefore stayed in the WALK states while the replicated
|
||||
// speed dragged it along at limp pace: the skate.
|
||||
//
|
||||
// So read the cell the way the binary had it: the master's authoritative
|
||||
// graphicAlarm, else the replicated engine cell (which the master now mirrors
|
||||
// for the warning voice, #78 -- the same value the binary put on the wire).
|
||||
// Mech::StateCount is 0x21 and the engine base only defines 0/1 there, so 3/4
|
||||
// are unambiguously the mech's gimp levels (the authored audio watchers on
|
||||
// SimulationState 3/4 are the binary's own confirmation).
|
||||
int BTMechGimpAlarmLevel(void *mech_v)
|
||||
{
|
||||
if (mech_v == 0)
|
||||
return 0;
|
||||
return (int)((Mech *)mech_v)->graphicAlarm.GetLevel();
|
||||
}
|
||||
|
||||
int BTMechGimpLevel(void *mech_v)
|
||||
{
|
||||
if (mech_v == 0)
|
||||
return 0;
|
||||
Mech *m = (Mech *)mech_v;
|
||||
const int alarm = (int)m->graphicAlarm.GetLevel();
|
||||
const int sim = (int)m->GetSimulationState();
|
||||
|
||||
// ONE-CELL PRECEDENCE: in the binary a fall / death / limbo state OVERWROTE
|
||||
// the gimp level in the same cell, so those gaits win and the NORMAL
|
||||
// drivers (which carry the death latch) must run. Report the alarm
|
||||
// unchanged there -- never gimp.
|
||||
if (sim == 2 || (sim >= 5 && sim <= 9))
|
||||
return alarm;
|
||||
if (alarm == 3 || alarm == 4)
|
||||
return alarm; // master authority
|
||||
if (sim == 3 || sim == 4)
|
||||
return sim; // the replicated cell
|
||||
return alarm; // 0/9/... unchanged
|
||||
}
|
||||
|
||||
void BTMechVitalSubsystemKill(void *owner_mech)
|
||||
{
|
||||
if (owner_mech != 0)
|
||||
|
||||
Reference in New Issue
Block a user