diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index 150d462c..aefc563c 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -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 - duck_press( - Mech::DuckRequestMessageID, - sizeof(ReceiverDataMessageOf), - 1); - Dispatch(&duck_press); - } + s_duckClock = 0.0f; + ReceiverDataMessageOf + duck_press( + Mech::DuckRequestMessageID, + sizeof(ReceiverDataMessageOf), + 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 diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index da129c52..d264dc67 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -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 diff --git a/restoration/source410/BT/MECH4.NOTES.md b/restoration/source410/BT/MECH4.NOTES.md index d88d1114..f82b9a6a 100644 --- a/restoration/source410/BT/MECH4.NOTES.md +++ b/restoration/source410/BT/MECH4.NOTES.md @@ -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. diff --git a/restoration/source410/BT/MECHDMG.CPP b/restoration/source410/BT/MECHDMG.CPP index a0f43a3c..fa2415d1 100644 --- a/restoration/source410/BT/MECHDMG.CPP +++ b/restoration/source410/BT/MECHDMG.CPP @@ -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")) diff --git a/restoration/source410/BT/MECHDMG.NOTES.md b/restoration/source410/BT/MECHDMG.NOTES.md index 27a25eff..ae9593e4 100644 --- a/restoration/source410/BT/MECHDMG.NOTES.md +++ b/restoration/source410/BT/MECHDMG.NOTES.md @@ -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 diff --git a/restoration/source410/BT/MISSILE.NOTES.md b/restoration/source410/BT/MISSILE.NOTES.md index 20c78a00..3914a3e5 100644 --- a/restoration/source410/BT/MISSILE.NOTES.md +++ b/restoration/source410/BT/MISSILE.NOTES.md @@ -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).