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=<n> 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 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-29 15:49:39 -05:00
co-authored by Claude Fable 5
parent 520f6eecd3
commit 36f68718c2
9 changed files with 506 additions and 127 deletions
+1 -1
View File
@@ -475,7 +475,7 @@ default-ON (`'0'` disables).
| `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_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=<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 |
+42 -14
View File
@@ -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/1damage, 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`.
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=<n>` 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.
+17
View File
@@ -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.
+1 -1
View File
@@ -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,
+24 -9
View File
@@ -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) ---
+329 -52
View File
@@ -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);
}
+30 -30
View File
@@ -212,13 +212,13 @@ template<class...A> 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;
}
+53 -15
View File
@@ -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=<n>: 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);
}
}
+9 -5
View File
@@ -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;
}