Commit Graph
56 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 d07ac7dd49 STEP 6 COMPLETE: cylinder hit-location LIVE — unaimed hits resolve to zones
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>
2026-07-08 08:32:17 -05:00
arcattackandClaude Opus 4.8 7fc4acb89f gauge-complete P4g: MessageBoard reconstructed + registered -> LAST parse-skip cleared (0 unregistered gauges)
The "messageBoard" cockpit primitive (the secondary-MFD comm/status message ticker,
L4GAUGE.CFG:4913) was PROSE-ONLY (no bodies/methodDescription) -> parse-skipped. This
was the last unregistered gauge primitive.

Reconstructed byte-verified (Make @4cb678, ctor @4cb704, dtor @4cb788, BecameActive
@4cb7fc, Execute @4cb82c; vtable 0051bddc; sizeof 0xA4). THREE header mislabels fixed
(decode + adversarial verify): int enabled -> Entity* trackedMech (a pointer deref'd at
+0x190, set by SetSource not SetEnable); previousMessageId/previousNameId were SWAPPED
(BecameActive writes 0x9c=-1/0xa0=-2; Execute compares messageId vs 0xa0, name vs 0x9c).
Execute blits the strip cell (id -> (id&3)<<7,(id>>2)<<5) + sender name; methodDescription
= 4 params (rate, mode, btsmsgs.pcx, color). Registered in BTL4MethodDescription[].

DEFERRED / EMPTY by design (authentic for bring-up): the source is never bound (SetSource
has no recovered caller) AND the per-player status queue (StatusMessagePool, btstubs.cpp:62)
is a NULL stub -> no status messages exist -> Execute early-returns on the NULL source (safe
no-op == empty board). Data read via a BTResolveMessageBoard bridge in btplayer.cpp (a real
/FORCE-safe stub returning False; the raw mech+0x190/+0x1dc reads stay in the complete-BTPlayer
TU, dodging the databinding trap). The name-cell path is a marked structural simplification to
restore when the feed lands.

VERIFIED: the gauge parse-skip list is now EMPTY ([gskip]=0 -- every config gauge primitive
registered + built); BTResolveMessageBoard resolves (no /FORCE __ImageBase AV); combat TARGET
DESTROYED, no crash; gauge composite renders identically (300/77, clusters, radar un-regressed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:39:47 -05:00
arcattackandClaude Opus 4.8 c0d2eb0aae gauge-complete P4f: PrepEngrScreen reconstructed + registered -> 12 engineering-screen label overlays build
The "prepEngr" cockpit primitive (the per-engineering-screen static label overlay,
12 CFG calls at L4GAUGE.CFG:4595-4759) was a MISLABELED stub (base Gauge, an Execute
override, mech/screenNumber SWAPPED) with no methodDescription -> parse-SKIPPED, none built.

Reconstructed byte-verified (ctor @4c7bf0, BecameActive @4c7e48, Make @4c7b30; vtable
0051a06c). THREE corrections vs the stub (decode workflow + vtables.tsv proof):
(1) base = GraphicGaugeBackground, not Gauge -- vtable-identical shape to the engine's
BackgroundBitmap (only dtor + BecameActive overridden); (2) the overridden slot is
BecameActive (a paint-on-activation), NOT Execute -- GraphicGaugeBackground has no Execute
virtual, so the Gauge::Execute->Fail->abort hazard doesn't apply; (3) screenNumber@0x6C /
mech@0x70 were swapped. sizeof 0x90.

BecameActive walks the mech roster (databinding-safe GetSubsystemCount/GetSubsystem +
BTGetSubsystemAuxScreen bridge, like VehicleSubSystems::Make), finds the subsystem whose
auxScreenNumber == this screen (1..12), and paints the screen-number numeric + subsystem
label + type-specific label cells dispatched on GetClassID (Sensor/Myomers/Emitter/PPC/
Projectile/Missile/GaussRifle). methodDescription = 9 params (mode, screen, 7 .pcc names).
Registered in BTL4MethodDescription[]. Two raw subsystem reads (heat-sink #, +0x224 label)
are guarded fail-soft best-efforts (marked; the aux-screen bridge label64 substitutes).

VERIFIED: the skip list drops from {prepEngr x12, messageBoard} to just {messageBoard} --
all 12 prepEngr gauges build; no screen-range/crash; boot+sim+combat run; gauge composite
renders identically (COOLANT 300/77, clusters, radar all un-regressed). The prepEngr labels
paint on the Eng (ModeMFD*Eng*) screens, not the default composite view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:32:12 -05:00
arcattackandClaude Opus 4.8 4a4ec6855c gauge-complete P4e: SectorDisplay reconstructed + registered -> radar SECTOR X/Z read-out LIVE
The "sectorDisplay" cockpit primitive (Secondary overlay, the radar SECTOR X/Z
coordinate read-out) was PROSE-ONLY in btl4gau3 (placeholder Make, no ctor/
methodDescription/registration) -> the config line was parse-SKIPPED and never built.

Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c,
methodDescription PE-parse): SectorDisplay : GraphicGauge, sizeof 0xC4. Its Execute
reads the linked mech's world position and shows two 100-unit sector numerics:
  numericA = Round(-localOrigin.z * 0.01) + 500
  numericB = Round( localOrigin.x * 0.01) + 500
(rounding = round-to-nearest == FUN_004dcd94, corrected from the reviewer's wrong
"truncate" claim; 0.01 const PE-verified; -Z/+X axis + fchs confirmed from asm).
Overridden slots: LinkToEntity(9) caches the subject, BecameActive(3, non-inactivating),
Execute(16) -> satisfies the container-Execute rule. Layout overflow-locked
(static_assert sizeof<=0xC4). Make/ctor/dtor mirror the registered PilotList sibling;
the config image name is copied (nameCopy) since Execute reads it per-frame.
Registered in BTL4MethodDescription[].

VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 pos=(125,579) image=helv15.pcc gridCached=1;
Execute -Z=960.4 X=361.6 -> sectorA=510 sectorB=504 (Round(9.6)+500=510, Round(3.6)+500=504
-- authentic 100-unit sectors from live mech position). Gauge composite renders full,
no crash. The skip list is now exactly the two remaining widgets (prepEngr x12, messageBoard).

Also: a permanent BT_GAUGE_SKIP_LOG diagnostic (GAUGREND.cpp, gated) that logs each
unregistered gauge primitive the dev-parse skips -- the tool that pinned this down
(earlier "not built" runs were killed before the lazy gauge-renderer init).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:25:48 -05:00
arcattackandClaude Opus 4.8 aae3ce29d8 gauge-complete P4d: Condenser::MoveValve reconstructed from the REAL @0x4ae464 (was a mislabel)
The reconstructed Condenser::MoveValve carried FUN_004afbe0's body -- a CONTROLS-mapper
display-mode cycler (cycles field@0x190 through 0/1/2 + repoints owner HUD gauges), NOT
the condenser valve. The real valve handler @0x4ae464 was not captured by the
assert-anchored exporter; disassembled it (tools/disas2.py) and reconstructed faithfully:
cycle valveState (@0x1D0) 1 -> 5 -> 50 -> 0 -> 1 (byte-verified @4ae480-4ae4bf), then
RecomputeCondenserValves(owner) to redistribute flow so every ValveSetting gauge updates.

DORMANT by design: the binary guards on owner->messageManager(+0x190)+0x274 (FUN_004ac9c8,
the 0xBD3 SubsystemMessageManager) and the message that invokes MoveValve arrives through
that same manager -- both DEFERRED to WAVE 8. Reproducing the raw owner+0x190 deref would
be a databinding trap in our layout, so the handler is faithful-but-uninvoked; the valve
gauge shows the authentic static 1/N (RecomputeCondenserValves at ctor) until 0xBD3 lands.
No V-key bring-up stand-in added.

Build green; combat TARGET DESTROYED, un-regressed (MoveValve is not yet routed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:29:25 -05:00
arcattackandClaude Opus 4.8 b91057aadc gauge-complete P4c: Condenser dedup + RecomputeCondenserValves -> ValveSetting gauge reads authentic 1/N
Two coupled fixes so the condenser valve gauge (ValveSetting -> coolantFlowScale
@0x15C) reads its authentic value instead of garbage/zero.

STEP 7 (gating dedup): heat.cpp carried STUB Condenser ctor/dtor/TestClass/
TestInstance/CreateStreamedSubsystem that ODR-duplicated the REAL bodies in
heatfamily_reslice.cpp and WON under /FORCE (heat.obj links first) -> the real
ctor (which sets valveState=1) was shadowed, leaving valveState=0xCDCDCDCD.
#if 0'd the heat.cpp stubs so the reslice ctor (@4ae568, byte-verified: valveState=1,
coolantFlowScale=0, massScale=refrigerationFactor, condenserNumber from name) is the
sole definition. DefaultData/GetClassDerivations/ResetToInitialState (not duplicated)
stay in heat.cpp.

STEP 9 (the real writer): FinishConstruction() at the Mech ctor tail was a no-op
template stub in place of FUN_0049f788 = RecomputeCondenserValves -> coolantFlowScale
was never written (stayed 0). Reconstructed it (byte-verified vs part_012.c:9264):
distribute coolant flow across the mech's condensers, coolantFlowScale_i =
valveState_i / sum(valveState), with the condenserAlarm@0x1DC change pulse
(2-if-flow<=old-else-1, then 0). Walks the populated subsystem roster filtering
Condensers via IsDerivedFrom (behaviorally identical to the binary's @mech+0x7cc
condenser chain, GUID 0x50e4fc). Wired as BTRecomputeCondenserValves(this) at the
Mech ctor post-init pass (binary @9457). _DAT_0049f850 fallback confirmed 0.0f (PE read).

Verified: [valve] the Blackhawk's 6 condensers each read flow=0.166667 (=1/6, total=6)
for both player + spawned enemy, no every-mech crash; combat TARGET DESTROYED,
un-regressed. Diagnostic BT_VALVE_LOG added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:22:01 -05:00
arcattackandClaude Opus 4.8 16f5f545ce gauge-complete P4b: AggregateHeatSink 0xBBE -> HeatSink/AmbientTemperature bound (last config NULL cleared)
The 0xBBE heat-sink BANK was built as a plain HeatSink, so the numeric-R
cockpit gauge binding HeatSink/AmbientTemperature (L4GAUGE.CFG:4552) had
no publisher -> the LAST unresolved config attribute.

Reconstruct AggregateHeatSink : HeatSink (ctor @4ae8d0, own GUID 0x50e590),
byte-exact + static_assert-locked (heatSinkCount@0x1D0, ambientTemperature
@0x1D4=300, helper@0x1D8 0xC link node, sizeof 0x1E4 == factory alloc @9993).
Publish HeatSinkCount + AmbientTemperature via a dense-prefix attribute table
chained to HeatSink::NextAttributeID (shared HeatSink table unchanged, so
CoolantMass/CoolantCapacity keep resolving). Move CreateHeatSinkBankSubsystem
into heatfamily_reslice.cpp (needs the class def) and build the real class at
factory case 0xBBE; mech.cpp unchanged.

DELIBERATE DEVIATION (documented in the class): keep the base HeatSinkSimulation
the HeatSink ctor installs; do NOT reimplement the authentic Performance @4ae73c
-- it derefs a raw self+0xE0 -> [+0x158] that does not map in our compiled layout
(AV/NaN, and runs for EVERY mech), and ambientTemperature is a frozen constant so
the gauge reads 300 either way. Authentic relaxation model deferred.

Verified: [attr] HeatSink/AmbientTemperature OK; all 50 config attribute
bindings resolve (0 NULL); no every-mech crash; combat TARGET DESTROYED,
FIRED #41+, un-regressed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:10:11 -05:00
arcattackandClaude Opus 4.8 12c4254aae gauge-complete P4a: heat-leaf data fixes (radar heat-degradation authentic)
The gauge-complete-decode workflow found the "deep heat-leaf byte-exact re-base" is
STALE -- the leaf layout already shipped byte-exact (alarm-unification/P7), and the
real work is a small misidentified-field data fix (highest value, lowest risk):

- Sensor radarPercent read the INHERITED heatEnergy (~1.3e7) -> hugely negative ->
  needed a bring-up guard forcing it to 1.0.  The binary (@004b1c4c:1829) reads
  *(this[0x38]+0x158) = the subsystem's OWN DamageZone::damageLevel [0,1] (0 intact ..
  1 destroyed).  Fixed: radarPercent = RadarBaseline - GetSubsystemDamageLevel(); guard
  DELETED (damageLevel is engine-clamped).  So the radar authentically degrades with
  sensor structural damage (RadarBaseline-damage, x0.5 on DegradationHeat, 0 on Failure).
