Commit Graph
14 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 aab7a8a137 Combat: FIX weapon effectiveRange -- it degraded with heatLoad, not host-zone damage (task #50)
MechWeapon::UpdateTargeting computed effectiveRange = (1 - heatLoad) * weaponRange,
reading the weapon's own inherited HeatableSubsystem heatLoad. The authentic decomp
(@004b9bdc:6983) reads *(weapon+0xE0)+0x158 = Subsystem::damageZone->damageLevel --
i.e. effectiveRange = (1 - HOST-ZONE DAMAGE) * weaponRange. Same @0xE0-DamageZone-vs-
heat misattribution already corrected in HeatSink::UpdateCoolant (heat.cpp:803).

Impact: for a charge/discharge weapon (ER laser) the weapon's OWN heatLoad swings
0..1 every fire cycle, so effectiveRange collapsed toward 0 and the weapon was
perpetually "out of range" -> Emitter::FireWeapon's `if (dist <= effectiveRange)`
gate skipped SendDamageMessage -> NO damage submission and hence NO impact explosion.
The beam still rendered (beamFlag/beamEndpoint set before the gate), so the shot LOOKED
like a hit but did nothing -- the user-reported "lackluster/absent laser hits, esp.
the ER medium, on mechs AND buildings". PPCs mostly worked only because their heatLoad
happened to sit low/stable.

Fix: read the QUALIFIED this->Subsystem::damageZone->damageLevel (the MechSubsystem
shadow is a shim -- heat.cpp:812) so an UNDAMAGED weapon holds its full, STABLE
weaponRange, and range shortens only as the weapon's host zone takes battle damage.

Verified (parked in range of a building, autofire): laser effRange 500 STABLE
(was fluctuating 0/59/340/424 -> mostly out of range); impact explosions 13 in 22s
(11 laser id=16 + 2 PPC), up from ~2. Lasers now consistently damage + spawn FX.

Also adds env-gated diagnostics used to root-cause this: [fireW] range trace +
per-weapon explID (emitter.cpp), and BT_FIRE_AT_STRUCT (mech4.cpp) which designates
the nearest world structure so weapon-vs-structure fire can be tested without the
screen aim ray.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:31:47 -05:00
arcattackandClaude Fable 5 bb795e2805 MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:

COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).

MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.

RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px.  Gotcha #17 (verify the FUN_ body, not its call shape).

WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed).  Polarity chain corrected end-to-end (failedState, fed by real
damage saturation).  Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched).  The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.

COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.

DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:24:15 -05:00
arcattackandClaude Fable 5 092408041c Cockpit: the CONFIG-MODE weapon-regrouping session + the live-play fire fixes (tasks #6+#11)
TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and
live-verified (hold-configure -> tap fire button -> toggle membership ->
release-commit):
- MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton
  @004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept)
  + the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/
  MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch).
- Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration /
  +0x40,+0x44 AddOrErase evt/dir.  No "secondary vtable @0050f498", no
  "CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP).
  L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the
  NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon
  latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO
  mapper implements the toggle (Thrustmaster no-ops = can't regroup).
- MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID
  (ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the
  destination to &fireImpulse (@004b99a8).
- ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table;
  buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale
  was wrong).  Finding [T1]: the shipped binary NEVER enables this gauge (no
  SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable.
- Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon);
  BT_CONFIG_TEST scripts a headless verify.  @004afbc4 corrected to its real
  Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups).
- Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries
  (PercentDone..WeaponState) -- "TriggerState is the only one" was wrong.

TASK #11 -- the user-reported live-play regressions (both real bugs):
- PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the
  victim on its OWN 0.3s cadence whenever fire was held -- desynced once the
  authentic recharges landed.  Retired (the real impact visual flows from
  each discharge via the messmgr SubmitExplosion).
- WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window
  (+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike
  jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness,
  and the weapon sticks in Loading at level ~10000 forever (observed live).
  Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick
  -- also fixes the I^2R integral over-heating generators on big steps.
  NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap).
