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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user