myomer damage reaches the wheels, legs gimp at half structure, reverse refuses -- and an ODR trap unmasked (#75/#78)
The speed-demand site (MechControlsMapper::InterpretControls) now applies the
drive scale the mover's feed roster applied in 1995: speedDemand multiplies by
the myomers' live speedEffect (gear ratio, thermal curve, 1 - zone damage, via
the BTMyomersDriveOf bridge) and, while GIMPED, by 0.5 [T3: VGL Lynx's
"roughly 50%", BT_GIMP_SPEED overrides]. The gimp states were already being
raised -- mechdmg sets graphicAlarm 3 (left) / 4 (right) when a LegDamageZone-
flagged zone crosses half structure -- but nothing downstream ever saw them.
While gimped, reverse input is refused ("reverse disabled"), matching the
old-timers' account; the pod's audio cue rides the alarm's watchers.
Bench, one trajectory, arithmetically exact: a deterministic right-leg ramp
(the new BT_SELF_DAMAGE_ZONE harness) crosses 0.5 and the demand goes
44.837 -> 6.726 = 44.837 x 0.5 (gimp) x 0.3 (a crit-chewed myomers from the
same ramp -- the #80 crits composing with #75's scale, unprompted).
The reason "nothing downstream ever saw them" is the real find of the night,
now gotcha #23: AlarmIndicator is typedef'd to DIFFERENT TYPES per header
family -- mech.hpp says ReconAlarm (4 bytes), heat.hpp says GaugeAlarm (0x54)
-- so Mech::graphicAlarm and EVERY member after it sit at different offsets
depending on a TU's include order. mechdmg wrote level 4 and read it back;
mechmppr read 0 from the same object, same expression. No compiler error can
catch it: each TU only ever sees one definition. Until the split is audited,
cross-TU reads of the gimp level go through BTMechGimpLevel (compiled in
mechdmg's TU) -- and the same split-brain explains why the port carries the
binary's ONE movementMode cell as two live members (engine simulationState vs
graphicAlarm level) that never meet.
Also landed en route: the Myomers un-powered self-repair observed healing in
the field logs at exactly 0.011 x the authored Explosive scale per tick --
the 2026-07-29 reversal confirmed live; and Mech message 0x15 "RealMaxSpeed"
raw-decoded (@0x49f604: sets mech+0x7a0 from the message unless the +0x7a4
latch holds -- a console-tunable top speed).
Open on #78, documented in locomotion.md: the Gimp animation clips (authored
keys in the binary's model-record parser; mech2's state enum vs mech3's
reverse-fix disagree about slots 0x12-0x17 -- reconcile before wiring) and
the audio-cue binding.
Diags: BT_SELF_DAMAGE_ZONE, BT_DRIVE_LOG, BT_GIMP_SPEED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8a99972f22
commit
520f6eecd3
@@ -473,6 +473,9 @@ default-ON (`'0'` disables).
|
||||
| `BT_SELF_DAMAGE=<dps>` | dispatch an unaimed `TakeDamage` at your OWN mech once a second, through the real `Entity::Dispatch` path, so the whole RESPAWN family is bench-testable solo (nothing else can kill the local pilot: `BT_MP_FORCE_DMG` only targets replicants). **Latches off at first death** so everything after the respawn is the respawn's doing, not the harness still shooting you |
|
||||
| `BT_POWER_DETACH_TEST=<name\|1>` | drop a subsystem's voltage link + force Auto, so the auto-hunt must recover it. `1` = first powered subsystem to tick; a NAME (`PPC_1`, `Myomers`) targets one, which is what proves FAILOVER to a different generator rather than a same-generator re-attach |
|
||||
| `BT_AUDIO_SOURCES=<n>` | request `n` OpenAL mono sources instead of the driver default (~256). **Opt-in on purpose** — the cap doubles as a governor, and with EFX reverb live a higher ceiling means more simultaneous voices mixing during heavy combat. Measure frame time. See [[wintesla-port]] |
|
||||
| `BT_SELF_DAMAGE_ZONE=<n>` | aim the BT_SELF_DAMAGE harness at an explicit zone (-1/unset = the lottery) -- ramps one zone deterministically past thresholds (#78 bench) |
|
||||
| `BT_DRIVE_LOG` | the demand-site drive scale: `[drive] n/drive/mm/dmd/mech` ~1Hz (drive = myomers speedEffect x gimp; mm = the gimp level via BTMechGimpLevel) |
|
||||
| `BT_GIMP_SPEED=<0..1>` | override the gimped speed factor (default 0.5 [T3 VGL Lynx]) |
|
||||
| `BT_PICK_LOG` | #73 aimed-pick diagnostics: `[segpick]` the segment→zone map at tree build (index, name, zone, sphere), `[pickwin]` the winning zone/score/t per pick (score ~0 = threading the part core, ~1 = envelope graze) |
|
||||
| `BT_CRIT_LOG` | #80 crit diagnostics: `[subarmor]` per-subsystem armour/scales/critBonus at ctor (proves the resource keys parsed + the zone got REAL scales), `[critroll]` per landed crit (zone, subsystem, its resulting own-zone level). NB the type-0x1e loader's `[crit]` tag is a different, older log |
|
||||
| `BT_DEVICELOST_TEST=<frame>[,crashrepro]` | #35 bench hook. `<frame>` forces the D3D9 DEVICELOST branch at that render frame (+600/+1200 = 3 cycles), driving the REAL `BTResetLostDevice` recovery. `,crashrepro` runs the field null-teardown shape (double `ParticleEngine::Destroy`) — pre-fix this reproduced the field crash byte-for-byte (`Destroy +0x11`, `target=0x0`); post-fix it must log `SURVIVED`. See [[wintesla-port]] §Device-loss |
|
||||
|
||||
@@ -253,3 +253,22 @@ Check `[shadowobj]` tag lines and `[sync]`/`BT_SYNC_LOG` before touching bias/ti
|
||||
## Key Relationships
|
||||
- Detail: `docs/P3_LOCOMOTION.md`. Uses: [[asset-formats]] (SKL/ANI), [[decomp-reference]] (offsets).
|
||||
- Feeds: [[combat-damage]] (collision→damage), [[rendering]] (shadow/visual-conform).
|
||||
|
||||
## Myomer drive + the GIMP (limp) chain — IMPLEMENTED 2026-07-30 (#75/#78) [T2 bench]
|
||||
The speed-demand site (`mechmppr.cpp` InterpretControls) now applies the **drive scale**:
|
||||
`speedDemand *= myomers.speedEffect × gimpFactor`. The myomers factor is the wrapper's live 0..1
|
||||
output (gear/thermal/1−damage, via `BTMyomersDriveOf`); the authentic coupling attached
|
||||
`&speedEffect` into the mover's feed roster, which the 2007 engine lacks — the multiplication at
|
||||
the demand site is the port equivalent. The GIMP chain: `mechdmg` raises **graphicAlarm 3 (left)
|
||||
/ 4 (right)** when a leg zone (the `LegDamageZone`-flagged zones; MadCat: 3/5/8 left, 10/16/19
|
||||
right) crosses `LegHalfStructure` (0.5); while gimped the demand additionally multiplies by
|
||||
**0.5 [T3 — VGL Lynx's "roughly 50%", `BT_GIMP_SPEED` overrides]** and **reverse input is
|
||||
refused** ("reverse disabled", `[gimp]` log; the pod's audio cue rides the alarm's watchers if
|
||||
audio rows exist). Measured composed and exact: `dmd 44.837 → 6.726 = 44.837 × 0.5 × 0.3` (a
|
||||
crit-damaged myomers at 0.3 during the same ramp). ⚠ Cross-TU reads of the gimp level MUST use
|
||||
`BTMechGimpLevel` (mechdmg.cpp) — see [[reconstruction-gotchas]] §23 (the AlarmIndicator typedef
|
||||
split). Open on #78: the **Gimp animation clips** (`Left/RightGimpAnimation` + transitions —
|
||||
authored KEYS in the binary's model-record parser; mech2's enum names states 0x12-0x17 for them
|
||||
but mech3's reverse-fix reassigned those slots to the reverse figures — reconcile before wiring
|
||||
clips) and the audio cue binding. Harness: `BT_SELF_DAMAGE_ZONE=<n>` ramps one zone
|
||||
deterministically; `BT_DRIVE_LOG` prints `[drive] n/drive/mm/dmd/mech`.
|
||||
|
||||
@@ -623,3 +623,27 @@ trusting what it is called — the reconstruction's names are reconstructions to
|
||||
|
||||
**Sweep note:** the same misnomer sits in all three headers; renaming is safe (no binary meaning
|
||||
attaches to the port's accessor name) but touches three size-locked classes, so do it deliberately.
|
||||
|
||||
## 23. `AlarmIndicator` is a DIFFERENT TYPE per header family — Mech's layout diverges across TUs (2026-07-30)
|
||||
|
||||
Found chasing #78: `mechdmg.cpp` wrote `mech->graphicAlarm.SetLevel(4)` and read 4 back;
|
||||
`mechmppr.cpp` read **0** from the *same object, same expression, same frame family*. Root cause:
|
||||
|
||||
```cpp
|
||||
mech.hpp:67 typedef ReconAlarm AlarmIndicator; // 4 bytes {unsigned level}
|
||||
heat.hpp:52 typedef GaugeAlarm AlarmIndicator; // 0x54 bytes (the real binary alarm)
|
||||
```
|
||||
|
||||
`Mech::graphicAlarm` is declared `AlarmIndicator` — so a TU's include ORDER decides which type
|
||||
that member is, and **every Mech member after it shifts by 0x50 between the two families**. A
|
||||
write through one family's layout is invisible to a read through the other. This is the shadow
|
||||
trap operating at the TYPEDEF level, where no compiler error can catch it (each TU only ever sees
|
||||
one definition).
|
||||
|
||||
**Rule:** any cross-TU read/write of a Mech member declared past `graphicAlarm` must go through a
|
||||
bridge compiled in a KNOWN TU (`BTMechGimpLevel` in mechdmg.cpp is the pattern) until the typedef
|
||||
split is audited and unified. The audit itself — which TUs resolve `AlarmIndicator` to which
|
||||
type, and which member traffic crosses families — is an open work item; the same split-brain also
|
||||
explains why `MovementMode()` (the engine `simulationState`) and the KB's "movementMode IS the
|
||||
graphicAlarm level" (task #1) never actually met in the port: they are two different cells, both
|
||||
alive, written by different subsystems.
|
||||
|
||||
Reference in New Issue
Block a user