Commit Graph
5 Commits
Author SHA1 Message Date
arcattackandClaude Opus 5 26e678e570 #67 part 2, ROOT-CAUSED AND FIXED: 1995 latent uninitialized fields, exposed by the port's allocator
THE HUNT (the deterministic rig repro made it a two-hour arc):
  1. cdb write-watch armed from the Torso ctor (bp plants `ba w4 this+0x21c`
     per torso -- ASLR-proof).  The poison reproduced (atUpd=-1250 this run;
     -3750 and int-15-as-float before)... and the watch stayed SILENT.  Nobody
     writes the garbage.  The field is never INITIALIZED.
  2. Confirmed in our ctor reconstruction: it inits every neighbour but skips
     targetTwist @0x218 and twistAtUpdate @0x21C (the function that zeroes
     them is a death-reset handler, not the ctor).
  3. Confirmed in the BINARY: the real ctor @004b6b0c contains no store to
     either offset -- a genuine 1995 latent bug (uninitialized read on the
     copy path).
  4. Why the pod never showed it: MemoryBlock arenas carve fresh OS-zeroed
     pages, one pool per type, low churn -- first allocations read as zero.
     The engine's own DEBUG_NEW_ON NaN-fill proves the developers knew the
     hazard class.  Why WE show it: mechrecon.hpp's Memory::Allocate shim
     ("a plain heap allocation is behaviour-equivalent" -- false) recycles
     dirty heap.  Replicant torsos spawned with garbage twist targets; the
     limit clamp turned any garbage into FULL TWIST -- rendered "twisted full
     right while not using TT" to every peer (playtest night 4, 3 reporters).

THE FIX -- environmental, class-wide, byte-faithful to the binary's code:
Memory::Allocate / AllocateArray / Alloc now ZERO-FILL, reproducing the pod's
EFFECTIVE allocation semantics for every never-stored field in every
reconstructed factory at once (Torso, Reservoir, all of them).  No ctor gains
stores the binary lacks.

PROVEN on the rig: the replicant thor now spawns cur=0 target=0 atUpd=0
(was cur=-3.31613 = hard against the limit).  Teardown clean.

Ships with part 1 (the dead-reckoning clock fix) in the next zip.  Remaining
on #67 for the next games night: live confirmation that twist TRACKING looks
right in play, and whether the fire-from-centre facet (very plausibly the same
never-stored-field class, now zeroed) is cured with it.

Tools kept: scratchpad/torso_watch.cdb + rig_watch.ps1 (the ctor-armed
write-watch pattern -- reusable for any "who wrote this field" hunt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:42:37 -05:00
arcattackandClaude Fable 5 5ae4410914 Gitea #46: ammo bay fire now DETONATES and KILLS (+ #47 fire icon) -- three stacked kill-switches removed, the fuse decoded, and a vptr-alias corruption caught by regression
Field report (night 3): "two ammo bay fires and no death" (Cyd + RajelAran; one
purged, one left burning).  Root cause = THREE independent kill-switches stacked
on the same path, all in ammobin.cpp:

  1. `GameClock::Now() { return 0; }` -- `cookOffTime < Now()` was `0 < 0`, so
     an ARMED bay fire never detonated.
  2. `InjectHeat(void*) {}` -- the detonation body was a no-op.
  3. The bin's Damage record was never stamped -- 0 damage of type 0 (which the
     mech TakeDamage handler drops) even if 1+2 had fired.

THE FUSE (raw disasm, scratchpad/disammo.py): the old "RandomDelay" was a Ghidra
carve artifact -- FUN_004dcd94 is __ftol and the export DROPPED the caller's x87
expression.  The real bytes @004bd450: fld 10.0 / fmul [ticksPerSecond] /
fadd 0.5 / __ftol -- a FIXED 10.0-SECOND fuse in clock ticks.  New gotcha #19
(reconstruction-gotchas.md) documents the __ftol export blind spot.

THE DAMAGE RECORD: bin+0x1F0..0x21C is a real engine `Damage` (FUN_0041db7c IS
Damage::Damage(), byte-matched to T0 DAMAGE.cpp).  The linked ProjectileWeapon's
ctor @004bc3fc stamps it from weapon->damageData @0x3A8 via
owner->roster[0x128][res+0x1C0] -- projweap.cpp's old comment called this "the
bin's HUD display block ... wired in the AmmoBin family"; both halves were wrong
and it was wired nowhere.  Now stamped (before MissileLauncher's ctor divides by
missileCount -- a missile bin authentically holds the per-SALVO amount).