- Duty-cycle measured (max-rate autofire): generators equilibrate in the
  degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker
  never trips solo, PPCs sustain ~11.8-damage full-charge fire.  The slow
  kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's
  counters (this regrouping UI, generator reassignment, coolant valves) are
  the reconstruction queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:39:35 -05:00
arcattackandClaude Fable 5 4e63a7b6c3 Heat: THE AUTHENTIC ELECTRICAL MODEL -- weapons recharge from generators (task #10)
Task #10 set out to fix "scrambled linked-sink routing"; a [heat-link] attach
log proved the routing was NEVER scrambled (every subsystem links its authored
condenser exactly; the "pools in Condenser1" read was the diagnostic-sampler
aliasing trap).  The real defect: emitter.cpp's local FUN_00417ab4 stub
returned NULL, so the whole electrical model was inert and the E7 force-charge
recharged every emitter in ONE frame (~0.3s cycle, 1501 fires/90s, ~1.7e9
heat/s -- the "runaway").

Landed authentically [T1: disasm + byte-verified constants]:
- Emitter ctor @004bb120: seekVoltage = authored fraction x generator
  ratedVoltage (10000); EC = energyTotal/(seekV^2 x 0.5); voltageScale@0x310 =
  (RechargeRate / -ln(1 - 1e-4 x seekV)) / EC -- charge reaches seekV[rec] in
  EXACTLY the authored RechargeRate (PPC 5s, ERL 4s, SRM 3s, ERM 2s) cold.
  Owner-flags ctor gate (the usual gotcha; the this-flags read never armed).
- PoweredSubsystem::ChargeTimeScale (@004b0d50, was a =1.0 stub): voltageScale
  x (1 + thermalResistivity x srcTempRise) -- hot generators charge slower.
  ("voltageScale is never read back" was wrong; corrected + swept.)
- TrackSeekVoltage @004ba838: charging I^2R -> the GENERATOR's pendingHeat
  (~3.5e8/full PPC charge) -- generators self-heat, conduct to their authored
  condensers, and throttle further charging.  The feedback economy closes.
- FailureHeat consumers found: this+0x184 == 2 gates BOTH weapon families
  (@004baa88 emitter: reset firing + hold charge 0; @004bbd36 ballistic:
  recoil=rechargeRate + alarm 7).  Emitter::GetFaultState un-stubbed.
- ProjectileWeaponSimulation @004bbd04 opens with call 0x4b0bd0 (disasm) --
  launchers now run the powered/heat step (their firing heat previously
  accumulated in pendingHeat forever).
- heat.cpp: per-instance BT_HEAT_LOG census (the old shared-static 1-Hz
  sampler aliased); [heat-link] attach log; the stolen-else Verify restored.

Verified live (120s max-rate autofire): PPC ~470-500 (was 55,000), bank
plateaus ~600 and sheds to ambient, generators 1100-1400, ERMLaser
self-regulates at the authored 2000 failure threshold (shutdown-cool-resume).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:59:29 -05:00
arcattackandClaude Fable 5 4ed2bbc293 Heat: authentic 1e7-unit heat + the ambient radiator land (task #9)
- Emitters: heatPortion closed form = heatCostToFire x 1e7 x (charge/seekV)^2
  (PPC 1.1e8 -> +632K on its own sink); the missing projectile/missile heat
  adds (raw -- resources author pre-scaled 1e7 units).
- The bank's AMBIENT RADIATOR @4ae73c reconstructed (the system's ONLY heat
  exit; conductance x 0.1 x HeatSinkCount -- _DAT_004ae974 float80 = 0.1) +
  the link-attach guard corrected (skip = the 0xBBE bank, not Condenser; the
  inversion blocked condenser->bank links and closed the system).
- Constant corrections (all byte-verified float80s): coolant epsilons
  0.0025/0.003/1e-4 (was a single 1e-4 serving three sites);
  CoolantCapacityScale 0.05 (was 1.738).
- Verified: heat flows + exits; max-rate autofire overheats weapons into the
  authentic heatLoad range-cutout (thermal spam unsustainable by design).
- KNOWN REMAINING: linked-sink routing scrambled (heat pools in Condenser1;
  authored map says PPC->C4/C6) -- the heat-stream offset audit, filed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:21:37 -05:00
arcattackandClaude Fable 5 fd055281a8 Combat: THE AUTHENTIC DAMAGE ECONOMY -- authored per-weapon amounts through the real fire chain (task #8)
Three root causes, all fixed:
1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling
   the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2
   (closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35).
   The observed "0.25" was the degenerate EC=1 fallback, never authored data.
2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the
   release value (-65) is a negative NaN.  The binary's x87 unordered compare
   read it as "released"; IEEE-correct float compares latched the edge
   detector shut after the first release -- the reason the emitter discharge
   chain NEVER fired in-game.  Fixed with bit-pattern sign compares.
3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData
   (amount + damageForce=target-muzzle [the gyro directional-bounce feed] +
   impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) ->
   messmgr consolidation with per-weapon records + explosion bundling.
   The mech4 bring-up damage block + flat kShotDamage retired to diag hooks.
Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread).  Zone model
verified byte-exact (no change).  Solo end-to-end: 5-record volleys (2 PPC
+ 3 ERML with authored amounts), per-weapon zone granularity, explosions,
kill.  Heat stays bring-up scale pending the heat-calibration audit [T3].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:32:24 -05:00
arcattackandClaude Fable 5 8ed6184d65 Combat: AUTHENTIC weapon groups -- streamed per-mech button bindings + the real fire chain (task #5)
The recovered system: fire channels = LBE4ControlsManager buttonGroups
(0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map
resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port
already called -- it needed only the TriggerState attribute (id 0x13 PINNED
to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an
input feed.  Keyboard/harness now push press/release edges into the button
groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse
hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger
= 4 weapons).  Myomers @4b9550/@4b95b8 misattribution corrected (they are
MechWeapon ConfigureMappables/ChooseButton).  Verified 2-node: kill through
the authentic chain (12 hits vs ~36 pre-groups).  Config-mode session
(regrouping UI) = the remaining stage, KB-scoped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 13:06:29 -05:00
arcattackandClaude Fable 5 29035028fd Cross-pod beams: replicate emitter discharge via subsystem update records
User report: lasers only visible on the window firing them.  The peer's
replicant emitters never learned the master fired.

THE AUTHENTIC PIPE (decomp-verified):
- FUN_0041c350 (the "beam keepalive" ServiceDischarge/ContinueDischarge call)
  does TWO things: queue the LOCAL deferred beam-effect callback (@0x4bac0c)
  on the app+0x34 manager -- our per-weapon render walk already plays that
  role -- and set the subsystem DIRTY bit, which maps to the 2007 engine's
  updateModel / ForceUpdate().
- Replication rides SUBSYSTEM UPDATE RECORDS inside the mech's update message:
  the roster walk already hands the entity's stream to every subsystem's
  PerformAndWatch; Simulation::WriteSimulationUpdate serializes each requested
  updateModel bit; Entity::UpdateMessageHandler routes received records by
  subsystemID to the subsystem's ReadUpdateRecord.  All engine machinery --
  the missing pieces were the Emitter's serialize/apply pair + the triggers.

CORRECTIONS to the dormant task-33-era transcriptions (never exercised --
nothing ever set updateModel -- so the latent misreads never surfaced):
- The weapon-family VTABLE SLOT MAP was swapped: slot 6 = ReadUpdateRecord,
  7 = WriteUpdateRecord, 9 = TakeDamage (evidence: Mech hierarchy symmetry +
  body semantics; @004ba568 resolves an EntityID at rec+0x30 through the
  entity index -- record semantics, not Damage).  Renamed across MechWeapon /
  Emitter / ProjectileWeapon; the real Emitter::TakeDamage @004bafc8 is
  undecoded (inherits MechWeapon for now).
- Emitter/MechWeapon Write: `*record = 0x38/0x18` is the record LENGTH, not
  recordID; rec+0x30 is the TARGET's EntityID (GetEntityID()), not a colour --
  the old `CopyColor(targetEntity+0x184)` was also a databinding trap.
- OVERRIDE-SIGNATURE TRAP: the decls used each class's own shadowing
  UpdateRecord typedef as the param type, silently NOT overriding the engine
  virtual (the base ran instead; nothing would ever have serialized).
  Base-typed params (Simulation__UpdateRecord*), casts inside.
- Emitter::ReadUpdateRecord reconstructed (@004ba568): target EntityID resolve
  (drop unknown non-null targets), MechWeapon alarm apply chain, beam fields.
- ServiceDischarge/ContinueDischarge: ForceUpdate() per keepalive tick + one
  final record at beam end (turns the peer's beam off).
- Mech::DrawWeaponBeams extracted from the player-only drive block so the walk
  runs for REPLICANTS (+ per-mech gun-port cache -- the process-wide statics
  would have served the player's segment pointers as the replicant's muzzles).

VERIFIED 2-node: A fires 57 volleys -> 225 emitter records -> B applies all
225 -> B draws 414 beams (PPC blue / laser red, from A's replicant's own gun
ports).  Solo un-regressed (150 beams, kill chain, no crash).

Also preserved: the full Mech::WriteUpdateRecord @0x4a0c2c recovery
(reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) with all 9 record
types decoded (pose/alarm/leg-state+heat with the body-channel write-through
re-sync, knockdown, death, impact, movementMode) -- transcription deferred;
it replicates remote knockdown/death/heat and was not needed for beams.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:04:34 -05:00
arcattackandClaude Fable 5 14c0dd06f5 Three-trigger keyboard weapon groups: 1=lasers 2=PPCs 3=missiles (task #43)
Interim pod-like grouping ahead of the authentic ConfigureMappables/
ChooseButton mapper channels (decomp-anchored in open-questions):
- Three fire channels like three pod buttons: gBTWeaponTrigger (lasers,
  key 1 or SPACE), gBTPPCTrigger (PPCs, key 2), gBTMissileTrigger
  (missiles, key 3 or CTRL). Keyboard only per user (mouse buttons
  removed).
- EmitterSimulation picks its channel by classID (PPC 3028 vs Emitter
  3016); BT_AUTOFIRE holds all three.
- The BT_BEAM_LOG sampler modulus 30 aliased to ONE weapon under the
  new synchronized 5-beam volleys (5 | 30) -> 31 (coprime).

Verified: all 5 energy weapons fire (31 samples each), aimed hits land,
kill completes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:31:16 -05:00
arcattackandClaude Fable 5 c2f70f6348 Beams CUT on a destroyed target (the real FUN_0049fb54, was a mislabeled stub)
Answers "can I keep targeting the dead mech?": the lock isn't cleared by
anything we've found, but the binary REFUSES to sustain energy beams on a
destroyed mech: ServiceDischarge (@004ba8d0) and ContinueDischarge (@004baa20)
check the beam's target EVERY discharge frame -- IsDerivedFrom(Mech) then
Mech::IsDestroyed (FUN_0049fb54 = movementMode 2||9) -- and kill the beam the
moment it dies.  EmitterSimulation (@004baa88) applies the same check to the
OWNER (a dead mech's own weapons drop everything).

Our recon had FUN_0049fb54 as a mislabeled "cockpit/HUD query" no-op stub
called with NULL, and never set the beam's target (0x474) at fire -- all three
authentic gates were dead.  Now: FireWeapon stashes the owner's target entity;
both discharge paths + the owner gate run the real check (BTMechDestroyed).

Verified: 40 beam samples before the kill, 0 after -- lasers flash-fail on the
wreck instead of cutting it like butter (heat is still spent per the binary's
FireWeapon, which fires blind; only the DISCHARGE checks the corpse).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:46:08 -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 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 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

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

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

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