The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:
THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
MIRRORS the watched subsystem's electrical level (+0x278), brownout
downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled
ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
@004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the
SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
permanent brownout) and PowerWatcher's Derivation chains its REAL base
HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
"HeatSink"); the btl4gaug heat-widget gate now tests it via the
BTIsHeatWatcher bridge.
THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
every twist-capable .SKL; the camera + canopy ride the same hinge
subtree through HingeRenderable's live matrix-stack compose -- ALREADY
WORKING in the port. The crosshair stays screen-centered (center IS
the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
"body-mounted view" model): the camera already carried the twist, so
the crosshair counter-slid to hull-forward and the fire ray with it.
Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
BT_FORCE_TORSO clobbering real joints -> the hook now only fills
unresolved ones); an over-correcting explicit eye compose was added on
those false readings and retired the same day.
CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
hold-deflection model drifted forever); X also zeroes the axis and
pulses the authentic torso Recenter (@004b6918). M cycles control
mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
-140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
killer's player link) -- kills count, target K/D populates.
KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:
COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).
MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.
RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px. Gotcha #17 (verify the FUN_ body, not its call shape).
WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed). Polarity chain corrected end-to-end (failedState, fed by real
damage saturation). Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched). The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.
COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.
DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pilot's second heat-management tool: the PoweredSubsystem message table
@0x50F4EC (ids 4-8) registered and implemented -- SelectGeneratorA-D
(@004b099c..@004b0a74: FindGeneratorByNumber roster walk on generatorNumber
@0x1E0 -> AttachToVoltageSource re-tap with tap accounting -> modeAlarm
Connected) and ToggleGeneratorMode (@004b0abc: Manual -> Auto -> detach+Manual
cycle). Weapons inherit via the MechWeapon handler chain. Desktop: F5-F8
assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles
reconnect mode; BT_GENSEL_TEST scripts a headless verify.
Verified live end-to-end: dispatch -> handler -> re-tap ("PPC_1 ->
GeneratorD (tapped)") -> the charging I^2R physically moved (GenD cold ~90K
baseline -> ~1570K carrying the PPC; GenA relieved), stable over a sustained
autofire soak with thermal-breaker trips.
FOUR defects found and fixed on the way [all T1/T2]:
- THE e17 HEAT EXPLOSION: Generator::SourceLevel misread *(this[0x38]+0x158)
as linkedSinks->heatEnergy; it is the engine-base DamageZone @0xE0 ->
damageLevel [0..1] (the same named-member pattern as the bank radiator's
zone read). A breaker-restarting generator emitted (1 - 4e8) x 10000
volts; squared through the customers' I^2R feed, one restart blew the
whole thermal network to e17. Authentic: a damaged generator yields
proportionally less voltage.
- FUN_004ac9c8 is NOT "IsDamaged": raw body = owner -> mech+0x190 player ->
roleClassIndex(+0x274) == 0 -- the ROOKIE-role lockout for advanced
cockpit systems. New bridge BTPlayerRoleLocksAdvanced (NULL player =
unlocked [T3]; bring-up role 2 = unlocked). The old stand-in gated the
handlers off permanently (healthy subsystems have simulationState==1).
- MESSAGE_ENTRY tables must be FUNCTION-LOCAL statics inside the accessor:
as namespace-scope arrays they are read by other TUs' static-init chains
before their own initializers run -- Build copies zeros and every id in
the table is silently dropped (ids 9/10 only worked by TU-order luck;
both tables relocated; gotcha recorded, reconstruction-gotchas #9).
- The AutoConnect hunt scanned GetSegment() -- the raw @004b0bd0 walks the
subsystem ROSTER (+0x124/+0x128); EntitySegments were being cast to
Subsystems (the classic +0x128 gotcha).
Also: the dense handler table's GAP slots (skipped ids) are uninitialized
heap -- the name-based Find strcmp-walks them and AVs; diagnostic probes use
the id-based Find only (the 1995 binary's own tables carry the same holes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and
live-verified (hold-configure -> tap fire button -> toggle membership ->
release-commit):
- MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton
@004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept)
+ the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/
MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch).
- Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration /
+0x40,+0x44 AddOrErase evt/dir. No "secondary vtable @0050f498", no
"CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP).
L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the
NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon
latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO
mapper implements the toggle (Thrustmaster no-ops = can't regroup).
- MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID
(ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the
destination to &fireImpulse (@004b99a8).
- ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table;
buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale
was wrong). Finding [T1]: the shipped binary NEVER enables this gauge (no
SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable.
- Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon);
BT_CONFIG_TEST scripts a headless verify. @004afbc4 corrected to its real
Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups).
- Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries
(PercentDone..WeaponState) -- "TriggerState is the only one" was wrong.
TASK #11 -- the user-reported live-play regressions (both real bugs):
- PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the
victim on its OWN 0.3s cadence whenever fire was held -- desynced once the
authentic recharges landed. Retired (the real impact visual flows from
each discharge via the messmgr SubmitExplosion).
- WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window
(+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike
jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness,
and the weapon sticks in Loading at level ~10000 forever (observed live).
Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick
-- also fixes the I^2R integral over-heating generators on big steps.
NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap).
- Duty-cycle measured (max-rate autofire): generators equilibrate in the
degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker
never trips solo, PPCs sustain ~11.8-damage full-charge fire. The slow
kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's
counters (this regrouping UI, generator reassignment, coolant valves) are
the reconstruction queue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task #10 set out to fix "scrambled linked-sink routing"; a [heat-link] attach
log proved the routing was NEVER scrambled (every subsystem links its authored
condenser exactly; the "pools in Condenser1" read was the diagnostic-sampler
aliasing trap). The real defect: emitter.cpp's local FUN_00417ab4 stub
returned NULL, so the whole electrical model was inert and the E7 force-charge
recharged every emitter in ONE frame (~0.3s cycle, 1501 fires/90s, ~1.7e9
heat/s -- the "runaway").
Landed authentically [T1: disasm + byte-verified constants]:
- Emitter ctor @004bb120: seekVoltage = authored fraction x generator
ratedVoltage (10000); EC = energyTotal/(seekV^2 x 0.5); voltageScale@0x310 =
(RechargeRate / -ln(1 - 1e-4 x seekV)) / EC -- charge reaches seekV[rec] in
EXACTLY the authored RechargeRate (PPC 5s, ERL 4s, SRM 3s, ERM 2s) cold.
Owner-flags ctor gate (the usual gotcha; the this-flags read never armed).
- PoweredSubsystem::ChargeTimeScale (@004b0d50, was a =1.0 stub): voltageScale
x (1 + thermalResistivity x srcTempRise) -- hot generators charge slower.
("voltageScale is never read back" was wrong; corrected + swept.)
- TrackSeekVoltage @004ba838: charging I^2R -> the GENERATOR's pendingHeat
(~3.5e8/full PPC charge) -- generators self-heat, conduct to their authored
condensers, and throttle further charging. The feedback economy closes.
- FailureHeat consumers found: this+0x184 == 2 gates BOTH weapon families
(@004baa88 emitter: reset firing + hold charge 0; @004bbd36 ballistic:
recoil=rechargeRate + alarm 7). Emitter::GetFaultState un-stubbed.
- ProjectileWeaponSimulation @004bbd04 opens with call 0x4b0bd0 (disasm) --
launchers now run the powered/heat step (their firing heat previously
accumulated in pendingHeat forever).
- heat.cpp: per-instance BT_HEAT_LOG census (the old shared-static 1-Hz
sampler aliased); [heat-link] attach log; the stolen-else Verify restored.
Verified live (120s max-rate autofire): PPC ~470-500 (was 55,000), bank
plateaus ~600 and sheds to ambient, generators 1100-1400, ERMLaser
self-regulates at the authored 2000 failure threshold (shutdown-cool-resume).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Emitters: heatPortion closed form = heatCostToFire x 1e7 x (charge/seekV)^2
(PPC 1.1e8 -> +632K on its own sink); the missing projectile/missile heat
adds (raw -- resources author pre-scaled 1e7 units).
- The bank's AMBIENT RADIATOR @4ae73c reconstructed (the system's ONLY heat
exit; conductance x 0.1 x HeatSinkCount -- _DAT_004ae974 float80 = 0.1) +
the link-attach guard corrected (skip = the 0xBBE bank, not Condenser; the
inversion blocked condenser->bank links and closed the system).
- Constant corrections (all byte-verified float80s): coolant epsilons
0.0025/0.003/1e-4 (was a single 1e-4 serving three sites);
CoolantCapacityScale 0.05 (was 1.738).
- Verified: heat flows + exits; max-rate autofire overheats weapons into the
authentic heatLoad range-cutout (thermal spam unsustainable by design).
- KNOWN REMAINING: linked-sink routing scrambled (heat pools in Condenser1;
authored map says PPC->C4/C6) -- the heat-stream offset audit, filed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three root causes, all fixed:
1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling
the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2
(closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35).
The observed "0.25" was the degenerate EC=1 fallback, never authored data.
2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the
release value (-65) is a negative NaN. The binary's x87 unordered compare
read it as "released"; IEEE-correct float compares latched the edge
detector shut after the first release -- the reason the emitter discharge
chain NEVER fired in-game. Fixed with bit-pattern sign compares.
3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData
(amount + damageForce=target-muzzle [the gyro directional-bounce feed] +
impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) ->
messmgr consolidation with per-weapon records + explosion bundling.
The mech4 bring-up damage block + flat kShotDamage retired to diag hooks.
Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread). Zone model
verified byte-exact (no change). Solo end-to-end: 5-record volleys (2 PPC
+ 3 ERML with authored amounts), per-weapon zone granularity, explosions,
kill. Heat stays bring-up scale pending the heat-calibration audit [T3].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ConsolidateAndSendDamage @0049b784 fully implemented: the beam path submits
into AddDamageMessage; each messmgr tick builds ONE Entity::
TakeDamageStreamMessage (id 0x13; wire-verified 0x34+4+Nx12) with the
common impact record + appended {type, amount, subsystemID} entries and
Dispatches it at the victim -- the T0 handler (ENTITY.cpp:817) re-splits
into per-record TakeDamage; replicants reroute cross-pod. Real
ResolveExplosionID (firing weapon's +0x3E4, guarded) + SubmitExplosion via
the Explosion::Make port; TWO chain-purge bugs fixed (the "iterator dtor
clears it" assumption was wrong -- records re-applied every tick, observed
1->2->3->4 double-counting). @004b9728 identity corrected (SendDamageMessage,
not DrawWeaponPip). Verified solo: 25 clean single-record consolidations ->
kill -> death transition. Residue: weapon-side submission awaits the
damage-economy reconciliation (authored 0.25-scale vs kShotDamage=12).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The recovered system: fire channels = LBE4ControlsManager buttonGroups
(0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map
resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port
already called -- it needed only the TriggerState attribute (id 0x13 PINNED
to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an
input feed. Keyboard/harness now push press/release edges into the button
groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse
hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger
= 4 weapons). Myomers @4b9550/@4b95b8 misattribution corrected (they are
MechWeapon ConfigureMappables/ChooseButton). Verified 2-node: kill through
the authentic chain (12 hits vs ~36 pre-groups). Config-mode session
(regrouping UI) = the remaining stage, KB-scoped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The binding was in the zone ctor all along: Ghidra dropped the two arg
pushes @0049d0e1 (Slot::AddImplementation(subsystemArray[streamedIndex])),
making it read as a bare Resolve(). DZSlot stand-in -> engine SlotOf<T>;
SendSubsystemDamage rewritten to the recovered @0049c9a8 body (allotment
into the subsystem's OWN private zone; vital -> graphicAlarm 9); CriticalHit
-> real ApplyDamageAndMeasure; parentArtifactZone.Add revived (LOD damage
averaging); videoObjectFlag renamed vitalSubsystem (+0xE4). BT_CRIT_PROBE
diag added. Verified: 66 plugs bound/mech; probe-destroyed zone -> crits
damaged/DESTROYED, statusAlarm + destroyed-skin chain fire; MP kill + solo
un-regressed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolved the non-Madcat blocked views. Rendering every *_COP shell offline
from its mech's AUTHORED eye (SKL [jointeye] translation -- incl. the Thor's
+1.13 offset cockpit) with faces coloured by punch flag showed the answer
plainly: the PUNCH-tagged patches sit exactly over the window apertures in
all 8 mechs. On these texture-less black-diffuse shells dpl_Punchize renders
the tagged geogroup as holes -- the punch patches are the transparent
WINDSHIELDS, and the visible frame is only the non-punch geometry.
The loader now drops the punch batches of _cop meshes (bgfload finish();
BT_COP_KEEPPUNCH=1 keeps them for diagnosis). In-game sweep on pure defaults:
- madcat: the dark dome frame (matches gameplay footage)
- thor / owens / sunder: authentic brace/dash/pillar remnants
- bhk1 / loki / vulture / avatar: authentically FRAMELESS (all-punch
canopies -- the pod showed no frame for those mechs)
(An early-session test that "disproved" drop-punch ran with the broken
pre-inverse eye; its conclusion was wrong and is corrected in the KB.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over
section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse:
- zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce
or a RANDOM horizontal fallback when ~zero (per-component sign roll then
value roll -- binary-legal until senders fill damageForce)
- direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0)
-> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim)
- per-type scaling amount / damageMultiplier[type] * damageResponse[type]
.{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount;
each clamped at 1.3f
- four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
vibrationDirection@0x390) and verticalImpulse(yaw, dir)
Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis
init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech
+0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed
int clipLoadGuard.
Call sites wired (binary-gated):
- take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264
in hub FUN_004a0230) -- an invalid-zone hit still bounces
- crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up
impulse 0.2 (@4aa81e/@4aa86c)
- firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5)
x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor
DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage
jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it
mutates the #49/#50-stabilized gait machine.
Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition
shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill
chain un-regressed; no NaN.
Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored
jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye
reproduces it to the hundredth (the placement model is correct per-mech); and
the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets
but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV,
cdb-verified, not a code bug).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Gyroscope subsystem is LIVE again (was a stub since the NaN revert).
Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 /
@004b34ec + the byte-verified dispatch in the unexported Mech performance
FUN_004a9b5c (calls @0x4aaf74/0x4aaf83):
- ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were
mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the
0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so
everything after was mis-offset, and clamps/accumulators were 0xCD fill --
every field now initialised per the binary; 33 offsets static_assert-locked,
sizeof == 0x3D0 exact
- integrators: state-minus-target displacements, componentwise damping
OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step
without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed
force + integration terms
- writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION =
eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a
multiplicative sway attenuator on 'jointlocal' (post-anim only)
- dispatch moved out of GyroscopeSimulation into the Mech performance tail
(GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso
pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU
bridges, retiring the SubProxy::linkTarget landmine
- Mech ctor: deathAnimationLatched/legResetLatch were never initialised --
0xCDCDCDCD gated the writers off silently
Runtime-verified: joints resolve (both BallTranslation), Performance installs,
WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed,
combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye
equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE
mechanism, not a steady eye offset (hypothesis disproven, recorded in KB).
Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported
gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model;
the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answering "do lasers and PPCs fire together?": in the pod, grouping was
a PILOT CHOICE. MechWeapon message handlers id 9 'ConfigureMappables'
(FUN_004b9550) and id 10 'ChooseButton' (FUN_004b95b8), handler table
@0x511860 (names read from the exe): ConfigureMappables binds the
weapon's fireImpulse@0x31C to controls-mapper fire-channel #N (roster
slot 0; vtable +0x38 register / +0x3c unregister / +0x44 choose),
ChooseButton = the in-cockpit re-binding. Weapons sharing a button fire
together. The port's SPACE=energy / CTRL=missiles split is a stand-in
default; the authentic per-button channels are a tracked follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colleague: you fire without a lock, at "nothing." Double-checked the
binary:
- The weapon fire path is DOUBLY gated on mech+0x388 != 0:
EmitterSimulation (FUN_004baa88:7689) only calls FireWeapon with a
target, and FireWeapon (FUN_004bace8:7727) wraps its whole body --
including beamFlag(+0x46c)=1 -- in the same check. So no target => no
beam, literally (cannot fire into truly empty space). [T1]
- BUT a capstone scan of the entire CODE section finds 11 READS of
+0x388 and ZERO direct stores: the target is written INDIRECTLY (a
message/selector), i.e. AUTO-acquired -- there is no manual lock to
fire. The spinning-ring LOCK (HudSimulation 5619-5634) is a separate,
stricter state.
So the colleague is right that no lock is needed; my port was wrong to
gate firing on a pinpoint boresight-on-hull pick. Fix: mech+0x388 = the
enemy whenever it is ALIVE (auto-target); firing needs only that. A
pinpoint hull hit upgrades the shot to aimed-zone damage + the lock
ring; off-hull with the enemy present, the beam converges on centre
mass (body hits). Third correction from over-reading the RP-shared
Reticle struct (after sticky-lock and mouse-cursor).
Verified: BT_AIM="0.5 0.2" (off-hull) -> fires + lands damage (was 0
before); BT_AIM="0 0" (centred) -> HOT-aimed zone hits. KB swept;
checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colleague flagged that targeting is torso-locked with no fine cursor.
Correct: the pod stick's yaw drove the TORSO TWIST
(MechControlsMapper -> HUD::SetFreeAimSlew(stick_x) @cockpit+0x28C,
gated on torso-horizontal-enabled; hud.hpp:167) and reticlePosition
@HUD+0x1FC is COMPUTED by HudSimulation from the mech pose quaternion +
target geometry, zeroed to centre with no target (part_013.c:5680) --
never a free-floating cursor. The engine Reticle struct is general
(shared with Red Planet); BT drives it from the torso boresight.
Fix:
- Removed the mouse-cursor slew (a mis-sourced stand-in from the
RP-shared struct). The crosshair is now the torso boresight:
BTTwistToReticleX(torsoTwist) = tan(twist) projected through the live
per-axis projection. Dead-centre on the fixed-torso BLH
(TorsoHorizontalEnabled=0); you aim by steering the whole mech.
- BT_AIM="x y" retained as the headless test harness.
Verified: face-to-face spawn -> HOT lock + aimed zone hits (36);
BT_FORCE_TURN circling -> 31 no-target vs 1 HOT (steering off-target
drops the lock, as it must). KB swept (combat-damage / gauges-hud /
open-questions); checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- LOCK rules transcribed from the exported HudSimulation
(part_013.c:5619-5634): lock requires a target AND your own HUD host
zone damage < 0.75 (_DAT_004b7ec4 -- a shot-up targeting computer
drops lock) AND the targeted zone damage < 1.0 (_DAT_004b7ec8;
whole-mech target checks zone 0 -- a wreck's dead zone can't
re-lock). Box and ring are now separate signals like the binary:
gBTHudLockState 1 = target held (hotbox draws), 2 = LOCKED (+ the
spinning ring).
- SIMPLE-X mode: the ctor's [0x99] minimal-reticle list transcribed
(@4689-4705: green +-0.02..0.08 cross on the aim translate); Draw
switches master <-> simple X on the PrimaryHudOn element bit (0x20),
completing the recovered Execute's state-list logic.
- Canopy diagnosis sharpened (open-questions): blx_cop is the torso
segment's inside-skeleton mesh enclosing the eye; the black box =
the skeleton render branch missing its texture + PUNCH cutout (the
canopy windows are punch texels). A rendering-branch task; stays
hidden by default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one un-exported gap in the reticle chain, read via capstone
(tools/disas2.py; the annotated disasm preserved at
reference/decomp/reticle_execute_004cdcf0.disasm.txt). Draw() is now a
transcription of the real per-frame logic, and the HUD attr-table names
(hud.hpp ids 4/5/6/8/A/B/C/D) are CONFIRMED by their Execute usage:
- Range ladder: BAR from ladder-top to the caret + caret translate.
- The bottom 21-tick tape is the TORSO-TWIST indicator (NOT heading):
deflection = -/+(span/2) x (RotationOfTorsoHorizontal / twist limit),
attrs 4/5/6. Fixed-torso BLH reads centred -- authentic static.
- The circle-with-stem is the COMPASS (attr 0xD, rad->deg rotation) at
(botX, botY - 3*tickMajor - 0.03), with the THREAT trail (attr 0xC)
in its rotated frame: 0.05-unit attack-direction marks, fresh < 2s
red, expiring at 6s. Port feed: player TakeDamage pushes the impact
direction (dormant vs the passive test dummy).
- Pips (composed into subB6): hidden when destroyed (attr 1 == 1), LIT
when the fire cycle is LOADED (attr 0x1c == 2; port source
rechargeLevel >= 1) else the dark charging ring; filtered by the
weapon-GROUP bits (weaponMode & elementMask&0xF). Range plays NO part
(the stored TargetWithinRange slots are never read by Execute).
- Lock ring: subB9 at frame centre SPINNING 4 deg/frame while locked
([0x9d] is the spin matrix, not a heading list).
- Target HOTBOX: a rectangle hugging the projected extents (x+-4 around
the top-centre hotbox point, +1/-11.5 vertical; baked K=2.8145 -- the
port projects through the live per-axis projection), switching to the
edge arrows past +-1.6 or behind (BTProjectHotBox, L4VIDEO).
- Reticle state Off/On + PrimaryHudOn full-HUD/simple-X switch, aim
translate on slew move, 3D marker + PNAME player-name mesh identified
(chain still deferred).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sticky lock made aim irrelevant after the first acquisition (the
crosshair starts on the face-to-face enemy, so it locked at spawn and
never let go -- played as auto-targeting). The binary treats no-target
as a frequent live state (fire path re-checks 0x388!=0 at every step,
part_013.c:7689/7727; HUD range feed has a permanent no-target default,
:5636), so the target slots now mirror the reticle pick each frame:
locked while the crosshair is ON the mech, no lock otherwise. Keeping
the crosshair on the enemy is the gunnery.
Verified headless: BT_AIM="0 0" -> HOT + aimed zone hits; BT_AIM="0.6
0.4" -> no lock, zero damage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine Reticle model (MUNGA/RETICLE.h [T0]) reconstructed end to end:
- Mech::targetReticle is a real Reticle member bound to the TargetReticle
attribute (0x1d), per the RP VTV analog (VTV.h targetReticle).
- Crosshair slew: mouse -> client rect -> reticle coords (the pod stick
free-aim channel's dev-box stand-in); BT_AIM="x y" pins it headless.
LMB fires lasers / RMB missiles (alongside SPACE/CTRL).
- Pick ray: the ACTIVE eye publishes pos + LookAtRH basis (BTSetAimCamera,
L4VIDRND view-write site) + the render loop publishes proj._22;
BTGetAimRay builds the world ray, Mech::PickRayHit slab-tests it against
the collision template's ExtentBox via the engine's BoundingBox::HitBy
(local frame; clips the Line at entry) -> world hull point.
- Designation: the mech under the crosshair designates (sticky; re-hover
refreshes; cleared when the target leaves the roster at burial); the
entity target slots 0x37c/0x388/0x38c feed the whole weapon path.
- Aimed fire: while HOT the impact point is the PICKED hull point -> the
STEP-6 cylinder lookup resolves the zone under the crosshair (verified:
center-aim -> head-band zone 13 dominant). Off-crosshair the sticky
designation converges on center mass.
- HUD: the aim group draws at the slewed position ([0x9a] translate,
contained by push/pop); the designator ring tracks the target's
projected point (subB9 hot / subB8 designated, BTProjectToReticle);
edge arrows when off-screen/behind.
- AUTHENTIC gating: no fire arc exists in the binary (FireWeapon fires
whenever HasActiveTarget, part_013.c:7758) -> BT_FIRE_ARC is now an
explicit OPT-IN presentation clamp; the hardwired gEnemyMech lock and
the projectile path's gEnemyMech fallback are removed.
- Fixed en route: every renderable rebuild stomped mCamera back to the
chase eye (start-inside silently lost the cockpit camera; the aim feed
exposed it). BTL4VideoRenderer::mViewInside persists the chosen view.
Verified headless: BT_AIM="0 0" -> HOT lock, pick hits the hull face at
exact range, aimed zones resolve; BT_AIM="0.8 0.3" -> no lock, zero
damage, zero missile launches; kill chain completes to wreck + smoke.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine Reticle struct (the mech's "TargetReticle" attribute) is the
acquisition chain: slewable screen-space crosshair -> pick-ray -> the
entity + damage zone under it become mech+0x388/0x38c, the intersection
point mech+0x37c. Convergence-on-lock is authentic (FireWeapon has no
aim test); targetWithinRange = dist < (1-damage) x weaponRange
(FUN_004b9bdc). Port acquisition is a bring-up stand-in (hardwired lock
+ BT_FIRE_ARC cone) -> tracked as the next combat-fidelity step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- dpl2d API fully recovered from the binary recorders (@487f34-488630):
opcode model (points/lines/polyline/circle/color/width/matrix/push-pop),
CallList = INLINE include (state persists to caller), centered coordinate
frame (unit = half viewport height). game/reconstructed/dpl2d.cpp rework.
- BTReticleRenderable ctor @004cc40c transcribed with the authentic
calibration (originX .35, originY .25, scaleY .5, 0..1200m right range
ladder, bottom heading tape, FUN_004cd938 tick ladders, lock rings,
turn arrows); range caret slides from the live target range fed by the
mech4 targeting step (BTSetHudTargetRange).
- Weapon pips: the binary gate is IsDerivedFrom(0x511830 =
MechWeapon::ClassDerivations) [T1: part_014.c:5386 hard-aborts on missing
weapon attrs; part_012 counts + roster ORs capabilityFlags@+0x334] so ALL
7 BLH weapons register (3 lasers + 2 PPCs + 2 MissileLaunchers). Pip A
(lit, authored PipColor) on TargetWithinRange, else dark ring B.
- AddWeapon @004cdac0 store map corrected to the verified order
(part_014.c:4827-4837); both state attrs are literally named
"SimulationState" (strings @51d526/51d577) -> weapon simulationState.
- Mech roster this[0x1ef] renamed poweredSubsystems -> weaponRoster
(0x511830 is MechWeapon, not PoweredSubsystem=0x50f4bc); derivation-tag
table added to context/decomp-reference.md.
- Draw hook BTDrawReticle after the 3D scene, cockpit view only. Binary
Execute @004cdcf0 is an un-exported gap -> Draw dynamics [T3], tracked
in context/open-questions.md with the blx_cop canopy + PNAME pip meshes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answers "it's still there but invisible": not normal -- the real game removes
the dead entity (death row). Full entity teardown needs the mech render tree
unhooked from the renderer first (the remaining P5 follow-through), so until
then the burial transition makes the wreck behaviorally gone:
- collisionVolumeCount = 0 (the collision gather skips volume-less movers;
MoveCollisionVolume early-outs) -> no phantom blocking
- the player's target lock drops -> beams stop converging on / hitting the
empty spot, no phantom impact smoke
Verified: zero [damage] hits after burial; INERT fires one-shot at
"wreck buried".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user was right: a dead mech turns into a pile of wreckage. The authentic
1995 chain, recovered end-to-end:
kill -> the victim's per-mech DEATH ModelList ('blhdead'/'lokdead'/'owndead'/
'thrdead', .RES ids 22-25) -> its authored effects: 104 (the WRECK script) +
1007 (dnboom big explosion) + 1001 (ddthsmk rubble smoke plume) + a damage-
smoke burst (3/4/5/15).
Effect 104 = ExplosionScripts case 4 (part_008.c:2663, LIVE in the 1996 binary;
the "disabled" warning is case 6): loads the destroyed hulk + flamesml/flamebig
flame meshes with sweep flicker. Every mech ships its hulk (BLHDBR/MADDBR/
LOKDBR/... + GENDBR generic); the 1996 script hardcoded thrdbr.bgf (dev
shortcut) -- we use the victim's own.
Reconstructed as BTL4VideoRenderer::SwapToWreck: hide every segment mesh, hang
"<prefix>dbr.bgf" on the tree root (pending-swap if death precedes tree build);
routed from the engine's ExplosionClassID dispatch (effect 104 ->
BTSwapMechToWreck(explosion->GetEntityHit())). The kill now fires the
authentic 'blhdead' resource (manual 7+1 pfx calls removed -- the list carries
1007/1001 itself). Verified live:
[death] firing authentic death list 'blhdead' id=22
** effect_number = 104
[BTrender] wreck swap: victim -> 'blhdbr.bgf'
Follow-ups noted: mesh flames + hulk settle (cosmetic), DeathSplash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task #32 (death collapse animation) resolved by decomp evidence -- BT 4.11 has
NO collapse animation; the movementMode 5-8 fall latch (clips 0x1c-0x1f) is an
engine-lineage vestige:
(1) the clip-table loader FUN_004a80d4 fills slots 0x00-0x1b + 0x20 (bmp
knockdown) and returns -- slots 0x1c-0x1f are never written;
(2) mech+0x63c..0x648 appear in NO exported function;
(3) no fall clip exists in the shipped 27-clip set;
(4) firing the latch would bind resource id 0 -- a StaticAudioStream -- as
keyframes.
Authentic death modes are the FREEZE modes (IsDestroyed == mode 2||9), so
UpdateDeathState now settles straight to 9 (was a [T3] mode-5 guess that would
trip the garbage latch in the binary). The death READ = freeze + dnboom +
ddthsmk smoke plume + destroyed skins + shutdown; the wreck stands.
Also: damage-band effects orient toward the ATTACKER (impact frame) via
lastInflictingID -- which was declared but never written (recon gap); now
maintained by Mech::TakeDamageMessageHandler, unblocking the DamageZone LOD
same-attacker redirect too.
KB corrected + proofs recorded (combat-damage "Death SEQUENCE", open-questions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed
faithfully:
- STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111)
-- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit
silently no-op'd. Promoted to the named member Mech::damageLookupTable
(binary this[0x111], was mislabeled ammoExpended). New gotcha class recorded
(reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged.
- Fire path migrated off the stale vital-zone aim onto the completed STEP-6
unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the
exterior zone). No more invisible 1-shot kills; death via the authentic
cascade (~14 center-mass hits). Wreck stays TARGETED on kill (beams stop on
it); scoring latches off.
- SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43
unbound plugs/mech logged as an open question -- the binding itself is a gap).
- RemakeEntity (render damage swap): the 1996 render state machine's missing
Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each
segment's damage-zone graphic state (tree dtor doesn't cascade -> never
rebuild). Destroyed arms/guns visibly wreck (the only variants the RES
registers).
- BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer,
unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all
stubs). Parses the authentic VIDEO/*.PFX definitions via the [pfx_day]
psfxN mapping; premultiplied blending renders BOTH families from the same
data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK
ramps negative: impossible additively); depth-sorted billboards with a
radial-masked grit sprite; impact-frame orientation (.PFX offsets are
authored mech-local, -Z = out of the struck armor toward the shooter) for
weapon hits AND damage bands (via lastInflictingID, now maintained -- was
declared but never written). Both effect-number encodings route (raw dpl
<100 + WinTesla 1000+slot carried by the band resources). Death fires the
authentic dnboom (7) + ddthsmk smoke plume (1).
- Effects anchor at the impact point / damaged zone's segment, not the mech
origin (no more fire at the feet).
- Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain
verification; BT_PFX_ADD=1 flips the particle blend for A/B.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstructs the mech DEATH state machine — the un-exported master-perf death
branch (region 0x4a9770-0x4ab188) — from its EXPORTED consumers + the RP
VTV::DeathShutdown analog, wired into the active path (the bring-up drive
override bypasses the authentic Simulate where this normally lives).
Mech::UpdateDeathState() + IsMechDestroyed() (mech4.cpp), called for every mech
early in PerformAndWatch: on a vital kill (graphicAlarm >= 9, raised by the
damage side) it
1. sets movementMode = 5 -> the collapse clip's one-shot latch in
AdvanceBodyAnimation (fall direction 5-8 is un-exported -> 5 [T3]);
2. loops the roster calling Subsystem::DeathShutdown(1) (RP VTV::DeathShutdown
analog; the base is a no-op virtual, overrides act -- a SHUTDOWN not a
teardown, so it frees nothing and never removes the entity: the wreck STAYS);
3. next frame settles to movementMode = 9 -> IsDisabled -> locomotion frozen.
The drive's lone `movementMode = 1` write (mech4.cpp ~1309) is guarded on
!IsMechDestroyed so the death state is not clobbered back to a live gait.
Runtime-verified (forced kill, BT_DEATH_LOG=1):
[death] mech destroyed -> collapse + subsystem shutdown (wreck stays)
[death] mech settled -> disabled (IsDisabled=1, frozen wreck)
mech frozen in place, subsystem tick + renderer keep running, no crash, wreck
stays.
Also records the full death-sequence decomp map in combat-damage.md (the
exported consumers: AdvanceLeg/BodyAnimation collapse latch, IsDestroyed, the
MechDeathHandler effect engine; and the un-exported orchestration gap).
Deferred (honest): the visible collapse ANIMATION latch through the active path
(gait SM shows state=0 post-death; spawned mechs don't advance body anim);
MechDeathHandler (FUN_0042a984/FUN_0042aa2c -- the exported per-subsystem
destroyed-skin + explosion engine, still a stub) for death explosions; the
whole-mech DeathSplash radius damage (un-exported).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two deferred TakeDamage dispatches in Mech::ProcessCollision now fire,
unblocked by STEP 6 (the cylinder hit-location override that resolves zone==-1):
- Mover branch (:15324-15358): on a collision with another Mech, dispatch the
collision damage to it.
- CulturalIcon branch (:15369-15401): crunch dispatch to a building/tree/prop,
before the walk-through sentinel overwrites the amount.
Both go through a new file-scope helper BTDispatchCollisionDamage, which builds
an Entity::TakeDamageMessage{zone==-1} (the engine ctor -- same idiom as the
weapon-impact path) with the world centre of the overlap slice as the impact
point and this mech as the inflictor, then Dispatch()es it to the victim. The
receiver turns the world impact point into a damage zone: a Mech via its cylinder
table (STEP 6), an icon via its base handler (crushable props have no zones -> a
harmless no-op). Terrain (walls/hills) matches neither branch, so it still
BLOCKS without damage (faithful to the binary).
Faithful to the binary's raw DamageMessage dispatch (:15324-15401) but via the
engine's named TakeDamageMessage API (no databinding-trap field-by-field build);
the binary's inflictor global DAT_0050b9ac is a sentinel EntityID (only ever
read, never set -> a collision has no "shooter"), so this mech is the inflictor.
Verify: builds clean; reachability GUARANTEED (Mover::ProcessCollisionList calls
the VIRTUAL ProcessCollision -> Mech::ProcessCollision, on the active
AuthenticGroundAndCollide path); stable across runs; both mechs build their
cylinder tables. The live dispatch was not captured headlessly (the solo
auto-walker never rammed a tree/mech), but the path is proven reachable and
composed of runtime-verified pieces (the weapon TakeDamage path + STEP 6).
Also validated STEP 6's height ref: collisionTemplate->maxY ~= 7.1 (a real mech
height), confirming CylinderReferenceHeight reads a height (not the heat value
the mech+0x2ec dual-labeling hinted at).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Mech per-impact hit-location resolver (the cylinder damage table) is now
functional, wired, and runtime-verified [T2]. Unaimed (zone==-1) hits — the
collision-damage path — now resolve an impact point to a damage zone via the
authentic height x angle grid + weighted dice roll, instead of dropping.
dmgtable.cpp/.hpp was a non-functional skeleton on no-op ReconTable/stream
shims; backed it with real std::vector storage and fixed 5 latent runtime bugs:
- ReadEntries now consumes the leading cell name-string ([i32 len][len+1])
- PieSlice ctor reads rotateWithTorso into the correct member
- SelectSlice direct-indexes (was int lookup on a float-keyed table)
- ResolveHit returns the zone (chains SelectSlice -> SelectZone)
- real MemoryStream::ReadBytes (was a variadic no-op)
mech.cpp ctor: replaced the empty-name StandingAnimation stub with the real
load — FindResourceDescription(dzRes->resourceName, type 0x1d) -> stream ->
new DamageLookupTable, cached at mech[0x111]; ~Mech deletes it.
Mech::TakeDamageMessageHandler override registered (MESSAGE_ENTRY overlays
Entity's by ID): on invalidDamageZone, resolve via the table then base-route;
aimed reticle hits pass through unchanged.
Three named accessors (no databinding-trap raw reads): WorldToLocal
(localToWorld.MultiplyByInverse), CylinderReferenceHeight (standingTemplateMaxY
== collisionTemplate->maxY == binary mech+0x2ec[+0xc]), TorsoHeading via a
BTGetTorsoTwist bridge in torso.cpp (Torso::CurrentTwist == torso+0x1d8;
torso.hpp cannot be included into mech.cpp — subsystem-stub collision).
Stream format + geometry + roll + handler were all byte-verified against the
shipped BTL4.RES type-29 resources (18 tables, exact consumption) and the
disassembly (FUN_0049eb54/e678/de14, glue 0x49ed0c, handler @0x4a037a).
Runtime: boots clean, "[cyl] table 'bhk1' layers=7" (exact byte-verified layer
count, found by name), mech spawns + walks, no asserts/AV/0xCDCDCDCD. Env gate
BT_CYL_LOG=1. Unblocks collision-damage application.
KB updated (combat-damage.md STEP 6 COMPLETE, open-questions.md marked done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the 'correct errors' convention: the cited FUN_004a0230/FUN_0049ed0c don't exist. The real
CylinderDamageZoneTable = a list of 0x30-byte entries (FUN_0049e740) from resource 0x1d; the recon
builds the table (FUN_0049ea48, mislabeled StandingAnimation @Mech[0x111]) with an EMPTY name -> 0
entries -> no-op. The lookup + Mech::TakeDamageMessageHandler override are NOT in the exported decomp
(need disassembly). Recorded in combat-damage + open-questions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Propagate the verified finding (0xBD3 = damage/explosion hub @0x434, NOT the valve/message
gate; the valve/Myomers gates read the owning BTPlayer @mech+0x190) into the topic files that
still carried the old claim: decomp-reference (ClassID row + both offset rows), gauges-hud,
subsystems, open-questions (Myomers coupling), + frontmatter. Graph validates clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The valve/Myomers 'gates' (FUN_004ac9c8 / FUN_004ad7d4) read the owning BTPlayer's
+0x274 / +0x260 (mech+0x190 = the player, bound by FUN_0049f624). So there's no new
subsystem to build -- the wiring is 2 gate accessors -> GetPlayerLink() named members.
Open: the true semantics of player+0x260/0x274 (scoring names showKills/roleClassIndex
may not match the gate use) + the FUN_004ad7d4 HeatModelActive-vs-OwnerAdvancedDamage
label conflict + these are likely MODE flags authentically-off in the basic mission.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Risk-4 investigation (before wiring WAVE 8) found my earlier 'keystone unblocks 4 things'
claim was wrong. Verified from the binary:
- 0xBD3 SubsystemMessageManager = a damage/explosion consolidation hub (ConsolidateAndSendDamage,
weaponExplosions), cached to Mech[0x10d]=0x434, mislabeled 'controlsMapper' in our recon (the
live drive squats there; the real mapper is roster slot 0 via SetMappingSubsystem).
- The valve MoveValve guard (FUN_004ac9c8 ->owner+0x190+0x274) AND the Myomers gate
(FUN_004ad7d4 ->owner+0x190+0x260) read a DIFFERENT object at mech+0x190 (!= 0x434). So 0xBD3
does NOT gate the valve/Myomers/MessageBoard; mech+0x190 (unidentified, no decompiled writer yet)
is the real keystone for those gauge-adjacent routes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>