BT410 5.3.121: the mode alarm comes home -- the decomp had the master perf all along, and the damage code writes the mode directly

The 'never decompiled' ledger note was wrong: part_013.c:3059 carries the
ENTIRE master performance @004a9b5c as plain C (and @004ab430 is the full
replicant perf; IntegrateMotion sits between them).  Reading it end to end
rewrote the movement-mode model:

- mech+0x2c is the entity MOVEMENT-MODE AlarmIndicator; +0x40 is its LEVEL
  cell -- everything previously read as 'raw simulationState' is this
  alarm.  MECHDMG writes it DIRECTLY: half-gone right leg -> 4 GimpRight
  @0x49c8fb, left -> 3 GimpLeft @0x49c926, leg destroyed while limping ->
  9 @0x49c88f/@0x49c8c5, vital -> 9 @0x49c83c, gated by the real
  InDeathTransition.  The staged statusAlarm->mode 'promotion' was a
  misconstruction -- retired; statusAlarm@0x714 is only the body-graphic
  clip selector.
- The master perf's block 7 is three STREAMED mode-request counters
  (+0x334 DefaultState / +0x338 GimpLeft / +0x33c GimpRight -- authentic
  names, traced as consumed), fed only by creation/update records; landed
  with ctor/Reset zeroes and the Reset mode drop.
- The duck driver moved to its authentic slot (counters -> duck -> limp
  standup -> volume swap -> advance) and its gate is renamed
  mobilityScale: +0x79c is the MAX LEG EFFECTIVENESS over the +0x7ac
  roster (scales throttle, zeroes turn, gates duck), not a crouch analog.
- The advance dispatch is death-guarded (InDeathTransition before the
  gimp/normal pick) -- and with mode 9 finally written on kills,
  InDeathTransition and the camera director's death cut-away are LIVE.

Corrected identities recorded in MECH4.NOTES.md: +0x3f4 = REVERSE/BRAKE
flag (not airborne) with fwd/rev accel pick and gyro lurch kicks; 5-notch
throttle quantizer; RealMaxSpeed(0x15) one-shot send; @0049fa1c =
combat-effectiveness census -> +0x414; @0049fe80 = damage averages ->
+0x354/8/c; block 8 = move/collide with self-dispatched TakeDamage
collision damage + stagger clip 0x20; the instability accumulator and the
five ring buffers.