- New databinding-SAFE accessor MechSubsystem::GetSubsystemDamageLevel()/Set... reads the
  ENGINE DamageZone::damageLevel NAMED member (NOT the ReconDamageZone proxy, whose
  offset-0 structureLevel aliases the vtable ptr as a float = garbage).
- HeatSink::HandleMessage msg==1: same misidentification -> SetSubsystemDamageLevel(0.5)
  (was linkedSinks->heatEnergy=0.5, wrong object+field).
- MyomerCluster seek lamp: subsys+0x800 (OOB for 0x358 Myomers) -> currentSeekVoltageIndex
  @0x320 (the Emitter+0x3f0 analog; INFERRED -- ctor not in the assert-anchored export).
- numericSpeed (~225 vs 61.5): verified NOT A BUG (NumericDisplaySpeed applies x3.6; the
  feed is correct u/s) -- closed, no code.

No layout change (already byte-exact); pure data-read retargets.  Verified DBASE+dev
gauges: no crash, combat un-regressed (TARGET DESTROYED).  (radarPercent degrade is only
OBSERVABLE once per-subsystem damage routes to each engine DamageZone -- a separate task;
today the sensor is undamaged so radarPercent = full 1.0, which is correct.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:03:07 -05:00
arcattackandClaude Opus 4.8 a9210e038a gauge wave P3: fix the DEATHS runaway + RANK=-1 (solo scoreboard correct)
RANK=-1: Player::DefaultFlags creates every player NonScoring (CalcRanking only
ranks IsScoringPlayer()==true, else -1), and the bring-up never cleared it.  Call
SetScoringPlayerFlag() in DropZoneReplyMessageHandler's mech branch (a piloted mech
IS an active scoring combatant; the camera-ship branch stays non-scoring).  -> RANK=0.

DEATHS runaway (climbed ~1/s, not the "1" first seen): @004c012c -- which our
MESSAGE_ENTRY binds as the VehicleDead handler -- is actually the drop-zone
RESPAWN-RETRY helper.  The engine's RequestDropZone (PLAYER.cpp:400) posts a
2s-delayed VehicleDead(deathCount=-2) as a "did the drop zone reply?" timer; our
handler ++deathCount + RE-POSTED it UNCONDITIONALLY -> an infinite loop, because the
drop-zone handshake never "completes" in the bring-up (no console/drop-zone system).
Traced it to a pure boot artifact (13 fires with zero combat; the only VehicleDead
producers are the re-post + my kill-producer, and the kill fires AFTER the loop
starts).  FIX (correct respawn-retry semantics): if the player already has a vehicle
the drop zone WAS acquired (the DropZoneReply created it), so the retry is moot --
return without counting a death or re-posting.  -> DEATHS=0 in solo, loop gone.

