Reading the art settled where the titles live: qsensors/qmyomers/qermlas/
qppc/qstrk6.pcc are 299x26 TITLE BANNERS, while the qblh0..7.pcc strings
the cfg hands vehicleSubSystems are 77x120 MECH ICONS (already lit by the
5.3.28 placement fix). So the authored auxScreenLabel IS the panel title
-- it just belongs on the MFD panel, not only the ENG page.
SubsystemCluster now builds a titleBanner child from the cached label.
Placement was MEASURED against the shipped framebuffer through the A/B rig
(BackgroundBitmap places by left/BOTTOM): a bring-up BT_TITLE_DY knob, then
a green-pixel correlation over the title band, then baked at x+0x09,
y+0xb3 from the panel origin. "SENSOR CLUSTER", "MYOMERS", "TYPE II 65
TONS" and both "ERMED LASER RANGE 500M" banners now render where the
shipped binary puts them.
Score progression vs the shipped cockpit on identical GAUGE content:
5.3.26 birth 90.1% identical / 82% coverage
5.3.28 authored screens 93.2% / 85%
5.3.29 titles tuned 94.8% / 91%
Also: grab.ps1 now focuses the target window before capturing -- the
screen-copy grabbed whatever was in front, which cost two junk captures
(a white frame and an unrelated 3-D app window).
Gauge fight 11/11 rounds and smoke both clean, zero faults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ran the reconstructed exe and the shipped ALPHA_1 binary against the SAME
GAUGE content, same mount, gauges on, and diffed the 640x480x16
framebuffers. OUR EXE DRAWS THE COCKPIT: 90.1% pixel-identical on the
first run, and every difference traced to a single root cause.
The authored aux-screen block (screen number / background placement /
label) lives in the subsystem resource -- which is freed with the Mech
ctor's stream buffer (the 5.3.24 use-after-free landmine). Both this tree
and the BT411 port had stubbed it, which is why BT411's displays show the
same artifacting. PoweredSubsystem now caches the block at ctor time with
accessors, and btl4gau2 uses the AUTHORED screen instead of a roster-order
stand-in and builds the placement strip art. Panels snapped onto their
authored screens -- weapon dials now land on the same panels as the
shipped exe, mech icons appear on the sensor/myomer panels, generator
letters correct -- taking the match to 93.2% identical / 85% coverage.
Remaining differences are donor-inherited stubs: the panel title art (now
proven to come from the q-strip statusImage path, not auxScreenLabel --
the label blit is wired and guarded regardless), the pilot name bitmap,
and some lamp frames.
The A/B rig is banked at emulator/render-bridge/gauge-ab/ (both confs +
the window-grab script) so the comparison is repeatable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New IO-ARCHITECTURE.md, derived from the v4.2 image, the U7 GAL decode
and schematic sheets 1/3:
- $A010 control-latch bit map (CCK / D_OUT / WR_SB / RD_SB / SEL0-2)
- the 50-byte script format the firmware replays, and the ROM table map
- full port/address population: 9 buttons boards + 2 keypads across all
8 ports; the 0x00-0x6F logical map and its 0x48-0x4F gap
- the keypad engine ($CC53/$CC7E): 4-row matrix scan, row-patched RAM
scripts, the $DC14 key-code table, message type $8B
- lamp readback ($21C2) and the 72-byte lamp-fault mask at $DFA8 --
fault reports are type $03 with the lamp index in $2519
- scan cycle budget: 1.91 ms/pass, 17.3 ms/scan, ~58 Hz; demux is 60%
- expansion: one spare buttons board, and nothing further without a
protocol revision
Corrections to existing docs:
- $CC53 was cited as the encoder sweep; it is the keypad-1 scanner.
The sweep is $C8CC-$C9A7, driven from $C0CB.
- U31 is on sheet 3, not sheet 1, and is completely uncommitted: no
address, data, strobe or pod-bus signal reaches it. Not an expansion
hook -- populating it does nothing without new wiring.
- No spare HCTL-2016 footprints exist. The decode has 3 free selects on
U9, but the PCB carries exactly 5 positions and sheet 1 draws 5.
More analog axes need hardware, not firmware.
Also carries the previously-uncommitted standard-rate (16550)
feasibility analysis and baudscan.py, plus a note that the FastRIO
"FTDI-class adapter" rule really means arbitrary-rate generation:
CP2102N qualifies, classic CP2102 snaps 31250 to 38400. Bench-measured
2026-07-26; on-cockpit latency check still pending.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gauges now read the real simulation: heat/power/sensor/mech attribute
tables published under the 1995 L4GAUGE.CFG spellings the widgets already
bind by name. Temperatures, coolant mass/capacity/leak rate, condenser
valve settings, generator voltages and numbers, radar percent, and the
mech's radar/speed rows all resolve to live members.
THE NUMBERING IS PINNED: the chain publishes 2..0x0E so
PoweredSubsystem::NextAttributeID lands on the authentic 0x0F -- the
surviving SENSOR.HPP numbers RadarPercent off it and MechWeapon's
binary-pinned table starts at 0x12, so its pads shrank 16 -> 3
(0x0F..0x11) exactly as the 5.3.x header comment predicted. Sensor's
authentic enum finally has its table defined; MechWeapon/Sensor rechain to
PoweredSubsystem and Reservoir/AggregateHeatSink to HeatSink so the whole
family is visible where the cfg expects it.
Verified with BT_GAUGE_ATTR_LOG: 48 OK / 2 NULL (was 33/17, and 0/50 at
the block's birth). The two remaining have no member to bind --
HeatSink/AmbientTemperature (a sim constant) and Searchlight/LightOn (a
memberless subclass) -- and fall to the documented zero cell. Gauge
fight (15/15 rounds, 127 zone hits), smoke and novice all clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six TUs born in one wave (4-agent re-hosting workflow from the BT411 donors
+ inline integration): BTL4GAUG (16 widget primitives + 5 connections, the
TU anchor @004c3f6c), BTL4GAU2 (13 composite panel clusters), BTL4GAU3
(PilotList/PlayerStatus/SectorDisplay/MessageBoard/mapping group), BTL4RDR
(the MapDisplay radar vs the authentic surviving header), BTL4GRND (the
real renderer: L4LampManager + L4Warehouse + BuildConfigurationFile with
the 19-entry BT registry chained onto the engine table). btl4.lib grows
7 -> 11 members; all 50 game TUs compile; link clean.
VERIFIED LIVE: with L4GAUGE=640x480x16 the interpreter parses the full
authentic L4GAUGE.CFG, Bhk1Init runs, ColorMapperArmor binds the live
mech's damage zones by name (cross-chassis names take the authentic inert
path), and the complete mutual missile fight runs with the cockpit stack
updating from real combat -- 16/16 rounds, 138 zone hits, zero faults.
Gauges-off regressions untouched.
Engine bring-up deviation (documented): ParseAttribute normalizes
unpublished attribute names to a shared writable zero cell -- the 1995
GaugeConnectionDirectOf ctor hard-derefs its source (first boot crashed
there); widgets on unpublished names draw static zeros and light up
automatically as the attribute waves publish the 1995 CFG spellings the
bindings already carry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three-dossier workflow output (65KB, verbatim): every class across the six
missing gauge TUs audited -- engine base, the L4GAUGE.CFG keyword + the
attribute each widget binds (and which reconstructed subsystem publishes
it), the port-shim -> 1995-native mapping, BC++4.52 hazards, and the
binary @ADDR anchors. This is the implementing session's hot-start
worksheet for the first rendered milestone (see RENDER-ROADMAP.NOTES.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four-dossier workflow synthesis: the engine half of BOTH render paths
(DPLRenderer/libDPL for the division-card 3D view; the L4 gauge stack for
the cockpit instruments) ALREADY compiles and links into btl4opt.exe from
the CODE originals, and the emulator's VPX board-side device is
EXE-agnostic (pure wire-level). What's missing is game-TU work with
complete BT411 donors: the 6 gauge TUs (~302 fns, the first rendered
milestone -- diffable against the shipped exe on the same GAUGE content)
and then btl4vid (the 3D world renderable factory). Full protocol map,
env gates, and the staged plan in RENDER-ROADMAP.NOTES.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scoring roles bound (binary ctor tail @004c0bc8): the mission role dict ->
Player::scenarioRole; TEST.EGG's Role::Default = the baked dfltrole
(lives=1000, killBonus=500) -- authored data flowing. The three score
handlers are live per the decomp derivations (@004c02e4 / @004c0200 /
@004c02a8 + CalcInflictedScore @004c052c; tonnage/bias staged); the
producers post damage scores per delivered hit and the kill credit from
the death transition with the latched killing-blow magnitude. First live
kill: award=3.43, kills=1, score=123.6.
Out of lives -> the mission END: the binary's +10s review post sits in the
one decomp gap, so the staged stand-in enters the engine's own cascade
(StopMissionMessage +10s) -- EndingMission -> the 3s fade -> the second
Stop -> Application::Stop -> RunMissions exits. The BTL4/L4 stop layers
run authentically (plasma score display off, egress lamps).
VERIFIED END-TO-END: a 2-lives egg (role-page return=2 override) -- death
#1 debits and respawns healed; death #2 is OUT OF LIVES; +10s later the
mission ends and BTL4OPT.EXE exits CLEANLY to DOS (GAME-RC prints). The
full 1995 pod mission loop -- egg, boot, spawn, fight, score, die,
respawn, die, mission end, exit -- now runs from reconstructed source.
Zero faults; fight/smoke/novice regressions green. Dev knob:
BT_PLAYER_PASSIVE=1 (piloted mechs hold force-fire).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full circuit, workflow-researched (5 parallel dossiers over BT411 + the
engine) and adversarially reviewed (3 lenses) before landing:
Mech side: the once-per-death transition in Simulate (freeze, motion kill,
DeathShutdown sweep, MASTER-only VehicleDead(-1) to the player link);
Mech::Reset @0049fb74 (reposition + heal every hull zone + the roster
DeathReset sweep + PreRun -- the reset-based respawn that REUSES the entity);
dead-owner terms in both weapon hard gates (wrecks fall silent).
Player side: the VehicleDead(-1) branch (deathPending dedup, deathCount
bump+stamp, scenario-role life debit, +5s re-post -> the engine drop-zone
hunt) and the DropZoneReply respawn branch (reset the dead mech at the
replied drop zone; probes on a live mech are moot).
Subsystem side: the engine base virtual DeathReset implemented across the
family -- MechSubsystem (zone heal + DestroyedState clear), MechWeapon (full
powered/thermal restore + fresh Loading cycle), AmmoBin (restock from the
ctor-cached count), HeatSink/HeatWatcher/PowerWatcher/Generator/Sensor/
MissileLauncher.
Review catches fixed before commit: AmmoBin restocked through the FREED
padBuffer pointer (use-after-free -> initialAmmoCount; MechSubsystem::
resource is now documented as never-deref-post-ctor); died-hot weapons
respawned at FailureHeat (now full thermal re-init); Generator tap counts
desynced across reset (preserved); Sensor skipped the base heal; PowerWatcher
inherited a statically-bound reset; cooling toggle + connect mode restored.
Verified: kill -> wreck (0 shots while dead) -> +5s -> drop-zone hunt ->
HEALED + placed at a real drop zone -> 134 shots after respawn incl. 12 SRM
(restock proof); unowned enemy wreck settles silently; fight/smoke/novice
regressions green, zero faults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DMGTABLE born (type-29 streams, BT411 byte-verified format): the height x
angle grid -- DamageLookupTable rows by height, PieSlice cells by angle
(rotate-with-torso rows add the live twist), DamageZonePercentTable = the
cumulative hit-distribution roll (first threshold above the roll -- the
BattleTech dice scatter). Loaded in the Mech ctor by the DamageZoneStream
member's NAME (mech+0x444, resident); Mech::TakeDamageMessageHandler
resolves invalidDamageZone hits through it (binary @0x4a0264 tail).
The missile fuse becomes the authentic unaimed producer: zone -1 + the
round's world position as the impact point (ENTITY3.HPP: "damage zones are
only valid via reticle based weapons").
Fight-verified on the authentic 7-row bhk1 table: flat-flying SRMs strike
low -> row 0 -> FEET AND LEG zones, side-correct by impact angle (+x right,
-x left), identical geometry spread across the cell distribution by the
roll. 17/17 missile lifecycle, zero faults; smoke + novice green. Also
fixed: the ctor's reservedState zero-loop counted past the shrunken array.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MechWeapon::GetMuzzlePoint (binary @004b9948): the mount segment's world
frame via GetSegmentToWorld (segmentToEntity x localToWorld), translation
extracted by transforming the segment-space origin. The missile spawn now
launches from the rack mount -- verified live: SRM6_1/SRM6_2 leave from
distinct world positions 4.0 units above the hull origin, laterally offset
per rack and rotated through the mech's yaw, on BOTH mechs. 19/19
launch-detonate over 100s, zero faults; smoke green.
(The authored MuzzleVelocity up-tilt composed in the mount frame + the mech
velocity term remain with the beam/render wave -- the seeker converges from
the aimed initial velocity either way.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The explosionModelFile decode: SRM6's streamed value 17 is a real resource
FAMILY id -- 'mslhit', 3 members: VideoModel(225), AudioStreamList(227) and
an 8-byte GameModel(228) = { int 498, maxTimeOfFlight 5.0 }. The round now
spawns on that family and reads the AUTHORED 5.0s lifetime (range 800 at
~160 u/s checks out); the owner-family fallback stays for content without a
projectile family (sanity band keeps the default there).
Two engine truths closed the wave: (1) collision volumes are the Mover
DEFAULT -- a projectile spawn MUST pass NoCollisionVolumeFlag or the ctor
demands a BoxedSolidStream member the family doesn't carry (SearchList
NULL -> Lock() page fault -- the crash that originally masqueraded as a
"model-list index"); (2) the @0x414938 SearchList crash is the
missing-MEMBER case (Check compiled out) -- probe families manually before
handing them to engine paths. Resource type numbering mapped and recorded
(15=GameModel, 10=VideoModel, 20=DamageZoneStream, ...). BT_MODEL_DUMP=1
keeps the raw family/record dump as a decode bench.
Verified: 20 launches -> 20 detonations on the mslhit family over 110s,
zero faults; smoke + novice green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The death-row page fault was the THIRD strike of the uninitialized
zone-array trap: the staged spawn rides the mech family's resourceID, whose
DamageZoneStream makes the Entity base ctor allocate the 20-pointer
damageZones[] array with UNINITIALIZED slots -- the Missile never fills it,
and ~Entity's teardown deleted 20 garbage pointers through trash vtables
(the dtor-chain bisect showed every dtor completing, then EIP-in-heap).
Fix: the Projectile ctor NULLs every inherited zone slot. RULE, now
three-crashes-proven: any entity subclass that does not FILL the allocated
zone array must NULL it -- the engine never initializes the slots, on
construction OR destruction.
Missiles are now the default SRM delivery (BT_MISSILE_INSTANT=1 = the
5.3.18a instant-hit as an A/B opt-out). Verified: 20 launches -> 20
detonations -> 20 clean death-row teardowns over a 110s mutual fight, zero
faults; smoke + novice regressions green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the firmware-disasm-confirmed Firestorm box protocol in the
MatrixPortal replica (ESC X outline / ESC x mode-fill via a BOXOP operand
collector, plus the ESC Y region op), updates FIRMWARE.md with the findings,
and adds FS-plasma-bench.ps1 -- a simulated MW4 serial stream for benching
the display without a pod.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Missile entity family is reconstructed: Projectile : Mover gets a real
make-path ctor; Missile hosts the Seeker + MissleThruster roster (the
AUTHENTIC surviving SEEKER.HPP / MISTHRST.HPP interfaces -- the 1995
misspelling included) and flies the binary's guided integrator (@004bef78):
age/lifetime, seeker re-lead with the decoded loft constants
(200/50/300/0.1), thruster burn, steering (gain 4.0, deadband 1e-4), and
the proximity fuse delivering the cluster damage ONCE with the launcher as
inflictor. Live-verified end-to-end: LAUNCH -> flight telemetry (thruster
visibly accelerating) -> DETONATE on the target ~0.9s out.
OPEN: the first ~Missile off the death row completes, then the frame
page-faults with EIP in the heap -- the first entity/subsystem teardown the
reconstructed sim has ever run. The spawn path is therefore OPT-IN
(BT_MISSILE_FLIGHT=1); default SRM delivery stays the 5.3.18a instant-hit
cluster (verified 1:1, zero exceptions). Forensics + engine truths
(SearchList crashes on non-directory ids; MakeMessages need FAMILY resource
ids; explosionModelFile is a model-list INDEX) in MISSILE.NOTES.md.
Also fixed on the way: the Subsystem NAME ctor left damageZone
UNINITIALIZED (latent garbage for every name-built subsystem).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BT411 task-#62 truth: the arcade fires ONE cluster Missile per salvo and its
zone damage lands EXACTLY ONCE (burstCount feeds only the gyro bounce +
splash falloff, never the zone armor formula). The 5.3.16 per-missile
delivery loop was ~6x too lethal. MISLANCH now sends a single salvo-split
damage message (verified 1:1 FIRED-to-damage in the mutual fight; zero
exceptions). The cluster splash joins the Missile entity wave.
Also: BT_FORCE_ZONE=n dev hook (pin every hit -- the cascade verification
knob) landed with 5.3.17's runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mech::DamageZone::TakeDamage is the full binary cascade (@0049c690): LOD
artifact router with same-attacker clustering (@0049c40c, hysteresis 0.33),
artifact parent level = mean of children, Energy-hit generator shorts
through the crit plug binding (novice-exempt), weighted CriticalHit
(@0049ccc4), zone-death allotment push (SendSubsystemDamage @0049c9a8) and
the segment-table destruction descent (RecurseSegmentTable @0049cad4 --
SIBS + DESCEND via the engine EntitySegment iterators).
Supporting bricks: Mech's OWN handler table with the TakeDamage override
(latches lastInflictingID @0x43c; gyro feed staged; cylinder table
deferred); friend class Mech__DamageZone (authentic); subsystem private
zones ARMED (structureReference + armorByFacing[5] normalized -- crits are
measurable); ApplyDamageAndMeasure; ForceCriticalFailure sets
DestroyedState so the weapon FSMs' GetSimulationState()==1 hard gate went
live; Generator::ForceShort + PoweredSubsystem::ForceShortRecovery.
Fight-verified (mutual BT_SPAWN_ENEMY + BT_FORCE_ZONE=8 concentrated fire):
arm zone to 1.0 -> sibling searchlight zone dies (Searchlight2/ThermalSight
crits destroyed, HUD degrading) -> DESCEND kills the gun zone -> PPC_2 +
ERMLaser_2 + Condenser6 DESTROYED and FALL SILENT (0 shots after death;
undamaged PPC_1 keeps firing); vital-zone hit = MECH KILL; PPC hit shorted
GeneratorD. This art has zero artifact zones (redirect=0 across all 20 --
router verified dormant-correct). Smoke + novice regressions clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The simulation is now a FIGHT. Mech target slot (+0x388), authentic
Loaded->Firing gate (viewFireEnable && HasActiveTarget), UpdateTargeting
range refresh, and SendDamage via the engine TakeDamageMessage.
MECHDMG.CPP is BORN against the AUTHENTIC surviving CODE/BT/BT/MECHDMG.HPP:
the Mech ctor's Pass-3 hull zone fill constructs Mech__DamageZone per zone
(the Entity base only allocates the raw pointer array -- unfilled slots were
crash #1; a base-class fill parses each record short and skews the stream --
crash #2). Streamed ctor parses the full BT tail (flags / Scalar-weighted
criticals with the Master+Dynamic plug gate / LOD redirect table) and
normalizes the armor economy: scale[type]=1/(raw x armorPoints), legs
halved -- BT411-verified @0049ce50.
Live two-mech verification (BT_SPAWN_ENEMY harness in DropZoneReply):
20 named hull zones stream on both mechs; PPC lands 11.77/hit (12 x
chargeRatio^2, type 4), ER-M laser 3.43 (type 3), SRM6 salvos 5.83 x 6
rounds on independent zones (type 2, one message per missile -- burstCount
is NOT in the zone formula); repeat hits climb linearly, leg zones absorb
at half scale, zones reach 1.0 Burning/Destroyed. No-target and novice
regressions hold (weapons load but never fire without a target).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The emitter charge cycle is now the authentic electrical model end to end.
- WIRE-VERIFIED resource fix: the Emitter block is graphicLength /
dischargeTime / seekVoltage[5] (FRACTIONS of the generator's rated voltage,
-1 sentinel) / seekVoltageRecommendedIndex. The old two-field guess read
seekVoltage[3]=0.99 as "dischargeTime 0.99s" -- the true PPC discharge is
0.2s, and the calibration reads seekV={6000,7000,8000,9900} x rated 10000,
recommended gear 2: the documented curve exactly.
- Ctor calibration (@004bb120): energyTotal=(dmg+heat)x1e7; EC pins
E=0.5V^2EC to energyTotal at the recommended gear; voltageScale calibrates
the exponential charge to reach seekV[rec] in the authored RechargeRate
seconds cold; damageFraction = the damage share.
- TrackSeekVoltage (@004ba838) + ChargeTimeScale (@004b0d50): the charge
integrates from the generator through the heat-stretched time scale, and
the I2R loss heats the GENERATOR -- verified GeneratorA at 477.9K charging
its PPC (BT411 predicted ~+480K) while the avionics generator idles at 77.
Hot generators charge slower: the heat/firepower feedback loop is CLOSED.
- Emitter::ComputeOutputVoltage override (now virtual, as in the binary
vtable): dial = level/seekV[gear], 0.01 snap, over-1 clamp.
- Sub-stepped Loading (1/60s slices -- the BT411 weapon-brick fix) + the
documented-divergence overcharge rescue. VERIFIED: the PPC loads at level
~7920, inside the binary's exact [7920,8080] snap window.
- FireWeapon energy algebra (@004bace8): damage/heat = energy shares x
chargeRatio^2. VERIFIED dmg=11.78 / heat=1.079e8 at ratio 0.9906. The
heatCost x 1e7 partial is retired -- heat now comes from the energy.
Zero Fail; jams/shutdowns regressions stay live under spam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The central heat bank is now its real class: the binary's 0x0BBE
AggregateHeatSink (the value our VDATA enum named HeatSinkClassID -- there is
no streamed plain HeatSink; the segment walk now builds the bank).
- Ctor (@4ae8d0): heatSinkCount from res +0xFC (bhk1 = 6, matching its six
condensers); thermalConductance x 0.1 x count (231000 -> 138600); ambient
setpoint 300 (the mission [mission] temperature overwrite joins the
Mech-PlayerLink wave).
- RadiatorSimulation (@4ae73c) replaces the base heat step on the bank -- THE
system's only heat exit: relax toward the ambient target with rate
k = conductance x (1-damage) x (coolant/capacity) x flowScale / mass; tail
tops the bank's coolant from the attached store via the DrawCoolant virtual
(base 0 until the reservoir-attach wave). VERIFIED signed-correct: the bank
warms 77 -> 300 from the cold start, then flips to actively radiating
(-48K..-167K/step) once fired heat pushes it past ambient. Until now heat
only ever POOLED in the central sink; the mech now genuinely sheds it.
- Reservoir master path: capacity = 0.05 x bankCount x streamed = 6 (the
authentic tank), refilled.
Zero Fail; the expert economy regression stays green (202 heat events under
forced spam).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The subsystem-family cockpit buttons now work through the authentic Receiver
dispatch -> per-class handler-table path. The id space decodes cleanly:
Receiver::NextMessageID == 3, so ToggleCooling = 3 on the HeatSink chain and
the per-class id 4 is MoveValve on a Condenser / InjectCoolant on the
Reservoir -- same number, different class, the binary's per-receiver-class
convention.
- HeatSink::ToggleCoolingMessageHandler (@004ad6f8, id 3): novice-locked,
press-only; toggles coolantAvailable + coolantFlowScale together.
- Condenser::MoveValveMessageHandler (@4ae464, id 4): novice-locked; cycles
the valve 1->5->50->0->1 and calls Condenser::RecomputeValves (@0049f788):
every condenser's coolantFlowScale = valve / sum-of-valves. The ctor now
streams the AUTHENTIC flowScale=0; the Mech ctor seeds equal shares once at
spawn -- the flowScale=1 interim is retired.
- Reservoir::InjectCoolantMessageHandler (@4aee70, id 4): novice-locked;
press arms the flush when the tank holds charge, release drops it.
- MechSubsystem::NoviceLockout() (@4ac9c8): owner -> playerLink -> experience
== novice; unlinked mechs read unlocked.
- DEV harness BT_PRESS_VALVE / BT_PRESS_FLUSH: dispatch the REAL messages ~6s
into the mission from Mech::Simulate.
VERIFIED: spawn shares 6 x 0.166667; one MoveValve press -> Condenser1
valve=5 flow=0.5, others 0.1 (valve/sum exact); flush arms via the real
button message; NOVICE locks both presses (valve lines stay spawn-only, zero
flush). Zero Fail throughout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The player-experience system now gates the entire heat/jam economy, verified in
BOTH directions with a one-token egg edit (TESTNOV.EGG, experience=novice):
NOVICE = 212 fire cycles all pinned at T=77, zero jams, zero shutdowns (the
heat model authentically absent); EXPERT = the full economy (duty cycles, 119
shutdowns + 2 authentic jams under forced fire). Zero Fail in both.
- BTPLAYER: the flag block renamed to its TRUE semantics (simLive @0x25c,
heatModelOn @0x260 -- the FUN_004ad7d4 master switch, advancedDamageOn pair,
levelFlag26c/270, experienceLevel); the ctor rows were already
binary-accurate (nov 0000 / std 1011 / vet 1111 / exp 1101); accessors +
[exp] sentinel.
- HeatSink::HeatModelActive() + ProjectileWeapon::LiveFireEnabled(): owner
mech -> Entity::GetPlayerLink() -> the BTPlayer flags, NULL-permissive.
Gates: both HeatSinkSimulation phases, every weapon fire-heat dump, and
CheckForJam is now the AUTHENTIC form (LiveFireEnabled + heatLoad<=0
early-outs + the minJamChance floor; the interim heat-degraded gate retired).
- THE LOAD-BEARING FIX: Mech ctor SetValidFlag(). Every 1995 entity ctor tail
marks itself valid; ours didn't -- Entity::Dispatch routes messages to an
INVALID entity into the deferred event queue, so the PlayerLink bind (and
every directly-dispatched mech message) silently never landed and the gates
read a NULL player forever.
- THE COOLANT SYSTEM (authentic bodies, byte-verified constants: HeatLoadScale
0.002 -> heatLoad now in [0,1]; equalize eps 1e-4; draw floor/ON
0.0025/0.003): UpdateCoolant (damage-scaled draw -- an undamaged mech leaks
nothing), BalanceCoolant (full clamp chain from ConductHeat), DrawCoolant
base=0 with the RESERVOIR override as THE SOURCE; Reservoir reconstructed
(capacity overlays thermalCapacity, CoolantSimulation + the full
InjectCoolant flush distribution, BT_FORCE_FLUSH dev hook); Condenser
RefrigerationSimulation (massScale = (1-damage)*refrigerationFactor >= 1 --
the heat pump that chills the bank; valveState inits 1; digit-suffix number
fix). Deferred: AggregateHeatSink family, cockpit-button handlers
(MoveValve/ToggleCooling/InjectCoolant), TrackSeekVoltage charge model.
Research driven by a 4-agent workflow dossier over the BT411 RE (verbatim
bodies for every function above + the egg experience plumbing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The heat economy now bites back, verified under sustained fire:
- Ballistic gate 1 (@4bbd36): destroyed-state or own-sink FailureHeat pins
full recoil + the unavailable alarm (the NoAmmo roach-motel re-asserts).
- CheckForJam (@4bbfcc): p = 0.41*T/failT clamped [minJamChance, 1.0], rolled
per granted shot against the MUNGA uniform Random. Interim heat-degraded
gate stands in for the deferred LiveFireEnabled novice switch (the exact
spurious-cold-jam trap the BT411 port documented). VERIFIED: SRM6s jam at
T~1200-1320 after riding past the authored 1000-degree threshold, and stay
jammed (mission-reset recovery only) -- authentic.
- Emitter hard gate (@4baab9): FailureHeat drops the beam state + charge each
frame; SELF-RECOVERING once conduction cools the sink below failure.
VERIFIED: the PPC settles into an emergent thermal duty cycle -- fire at
~1930K, spike to 2562K, shutdown, cool, refire -- firing exactly as fast as
its sink sheds heat; the lasers oscillate around ~2200K.
Zero Fail. Deferred: LiveFireEnabled/HeatModelOff experience gates,
mech-disabled gate halves, coolant depletion, jam recovery via mission reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moved from the vRIO repository to live with the other hardware
restoration work. Contains the PD01D221 reverse-engineering reference
(FIRMWARE.md, recovered from the TMS27PC512 EPROM dump) and the working
Matrix Portal S3 + HUB75 replica firmware (flashed and bench-tested
2026-07-22, including the G/B pin swap for these panels, the
PIN_BUTTON_UP/DOWN fix, and power-on cursor rendering).
The vPLASMA reference oracle (src/VPlasma.Core) and the VPlasma.Wire
differential-test tool stay in the vRIO repo; PlasmaNew pre-move commit
history is there too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Weapons dump firing heat into their own sinks, sinks conduct through the
Condenser bank into the central HeatSink, temperatures drive the degradation/
failure alarms, and every powered subsystem tracks its generator through the
electrical state machine. Verified: the authentic fire-discipline game -- a PPC
spikes 77->709K per shot, relaxes to 441K across its 5s reload, and sustained
fire climbs 441->674->838->960K, brushing the authored 1000K degradation
threshold. All calibration values match the BT411 audit exactly (central bank
1.39e6, PPC sink 174000, thresholds 77/1000/2000).
- HEAT: HeatSinkSimulation (@004ad924 absorb->T->load->conduct->alarm),
ConductHeat/ComputeHeatFlow (@004ad8ac/@004ad9ec two-body equilibrium
relaxation; flow==0 exactly at uniform T), UpdateHeatLoad (15-sample filter);
heat-state enum + accessors; installed by the HeatSink ctor.
- WIRE-VERIFIED: HeatSink resource gains linkedSinkIndex -- THE missing
ancestry int that shifted every descendant block +1 (voltageSourceIndex had
been reading the linked-sink index: "power sources" appeared to be
Condensers). Conduction topology wired from it at construction:
weapons->Condensers1-6->central; Generators->Condensers; Reservoir->central.
- MECHWEAP resource: authentic pip tail (pipPosition int + pipColor 3 floats +
pipExtendedRange int) per the BT411 verified overlay; with linkedSinkIndex
this closes the whole +3 alignment mystery (ProjectileWeapon pad deleted).
True bhk1 reads: PPC recharge 5.0s (not 1.0), discharge 0.99s, range 900.
- POWERSUB: ctor resolves voltageSourceIndex -> GeneratorA-D (wire-verified),
taps via Generator::TapVoltageSource (-1 when full); PoweredSubsystemSimulation
(@004b0bd0) electrical FSM (Starting/NoVoltage/Shorted/GeneratorOff/Ready);
GeneratorSimulation partial (start/short-recovery timers).
- Weapons: power step at sim head; Loading recharge gated on electrical Ready
(authentic @4bbdf5); FireWeapon dumps firing heat. HEAT UNITS: the chain is
1e7-native with TWO authoring conventions -- energy weapons store small
(PPC=11, x1e7 from the energy algebra), ballistics store native (SRM6=5.06e7,
dumped raw; double-scaling it was the bring-up runaway-temperature bug).
- SENSOR: authentic gating (@004b1c4c) -- power step, electrical-Ready gate,
heat-state switch (Degradation x0.5 / Failure 0). Verified Ready + 100%.
Deferred: coolant depletion/venting, the novice HeatModelOff gate, the charge
model (TrackSeekVoltage/voltage sag/I2R), weapon gate 1 + jam roll, the central
sink's forward-linked drain. Zero Fail across fire + neutral runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SRM6 MissileLaunchers now run the authentic ballistic fire cycle against
their linked AmmoBins, wire-verified end to end.
- PROJWEAP ProjectileWeaponSimulation (PARTIAL, binary @004bbd04 -- fully
recovered in the BT411 RE): one trigger-edge sample; the dry-bin gate pins
NoAmmo(7) every frame; Loaded(2) -- the AMMO PULL LIVES IN THE CALLER (a
denied shot does the 4->2 denial blip with NO ammo pulled -- early-returning
gates from FireWeapon while the caller cycles the alarm is the 1995 "denied
shot fakes a full firing cycle" defect class); granted -> Firing ->
FireWeapon -> Loading (or NoAmmo on the emptying pull), recoil=rechargeRate;
Loading bleeds recoil -> Loaded when a round is chambered, dial animates;
Jammed(5) held; NoAmmo(7) = the roach-motel (re-asserts unconditionally).
Deferred: electrical step, gate 1, eject, jam roll, HasActiveTarget.
- AMMOBIN: FeedAmmo() + GetAmmoState() (1 round-ready / 2 EMPTY).
- ammoBinLink wired from the resource's ammoBinIndex (the bin's roster slot).
- WIRE-VERIFIED RESOURCE ALIGNMENT: a raw-stream dump pinned the
ProjectileWeapon field block +3 ints past our staged struct (the MechWeapon
resource ancestry runs 3 ints short -- the pip-family fields). Fixed with
resourceAlignPad[3]; phantom muzzleVelocity/missileCount tail dropped.
Confirmed on-wire: ammoBinIndex=27/29 (the exact bin slots), tracerInterval=1,
minTOF=0.5, minVolt%=0.3, minJam=0.05, missileCount=6 (an SRM6!).
- MECHWEAP: weapon-state enum extended to the full 1995 set (DryTrigger 1,
Jammed 5, TriggerDuringJam 6, NoAmmo 7; count 8).
- MISLANCH FireWeapon PARTIAL (@004bcc60: heat + salvo spawn only -- spawn
needs the entity/targeting waves).
VERIFIED headlessly (BT_FORCE_FIRE): bins resolve with the authored 24 rounds;
SRM6s fire 6-missile salvos at the authored 1s cadence, rounds 24->0; after
the 24th salvo the launcher goes DRY and stays silent across ~3,000 subsequent
fire events while the energy weapons keep cycling. Zero Fail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The energy weapons now run a live fire cycle, and the REAL trigger wiring is in
place: the 1995 MechWeapon attribute table (binary @0x511890) is published with
PINNED IDs, so the streamed per-mech fire-button mappings bind TriggerState
(0x13) -> fireImpulse and the controls push writes the trigger into the weapon.
- MECHWEAP: attribute enum + table (PercentDone 0x12, TriggerState 0x13,
DistanceToTarget 0x14, TargetWithinRange 0x15, WeaponRange 0x16,
EstimatedReadyTime 0x1A, RearFiring 0x1B, WeaponState 0x1C). Pads bridge the
chain gap 2..0x11 -- LOAD-BEARING: AttributeIndexSet::Build leaves uncovered
gap slots as uninitialized garbage, so every ID up to the max must be covered
(they shrink to the authentic 0x0F..0x11 when the parent attribute waves land;
SENSOR.HPP pins PoweredSubsystem::NextAttributeID at 0x0F).
- MECHWEAP: fire-machine members (fireImpulse/previousFireImpulse,
rechargeLevel, rangeToTarget, effectiveRange, estimatedReadyTime, recoil,
weaponAlarm 0/2/3/4); CheckFireEdge (@004b9608, BIT-PATTERN int compare --
TriggerState carries raw ControlsButton ints whose release value is a float
NaN; IEEE compare would latch the detector shut); ComputeOutputVoltage
(@004b9c9c recharge dial).
- EMITTER: EmitterSimulation (PARTIAL @004baa88) -- Firing/Loaded/Loading FSM
on the weapon alarm, viewFireEnable gate at Loaded->Firing, recoil-decay
recharge over the authored RechargeRate seconds; FireWeapon (PARTIAL
@004bace8) discharge bookkeeping; ChargeLevel re-pinned to the authentic
0x1D; index re-chained to MechWeapon's. Deferred: electrical charge model
(TrackSeekVoltage), heat dump, HasActiveTarget gate, beam/damage submission.
- BT_FORCE_FIRE dev hook: trigger pulse whenever Loaded (auto-fire cadence).
VERIFIED headlessly: forward view -- PPC_1/2 + ERMLaser_1 cycle FIRED->LOADED
at the authored 0.6s/1s cadence, rear lasers silent; BT_FORCE_LOOK=3 -- ONLY
the rear lasers (ERMLaser_2/3) fire. The view-fire x fire-FSM interlock holds
both directions. Zero Fail. (SRM6 ballistic FSM = a later increment.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the weapon-side half of the look commit:
- MECHWEAP: real rearFiring/viewFireEnable members (binary @0x334/@0x3E0) +
accessors. Ctor resolves REAR-FIRING authentically (@004b99a8 tail): the
mount SEGMENT site name is tested for the 'b' (back) marker -- only the back
gun ports (sitelbgunport/siterbgunport) carry it. Spawn = forward-armed.
- MECH CommitLookState: re-arms every roster weapon per view (forward =
non-rear, LOOK-BACK = rear-mounted, side/down = none) and flips the HUD
reticle pip group with the authentic Reticle::Front/RearFiringWeaponsOn flags
(BT411's raw "bits 1/2" resolved to their 1995 names).
- MECHWEAP: defined MechWeapon::MessageHandlers (0-entry set inheriting the
Subsystem chain) -- it was declared-but-undefined, and the surviving CODE
PPC.CPP binds the inherited name into PPC::DefaultData; the zero-filled
common block was a latent NULL deref (same trap as Emitter::AttributeIndex).
Discovery: the TEST.EGG mech genuinely mounts TWO REAR LASERS (ERMLaser_2/3 on
the b-ports). Verified on a full-clean baseline (engine 181 + bt 42, 0 fail):
look-behind arms exactly the rear lasers, disarms the other five,
pipMask=0xfffffffe; neutral run holds forward-armed; zero Fail.
build410.sh HARDENED against the stale-obj layout-skew trap this wave exposed:
cc() skipped recompiles on obj-vs-.cpp timestamps only, so the MECHWEAP.HPP
member additions left emitter.obj on the OLD MechWeapon layout -- its
chargeLevel=0.0f write landed exactly on the new rearFiring (a silent cross-TU
struct skew, heisenbug-grade). cc() now honors header stamps: a source410
engine-header edit invalidates every bucket, a BT/BT_L4 header edit
additionally invalidates the game buckets. CODE/ is immutable, no stamp needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstructs the binary's five-state LOOK machine (controls mapper tail,
part_013.c:396-459) as proper members/methods:
- MECHMPPR: LookState enum (None/Left/Right/Behind/Down) + lookState/
previousLookState (out of reserved[24] -> [22]). InterpretControls picks the
state from the look buttons each frame and on a CHANGE calls
Mech::CommitLookState. BT_FORCE_LOOK=<1..4> dev hook.
- MECH: authored look angles lookLeft/Right/Front/BackAngle (deg->rad from the
GameModel resource, guarded) + lookPitch/lookYaw + CommitLookState(int):
side looks yaw by the authored angle, look-behind = yaw pi + lookBackAngle
pitch, look-down = lookFrontAngle pitch, forward = identity. The per-frame
eyepoint compose in Simulate adds the live Torso elevation on top.
(reservedState [208] -> [202].)
- Deferred inside the commit (needs MechWeapon viewFireEnable/rearFiring):
per-view weapon fire re-arm + HUD pip group-mask flip.
Verified headlessly: model reads clean authored angles (L=60 R=-60 F=-10 B=0
deg -- ModelResource layout confirmed again); BT_FORCE_LOOK=3 fires ONE commit
([look] state=3 yaw=3.14159 pitch=0) and the per-frame compose holds eyeYaw=pi
with eyePitch=0.349066 (lookBackAngle + the 20deg-clamped elevation). Zero Fail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Investigated the next planned binding (elevation -> gun joints jointlgun/
jointrgun) before implementing it, and found it was the wrong target: BT411's
own reverse-engineering history records that NO gun/arm elevation joint exists
in the authentic engine. The pod's stick-Y aim pitches the COCKPIT EYE and the
weapon boresight directly (mech4.cpp @~5219, pixel-calibrated against real
screenshots) -- the torso geometry never tilts for elevation on this mech
family. Reconstructing a gun-joint binding would have been invented behavior.
- MECH.HPP: EulerAngles eyepointRotation member (out of reservedState, now
[208]) + GetEyepointRotation() accessor.
- MECH.CPP: composed each frame in Simulate -- eyepointRotation =
EulerAngles(Radian(Normalize(lookPitch+elevation)), Radian(Normalize(lookYaw)),
Radian(0)), reading Torso::CurrentElevation() via sinkSourceSubsystem.
lookPitch/lookYaw are 0 until the look-button wave (BTCommitLookState) lands.
Verified: BT_FORCE_ELEV=0.8 -> torsoElev and eyePitch both read 0.349066 (the
authentic 20 degree resource limit) every frame, exact match; neutral -> 0.
Zero Fail. Corrected TORSO.NOTES.md's prior (wrong) "next: gun joints" note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstructs the shared skeleton-joint resolver and binds the first subsystem
(Torso) to the live skeleton.
- Mech::ResolveJoint(name) (mech.cpp @00424b60): GetSegment(name) -> segment
joint index -> GetJointSubsystem()->GetJoint(idx).
- Verified the JointedMover base streams the full skeleton headlessly:
jointCount=19, 40 named segments (gun joints jointlgun/jointrgun, shoulders,
hip, leg joints jointlthigh..jointrankle). BT_MECH_LOG [skel] summary;
BT_SKEL_DUMP lists every segment/jointIdx.
- Torso resolves + binds its twist joint (ResolveJoint(torsoHorizontalJoint))
and pushes currentTwist onto it via Joint::SetRotation (hinge->Radian,
ball->EulerAngles yaw). The TEST.EGG mech has a fixed torso (horizJoint='',
enabled=0) so its twist path is inert -- correct + guarded, zero Fail.
Next joint piece: elevation -> gun joints (jointlgun/jointrgun, the fixed-torso
aim mechanism) via weapon-joint binding; then gait leg animation. Both are
VISIBLE only under the renderer; headlessly the joint transforms are loggable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstructs Torso::TorsoSimulation (installed as the Torso's per-frame
Performance) and wires the torso aim into MechControlsMapper::InterpretControls,
so the control surface now covers aiming as well as locomotion.
- TORSO: analogElevationAxis/analogTwistAxis + SetAnalog* setters,
CurrentElevation()/GetHorizontalEnabled() accessors. TorsoSimulation slews
currentElevation += analogElevationAxis*baseElevationRate*dt clamped to the
vertical limits, and (if horizontalEnabled) currentTwist by the twist axis
clamped to the horizontal limits (authentic torso.cpp core; the skeleton-joint
application is deferred with the render wave).
- InterpretControls: routes stick pitch (stick_y, squared) into the torso
elevation every mode; Std/Vet route stick yaw into the twist. BT_FORCE_ELEV
dev hook.
Verified: BT_FORCE_ELEV=0.8 -> the elevation slews up and clamps at the authentic
verticalLimitTop = 0.349 rad = 20deg; neutral -> 0; zero Fail.
Deferred: skeleton-joint application (render wave), HUD free-aim slew,
look/eyepoint commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Mech ctor now reads walkingTurnRate / runningTurnRate / maxAcceleration /
throttleAdjustment from the GameModel resource (SearchList(GameModelResourceType)
-> ModelResource), replacing the bring-up-default constants for those fields.
Each read is sanity-guarded (out-of-band -> keep default) since BT411 flags parts
of the ModelResource layout as mis-decoded -- but the live values came back clean
and authentic: walkTR=75deg/s, runTR=50deg/s, maxAcc=30 u/s^2, throttleAdj=1.0
(walking turns faster than running; maxAcc matches the madcat note), confirming
our reconstructed struct layout is correct for these fields.
Verified: at walk speed the authentic 75deg/s tightens the turn circle to r~6.9
(=speed/turnRate); zero Fail. Still on defaults (pending LoadLocomotionClips):
reverseStrideLength (top speed) / walkStrideLength / reverseSpeedMax -- measured
from the walk/run animation clips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Installs Mech::Simulate as the mech's per-frame Performance (was DoNothingOnce).
Reconstructs the load-bearing spine of the 1995 Simulate (mech4.cpp @004ab430):
integrate the body velocity into localOrigin (linearPosition.AddScaled(..,
worldLinearVelocity, dt)) and commit the Origin to the world transform with the
engine idiom (localToWorld = localOrigin, per ENTITY.cpp:988 / MOVER.cpp:850).
Uses the NAMED engine base Mover fields (localOrigin/localToWorld/
worldLinearVelocity) -- BT411's MechBaseLayoutCheck proved the WinTesla decomp's
raw this+0x100/0x260 offsets actually stomp those base fields; the clean 1995
build addresses them by name.
- MECH.HPP: Mech Performance typedef + SetPerformance + Simulate() decl.
- MECH.CPP: SetPerformance(&Mech::Simulate) at ctor tail; Simulate body.
Verified headlessly (BT_MECH_LOG "[sim] mech pos=" 1Hz): BT_DRIVE="5,0,10"
advances the mech +5.0/s x, +10.0/s z (y fixed) = exactly the injected world
velocity; no BT_DRIVE -> worldLinearVelocity is 0, mech holds spawn pose. Zero
Fail/Exception either way. BT_DRIVE is a retained dev hook for headless motion.
Deferred (locomotion wave): gait-cycle self-propulsion feeding worldLinearVelocity
(IntegrateMotion->AdvanceBodyAnimation, steered by mapper throttle/turn), heading
integration, terrain-height drop, cockpit telemetry filters.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The world now executes. Entity::Execute only PerformAndWatch's entities in
application state RunningMission, reached by two RunMissionMessages
(WaitingForLaunch->LaunchingMission->RunningMission). The 2nd is dispatched by
Player::ManageApplicationStatus when the launch fade expires -- but that only
runs inside PlayerSimulation, not the launch-phase HuntForDropZone. So the
player must switch Performance onto PlayerSimulation after it spawns.
- BTPLAYER.CPP DropZoneReplyMessageHandler: after CreatePlayerVehicle +
InitializePlayerLink, choose the per-vehicle Performance by class -- Mech ->
SetPerformance(PlayerSimulation)+SetScoringPlayerFlag; else CameraShipSimulation.
- BTPLAYER.CPP PlayerSimulation: chains the authentic base Player::PlayerSimulation
(CalcRanking + ManageApplicationStatus + vehicle-pos copy + status service);
console SCORE-delta flush deferred to the scoring wave.
- SENSOR.CPP SensorSimulation: minimal-safe partial (radarPercent = 1 -
GetSubsystemDamageLevel, sensor healthy) so the roster tick path can't Fail
once RunningMission engages. The heat/electrical gating awaits the power/heat
sim wave (PoweredSubsystem/Generator/HeatSink/HeatWatcher *Simulation staged).
Verified: BT_MECH_LOG run reaches "[tick] roster live (first Sensor frame)",
two RunMissionHandler transitions, zero Fail/Exception. The mech is executed
each frame; its BODY Performance is still DoNothingOnce (real Mech::Simulate =
Phase 5.3, now the frontier).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase-5.1 crash solved + two BTPlayer bricks reconstructed. The tree now
builds AND boots end-to-end with no staged Fail() reached, into the running
per-frame game loop.
- EMITTER.CPP: define Emitter::AttributePointers[]/AttributeIndex (ChargeLevel,
chained to Subsystem::AttributeIndex) and point Emitter::DefaultData at it.
EMITTER.HPP declared the index but the .cpp never defined it, so the surviving
PPC.CPP (PPC::DefaultData binds inherited Emitter::AttributeIndex) and
GAUSS.CPP (chains it) captured a NULL activeAttributeIndex -> GetAttributePointer
faulted on the first PPC (roster slot 22). Fix clears all 33 roster slots and
the control-mapping binding block.
- BTPLAYER.CPP InitializePlayerLink: real body -- dispatch PlayerLinkMessage
(our EntityID) to playerVehicle + replicants (mirrors engine
CameraDirector::CreateCameraShip). No staging.
- BTPLAYER.CPP VehicleDeadMessageHandler: non-death flavours (-2 drop-zone probe,
>=0 respawn re-post) delegate to the authentic Player base handler; only the
-1 death cycle stays staged (needs mech4 + scoring roles).
Verified: 516-line smoke run, zero Fail/Exception/abort; boot reaches
LBE4ControlsManager::Execute per-frame, waiting only on absent RIO hardware.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Trace confirms the MechControlsMapper (slot 0) publishes its control attributes
correctly: throttleAttr/controlModeAttr resolve to non-NULL pointers. So the
mapper's AttributeIndex works. The post-construction crash (GetAttributePointer
attr=0x13) is on a DIFFERENT object (EAX heap addr after the mapper) -- a streamed
watcher targeting another simulation, not the mapper. Localizing next. (BT_MECH_LOG
mapper trace retained.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstructed the MechControlsMapper attribute table (StickPosition..PilotArray,
IDs 3-0x16 chained from Subsystem::NextAttributeID==2) + the real members
(stickPosition ControlsJoystick, throttle/pedals/speed/turn Scalars, the look/
torso ControlsButtons, control/display/pilotArray ints) + AttributePointers[] +
AttributeIndex (chains Subsystem::AttributeIndex). Fixed the mapper hierarchy
statics in BTL4MPPR.CPP: L4/RIO/Thrustmaster ClassDerivations now chain correctly
(L4 defined first for static-init order) and all use MechControlsMapper::
AttributeIndex so the mapper instance publishes the control attributes the
streamed mappings bind to.
Mech still constructs (33 subsystems). The post-construction crash is UNCHANGED
(GetAttributePointer, attribute=0x13) -> so it's NOT the mapper; a streamed
AttributeWatcher (numeric ID 19) resolves on another object via GetSimulation.
Next diagnostic: which object/subsystemID the watcher targets. BT: 42 ok.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Mech ctor fully constructs (33 subsystems / 7 weapons on real TEST.EGG data,
trace-confirmed). The crash is the control-mapping resource binding in
MakeViewpointEntity resolving subsystem attribute IDs via GetAttributePointer --
the subsystems publish none (reuse base AttributeIndex). Documented the precise
phase-5 step-1 scope (per-subsystem AttributeIndex + the ID chain) in MECH.NOTES.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live trace (BT_MECH_LOG) on the pod TEST.EGG mech: '[mech] segment walk done:
subsystemCount=33 weaponCount=7' -- the segment walk instantiates the ENTIRE real
subsystem roster (31 subsystems + 2 sentinels, 7 weapons) from the actual streamed
model, no crash. The structural reconstruction is proven end-to-end on real data.
The crash is AFTER full construction, in MakeAndLinkViewpointEntity's attribute-
watcher setup (an AttributeWatcher resolving a subsystem attribute via
GetAttributePointer). Phase-5 frontier confirmed = per-subsystem AttributeIndex
tables + watcher wiring. Trace gated behind BT_MECH_LOG.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records that the Mech constructor works live (segment walk instantiates the full
roster, mech links as viewpoint, mapper installs) and the precise phase-5
frontier: Simulation::GetAttributePointer via PerformAndWatch, because the
reconstructed subsystems reuse the base AttributeIndex and publish none of their
real attributes. Phase-5 plan (per-subsystem AttributeIndex -> watcher/chain/plug
wiring -> mech2/3/4 per-frame sim -> rendering) in MECH.NOTES.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unrecognised subsystem classIDs now get a base MechSubsystem (Laser/ParticleCannon
-> Emitter) so control/damage bindings that resolve a subsystemID find a real
subsystem, not a NULL plug. This moved the boot PAST the first Link::AddToPlug
NULL-deref; it now runs to Simulation::GetAttributePointer -- the subsystem
ATTRIBUTE system. Each reconstructed subsystem currently reuses the base
Subsystem::AttributeIndex, but the streamed control-mapping / gauge bindings
reference subsystem-specific published attributes (Sensor RadarPercent, Generator
OutputVoltage, Emitter ChargeLevel, ...) that aren't in the base index -> the
attribute lookup walks off the end. Reconstructing each subsystem's AttributeIndex
(+ plug/capability-chain wiring) is the phase-5 integration. BT: 42 ok.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SetMappingSubsystem installs the control mapper into roster slot 0 (the streamed
control-mapping binds DirectMappings to subsystemID 0). MechControlsMapper ctor
constructs (per-frame InterpretControls deferred to phase 5). With RIO controls,
MakeViewpointEntity now builds the MechRIOMapper, installs it, and gets PAST the
controls check -- the mech constructs AND links as the viewpoint entity. Boot now
advances into the mission-startup / control-binding path (next: a real NULL-deref
there, deeper than the staged Fails). BT: 42 ok.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Mech ctor now finds the model's subsystem-model-stream resource
(SearchList by SubsystemModelStreamResourceType), wraps it in a MemoryStream,
and walks the segments -- switching on seg->classID to into the base Entity's subsystemArray, caching sensor/gyro/sinkSource/hud
and counting weapons.
KEY FINDING: the source410 VDATA ClassID values MATCH the 4.10 binary for the
core subsystems (0xBBD=Condenser, 0xBC3=Sensor, 0xBC5=Torso, 0xBD6=HUD, ...) --
BT411's 'mislabels' were its OWN wrong enum names. So the walk switches on the
real VDATA names cleanly. Unrecognised classIDs leave a NULL slot (roster stays
aligned) rather than aborting.
LIVE: boot now runs the full Mech ctor -- segment walk instantiates the roster,
no crash -- and advances PAST it to BTL4Application::MakeViewpointEntity, which
halts at 'Mech has no controls mapping!' (the slot-0 control mapper /
SetMappingSubsystem is the next brick). The largest function in the game
executes. BT: 42 ok; tree links clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaced MECH.HPP reserved[331] with the named Mech-own members: cached subsystem
pointers (sensor/gyro/sinkSource/hud) + messageManager + weaponCount, the 5
capability chains (ChainOf<Subsystem*>), and the embedded status/animation state
(4 AlarmIndicators, Reticle, 3 StateIndicators, 2 SequenceControllers, NameFilter,
telemetryFilter[5]=AverageOf<Scalar>, 3 CStrings) + a reservedState pad for the
phase-5 per-frame fields. The subsystem ROSTER (subsystemArray/subsystemCount)
lives in the base Entity (GetSubsystem/GetSubsystemCount), so the Mech only caches
back-pointers.
The Mech ctor now CONSTRUCTS all embedded members (alarms Initialize'd, reticle
armed, state indicators sized, telemetry filters sized, sequence controllers
Init'd) -- compile-verified -- then halts at the segment walk (the remaining
piece). BT: 42 ok; tree links clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>