From 36f68718c22756f31e701a8b2e8e8bff9d2c6584 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 29 Jul 2026 15:49:39 -0500 Subject: [PATCH] the visible limp (#78): there was never a jump-jet clip set -- the 'Airborne' drivers ARE the gimp gait machines The port had FUN_004a5bf8/71f4 fully reconstructed as AdvanceBody/Leg- AnimationAirborne, gated on (MovementMode()==3||4) && jumpCapable@0x580 and believed dead ("the test mech never jumps"). The binary says otherwise: mech+0x40 in that gate is the graphicAlarm LEVEL (3=left-leg gimp, 4=right) and +0x580 is hasGimpClips, set by the conditional loader block that probes 'wgl' and fills clip slots 22-27 (wgl/wgr/ggr/ggl/gsl/gsr) plus the four measurements at 0x53c-0x548 (wg entry strides = speed caps, gg cycle strides). Renamed the five jump* members + both drivers accordingly. New: GimpBodyClipFinished @004a6344 / GimpLegClipFinished @004a7970 -- the gimp transition machines, branched from the normal finished-callbacks. Phase- correct limp entry (left-gimp enters 0x16/wgl only from a RIGHT step, right- gimp 0x17/wgr from a LEFT step), gg cycles at gimp cadence, gs exits, and the demand clamp to the gimped side's speed cap (leg cb writes it back into the mapper -- the binary's authentic slowdown; the T3 x0.5 stand-in in mechmppr is retired, BT_GIMP_SPEED now defaults 1.0). The binary's gimp machines have no reverse entry -- the "reverse disabled" behavior is now binary-proven. Reviving the dead drivers replayed two port-glue bugs (gotcha #24): the raw *(controlSource) mapper read (null -> crash at first engagement) and the missing alarm->member state re-sync (machine pinned in one run state). Both fixed; bench harness gained BT_SELF_DAMAGE_TICKS= to hold a zone past LegHalfStructure without destroying it. Bench-verified (madcat, novice, zone 16): crossing -> alarm 4 -> wgr entry from a left step -> 8k+ frames stable in the ggl limp cycle at cadence 14.77 (vs 18.5 walk / 22+ run) with raw demand still 50. [T2] Co-Authored-By: Claude Fable 5 --- context/decomp-reference.md | 2 +- context/locomotion.md | 56 +++-- context/reconstruction-gotchas.md | 17 ++ docs/P3_LOCOMOTION.md | 2 +- game/reconstructed/mech.hpp | 33 ++- game/reconstructed/mech2.cpp | 381 ++++++++++++++++++++++++++---- game/reconstructed/mech3.cpp | 60 ++--- game/reconstructed/mech4.cpp | 68 ++++-- game/reconstructed/mechmppr.cpp | 14 +- 9 files changed, 506 insertions(+), 127 deletions(-) diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 5827e11..3ef2be5 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -475,7 +475,7 @@ default-ON (`'0'` disables). | `BT_AUDIO_SOURCES=` | 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=` | 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_GIMP_SPEED=<0..1>` | EXTRA gimp demand multiplier (default 1.0 = authentic since 2026-07-30: the real slowdown is the gimp gait machines' clamp to the wg clip speed — see [[locomotion]] §visible limp; the old 0.5 T3 stand-in is retired) | | `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=[,crashrepro]` | #35 bench hook. `` 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 | diff --git a/context/locomotion.md b/context/locomotion.md index e8069d7..0f6d4a2 100644 --- a/context/locomotion.md +++ b/context/locomotion.md @@ -254,21 +254,49 @@ Check `[shadowobj]` tag lines and `[sync]`/`BT_SYNC_LOG` before touching bias/ti - 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 +## Myomer drive + the GIMP (limp) chain — COMPLETE 2026-07-30 (#75/#78) +The speed-demand site (`mechmppr.cpp` InterpretControls) applies the **drive scale**: +`speedDemand *= myomers.speedEffect`. 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=` ramps one zone -deterministically; `BT_DRIVE_LOG` prints `[drive] n/drive/mm/dmd/mech`. +right) crosses `LegHalfStructure` (0.5), and **reverse input is refused** ("reverse disabled", +`[gimp]` log — and the binary agrees: the gimp gait machines have NO standing→reverse entry). +⚠ Cross-TU reads of the gimp level MUST use `BTMechGimpLevel` (mechdmg.cpp) — see +[[reconstruction-gotchas]] §23 (the AlarmIndicator typedef split). Harness: +`BT_SELF_DAMAGE_ZONE=` ramps one zone deterministically; `BT_DRIVE_LOG` prints +`[drive] n/drive/mm/dmd/mech`. + +### The VISIBLE limp — the gimp gait machines (reconstructed + bench-verified 2026-07-30) [T2] +Bench (madcat, novice, `BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=16 BT_SELF_DAMAGE_TICKS=2`, +`BT_GOTO` walk): right-leg crossing → alarm 4 → body enters 0x17/wgr FROM A LEFT STEP +(phase-correct) → both channels settle in 0x19/ggl and hold it 8k+ frames at the gimp cadence +cap (cycle 14.77 = the wgr entry stride, vs 18.5 walk / 22+ run) while raw demand stays 50 — +the slowdown IS the gait machine, no demand multiply. Two revive-bugs fixed en route: see +[[reconstruction-gotchas]] §24. +The binary's limp was hiding behind a **"jump-jet" misread**: the port had `FUN_004a5bf8` / +`FUN_004a71f4` reconstructed as `AdvanceBody/LegAnimationAirborne`, gated on +`(MovementMode()==3||4) && jumpCapable@0x580` and thought DEAD (mechs never jump). Truth +[T1, raw part_012.c]: **mech+0x40 there is the graphicAlarm level (3=left gimp, 4=right)** +and **+0x580 is `hasGimpClips`** — set by the conditional loader block that probes `wgl` and +loads clip slots 22-27 (`wgl wgr ggr ggl gsl gsr`, +0x624..0x638) plus four measurements: +`gimpLeft/RightSpeedMax`@0x53c/0x540 (the wg entry clips' final keyframe strides) and +`gimpLeft/RightStrideLength`@0x544/0x548 (MeasureClipStride over the gg cycles). The five +`jump*`-named members were renamed accordingly (mech.hpp; there is no jump clip set). +Machinery (all four reconstructed in mech2.cpp, gates in mech4.cpp): +- **Drivers** `AdvanceBodyAnimationGimp`@004a5bf8 / `AdvanceLegAnimationGimp`@004a71f4 — + selected per-frame when `(gimpLevel 3|4) && hasGimpClips`; clamp the demand (body: + `bodyTargetSpeed`; leg: the LIVE mapper `speedDemand`, written back) to the gimped side's + speed cap and advance the gg cycles at gimp cadence. **This clamp IS the authentic gimp + slowdown** — ground speed is clip travel, so the limp cadence bounds it; the earlier T3 + `×0.5` stand-in in mechmppr is retired (`BT_GIMP_SPEED` remains as an override, default 1.0). +- **Finished-callbacks** `GimpBodyClipFinished`@004a6344 / `GimpLegClipFinished`@004a7970 — + branched to from the tops of the normal `Body/LegClipFinished`. **Phase-correct entry**: + left-gimp enters 0x16/wgl only from a RIGHT step (walk-R cases 5/6/0xe), right-gimp enters + 0x17/wgr from a LEFT step (7/0xf) — the machine forces one extra normal step if mid-wrong-foot. + Cycles: left = 0x18 (ggr clip, stride @0x544), right = 0x19 (ggl, @0x548); exits through the gs + transitions 0x1a/0x1b when demand dies. No reverse entry exists while gimped. +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); the gimp audio cue binding. diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index a8a8ace..0312688 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -647,3 +647,20 @@ type, and which member traffic crosses families — is an open work item; the sa 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. + +## 24. Reviving DEAD reconstructed code replays every port-glue fix it predates (2026-07-30) +The gimp gait drivers (`AdvanceBody/LegAnimationGimp`, ex-"Airborne") sat reconstructed-but-dead +for weeks while their gate read the wrong cell (gotcha #23's split-brain). The moment the gate +was fixed and they ran for the first time, they reproduced — within seconds of engagement — TWO +bugs the LIVE drivers had each been cured of long before: +1. **the raw `*(this->controlSource)` mapper read** (`controlSource@0x128` is never wired in the + port; the live drivers were converted to `MappingMapper()` — the dead one crashed at + `AdvanceLegAnimationGimp+0x16`, null deref, first live engagement); +2. **the missing alarm→member state re-sync** (`bodyAnimationState = bodyStateAlarm.GetLevel()`; + the binary has ONE cell, the recon has two — the ground drivers re-sync at :831/:1181; the + dead driver didn't, so the state member froze at its pre-gimp value and the machine pinned in + one run state for 8000+ frames). +**Rule:** before wiring a long-dead reconstructed function into a live path, diff it against its +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. diff --git a/docs/P3_LOCOMOTION.md b/docs/P3_LOCOMOTION.md index 0848472..c2b461c 100644 --- a/docs/P3_LOCOMOTION.md +++ b/docs/P3_LOCOMOTION.md @@ -45,7 +45,7 @@ be "called on": + `IntegrateMotion`) in `PerformAndWatch`, retiring the Step-1/2 stand-in translation + free-standing `gBodyAnim`. Gives the authentic locally-simulated + displayed-motion gait. -**Deferred polish (post-core):** airborne/fall gaits (AdvanceBody/LegAnimationAirborne), torso-twist-to-target +**Deferred polish (post-core):** ~~airborne/fall gaits (AdvanceBody/LegAnimationAirborne)~~ — RESOLVED 2026-07-30: those were the GIMP (limp) drivers, now `AdvanceBody/LegAnimationGimp`, live for #78 (see context/locomotion.md §visible limp); torso-twist-to-target (Torso is FULLY reconstructed — TorsoSimulation twist/elevation @004b5cf0 + WriteJoints; needs wiring), gyro sway. **First-milestone recommendation:** Steps 1-2 give a visibly correct animation-driven walk/run (real speeds, diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 2ba25b6..a2cdcd9 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -1017,10 +1017,16 @@ protected: Scalar standSpeed; // @0x530 Scalar walkStrideLength; // @0x534 Scalar reverseSpeedMax; // @0x538 - Scalar jumpRunSpeedMax; // @0x53c - Scalar jumpWalkSpeedMax; // @0x540 - Scalar jumpRunStrideLength; // @0x544 - Scalar jumpWalkStrideLength; // @0x548 + // GIMP (limp-gait) measurements, filled by the conditional 'wgl' loader + // block (FUN_004a80d4 raw :13705-13733). "Left/Right" = WHICH LEG IS + // GIMPED (graphicAlarm level 3 = left, 4 = right). Speeds are the wg + // entry clips' final keyframe strides; strides are MeasureClipStride + // over the gg cycle clips. (These four + hasGimpClips@0x580 were + // mislabeled jump* -- there is no jump-jet clip set.) + Scalar gimpLeftSpeedMax; // @0x53c wgl final stride (mode-3 speed cap) + Scalar gimpRightSpeedMax; // @0x540 wgr final stride (mode-4 speed cap) + Scalar gimpLeftStrideLength; // @0x544 ggr cycle stride (mode 3 cycles state 0x18) + Scalar gimpRightStrideLength; // @0x548 ggl cycle stride (mode 4 cycles state 0x19) Scalar walkingTurnRate; // @0x574 (WalkingTurnRate, rad/s) -- the master Scalar runningTurnRate; // @0x578 (RunningTurnRate, rad/s) perf turn-rate lerp Scalar groundCycleRate; // @0x5b8 @@ -1035,9 +1041,9 @@ protected: Scalar globalTimeScale; // @0x5a8 Scalar idleStrideScale; // @0x5ac Scalar gimpCycleRate; // @0x5b0 - int jumpCapable; // @0x580 - int hasReverseGimpSet; // @0x584 - int hasCrashSet; // @0x588 + int hasGimpClips; // @0x580 set iff the model ships the wg/gg/gs clip set + int squatCapable; // @0x584 set iff squ/sqd ship + int turnCapable; // @0x588 set iff trn ships int deathAnimationLatched; // @0x650 int legResetLatch; // @0x654 int bodyResetLatch; // @0x658 @@ -1084,8 +1090,17 @@ protected: Scalar LegTransition(int next_state, Scalar adv_time, int move_joints); Scalar AdvanceLegAnimation(Scalar time_slice); // @004a5028 Scalar AdvanceBodyAnimation(Scalar time_slice, int loop); - Scalar AdvanceBodyAnimationAirborne(Scalar time_slice, int loop); - Scalar AdvanceLegAnimationAirborne(Scalar time_slice); + Scalar AdvanceBodyAnimationGimp(Scalar time_slice, int loop); // @004a5bf8 (was "Airborne" -- it's the LIMP driver) + Scalar AdvanceLegAnimationGimp(Scalar time_slice); // @004a71f4 + // GIMP finished-callbacks: BodyClipFinished/LegClipFinished tail-branch + // into these when (gimpLevel==3||4) && hasGimpClips. Same transition + // machine as the normal cbs plus: a top clamp of the demand to the + // gimp speed cap, phase-correct wg entries from the walk cases + // (left-gimp enters 0x16/wgl from a RIGHT step, right-gimp 0x17/wgr + // from a LEFT step -- forcing one extra normal step if mid-wrong-foot), + // the gg cycles 0x18/0x19 at gimp cadence, and gs exits 0x1a/0x1b. + Scalar GimpBodyClipFinished(Scalar carryover, int move_joints); // FUN_004a6344 + Scalar GimpLegClipFinished(Scalar carryover); // FUN_004a7970 Logical IsDisabled(); // FUN_0049fb54 // --- death sequence (the un-exported master-perf death branch, rebuilt // from its exported consumers + the RP VTV::DeathShutdown analog) --- diff --git a/game/reconstructed/mech2.cpp b/game/reconstructed/mech2.cpp index 898f368..5f1f890 100644 --- a/game/reconstructed/mech2.cpp +++ b/game/reconstructed/mech2.cpp @@ -19,8 +19,8 @@ // // @004a5028 Mech::AdvanceLegAnimation (1543 bytes) // @004a5678 Mech::AdvanceBodyAnimation (1333 bytes) -// @004a5bf8 Mech::AdvanceBodyAnimationAirborne(1792 bytes) -// @004a71f4 Mech::AdvanceLegAnimationAirborne (1840 bytes) +// @004a5bf8 Mech::AdvanceBodyAnimationGimp(1792 bytes) +// @004a71f4 Mech::AdvanceLegAnimationGimp (1840 bytes) // // The embedded assert path on every one of them is // "d:\tesla\bt\bt\MECH2.CPP" @@ -180,10 +180,10 @@ enum MechAnimationState // @0x530 standSpeed "at rest" / minimum move speed threshold // @0x534 walkStrideLength forward walk/run clip length (also speed cap) // @0x538 reverseSpeedMax speed cap while decelerating into Reverse (?) -// @0x53c jumpRunSpeedMax airborne speed cap, movementMode==3 -// @0x540 jumpWalkSpeedMax airborne speed cap, otherwise -// @0x544 jumpRunStrideLength airborne clip length, movementMode==3 -// @0x548 jumpWalkStrideLength airborne clip length, otherwise +// @0x53c gimpLeftSpeedMax airborne speed cap, movementMode==3 +// @0x540 gimpRightSpeedMax airborne speed cap, otherwise +// @0x544 gimpLeftStrideLength airborne clip length, movementMode==3 +// @0x548 gimpRightStrideLength airborne clip length, otherwise // @0x598 motionEventName (string) cleared to "" on fall/reset // @0x5a4 motionEventArmed (int) reset to 0 on fall/reset // @0x5a8 globalTimeScale multiplies every clip increment @@ -313,10 +313,16 @@ Scalar Scalar Mech::BodyClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj) { - // airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a6344 (deferred; safe - // no-op while grounded -- the test mech never jumps). - if ((m->MovementMode() == 3 || m->MovementMode() == 4) && m->jumpCapable) - return 0.0f; + // GIMP branch (FUN_004a6d8c top): a limping mech's body transitions run the + // gimp machine instead. Mode = the graphicAlarm level (the binary's real + // mech+0x40: 3=left-leg gimp, 4=right) read via the mechdmg bridge -- + // NEVER graphicAlarm directly here (AlarmIndicator ODR split, gotcha #23). + { + extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (the TU-safe read) + const int gl = BTMechGimpLevel(m); + if ((gl == 3 || gl == 4) && m->hasGimpClips) + return m->GimpBodyClipFinished(carryover, mj); + } const Scalar fcr = m->forwardCycleRate; // 0x344 const Scalar gts = m->globalTimeScale; // 0x5a8 @@ -454,9 +460,14 @@ Scalar Scalar Mech::LegClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj) { - // airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a7970 (deferred). - if ((m->MovementMode() == 3 || m->MovementMode() == 4) && m->jumpCapable) - return 0.0f; + // GIMP branch (FUN_004a6928 top): route a limping mech's leg transitions + // into the gimp machine (mode = graphicAlarm level via bridge, gotcha #23). + { + extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (the TU-safe read) + const int gl = BTMechGimpLevel(m); + if ((gl == 3 || gl == 4) && m->hasGimpClips) + return m->GimpLegClipFinished(carryover); + } // The binary reads edx = *(mech+0x128) then [edx]+0x128: subsystemArray[0] // (the roster's ControlsMapper slot 0) -> speedDemand; null (no mapper) @@ -546,6 +557,250 @@ Scalar } +//########################################################################### +//########################################################################### +// GimpBodyClipFinished / GimpLegClipFinished (#78 visible limp) +// +// @004a6344 (body) / @004a7970 (leg) -- the GIMP transition machines, entered +// from the normal finished-callbacks when (gimpLevel 3|4) && hasGimpClips. +// Same walk/run/reverse alternation as the normal cbs, PLUS: +// - a top clamp of the demand to the gimped leg's speed cap (body: clamps +// bodyTargetSpeed@0x6b4; leg: clamps the LIVE mapper speedDemand and +// writes it back -- the binary's own "you can't outrun a shot leg"); +// - PHASE-CORRECT limp entry from the walk cases: left-gimp (mode 3) enters +// 0x16/wgl only from a RIGHT step (5/6/0xe) -- the L case forces one more +// normal R step first; right-gimp (mode 4) mirrors into 0x17/wgr from the +// L case (7/0xf). The limp always starts on the correct foot. +// - the gg cycles 0x16/0x18 (left, ggr clip, stride @0x544) and 0x17/0x19 +// (right, ggl, @0x548), continuing at gimp cadence or exiting through the +// gs transitions 0x1a/0x1b when the demand dies; +// - NO standing->reverse entry exists in the gimp machines (or drivers): +// the binary itself refuses REVERSE while gimped. +// Mode is the graphicAlarm level via the mechdmg bridge (gotcha #23). +//########################################################################### +//########################################################################### +Scalar + Mech::GimpBodyClipFinished(Scalar carryover, int mj) +{ + extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (TU-safe) + const int mode = BTMechGimpLevel(this); // binary: this+0x40 + const int state = bodyAnimationState; // 0x728 + const Scalar fcr = forwardCycleRate; // 0x344 + const Scalar gts = globalTimeScale; // 0x5a8 + + // Top clamp (0x4a6352): while in a moving cycle, cap the body target speed + // to the gimped side's entry-clip speed, then floor at zero. + if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2 + || (unsigned)(state - 0x12) < 2) + { + const Scalar cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax; // 0x53c / 0x540 + if (bodyTargetSpeed > cap) bodyTargetSpeed = cap; + if (bodyTargetSpeed < ZeroSpeed) bodyTargetSpeed = ZeroSpeed; + } + + const Scalar cyc = bodyCycleSpeed; // 0x6b8 + const Scalar tgt = bodyTargetSpeed; // 0x6b4 (post-clamp) + const Scalar T = carryover * gts; // plain end-tail time + int next; + + switch (state) + { + default: // 0/1 + unmapped: parked + return 0.0f; + case 2: + bodyStateAlarm.SetLevel(1); return 0.0f; + case 3: case 4: case 8: case 9: case 0x14: case 0x15: + case 0x1a: case 0x1b: case 0x20: + bodyStateAlarm.SetLevel(0); return 0.0f; + + // -- walk-R (0x4a6440): states 5,6,0xe -- the left-gimp entry point -- + case 5: case 6: case 0xe: + { + bool cont = (tgt >= standSpeed) || ((cyc - fcr * carryover) >= standSpeed); + if (!cont) { next = 9; break; } // walk->stand L + bool up = (tgt > walkStrideLength) && ((fcr * carryover + cyc) > walkStrideLength); + if (up) { next = 0xb; break; } // toward run (dead once clamped) + int alt = 7; // normal alternation -> wwl + if (mode == 4) alt = 7; // right-gimp: one more normal step + if (mode == 3) alt = 0x16; // left-gimp: enter wgl on this R step + return BodyTransition(alt, carryover * cyc * gts / walkStrideLength, mj); // LAB_004a6505 + } + // -- walk-L (0x4a63ff): states 7,0xf -- the right-gimp entry point -- + case 7: case 0xf: + { + bool cont = (tgt >= standSpeed) || ((cyc - fcr * carryover) >= standSpeed); + if (!cont) { next = 8; break; } // walk->stand R + bool up = (tgt > walkStrideLength) && ((fcr * carryover + cyc) > walkStrideLength); + if (up) { next = 0xa; break; } + int alt = 6; // normal alternation -> wwr + if (mode == 3) alt = 6; // left-gimp: force back to the R step + if (mode == 4) alt = 0x17; // right-gimp: enter wgr on this L step + return BodyTransition(alt, carryover * cyc * gts / walkStrideLength, mj); + } + // -- run cycles (0x4a65f7 / 0x4a6663): 10/12 <-> 11/13 -- + case 0xa: case 0xc: + { + bool cont = (tgt >= reverseSpeedMax) || ((cyc - fcr * carryover) >= reverseSpeedMax); + if (cont) // LAB_004a6648 run-cadence tail + return BodyTransition(0xd, carryover * cyc * gts / reverseStrideLength, mj); + next = 0xf; break; // run->walk R + } + case 0xb: case 0xd: + { + bool cont = (tgt >= reverseSpeedMax) || ((cyc - fcr * carryover) >= reverseSpeedMax); + if (cont) + return BodyTransition(0xc, carryover * cyc * gts / reverseStrideLength, mj); + next = 0xe; break; + } + // -- back cycles (0x4a66d5 / 0x4a6754): 0x10/0x12 <-> 0x11/0x13 -- + case 0x10: case 0x12: + { + bool up = (tgt > gimpSpeedMax) && ((gimpCycleRate * carryover + cyc) > gimpSpeedMax); + if (up) { next = 0x15; break; } // back->stand L + Scalar t = carryover * cyc * gts / gimpStrideLength; + if (t <= 0.0f) t = -t; // 0x350 stored negative + return BodyTransition(0x13, t, mj); + } + case 0x11: case 0x13: + { + bool up = (tgt > gimpSpeedMax) && ((gimpCycleRate * carryover + cyc) > gimpSpeedMax); + if (up) { next = 0x14; break; } + Scalar t = carryover * cyc * gts / gimpStrideLength; + if (t <= 0.0f) t = -t; + return BodyTransition(0x12, t, mj); + } + // -- GIMP cycles (0x4a67cf / 0x4a6836): left 0x16/0x18, right 0x17/0x19 -- + case 0x16: case 0x18: + { + bool cont = (tgt >= gimpLeftSpeedMax) || ((cyc - fcr * carryover) >= gimpLeftSpeedMax); + if (cont) // keep limping (ggr cycle) + return BodyTransition(0x18, carryover * cyc * gts / gimpLeftStrideLength, mj); + next = 0x1a; break; // demand died -> gsl exit + } + case 0x17: case 0x19: + { + bool cont = (tgt >= gimpRightSpeedMax) || ((cyc - fcr * carryover) >= gimpRightSpeedMax); + if (cont) + return BodyTransition(0x19, carryover * cyc * gts / gimpRightStrideLength, mj); + next = 0x1b; break; // -> gsr exit + } + } + return BodyTransition(next, T, mj); // shared plain tail +} + +Scalar + Mech::GimpLegClipFinished(Scalar carryover) +{ + extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (TU-safe) + const int mode = BTMechGimpLevel(this); // binary: this+0x40 + const int state = legAnimationState; // 0x3b0 + MechControlsMapper *mppr = MappingMapper(); // **(this+0x128) + + // Top clamp (0x4a7995): cap the LIVE COMMANDED speedDemand itself while in + // a moving cycle, writing the clamp back -- this is the binary's authentic + // gimp slowdown (the mapper re-derives demand each tick; this cb re-caps + // it every clip end, and the gimp cycle cadence below enforces it anyway). + if (((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2 + || (unsigned)(state - 0x12) < 2) && mppr != 0) + { + const Scalar cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax; // 0x53c / 0x540 + if (mppr->speedDemand > cap) mppr->speedDemand = cap; + if (mppr->speedDemand < ZeroSpeed) mppr->speedDemand = ZeroSpeed; + } + + const Scalar spd = (mppr != 0) ? mppr->speedDemand : 0.0f; // (binary derefs; port guards null) + const Scalar fcr = forwardCycleRate; // 0x344 + const Scalar gts = globalTimeScale; // 0x5a8 + const Scalar cyc = legCycleSpeed; // 0x348 + const Scalar T = carryover * gts; + int next; + + switch (state) // all leg tails mj=1 (binary) + { + default: + return 0.0f; + case 2: + legStateAlarm.SetLevel(1); return 0.0f; + case 3: case 4: case 8: case 9: case 0x14: case 0x15: + case 0x1a: case 0x1b: case 0x20: + legStateAlarm.SetLevel(0); return 0.0f; + + // -- walk-R (0x4a7a52): states 5,6,0xe -- left-gimp entry -- + case 5: case 6: case 0xe: + { + bool cont = (spd >= standSpeed) || ((cyc - fcr * carryover) >= standSpeed); + if (!cont) { next = 9; break; } + bool up = (spd > walkStrideLength) && ((fcr * carryover + cyc) > walkStrideLength); + if (up) { next = 0xb; break; } + int alt = 7; + if (mode == 4) alt = 7; // right-gimp: one more normal step + if (mode == 3) alt = 0x16; // left-gimp: enter wgl on this R step + return LegTransition(alt, carryover * cyc * gts / walkStrideLength, 1); // LAB_004a7b38 + } + // -- walk-L (0x4a7bb5): states 7,0xf -- right-gimp entry -- + case 7: case 0xf: + { + bool cont = (spd >= standSpeed) || ((cyc - fcr * carryover) >= standSpeed); + if (!cont) { next = 8; break; } + bool up = (spd > walkStrideLength) && ((fcr * carryover + cyc) > walkStrideLength); + if (up) { next = 0xa; break; } + int alt = 6; + if (mode == 3) alt = 6; // left-gimp: force back to the R step + if (mode == 4) alt = 0x17; // right-gimp: enter wgr on this L step + return LegTransition(alt, carryover * cyc * gts / walkStrideLength, 1); + } + // -- run cycles (0x4a7c33 / 0x4a7c93): -- + case 0xa: case 0xc: + { + bool cont = (spd >= reverseSpeedMax) || ((cyc - fcr * carryover) >= reverseSpeedMax); + if (cont) // LAB_004a7c79 + return LegTransition(0xd, carryover * cyc * gts / reverseStrideLength, 1); + next = 0xf; break; + } + case 0xb: case 0xd: + { + bool cont = (spd >= reverseSpeedMax) || ((cyc - fcr * carryover) >= reverseSpeedMax); + if (cont) + return LegTransition(0xc, carryover * cyc * gts / reverseStrideLength, 1); + next = 0xe; break; + } + // -- back cycles: 0x10/0x12 <-> 0x11/0x13 -- + case 0x10: case 0x12: + { + bool up = (spd > gimpSpeedMax) && ((gimpCycleRate * carryover + cyc) > gimpSpeedMax); + if (up) { next = 0x15; break; } + Scalar t = carryover * cyc * gts / gimpStrideLength; + if (t <= 0.0f) t = -t; + return LegTransition(0x13, t, 1); + } + case 0x11: case 0x13: + { + bool up = (spd > gimpSpeedMax) && ((gimpCycleRate * carryover + cyc) > gimpSpeedMax); + if (up) { next = 0x14; break; } + Scalar t = carryover * cyc * gts / gimpStrideLength; + if (t <= 0.0f) t = -t; + return LegTransition(0x12, t, 1); + } + // -- GIMP cycles: left 0x16/0x18, right 0x17/0x19 -- + case 0x16: case 0x18: + { + bool cont = (spd >= gimpLeftSpeedMax) || ((cyc - fcr * carryover) >= gimpLeftSpeedMax); + if (cont) + return LegTransition(0x18, carryover * cyc * gts / gimpLeftStrideLength, 1); + next = 0x1a; break; + } + case 0x17: case 0x19: + { + bool cont = (spd >= gimpRightSpeedMax) || ((cyc - fcr * carryover) >= gimpRightSpeedMax); + if (cont) + return LegTransition(0x19, carryover * cyc * gts / gimpRightStrideLength, 1); + next = 0x1b; break; + } + } + return LegTransition(next, T, 1); // shared plain tail (mj=1) +} + + //########################################################################### //########################################################################### // AdvanceLegAnimation (channel A, ground) @@ -664,7 +919,7 @@ Scalar // narrow near-zero entry gate (the pre-#64b accommodation). [T3] const int trnIsRepl = (GetInstance() == ReplicantInstance); const Scalar trnEntryMax = trnIsRepl ? standSpeed * 0.25f : standSpeed; - if (hasCrashSet != 0 && mppr != 0 + if (turnCapable != 0 && mppr != 0 && commandedSpeed >= ZeroSpeed && commandedSpeed <= trnEntryMax && (mppr->turnDemand > 0.05f || mppr->turnDemand < -0.05f) @@ -1112,38 +1367,47 @@ Scalar //########################################################################### //########################################################################### -// AdvanceBodyAnimationAirborne (channel B, jump-capable) +// AdvanceBodyAnimationGimp (channel B, limping) // // @004a5bf8 (MECH2.CPP:0x2E8) // -// Airborne variant of AdvanceBodyAnimation selected by Mech::IntegrateMotion -// when jump jets are active. Adds a pre-clamp of bodyTargetSpeed to the jump -// speed limits and handles the FallForward / FallBackward jump cycles -// (states 0x18/0x19); gimp-to-stand (0x16/0x17) and the lateral falls -// (0x1a/0x1b) join the normal advance group here rather than resetting. +// GIMP (limp-gait) variant of AdvanceBodyAnimation, selected when +// (gimpLevel 3|4) && hasGimpClips ("Airborne"/jump-jet was a misread -- the +// clip set is wg/gg/gs, #78). Adds a pre-clamp of bodyTargetSpeed to the +// gimped side's speed cap and drives the gg limp cycles (0x18 left / 0x19 +// right) at gimp cadence; the wg entries (0x16/0x17) and gs exits (0x1a/0x1b) +// join the plain advance group. Standing (case 0) only ever enters FORWARD +// walk -- the binary refuses reverse while gimped. //########################################################################### //########################################################################### Scalar - Mech::AdvanceBodyAnimationAirborne(Scalar time_slice, int loop) + Mech::AdvanceBodyAnimationGimp(Scalar time_slice, int loop) { Scalar distance = 0.0f; + // RE-SYNC alarm -> state member (the binary's one cell is split in the + // recon, same as the ground drivers at :831/:1181). Without this the + // member freezes at its pre-gimp value the moment this driver takes over + // and the whole machine pins in one state (live-diagnosed 2026-07-30). + bodyAnimationState = (int)bodyStateAlarm.GetLevel(); + // // While in any forward/reverse/gimp *moving* cycle, clamp the target to - // the jump speed limit for the current gait, then floor at zero. + // the gimp speed limit for the current gait, then floor at zero. // { int state = bodyAnimationState; // this+0x728 if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2 || (unsigned)(state - 0x12) < 2) { - if (MovementMode() == 3) // run jump + extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23) + if (BTMechGimpLevel(this) == 3) // left leg gimped { - if (bodyTargetSpeed > jumpRunSpeedMax) bodyTargetSpeed = jumpRunSpeedMax; // 0x53c + if (bodyTargetSpeed > gimpLeftSpeedMax) bodyTargetSpeed = gimpLeftSpeedMax; // 0x53c } else // walk jump { - if (bodyTargetSpeed > jumpWalkSpeedMax) bodyTargetSpeed = jumpWalkSpeedMax; // 0x540 + if (bodyTargetSpeed > gimpRightSpeedMax) bodyTargetSpeed = gimpRightSpeedMax; // 0x540 } if (bodyTargetSpeed < ZeroSpeed) bodyTargetSpeed = ZeroSpeed; } @@ -1259,7 +1523,8 @@ Scalar case 0x18: case 0x19: // FallForward / FallBackward (jump) { Scalar ratio; - if (MovementMode() == 3) // run jump: caps 0x53c / 0x544 + extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23) + if (BTMechGimpLevel(this) == 3) // left-gimp cycle: caps 0x53c / 0x544 { if (bodyTargetSpeed <= bodyCycleSpeed) { @@ -1267,16 +1532,16 @@ Scalar { bodyCycleSpeed -= forwardCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; - if (bodyCycleSpeed < jumpRunSpeedMax) bodyCycleSpeed = jumpRunSpeedMax; // 0x53c + if (bodyCycleSpeed < gimpLeftSpeedMax) bodyCycleSpeed = gimpLeftSpeedMax; // 0x53c } } else { bodyCycleSpeed += forwardCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; - if (bodyCycleSpeed > jumpRunStrideLength) bodyCycleSpeed = jumpRunStrideLength; // 0x544 + if (bodyCycleSpeed > gimpLeftStrideLength) bodyCycleSpeed = gimpLeftStrideLength; // 0x544 } - ratio = bodyCycleSpeed / jumpRunStrideLength; // 0x544 + ratio = bodyCycleSpeed / gimpLeftStrideLength; // 0x544 } else // walk jump: caps 0x540 / 0x548 { @@ -1286,16 +1551,16 @@ Scalar { bodyCycleSpeed -= forwardCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; - if (bodyCycleSpeed < jumpWalkSpeedMax) bodyCycleSpeed = jumpWalkSpeedMax; // 0x540 + if (bodyCycleSpeed < gimpRightSpeedMax) bodyCycleSpeed = gimpRightSpeedMax; // 0x540 } } else { bodyCycleSpeed += forwardCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; - if (bodyCycleSpeed > jumpWalkStrideLength) bodyCycleSpeed = jumpWalkStrideLength; // 0x548 + if (bodyCycleSpeed > gimpRightStrideLength) bodyCycleSpeed = gimpRightStrideLength; // 0x548 } - ratio = bodyCycleSpeed / jumpWalkStrideLength; // 0x548 + ratio = bodyCycleSpeed / gimpRightStrideLength; // 0x548 } distance = bodyAnimation.Advance( time_slice * ratio * globalTimeScale, loop); @@ -1315,24 +1580,36 @@ Scalar //########################################################################### //########################################################################### -// AdvanceLegAnimationAirborne (channel A, jump-capable) +// AdvanceLegAnimationGimp (channel A, limping) // // @004a71f4 (MECH2.CPP:0x672) // -// Airborne variant of AdvanceLegAnimation. Like the ground version it reads -// the live commanded speed from the controls subsystem, but here it also -// CLAMPS that source value to the jump speed limit (writing it back), and -// handles the FallForward / FallBackward jump cycles (0x18/0x19). No death -// latch / footstep block; gimp-to-stand and lateral falls join the normal -// advance group. +// GIMP variant of AdvanceLegAnimation (see the body variant's note). Like +// the ground version it reads the live commanded speed from the controls +// subsystem, but here it also CLAMPS that source value to the gimped side's +// speed cap (writing it back -- the authentic "can't outrun a shot leg"), +// and drives the gg limp cycles (0x18/0x19). No death latch / footstep +// block; wg entries and gs exits join the normal advance group. //########################################################################### //########################################################################### Scalar - Mech::AdvanceLegAnimationAirborne(Scalar time_slice) + Mech::AdvanceLegAnimationGimp(Scalar time_slice) { - ReconMotionSource *motionSource = *(ReconMotionSource **)(this->controlSource); // **(this+0x128) + // Binary: **(this+0x128) + 0x128 = the mapper's live speedDemand. The + // port's controlSource@0x128 is NOT wired (null -> the first live gimp + // engagement crashed here, 2026-07-30 bench) -- use the roster idiom the + // ground driver + LegClipFinished use (MappingMapper), pointing the shim + // straight at the speedDemand cell. + MechControlsMapper *gimpMppr = MappingMapper(); + static Scalar s_nullDemand = 0.0f; // no mapper -> demand 0, writes inert + ReconMotionSource *motionSource = gimpMppr + ? (ReconMotionSource *)&gimpMppr->speedDemand + : (ReconMotionSource *)&s_nullDemand; Scalar distance = 0.0f; - int mode = MovementMode(); // this+0x40 = simulationState + extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23) + int mode = BTMechGimpLevel(this); // binary this+0x40 = gimp level + // RE-SYNC alarm -> state member (see AdvanceBodyAnimationGimp note). + legAnimationState = (int)legStateAlarm.GetLevel(); int state = legAnimationState; // this+0x3b0 // @@ -1344,13 +1621,13 @@ Scalar { if (mode == 3) { - if (motionSource->commandedSpeed > jumpRunSpeedMax) - motionSource->commandedSpeed = jumpRunSpeedMax; // 0x53c + if (motionSource->commandedSpeed > gimpLeftSpeedMax) + motionSource->commandedSpeed = gimpLeftSpeedMax; // 0x53c } else { - if (motionSource->commandedSpeed > jumpWalkSpeedMax) - motionSource->commandedSpeed = jumpWalkSpeedMax; // 0x540 + if (motionSource->commandedSpeed > gimpRightSpeedMax) + motionSource->commandedSpeed = gimpRightSpeedMax; // 0x540 } if (motionSource->commandedSpeed < ZeroSpeed) motionSource->commandedSpeed = ZeroSpeed; @@ -1477,16 +1754,16 @@ Scalar { legCycleSpeed -= forwardCycleRate * time_slice; if (legCycleSpeed < motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; - if (legCycleSpeed < jumpRunSpeedMax) legCycleSpeed = jumpRunSpeedMax; + if (legCycleSpeed < gimpLeftSpeedMax) legCycleSpeed = gimpLeftSpeedMax; } } else { legCycleSpeed += forwardCycleRate * time_slice; if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; - if (legCycleSpeed > jumpRunStrideLength) legCycleSpeed = jumpRunStrideLength; + if (legCycleSpeed > gimpLeftStrideLength) legCycleSpeed = gimpLeftStrideLength; } - ratio = legCycleSpeed / jumpRunStrideLength; // 0x544 + ratio = legCycleSpeed / gimpLeftStrideLength; // 0x544 } else // walk jump { @@ -1496,16 +1773,16 @@ Scalar { legCycleSpeed -= forwardCycleRate * time_slice; if (legCycleSpeed < motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; - if (legCycleSpeed < jumpWalkSpeedMax) legCycleSpeed = jumpWalkSpeedMax; + if (legCycleSpeed < gimpRightSpeedMax) legCycleSpeed = gimpRightSpeedMax; } } else { legCycleSpeed += forwardCycleRate * time_slice; if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; - if (legCycleSpeed > jumpWalkStrideLength) legCycleSpeed = jumpWalkStrideLength; + if (legCycleSpeed > gimpRightStrideLength) legCycleSpeed = gimpRightStrideLength; } - ratio = legCycleSpeed / jumpWalkStrideLength; // 0x548 + ratio = legCycleSpeed / gimpRightStrideLength; // 0x548 } distance = legAnimation.Advance(time_slice * ratio * globalTimeScale, 1); } diff --git a/game/reconstructed/mech3.cpp b/game/reconstructed/mech3.cpp index b04e42b..45aa124 100644 --- a/game/reconstructed/mech3.cpp +++ b/game/reconstructed/mech3.cpp @@ -212,13 +212,13 @@ template inline void *FUN_00406db4(A&&...) { return 0; } // ResourceF // @0x530 standSpeed = last-keyframe stride of the stand->walk clip // @0x534 walkStrideLength = (s_6+s_7)/(d_6+d_7) (forward walk/run) // @0x538 reverseSpeedMax = last-keyframe stride of the reverse-base clip -// @0x53c jumpRunSpeedMax = last-keyframe stride of the run-jump clip -// @0x540 jumpWalkSpeedMax = last-keyframe stride of the walk-jump clip -// @0x544 jumpRunStrideLength = s/d of fall-forward jump clip (slot 0x18) -// @0x548 jumpWalkStrideLength = s/d of fall-backward jump clip (slot 0x19) -// @0x580 jumpCapable (int) set 1 iff the model defines the jump clip set -// @0x584 hasReverseGimpSet (int)? set 1 iff the second optional clip set exists -// @0x588 hasCrashSet (int)? set 1 iff the third optional clip set exists +// @0x53c gimpLeftSpeedMax = last-keyframe stride of the run-jump clip +// @0x540 gimpRightSpeedMax = last-keyframe stride of the walk-jump clip +// @0x544 gimpLeftStrideLength = s/d of fall-forward jump clip (slot 0x18) +// @0x548 gimpRightStrideLength = s/d of fall-backward jump clip (slot 0x19) +// @0x580 hasGimpClips (int) set 1 iff the model defines the jump clip set +// @0x584 squatCapable (int)? set 1 iff the second optional clip set exists +// @0x588 turnCapable (int)? set 1 iff the third optional clip set exists // @0x5b8 groundCycleRate forward-cycle slew rate, on-ground (-> 0x344) // @0x5bc airborneCycleRate forward-cycle slew rate, airborne (-> 0x344) // @0x5c4 gyroRumbleTimer (float) reset to 0 at the top of a clip load @@ -319,7 +319,7 @@ void // stride @0x534, reverseSpeedMax @0x538, reverse stride @0x34c, gimpSpeedMax // @0x52c, gimp stride @0x350), then probe for the three OPTIONAL clip sets // (jump / reverse-gimp / crash) and, if their marker resource exists, resolve -// those too and set the matching capability flag (jumpCapable @0x580 etc.). +// those too and set the matching capability flag (hasGimpClips @0x580 etc.). //########################################################################### //########################################################################### void @@ -406,10 +406,10 @@ void // ---- OPTIONAL clip set 1: JUMP JETS ---- // Probe for the run-jump marker; if present, the model is jump-capable. // - jumpCapable = 0; // this+0x580 + hasGimpClips = 0; // this+0x580 if (ResolveAnimationClip(prefix, "wgl") != 0) // FUN_00406ff8 probe (real) { - jumpCapable = 1; + hasGimpClips = 1; // SLOT MAP BINARY-VERIFIED (raw :13787-13812 + .data @0050d921-35): // wgl->0x624 [22], wgr->0x628 [23], ggr->0x62c [24], ggl->0x630 [25], @@ -417,19 +417,19 @@ void // shifted the rest one slot low.) animationClips[22] = *ResolveAnimationClip(prefix, "wgl"); // 0x624 state 0x16 legAnimation.SelectSequence(animationClips[22], PTR_LAB_0050d738, DAT_0050d73c, DAT_0050d740); - jumpRunSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x53c + gimpLeftSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x53c animationClips[23] = *ResolveAnimationClip(prefix, "wgr"); // 0x628 state 0x17 legAnimation.SelectSequence(animationClips[23], PTR_LAB_0050d744, DAT_0050d748, DAT_0050d74c); - jumpWalkSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x540 + gimpRightSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x540 animationClips[24] = *ResolveAnimationClip(prefix, "ggr"); // 0x62c state 0x18 MeasureClipStride(0x18, &s0, &d0); - jumpRunStrideLength = s0 / d0; // 0x544 + gimpLeftStrideLength = s0 / d0; // 0x544 animationClips[25] = *ResolveAnimationClip(prefix, "ggl"); // 0x630 state 0x19 MeasureClipStride(0x19, &s0, &d0); - jumpWalkStrideLength = s0 / d0; // 0x548 + gimpRightStrideLength = s0 / d0; // 0x548 animationClips[26] = *ResolveAnimationClip(prefix, "gsl"); // 0x634 state 0x1A animationClips[27] = *ResolveAnimationClip(prefix, "gsr"); // 0x638 state 0x1B @@ -443,10 +443,10 @@ void // suffixes/slots and stored into side members SetLeg/BodyAnimation never // consult -- states 2/3 read animationClips[] directly.) // - hasReverseGimpSet = 0; // this+0x584 (squatCapable) + squatCapable = 0; // this+0x584 (squatCapable) if (ResolveAnimationClip(prefix, "squ") != 0) { - hasReverseGimpSet = 1; + squatCapable = 1; animationClips[3] = *ResolveAnimationClip(prefix, "squ"); // 0x5d8 animationClips[2] = *ResolveAnimationClip(prefix, "sqd"); // 0x5d4 } @@ -457,14 +457,14 @@ void // flag @0x588 (turnCapable); loads trn -> 0x5dc == animationClips[4] -- // the state-4 turn-in-place clip (SetLegAnimation(4) arms it). // - hasCrashSet = 0; // this+0x588 (turnCapable) + turnCapable = 0; // this+0x588 (turnCapable) if (ResolveAnimationClip(prefix, "trn") != 0) { - hasCrashSet = 1; + turnCapable = 1; animationClips[4] = *ResolveAnimationClip(prefix, "trn"); // 0x5dc } DEBUG_STREAM << "[loadclips] end: fScale=" << forwardThrottleScale - << " gimpSpeedMax=" << gimpSpeedMax << " jumpCapable=" << jumpCapable << "\n" << std::flush; + << " gimpSpeedMax=" << gimpSpeedMax << " hasGimpClips=" << hasGimpClips << "\n" << std::flush; } @@ -549,25 +549,25 @@ void // // ---- OPTIONAL clip set 1: JUMP JETS (probe "wgli") ---- // - jumpCapable = 0; // this+0x580 + hasGimpClips = 0; // this+0x580 if (ResolveAnimationClip(prefix, "wgli") != 0) { - jumpCapable = 1; + hasGimpClips = 1; animationClips[22] = *ResolveAnimationClip(prefix, "wgli"); // 0x624 state 0x16 legAnimation.SelectSequence(animationClips[22], PTR_LAB_0050d738, DAT_0050d73c, DAT_0050d740); - jumpRunSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x53c + gimpLeftSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x53c animationClips[23] = *ResolveAnimationClip(prefix, "wgri"); // 0x628 state 0x17 legAnimation.SelectSequence(animationClips[23], PTR_LAB_0050d744, DAT_0050d748, DAT_0050d74c); - jumpWalkSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x540 + gimpRightSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x540 animationClips[24] = *ResolveAnimationClip(prefix, "ggri"); // 0x62c state 0x18 MeasureClipStride(0x18, &s0, &d0); - jumpRunStrideLength = s0 / d0; // 0x544 + gimpLeftStrideLength = s0 / d0; // 0x544 animationClips[25] = *ResolveAnimationClip(prefix, "ggli"); // 0x630 state 0x19 MeasureClipStride(0x19, &s0, &d0); - jumpWalkStrideLength = s0 / d0; // 0x548 + gimpRightStrideLength = s0 / d0; // 0x548 animationClips[26] = *ResolveAnimationClip(prefix, "gsli"); // 0x634 state 0x1A animationClips[27] = *ResolveAnimationClip(prefix, "gsri"); // 0x638 state 0x1B @@ -576,10 +576,10 @@ void // // ---- OPTIONAL clip set 2: SQUAT (probe "squi") ---- // - hasReverseGimpSet = 0; // this+0x584 (squatCapable) + squatCapable = 0; // this+0x584 (squatCapable) if (ResolveAnimationClip(prefix, "squi") != 0) { - hasReverseGimpSet = 1; + squatCapable = 1; animationClips[3] = *ResolveAnimationClip(prefix, "squi"); // 0x5d8 animationClips[2] = *ResolveAnimationClip(prefix, "sqdi"); // 0x5d4 } @@ -587,14 +587,14 @@ void // // ---- OPTIONAL clip set 3: TURN-IN-PLACE (probe "trni") ---- // - hasCrashSet = 0; // this+0x588 (turnCapable) + turnCapable = 0; // this+0x588 (turnCapable) if (ResolveAnimationClip(prefix, "trni") != 0) { - hasCrashSet = 1; + turnCapable = 1; animationClips[4] = *ResolveAnimationClip(prefix, "trni"); // 0x5dc } DEBUG_STREAM << "[loadclips] INTERIOR ('i') set: fScale=" << forwardThrottleScale - << " walkStride=" << walkStrideLength << " jumpCapable=" << jumpCapable + << " walkStride=" << walkStrideLength << " hasGimpClips=" << hasGimpClips << "\n" << std::flush; } diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 2a7103d..cd459ed 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -67,7 +67,7 @@ // Helper / engine routine name mapping used below: // FUN_0049fb54 Mech::IsDisabled() -> Logical // FUN_004a5678 Mech::AdvanceBodyAnimation(dt, loop) [mech2.cpp] -// FUN_004a5bf8 Mech::AdvanceBodyAnimationAirborne(dt, loop) [mech2.cpp] +// FUN_004a5bf8 Mech::AdvanceBodyAnimationGimp(dt, loop) [mech2.cpp] // FUN_004ab188 Mech::DeadReckonPose(dt) // FUN_004ab1c8 Mech::IntegrateMotion(dt, loop) -> Logical (disabled?) // FUN_004086ac Vector::Scale(out, in, scalar) @@ -239,7 +239,7 @@ struct MechBaseLayoutCheck // @0x4b8 templateBottomLift (mech.hpp) ctor: 0.05 x volume X width // @0x518 standingTemplateMaxY (mech.hpp) CORRECTED (was "heatLevel") // @0x51c duckedTemplateMaxY (mech.hpp) CORRECTED (was "heatCapacity"); 0.6 x standing -// @0x580 jumpCapable (mech3) +// @0x580 hasGimpClips (mech3) // @0x598 motionEventName (mech2) // @0x5a4 motionEventArmed (mech2) // @0x5b8 groundCycleRate (mech3) @@ -285,7 +285,7 @@ void // // Advance the *displayed-motion* (channel-B) body gait one frame and integrate // the resulting cycle distance into the world transform. Picks the airborne -// body updater when (movementMode==3||4) && jumpCapable, else the ground one. +// body updater when (movementMode==3||4) && hasGimpClips, else the ground one. // Also fires the queued footstep / motion event when the mech "arrives". // Returns the IsDisabled() result captured at entry (the caller branches on it). //########################################################################### @@ -337,12 +337,19 @@ Logical } } - // Advance the body gait (airborne flavour while jumping). + // Advance the body gait (GIMP flavour while limping, #78). The binary's + // mech+0x40 here is the graphicAlarm level (3=left gimp, 4=right) -- read + // via the mechdmg bridge, never MovementMode()/graphicAlarm directly + // (simulationState split-brain + AlarmIndicator ODR, gotcha #23). Scalar cycleDistance; - if ((MovementMode() == 3 || MovementMode() == 4) && jumpCapable) // 0x40, 0x580 - cycleDistance = AdvanceBodyAnimationAirborne(time_slice, loop); // FUN_004a5bf8 - else - cycleDistance = AdvanceBodyAnimation(time_slice, loop); // FUN_004a5678 + { + extern int BTMechGimpLevel(void *mech_v); + const int gl = BTMechGimpLevel(this); + if ((gl == 3 || gl == 4) && hasGimpClips) // 0x40, 0x580 + cycleDistance = AdvanceBodyAnimationGimp(time_slice, loop); // FUN_004a5bf8 + else + cycleDistance = AdvanceBodyAnimation(time_slice, loop); // FUN_004a5678 + } // The raw velocity vector (0x298) = {0, 0, -cycleDistance/dt}; the world-step // FUN_00408744 reads all three components, so 0x2a0 (== angularAccum[2]) MUST be @@ -2427,9 +2434,16 @@ void else if (!turning && bs == 4) // turn stopped -> back to stand SetBodyAnimation(0); } + // GIMP routing (#78): a limping PEER runs the gimp machines too + // (the graphicAlarm level replicates with the damage state). + extern int BTMechGimpLevel(void *mech_v); + const int replGl = BTMechGimpLevel(this); + const bool replGimp = (replGl == 3 || replGl == 4) && hasGimpClips; const Scalar replLegAdv = s_peerLegCh - ? AdvanceLegAnimation(dt) // old: re-derived leg SM - : AdvanceBodyAnimation(dt, 1); // authentic: body channel, mj=1 poses skeleton + ? (replGimp ? AdvanceLegAnimationGimp(dt) // FUN_004a71f4 + : AdvanceLegAnimation(dt)) // old: re-derived leg SM + : (replGimp ? AdvanceBodyAnimationGimp(dt, 1) // FUN_004a5bf8 + : AdvanceBodyAnimation(dt, 1)); // authentic: body channel, mj=1 poses skeleton // AUTHENTIC coupled position (mirror of the master's world-step at // mech4.cpp:3325-3336 == Mech::IntegrateMotion tail @004ab1c8): between // records the body advances by the CLIP'S OWN travel rotated by the @@ -3897,13 +3911,25 @@ void // INVISIBLE in the binary" (locomotion.md) -- now it is here too. // BT_BODY_MJ=1 restores the old double-writer for A/B. static const int s_bodyMj = BTEnvOn("BT_BODY_MJ", 0); - adv = AdvanceBodyAnimation(dt, s_bodyMj); // channel B: replication projection (mj=0) + // GIMP routing (#78): route BOTH channels through the gimp + // drivers while limping -- (gimpLevel 3|4) && hasGimpClips, + // level via the mechdmg bridge (gotcha #23). + extern int BTMechGimpLevel(void *mech_v); + const int gl2 = BTMechGimpLevel(this); + const bool gimpy = (gl2 == 3 || gl2 == 4) && hasGimpClips; + adv = gimpy ? AdvanceBodyAnimationGimp(dt, s_bodyMj) // FUN_004a5bf8 + : AdvanceBodyAnimation(dt, s_bodyMj); // channel B: replication projection (mj=0) mirrorBodyAdv = adv; // stash for the send-mirror gait advance - legAdv = AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel + legAdv = gimpy ? AdvanceLegAnimationGimp(dt) // FUN_004a71f4 + : AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel } else { - adv = AdvanceBodyAnimation(dt, 1); // single-channel: body does both + extern int BTMechGimpLevel(void *mech_v); + const int gl2 = BTMechGimpLevel(this); + const bool gimpy = (gl2 == 3 || gl2 == 4) && hasGimpClips; + adv = gimpy ? AdvanceBodyAnimationGimp(dt, 1) + : AdvanceBodyAnimation(dt, 1); // single-channel: body does both } // [syncF] per-frame divergence probe (task #49): log every frame // where the two channels return different distances or either @@ -5608,10 +5634,21 @@ void && !IsMechDestroyed() && damageZoneCount > 0) { static float s_sdAccum = 0.0f; + static int s_sdTicks = 0; + // BT_SELF_DAMAGE_TICKS=: stop after n hits (bench: ramp a leg + // PAST LegHalfStructure then hold, so the limp window is unbounded + // instead of the 1 s between half and destroyed at high dps). + static int s_sdMaxTicks = -1; + if (s_sdMaxTicks < 0) + { + const char *te = getenv("BT_SELF_DAMAGE_TICKS"); + s_sdMaxTicks = (te != 0 && *te != '\0') ? atoi(te) : 0; // 0 = unlimited + } s_sdAccum += dt; - if (s_sdAccum >= 1.0f) + if (s_sdAccum >= 1.0f && (s_sdMaxTicks <= 0 || s_sdTicks < s_sdMaxTicks)) { s_sdAccum = 0.0f; + ++s_sdTicks; Damage dmg; dmg.damageType = Damage::ExplosiveDamageType; float want = (float)atof(getenv("BT_SELF_DAMAGE")); @@ -5634,7 +5671,8 @@ void sizeof(Entity::TakeDamageMessage), GetEntityID(), sdZone /*-1 -> cylinder lottery*/, dmg); DEBUG_STREAM << "[selfdmg] " << dmg.damageAmount - << " to own mech (unaimed)" << std::endl << std::flush; + << " to own mech (zone " << sdZone << ", -1=lottery)" + << std::endl << std::flush; Dispatch(&td); } } diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 0c15b98..ee0a4be 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -974,9 +974,13 @@ void // attached feed into its drive; the 2007 engine's mover has no feed // roster, so the same multiplication is applied to the speed demand here. // speedEffect is the wrapper's live 0..1 output (gear ratio, thermal - // curve, 1 - zone damage); a mech with no Myomers drives at 1.0. While - // GIMPED (mode 3/4) the demand additionally halves -- [T3: the 0.5 is VGL - // Lynx's "roughly 50%" firsthand account, BT_GIMP_SPEED overrides]. + // curve, 1 - zone damage); a mech with no Myomers drives at 1.0. + // GIMP slowdown: the binary's real mechanism is the gimp gait machines + // (FUN_004a5bf8/71f4/6344/7970) -- the LIVE demand is clamped to the wg + // clip's speed (GimpLegClipFinished writes it back) and the limp cycle + // cadence enforces it; an EXTRA multiply here would double-slow. The + // earlier T3 0.5 stand-in is therefore retired; BT_GIMP_SPEED remains as + // an optional override multiplier (unset = 1.0 = authentic). { extern Scalar BTMyomersDriveOf(void *subsystem); Scalar drive = 1.0f; @@ -998,8 +1002,8 @@ void if (s_gimp < 0.0f) { const char *e = getenv("BT_GIMP_SPEED"); - s_gimp = (e != 0 && *e != '\0') ? (Scalar)atof(e) : 0.5f; - if (s_gimp < 0.0f || s_gimp > 1.0f) s_gimp = 0.5f; + s_gimp = (e != 0 && *e != '\0') ? (Scalar)atof(e) : 1.0f; // 1.0 = authentic (clamp lives in the gimp SM) + if (s_gimp < 0.0f || s_gimp > 1.0f) s_gimp = 1.0f; } drive *= s_gimp; }