Verified DBASE+dev gauges: the Comm roster shows KILLS=1 / DEATHS=0; the KILL log
reads killCount=1 deaths=0 score=164 rank=0; 0 crashes.  (MP real-death DEATHS -- a
destroyed vehicle via the separate @004c05c4 path -- remains deferred per the plan;
solo never dies so the retry gate is correct there.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:19:00 -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 88cd68aaa1 gauge wave P2d: GeneratorCluster un-gated -- the 4 generator panels render
Root-caused the P2c abort with a cdb attach to the frozen abort dialog: the stack
was Gauge::Execute (GAUGE.cpp:598) -> abort, via GuardedExecute <- Update <-
ProcessOneActiveGauge.  The engine base Gauge::Execute is Fail("not overridden")
-> abort, so an ACTIVE container that doesn't override Execute aborts by design;
GuardedExecute's SEH can't catch an abort().  The decode's "GeneratorCluster
overrides ONLY the dtor / renders via its children" was wrong -- the 1995 base
Gauge::Execute was a no-op, the 2007 WinTesla engine's aborts, so the container
MUST override Execute (the sibling SubsystemCluster does).

FIX: give GeneratorCluster the two container overrides (mirroring SubsystemCluster):
- BecameActive() -- blit the panel's static background pixmap (DrawPixelMap8) and,
  by overriding the default, DON'T self-inactivate so the panel stays active.
- Execute() -- no-op (the 5 self-registered child gauges draw the dynamic content;
  the override exists only to avoid the base Fail->abort).

Re-registered in BTL4MethodDescription[].  Verified DBASE+dev gauges: all 4
generator panels (A/B/C/D) render -- blue OutputVoltage bars (the @004c72ac
ScalarBarGauge Scalar* variant), labels, status lamps -- 0 "BecameActive not
defined" warnings, NO abort, combat un-regressed (TARGET DESTROYED).  Phase 2 is
now COMPLETE (4/4 widgets: LeakGauge, VertNormalSlider, PilotList, GeneratorCluster).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:57:20 -05:00
arcattackandClaude Opus 4.8 b44e908101 gauge wave P2c: GeneratorCluster reconstructed + infra (gated -- parent aborts)
Reconstructed the 4th unbuilt widget (the 4 generator engineering panels, buttons
9-12) + its companions, but left it UNREGISTERED pending a runtime-abort fix:

- Generator OutputVoltage attribute table (powersub) -> outputVoltage@0x1DC, chained
  to HeatSink's dense index (temps already reach it).  Correct + reusable.
- ScalarBarGauge @004c72ac plain-Scalar variant (btl4gau2) -- the DIRECT-Scalar* bar
  the GeneratorCluster voltage bar needs (the existing @004c721c takes a resolved
  source object).  Correct + harmless.
- GeneratorCluster Make/ctor/dtor + 15-param methodDescription reconstructed from
  part_014.c:891-1013 (replacing the prose + placeholder Make + the bogus DAT pool);
  builds the 5 children (temp bar / voltage bar / leak wipe / 2 lamps).  Header fixed
  to the binary ctor param order (secondaryColor@0x94, +_reserved0xB0 -> sizeof 0xB4).
  ChildRate forward-declared.

⚠ REGISTRATION GATED (commented out in BTL4MethodDescription[]): registering it
aborts at runtime -- the 4 panels build (the 4 "BecameActive not defined" warnings
fire) then abort().  ISOLATED to the PARENT lifecycle: the abort PERSISTS with all 5
children nulled; there are no pure virtuals; Execute is SEH-guarded under BT_DEV_GAUGES
so the fault is in the unguarded ctor/lifecycle path.  The sibling SubsystemCluster
overrides BecameActive/Execute/TestInstance whereas the decode has GeneratorCluster
overriding ONLY the dtor -- that "inherits only the dtor" claim is the prime suspect.
Deferred; the 3 other P2 widgets (LeakGauge/VertNormalSlider/PilotList) ship clean.

Verified: unregistered -> parse-skips -> no abort; combat un-regressed (DESTROYED),
0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:43:47 -05:00
arcattackandClaude Opus 4.8 0060a3e1ca gauge wave P2b: build the Comm KILLS/DEATHS pilot roster (PilotList)
PilotList (keyword "pilotList") was PROSE-ONLY, so the Comm surface showed only
the baked btcomm.pcx labels.  Reconstructed all 7 functions from part_014.c:
3156-3434 (Make/ctor/dtor/BecameActive/TestInstance/Execute/DrawMechIcon), with
the 8x(x,y,layoutMode) layout table DAT_0051af88 PE-parsed exact from BTL4OPT.EXE.
Draws ONE roster slot/frame (round-robin) from the viewpoint mech's cockpit-mapper
pilot roster.

- Roster FEED: new BTResolveRosterPilot(slot) bridge in mechmppr.cpp (a complete-Mech
  TU) resolves the viewpoint mech's ControlsMapper (subsystemArray[0]) -> GetPilot;
  btl4gau3.cpp reads the returned pilot at raw BTPlayer offsets.  The mapper's
  FillPilotArray already fills the roster (pilotArray[0]=GetMissionPlayer,
  [1..]=FindGroup("Players")).
- KILLS = killCount@0x27c (real; ScoreMessageHandler increments it in combat).
- DEATHS: the binary reads pad_0x280 which has NO writer anywhere (a shipped dead
  field -> perpetual 0).  FIX per "if it doesn't work, fix it": read the real
  deaths counter Player::deathCount@0x200 (VehicleDeadMessageHandler increments it)
  so DEATHS is meaningful in MP.
- DrawMechIcon: App+0xC8 name-bitmap cache is unwired (same deferral PlayerStatus
  uses) -> LookupPlayerNameBitmap returns NULL -> the tinted name box (never an AV).

Dropped the bogus x,y from the header ctor (positions come from DAT_0051af88).
/FORCE-safe (all vtable slots real; link log clean, no unresolved PilotList/
BTResolveRosterPilot).  Verified DBASE+dev gauges: the Comm surface now renders the
live local pilot row (KILLS 0 DEATHS 0 + name box; 0/0 authentic in solo -- the
combat scoring feed that moves them is Phase 3), combat un-regressed (DESTROYED),
0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:24:24 -05:00
arcattackandClaude Opus 4.8 d68284ede2 gauge wave P2a: build the condenser LeakGauge + valve slider (heat panel)
The gauge-widget-decode workflow reconstructed the unbuilt cockpit widgets from
the binary.  First two (the heat MFD's missing SET/LEAK columns):

- LeakGauge (BitMapInverseWipe, keyword "LeakGauge"): the class body already
  existed + is byte-faithful; it was just UNREGISTERED so every LeakGauge(...)
  config line was parse-skipped.  Added the Make factory + methodDescription +
  the BTL4MethodDescription[] registration.  Value @6 = Condenser/CoolantMassLeakRate
  (already published P1); 0 undamaged -> the leak wipe is authentically empty.

- VertNormalSlider (keyword "vertNormalSlider"): the condenser VALVE slider @2 was
  PROSE-ONLY.  Reconstructed all 7 functions (Make/ctor/dtor/TestInstance/
  BecameActive/Execute/Draw) from part_013.c:14051-14175 (Make @004c4b08 by
  disassembly) -- an XOR indicator row=Round(span*value) over the track; fixed the
  jumbled header member layout to the byte-exact order (sizeof 0xB0).  Published
  ValveSetting -> coolantFlowScale@0x15C on the HeatSink table so Condenser/ValveSetting
  resolves (verified: all 6 condensers OK).  The valve indicator renders (condenser 1
  shows it near the top = coolantFlowScale 1.0 = valve open).

Both /FORCE-safe (every vtable slot has a real body; link log grep clean, no
unresolved VertNormalSlider/BitMapInverseWipe).  Verified DBASE+dev gauges: the
heat panel now shows all 3 columns per condenser (TEMP scales, SET valve indicator,
LEAK track), coolant bar stays full, combat un-regressed (TARGET DESTROYED), 0 crashes.
The player-valve-toggle -> coolantFlowScale drive (SetValveSetting vtable+0x48) is
the remaining Phase-3 valve-mechanism work; the slider shows the static valve value today.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:19:04 -05:00
arcattackandClaude Opus 4.8 64975ec22f gauge wave P1b: Searchlight/LightOn + fix coolant/refrigeration source misread
- Searchlight publishes "LightOn" -> lightState@0x1D8 (the button-5 searchlight
  lamp; the empty default-constructed index resolved NOTHING).  The config binds
  the name "LightOn", so the enum id is renamed LightOnAttributeID and the table
  is chained to PowerWatcher's dense index.  Verified: LightOn resolves OK.

- FIX a genuine reconstruction bug the heatmodel decode caught (heat.cpp
  UpdateCoolant + heatfamily_reslice RefrigerationSimulation): both read
  linkedSinks.Resolve()->heatEnergy where the binary reads *(this[0x38]+0x158) =
  the subsystem's OWN DamageZone.damageLevel (the engine base zone @0xE0).
  Consequences of the misread: coolantDraw = master.heatEnergy(~1.3e7) * heatLoad
  would SLAM the live CoolantMass bars to empty whenever the link resolved; and
  RefrigerationSimulation clamped massScale permanently to 1.0 (minimum) instead
  of the authentic 3.0, plus null-deref'd a Condenser whose linkedSinks is skipped.
  Now both read this->Subsystem::damageZone->damageLevel (qualified past the
  MechSubsystem shadow, per mechweap.cpp:252): undamaged 0 -> coolantDraw 0 (no
  leak, coolant bars stay full = authentic pristine) / massScale = 3.0, rising
  only as the sink/condenser itself takes battle damage.

After this only HeatSink/AmbientTemperature remains NULL (the #if 0'd aggregate
HeatSink bank -> P3).  Verified DBASE+dev gauges: no crash, combat un-regressed
(TARGET DESTROYED).  (Also noted: the Condenser dup-ctor links to the REAL
heatfamily_reslice definition -- LNK4006 keeps the first/real one; the heat.cpp
stub is ignored -- so no bug today; #if 0 cleanup deferred to the Condenser table.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:27:57 -05:00
arcattackandClaude Opus 4.8 070af409f7 gauge wave P1a: publish heat/power/weapon attributes + radar zoom
The gauge-databinding-map workflow found most cockpit gauges resolve NULL
because the reconstructed subsystems publish only a fraction of the attributes
the config binds.  First publishing batch (attribute tables are read-only static
data; ids kept a dense prefix from each parent's NextAttributeID):

- HeatSink table dense-append: DegradationTemperature/FailureTemperature (the
  condenser temp-bar warn/max endpoints -- were NULL, so the two-part bars could
  not scale), NormalizedPressure/DegradationPressure/CoolantMassLeakRate, and the
  HeatSink link.  Condenser/Reservoir inherit this -> all 6 condenser temp bars
  now resolve current/warn/max (verified: BT_GAUGE_ATTR_LOG all OK).
- PoweredSubsystem::GetAttributeIndex() (new) publishes InputVoltage->voltageSource
  -- the cluster power-branch gate (the power-lamp/generator-voltage/state-lamp
  sub-branch is skipped when it resolves NULL).  Flows to Sensor/Myomers/weapons.
- MechWeapon::GetAttributeIndex() (new) publishes OutputVoltage/PercentDone->
  rechargeLevel; Emitter/PPC/ProjectileWeapon/MissileLauncher/GaussRifle DefaultData
  re-pointed at it (they carried an EMPTY default-constructed index -> resolved
  NOTHING).  Verified: the ER MED LASER / PPC / STREAK weapon clusters now render
  live recharge dials (were blank TEMP/STATUS).
- Mech::SetTargetRange un-stubbed (radarRange = range) -> the radar map scale +
  overlay range readout track the mapper's zoom (was frozen at 1000).
- GAUGREND ParseAttribute: env-gated per-binding resolution trace (BT_GAUGE_ATTR_LOG)
  -- durable diagnostic infra for the wave.

Verified DBASE+dev gauges: no startup/gauge-construction crash (dense chain intact),
combat un-regressed (TARGET DESTROYED), clusters build with InputVoltage resolving.
Remaining config-binding NULLs: HeatSink/AmbientTemperature (aggregate bank, P3) +
Searchlight/LightOn (P1b).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:22:45 -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 bf44a45ab7 WAVE 7 Phase B fix: projectile weapons no longer permanently jam in bring-up
Interactive repro: fire one missile salvo, then only lasers.  Cause: ProjectileWeapon::
CheckForJam gates the heat-scaled jam roll on `heatLoad > 0` (true whenever firing), and
the roll uses currentTemperature -- which the reconstructed heat model pins at a constant
~77 (thermalMass is huge, so temp doesn't rise/fall with fire yet).  So the roll fired
spuriously on ~every shot, and since JammedState clears only on ResetToInitialState, a
weapon that jammed stayed jammed for the whole mission.  (The auto-fire harness hid this --
it kills the target before jams accumulate.)

Fix: gate the roll on the weapon being in a degraded HEAT STATE (heatAlarm level >=
DegradationHeat) instead of raw heatLoad -- the authentic "jam risk scales with heat" rule.
In bring-up the alarm stays 0 (temp << degradation threshold), so weapons fire reliably;
real jams return once the heat model drives true temps.  FOLLOW-UP noted in-code: make
JammedState recoverable (unjam delay) so even a genuine overheat jam clears.

Verified: 31 projectile impacts sustained over 18s, 0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:46:20 -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 2bcca26cea WAVE 7 Phase A: wire the projectile/missile weapon SUBSYSTEMS byte-exact (un-mislabeled)
An 8-agent decomp-mapping workflow mapped every weapon-family ctor + fire/spawn path
(scratchpad/wave7_maps.txt).  KEY IDENTITY CORRECTION via VDATA.h enum (base 3000=0xBB8):
the factory ctor-address comments were right but the built CLASS names were stubs/base --
    0xBCD == ProjectileWeapon  (was building the base MechWeapon stub)
    0xBCE == GaussRifle         (NOT MissileLauncher -- was a MissileLauncher stub)
    0xBD0 == MissileLauncher    (NOT BallisticWeapon -- was a BallisticWeapon stub)
Un-mislabeled + wired via Create<Class>Subsystem bridges (mirroring CreateEmitterSubsystem),
each static_assert-locked byte-exact on the now-locked MechWeapon(0x3F0)/Emitter(0x478) bases:

  * ProjectileWeapon (0xBCD, @4bc3fc : MechWeapon, sizeof 0x448)
      FIX: AmmoBinConnection was an empty 1-byte struct -> retyped to the binary's 0xC
           SharedData::Connection (was making the object 8 bytes short, sliding
           MissileLauncher's missileCount off 0x448).
  * MissileLauncher (0xBD0, @4bcff0 : ProjectileWeapon, sizeof 0x44C)
      FIX: deleted muzzleVelocity (ALIAS of inherited ProjectileWeapon launchVelocity@0x410)
           and salvoCount (SHADOW of inherited MechWeapon damageData.burstCount@0x3d4),
           keeping the single own field missileCount@0x448.
  * GaussRifle (0xBCE, @4bdcb4 : Emitter, sizeof 0x484 = Emitter + Vector3D muzzleVelocity)
      DEDICATED bridge (not CreateEmitter): its FireWeapon is a no-op (mov [eax+0x414],0) --
      a non-functional weapon in this 1995 build.  Also fixed GAUSS.CPP's discharge write
      rechargeLevel -> currentLevel@0x414 (the binary writes this+0x414).

Phase A safe-stubs FireWeapon (consume round + recoil, NO spawn): the reconstructed
Projectile (@4be1bc, 0x340) + Missile (@4bf5b4, 0x368) ENTITY classes are NOT spawn-ready
(Entity-base phantom members overflow their New() alloc -> heap corruption; New() takes
(int)this instead of the 0xD4 descriptor; Entity base size unconfirmed 2007-vs-1995, the
Mech 0x638-vs-0x854 problem).  The workflow's adversarial verify flagged the live spawn as a
heap-overflow hazard and recommended exactly this phasing.  Phase B = the entity byte-exactness
+ descriptor build + New(MakeMessage*) so a fired shot becomes a flying entity that damages.

Factory now 18 of 20 cases wired to real ticking classes (remaining: SubsystemMessageManager
0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]).  Verified: Mad Cat (LRM/ballistic) + BLH construct
the real weapon classes + tick authentic charge/ammo/heat/recoil, DESTROYED-in-8, 0 crashes,
heapcheck-clean through construction.  Energy weapons already damage via the mech4 beam path,
so combat is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 13:35:43 -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 1356870e56 P7: byte-exact re-base of the CORE heat leaf (HeatSink/Condenser/Reservoir/Generator/Myomers)
The reconstruction modeled the binary's shared alarm/connection types with undersized
stand-ins, sliding every field above them low (the 72-byte auxScreenNumber gap). Fix the
foundational heat-leaf classes byte-exact + static_assert-lock them, from the ctor decomp:

Shared types corrected:
  * SubsystemConnection 4 -> 0xC  (binary link node FUN_004af9cf; FUN_00417ab4 derefs +8)
  * GaugeAlarm54 = 0x54           (real AlarmIndicator FUN_0041b9ec; STATUS level at +0x14,
                                   so subsystem+0x184 == heatAlarm+0x14 == GetLevel())
    WatcherGaugeAlarm now typedefs GaugeAlarm54 (Watcher branch locks stay valid).

Byte-exact + locked (ctor-verified):
  * HeatSink   heatEnergy@0x158 linkedSinks@0x164 heatAlarm@0x170 resource@0x1C4
               pendingHeat@0x1C8, sizeof 0x1D0  (@004adda0)
  * Condenser  valveState@0x1D0 condenserAlarm@0x1DC  (@004ae568)
  * Reservoir  reservoirAlarm@0x1D0 ... squirtEfficiency@0x22C, sizeof 0x230  (@4aef78)
  * Generator  stateAlarm@0x1FC, sizeof 0x250  (@004b225c)
  * Myomers    phantom moverConnection tail removed (fits 0x358)

Three systemic bug classes fixed (added to the checklist in CLAUDE.md / HARD_PROBLEMS.md):
  * alias field    - a subclass member re-declaring an inherited slot the ctor reuses
                     (Condenser refrigerationOutput==massScale@0x160; Reservoir
                     coolantCapacity==thermalCapacity@0x128) -> use the inherited name
  * alarm-interior - a value read at alarm+0x14 modeled as a separate member
                     (HeatSink heatState@0x184, Reservoir injectActive@0x1e4)
                     -> route to alarm.GetLevel()
  * phantom field  - a member past the object end (Generator shortFlag@0x25C is really
                     *(owner+0x190)+0x25c the msg-manager, @004b0efc; Myomers
                     moverConnection@0x110 a write-only base slot) -> remove it

Heat conduction now reads the REAL heatEnergy=1.34e7 (not garbage); combat DESTROYED-in-8,
0 crashes, heapcheck-clean through construction.

REMAINING (measured; a distinct larger task): making PoweredSubsystem byte-exact grows it
+0x98 and cascades into MechWeapon/Emitter/PPC/Sensor/Myomers -- all model the 0x54
AlarmIndicator with 4-byte ReconAlarm / 8-byte HeatAlarm stand-ins and are short +
phantom-tailed; retyping without byte-exacting them overflows the Emitter alloc (heap
corruption). PoweredSubsystem kept on HeatAlarm(8) stand-ins (marked) pending a
subsystem-tree ALARM UNIFICATION. See docs/HARD_PROBLEMS.md P7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 09:47:37 -05:00
arcattackandClaude Opus 4.8 dc6af8cef6 vehicleSubSystems: reconstruct the stubbed connection helpers from decomp
Replace the return-0 stubs with the real functions:
- ResolveLink (FUN_00417ab4) = SubsystemConnection::Resolve (two-level deref of the
  plug's bound link); an unbound plug resolves to 0, the authentic no-source branch.
- FindLinkedSubsystem (FUN_0041bf44) = GetAttributePointer(index) via the engine
  Simulation attribute-index system (the cooling-loop master at slot 3).
- IsGeneratorDerived (FUN_0041a1a4) = the real IsDerivedFrom(PoweredSubsystem
  ClassDerivations 0x50f4bc) via a new powersub.cpp bridge BTIsPoweredSubsystem
  (shared with the aux-screen filter) -> the generator stateLamp now builds.

So the cooling/power/voltage connections resolve real data when the plug is bound,
instead of always reading 0.  Build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:45:57 -05:00
arcattackandClaude Opus 4.8 4ab6748563 vehicleSubSystems follow-up: reconstruct BitMapInverseWipe::Execute (LeakGauge)
Replace the minimal-safe Execute stub with the authentic wipe (@004c5d08): round
the level, repaint `frames` segments stacked vertically, each picking its strip
frame (empty/half/full via colorA/colorB) from the level.  The coolant LeakGauge in
every SubsystemCluster now renders faithfully from its CoolantMassLeakRate value.
Build clean, 7 panels, 0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:34:12 -05:00
arcattackandClaude Opus 4.8 fbf5118c9c vehicleSubSystems follow-up: reconstruct SegmentArc270 recharge dial
Reconstruct SegmentArc270 (@004c6244): engine SegmentArc base + a Scalar connection
driving currentValue + the |n|/(|n|-1)*0.75 segment-span factor.  Wire it into
WeaponCluster as the recharge dial (was a deferred NULL), reading the weapon's real
PercentDone attribute.  Render-verified (BT_DEV_GAUGES_DOCK): the green segmented
recharge circles draw in the weapon panels; 7 panels build, 0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:30:44 -05:00
arcattackandClaude Opus 4.8 6fceb58439 vehicleSubSystems Phase 2: authentic aux-screen assignment -> panels build
The engineering-screen cluster panels now build for each subsystem on its real
aux-screen position.  Two pieces:

1. Aux-screen bridge (powersub.cpp BTGetSubsystemAuxScreen): the assignment lives
   on PoweredSubsystem (auxScreenNumber/Placement/Label, resource +0x104/108/10C)
   which our ctor already populates -- but the reconstructed heat-leaf branch is
   NOT byte-exact, so the Make's raw sub[0x1dc]/[0x1e0]/[0x1e4] reads were garbage
   (0xCDCDCDCD).  The bridge casts through the real PoweredSubsystem type + returns
   the NAMED fields (and is the FUN_0041a1a4/0x50f4bc type filter).  Make + Subsystem
   Cluster now read via the bridge.

2. Reconstruct the 4 child gauges the clusters build that were declared-but-undefined
   (only VertTwoPartBar/OneOfSeveral/PixInt existed): HorizTwoPartBar (full, mirrors
   Vert), OneOfSeveralInt (ctor), OneOfSeveralStates (ctor + BecameActive + State
   Connection @004c3324), BitMapInverseWipe/LeakGauge (full) + SeekVoltageGraph's
   Execute/BecameActive/dtor -- their /FORCE-stubbed ctors were segfaulting when a
   cluster built them.

VERIFIED (BT_DEV_GAUGES + BT_VSS_LOG): 7 authentic panels build -- Sensor(scr5,
qsensors) HeatSinkCluster, Myomers(scr6,qmyomers) MyomerCluster, PPC(scr1,10) +
Emitter(scr4,8,7) Energy/Ballistic clusters -- with real labels + placements, 0
crashes, combat un-regressed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:10:47 -05:00
arcattackandClaude Opus 4.8 a02339112a vehicleSubSystems: verify Phase 1 end-to-end + confirm the Phase-2 blocker
Add a BT_VSS_LOG diagnostic (Make entry + per-subsystem classID/auxScreen dump +
the ConfigureForModel path).  Verified under BT_DEV_GAUGES: the gauge renderer
builds, ConfigureForModel(Init) runs bhk1Init, and VehicleSubSystems::Make is
called with the full 33-subsystem roster -- the dispatcher iterates every
subsystem and its classID switch resolves the displayable types (0xBC3 Sensor,
0xBC6 Myomers, 0xBC8/0xBD4 Emitter/PPC, 0xBCD/0xBD0 Proj/Missile).

CONFIRMED (empirically) the Phase-2 blocker: subsystem[0x1dc] (the aux-screen
position the Make gates on) reads 0xCDCDCDCD (uninitialized) / garbage for the
displayable subsystems -> every panel hits the "Illegal aux screen position"
skip, so nothing renders yet.  Phase 2 = populate the aux-screen field from the
subsystem resource.  Combat remains un-regressed (the skip path is inert).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:54:44 -05:00
arcattackandClaude Opus 4.8 674c172cf8 vehicleSubSystems Phase 1: reconstruct + register the cluster-panel family
Reconstruct the whole engineering-screen (MFD) subsystem cluster-panel system in
btl4gau2 (from the Ghidra decomp of BTL4OPT.EXE) and register the vehicleSubSystems
config primitive:

- Sub-gauges: AnimatedSubsystemLamp/AnimatedSourceLamp (OneOfSeveral + cooling/
  power connections), ScalarBarGauge (BarGraphBitMapScalar + generator-voltage
  connection), ConfigMapGauge, plus the 3 file-private GaugeConnection classes
  (@004c3134/31ec/3288 samplers).
- SubsystemCluster base (ctor builds ~11 child gauges across the MFD + engineering
  ports; Execute/dtor/frame-draw/enable helpers) + the 5 subclasses HeatSink/
  Myomer/Weapon/Energy/Ballistic (ctors + Executes).
- VehicleSubSystems::Make dispatcher (@004cbaf0): per-subsystem factory keyed on
  classID (HeatSink 0xBC3 / Myomer 0xBC6 / Energy 0xBC8,0xBD4 / Ballistic
  0xBCD,0xBD0), placed via the 12-row aux-screen geometry table, + registered in
  BTL4MethodDescription[].

SAFE + gated: gauges only build under BT_DEV_GAUGES/pod, and until Phase 2
populates the subsystem aux-screen field (subsystem[0x1dc]) the Make hits its
"aux screen = zero" skip -> combat untouched.  Two minor children deferred as
marked NULLs (SegmentArc270 recharge dial + BitMapInverseWipeScalar eject wipe --
their ctors are btl4gaug placeholders).  Raw-offset subsystem reads (0x1dc/0x1e0/
0x1e4/0x31c/0x334/0x364/0x43c...) marked BEST-EFFORT pending Phase 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:43:32 -05:00
arcattackandClaude Opus 4.8 b6918632fc gauges: reconstruct + register PlayerStatus (mission-review scoreboard)
PlayerStatus (btl4gau3) had only a BUGGY Make defined (it read raw DAT_ pools,
passed (int)entity/(int)gauge_renderer as x/y, and DROPPED the graphics_port_number
param -- the header ctor was one int short); the ctor/Execute/dtor/BecameActive and
the PlayerStatusMappingGroup + CreateMutantPixelmap8 helpers were comment-only
stubs.  Reconstructed the whole family (mapped by the playerstatus-decomp-map
workflow, 6 agents):

* methodDescription (8 params: rate,modeMask,integer player#,string font,4x color)
  + rewired Make reading parameterList[] with the port/x/y bug fixed.
* ctor (@004cb1a8): GraphicGauge base (owner folded to 0), store colours+port,
  SetOrigin, playerIndex=player_number-1, build a score NumericDisplay (fmt 2 =
  signedBlankedZeros).  Fixed the header layout: nameImage is a BitMap* (not a
  Pixmap), and two missing members (dirty@0x2F, previousStatus@0x30).
* dtor / BecameActive / TestInstance.
* PlayerStatusMappingGroup (@004c9bd0): 28 ColorMapperArmor zone tints over the
  mech-outline schematic, one per dz_* damage zone (mech->GetDamageZoneIndex),
  using the EXISTING 10-arg ColorMapperArmor ctor (per the workflow's correction --
  changing it would break cmArmor); added ColorMapper/Armor::SetColor (FUN_004c3c38,
  stores @0x6C).
* Execute (@004cb358): resolve the player (WinTesla-clean via GetMissionPlayer for
  the local player, instead of the binary's raw App+0x24 "Players"-node dictionary
  walk), then draw the score + name box + alive/dead status box; null-guarded.

KEY FINDING: PlayerStatus lives in the config's `cameraInit` block (the MISSION-
REVIEW / spectator camera cockpit), NOT `MechInit` -- so it is NOT part of the mech
cockpit and does not build during normal mech play (which is why the mech test shows
it un-regressed and its Execute never runs).  It is the post-mission scoreboard
(all 8 players' name/score/mech-status), rendered by the BTCameraDirector game
model.  Registered in BTL4MethodDescription[] so `cameraInit` builds it instead of
parse-skipping.

STATE: complete reconstruction; compiles, links (no new /FORCE unresolved), and the
mech cockpit is un-regressed (TARGET DESTROYED, 0 crashes).  NOT runtime-verified --
cameraInit is only built by the mission-review camera model, which a normal
`vehicle=<mech>` egg does not trigger.  BRING-UP STUB (marked): CreateMutantPixelmap8
returns NULL (the mech-outline recolor needs the DynamicMemoryStream read API +
Pixmap pixel-copy mapped) -> the score/name-box/status-box render but not the
recoloured mech schematic.  BT_PS_LOG traces the resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:13:32 -05:00
arcattackandClaude Opus 4.8 fb3ed38fa7 map: fix contact-blip projection -- enemy now plots inside the radar FOV wedge
The previous commit found+drew contacts but they landed off-radar.  Debugged with
BT_MAP_LOG entity/blip tracing: the reads were CORRECT all along -- the enemy mech
(cls 0xBB9, own=0) sits at dsq=14400 (exactly 120u) from the player (own=1, dsq=0),
and the grid also carries ~311 cls-0x5E vehicle props per cycle (the map's
scattered vehicles) which the 0xBB9 blip filter correctly skips.

The bug was the PROJECTION: worldToView is a camera (view->world) matrix and
Point3D::Multiply bakes a scale that is wrong for a point transform -- a 120u
contact at ppm=0.366 projected to |167px| instead of |44px| (~3.8x), and with a
spurious rotation.  (The earlier "377-828m" reading was this wrong scale amplifying
the varying player-drift distance, not a distance error.)  Fixed by projecting
delta = (entity - viewpoint) DIRECTLY: rotate by -heading (from the orientation
quaternion's yaw, top-down heading-up) and scale by pixelsPerMeter.  Now |screen|
== |delta|*ppm exactly (blip @(0,44) for the 120u enemy), on-radar inside the view
wedge.  Blip is a cross + box so it reads as a distinct contact marker.

Verified live (BT_DEV_GAUGES): the enemy contact renders as a box marker INSIDE
the FOV wedge (render-confirmed via a 4x radar crop); combat un-regressed (TARGET
DESTROYED after 8 hits), heap-clean under BT_HEAPCHECK.  BT_MAP_LOG traces blip
coords + scale + delta.  Remaining map polish: blip-vs-wedge rotation convention
(the enemy reads ahead/in-wedge, which is correct); the authentic pip symbol/name
infrastructure (still stubbed); SetTargetRange (radar zoom, 1km default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:22:33 -05:00
arcattackandClaude Opus 4.8 0bceb9a638 gauges/map: populate the radar entity grid + draw mech contact blips
Root-caused why the radar showed 0 contacts: the gauge renderer's moving/static
entity grids (queried by GetMoving/StaticEntitiesWithinBounds) were NEVER
populated -- the 1995 game fed them from ExecuteImplementation's InterestingEntity
iterator, which the WinTesla port dropped.

Fix (engine): add GaugeRenderer::RebuildEntityGrid() -- Clear + repopulate
movingEntities from the world's DynamicMaster + DynamicReplicant entities (the
vehicles/mechs; AllEntity would flood the grid with ~300 props/effects/terrain).
Called from the map's Execute phase 0 (under the gauge's guarded Execute, so a
fault disables only the map).  Verified: contacts are now found, combat un-
regressed (TARGET DESTROYED), heap-clean under BT_HEAPCHECK.

Fix (map): DrawMoving now draws a cross blip for mech-class contacts (0xBB9) when
the authentic pip raster set is absent (the BT pip table is stubbed in this engine
build).  KEY PROJECTION FIX: worldToView is a camera (view->world) matrix and
Point3D::Multiply applies only rotation+scale (NOT translation), so projecting the
absolute entity position left the viewpoint un-subtracted and the blip landed
off-screen (verified: blip @(580,-1154)).  Project the RELATIVE position (delta =
entity - viewpoint, already computed for the range check) instead -> the blip
projects viewpoint-relative + heading-rotated correctly (@(164,221)).

radarRange default 500->1000 (1km zoom) so contacts within ~500m show on the
radar (SetTargetRange, the real player zoom, is still stubbed).  BT_MAP_LOG traces
the blip coords.

STATE: the grid population + viewpoint-relative blip projection are correct and
combat-safe.  A reliable on-screen contact demo still needs the display-scale vs
spawn-distance reconciliation (the BT_SPAWN_ENEMY dummy sits at a varying, large
distance -- 377-828m -- beyond the display radius across runs) + the authentic pip
symbol/name/video-object infrastructure (still stubbed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:07:19 -05:00
arcattackandClaude Opus 4.8 48ed3a8b73 gauges: register + wire the map (radar) -- the tactical display renders
MapDisplay (the radar/threat-map gauge, btl4rdr.cpp) was fully reconstructed
(ctor + Execute + DrawViewWedge/DrawStatic/DrawMoving/DrawNames) but never
registered and its data attributes never published.  Wire it up end-to-end:

DATA (the attributes the map binds):
* Mech table extended to the full 0x15..0x38 (was the 0x21 prefix): add
  RadarRange@0x2f -> radarRange, RadarLinearPosition@0x30 -> radarLinearPosition
  (Point3D* -> &localOrigin.linearPosition), RadarAngularPosition@0x31 ->
  radarAngularPosition (Quaternion* -> &localOrigin.angularPosition), DuckState
  @0x37 -> duckState; the rest bind the shared attrPad (kept dense so Find can't
  strcmp a gap).  New members set in the Mech ctor; radarRange defaults to 500
  (SetTargetRange is still stubbed) so nearby contacts are on-scale.  The map's
  position/angle attributes resolve to Point3D**/Quaternion** (AttributePointer
  is int Simulation::*, so the ATTRIBUTE_ENTRY reinterpret binds pointer members
  fine -- verified live: posPtr/anglePtr resolve).
* Sensor (named "Avionics") publishes RadarPercent/SelfTest/BadVoltage: define
  Sensor::AttributePointers[] + build the previously-empty Sensor::AttributeIndex
  chained to PoweredSubsystem::GetAttributeIndex() (= HeatSink's dense index).

WIDGET (the registration glue -- NOT in the assert-anchored decomp, so
reconstructed from the config + ctor):
* MapDisplay::methodDescription (config "map") + MapDisplay::Make.  The
  offset_position keyword "center"/"bottom" is typed as a STRING and converted
  in Make, avoiding a ModeManager named-constant registration.  Registered in
  BTL4MethodDescription[] (btl4grnd.cpp now includes btl4rdr.hpp).

Three crash fixes exposed once the map ran (each a pre-existing stub the map is
the first consumer of):
* Sensor radarPercent went hugely negative (RadarBaseline - HeatMasterEnergy(),
  where the inherited heatEnergy is un-normalized in the not-byte-exact heat-leaf
  branch) -> the radar reported non-operational.  Guard: treat an out-of-[0,1]
  heat term as no penalty (marked bring-up; real overheat penalty needs the heat
  normalization).
* ResolveOperatorEntity was a stub returning NULL -> EntityPosition(NULL) crashed
  CalculateBounds.  Fixed to the renderer linked-entity + application viewpoint
  fallback (same as HeadingPointer).
* MapName::ExtractFromEntity didn't guard a NULL entity (the binary's Verify(False)
  iterator-mismatch path compiles out at DEBUG_LEVEL 0) -> EntityPosition(NULL).
  Guarded.

Verified live (BT_DEV_GAUGES): the radar now draws the view wedge (the mech's
180-deg FOV cone) and completes the full wedge/static/moving/names cycle with
operating=1, cap=1, scale=500, position/angle resolved, 0 crashes.  Combat un-
regressed (TARGET DESTROYED after 8 hits), heap-clean under BT_HEAPCHECK.
Permanent gated diagnostic: BT_MAP_LOG traces the phase/draw pipeline.  Contacts
show 0 (the spatial-query entity classification is a follow-up); DuckState now
resolves for the duck button too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 21:16:29 -05:00
arcattackandClaude Opus 4.8 a538d6438e gauges: reconstruct + register oneOfSeveralPixInt -- cockpit button-state lamps
The OneOfSeveral family was declared but undefined.  Reconstruct the shared base
(OneOfSeveral: ctor @004c4d88, BecameActive @004c4f14, Execute @004c4f28, dtor
@004c4e7c) and the OneOfSeveralPixInt subclass (Make @004c5204, ctor @004c52d8,
dtor @004c5364) + its methodDescription, and register "oneOfSeveralPixInt".

OneOfSeveral is a multi-frame strip selector: the base holds a BitMap or PixMap8
strip of columns*rows frames + the per-frame size (source size / columns,rows);
Execute picks frame `selected` (col=selected%columns, row=selected/columns) and
blits that sub-rectangle -- DrawPixelMap8(opaque) for a pixmap strip (its own
palette, no SetColor), or SetColor(bg)+DrawBitMapOpaque(fg) for a bitmap strip.
Added the missing OneOfSeveral::foregroundColor field (@0x9C -- the DrawBitMap
Opaque background arg).  OneOfSeveralPixInt forces the PixMap path (fromImageStrip
=0, bg/fg=0) and wires one GaugeConnectionDirectOf<int> to the frame index.
methodDescription = (rate,modeMask,string strip,integer columns,integer rows,
attribute state) matching oneOfSeveralPixInt(rate,mode,strip.pcc,cols,rows,attr).
Strip resource via warehouse->pixelMap8Bin (WRHOUS.h) .Get/.Release.

The config binds the duck / searchlight / display-mode / piloting-mode buttons;
ControlsMapper/DisplayMode already resolves (MechControlsMapper table), and
DuckState/Searchlight/LightOn degrade to frame 0 (safe) until their tables land.

Verified live (BT_DEV_GAUGES): the cockpit button-lamp PixMap8 icons now render
(were absent).  No /FORCE unresolved externals.  Combat un-regressed (TARGET
DESTROYED after 8 hits), 0 crashes, no missing-image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:33:16 -05:00
arcattackandClaude Opus 4.8 a2ee1181e6 gauges: reconstruct + register segmentArcRatio -- the cockpit SPEED arc
SegmentArcRatio (config keyword "segmentArcRatio") was declared but undefined.
It is a thin subclass of the engine SegmentArc gauge: two Scalar connections
(numerator=value, denominator=max) and an Execute override that computes the
[0,1] fill fraction into the base's currentValue, then delegates drawing to
SegmentArc::Execute (which lights numerator/denominator of the segments).

Reconstruct Make (@004c62fc) / ctor (@004c6394) / Execute (@004c6488) / dtor +
the methodDescription (rate,modeMask,scalar inner,scalar outer,scalar deg0,
scalar deg1,integer segs,color bg,color fg,attribute numerator,attribute
denominator -- matching the engine SegmentArcNormalized param types; the config
gives inner/outer once and the Make duplicates them into inner0==inner1 /
outer0==outer1).  Execute recovered by disassembling the vtable override
@004c6488 (Ghidra dropped the x87):
  if (denominator < 1) currentValue = 0;
  else currentValue = clamp(|numerator/denominator * segmentSpan|, 0, 1);
  SegmentArc::Execute();
where FUN_004dcd00 = fabs and the ctor precomputes
  segmentSpan = (Scalar)(|n|/(|n|-1)) * 0.75f    // int division => 0.75 for n=36
(_DAT_004c6484 = 0.75f, read live from the binary).  Fixed the header field type
int segmentSpan -> Scalar (the ctor fstp's it and Execute fmul's it).

The config binds numerator=LinearSpeed, denominator=MaxRunSpeed -- both now
resolved by the Mech attribute table -- so the arc sweeps with speed.

Verified live (BT_DEV_GAUGES + drive): the radar-surface SPEED dial now shows a
green segmented arc filling with speed (was a plain ring).  No /FORCE unresolved
externals.  Combat un-regressed (TARGET DESTROYED after 8 hits), 0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:50:35 -05:00
arcattackandClaude Opus 4.8 18d58f38f6 gauges: reconstruct + register vertBar (VertTwoPartBar) -- cockpit coolant bars
VertTwoPartBar was declared in btl4gaug.hpp (ctor/field offsets/decomp addrs)
but never defined.  Reconstruct Make (@004c462c) / ctor (@004c4724) /
BecameActive (@004c48e0) / Execute (@004c48fc) / dtor + the "vertBar"
methodDescription, and register it in BTL4MethodDescription[].

The Execute pixel math is x87 that Ghidra dropped (FUN_004dcd94 is just
(int)ROUND(ST0); the operands are on the FPU stack) -- recovered by disassembling
@004c4940/@004c4960:
  warnPix = clamp(round(height*low/high  + 0.5), 0, height)
  valPix  = clamp(round(height*value/high + 0.5), 0, height)
value is first clamped to [0,high]; on a change it tile-blits the bitmap over
[0,warnPix), fills [warnPix,valPix) with fillColor, clears [valPix,height) with
extraColor (reusing the existing DrawTiledBitmap helper = FUN_004c2ff8; view
methods SetPositionWithinPort/SetColor/MoveToAbsolute/DrawFilledRectangleTo
Absolute = vtbl +0x08/+0x18/+0x24/+0x48).  methodDescription param types
(rate,modeMask,vector,string,color,color,color,attribute,attribute,attribute)
match the config vertBar(rate,mode,(w,h),tile,bg,fill,extra,current,warn,max).

For the coolant bars the config binds current=CoolantMass, warn=max=Coolant
Capacity, so the warn line sits at full and the bar is a simple level gauge that
empties as CoolantMass drops -- now driven by the HeatSink attribute table
committed earlier.

Verified live (BT_DEV_GAUGES): the Heat surface COOLANT/RES bars now render the
btwarn.pcc hatch fill (was empty), reading the real coolant attributes (full
hatch = coolantLevel~=capacity).  No /FORCE unresolved externals on the new
symbols.  Combat un-regressed (TARGET DESTROYED after 8 hits), 0 crashes, and
the gauge build + mission load are heap-clean under BT_HEAPCHECK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:42:04 -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 9ea4bdaf50 gauges: publish HeatSink attribute table (CoolantMass/CoolantCapacity/CurrentTemperature)
The cockpit gauge config binds Subsystem/Attribute names (e.g.
HeatSink/CurrentTemperature) resolved by ParseAttribute -> FindSubsystem ->
GetAttributePointer -> the class's activeAttributeIndex.  HeatSink wired
GetAttributeIndex() into DefaultData but published an EMPTY table (only the
inherited SimulationState id 1), so every HeatSink/* numeric read NULL ->
guarded to 0.

Add HeatSink::AttributePointers[] + GetAttributeIndex() publishing CoolantMass
(coolantLevel @0x12C), CoolantCapacity (thermalCapacity @0x128), and
CurrentTemperature (inherited currentTemperature @0x114), chained to the parent
index so SimulationState is preserved and the built index stays dense (ids run
contiguously from HeatableSubsystem::NextAttributeID -- a gap would make
AttributeIndexSet::Find strcmp a garbage slot).  Condenser and Reservoir derive
from HeatSink with no override, so they inherit this table for free.

Verified live (BT_DEV_GAUGES): the Heat surface heatsink-temperature readout
(numeric HeatSink/CurrentTemperature, a base engine primitive that was already
drawing NULL) now shows 77 (= startTemp) instead of 0.  Combat un-regressed
(TARGET DESTROYED after 8 hits), 0 crashes.  The coolant attributes are ready
for the vertBar widget (next increment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:19:09 -05:00
arcattackandClaude Opus 4.8 d006b40927 gauges: reconstruct cmCrit -- subsystem-critical tint; ColorMapper family complete [widget recon 5]
Reconstructs ColorMapperCritical -- tints a schematic colour by a subsystem's
operational state (the "critical" secondary display mode):
- CriticalConnection (@004c3598/@004c3610): src==0 -> 0; src->simulationState==1
  (DestroyedState) -> 100; else the subsystem's own damage-zone damageLevel*100.
- ColorMapperCritical::Make/ctor (@004c3ddc/@004c3e40): FindSubsystem by name +
  wire the CriticalConnection.

Resolved the subsystem damageZone@0xE0 shadow: MechSubsystem::damageZone is
declared ReconDamageZone*, but the assignment (mechsub.cpp: damageZone =
(ReconDamageZone*)new DamageZone(...)) shows the pointer IS a real engine
DamageZone -- so cast it back and read damageLevel. Added public accessors
GetSimulationState()/GetDamageZoneProxy() to MechSubsystem (those fields are
protected; mirrors how HeatConnection reads the public currentTemperature).

Completes the ColorMapper family (cmHeat, cmArmor, colorMapperMultiArmor,
cmCrit). Verified: parses, builds, all subsystems resolve (0 warnings), no
/FORCE unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes), stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:14:54 -05:00
arcattackandClaude Opus 4.8 fb8c1d6ace gauges: colorMapperMultiArmor (worst-of-8-zones) + fix the interpreterTable overflow [widget recon 4]
Reconstructs colorMapperMultiArmor -- tints one schematic colour (a whole torso
section) by the WORST of up to 8 damage zones:
- MultiArmorConnection (@004c346c/@004c34f4): scans 8 zones via
  entity->damageZones[idx], keeps the max damageLevel, count==0 ? 100 :
  Round(worst*100).
- ColorMapperMultiArmor::Make/ctor (@004c3c48/@004c3d60): resolves 8 named
  zones to indices via Entity::GetDamageZoneIndex (13-param methodDescription).

ALSO fixes a latent HEAP CORRUPTION that registering this widget exposed (and
that would have blocked every further widget): GaugeInterpreter's bytecode
buffer is a fixed interpreterTableSize=46864 (GAUGREND.h) tuned for RP's config,
and its Insert() bounds guard is a Verify() that compiles out at DEBUG_LEVEL 0.
As each BT gauge widget is registered, more of BT's larger L4GAUGE.CFG resolves
into bytecode (vs being parse-skipped); colorMapperMultiArmor's 13 params x
hundreds of calls pushed the total past 46864 -> silent overflow past the char[]
-> heap smash detected later in mission bitmap loading (not the gauge code).
Fix: interpreterTableSize -> 262144 (sized for BT's full config).

Verified: parses, builds, all zones resolve, no /FORCE unresolved, combat
un-regressed (TARGET DESTROYED, 0 crashes), stable. Details: docs/GAUGE_COMPOSITE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:56:13 -05:00
arcattackandClaude Opus 4.8 1d2ddd8399 gauges: reconstruct cmArmor -- the per-zone ARMOR DAMAGE schematic [widget recon 3]
Each armor zone on the cockpit schematic is now tinted by that zone's live
damage. Render-verified: the schematic shows an all-green Blackhawk (all zones
damageLevel=0 at spawn), replacing the static green+red it showed before cmArmor
was registered -- i.e. cmArmor now owns those zone colors and shows the real
undamaged state; damaged zones shift toward red.

Reuses the ColorMapper base (from cmHeat) + two pieces reconstructed from the
binary (Ghidra dropped the x87, recovered by disassembly):
- ArmorZoneConnection (@004c33a4/@004c3430): resolves one zone from the owner's
  inherited Entity::damageZones[zone_index]; per-frame feed = zone==NULL ? 100
  : Round(zone->damageLevel * 100) (the 0..1 damage ratio -> 0..100 percentage
  -> the colour index ColorMapper::Execute pushes into the palette slot).
- ColorMapperArmor::Make/ctor (@004c3aa4/@004c3b98): Make resolves the CFG zone
  name (dz_ltorso etc., the 6th param) to an index via Entity::GetDamageZoneIndex
  (== the binary's FUN_0042076c, scanning damageZones[] by name); the ctor wires
  the ArmorZoneConnection.

Decomp fact (corrects the cmHeat note): the ColorMapper base ctor takes NINE
args -- an owner_ID sits between renderer and graphics_port_number. Our 8-arg
ColorMapper::ColorMapper folds owner_ID=0 in (gauges have owner 0), so it is
equivalent; cmArmor's mapping matches cmHeat plus the zone name.

Verified: parses, builds, all dz_* zones resolve (0 "not found"), no /FORCE
unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes). The dynamic
red-on-damage needs the player to take damage (the passive spawn dummy never
hits back). Details: docs/GAUGE_COMPOSITE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:30:45 -05:00
arcattackandClaude Opus 4.8 e869b00181 gauges: reconstruct headingPointer -- the rotating compass + heading readout [widget recon 2]
The visible money-shot: a green compass needle that rotates with the mech's
facing plus a numeric heading in whole degrees (render-verified: the needle
swung and the number went 247 -> 182 as the mech turned).

Full class reconstruction from the binary (Make/ctor/dtor/TestInstance/
ShowInstance/BecameActive/Execute). Ghidra dropped the x87 float math feeding
Execute's needle endpoints and the ctor's NumericDisplay centering, so it was
recovered by disassembling BTL4OPT.EXE with capstone (scratchpad/disas_hp.py):
the needle is a radial line from innerRadius(20) to outerRadius(39) at the
heading angle, endpoints rounded half-up; the readout is round(360 - deg).

Two corrections vs the map:
- The header ctor was 12 args; the binary's is 14 -- widened it. Fields @0x98/
  @0x9C are the needle's inner/outer RADIUS (Execute multiplies them by sin/cos),
  not "color/spacing" as the old field names implied; renamed accordingly.
- ENGINE-CONVENTION FIX: the binary read the heading from EulerAngles index [0],
  but the WinTesla EulerAngles(Quaternion) decomposition is ambiguous for a
  yawing mech (the quaternion double-cover flips it to a pitch=roll=pi branch as
  yaw sweeps past +/-pi -> the needle spins erratically). Switched to
  YawPitchRoll, whose yaw-first .yaw is the clean continuous heading
  (pitch=roll~0). Faithful to the binary's intent (heading), correct for this
  engine's decomposition.

renderer->GetLinkedEntity() resolves the viewpoint mech (a GetViewpointEntity
fallback is kept belt-and-braces). Deps (NumericDisplay/GraphicsViewRecord/
GraphicGauge) are real engine classes -> no /FORCE risk; verified no unresolved
HeadingPointer externals, no parse desync, combat un-regressed (TARGET
DESTROYED, 0 crashes). Details + the disasm technique: docs/GAUGE_COMPOSITE.md.

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