Soaks: duck cycles green from the new slot (3 full cycles, crash-sig 0).
The lifecycle kill-cycle conf crashes -- PROVEN PRE-EXISTING by a stashed
control build (identical 0xFC fault, EIP moves with the relink); forensics
in MISSILE.NOTES.md, broke unnoticed somewhere in 5.3.26..120.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-06 19:17:03 -05:00
co-authored by Claude Fable 5
parent 6b9811473b
commit 6da282738c
6 changed files with 359 additions and 102 deletions
+120 -78
View File
@@ -302,7 +302,12 @@ Mech::Mech(
//
duckRequestLatch = 0;
duckPhaseRequest = 0;
duckDemand = 0.0f;
mobilityScale = 1.0f; // [T1] held at "legs healthy" until the
// +0x7ac leg-roster scan lands; the
// binary re-derives it every frame
defaultStateRequests = 0; // the streamed mode-request counters
gimpLeftRequests = 0; // (binary ctor @004a1674 zeroes
gimpRightRequests = 0; // +0x334/+0x338/+0x33c)
collisionVolumeState.Initialize(2);
collisionVolumeState.SetLevel(1);
standingVolumeHeight = 0.0f;
@@ -1260,6 +1265,16 @@ void
currentBodySpeed = 0.0f;
bodyTargetSpeed = 0.0f;
//
// The binary Reset (@0049fb74): the movement-mode alarm drops to
// DefaultState (FUN_0041bbd8(mech+0x2c, 0)) and the streamed mode
// request counters clear with it (+0x334/+0x338/+0x33c = 0).
//
SetSimulationState(0);
defaultStateRequests = 0;
gimpLeftRequests = 0;
gimpRightRequests = 0;
statusAlarm.SetLevel(0);
deathTransitionDone = 0;
@@ -1658,99 +1673,76 @@ void
// reconstructed.
//
//
// STAGED (the master performance's job, mech4 @004a9b5c -- never
// decompiled): promote the damage model's limp signal (statusAlarm level
// 3/4, MECHDMG's leg-half write to mech+0x2c) into the movement mode the
// gait machinery keys on. The binary's damage code READS mode 3/4 to
// decide "already limping", so the promotion demonstrably exists; the
// master perf is the only unaccounted-for writer. Guarded so a death
// (mode 5+) is never overwritten.
// BT_FORCE_DUCK=1: one authentic duck press every ~4s on its own
// clock (independent of the one-shot button harness below) -- the
// DuckRequest message, plus the held cell and analog demand every
// frame. Press one ducks, the next (from the hold) stands: the
// full-machine cycle soak.
//
{
unsigned status = statusAlarm.GetLevel();
int mode = MovementMode();
if ((status == 3 || status == 4) && mode < 3)
static int s_forceDuck = -1;
static Scalar s_duckClock = 0.0f;
if (s_forceDuck < 0)
{
SetSimulationState(status);
s_forceDuck = (getenv("BT_FORCE_DUCK") != NULL) ? 1 : 0;
}
}
int limping;
{
int mode = MovementMode();
limping = (mode == 3 || mode == 4) && hasGimpClips;
}
//
// The master performance clears the leg reset latch at the top of every
// frame (@0x4a9bff); the wind-down inside the advancer may set it again,
// and the turn-in-place dispatcher below honours it -- so a walk that
// wound down THIS frame cannot re-enter as a turn until the next.
//
legResetLatch = 0;
//
// BT_FORCE_DUCK=1: one authentic duck press every ~4s on its own
// clock (independent of the one-shot button harness below) -- the
// DuckRequest message, plus the held cell and analog demand every
// frame. Press one ducks, the next (from the hold) stands: the
// full-machine cycle soak.
//
if (s_forceDuck)
{
static int s_forceDuck = -1;
static Scalar s_duckClock = 0.0f;
if (s_forceDuck < 0)
MechControlsMapper
*pulse_mapper = (MechControlsMapper *)subsystemArray[0];
if (pulse_mapper != NULL)
{
s_forceDuck = (getenv("BT_FORCE_DUCK") != NULL) ? 1 : 0;
pulse_mapper->duckCommand = 1;
}
if (s_forceDuck)
mobilityScale = 1.0f; // hold the legs-healthy gate open
s_duckClock += time_slice;
if (s_duckClock >= 4.0f)
{
MechControlsMapper
*pulse_mapper = (MechControlsMapper *)subsystemArray[0];
if (pulse_mapper != NULL)
{
pulse_mapper->duckCommand = 1;
}
duckDemand = 1.0f;
s_duckClock += time_slice;
if (s_duckClock >= 4.0f)
{
s_duckClock = 0.0f;
ReceiverDataMessageOf<int>
duck_press(
Mech::DuckRequestMessageID,
sizeof(ReceiverDataMessageOf<int>),
1);
Dispatch(&duck_press);
}
s_duckClock = 0.0f;
ReceiverDataMessageOf<int>
duck_press(
Mech::DuckRequestMessageID,
sizeof(ReceiverDataMessageOf<int>),
1);
Dispatch(&duck_press);
}
}
if (limping)
{
AdvanceLegAnimationGimp(time_slice);
}
else
{
AdvanceLegAnimation(time_slice);
}
//
// The gyro's joint writes (binary master-perf tail @0x4aaf74/83, AFTER
// the animation pass): the idle sway onto the EyeJoint, and the
// integrated eye offset + body tip onto 'jointeye' -- the joint the
// cockpit eyepoint rides. Gated off during the death clips exactly as
// the tail is.
// THE MODE-REQUEST COUNTERS (master perf @004a9b5c -- the function IS
// in the BT411 decomp after all, part_013.c:3059; the old "never
// decompiled" note was wrong). Three STREAMED request counters
// (+0x334/+0x338/+0x33c); while one is positive the master perf
// re-asserts the movement-mode alarm every frame, tracing the
// authentic state name to the debug stream. They are fed only by the
// network stream appliers (creation record +0x1c0.., update records);
// the LOCAL damage path writes the mode directly (MECHDMG,
// @0x49c8fb/@0x49c926 half-leg limps, @0x49c88f/@0x49c8c5/@0x49c83c
// kills) -- the staged statusAlarm->mode "promotion" that lived here
// was a misconstruction and is retired.
//
if (gyroSubsystem != NULL && MovementMode() < 5)
if (defaultStateRequests > 0)
{
((Gyroscope *)gyroSubsystem)->WriteEyeJoint();
((Gyroscope *)gyroSubsystem)->WriteMechJoint();
DEBUG_STREAM << "DefaultState" << endl << flush;
SetSimulationState(0);
}
if (gimpLeftRequests > 0)
{
DEBUG_STREAM << "GimpLeft" << endl << flush;
SetSimulationState(3);
}
if (gimpRightRequests > 0)
{
DEBUG_STREAM << "GimpRight" << endl << flush;
SetSimulationState(4);
}
//
// THE DUCK DRIVER (master perf @004a9f61..@004aa155, disasm-decoded).
// THE DUCK DRIVER (master perf @004a9f61..@004aa155; 5.3.121 confirmed
// line-by-line against the decomp C, part_013.c:3059, and MOVED here --
// the binary runs it right after the mode-request counters, BEFORE the
// leg advance, so a duck arm is seen by the same frame's advance).
//
// Phase pick, every frame: not standing (raw state != 0) or duck button
// released -> no request. Standing with the button held: from gait
@@ -1792,8 +1784,12 @@ void
leg_state = (int)legStateAlarm.GetLevel();
if (leg_state == 0 || leg_state == 4)
{
//
// The binary gate here is |mobilityScale| > 1e-4: "the
// legs still work" (see MECH.HPP) -- NOT a crouch analog.
//
duckPhaseRequest =
(duckDemand <= 0.0001f && duckDemand >= -0.0001f) ? 0 : 1;
(mobilityScale <= 0.0001f && mobilityScale >= -0.0001f) ? 0 : 1;
}
else if (leg_state == 1)
{
@@ -1893,9 +1889,55 @@ void
collisionVolumeState.SetLevel(collisionVolumeState.GetLevel());
}
int limping;
{
int mode = MovementMode();
limping = (mode == 3 || mode == 4) && hasGimpClips;
}
//
// The master performance clears the leg reset latch at the top of every
// frame (@0x4a9bff); the wind-down inside the advancer may set it again,
// and the turn-in-place dispatcher below honours it -- so a walk that
// wound down THIS frame cannot re-enter as a turn until the next.
//
legResetLatch = 0;
//
// DEATH-GUARDED (master perf: FUN_0049fb54 right before the pick) -- a
// mech in the death transition (modes 2/9) does not advance the leg
// gait; the crash clips run on the body machine's clock. (The binary
// leaves its advance-distance stack slot stale in that case; we skip.)
//
if (!Mech::InDeathTransition(this))
{
if (limping)
{
AdvanceLegAnimationGimp(time_slice);
}
else
{
AdvanceLegAnimation(time_slice);
}
}
//
// The gyro's joint writes (binary master-perf tail @0x4aaf74/83, AFTER
// the animation pass): the idle sway onto the EyeJoint, and the
// integrated eye offset + body tip onto 'jointeye' -- the joint the
// cockpit eyepoint rides. Gated off during the death clips exactly as
// the tail is.
//
if (gyroSubsystem != NULL && MovementMode() < 5)
{
((Gyroscope *)gyroSubsystem)->WriteEyeJoint();
((Gyroscope *)gyroSubsystem)->WriteMechJoint();
}
//
// THE TURN-IN-PLACE DISPATCHER (master perf @0x4aa505-0x4aa588, decoded
// from raw disasm -- Ghidra never decompiled the function). From
// from raw disasm; 5.3.121: confirmed against the decomp C, which DOES
// carry the master perf at part_013.c:3059). From
// Standing, arm the trn clip when the mech is TURNING (the binary tests
// |angularVelocity| > 1e-4; at a standstill the turn rate is exactly
// walkingTurnRate, so the operand is turnDemand * walkingTurnRate), the
+32 -5
View File
@@ -673,11 +673,19 @@
TakeDamageMessageHandler(TakeDamageMessage *message);
//
// Damage-side death flag: the body graphic alarm (statusAlarm,
// binary @0x714) at level >= 9 = the death/fall state.
// Damage-side death flag: the MOVEMENT MODE (the entity mode alarm,
// binary mech+0x2c, level cell +0x40 -- entity-base machinery, the
// engine seeds it from the class descriptor @0041bdf0) at level >= 9
// = killed. 5.3.121 CORRECTION: the old claim that the damage code
// wrote the @0x714 body-graphic alarm was a mis-anchor; the binary
// TakeDamage kill writes are FUN_0041bbd8(mech+0x2c, 9) at
// @0x49c88f/@0x49c8c5/@0x49c83c. Death-SEQUENCE membership is the
// narrower Mech::InDeathTransition (modes 2/9, @0049fb54); the fall
// variants 5-8 sit between (armed as crash clips by the body
// machine) -- their writer is still undecoded.
//
Logical
IsMechDestroyed() { Check(this); return statusAlarm.GetLevel() >= 9; }
IsMechDestroyed() { Check(this); return (int)GetSimulationState() >= 9; }
//
// The live torso twist (the cylinder table's rotate-with-torso rows
@@ -914,7 +922,13 @@
// recomputed every frame from the raw simulation state, the
// mapper's duck button cell, the leg gait state and the duck
// analog demand (deadzone 1e-4 @0x4ab16c).
// duckDemand (+0x79c) the analog crouch input.
// mobilityScale (+0x79c) NOT an analog input (5.3.121, from
// the master perf C): recomputed each frame as the MAX leg
// effectiveness (item +0x31c) over the +0x7ac leg roster,
// it scales the throttle command, zeroes the turn command
// when ~0, and the duck-down pick reads it as the "legs
// still work" gate. Our leg-roster scan is not
// reconstructed yet, so the cell HOLDS 1.0 -- [T1] staging.
// collisionVolumeState (+0x4c4) the volume alarm: level 1 =
// STANDING, 0 = DUCKED. On an edge the collision template's
// maxY (BoxedSolid +0xc -- the box TOP; the ground probe's
@@ -926,11 +940,24 @@
//
int duckRequestLatch;
int duckPhaseRequest;
Scalar duckDemand;
Scalar mobilityScale;
AlarmIndicator collisionVolumeState;
Scalar standingVolumeHeight;
Scalar duckedVolumeHeight;
//
// The streamed movement-mode request counters (binary +0x334
// DefaultState / +0x338 GimpLeft / +0x33c GimpRight -- the
// authentic state names, traced by the master perf as it consumes
// them). They are fed ONLY over the wire (the creation-stream
// ctor @004b3778 reads them at record +0x1c0.., the update-record
// appliers likewise); the LOCAL damage path writes the mode alarm
// directly. Zeroed by the ctor (@004a1674) and Reset (@0049fb74).
//
int defaultStateRequests;
int gimpLeftRequests;
int gimpRightRequests;
void
DuckRequestMessageHandler(Receiver::Message *message);
void
+159 -7
View File
@@ -77,10 +77,21 @@ per contact by `Mover::ProcessCollisionList`. The decomp
`ret`), hidden inside the manifest's mis-sized @004a9770 entry (that fn
is really the stream-builder's 6.6KB name→SharedData strcmp resolver,
and Ghidra swallowed everything to @004ab188 behind it). Registration
proven from the ctor decomp: `(flags & 0xC) == 4` → replicant perf
@004ab9d8 (also hidden, inside @004ab430's claimed range); else master
@004a9b5c. Recipe: extract CODE bytes (va 0x1000 → file 0xe00),
`ndisasm -b 32 -o 0x4a9b5c`.
proven from the ctor decomp: `(flags & 0xC) == 4` → replicant perf,
else master @004a9b5c. Recipe: extract CODE bytes (va 0x1000 → file
0xe00), `ndisasm -b 32 -o 0x4a9b5c`. **5.3.121 corrections: the DECOMP
has both perfs in plain C (master = part_013.c:3059, and @004ab430 IS
the complete replicant perf — my "@004ab9d8 hidden inside it" note was
its CONSTANT POOL @004ab9c8.. right after the body). Replicant perf
read (not yet landed): zero +0x1dc, IntegrateMotion(dt, flavor 1);
ret 1 (fresher update present) → quaternion slerp +0x10c toward the
update target +0x26c with t = dt/(update age + dt) + matching velocity
lerps (+0x1d0 vs +0x2a4, +0x1f4 vs +0x298); either path applies the
+0x598 error-offset vector smoothed over the @004ab9cc window while
the +0x5a4 latch holds, then position ← the DR anchor +0x260; without
flags&0x4000 it re-resolves its room via the world chain (@0040e36c on
the +0x2e8 box) → +0x2f0; same ground clamp; `FUN_00421b6c`; same five
ring publishes + acceleration tail; NO update-record writer.**
**The ordered call skeleton (111 calls)** — blocks in execution order,
matching our Simulate's structure and locating every staged gap:
@@ -106,9 +117,11 @@ per contact by `Mover::ProcessCollisionList`. The decomp
awaits a BT_MAP_LOG audit on a duck-bound RES.
4. verify-guarded state read (mech4 accessors + Fail 0x40385c)
5. gyro feed a (vec pair → gyro_b 0x4b2de4, gyro_a 0x4b2d8c)
6. death arm: InDeathTransition 0x49fb54 → the 1.9KB mech2 advancer
0x4a71f4 under it (death-clip arming)
7. **status→mode LIMP PROMOTION** (0x4a5028 + SetAnim/Mark/SetLevel)
6. death GUARD on the advance dispatch: InDeathTransition 0x49fb54;
0x4a71f4 = AdvanceLegAnimationGimp (modes 3/4 + 0x580), 0x4a5028 =
AdvanceLegAnimation -- NOT a death advancer (5.3.121 correction)
7. ~~status→mode LIMP PROMOTION~~ -> the STREAMED MODE-REQUEST COUNTERS
(see the 5.3.121 section; landed)
8. update-record block (0x421bac/0x40e5f0/0x4227e0/0x41db7c/0x422bac +
Point3D copies) — the dead-reckon/update writer
9. collision → gyro crunch (gyro feeds + alarms)
@@ -130,3 +143,142 @@ per contact by `Mover::ProcessCollisionList`. The decomp
(its replicant branch writes it with a vector subtract), so
`motionEventName` in MECH.HPP is mistyped as CString — fix when this
lands.
## 5.3.121 -- THE DECOMP HAS THE MASTER PERF (ledger correction + full C read)
**`part_013.c:3059` = `@004a9b5c file=bt/mech4.cpp` -- the ENTIRE master
performance is in the BT411 decomp as plain C.** Every "never decompiled /
raw-disasm-only" note above is superseded; the asm read (scratchpad
masterperf.asm) and the C agree line for line where both were done. Rule
for the rest of this file: read the C first, keep ndisasm for byte-level
checks. (`@004ab1c8` IntegrateMotion and `@004ab188` -- its dead-reckon
position fold -- are decompiled right below it; the replicant-perf interior
sits inside `@004ab430` from part_013:3804 with the SAME five ring
publishes at :3948.)
### The movement-mode carrier (the big one)
* **mech+0x2c is the entity MOVEMENT-MODE AlarmIndicator; +0x40 is its
LEVEL cell.** Everything we had read as "raw simulationState at +0x40"
is this alarm's level. It is entity-BASE machinery: the engine seeds it
from the class descriptor's state count (`@0041bdf0`,
`FUN_0041b9ec(entity+0x2c, *(desc+0xc))`).
* Mode roster so far: 0 DefaultState, 2/9 death transition (InDeathTransition
`@0049fb54` == 2||9 exactly), 3 GimpLeft, 4 GimpRight, 5-8 the four fall
variants (the body machine `@004a5678` head arms body clips 0x1c-0x1f from
them, one-shot latched by +0x650), 9 killed. WHO WRITES 2 and 5-8 is
still undecoded (the death FSM brick). Mode 1: seen only as "not
standing" in duck gates; identity open.
* **The damage path writes the mode DIRECTLY**: TakeDamage `@0049c690`
calls `FUN_0041bbd8(mech+0x2c, ...)` -- half-gone right leg -> 4
(`@0x49c8fb`), left -> 3 (`@0x49c926`), leg destroyed while already
limping -> 9 (`@0x49c88f`/`@0x49c8c5`), vital zone -> 9 (`@0x49c83c`),
gated by the REAL `InDeathTransition`. The old model (statusAlarm@0x714
as damage carrier + a Simulate "promotion") was a misconstruction --
RETIRED in 5.3.121; MECHDMG now writes the mode, and the @0x714 alarm is
ONLY the body-graphic clip selector (level = body clip index 0..0x20,
0x3c-stride name table at 0x50cfe8 "StandingAnimation"...).
* **Block 7 truth**: not a promotion. Three STREAMED request counters --
+0x334 DefaultState / +0x338 GimpLeft / +0x33c GimpRight (authentic
names, traced to the debug stream as consumed) -- re-assert the mode
alarm every frame they are positive. Writers: the creation-stream ctor
`@004b3778` (record +0x1c0/1c4/1c8) and the update-record appliers;
zeroed by ctor `@004a1674` and Reset `@0049fb74` (which also drops the
mode alarm to 0 and zeroes +0x328/+0x32c/+0x330 -- those three cells
still unidentified). LANDED with the authentic traces; inert until the
MP stream feed exists.
### Corrected field identities (from the C)
* **+0x79c = the MOBILITY SCALE**, not a duck analog: zeroed each frame,
then MAX of item+0x31c over the +0x7ac leg roster (count via the
iterator); throttle command (mapper+0x128) *= it; |scale| <= 1e-4 also
ZEROES the turn command (mapper+0x12c). The duck-down pick reads it as
the "legs still work" gate. Our tree HOLDS it at 1.0 ([T1]) until the
leg-roster scan is reconstructed.
* **+0x3f4 = the REVERSE/BRAKE flag** (the "airborne re-pick" note was
wrong): set when speedCmd < 0 while +0x348 (current speed) > 0, cleared
otherwise; +0x344 (the accel rate the body advancers slew by) picks
+0x5b8 forward / +0x5bc reverse accel (sentinel -1.1f @0x4ab174 = whole
system disabled; IntegrateMotion re-derives the pick at its head).
Entering reverse: MarkUpdate(0x100), gyro kick (0,0,+/-rand)@0.4 (50%
sign flip via 0x408050 rand + 0.5f compare @0x4ab170). While reversing
above the walk threshold (+0x534 < +0x348): pitch kick (0,1,0)@0.2 on a
0.4s countdown (+0x5c4) -- the braking shudder. NOT YET LANDED.
* +0x654 = the legResetLatch (saved at frame top, cleared, compared at
frame end -> MarkUpdate(8) on change). +0x584 = squat-capable,
+0x588 = turn-clip-capable, +0x580 = gimp-clip-capable.
* +0x574/+0x578 = walk/run turn rates; turn attenuation: speed >= +0x538
-> t=(speed-+0x534)/(+0x34c-+0x534), lerp A->B, t>1 -> B/t^2; clamp >=0;
x mapper turn cmd (+0x12c); ZERO while fallen (+0x650) or legState
1/2/3; result -> +0x1d4.
* +0x348 = current speed, +0x34c = max run speed, +0x350 = min(?) speed
clamp in the shortfall term, +0x7a0 = real max speed (5-notch throttle
quantizer: cmd = round(5*cmd/+0x7a0 + 0.5)/5 * +0x7a0... exact rounding
via ftol of +0.5f), +0x6b4/+0x6b8 = body-channel demand/speed pair.
* Perf head: `FUN_0049fa1c` = the combat-effectiveness census -> +0x414:
walks roster slots 2.., classID 0xBBE (generator) charge fraction
+0x12c/+0x128, 0xBC1 count with state != 1 && +0x210 != 4 (working
coolant units), RTTI-gated weapon count (excluding launchers with
+0x364 == 7); +0x414 = 1 when weapons < +0x448 min, no coolant, charge
below threshold, or limping with the duck cell 0. Consumer unknown.
* Perf head one-shot: +0x7a4 set && +0x7a8 clear -> sends the
**RealMaxSpeed message (0x15, 0x20 bytes)** carrying +0x7a0, then
latches +0x7a8 -- the network share of the computed real max speed
(pairs with the 0x15 handler @0049f604, still to land).
* Link plug at +0x418: first bound entity's distance -> +0x400, presence
-> +0x3fc (identity of the link: open).
* Mission-list y-compare -> +0x394 (the old "verify-guarded read" block):
compares *(+0x67c) against a mission-chain entry's position y when
legState not 0/1 -- identity open (in-water / off-pad flag?).
### Block 8 (move/collide) -- decoded, NOT yet landed
Ground clamp `FUN_0040e5f0(+0x2f0, pos)` (penetration -> pos.y and the
+0x2e8 box y/z); save old basis/pos; `FUN_004227e0(this,0)` volume;
+0x44c = 0; `FUN_00422bac(this, vol, dt, &oldpos, &probe)` = THE MOVE.
Outcomes on probe[1] (local_1c8): == sentinel @0x4ab180 -> restore
pos+basis, gyro kick along normal @0.4 + pitch @0.2, CollisionState alarm
(+0x450, 3 levels) -> 0; <= 0 -> alarm 0; else REAL HIT: partner
material *(partner+0x24) -> +0x4a4, world->body normal -> +0x4a8, closing
speed -(n . v) -> +0x4b4 (the CollisionMaterialType / CollisionNormal /
CollisionSpeed attribute cells), alarm <- +0x44c, STOP at old pos
(basis <- identity const @0x4e0f74), then **collision damage = a
SELF-DISPATCHED TakeDamage message (id 0x12, 100 bytes, damage type
DAT_0050b9ac, attacker = own entity id +0x184)** via vtbl+0xc; slow
(speed^2 <= @0x4ab184) -> legs to 0 + legResetLatch=1 (+ MarkUpdate(1|
0x40) if +0x464 == 1), fast -> stagger clip 0x20 + MarkUpdate(1|0x20).
After: +0x464 changed over frame -> MarkUpdate(0x80); vel.y = pos.y -
oldpos.y (raw delta); `FUN_00421b6c`; IntegrateMotion(dt, 0); dirty
thresholds (+0x768 pos-xz^2, +0x76c turn-delta, +0x770 heading-delta ->
MarkUpdate(0x10)/(1); mapper cmd != +0x6b4 -> MarkUpdate(4)).
### The tail (decoded, partially ours already)
Instability accumulator +0x3f0, THREE terms then clamp 1.0:
(|accelvec +0x82c| / +0x784 * +0x788)^2 + speed-shortfall
(clamped cmd vs +0x348, relative) * +0x78c + (legState==4 ? +0x798 : 0);
-> gyro+0x3a8 when gyro exists. Gyro churn `FUN_004b2eac(gyro, dt)` only
when legState != 0 && +0x57c == 0, then `FUN_004b34ec(gyro)` always --
both gated !fallen(+0x650). Torso::UpdateJoints(+0x438, &+0x360) gated
!fallen. `FUN_0049fe80(this)` (a second roster census -- damage-side;
decode parked). Five RING BUFFERS (FUN_0043ade4 = ring push, 0x43ae47/
0x43ae0b = ring reduces): +0x7e0<-vel.z, +0x7ec<-vel.y, +0x7f8<-vel.x,
+0x804<-turn rate, +0x810<-dt; smoothed values -> acceleration vector
+0x1dc (delta/sum-dt vs prevs +0x81c..+0x828) -> copied to +0x82c (the
instability input). `FUN_00421b2c` opens AND closes the frame (called
twice); the mobility iterator is destroyed at the very end
(`FUN_004a4ffb`).
### Landed in 5.3.121
MECHDMG kill/limp writes retargeted to the mode (SetSimulationState);
IsMechDestroyed reads the mode (>= 9); the staged promotion replaced by
the authentic counter block (+ ctor/Reset zeroes + Reset mode drop); the
duck driver moved to its authentic slot (counters -> duck -> limp standup
-> volume swap -> advance) and its gate renamed mobilityScale; the
advance dispatch death-guarded. SIDE EFFECT: `Mech::InDeathTransition`
and the camera director's cut-away are now LIVE (mode 9 is finally
written on kills) -- the director path needs a rig eyeball when a pod is
back.
+19 -7
View File
@@ -404,7 +404,12 @@ void
//
if (damageLevel >= StructureMax)
{
mech->statusAlarm.SetLevel(9);
//
// Binary @0x49c88f/@0x49c8c5: the kill is a DIRECT write of
// the movement-mode alarm (mech+0x2c) to level 9 -- 5.3.121
// carrier correction, formerly mis-routed via statusAlarm.
//
mech->SetSimulationState(9);
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[death] LEG zone " << damageZoneIndex
@@ -413,16 +418,23 @@ void
}
}
}
else if (!mech->IsMechDestroyed())
else if (!Mech::InDeathTransition(mech))
{
// half-gone leg -> the limp gait (right 4, left 3)
//
// Binary @0x49c8fb/@0x49c926: a half-gone leg writes the
// movement mode DIRECTLY (right -> GimpRight 4, left ->
// GimpLeft 3 -- the authentic state names from the master
// perf's request traces). The gate is the authentic
// FUN_0049fb54 (InDeathTransition), not the staged
// "not already destroyed".
//
if (rightLeg != 0 && damageLevel >= LegHalfStructure)
{
mech->statusAlarm.SetLevel(4);
mech->SetSimulationState(4);
}
else if (leftLeg != 0 && damageLevel >= LegHalfStructure)
{
mech->statusAlarm.SetLevel(3);
mech->SetSimulationState(3);
}
}
}
@@ -432,7 +444,7 @@ void
// Vital zone destroyed -> mech death.
//
SetDamageZoneState(BurningState);
mech->statusAlarm.SetLevel(9);
mech->SetSimulationState(9); // binary @0x49c83c: mode alarm, level 9
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[death] VITAL zone " << damageZoneIndex
@@ -666,7 +678,7 @@ void
{
if (victim->IsVitalSubsystem())
{
((Mech *)GetOwningSimulation())->statusAlarm.SetLevel(9);
((Mech *)GetOwningSimulation())->SetSimulationState(9);
}
victim->ForceCriticalFailure();
if (getenv("BT_MECH_LOG"))
+8 -5
View File
@@ -74,11 +74,14 @@ mapper) installs post-ctor, slot 1 (voltage bus) can be empty, and
roll the special-damage short (Energy hit + a Generator crit pick →
`Generator::ForceShort()`, novice-exempt), and drive failure state: a
destroyed non-leg zone runs `RecurseSegmentTable`; a destroyed LEG or
VITAL zone is the mech kill (statusAlarm level 9 = the @0x714 body-graphic
death level; `Mech::IsMechDestroyed()`); half-gone legs set the gimp
graphic (right 4 / left 3). The binary's MovementMode(3||4)/IsDisabled
gates on the leg branches are STAGED as "not already destroyed" until the
gait FSM exists.
VITAL zone is the mech kill; half-gone legs set the limp. **5.3.121
CARRIER CORRECTION**: every one of those writes goes to the MOVEMENT-MODE
alarm at mech+0x2c (level cell +0x40) -- `FUN_0041bbd8(mech+0x2c, ...)`:
right-leg-half → 4 GimpRight @0x49c8fb, left → 3 GimpLeft @0x49c926, leg
destroyed while limping → 9 @0x49c88f/@0x49c8c5, vital → 9 @0x49c83c --
NOT the @0x714 body-graphic alarm (that one's level is the body CLIP
index; damage never touches it). The leg branch gate is the real
`InDeathTransition` (@0049fb54, modes 2||9), landed to match.
- **`LODDamageRouter` (@0049c40c)**: same-attacker clustering — reuse the
previous child inside [0.25s, 10s] (hysteresis 0.33 — a binary DOUBLE,
bytes 1f85eb51b81ed53f); new attacker or stale window re-rolls
+21
View File
@@ -92,3 +92,24 @@ app task, one delete per frame, not mid-walk).
- Cluster SPLASH + explosion entity (ClassID 0x5C @004be078), world-geometry
collision (@0042291c), target-velocity intercept lead, Missile
WriteUpdateRecord (tag 0x78) for MP replication.
## OPEN DEFECT (found 5.3.121, PRE-EXISTING): lifecycle kill-cycle crash
`lifecycle2.conf` (KILLTEST.EGG, BT_SPAWN_ENEMY + BT_FORCE_FIRE +
BT_FORCE_ZONE=10) now dies ~seconds after the first SRM salvos launch,
BEFORE any [death]: `Exception 0E ... illegal address 000000FC`, faulting
instruction `mov eax,[eax+0xfc]` after `mov eax,[ebx+0x128]; mov eax,[eax]`
with ebx = THE ENEMY MECH (matches the [enemy] spawn address in OUT.TXT).
Map resolves the EIP into `Missile::MoveAndCollide` (0x4b1b54-base) -- the
seeker/thruster work is folded in there, but our LeadTarget NULL-guards
targetEntity, so the exact deref is not yet identified (mech+0x128 in OUR
layout -> a pointer whose first dword is NULL -> +0xfc).
**PROVEN PRE-EXISTING by a control run**: the identical crash (same 0xFC
address, EIP shifted 0xCC by the relink) reproduces on a build with all
5.3.121 changes stashed. The rig last ran green at 5.3.25 -- the break
landed somewhere in the gait/renderer/duck arc (5.3.26..5.3.120) and was
never noticed because nothing re-ran the kill-cycle conf. Bisect hint:
the [msl] LAUNCH lines print, so the launch path is fine; the fault is in
the per-frame chase or the mutual-fire path against the enemy mech.
Two-agreeing-runs done (both builds crash identically, deterministic).