Commit Graph
15 Commits
Author SHA1 Message Date
arcattackandClaude Fable 5 bf87360c42 Buried wreck goes INERT: collision off + target lock dropped
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>
2026-07-08 16:01:46 -05:00
arcattackandClaude Fable 5 5236d4263a Wreck sink (quadratic burial) + ldbr debris field -- the 1996 script completed
Answers "does the wreck fade away?": YES, by sinking.  FUN_00456410 (the 1996
sink renderable) computes offsetY = rate * t^2; the hulk's authored rate is
-0.025 -> the ~7-unit hulk is fully underground ~17s after the kill.  The
script also pairs the standing hulk with the LDBR strewn-debris field (12x13u
flat scatter), parented together and sinking together.

Also verified from the mesh data: BLHDBR (1537 verts -- more than the intact
torso) IS the authored Blackhawk wreck: the classic standing-leg-in-rubble
sculpt.  The "just a leg standing there" report is the authentic art.
(THRDBR -- the mesh the 1996 script hardcoded -- parses to ZERO vertices;
more evidence the hardcode was an unfinished dev shortcut.)

Implementation: SwapToWreck adds the ldbr piece; TickWreck applies the
quadratic sink per frame (driven from the dead mech's UpdateDeathState),
hides both pieces at burial and reports it so the wreck-smoke re-arm stops
with the wreck.  DPLStaticChildRenderable::SetOffsetTranslation added
(Execute re-reads OrientationMatrix per frame -- same in-place idiom as
SetDrawObj).

Lifecycle verified live: kill -> 'blhdbr.bgf' + ldbr debris -> smoke re-arm
@10s -> wreck buried @~17s -> smoke stops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:55:01 -05:00
arcattackandClaude Fable 5 c0fa6bf21a Death = the burning WRECK-HULK swap (effect 104) -- the authentic chain, reconstructed
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>
2026-07-08 15:43:08 -05:00
arcattackandClaude Fable 5 b44c0f98f6 PFX continuous-rate emission (data-verified) + persistent wreck smoke
The one-burst emitter batching was wrong: in EVERY shipped .PFX,
maxIssue/rate == release_period (DAFC 25/100~=0.2, DNBOOM 35/150~=0.2, DDAM2
35/16~=2, DDTHSMK 30/3=10) -- release_period is the EMISSION WINDOW and rate
is particles/second emitted continuously across it.  The death smoke plume is
authored as a 3/sec trickle for 10 seconds (+6s particle life), not one
same-frame puff.  Emitters now integrate rate*dt until maximum_issue.

Wreck smoke (port addition, [T3]): a destroyed mech re-arms the death/rubble
smoke plume (psfx 1, DDTHSMK) every 10s -- its own authored window -- so the
dead hulk visibly keeps smoking instead of standing there looking alive (the
freeze-death is invisible on an already-stationary target).  New member
Mech::wreckSmokeTimer; UpdateDeathState takes dt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:13:10 -05:00
arcattackandClaude Fable 5 d9254736ab Death is a FREEZE, not a collapse -- fall-latch vestige proven; band-effect impact frames
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>
2026-07-08 14:50:12 -05:00
arcattackandClaude Fable 5 a3d67cc639 Combat visible + killable: Wword root-cause fix, .PFX effect layer, RemakeEntity swap
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>
2026-07-08 14:43:32 -05:00
arcattackandClaude Opus 4.8 7c455303bd MechDeathHandler: per-zone destroyed-skins + explosions (faithful, exported)
Fixes the "kills are invisible" report: a mech died only at the state level, with
no visible destruction on its body.  Root cause: the per-zone damage-state
descriptor table (binary zone+0xd4) that drives destroyed-skins + explosions was
never built -- its loader (Mech__DamageZone::LoadCriticalSubsystems / FUN_0041e4a8)
was a no-op stub, and MechDeathHandler itself was a no-op stub.  The surrounding
plumbing (the per-zone load loop over the type-0x1e resource, the death-handler
slot) was already correct.  Whole pipeline is EXPORTED -- no stand-ins.

Reconstructed:
- Mech__DamageZone descriptor table (binary this+0xd4): un-stubbed
  LoadCriticalSubsystems (FUN_0041e4a8/FUN_0042a748/FUN_0042a2c8) to parse the
  real type-0x1e stream -- entry = [f32 DamageLevel][i32 EffectResource]
  [i32 GraphicState][f32 TimeDelay], ascending by DamageLevel.  Verified live:
  6 entries/zone, first threshold 0.2, effect 26, across all 40 zones, no crash.
- The three lookups (FUN_0042a664 by-level / FUN_0042a5f4 crossing / FUN_0042a6c4
  by-graphic-state) as Mech__DamageZone methods.
- The real MechDeathHandler (FUN_0042a984 ctor / FUN_0042aa2c Performance /
  FUN_0042a9f4 dtor), replacing the stub: each tick it walks the zones and, as a
  zone's damageLevel rises across a descriptor threshold, fires that entry's
  explosion (binary +0xb8 & 4) and, on destruction, the Destroyed-graphic
  descriptor's explosion (+0xb8 & 8), applying the descriptor's GraphicState (the
  destroyed skin) to the zone.  Runs for EVERY mech, so the enemy visibly falls
  apart as it dies.

Integration: the binary ticks MechDeathHandler off the mech's Performance list
(mech+0xbc), which the bring-up drive override bypasses, so Mech::PerformAndWatch
drives Tick() directly (same approach as UpdateDeathState).  Effect spawn uses the
established Explosion::Make port (BTSpawnDamageEffect) -- the authentic dispatch
(class-5 message -> the 0xBD3 SubsystemMessageManager effect manager) is unported.

Runtime: builds clean, boots clean, 42 descriptor tables load with sensible
byte-aligned data, no crash.  Live effect firing is combat-triggered (verified by
the load + the wiring; the [deathfx] threshold-crossing log fires under BT_DEATH_LOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 11:18:30 -05:00
arcattackandClaude Opus 4.8 a9467481c5 Death-state sequence: collapse + subsystem shutdown + freeze (wreck stays)
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>
2026-07-08 09:30:34 -05:00
arcattackandClaude Opus 4.8 2af401eef8 Collision damage applied: mech-vs-mech + icon-crunch via the STEP-6 unaimed path
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>
2026-07-08 08:54:21 -05:00
arcattackandClaude Opus 4.8 34aaa7dda4 gauge wave P3: wire the combat scoring feed (KILLS tracks combat)
The Comm roster + score gauges read 0 for 4 independent reasons (none was handler
logic -- the BTPlayer ScoreMessage/VehicleDead/ScoreInflicted handlers were already
reconstructed).  Mapped by the scoring-feed-decode workflow; fixed all 4:

- NO PRODUCER: combat only dispatched Entity::TakeDamageMessage, never a scoring
  message.  Added producers (btplayer.cpp bridges, called from mech4.cpp): per-hit
  ScoreInflicted at the beam (:2171) + projectile (:831) damage dispatch -> SCORE;
  KillScore at the TARGET-DESTROYED edge (:2198) -> KILLS.  senderMechID = the
  VICTIM (so the local player is credited via the !=our-mech branch, not the
  suicide-negate branch); dispatched to application->GetMissionPlayer().
- CROSS-FAMILY MECH OFFSETS: MECH_OWNING_PLAYER/TONNAGE/DAMAGE_BIAS read raw binary
  offsets (garbage in our 0x638 Mech).  MECH_OWNING_PLAYER -> Entity::GetPlayerLink()
  (NULL for the ownerless dummy); tonnage/bias stubbed 1.0/0.0 (bring-up).
- NULL scenarioRole: the BTMission role registry has no WinTesla analog, so every
  award path NULL-deref'd.  CalcInflictedScore returns the neutral (damage+bias)
  when scenarioRole==0; DamageReceived guarded.
- NULL-owner dummy: the KillScore sender-owner increment + StatusMessage now guard
  GetPlayerLink() (the dummy has none) so a solo kill credits only the local player.
- DATABINDING: the PilotList read KILLS/DEATHS at raw offsets (pilot+0x27c/+0x200)
  that don't match our compiled layout -> silent 0.  Repointed to the compiled
  members via bridges (BTPilotKills/BTPilotDeaths -> BTPlayer::GetKillCount/GetDeaths).
  ⚠ ROOT-CAUSE of the empty scoreboard: the PilotList SELECT-TARGET highlight did a
  raw-offset deref (*(local+0x284) objectiveMech, then *(tgt+0x190)) that AV'd on our
  layout -- caught silently by the SEH GuardedExecute, aborting Execute BEFORE the
  KILLS/DEATHS draw.  Replaced with BTPilotIsSelected (accessors + GetPlayerLink).
- SCORE persistence: PlayerSimulation's 10s flush zeroed currentScore (the binary's
  console delta); now only flushes/zeros when a console host exists (solo keeps the
  running score for the gauge + CalcRanking).

Verified DBASE+dev gauges (BT_SCORE_LOG): SCORE climbs +5.83/hit -> 164; on the
kill KILLS 0->1, and the Comm roster RENDERS KILLS=1 (screenshot).  No crash.
Follow-ups (noted): DEATHS=1 is a pre-existing spawn/respawn-handshake artifact
(not from the producers -- the dummy's GetPlayerLink is NULL so no VehicleDead is
posted; confirmed killCount=1 not 2); RANK=-1 is CalcRanking's non-scoring mark
(Plasma serial surface, invisible on dev); MP VehicleDead/tonnage/role registry
remain per the plan.  Diagnostics kept: BT_SCORE_LOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:03:38 -05:00
arcattackandClaude Opus 4.8 c850ffa26c WAVE 7 Phase B polish: missiles launch from the mech's missile ports + look like missiles
Two issues the user hit driving the Mad Cat: (1) projectiles appeared to launch from the
mech's FEET -- MechWeapon::GetMuzzlePoint falls back to the mech origin (localOrigin) when the
weapon's mount segment (this+0xdc) doesn't resolve, so the passed muzzle was at ground level;
(2) they rendered as a thin laser-like line, not a missile.

Fix: BTPushProjectile now takes the SHOOTER mech and resolves the real launch port by NAME
(sitermissleport / sitelmissleport, alternating L/R for a salvo look; then torso ports, then
gun ports; then a raised fallback) -- the same segment-name -> world-transform mechanism the
visible laser beams use for the gun ports.  The tracer is now a 3-part missile look (dim smoke
trail behind + orange body + hot flame tip) instead of a single thin beam.  FireWeapon (both
ProjectileWeapon and MissileLauncher) passes owner as the shooter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:55:53 -05:00
arcattackandClaude Opus 4.8 f52bf057e6 WAVE 7 Phase B: the Mad Cat's LRMs LAUNCH flying missiles that fly + damage (autocannon too)
The byte-exact WORLD-ENTITY reconstruction (Projectile @4be1bc 0x340 / Missile @4bf5b4 0x368)
is infeasible on the 2007 engine: measured sizeof(engine Entity)=0x1BC vs the 1995 binary's
0x300, so the reconstruction's raw base-offset reads (velocity@0x1dc, roster@0x124, motion@0x250)
read GARBAGE on the engine (the Mech 0x638-vs-0x854 gap, but the entity integrator DEPENDS on
those offsets).  So -- like the mech drive and the beam renderer -- flying projectiles are a PORT
reconstruction (BTPushProjectile/BTUpdateProjectiles in mech4.cpp, a static array + stack
messages, ZERO heap ops): seeded from the launcher's fire with the decomp's real muzzle
(GetMuzzlePoint) / launch speed (|launchVelocity|) / per-shot damage (damageData, split across
missileCount), they fly to the target (tracer via BTPushBeam) and deliver the weapon's damage on
impact through the SAME Entity::TakeDamage path as the beam (aim Mech::FirstVitalZone()).

THREE bring-up fixes were needed to make a projectile weapon fire at all (found by tracing):
  1. TRIGGER: fireImpulse was only driven for the Emitter; ProjectileWeaponSimulation now sets
     fireImpulse = gBTWeaponTrigger too (else CheckFireEdge never sees an edge).
  2. AMMO BIN: OwnerSubsystemCount/OwnerSubsystem were stubbed ->0, so ammoBinLink never resolved
     and ConsumeRound always failed; redirected to the real roster (owner->GetSubsystemCount()/
     GetSubsystem(i) -- the AmmoBin 0xBCB constructs before the weapons 0xBCD/0xBD0).
  3. JAM ROLL: UniformRandom() was stubbed `return 0.0f`, so CheckForJam's `0 < jamChance` ALWAYS
     jammed (a projectile weapon could NEVER fire); replaced with a real LCG [0,1) (fires
     ~1-jamChance of the time -- authentic occasional jams).
Also: the mech's own target slot (owner+0x388) isn't populated in bring-up (the visible fire
targets the gEnemyMech global), so BTPushProjectile falls back to gEnemyMech.

Verified: Mad Cat PUSH=62 / IMPACT=31 (LRM missiles 3.33 dmg each split across the salvo + AFC100
autocannon 25 dmg), TARGET DESTROYED, 0 crashes, construction heapcheck-clean; BLH un-regressed
(also fires its ballistic weapon now).  Diagnostics BT_PROJ_LOG ([projectile] PUSH/IMPACT).

REMAINING (deferred): the byte-exact world-entity Missile (Projectile : Mover, MP-replicable via
Registry::MakeEntity) -- the port projectile is master-local only (no MP replication); the real
per-weapon fire-rate/heat wiring off the subsystem sim (mech4 beam path is still the bring-up harness).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:19:38 -05:00
arcattackandClaude Opus 4.8 9d82be46a1 P7: subsystem-tree alarm unification -- the whole PoweredSubsystem weapon/power subtree is byte-exact
The reconstruction modeled the binary's 0x54 AlarmIndicator (FUN_0041b9ec) with undersized
stand-ins (AlarmIndicator==ReconAlarm==4B; HeatAlarm==8B) across the entire subsystem tree, so
every field above an alarm sat at the wrong compiled offset.  Retype every such stand-in to
GaugeAlarm54(0x54) and de-phantom each class against its ctor, so the whole PoweredSubsystem
subtree becomes byte-exact.  An 8-agent read-only decomp-mapping workflow decoded every ctor
first; then hands-on implementation.  static_assert-locked chain (verified vs the raw ctors):

  HeatSink 0x1D0
   -> PoweredSubsystem 0x31C   retype electricalStateAlarm@0x264 + modeAlarm@0x2B8   @004b0f74
   -> MechWeapon      0x3F0   retype weaponAlarm@0x350; delete 5 phantom tail fields  @004b99a8
   -> Emitter         0x478   delete outputVoltage/beamLengthRatio/firingArmed aliases
                              + beamHit*/beamColor/beamHitData/energyRampTime phantoms;
                              retype beamOrientation EulerAngles->Quaternion(16B)       @004bb120
   -> PPC             0x478   (no own fields)                                           @004bb888
  PoweredSubsystem -> Sensor  0x328   (no alarm; 3 own fields)                          @004b1d18
  PoweredSubsystem -> Myomers 0x358   (no alarm)                                        @004b8fec

New systemic bug-class instances fixed (added to the CLAUDE.md checklist):
  * alias field    - Emitter outputVoltage==inherited rechargeLevel@0x320; beamLengthRatio==
                     beamScale.z@0x434; firingArmed==inherited useConfiguredPip@0x3E0
  * phantom field  - MechWeapon segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint
                     (past 0x3F0); Emitter beamHitPoint/beamImpact/beamImpactScalar/beamColor/
                     beamHitData/energyRampTime (binary writes inherited damageData/voltageScale
                     or the value is a method local)

Non-layout fixes required in the same wave:
  * outputVoltage->rechargeLevel also in the compiled GAUSS.CPP:74/93 (external readers of the
    removed Emitter field -- must grep EVERY TU, not just the class's own .cpp)
  * MechWeapon::GetMuzzlePoint reimplemented faithfully (removed muzzlePoint collided with
    Emitter's own fields at 0x3F0) via a BTResolveWeaponMuzzle void* bridge in mech4.cpp,
    resolving the weapon's mount segment (inherited this+0xdc) through the owner segment table
  * DetachFromVoltageSource fixed to set electricalStateAlarm not modeAlarm (raw @004b0e30
    writes the 0x264 alarm)

The vehicleSubSystems aux-screen gauge raw reads (btl4gau2.cpp:868/952 at subsystem+0x2b8/+0x278)
and the Sensor RadarPercent path now read the CORRECT byte offsets (garbage under the short layout).

Verified: combat DESTROYED-in-8, 28 shots, 0 crashes, heat heatEnergy=1.34e7, every static_assert
lock passes, heapcheck-clean through construction (the phase the isolated PoweredSubsystem retype
had overflowed).  LESSON: a factory-bridge runtime Check(sizeof<=alloc) does NOT fail the build --
only a static_assert sizeof lock catches alloc overflow at compile time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 11:49:58 -05:00
arcattackandClaude Opus 4.8 9e31408ba2 gauges: publish Mech attribute table (dense prefix 0x15..0x21 -> LinearSpeed live)
mech.cpp shipped an EMPTY, unchained Mech::AttributeIndex (a default-constructed
AttributeIndexSet passed to DefaultData), so Simulation::GetAttributePointer
severed the whole parent chain -> EVERY bare Mech gauge binding (LinearSpeed /
MaxRunSpeed / DuckState / RadarRange / ...) resolved to NullAttribute.

Add the Mech AttributeID enum (full binary set 0x15..0x38, VA 0x50be84) + a
Mech::AttributePointers[] table + Mech::GetAttributeIndex() chained to
JointedMover::GetAttributeIndex(), and change DefaultData to pass it.

DENSE-TABLE HAZARD (systemic): AttributeIndexSet::Build (SIMULATE.cpp:565) sizes
the built index to max(id) and Find (SIMULATE.cpp:663) strcmps EVERY slot, and
Build does NOT zero gap slots -> an unpublished id between the parent's
NextAttributeID and the max published id holds a garbage entryName -> AV on the
next name lookup.  So the table is a DENSE PREFIX 0x15..0x21 (JointedMover's
NextAttributeID through LinearSpeed): the ids the BLH cockpit doesn't bind
(collision/eyepoint/reticle/footstep/anim-state) point at one shared read-only
attrPad member; CurrentSpeed/MaxRunSpeed bind to the existing gait members
legCycleSpeed/reverseStrideLength; LinearSpeed binds a new linearSpeed member
populated each frame in PerformAndWatch (|adv|/dt = forward ground speed).  Ids
0x22..0x38 stay declared in the enum for later extension (RadarRange/DuckState
when the map/duck widgets land) but are NOT published yet.  New members appended
to Mech's own region (no locked base/gait offset shifts); inited in the ctor.

Verified live (BT_DEV_GAUGES + drive): the radar-surface SPEED readout
(numericSpeed(LinearSpeed), a base engine primitive already drawing NULL) now
shows 225 (~63 u/s in the widget's display units) instead of 0, tracking the
mech's motion.  Heat surface CurrentTemperature=77 un-regressed.  Combat un-
regressed (TARGET DESTROYED after 8 hits), 0 attribute-not-found, and Mech
construction + attribute-index build is heap-clean under BT_HEAPCHECK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:26:58 -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