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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 10–23 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 0x16–0x1b 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 10–23; 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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user