BT410 5.3.96: the LIMP is real -- all twelve mech2 functions reconstructed, and the drag leg shows up on the wire

The "gimp" family had two conflicting donor readings -- limp vs jump-jet --
flagged in 5.3.94 rather than guessed at.  The binary settles it: LIMP.  The
mode test in every gimp function is mech+0x40 in {3,4}, the same values the
damage model documents as "limp gait graphic (left 3 / right 4)"; there is no
jump-jet control anywhere in the pod cockpit; and the donor's port routing
("graphicAlarm 3/4") was the same field under a different name.  Its
"run-jump clip" mech3 annotations were the misreading that started the murk.

RECONSTRUCTED, completing all 12 of mech2's census functions:

  GimpLegClipFinished  @004a7970    GimpBodyClipFinished @004a6344
  AdvanceLegAnimationGimp @004a71f4 AdvanceBodyAnimationGimp @004a5bf8
  + the limp branch atop both normal *ClipFinished
  + the limp pick in Simulate (replaces the normal advancers while limping)

HOW A LIMP WORKS, now from the bytes rather than description:

  It replaces ONE stride.  Limping left, the right stride (6) hands off to
  the left limp figure (0x16 -> the self-cycling 0x18) while the other leg
  keeps its normal clips.  The asymmetry IS the limp.

  Both machines CLAMP THEIR DEMAND while in a cycle -- the leg machine
  writes the mapper's own speedDemand cell down to the damaged side's cap
  (new MechControlsMapper::SetSpeedDemand, matching the binary's direct
  mapper+0x128 write), the body machine clamps bodyTargetSpeed, both floor
  at zero.  A limping mech cannot command more than its figure carries, nor
  reverse out of a forward cycle.

  The limp advancers keep states 0x16-0x1b LIVE -- the normal advancers
  treat those as the reset group, which is exactly why the limp flavours
  must be selected while limping or the figure is neutralized mid-cycle.
  No death latch, no wind-down: the movement modes are exclusive.

  ALSO EXPLAINED IN PASSING: Ghidra's 3760-byte FUN_004a6344 -- the census's
  largest function -- is really THREE functions.  The two normal ClipFinished
  callbacks (@004a6928/@004a6d8c) are reached only via data pointers, so the
  decompiler folded them into the gimp-body machine's extent.

All movement-mode reads route through Mech::MovementMode() (mech+0x40 == the
simulation state), which honours a BT_FORCE_LIMP=3|4 dev hook so the gait
could be verified before the damage model's limp hook exists.

VERIFIED, two runs on the rig:

  NO-REGRESSION: the normal mission's speed sequence is BIT-IDENTICAL to
  5.3.95 (7.31972, 26.6726, 22.1601, ...).  The branch costs nothing.

  FORCED LEFT LIMP (new pod_render_limp.conf): the mapper still demands
  26.9; the hull lurches at 10-23.  And on the wire the healthy walk's tight
  pose-count pairs (650/649 ... 434/421) BREAK to a 3x asymmetry -- the
  drag-leg joint at 18 poses against its partner's 55.  The limp is visible
  in the data exactly the way it will be visible on screen.

