The gyro's sway slew (@004b275c) was already reconstructed and already read the cell the master perf writes -- swayBias -- which nothing had ever written. So the mechInstability member I added to the gyro in 5.3.127 was a duplicate of that same cell, the second such mistake this arc after realMaxSpeed/runSpeedMax. The lesson repeats: grep our own tree for an existing carve before adding a member for a binary offset. Merged, and the loop closes: the instability model now publishes into swayBias, so an unstable mech shakes its pilot harder through code that was already correct and waiting for a writer. Also corrected: the binary's impaired gate has three terms where ours had two -- a gyro destroyed outright swings to the impaired sway base, not only an unpowered or heat-failed one. Verified on the rig (BT_SWAY_LOG): bias arrives, the angle slews toward base+bias and stays inside the authored band, zero faults. AND THE LOG IMMEDIATELY EARNED ITS KEEP. It showed impaired=1 for an entire clean run. Instrumenting the three terms: state=0, heat=0 -- so the new term is inert and correct -- but watchdog=0 where Ready is 4. PowerWatcher::UpdateWatch falls back to 0 when its watched link resolves NULL, and watchedLink is default-constructed and bound NOWHERE in the tree, for PowerWatcher and HeatWatcher alike. So the whole watcher family is inert: every watcher resolves NULL, every PowerWatcher reports its target unpowered, and the gyro has been sitting on its destruction-grade sway base since the sway model landed -- invisibly, until this log existed. The fix needs the streamed watch index and a real ctor bind; guessing a binding is precisely what this project forbids, so it is recorded with its evidence for the next sitting rather than improvised now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
30 KiB
MECH4 — pre-TU reconstruction notes
Status: no MECH4.CPP exists yet. The ground snap, frame rejection,
knockdown stagger and collision economy that the binary keeps in mech4's
master performance are currently RE-HOSTED inside Mech::Simulate
(MECH, 5.3.98–100) at seams marked STAGED. This file carries the decode
work for the pieces not yet written, so it survives session boundaries.
Mech::ProcessCollision (@004abb40) — decoded, not yet reconstructed
The override of the engine's protected virtual (MOVER.CPP:1354), called
per contact by Mover::ProcessCollisionList. The decomp
(part_012.c:15206–15416) reads as the BASE function plus four additions:
-
The base head, verbatim:
collisionVolume->ProcessCollision(collision, worldLinearVelocity, lastCollisionList, &damage->surfaceNormal, &penetration); on a miss NOTHING runs. Clamp penetration to the slice, ratio = pen/dt,damage->damageAmount = StaticBounce(old, dt, r, normal, &elasticity, minimumBounceSpeed, &friction)with the elasticity/friction locals seeded from the mover's coefficients. -
The Mover-family branch (owner = the collided solid's owner entity, solid+0x1c; family test FUN_0041a1a4 against the "Mover" derivation GUID @0x4e4518):
- SEPARATING-CONTACT GATE:
(ourWorldVel − other->WorldVel[vtbl+0x20]) · normal < −1e-4(_DAT_004ac044) → return, no damage, no dispatch. - If the other's classID == 0xbb9 (mech), build a TakeDamage message
(raw wire shape
{id, len, inflictor = DAT_0050b9ac global, zone −1, the Damage verbatim}) and Dispatch it AT THE OTHER — the mech-vs-mech ram. Use ourEntity::TakeDamageMessagector as the weapon path does.
- SEPARATING-CONTACT GATE:
-
The CulturalIcon branch (GUID @0x4e7038):
stopping = other->instanceFlags & 0x8000(the StoppingCollisionVolume flag, entity+0x28).- Same separating gate against a ZERO other-velocity (icons are static).
- Dispatch the crunch TakeDamage at the icon (crushable props have no zones → their base handler no-ops it, but the dispatch is real).
- If NOT stopping:
damage->damageAmount = 0.00123f(bits 0x3aa137f4) — the WALK-THROUGH SENTINEL. The caller (the response block in Simulate) must treat exactly 0.00123f as "the move stands": restore the post-snap saves, zero the amount, feed the gyro crunch (torque 0.4 along the normalized force, upward impulse 0.2 — binary @4aa81e/@4aa86c).
-
The material tail: the elasticity/friction OUT values from StaticBounce are compared against
_DAT_004ac048; setscollisionTemporaryState(+0x44c) to 1, or 2 when both match and the state was 0. The state is pushed into thecollisionStateStateIndicator once per frame by the response block (0→1 fires the impact sound). Needs the_DAT_004ac048constant identified before writing (likely the default-material coefficient — check the literal pool at 0x4ac048).
Two hazards the donor fought that DO NOT apply here
- Compounded StaticBounce reflections: our engine's
Mover::ProcessCollisionList(MOVER.CPP:1203) restoresworldLinearVelocity = initial_velocityand position PER CONTACT before each ProcessCollision call, and averages the results. The port'sframeEntryWorldVelocityeconomy guard has no counterpart hazard in this engine — do not import it. - The AlarmIndicator ODR split: a port-only problem; our tree reads
MovementMode()/ the alarms directly.
Already re-hosted in Mech::Simulate (move here when mech4.cpp exists)
| piece | binary | landed |
|---|---|---|
| ground probe + snap | @4aa630–4aa6cc | 5.3.98 |
| frame rejection + crash self-damage + knockdown | @4aa6cf–4aab0b | 5.3.99 |
| collision economy divert + distributor | @0x4a0368 → @0049ffcc | 5.3.100 (in the damage hub, where the binary has it) |
| status→mode limp promotion | master perf (un-decompiled) | 5.3.97, STAGED |
| forwardCycleRate re-pick (ground/airborne) | IntegrateMotion head | partially — the ctor sets it once; the per-frame re-pick needs +0x3f4 |
Still to decode
-
FUN_004a9b5c— the master performance. ISOLATED AND MAPPED (2026-08-04): one single 5,676-byte body @004a9b5c..@004ab168 (oneret), 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, 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:
- controls/demand intake (mech.cpp helpers 0x4a4eab/0x4a4ee9/0x4a4f9e)
- three cerr+SetLevel triplets — illegal-state alarm guards
- THE DUCK DRIVER — DECODED + LANDED 5.3.120, live-verified (BT_FORCE_DUCK cycles down/hold/up continuously on the rig). The full system: DuckRequest message 0x1A (authentic name from the binary's handler table; roster = RealMaxSpeed 0x15, BalanceCoolant 0x16, SetBurningState 0x17, ClearBurningState 0x18, EjectPilot 0x19, DuckRequest 0x1A) latches one evaluation (+0x398, one-shot, Reset clears); the phase machine reads the RAW simulation state, the mapper's duck cell (+0x25c) and the analog demand (+0x79c, deadzone 1e-4 @0x4ab16c): gait 0/4 + input -> down (LEG slot 2, sqd), gait 1 -> up (slot 3, squ); limp modes 3/4 force stand-up; the collisionVolumeState alarm (+0x4c4; 1=standing, 0=ducked; "Whoa! Bad Collision Volume State!" guard, line 417) swaps the collision template's maxY (ExtentBox +0xc == the box TOP; the ground probe's minY untouched) between the ctor-captured height and 0.6x it (the double @004a2d38); duckState publishes for the cockpit selector. Still open here: the two MarkUpdate masks (8 then 1) ride our ForceUpdate no-op, and the mapper duck cell's streamed binding id awaits a BT_MAP_LOG audit on a duck-bound RES.
- verify-guarded state read (mech4 accessors + Fail 0x40385c)
- gyro feed a (vec pair → gyro_b 0x4b2de4, gyro_a 0x4b2d8c)
- death GUARD on the advance dispatch: InDeathTransition 0x49fb54; 0x4a71f4 = AdvanceLegAnimationGimp (modes 3/4 + 0x580), 0x4a5028 = AdvanceLegAnimation -- NOT a death advancer (5.3.121 correction)
status→mode LIMP PROMOTION-> the STREAMED MODE-REQUEST COUNTERS (see the 5.3.121 section; landed)- update-record block (0x421bac/0x40e5f0/0x4227e0/0x41db7c/0x422bac + Point3D copies) — the dead-reckon/update writer
- collision → gyro crunch (gyro feeds + alarms)
- knockdown/bump arms (EntityID pair + two SetAnim/Mark/SetLevel arms)
- IntegrateMotion @004ab1c8 + position dirty marks
- pose/joint tail: gyro_d/gyro_c → Torso::UpdateJoints @004b66b4 → five StateIndicator::SetState publishes + four GetState reads
- frame close (position latch)
MarkUpdate = FUN_004a4c54 (×20!) — the update-record dirty-mask OR (death strips motion bits); our
ForceUpdate(int)is an EMPTY STUB, so none of these marks exist in our tree yet (MP replication feed). Next sitting: instruction-level read of blocks 3 and 7 (the two STAGED items), then the mark masks. -
@004ab430ReplicantPerformance (1432B) — the replicant-side interior. -
@004ab1c8IntegrateMotion — decoded (see 5.3.95 notes) but only partially re-hosted: the local-velocity/orientation integration and the dead-reckon latency fold (+0x778/+0x77c) remain.+0x598is a Point3D (its replicant branch writes it with a vector subtract), somotionEventNamein 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@004a5678head 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
@0049c690callsFUN_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 REALInDeathTransition. 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@004a1674and 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.
5.3.122 -- the message roster fills (three of the six landed)
Landed: RealMaxSpeed (@0049f604: adopt streamed +0x7a0 while +0x7a4
clear; wire payload at +0x1c, sender EntityID stamped at +0xc by Entity's
network send @0041f640 = {stamp id, msg+0x14 <- entity+0x194, EntityManager
(app+0x30) send @00434ea0}), SetBurningState (@0049f674: park the wreck
at (localHostID x 10000, localHostID x -1000) -- constants @0049f6fc/f8,
host id = HostManager::GetLocalHostID, THE accessor the instance dispatch
compares against ownerID -- then MODE 2 = BURNING, the second
InDeathTransition state's missing writer, + ForceUpdate(1) +
AlwaysExecute), ClearBurningState (@0049f700: mode 0 + same pair).
simulationFlags bit 1 == DontExecuteFlag (SIMULATE.HPP) -- the binary's
&= ~2 IS AlwaysExecute().
Still to land from the roster:
- BalanceCoolant @0049f728 -- LANDED 5.3.123 (equal-split press + RedistributeCoolantShares @0049f788 + the heatable ctor sweep that populates the heatableSubsystems chain + ctor/Reset redistribute calls; soak: 20 heatables, first share 0.05, ctor + BT_PRESS_BALANCE both renormalize, crash-sig 0). UNIFIED 5.3.124: MoveValve steps the heatable-base coolantPriority and calls the mech's RedistributeCoolantShares; valveState and the condenser-local renormalization are retired (RecomputeValves survives only as the [T3] conduction-flow bridge -- the fight-verified pump consumes a CONDENSER-relative fraction, refreshed from the redistribute tail; the ratios are numerically identical to the old math since non-condenser priorities cancel). REGISTRATION CAVEAT: the @0x50e534 name table sits in the HEAT TU statics but its owning class (heatable base vs Condenser) is NOT proven -- our registration stays Condenser id 4 (the live-verified streamed binding); revisit if a non-condenser valve binding ever surfaces. ValveSetting attribute still publishes coolantFlowScale (the displayed fractions are unchanged).
- EjectPilot @0049f854 -- LANDED 5.3.125 with the census (@0049fa1c -> combatIneffective, the master perf's opening call). Census truths, all pinned: 0xBBE = the AGGREGATE HEATSINK BANK (its coolant fraction +0x12c/+0x128 < 0.05 @0049fb50 = crippled term; my earlier "generator charge" reading was wrong), 0xBC1 = GENERATOR (working = state != 1 && generator state != 4 = GeneratorFailed -- our enum matches), weapons by RTTI with dry launchers excluded (+0x364 == 7 == our NoAmmoState exactly), minimum weapon count = mech+0x448 = CTOR-HARDCODED 2 (part_012.c:15749), limping without the crouch cell completes the roster of crippled terms. The eject: mode 10 + mapper ejectLatch (+0x258) + self-dispatched explosive TakeDamage (type 2, zone -1 -> cylinder resolver, impact = own position). STAGED, recorded: the damage amount (binary reads ((mapper+0x208)+0x1c), a bound/cached pointer with NO game-side writer in the decomp -- needs the mapper ctor binding-loop decode) and the score notify (@00429078 accessor + app+0x20 vcall, event code 5 -- same poster idiom as the player-side @004c1044 throttle). Dev kit: BT_FORCE_CRIPPLED=1 + BT_PRESS_EJECT=1 (pod_render_eject.conf). THE AUTHENTIC MESSAGE ROSTER IS NOW 6/6. Mode roster: 0 default, 2 burning, 3/4 gimp L/R, 5-8 falls, 9 killed, 10 ejected; writers of 5-8 still open.
5.3.127 -- THE SUPER STOP + THE INSTABILITY MODEL (landed)
The model resource's own field names decoded these blocks. The ctor
loads (part_012.c:15798-15804) tie the master perf's anonymous offsets to
the authored names our Mech__ModelResource already carried:
| model rec | mech | name (authentic, from the struct) |
|---|---|---|
| +0x44 | +0x344, +0x5b0, +0x5b8 | maxAcceleration (live rate + forward figure) |
| +0x48 | +0x5bc | superStopAcceleration |
| +0x4c | +0x5c0 | throttleAdjustment |
| +0x80 | +0x784 | maxUnstableAcceleration |
| +0x84 | +0x788 | unstableAccelerationEffect |
| +0x88 | +0x78c | unstableGunTheEngineEffect |
| +0x8c | +0x790 | unstableSuperStopEffect (streamed, consumer OPEN) |
| +0x90 | +0x794 | unstableHighVelocityEffect (streamed, consumer OPEN) |
| +0x94 | +0x798 | unstableStopedTurnEffect |
That naming is what made the instability formula legible: the term added
when the leg gait state is 4 is unstableStopedTurnEffect and gait 4 IS
turn-in-place; the speed-shortfall term is unstableGunTheEngineEffect.
The names confirm the arithmetic, and the arithmetic confirms the names.
The super stop (@0x4aa353): the throttle going negative while the
mech still rolls forward is a hard brake, not a reverse. Edge -> swap
+0x344 from the forward rate to superStopAcceleration, MarkUpdate(0x100),
and ONE random-signed lurch through both gyro channels at weight 0.4
(magnitude roll + a 0.5 coin flip @0x4ab170). While it holds above the
walk threshold, a (0,1,0)@0.2 pitch kick every 0.4s. Release restores the
forward rate. superStopAcceleration == -1.1 (@0x4ab174) DISABLES the
system for that chassis -- the sentinel must survive the model read
unguarded.
The instability accumulator (tail @0x4aab8e): three weighted terms,
clamped to 1, into unstablePercentage (+0x3f0 -- ALREADY a published
cockpit attribute in our tree, so this lights a live gauge) and
republished to gyro+0x3a8. The gyro's own tip/sway machinery
(@004b275c) reads that sum against its +0x3bc threshold -- that consumer
is the next gyro brick. Term 1 rides the ring-buffer acceleration
(+0x82c) and stays inert [T1] until the rings land.
CORRECTION to 5.3.122: realMaxSpeed was a DUPLICATE member for
mech+0x7a0, which our tree already carried as runSpeedMax (the myomers
raise it). Merged; the 0x15 handler writes runSpeedMax. And the
+0x7a4 "we computed our own" flag is set by the ctor's MYOMERS sweep
(part_012.c:15874) -- i.e. a mech with myomers broadcasts its cap, a mech
without adopts the streamed one.
Gyro primitives already existed: FUN_004b2d8c == our
Gyroscope::ApplyDamageImpulse (eyeForce), FUN_004b2de4 == our
ApplyDamageTorque (bodyForce). OPEN, worth a look: the binary's torque
zeroes bodyForce.y then negates ONLY .x (+0x308); ours negates the whole
vector, so the .z sign may differ.
5.3.128 -- THE TELEMETRY RINGS (landed)
The five ring buffers ARE the engine's AverageOf<Scalar>, and our tree
already carried them: telemetryFilter[5], sized 15 in the ctor since the
Mech-layout wave, fed by nothing until now. FUN_0043ade4 = Add (with
wraparound), FUN_0043ae0b = CalculateAverage, and FUN_0043ae47 =
CalculateOlympicAverage -- sum minus min minus max over count-2, the
authentic name for a judge-style trimmed mean.
Feeds: vel.z / vel.y / vel.x / turn rate / frame slice. The derived acceleration is the change in FILTERED values over the FILTERED slice, which is why instability answers a sustained shove and not one noisy frame.
TWO ODDITIES KEPT VERBATIM (the binary is explicit; do not tidy them): the vel.y ring is fed every frame and never reduced, and the acceleration's components are a mongrel -- x from mean vel.x, y from MEAN vel.z, z from OLYMPIC vel.z. Only the LENGTH is consumed, so behaviour is unaffected.
LIVE RESULT + A FREE DIAGNOSTIC: unstablePercentage now moves over its
whole range (0 -> 1.0 clamp) on the rig. Walking, it pins near 0.84-0.99,
dominated by the gun-the-engine term -- because the mech demands 35.9 u/s
and the reconstructed gait delivers 7.0. Whether that gap is authentic
acceleration or a gait shortfall is a SEPARATE open question, and the new
gauge is now the instrument for answering it (BT_TELEMETRY_LOG).
5.3.129 -- THE MOBILITY SCAN (landed; the mobilityScale [T1] retired)
Chain identity PROVEN, not inferred. My 5.3.122 note pinned the
+0x7ac chain to Myomers "by static-layout adjacency to the string pool" --
a weak argument that nearly went wrong, because the first cell I checked
(+0x31c) is a MechWeapon trigger in the weapon TU. The decisive evidence
is arithmetic: the Myomers cap-raise @004b8ef0 (our
Myomers::RegisterMaxOutput) reads seekVoltage at +0x330 indexed by
maxSeekVoltageIndex at +0x32c. Walking OUR member order backwards --
maxSeek 0x32c, minSeek 0x328, recommended 0x324, current 0x320 -- lands
speedEffect exactly on +0x31c, the cell the scan maxes. GUID 0x51155c
== Myomers, confirmed. (0x511830 == MechWeapon was independently
confirmed by the census, which tests it and then tests 0x5121a8 for the
dry-launcher exclusion.)
The scan: max speedEffect over the chain -> the mobility scale, applied
straight to the mapper's command cells (throttle *= scale; a scale inside
the 1e-4 deadzone ALSO zeroes the turn -- a mech with dead muscles cannot
even pivot). It is the same cell the duck driver reads as "the legs still
work". Guarded by the myomer count, so a chassis without them is
untouched. SAFE BY CONSTRUCTION: the mapper reassigns its demands from
scratch in InterpretControls, which ticks before the mech's own
performance, so the in-place multiply cannot compound frame to frame.
The ctor's myomer sweep is also what sets +0x7a4 -- so a mech WITH myomers computes and broadcasts its own top speed, and one without adopts the streamed value. Both halves now land together.
Soak: [mobility] scale 1 over 1 myomers on an undamaged bhk1 -- throttle
unscaled, drive unchanged (demand 35.9, speed 7.0), zero faults.
5.3.130 -- two findings from re-reading landed code
A REAL SIGN BUG, fixed. Gyroscope::ApplyDamageTorque (binary
@004b2de4) ended with bodyForce.Negate(bodyForce), but the binary
negates ONLY the x component in place (*(+0x308) = -*(+0x308)) after
zeroing y at +0x30c. Vector3D::Negate flips all three, so the z
component has carried the wrong sign on every damage shake and every
collision crunch since the function landed. Fixed to bodyForce.x = -bodyForce.x. STILL WORTH A LOOK: ApplyVerticalImpulse ends with
bodyForce.y = bodyForce.x, which is odd enough to want its binary
address found and checked the same way.
THE DAMAGE AVERAGES: computed, consumed by nobody -- NOT landed.
@0049fe80 (the master perf's second census) writes three means to
mech+0x354 / +0x358 / +0x35c: over the HULL zones, over every subsystem's
private zone, and over the MYOMERS' private zones (the 0x51155c test --
the same GUID the mobility scan proved). Default 0.0 (@0049ffc8) when a
count is empty.
A reader hunt found none. The apparent hits at part_013:9498 and 13728
are on OTHER objects at the same offsets -- the gyro's damage multipliers
inside its damage fan-out (@004b2980) and a myomers cell inside the
myomer simulation (@004b8d18) -- a reminder that offsets are per-class and
a bare + 0x354) grep proves nothing. No Mech attribute row binds them
either. Landing them would be three loops feeding dead memory, so per the
project rule (record where the binary is silent, never invent a consumer)
this stays decoded and unlanded until a consumer turns up.
5.3.131 -- the instability loop CLOSES, and a family of watchers is found inert
The loop is closed. @004b275c -- the gyro's sway slew -- was ALREADY
reconstructed in our tree, and it already read the cell the master perf
writes: swayBias, which nothing had ever written. So the 5.3.127
mechInstability member I added on the gyro was a DUPLICATE of that same
cell (the second such mistake this arc, after realMaxSpeed/runSpeedMax
-- the lesson is the same: grep our own tree for an existing carve before
adding a member for a binary offset). Merged: the master perf now
publishes into swayBias through a setter, and the instability model
finally does something -- an unstable mech shakes its pilot harder,
through code that was already correct and waiting.
Also landed: the binary's impaired gate has THREE terms and ours had two.
A gyro DESTROYED outright (gyro+0x40 == 1) swings to the impaired sway
base, not only an unpowered or heat-failed one.
VERIFIED on the rig (BT_SWAY_LOG): bias arrives (0 -> 1.0 -> 0.90), the angle slews toward base+bias and stays inside the authored [0, 1.2] band, zero faults.
NEW DEFECT FOUND, precisely located, NOT fixed here
The same log showed impaired=1 for an entire clean run. Instrumenting
the three terms settled it: state=0 heat=0 (so my new term is inert and
correct) but watchdog=0 where Ready is 4 -- that term fires forever.
Chasing it up: PowerWatcher::UpdateWatch sets the watchdog from the
WATCHED subsystem's voltage state and falls back to 0 when
watchedLink.Resolve() is NULL. And watchedLink is
DEFAULT-CONSTRUCTED (HEAT.CPP:886) and bound nowhere in the tree --
not for PowerWatcher, not for HeatWatcher (HEAT.CPP:964 resolves the same
way).
So the whole WATCHER family is inert: every watcher resolves NULL, every
PowerWatcher reports its target unpowered, and the gyro has consequently
been sitting on its percentageOnDestruction sway base since the sway
model landed -- invisibly, until this log existed.
The fix needs the streamed watch index out of the watcher resource plus the ctor bind, in the shape the crit-table binding already uses. That is a proper brick, not a guess, and guessing a binding is exactly what this project's rules forbid -- so it is recorded here for the next sitting.