#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.
|
||||
|
||||
Reference in New Issue
Block a user