STILL OPEN in this family: the damage hook (leg zone >= 0.5 -> mode 3/4, a
MECHDMG increment -- nothing sets the mode in real play yet), and
IntegrateMotion's remaining pieces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-03 13:19:10 -05:00
co-authored by Claude Fable 5
parent c578a9b790
commit 7c2363d89f
6 changed files with 1087 additions and 9 deletions
@@ -0,0 +1,87 @@
# pod_render_norio.conf -- pod_render_rec with the RIO serial port OFF.
#
# The emulator log shows a steady stream of serial1 RX OVERRUN errors on
# the RIO pipe, and controls post their events at HighEventPriority
# UNCONDITIONALLY (CONTROLS.HPP:250) -- so a chattering RIO port would
# flood a priority the background pump always serves first, starving the
# priority-0 renderer events the load gate waits on. This conf tests that
# by removing the port entirely. Everything else is identical to the rec
# conf, so a launch here and a hang there isolates the RIO.
#
[sdl]
output=opengl
# higher,higher not highest: HIGH_PRIORITY_CLASS starved the host desktop;
# with the retry patches a rare dropout self-recovers (see gauge_rio.conf).
priority=higher,higher
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
# match the EMU8000s' native rate (no resample) and buffer ~60ms so brief
# emulation-thread stalls (RIO retry recovery) don't audibly chop
rate=44100
blocksize=1024
prebuffer=60
[serial]
# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's
# few-ms ACK deadline is met; plasma display on COM2 (real pod has both).
# VWE fork namedpipe backend (com0com/realport retired -- COM1/COM2 gone):
# DOSBox = pipe client (retry), vRIO/vPLASMA apps = servers; an unconnected
# pipe behaves as an unplugged cable so the mission still runs. serialnamedpipe.h
serial1=disabled
serial2=namedpipe pipe:vplasma
# live UNBUFFERED game output: DOS char devices are not buffered, so
# redirecting stdout to COM3 lands every line immediately. A normal
# '> file' redirect stays 0 bytes until the process exits, which hides
# all progress on a run that does NOT crash.
serial3=file file:C:\VWE\TeslaRel410\emulator\render-bridge\podlog.txt
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
c:
cd \REL410\BT
set VIDEOFORMAT=svga
rem production pod card init (PARAMETR.BAT:181-186): DIAGNOSE + AWEUTIL per
rem card -- AWEUTIL /S does the EMU8000 bring-up and DRAM detect the HMI SOS
rem driver relies on; skipping it left the cards uninitialized (silent).
rem aweutil /s SKIPPED for now: it verifies the AWE32 GM ROM, which the
rem emulated cards lack (hangs in a retry loop) -- restore once the ROM is
rem dumped from a real card. diagnose /s kept (passes, sets mixer config).
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
rem arena1 city mission (TESTARN.EGG: map=arena1, time=day) with the RIO
rem attached; stdout redirected so mission-load progress survives kills.
set BT_JOINTS=1
set BT_FORCE_LIMP=3
set BT_MECH_LOG=1
set BT_MAP_LOG=1
set BT_MER_LOG=1
set BT_VID_LOG=1
set BT_LAUNCH_LOG=1
set BT_STACK_LOG=1
set BT_FORCE_THROTTLE=0.6
set BT_FORCE_TURN=0.25
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r s n p
32rtm.exe -x
BTL4REC.EXE -egg testarn.egg > COM3
echo GAME-RC=%errorlevel% >> RC.TXT
32rtm.exe -u
echo ALPHA1-RUN-DONE
pause
+36 -2
View File
@@ -249,6 +249,14 @@ Mech::Mech(
deathAnimationLatched = 0;
legResetLatch = 0;
bodyResetLatch = 0;
limpModeOverride = 0;
{
const char *fl = getenv("BT_FORCE_LIMP");
if (fl != NULL && (*fl == '3' || *fl == '4'))
{
limpModeOverride = *fl - '0';
}
}
{
int i;
for (i = 0; i < AnimationSlotCount; ++i)
@@ -1242,9 +1250,35 @@ void
// dead-reckon latency fold, and the turn-in-place dispatcher (mech4).
//-----------------------------------------------------------------------
//
AdvanceLegAnimation(time_slice);
//
// The limp pick (IntegrateMotion @004ab1c8): movement modes 3/4 are the
// left/right leg limps, and while limping (and the model HAS limp clips)
// the Gimp advancers replace the normal pair -- they must, because the
// normal ones treat the limp states as their reset group.
//
// BT_FORCE_LIMP=3|4 is a DEV hook that forces the pick without touching
// the simulation state, so the limp gait can be verified before the
// damage model's limp hook (leg zone >= 0.5 -> mode 3/4) is
// reconstructed.
//
int limping;
{
Scalar stride = AdvanceBodyAnimation(time_slice, 0);
int mode = MovementMode();
limping = (mode == 3 || mode == 4) && hasGimpClips;
}
if (limping)
{
AdvanceLegAnimationGimp(time_slice);
}
else
{
AdvanceLegAnimation(time_slice);
}
{
Scalar stride = limping
? AdvanceBodyAnimationGimp(time_slice, 0)
: AdvanceBodyAnimation(time_slice, 0);
if (animationClips[5] != ResourceDescription::NullResourceID)
{
+42
View File
@@ -422,6 +422,30 @@
Scalar
AdvanceBodyAnimation(Scalar time_slice, int move_joints);
//
//--------------------------------------------------------------------
// The LIMP flavours (mech2.cpp). Selected instead of the pair above
// while the movement mode is 3 (left-leg limp) or 4 (right) and the
// model carries the limp clip set. SETTLED from the binary after two
// conflicting donor readings: the mode test is mech+0x40 -- the same
// field the damage model documents as "limp gait graphic (left 3 /
// right 4)" -- and NOT a jump-jet system.
//
// The normal advancers treat states 0x16-0x1b as the RESET group, so
// the limp flavours must be selected while limping or the limp figure
// gets its skeleton neutralized mid-cycle.
//--------------------------------------------------------------------
//
Scalar
AdvanceLegAnimationGimp(Scalar time_slice);
Scalar
AdvanceBodyAnimationGimp(Scalar time_slice, int move_joints);
Scalar
GimpLegClipFinished(Scalar carryover);
Scalar
GimpBodyClipFinished(Scalar carryover, int move_joints);
//
// The respawn heal-and-move (binary @0049fb74): reposition the SAME
// entity at the drop-zone origin, kill all motion, clear the death
@@ -712,6 +736,24 @@
{
Check(this);
}
//
// The movement mode (binary mech+0x40 == the simulation state):
// 3/4 = left/right leg limp, 5-8 = the falls/deaths, 2||9 = the
// death transition. Every gait read goes through here so the
// BT_FORCE_LIMP bring-up hook (set once in the ctor) can exercise
// the limp machinery before the damage model's limp hook exists.
//
int
MovementMode() const
{
Check(this);
return (limpModeOverride != 0)
? limpModeOverride
: (int)((Mech *)this)->GetSimulationState();
}
int limpModeOverride; // DEV: 3/4 from BT_FORCE_LIMP; 0 = off
AverageOf<Scalar> telemetryFilter[5];
CString resourceNameA;
CString resourceNameB;
+859 -2
View File
@@ -150,6 +150,21 @@ Scalar
{
Check(mech);
//
// A LIMPING mech's transitions run the limp machine instead (movement
// mode 3 = left-leg limp, 4 = right; the damage model's own "limp gait
// graphic (left 3 / right 4)"). Guarded on the clip set existing --
// LoadLocomotionClips leaves hasGimpClips 0 for a model without it.
//
{
int
mode = mech->MovementMode();
if ((mode == 3 || mode == 4) && mech->hasGimpClips)
{
return mech->GimpLegClipFinished(carryover);
}
}
//
// The binary reads subsystemArray[0] -- the roster's controls-mapper slot.
// A mech without one (a replicant) reads zero and idles, which is right.
@@ -339,6 +354,15 @@ Scalar
{
Check(mech);
{
int
mode = mech->MovementMode();
if ((mode == 3 || mode == 4) && mech->hasGimpClips)
{
return mech->GimpBodyClipFinished(carryover, move_joints);
}
}
Scalar
cycle_rate = mech->forwardCycleRate,
time_scale = mech->globalTimeScale,
@@ -522,7 +546,7 @@ Scalar
//
if (!deathAnimationLatched)
{
switch (GetSimulationState())
switch (MovementMode())
{
case 5: SetLegAnimation(0x1c); deathAnimationLatched = 1; break;
case 6: SetLegAnimation(0x1d); deathAnimationLatched = 1; break;
@@ -778,7 +802,7 @@ Scalar
if (!deathAnimationLatched)
{
switch (GetSimulationState())
switch (MovementMode())
{
case 5: SetBodyAnimation(0x1c); deathAnimationLatched = 1; break;
case 6: SetBodyAnimation(0x1d); deathAnimationLatched = 1; break;
@@ -930,6 +954,839 @@ Scalar
return distance;
}
//
//#############################################################################
// THE LIMP MACHINES (@004a7970 leg / @004a6344 body) -- the transition
// tables that run while movement mode is 3 (left-leg limp) or 4 (right).
//
// The limp replaces ONE stride: limping left, the right stride (6) hands off
// to the left limp figure (0x16 -> the 0x18 cycle); limping right, the left
// stride (7) hands off to the right figure (0x17 -> 0x19). The other leg's
// clips keep their normal alternation, which is what makes it read as a limp
// rather than a different gait.
//
// Both machines CLAMP THEIR DEMAND while in any cycle -- the leg machine
// writes the mapper's own speedDemand cell down to the damaged side's cap,
// the body machine clamps bodyTargetSpeed -- so a limping mech cannot
// command more speed than its limp figure carries, and cannot command a
// reverse out of a forward cycle (the clamp floors at zero).
//#############################################################################
//
Scalar
Mech::GimpLegClipFinished(Scalar carryover)
{
Check(this);
MechControlsMapper
*mapper = (MechControlsMapper *)subsystemArray[0];
Check_Pointer(mapper);
int
mode = MovementMode(),
state = (int)legStateAlarm.GetLevel();
//
// The demand clamp (see the block comment above).
//
if (
state == 6 || state == 7 || state == 0xc || state == 0xd ||
state == 0x12 || state == 0x13
)
{
Scalar
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
demand = mapper->GetSpeedDemand();
if (demand > cap)
{
demand = cap;
}
if (demand < 0.0f)
{
demand = 0.0f;
}
mapper->SetSpeedDemand(demand);
}
Scalar
demand = mapper->GetSpeedDemand(),
cycle_rate = forwardCycleRate,
time_scale = globalTimeScale,
cycle = legCycleSpeed;
int
plain_next;
switch (state)
{
case 2:
legStateAlarm.SetLevel(1);
return 0.0f;
case 3: case 8: case 9: case 0x14: case 0x15: case 0x1a: case 0x1b:
case 4:
case 0x20:
legStateAlarm.SetLevel(0);
return 0.0f;
//
// The right-stride walk family. Continuing hands off to the LEFT
// stride when the RIGHT leg is the good one (mode 4), and to the left
// limp figure when the left leg is the bad one (mode 3).
//
case 5: case 6: case 0xe:
if (
demand < standSpeed &&
(cycle - cycle_rate * carryover) < standSpeed
)
{
plain_next = 9;
break;
}
if (
demand > walkStrideLength &&
(cycle + cycle_rate * carryover) > walkStrideLength
)
{
plain_next = 0xb;
break;
}
SetLegAnimation((mode == 4) ? 7 : 0x16);
return legAnimation.Advance(
carryover * cycle * time_scale / walkStrideLength, 1);
case 7: case 0xf:
if (
demand < standSpeed &&
(cycle - cycle_rate * carryover) < standSpeed
)
{
plain_next = 8;
break;
}
if (
demand > walkStrideLength &&
(cycle + cycle_rate * carryover) > walkStrideLength
)
{
plain_next = 10;
break;
}
SetLegAnimation((mode == 3) ? 6 : 0x17);
return legAnimation.Advance(
carryover * cycle * time_scale / walkStrideLength, 1);
case 10: case 0xc:
if (
demand < reverseSpeedMax &&
(cycle - cycle_rate * carryover) < reverseSpeedMax
)
{
plain_next = 0xf;
break;
}
SetLegAnimation(0xd);
return legAnimation.Advance(
carryover * cycle * time_scale / reverseStrideLength, 1);
case 0xb: case 0xd:
if (
demand < reverseSpeedMax &&
(cycle - cycle_rate * carryover) < reverseSpeedMax
)
{
plain_next = 0xe;
break;
}
SetLegAnimation(0xc);
return legAnimation.Advance(
carryover * cycle * time_scale / reverseStrideLength, 1);
case 0x10: case 0x12:
if (
demand > gimpSpeedMax &&
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
)
{
plain_next = 0x15;
break;
}
SetLegAnimation(0x13);
{
Scalar
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return legAnimation.Advance(cycle_time, 1);
}
case 0x11: case 0x13:
if (
demand > gimpSpeedMax &&
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
)
{
plain_next = 0x14;
break;
}
SetLegAnimation(0x12);
{
Scalar
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return legAnimation.Advance(cycle_time, 1);
}
//
// The limp figures themselves: keep cycling while the demand holds,
// exit toward standing when it drops.
//
case 0x16: case 0x18:
if (
demand < gimpLeftSpeedMax &&
(cycle - cycle_rate * carryover) < gimpLeftSpeedMax
)
{
plain_next = 0x1a;
break;
}
SetLegAnimation(0x18);
return legAnimation.Advance(
carryover * cycle * time_scale / gimpLeftStrideLength, 1);
case 0x17: case 0x19:
if (
demand < gimpRightSpeedMax &&
(cycle - cycle_rate * carryover) < gimpRightSpeedMax
)
{
plain_next = 0x1b;
break;
}
SetLegAnimation(0x19);
return legAnimation.Advance(
carryover * cycle * time_scale / gimpRightStrideLength, 1);
default:
return 0.0f;
}
//
// The plain tail every non-cycle exit lands in.
//
SetLegAnimation(plain_next);
return legAnimation.Advance(carryover * time_scale, 1);
}
Scalar
Mech::GimpBodyClipFinished(Scalar carryover, int move_joints)
{
Check(this);
int
mode = MovementMode(),
state = (int)bodyStateAlarm.GetLevel();
if (
state == 6 || state == 7 || state == 0xc || state == 0xd ||
state == 0x12 || state == 0x13
)
{
Scalar
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax;
if (bodyTargetSpeed > cap)
{
bodyTargetSpeed = cap;
}
if (bodyTargetSpeed < 0.0f)
{
bodyTargetSpeed = 0.0f;
}
}
Scalar
demand = bodyTargetSpeed,
cycle_rate = forwardCycleRate,
time_scale = globalTimeScale,
cycle = bodyCycleSpeed;
int
plain_next;
switch (state)
{
case 2:
bodyStateAlarm.SetLevel(1);
return 0.0f;
case 3: case 8: case 9: case 0x14: case 0x15: case 0x1a: case 0x1b:
case 4:
case 0x20:
bodyStateAlarm.SetLevel(0);
return 0.0f;
case 5: case 6: case 0xe:
if (
demand < standSpeed &&
(cycle - cycle_rate * carryover) < standSpeed
)
{
plain_next = 9;
break;
}
if (
demand > walkStrideLength &&
(cycle + cycle_rate * carryover) > walkStrideLength
)
{
plain_next = 0xb;
break;
}
SetBodyAnimation((mode == 4) ? 7 : 0x16);
return bodyAnimation.Advance(
carryover * cycle * time_scale / walkStrideLength, move_joints);
case 7: case 0xf:
if (
demand < standSpeed &&
(cycle - cycle_rate * carryover) < standSpeed
)
{
plain_next = 8;
break;
}
if (
demand > walkStrideLength &&
(cycle + cycle_rate * carryover) > walkStrideLength
)
{
plain_next = 10;
break;
}
SetBodyAnimation((mode == 3) ? 6 : 0x17);
return bodyAnimation.Advance(
carryover * cycle * time_scale / walkStrideLength, move_joints);
case 10: case 0xc:
if (
demand < reverseSpeedMax &&
(cycle - cycle_rate * carryover) < reverseSpeedMax
)
{
plain_next = 0xf;
break;
}
SetBodyAnimation(0xd);
return bodyAnimation.Advance(
carryover * cycle * time_scale / reverseStrideLength, move_joints);
case 0xb: case 0xd:
if (
demand < reverseSpeedMax &&
(cycle - cycle_rate * carryover) < reverseSpeedMax
)
{
plain_next = 0xe;
break;
}
SetBodyAnimation(0xc);
return bodyAnimation.Advance(
carryover * cycle * time_scale / reverseStrideLength, move_joints);
case 0x10: case 0x12:
if (
demand > gimpSpeedMax &&
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
)
{
plain_next = 0x15;
break;
}
SetBodyAnimation(0x13);
{
Scalar
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return bodyAnimation.Advance(cycle_time, move_joints);
}
case 0x11: case 0x13:
if (
demand > gimpSpeedMax &&
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
)
{
plain_next = 0x14;
break;
}
SetBodyAnimation(0x12);
{
Scalar
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return bodyAnimation.Advance(cycle_time, move_joints);
}
case 0x16: case 0x18:
if (
demand < gimpLeftSpeedMax &&
(cycle - cycle_rate * carryover) < gimpLeftSpeedMax
)
{
plain_next = 0x1a;
break;
}
SetBodyAnimation(0x18);
return bodyAnimation.Advance(
carryover * cycle * time_scale / gimpLeftStrideLength, move_joints);
case 0x17: case 0x19:
if (
demand < gimpRightSpeedMax &&
(cycle - cycle_rate * carryover) < gimpRightSpeedMax
)
{
plain_next = 0x1b;
break;
}
SetBodyAnimation(0x19);
return bodyAnimation.Advance(
carryover * cycle * time_scale / gimpRightStrideLength, move_joints);
default:
return 0.0f;
}
SetBodyAnimation(plain_next);
return bodyAnimation.Advance(carryover * time_scale, move_joints);
}
//
//#############################################################################
// THE LIMP ADVANCERS (@004a71f4 leg / @004a5bf8 body). Selected instead of
// the normal pair while limping, and structurally different in exactly the
// ways a limp needs:
//
// * States 0x16-0x1b are LIVE here -- the limp entries advance in the
// plain group and the 0x18/0x19 figures get their own slewed cycles with
// the damaged side's caps. (The NORMAL advancers treat those states as
// the reset group, which is why these flavours must be selected while
// limping -- the normal one would neutralize the figure mid-cycle.)
//
// * No death latch and no wind-down: the movement modes are exclusive, so
// a limping mech is by definition not falling.
//
// * The same demand clamp as the transition machines, applied per frame.
//#############################################################################
//
Scalar
Mech::AdvanceLegAnimationGimp(Scalar time_slice)
{
Check(this);
MechControlsMapper
*mapper = (MechControlsMapper *)subsystemArray[0];
Check_Pointer(mapper);
int
mode = MovementMode(),
state = (int)legStateAlarm.GetLevel();
if (
state == 6 || state == 7 || state == 0xc || state == 0xd ||
state == 0x12 || state == 0x13
)
{
Scalar
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
clamped = mapper->GetSpeedDemand();
if (clamped > cap)
{
clamped = cap;
}
if (clamped < 0.0f)
{
clamped = 0.0f;
}
mapper->SetSpeedDemand(clamped);
}
Scalar
demand = mapper->GetSpeedDemand(),
distance = 0.0f;
switch (legStateAlarm.GetLevel())
{
case 0:
if (demand <= standSpeed)
{
break;
}
SetLegAnimation(5);
// fall through
case 2: case 3: case 5: case 8: case 9: case 10: case 0xb:
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20:
advance_clip:
distance = legAnimation.Advance(
time_slice * globalTimeScale * idleStrideScale, 1);
legCycleSpeed = distance / time_slice;
break;
case 1:
break;
case 4:
if (standSpeed < demand)
{
legStateAlarm.SetLevel(0);
ForceUpdate(8);
break;
}
goto advance_clip;
case 6: case 7:
if (demand > legCycleSpeed)
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > walkStrideLength)
{
legCycleSpeed = walkStrideLength;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= forwardCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < standSpeed)
{
legCycleSpeed = standSpeed;
}
}
distance = legAnimation.Advance(
time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale,
1);
break;
case 0xc: case 0xd:
if (demand > legCycleSpeed)
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > runSpeedMax)
{
legCycleSpeed = runSpeedMax;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= forwardCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < reverseSpeedMax)
{
legCycleSpeed = reverseSpeedMax;
}
}
distance = legAnimation.Advance(
time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale,
1);
break;
case 0x12: case 0x13:
if (demand > legCycleSpeed)
{
legCycleSpeed += gimpCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > gimpSpeedMax)
{
legCycleSpeed = gimpSpeedMax;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= gimpCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < gimpStrideLength)
{
legCycleSpeed = gimpStrideLength;
}
}
{
Scalar
ratio = legCycleSpeed / gimpStrideLength;
if (ratio <= 0.0f)
{
ratio = -ratio;
}
distance = legAnimation.Advance(
ratio * time_slice * globalTimeScale, 1);
}
break;
//
// The limp figures: slewed like a walk, inside the damaged side's caps.
//
case 0x18: case 0x19:
{
Scalar
speed_cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
stride = (mode == 3) ? gimpLeftStrideLength : gimpRightStrideLength;
if (demand > legCycleSpeed)
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > stride)
{
legCycleSpeed = stride;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= forwardCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < speed_cap)
{
legCycleSpeed = speed_cap;
}
}
distance = legAnimation.Advance(
time_slice * (legCycleSpeed / stride) * globalTimeScale, 1);
}
break;
default:
Fail("Unsupported mech animation");
}
return distance;
}
Scalar
Mech::AdvanceBodyAnimationGimp(Scalar time_slice, int move_joints)
{
Check(this);
int
mode = MovementMode(),
state = (int)bodyStateAlarm.GetLevel();
if (
state == 6 || state == 7 || state == 0xc || state == 0xd ||
state == 0x12 || state == 0x13
)
{
Scalar
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax;
if (bodyTargetSpeed > cap)
{
bodyTargetSpeed = cap;
}
if (bodyTargetSpeed < 0.0f)
{
bodyTargetSpeed = 0.0f;
}
}
Scalar
demand = bodyTargetSpeed,
distance = 0.0f;
switch (bodyStateAlarm.GetLevel())
{
case 0:
if (demand <= standSpeed)
{
break;
}
SetBodyAnimation(5);
// fall through
case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0xb:
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20:
distance = bodyAnimation.Advance(
time_slice * globalTimeScale * idleStrideScale, move_joints);
bodyCycleSpeed = distance / time_slice;
break;
case 1:
break;
case 6: case 7:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > walkStrideLength)
{
bodyCycleSpeed = walkStrideLength;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < standSpeed)
{
bodyCycleSpeed = standSpeed;
}
}
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale,
move_joints);
break;
case 0xc: case 0xd:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > runSpeedMax)
{
bodyCycleSpeed = runSpeedMax;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < reverseSpeedMax)
{
bodyCycleSpeed = reverseSpeedMax;
}
}
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale,
move_joints);
break;
case 0x12: case 0x13:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += gimpCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > gimpSpeedMax)
{
bodyCycleSpeed = gimpSpeedMax;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= gimpCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < gimpStrideLength)
{
bodyCycleSpeed = gimpStrideLength;
}
}
{
Scalar
ratio = bodyCycleSpeed / gimpStrideLength;
if (ratio <= 0.0f)
{
ratio = -ratio;
}
distance = bodyAnimation.Advance(
ratio * time_slice * globalTimeScale, move_joints);
}
break;
case 0x18: case 0x19:
{
Scalar
speed_cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
stride = (mode == 3) ? gimpLeftStrideLength : gimpRightStrideLength;
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > stride)
{
bodyCycleSpeed = stride;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < speed_cap)
{
bodyCycleSpeed = speed_cap;
}
}
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / stride) * globalTimeScale,
move_joints);
}
break;
default:
Fail("Unsupported mech animation");
}
return distance;
}
//
//#############################################################################
// @004a7f50 -- prefix + suffix -> the clip's resource ID.
+53 -5
View File
@@ -1,10 +1,10 @@
# MECH2.CPP — reconstruction notes
**Status: THE GAIT IS LIVE (2026-08-03). Transition machine + clip loader +
the two GROUND `Advance*` entry points reconstructed and wired into
`Mech::Simulate`; first live run showed 16 of 22 joints animating in paired
left/right strides. Still deferred: the two AIRBORNE `Advance*` flavours and
the `Gimp*ClipFinished` limp machines — see "What is deferred".**
**Status: ALL TWELVE FUNCTIONS RECONSTRUCTED (2026-08-03) — the gait AND the
limp are live-verified. The walk shows 16 of 22 joints in paired strides; the
forced limp breaks the pair symmetry exactly as it should (drag-leg joint at
18 poses vs its partner's 55) with the hull lurching at 1023 under a 26.9
demand.**
`mech2.cpp` is the mech's gait: which walk clip is playing, when it changes,
and to what. It sits between the locomotion demand and the clip player:
@@ -321,3 +321,51 @@ dispatcher. One correction queued by the raw decomp: `+0x598` is written by a
VECTOR op in IntegrateMotion's replicant branch (FUN_00408644 on floats), so
`motionEventName` is likely a Point3D, not a CString — revisit when that
branch is reconstructed.
## The limp, settled and live (2026-08-03)
The "gimp" family had two conflicting donor readings — limp vs jump-jet. The
binary settles it: **limp**. The mode test in every gimp function is
`mech+0x40` ∈ {3, 4}, the same values the damage model documents as "limp
gait graphic (left 3 / right 4)"; there is no jump-jet control anywhere in
the cockpit; and the donor's own port routing (graphic-alarm 3/4) was reading
the same field under a different name. The donor's "run-jump clip"
annotations in its mech3 member map were its misreading, which is why its
mech2 header said "airborne/jump-jet" — recorded so nobody re-litigates it.
**How the limp actually works** (from @004a7970/@004a6344 + the advancers
@004a71f4/@004a5bf8, all four now reconstructed):
* The limp replaces ONE stride. Limping left, the right stride (6) hands off
to the left limp figure (0x16 → the self-cycling 0x18); the other leg's
clips keep their normal alternation. That asymmetry is what reads as a
limp rather than a different gait.
* Both machines CLAMP THEIR DEMAND while in a cycle — the leg machine writes
the mapper's own speedDemand cell down to the damaged side's cap (hence
`MechControlsMapper::SetSpeedDemand`), the body machine clamps
bodyTargetSpeed, and both floor at zero. A limping mech cannot command
more speed than its figure carries, nor reverse out of a forward cycle.
* The limp flavours of Advance* keep states 0x160x1b LIVE (the normal
flavours treat them as the reset group — which is precisely why the limp
flavours must be selected while limping, or the figure gets neutralized
mid-cycle). They carry no death latch and no wind-down: movement modes are
exclusive, a limping mech is not falling.
* All movement-mode reads go through `Mech::MovementMode()` (mech+0x40 ==
the simulation state), which also honours the `BT_FORCE_LIMP=3|4` dev hook
(set once in the ctor) so the machinery can be exercised before the damage
model's limp hook exists.
**Verification, two runs:**
* No-regression: the normal mission's speed sequence is BIT-IDENTICAL to
5.3.95 (7.31972, 26.6726, 22.1601, …) — the branch costs nothing when not
limping.
* Forced left limp (`pod_render_limp.conf`): demand stays 26.9, hull lurches
at 1023; and on the wire the healthy run's tight pose-count pairs
(650/649 … 434/421) break to a 3× asymmetry — the drag-leg joint at 18
poses against its partner's 55. No fault, no "Unsupported mech animation".
**Still open in this family:** the damage hook itself (leg zone ≥ 0.5 →
mode 3/4 — a MECHDMG increment; nothing sets the mode in real play yet), and
IntegrateMotion's remaining pieces (orientation/velocity integration, the
dead-reckon fold, turn-in-place arming).
+10
View File
@@ -149,6 +149,16 @@
Scalar
GetTurnDemand() const { Check(this); return turnDemand; }
//
// The LIMP machines write the demand cell DOWN to the damaged side's
// speed cap (binary GimpLegClipFinished @004a7970 preamble writes
// mapper+0x128 directly) -- a limping mech cannot command more speed
// than its limp figure carries.
//
void
SetSpeedDemand(Scalar demand)
{ Check(this); speedDemand = demand; }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local Data -- the published control inputs (the streamed mappings write
// these; InterpretControls, phase 5.3, reads them to drive the mech).