THE DETONATION (@004ac274 = MechSubsystem::DistributeCriticalHit -- the old
"HeatableSubsystem::InjectHeat" label was wrong, and the old reconstruction
iterated a stand-in CriticalChain whose First()/Next() returned 0):
statusAlarm pulse Exploding(2)->Destroyed(1) (slot 13 = the printSimulationState
state PRINT @004ac8c0, not an "explosion notify"), own private zone pinned
destroyed, then collect the mech DamageZones whose crit entries plug the bin
(the binary filters plug classID 0x4E = DamageZoneClassID -- VDATA.h idx 78,
cross-checked via idx 28 = AudioStateTrigger), split the amount evenly, and send
the OWNER one full Entity::TakeDamageMessage per zone: inflictingEntity = SELF,
damageZone = the zone index, inflictingSubsystemID = the bin (the message-
manager explosion-bundling key, ENTITY3.h's own NOTE), printing the binary's
exact "ammo explosion damaging <zoneName>" @0050df61.
Port shape: Mech::AmmoExplosionFanOut (mechdmg.cpp) behind a databinding bridge;
guarded deviation: zoneCount==0 warns instead of the binary's unguarded divide.
CriticalChain/CriticalEntry stand-ins DELETED from mechrecon.hpp.

VERIFIED LIVE (BT_BAYTEST hook = message 1, the crit-induced arm channel):
  scratchpad/baytest.py : arm -> 10s -> "20 rounds x 35 = 700 (type 2)" ->
    "ammo explosion damaging dz_ltorso" -> zone cascade -> mech DESTROYED
    (authentic death list).
  scratchpad/baypurge.py: arm -> eject-hold purge -> "bay fire EXTINGUISHED
    (bin empty)", no detonation.
  scratchpad/sim3.py    : the HEAT route arms organically in combat (overheated
    AFC100), detonates "11 x 25 = 275 (type 1)" split across dz_larm + dz_lgun.
  BAYBOOM matchlog record added for MP field forensics.

FIX-OF-THE-FIX (caught by the sim3 regression, would have shipped a crash):
MechSubsystem's ReconDamageZone proxy puts structureLevel at OFFSET 0 -- which
ALIASES THE REAL DamageZone's VTABLE POINTER (the private zone is `new
DamageZone`, mechsub.cpp:154; mechsub.hpp:260 documents the alias).  My first
DistributeCriticalHit kept the old body's `damageZone->structureLevel = 1.0f`
and OVERWROTE THE ZONE'S VPTR with 0x3F800000; the respawn sweep's virtual
SetGraphicState (vtable+0xC) then called through it -> AV at 0x3f80000c in
RespawnRepair, one frame after a bay-fire death.  ALL EIGHT proxy-view sites in
mechsub.cpp swept to the engine view (((DamageZone*)damageZone)->damageLevel
@0x158) -- including two silently-wrong LIVE readers: GetStatusFlags (vptr as
float -> always "intact") and ApplyDamageAndMeasure (the crit cascade's
measure).  Ruled out first by evidence: the weapon->bin stamps were all clean
(six stamps, all classID 0xbcb, logged).

#47 (half 1 -- the FIRE ICON): BallisticWeaponCluster::Execute @004c9a38 reads
bin+0x18C = cookOffArmed into the btefire.pcc TwoState, and while armed computes
(Now - cookOffTime)/ticksPerSecond -- the COOK-OFF COUNTDOWN -- into the numeric
beside it.  The old reconstruction misread 0x18C as "the reload state" and
bridged the icon to BTAmmoBinFeeding, so it blinked on every feed and never lit
on a bay fire (RajelAran: "it doesn't").  Now driven by the
BTAmmoBinCookOffArmed/CookOffTime complete-type bridges.  [T2 -- the data path
is rig-verified; the pixels await the next live session.]

#47 (half 2 -- the ENG-BUTTON FLASH): fully mapped, deliberately NOT built this
session.  The authored data SHIPS (BTL4.RES carries exactly one type-31
GaugeAlarmStream); the chain is alarm SetLevel -> gauge-watcher socket ->
Renderer msg 7 -> GaugeAlarmManager::Activate @00448d00 (T0) -> the BTL4
override @004cc148..@004cc2fc (btl4galm.cpp's provenance note claiming "no
override body exists" is WRONG -- corrected in-file) -> LampManager::FindLamp
@00444c80 -> Lamp::SetAlertState @00444e64 (flash counter) -> the L4 flush
@00474e94 emitting flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239).
Missing: the override bodies, the gauge-watcher sender, the aux-button lamps.
3-piece plan in context/open-questions.md.

Also logged: HandleMessage is vtable slot 8/9 in the binary but NON-virtual
across 10 reconstruction classes (bit the BT_BAYTEST hook; typed call used, gap
documented in open-questions).

KB: combat-damage.md (the full cook-off section), decomp-reference.md (the
cluster addresses + the GaugeAlarm/lamp map + BT_BAYTEST env), gauges-hud.md
(the fire-icon correction), reconstruction-gotchas.md #19 (__ftol),
open-questions.md (2 entries), btl4galm.cpp provenance correction.
checkctx CLEAN.  40 LNK2019 unchanged (the two pre-existing families).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 14:58:16 -05:00
arcattackandClaude Opus 4.8 0e2401fb52 Audio Phase 4d (AUDIO_FIDELITY F5): footStep is the authored CONTACT LEVEL
The binary's per-frame perf (disasm @0x4a9e80-0x4a9eb6 leg / @0x4aba86-
0x4abab9 body) computes footStep@0x394 = (jointlocal.y <= *footStepThreshold)
with the threshold pointer = SequenceController+0x20 = &ANI hdr[2] -- the
header word the engine's own AnimationInstance captures (JMOVER.cpp:1415)
and our SelectSequence skipped.  States 0/1 retain the value.

Port: SelectSequence captures hdr[2]; PerformAndWatch evaluates the contact
level per frame from the cached jointlocal root joint (leg channel drives
the pose); the 150 ms clip-transition pulse + decay are RETIRED (steps fired
at clip boundaries with a fixed width; clips whose root crosses twice or
never counted wrong).

Live verification (30s walk): the authored threshold decodes real
(-0.232 root height); rootY oscillates across it per stride (-0.26 contact /
-0.19 swing) with clean 0->1->0 transitions per leg state (5/6/7); 34
footfall deliveries at stride rate with per-stride varying gains.

Also closes F23(2): the 17 authored AnimationState trigger states fire
empirically -- the EngineShiftFwd/Rev heard during the gait dead-band hunt
WERE states 10/11/14/15; the runtime clip numbering is correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:16:14 -05:00
arcattackandClaude Opus 4.8 a8eb8a427f MP: FIX peer spin hang/divergence -- exact quaternion integration + frequent orientation (task #50)
Answers 'how did the original handle this?' from the decomp (subagent hunt):
the 1995 binary's replicant reckoner (FUN_004ab1c8 -> FUN_00409f58, part_000.c:9359)
integrates heading EXACTLY: build a unit axis-angle rotation quaternion from
angularVelocity*dt ({axis*sin(t/2), cos(t/2)}) and Hamilton-multiply it onto the
heading (FUN_00409d9c) -- exact for any timestep, stays on the unit sphere.  It
further carries the full orientation quaternion in the FREQUENT pose record
(FUN_0040a938, 7-float pose), so the dead-reckon gap stays tiny.

Our reconstruction diverged two ways, both fixed:
 1. ReconQuatIntegrate (mechrecon.hpp) -- the reconstruction of FUN_00409f58 -- was
    STUBBED as , a crude small-angle VECTOR add.  Restored to
    the real exact axis-angle composition.  (A 'no stand-ins' violation: the comment
    even wrongly claimed Quaternion::Add == FUN_00409f58.)
 2. The engine Mover reckoner (MOVER.cpp AcceleratedDeadReckoner/LinearDeadReckoner)
    also did the vector Add on projectedOrigin.angularPosition -> over a long peer
    record gap it diverged to ~180deg then snapped (the reported spin HANG/hesitation).
    Routed both through a new ExactAngularProject() helper (same exact math).
 3. Orientation only rode the sparse type-4 resync; during a PURE spin the linear
    dense-send never fires (not translating), so the gap ballooned (~1.6s) and the now-
    exact projection sat far ahead -> the slerp jumped.  Added an ANGULAR dense-send
    (resync every frame while |yawRate|>0.1), mirroring the original's frequent-
    orientation model -> gap stays tiny -> smooth.

Verified live-autonomous (BT_AUTODRIVE+BT_FORCE_TURN + BT_RENDHDG render-rate probe):
the ~180deg divergence + multi-radian snaps are GONE (rendered maxStep 0.05-0.10 rad,
no jumps).  User confirms: no frame hang/hesitation.  MOVER.cpp change is strictly
more correct (exact==crude for the small per-frame master case; only large-gap peer
extrapolation changes), so walking is unaffected.

KNOWN REMAINING (separate, smaller): the turn-STEP leg animation (trn clip, mech2.cpp
advance_normally) runs at a FIXED idleStrideScale cadence that does not scale with the
rotation rate, so the legs lag/skip vs the (now-correct) body rotation.  Next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:05:16 -05:00
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00