The blocking identity is settled: DamageZoneClassID == 78 == 0x4e, computed from our own VDATA enum, so the binary's collector filter selects DAMAGE ZONES and the blast is shared across the mech's zones. Landed: AmmoBin::AmmoBinSimulation -- a Performance the class never had, installed under the same master+dynamic gate the binary's ctor uses -- and AmmoBin::CookOff. A bin whose fed weapon reaches heat failure latches a randomised fuse; when it burns down the bin is destroyed and its remaining rounds go off inside the mech, damage scaled by rounds remaining and SHARED across the zones, every victim announced. Correcting my own 5.3.136 write-up: heat clearing does NOT cancel the countdown. Both the arm gate and the cancel read the same cell -- the ammo alarm settled on spent -- so once armed, only spending or destroying the bin calls it off. Letting the gun cool does not save you. Live on the rig: three bins arm, the fuse spread sends them off on different frames rather than together, 20 rounds x 25 = 500 damage over 22 zones from one bin and 16 x 50 = 800 from another, feeding straight into the existing destruction cascade -- Searchlight and ThermalSight destroyed as a consequence. [T1] recorded: the per-round charge is a stand-in. The binary reads it from the bin's streamed explosion record, in the block our cookOffState reserves and nothing parses yet; we use the fed weapon's authored per-shot damage until that record is read. And a mistake worth keeping in the notes: the first attempt sourced the charge from heatPerRound, a heat figure of about 3e-9, so the detonation computed 4.8e-08 of damage and did precisely nothing -- a mechanic that fires, announces itself, and is silently inert. Only printing the arithmetic caught it. Log the numbers, not just the event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
755 lines
42 KiB
Markdown
755 lines
42 KiB
Markdown
# 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:
|
||
|
||
1. **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.
|
||
|
||
2. **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 our `Entity::TakeDamageMessage` ctor as the weapon path does.
|
||
|
||
3. **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).
|
||
|
||
4. **The material tail**: the elasticity/friction OUT values from
|
||
StaticBounce are compared against `_DAT_004ac048`; sets
|
||
`collisionTemporaryState` (+0x44c) to 1, or 2 when both match and the
|
||
state was 0. The state is pushed into the `collisionState` StateIndicator
|
||
once per frame by the response block (0→1 fires the impact sound). Needs
|
||
the `_DAT_004ac048` constant 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) restores
|
||
`worldLinearVelocity = initial_velocity` and position PER CONTACT before
|
||
each ProcessCollision call, and averages the results. The port's
|
||
`frameEntryWorldVelocity` economy 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 (one
|
||
`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,
|
||
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:
|
||
1. controls/demand intake (mech.cpp helpers 0x4a4eab/0x4a4ee9/0x4a4f9e)
|
||
2. three cerr+SetLevel triplets — illegal-state alarm guards
|
||
3. **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.
|
||
4. verify-guarded state read (mech4 accessors + Fail 0x40385c)
|
||
5. gyro feed a (vec pair → gyro_b 0x4b2de4, gyro_a 0x4b2d8c)
|
||
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)
|
||
10. knockdown/bump arms (EntityID pair + two SetAnim/Mark/SetLevel arms)
|
||
11. IntegrateMotion @004ab1c8 + position dirty marks
|
||
12. pose/joint tail: gyro_d/gyro_c → **Torso::UpdateJoints @004b66b4** →
|
||
five StateIndicator::SetState publishes + four GetState reads
|
||
13. 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.
|
||
* `@004ab430` ReplicantPerformance (1432B) — the replicant-side interior.
|
||
* `@004ab1c8` IntegrateMotion — 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. **`+0x598` is a Point3D**
|
||
(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.
|
||
|
||
## 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.
|
||
|
||
## 5.3.132 -- THE WATCHERS ARE BOUND (the family stops being inert)
|
||
|
||
The missing piece found in 5.3.131 is landed. `HeatWatcher` has always
|
||
streamed `watchedSubsystem` and read it into a member; nothing ever turned
|
||
that index into the link `UpdateWatch` resolves. The Mech ctor now binds
|
||
them in a POST-WALK pass -- necessarily post-walk, because a watcher may
|
||
watch a subsystem with a HIGHER roster id, which does not exist yet while
|
||
the segment walk is still running -- with out-of-range and self-reference
|
||
skipped rather than trusted.
|
||
|
||
THE BINDINGS PROVE THEMSELVES. Every index resolves to a semantically
|
||
right target on bhk1, which a wrong offset could not do:
|
||
|
||
Gyroscope -> Avionics HUD -> Avionics
|
||
Torso -> Myomers Searchlight -> Avionics
|
||
AmmoBinAFC100 -> AFC100 AmmoBinLRM15_1 -> LRM15_1
|
||
AmmoBinLRM15_2 -> LRM15_2
|
||
|
||
The gyro and HUD watch the avionics bus, the torso watches the muscles
|
||
that move it, and every ammo bin watches its own gun.
|
||
|
||
### The next question, now sharply posed
|
||
|
||
With the link live the gyro's watchdog moved 0 -> 1, so it is no longer
|
||
the NULL fallback. But 1 is not Ready (4), and `UpdateWatch` can produce
|
||
1 two ways: the watched subsystem reporting NoVoltage, or the BROWNOUT
|
||
branch (watched Ready, but its generator's measured voltage at or below
|
||
`minVoltage * RatedVoltage`).
|
||
|
||
Avionics is classID 3011 -- a SENSOR instance, named "Avionics" in the art
|
||
-- and the roster-live line reports a Sensor at voltState 4. So the
|
||
watched subsystem looks READY, which points at the BROWNOUT branch firing
|
||
persistently. Next sitting: is `minVoltage` authored/read correctly on
|
||
these watchers, or is the reconstructed generator genuinely sitting at the
|
||
brownout threshold? Log the watched level and the measured/rated pair
|
||
side by side to separate them -- do not adjust a threshold to make an
|
||
alarm quiet.
|
||
|
||
## 5.3.133 -- THE BROWNOUT WAS A UNIT BUG (chain closed end to end)
|
||
|
||
The question posed in 5.3.132 is answered, and it was neither of the
|
||
guesses being weighed -- it was arithmetic. `BT_WATCH_LOG` printed the
|
||
whole comparison in one line:
|
||
|
||
'Gyroscope' watched='Avionics' level=4 minVoltage=50
|
||
measured=10000 rated=10000 brownout=1
|
||
|
||
Every term is legible there. The bus is READY and at FULL rated voltage
|
||
(10000 of 10000) -- so there was never a real brownout. The trip value
|
||
was 50, and the test `measured <= minVoltage * rated` reads
|
||
`10000 <= 50 * 10000`, which is true for any voltage whatsoever.
|
||
|
||
The field is called `minVoltagePercent`. It is a PERCENT, and we stored
|
||
it 1:1 -- our own comment had flagged the missing scale as "a tuning
|
||
value (stored 1:1 here until located)". The authored data admits no
|
||
other reading: bhk1 gives the Gyroscope and HUD 50 and the Searchlight
|
||
10, round percents that are absurd as raw fractions (5000% / 1000%).
|
||
Divided by 100 the gyro trips below 5000V of a 10000V bus, which is a
|
||
sane undervoltage threshold, and the Searchlight -- a light -- tolerates
|
||
far more sag before complaining, which is exactly the right ordering.
|
||
|
||
CHAIN NOW CLOSED, all four links verified live in one run:
|
||
watcher bound (5.3.132) -> watchdog reads the target's REAL state (4,
|
||
Ready) -> impaired FALSE for the entire run (31 samples, none impaired)
|
||
-> the gyro sways on `percentageOnNormal` instead of its
|
||
destruction-grade base.
|
||
|
||
NOTE ON PROVENANCE: the binary's own scale SITE was not located in code;
|
||
the /100 is inferred from the field name plus authored data that is
|
||
unambiguous. Recorded as such rather than claimed as a byte-verified
|
||
decode.
|
||
|
||
## 5.3.134 -- THE "GAIT SHORTFALL" WAS A CONFOUNDED MEASUREMENT
|
||
|
||
The top open question from 5.3.128 -- "the mech demands 35.9 u/s and the
|
||
gait delivers 7.0, authentic acceleration or a gait shortfall?" -- is
|
||
answered: **authentic acceleration, and the measurement was confounded by
|
||
my own soak harness.**
|
||
|
||
The run I drew that reading from was `pod_render_telem`, which still had
|
||
`BT_SUPERSTOP_SOAK` driving the throttle to -0.5 every six seconds. The
|
||
mech was never given time to reach top speed; every sample caught the
|
||
first seconds of a climb that was about to be thrown into reverse.
|
||
|
||
`BT_GAIT_LOG` (new, state-change-triggered) on a clean full-throttle run
|
||
shows the ladder working exactly as designed:
|
||
|
||
leg=5 speed=0 -> stand-to-walk
|
||
leg=7 speed=5.69
|
||
leg=6 speed=17.0 -> the walk-to-run step
|
||
leg=11 speed=17.7
|
||
leg=12 speed=25.5
|
||
leg=13 speed=35.7
|
||
leg=12 speed=44.35 target=44.837 legCycle=bodyCycle=44.837
|
||
|
||
The mech accelerates 0 -> 45 u/s through six clip transitions and settles
|
||
on its demand. `runSpeedMax` reads 80.06, raised off its unsourced
|
||
default by the myomers -- the 5.3.129 landing doing its job. Both
|
||
run-clip slots are bound (clip6=344, clip8=345).
|
||
|
||
So the instability model pinning high during a climb was CORRECT all
|
||
along: a mech demanding far more speed than it has IS gunning the engine.
|
||
Nothing to fix in the gait or the instability model.
|
||
|
||
**METHOD LESSON, the expensive kind:** I took a measurement from a rig
|
||
whose harness was actively fighting the thing being measured, and wrote
|
||
the result into the notes, a commit message and memory as an open defect.
|
||
The hedge ("authentic or shortfall?") was the only thing that saved it
|
||
from becoming received wisdom. Before recording a number as evidence,
|
||
check what else the conf is driving.
|
||
|
||
## 5.3.135 -- WHAT THE WATCHER FIXES ACTUALLY TURNED ON
|
||
|
||
Chasing the downstream of 5.3.132/133 (watchers bound, brownout unit bug
|
||
fixed) found a user-VISIBLE consequence nobody was looking for.
|
||
|
||
`HeatWatcher::WatchSimulation` mirrors the WATCHED subsystem's temperature
|
||
into the watcher's own 3-level heat alarm. With every link NULL it held
|
||
NormalHeat forever, so the mirror was dead: the gyro never felt a hot
|
||
avionics bus, the torso never felt hot myomers, no ammo bin ever felt its
|
||
own gun's heat. That mirror is now live for all seven watchers.
|
||
|
||
**THE HUD PAGE WAS PERMANENTLY HIDDEN.** `HUD.CPP:145` gates page
|
||
visibility on `watchdogAlarm.GetLevel() != PoweredSubsystem::Ready`
|
||
(4). Unbound, the watchdog read 0 -> hidden. Bound but with the percent
|
||
bug, it read 1 -> still hidden. It took BOTH fixes to reach 4:
|
||
|
||
[hud] page_visible=1 watchdog=4 heat=0
|
||
|
||
So the HUD page is drawing for the first time. **Flag for the next
|
||
cockpit A/B session**: the gauge-ab rig last scored 98.9% identical
|
||
against the shipped binary with this page missing on our side -- if the
|
||
shipped exe draws it, some of that residual 1.1% should close on its own,
|
||
and the comparison needs re-running rather than trusted from the old
|
||
number.
|
||
|
||
Still worth exercising now that the mirror is live: what an AmmoBin does
|
||
with its gun's heat alarm (a cook-off or a feed lockout would be the
|
||
obvious candidates, and the binary is the place to check before assuming
|
||
either).
|
||
|
||
## 5.3.136 -- THE AMMO COOK-OFF, decoded (the payoff of binding the watchers)
|
||
|
||
Asking what an AmmoBin does with its now-live gun heat alarm turned up a
|
||
whole mechanic that has never run in our tree, for TWO independent
|
||
reasons: the watch link was unbound (5.3.132) so the mirrored alarm never
|
||
left NormalHeat, AND our `AmmoBin` has no Performance at all -- the
|
||
binary installs one (ctor @004bd5c4 stores the triple @0x512654 ->
|
||
`@004bd394`, gated master+dynamic as usual).
|
||
|
||
**The trigger** (@004bd394): when the bin's mirrored heat alarm (+0x140)
|
||
reads 2 = FailureHeat -- i.e. THE GUN IT FEEDS is cooking -- and the bin
|
||
is not already spent (+0x1a8 != 2) and no countdown is pending, it latches
|
||
one: `cookoffAt = now + <randomised delay>` (time from @00414b60, the
|
||
delay through the ftol helper @004dcd94). If the heat clears the
|
||
countdown is CANCELLED; if it expires the bin destroys itself
|
||
(state alarm -> 1, dirty mark) and detonates.
|
||
|
||
**The detonation** (@004bd300 -> @004ac274), and the damage is authored
|
||
per round: `damage = roundsRemaining * perRoundDamage (+0x1f4)`, so a full
|
||
bin guts the mech and a nearly empty one barely coughs. @004ac274 then:
|
||
|
||
* drives the bin's own state alarm 2 then 1 with an update mark each,
|
||
* pins its PRIVATE damage zone to 1.0 (destroyed),
|
||
* collects a target set by walking @004acfa9(mech, -1) and keeping
|
||
entries whose `+4` field equals **0x4e**,
|
||
* **divides the damage by the number of targets** -- the blast is
|
||
SHARED, not applied whole to each,
|
||
* dispatches a 100-byte TakeDamage (id 0x12) at each, printing the
|
||
authentic `"ammo explosion damaging <name>"` for every victim,
|
||
* finally zeroes the ammo count and sets the bin's 6-level state alarm
|
||
(+0x194, ctor `FUN_0041b9ec(+0x194, 6)`) to 2.
|
||
|
||
**THE ONE THING TO PIN BEFORE LANDING**: what @004acfa9 iterates and what
|
||
`+4 == 0x4e` selects. 0x4e is not a classID (those are 0xBBx), and the
|
||
per-victim reads -- a name via `*(+0x15c) + 8` and an index at `+0x13c` --
|
||
look more like DAMAGE ZONES than subsystems. Getting this wrong would
|
||
produce a plausible mechanic that damages the wrong things, which is
|
||
exactly the failure mode this project keeps catching, so it is decoded no
|
||
further here. Read @004acfa9 and @004ad03a/@004ad0a4 (the collector and
|
||
its list) first, then land the whole thing in one piece.
|
||
|
||
Note the shape of the find: two dead systems in series hid a third. The
|
||
watcher binding did not just fix an alarm -- it exposed that the mech has
|
||
never been able to suffer an ammo explosion.
|
||
|
||
## 5.3.137 -- THE AMMO COOK-OFF LANDS
|
||
|
||
The blocking identity from 5.3.136 is settled: **DamageZoneClassID == 78
|
||
== 0x4e**, computed from our own VDATA enum, so the collector's filter
|
||
selects DAMAGE ZONES and the blast is shared across the mech's zones.
|
||
|
||
Also CORRECTING my 5.3.136 write-up: I said heat clearing cancels the
|
||
countdown. It does not. Re-reading @004bd394, both the arm gate and the
|
||
cancel test the SAME cell (+0x1a8, adjacent to the ammo alarm and read as
|
||
the alarm having settled on SPENT). Once armed, a cook-off is only called
|
||
off by the bin being spent or destroyed -- letting the gun cool does NOT
|
||
save you.
|
||
|
||
Landed: `AmmoBin::AmmoBinSimulation` (a Performance the class never had,
|
||
installed master+dynamic gated as the binary's ctor does) plus
|
||
`AmmoBin::CookOff`. Our fuse is a millisecond COUNTDOWN where the binary
|
||
stores an absolute deadline -- same behaviour, no clock-source dependency.
|
||
|
||
LIVE (BT_FORCE_COOKOFF=1, pod_render_cookoff.conf):
|
||
|
||
[cookoff] 'AmmoBinAFC100' ARMED -- 20 rounds aboard
|
||
[cookoff] 'AmmoBinLRM15_1' ARMED -- 16 rounds aboard
|
||
[cookoff] 'AmmoBinLRM15_2' ARMED -- 16 rounds aboard
|
||
[cookoff] 'AmmoBinAFC100' DETONATES: 20 x 25 = 500 over 22 zones
|
||
[cookoff] 'AmmoBinLRM15_2' DETONATES: 16 x 50 = 800 over 22 zones
|
||
[critfail] 'Searchlight' DESTROYED ... 'ThermalSight' DESTROYED
|
||
|
||
The randomised fuse spreads the rack (the three bins go off at different
|
||
times, not on one frame), and the blast feeds straight into the existing
|
||
destruction cascade.
|
||
|
||
### [T1] THE PER-ROUND CHARGE IS A STAND-IN
|
||
|
||
The binary reads it from the bin's streamed EXPLOSION RECORD (+0x1f4 --
|
||
the second field of the block our `cookOffState[12]` reserves and nothing
|
||
parses), then writes the product back into a copy of that record. Until
|
||
that record is parsed we use the FED WEAPON's authored per-shot damage:
|
||
right order of magnitude, physically sensible, but a stand-in to be
|
||
replaced by the first pass over the ammo resource block.
|
||
|
||
**A caught mistake worth keeping:** the first landing used `heatPerRound`
|
||
for the charge. That is a HEAT figure of about 3e-9, so the detonation
|
||
computed 4.8e-08 of damage and did nothing at all -- a mechanic that
|
||
fires, announces itself, and is silently inert. Only printing the
|
||
arithmetic in the soak log exposed it. Log the numbers, not just the
|
||
event.
|