Commit Graph
333 Commits
Author SHA1 Message Date
CydandClaude Opus 4.8 844c11f792 emulator: in-fork plasma display (serial2=plasma) window in the explode layout
Bring the pod's 128x32 plasma readout in-fork, completing the self-contained
glass cockpit -- no vPLASMA process, no pipe.

- serialplasma.{cpp,h}: CSerialPlasma, a serial<n>=plasma backend that parses
  the game's ESC P graphics stream (L4PLASMA streams whole changed rows;
  receive-only, no replies) into a 128x32 1bpp framebuffer. Transcribed from
  vPLASMA's PlasmaProtocol + VPlasmaDevice (graphics path); the factory
  text-mode/fonts aren't used by the game and aren't rendered, but their escape
  operands are still consumed so the parser can't desync. Seam PLASMA_GetFrame()
  for the renderer.
- vpxlog.cpp: draw the framebuffer as an amber-on-black window in the explode
  layout, lazily created once a plasma port exists, parked centered under the
  lower-left MFD (VPX_PLASMA="x,y,w,h" overrides); WS_EX_NOACTIVATE so it can't
  steal DOSBox focus.
- Register SERIAL_TYPE_PLASMA (serialport.h/.cpp, dosbox.cpp -- documented in
  the vpx-device README step 3d, since those stock files live in the git-ignored
  src tree).
- Confs: serial2 in the _rio confs + deploy templates switches from
  namedpipe pipe:vplasma to the in-fork plasma.

Live-validated 2026-07-24. Real pods keep directserial realport:COM2; the
standalone vPLASMA app keeps namedpipe pipe:vplasma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 12:48:07 -05:00
CydandClaude Fable 5 e6eba36713 emulator: net_diag.conf refresh + dev-rig run artifacts
net_diag.conf brought in line with the current dev-rig conf family (NIC
repoint era); LAST.EGG / FAILURE.LOG are the latest pod-run leftovers the
archive tracks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:02:49 -05:00
CydandClaude Fable 5 c26d7baf8d docs: published team credits + archive context index
README gains "The team" -- the published VWE BattleTech credits roster
(scan supplied July 2026) cross-referenced against the 1994-96 source in
this tree; HISTORY.md links the podbay developer-script names to the
credited team members.  context/ adds the archive index README and its
generator script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:02:49 -05:00
CydandClaude Fable 5 bd082d8c4a PlasmaNew: ESC X/x box commands in the replica + MW4 bench stream
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>
2026-07-24 12:02:37 -05:00
CydandClaude Fable 5 110ecec2ca BT410 Phase 5.3.18: flying missiles -- spawn/home/detonate LIVE, teardown open
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>
2026-07-24 11:58:40 -05:00
CydandClaude Opus 4.8 9dd0aa8702 emulator: RIO panel click-to-press + lamp flash blink
Make the in-fork glass cockpit fully interactive and finish the lamp model.

- Click-to-press: clicking a button's region in a VDB head window presses that
  RIO address into the game (lights white-hot while held, releases on mouse-up).
  vpxlog rt_wndproc hit-tests the click against the bezel geometry (MFD 4 top /
  4 bottom, radar 6 left / 6 right; the radar bottom indicators are display-only,
  not clickable); head windows are tagged via GWLP_USERDATA. Clicks arrive on the
  VPX render thread, so the seam serialrio RIO_HostButton() queues them under a
  mutex and pollInput() drains them on the emu tick through the same incHold/
  decHold path as the keyboard/pad -- so a click also lights the bezel.
- Flash blink: decode the lamp byte's flash bits (RioLampState 1 slow / 2 med /
  3 fast) -- an unpressed flashing lamp now toggles between its brightness level
  and off (half-periods 500/250/125ms); solid lamps unchanged, a press still wins.
- Focus: head windows are WS_EX_NOACTIVATE so a button click never pulls
  foreground off DOSBox (which would demote the emu thread and stop SDL polling
  the pad); a click uses MA_NOACTIVATE too. Do NOT enable SDL background joystick
  events -- that raced the emu-thread controller poll against SDL's main-thread
  pump and crashed the process.

Explode layout only, and only when a serial=rio port is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 11:50:53 -05:00
CydandClaude Fable 5 f2424e9fdc BT410 Phase 5.3.18a: salvo economy corrected -- one cluster hit per trigger
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>
2026-07-24 11:19:27 -05:00
CydandClaude Fable 5 d4f0aef6e1 BT410 Phase 5.3.17: the destruction cascade -- zone deaths DESTROY things
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>
2026-07-24 11:13:28 -05:00
CydandClaude Opus 4.8 86d6b950e5 emulator: B2 RIO lamp/button bezel on the VDB heads + explode-view polish
Draw the cockpit RIO buttons around the mono-MFD and radar heads (explode
layout), lit by the host-commanded lamp state -- the display side of the
in-fork glass cockpit. Live-validated 2026-07-24 (operator).

- vpxlog.cpp: per-head button bezel in pal_draw, reading serialrio's new
  RIO_GetPanelState seam. The 5 MFD heads get red buttons (4 top / 4 bottom,
  100px tall tucked under a grown 640x500 display so a 10px lip shows); the
  radar gets amber Secondary/Screen side columns (6x 104px each) plus a
  centered bottom indicator strip (the 4 spares). Lamp byte decoded to
  off/dim/bright (vRIO RioLampState, brighter of the two brightness fields);
  a press shows white-hot. No labels -- the MFD shows each button's function.
  Also swapped the two upper-outer MFD window NAMES to match their (already
  position-swapped) desktop locations.
- serialrio.{cpp,h}: RIO_GetPanelState(lamps, pressed) accessor -- returns
  false when no rio port, so non-rio configs and other heads render normally.
- pod-launch: in explode (dev) layout the Division bridge is a normal, freely
  movable window -- not pinned topmost (Focus.cs) and nudged to 8,40 so its
  title bar clears the top of the screen (its client was at 0,0, pushing the
  frame off-screen). Cockpit/kiosk keeps the topmost + 0,0 borderless look.

Only affects the explode layout and only when a serial=rio port is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:43:41 -05:00
CydandClaude Opus 4.8 11f0a082e3 emulator: repoint dev-rig confs to the current NIC (realnic F70BADD7A)
The old Network Bridge adapter (DB5521D) no longer exists on the dev rig;
pcap now binds the physical Realtek directly. Update the tracked rendered
confs so a networked launch from the repo tree reaches the host. Symptom of
the stale value was the guest looping "NO PACKET DRIVER FOUND" (pcap open
fails -> NE2000 absent -> ODI stack can't bind). net_diag.conf left alone
(unrelated pre-existing churn).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:43:41 -05:00
CydandClaude Opus 4.8 73b597df35 emulator: in-fork virtual RIO cockpit board (serial1=rio)
Emulate the cockpit RIO board natively inside the DOSBox-X fork, driven by
an SDL game controller + host keyboard -- no external vRIO process and no
named pipe. The game<->board round trip runs in-process at the emulated
UART's own cadence, so the serial round-trip dropouts (livelock, TXMAXIDLE,
rxburst, 15s retry) can't occur.

- serialrio.{cpp,h}: RIO 9600-8N1 device state machine + protocol codec +
  input mapping, transcribed from the vRIO app (VRioDevice + Protocol +
  InputRouter/BindingProfileFormat), minus transport/pacer/UI/locks.
  B0 = gamepad (5 axes + joystick column). B1 = keyboard field: MFD banks on
  the letter rows, F-keys = Secondary/Screen, numpad = flight controls,
  LShift/LCtrl throttle slew; PAUSE/ScrollLock toggle panel<->DOS; vRIO-
  grammar bindings file via bindings:/VWE_RIO_BINDINGS.
- vpx-device/README.md: device notes + apply steps (the DOSBox-X src tree is
  git-ignored, so this dir is the tracked source of truth; the three sdlmain
  keyboard-hook edits are documented there).
- net_{loop,rp}_rio.conf + deploy templates; render-bridge/
  gauge_arena_rio_sound.conf (standalone -egg trim); pod-launch --rio
  (mutually exclusive with --pipe).

Additive: real pods keep directserial realport:COM1; vRIO-over-pipe keeps
namedpipe pipe:vrio. Validated live 2026-07-22 (pad + keyboard field +
console networking) on the dist install.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:43:41 -05:00
CydandClaude Fable 5 76cfc64353 BT410 Phase 5.3.16: targeting + damage -- two-mech fights are LIVE
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>
2026-07-24 10:21:43 -05:00
CydandClaude Fable 5 b772e716fe BT410 Phase 5.3.15: the electrical charge model -- real charge, closed feedback loop
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>
2026-07-24 09:14:56 -05:00
CydandClaude Fable 5 6451a0a38d BT410 Phase 5.3.14: AggregateHeatSink -- the bank + the ambient radiator (the heat EXIT)
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>
2026-07-24 08:54:21 -05:00
CydandClaude Fable 5 c01e57ab22 BT410 Phase 5.3.13: cockpit-button message layer -- valves, cooling, flush LIVE
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>
2026-07-24 08:43:01 -05:00
CydandClaude Fable 5 e1e5a9a6db BT410 Phase 5.3.12: experience gates + coolant system -- novice/expert modes REAL
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>
2026-07-22 22:15:07 -05:00
CydandClaude Fable 5 b1cbbe1c9b BT410 Phase 5.3.11: weapon heat consequences -- jams + thermal shutdowns LIVE
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>
2026-07-22 21:20:37 -05:00
CydandClaude Fable 5 4391ddd322 restoration: import PlasmaNew — the plasma display replica effort
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>
2026-07-22 17:20:25 -05:00
CydandClaude Fable 5 57507cb15c BT410 Phase 5.3.10: power/heat wave -- the thermal + electrical economy is LIVE
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>
2026-07-21 23:55:48 -05:00
CydandClaude Fable 5 ab7191dc38 BT410 Phase 5.3.9: ballistic fire path -- SRM launchers fire salvos + run dry
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>
2026-07-21 23:18:39 -05:00
CydandClaude Fable 5 896bbbed4d BT410 Phase 5.3.8: weapon fire path -- authentic attribute table + fire state machine
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>
2026-07-21 23:01:57 -05:00
CydandClaude Fable 5 d422dfffc0 BT410 Phase 5.3.7: weapon view-fire gating + build header-stamp hardening
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>
2026-07-21 22:43:38 -05:00
CydandClaude Fable 5 eb8bba3195 BT410 Phase 5.3.6: look-button state machine -- full eyepoint composition
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>
2026-07-21 22:08:50 -05:00
CydandClaude Fable 5 87b1c048cf BT410 Phase 5.3.5: eyepointRotation -- torso elevation pitches the eye/aim-ray, not a joint
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>
2026-07-21 21:51:28 -05:00
CydandClaude Fable 5 9fff0e969e BT410 Phase 5.3.4: Mech::ResolveJoint + skeleton-joint binding (torso twist)
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>
2026-07-21 16:17:37 -05:00
CydandClaude Fable 5 3a0920acc0 BT410 Phase 5.3.3: torso weapon-elevation aim -- completes the control surface
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>
2026-07-21 16:02:54 -05:00
CydandClaude Opus 4.8 84ea322a18 Document the weapon-fire/pick-range investigation, paused per user
Records tonight's finding before stopping this thread: the user correctly
pushed back on an oversimplification (the renderer isn't a purely passive
wire tap -- the protocol has a real synchronous pick-query shape, sect_pixel/
sect_vector, where the board runs its own Moller-Trumbore ray-triangle test
and replies with range+identity). Checked directly against the REAL firmware
(not just the reference software renderer's notes):

- sect_pixel/sect_vector never appear in this capture -- unused by this build.
- Action 0x23 (the notes' fire/pick candidate) gets no reply from the real
  firmware either (checked via the h_reply hook, which fires on ANY
  firmware-issued reply) -- but its payload doesn't match the documented
  FLYK format at all (no screen coords; a counter-tagged discharge-event
  shape instead), and its wire context (list_remove + zone-color + a
  PSFX-shaped 0x1d) reads as a weapon-discharge notification with visual
  consequences, not a pick query.

Net: open question, not resolved. BT4.10/MUNGA's real pick/range mechanism
(if board-side at all) is not yet identified. Paused per the user rather
than continue an open-ended search without a specific pointer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:58:48 -05:00
CydandClaude Fable 5 0cc7713865 BT410 Phase 5.3.2e: source authentic per-mech locomotion params from GameModel resource
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>
2026-07-21 15:52:20 -05:00
CydandClaude Opus 4.8 ccc6ee6914 Fix HUD vertical flip: dpl2d Y grows DOWN, not up
User-reported live: the torso-twist tape appeared at the TOP of the reticle
(should be bottom) and the range ladder filled the wrong direction. I had
assumed dpl2d used a standard math y-up convention (y0=bottom, y1=top) and
inverted accordingly.

Found the exact, already-solved answer in vrview_gl.py's own HUD vertex
shader comment: "dpl2d Y grows DOWN (screen convention): period VHS footage
shows the twist dial UNDER the reticle and the range bar filling top-to-
bottom; mapping straight onto GL's Y-up NDC rendered the whole HUD flipped"
-- the exact same bug, already diagnosed and fixed there. y0 (the smaller
raw view-rect value) is the TOP of screen, y1 the bottom -- direct mapping,
no inversion needed.

Verified offline: the composited reticle now shows the twist tape (with its
bowtie shape) at the bottom and the range ladder/pips reading top-to-bottom,
matching BT411's documented layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:46:03 -05:00
CydandClaude Opus 4.8 3e766c13c1 Wire the HUD reticle into live_render.py
Integrates hud2d_overlay.HudTracker into the live pipeline: fed exactly one
wire record per firmware consumption (synchronized with r.qi, not just queue
arrival, so hud2d_root's view/display-list state matches what the firmware
has actually processed at render time), composited onto each rendered frame
before present.

Verified via the same integration pattern (truncated direct-queue boot, no
socket overhead) against netdeath-20260708.fifodump: the reticle composites
starting at wire command 22906 (frame 22) -- identical resolution point and
visual result to the standalone hud2d_overlay test. A full-mission run
through the live socket path is genuinely slow around this capture's "battle"
content (multi-billion firmware steps per command in that section, pre-
existing and unrelated to this change) but not stalled -- confirmed by
reaching the same cmd/frame numbers via the direct path in 14.7s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:48:14 -05:00
CydandClaude Fable 5 ebf0d8c23f BT410 Phase 5.3.2: DRIVABLE mech -- authentic control interpretation + locomotion
The mech now drives from the authentic control chain (faithful route). Verified
headlessly: full throttle -> walks straight at top speed (30 u/s) along heading;
throttle+turn -> walks a circle (radius = speed/turnRate); neutral -> holds pose;
zero Fail.

MechControlsMapper::InterpretControls (mechmppr.cpp @004afd10) -- installed as the
mapper's per-frame Performance (roster slot 0, ticks before the mech):
- speedDemand = topSpeed*throttle*fwdScale (reverse inverts); soft stick (square)
  / pedal (cube) response; Basic=stick turn, Std/Vet=pedal turn; speed clamped
  while turning hard. Reads owner stride via Mech accessors.
- BT_FORCE_THROTTLE/BT_FORCE_TURN dev hooks for headless verification.

Mech::Simulate drive -- consumes the mapper demands:
- accelerate currentBodySpeed toward speedDemand (maxBodyAcceleration);
- authTurnRate = lerp(walkingTurnRate, runningTurnRate) by ground speed + over-run
  falloff (mech4.cpp master-perf @0x4aa3d3);
- integrate heading via Quaternion::Add(prevPose, (0,turn*rate*dt,0));
- facing = world -Z basis (GetFromAxis(Z_Axis)); worldLinearVelocity = facing*spd;
- integrate position (increment-1 core).

MECH.HPP: 9 named locomotion members out of reservedState (now [211]) --
walking/runningTurnRate, reverse/walkStrideLength, reverseSpeedMax,
forwardThrottleScale, maxBodyAcceleration, body/currentBodySpeed. BRING-UP
DEFAULTS (authentic values come from the model resource + LoadLocomotionClips --
next refinement).

Deferred: gait-clip-exact advance + leg anim (needs animation subsystem/renderer),
model-resource sourcing, terrain drop, torso/free-look aim, telemetry filters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:46:30 -05:00
CydandClaude Opus 4.8 859529d6f2 HUD reticle: the missing wire mechanism found + composited (verified live)
The user asked to investigate why HUD never renders, reasoning it would also
inform the weapon-fire/targeting question. Root cause: the cockpit HUD (reticle
+ weapon pips) is NOT part of the 3D scene-graph wire traffic this renderer has
been parsing all session -- it's a completely separate 2D display-list protocol
(dpl2d_*, wire actions 0x29 NewDisplayList / 0x2b FlushDisplayList), which this
pipeline has never looked at.

BT411's context/gauges-hud.md + phases/phase-02-dpl2d-reticle.md already fully
documented this from the game's decompiled source (BTReticleRenderable, the
dpl2d opcode table). More directly useful: dpl3-revive/patha/vrview.py (the
OTHER renderer) already has a COMPLETE, TESTED interpreter for the wire-level
opcodes (hud2d_prims/hud2d_root) -- reused directly here rather than re-derived.

hud2d_overlay.py: HudTracker feeds incrementally from the same wire records
already flowing through this pipeline (minimal node type+body tracking, the
dcs->view list_add edge, and 0x29/0x2b display-list accumulation -- NOT a full
scene-graph, just what hud2d_root/hud2d_prims need). composite_hud() maps the
resolved primitives from view-rect space (x0..x1, y0..y1, y up -- confirmed
live: x[-1,1] y[-0.615,0.615], hither=0.25/yon=1300/zeye=1.732=cot(30 deg) --
independently cross-validating the earlier fog-formula derivation from live
wire data, not just the static BTDPL.INI) into pixel space and draws them.

Verified offline (netdeath capture): the HUD binding resolves at wire record
~22661 (later in the mission than earlier test frames, which is why it never
appeared in this session's spot-checks); composited frame shows a green
crosshair + tick-mark cross, a ring, and a right-side range ladder with
weapon-pip circles -- an exact match for BT411's documented reticle layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:40:31 -05:00
CydandClaude Fable 5 3364b65ae8 BT410 Phase 5.3.1: Mech::Simulate motion core -- mech integrates motion per-frame
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>
2026-07-21 13:45:24 -05:00
CydandClaude Opus 4.8 6d827cd9c8 VDB-NOTES: add canvas->6-displays data-flow overview + mermaid diagram
Narrative on-ramp before the register-level RE: two independent video
systems, the 16bpp byte-lane split (low=radar / high=5 mono MFDs), the
three Bt477 heads, and the octopus fan-out to the six cockpit displays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:43:36 -05:00
CydandClaude Fable 5 3610ff1ecd BT410: entity simulation goes LIVE -- mission reaches RunningMission, mech + roster tick per-frame
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>
2026-07-20 12:20:23 -05:00
CydandClaude Fable 5 812ca99652 BT410: reconstructed source BOOTS into the live game loop (zero Fail)
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>
2026-07-20 11:51:04 -05:00
CydandClaude Opus 4.8 a89d08e1c9 Commit the vrend410 firmware asset + RE tooling; gitignore run artifacts
- vrend410.mng: the production firmware, a HARD dependency of the committed
  live pipeline (emu_main.py MAPS['vrend410'].fw loads it) -- the repo couldn't
  run the live renderer without it.
- txdn_probe.py / texstore_trace.py / texel_trace.py: standalone reverse-
  engineering probes (texture-source/DRAM-write/texel tracing) documenting the
  texid-binding and texel-format investigations.
- .gitignore: exclude the firmware-decomp run artifacts (emu860c.pyd rebuildable
  from source, *.log/*.pkl/*.png renderer outputs + caches), editor junk, and
  the game runtime state/logs written by running the pod.

Left untouched deliberately: the parallel Mech session's in-progress
restoration/source410/BT/{EMITTER,BTPLAYER}.CPP edits (theirs to commit) and
the tracked game runtime state (LAST.EGG/CTMIX.CFG etc. -- transient).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:43:46 -05:00
CydandClaude Opus 4.8 b447e71864 launch_live.ps1: enlarge exploded cockpit windows +25% (0.75 scale)
Per user request, bumped the cockpit MFD/radar windows from 0.6 to 0.75 of
native 640x480 (MFD 384->480, radar 288->360 logical). Live-verified on the
secondary: MFDs now render 331x266 (was 267x218, ~+24% after the primary's DPI
downscale), radar 251x346, all six still fit the 1707x1067 secondary with
margin, renderer unchanged on the primary. No wire stall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:37:43 -05:00
CydandClaude Opus 4.8 f5fc241603 Clear the renderer background to the fog color, not black
User-reported: a hard black strip at the horizon, between the arena walls and
the sky geometry. Those pixels aren't covered by any polygon -- the game relies
on the board's own background clear there (the sky dome doesn't extend down to
the wall tops). Our renderer cleared the framebuffer to black, so the gap read
as a black band.

Fill the background with the FOG far-color instead: anything no polygon covers
is at effectively infinite distance = fully fogged, so the horizon gap now
blends seamlessly into the same haze the distant geometry fogs toward (filled
far pixels -> FOG_RGB via the fog blend; unfilled background = FOG_RGB directly
-> continuous). Offline-verified: the black strip becomes a smooth blue-purple
haze band. (The game's actual view-flush back_color may differ slightly from
the fog far-color; the fog color is the physically-consistent proxy until
back_color is parsed off the wire -- noted in code.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:33:09 -05:00
CydandClaude Opus 4.8 20dd8440d3 launch_live.ps1: -Explode puts the cockpit VDB windows on the secondary display
Adds -Explode: the pentapus cockpit instruments (radar + 5 mono MFDs, the VDB
video-splitter decode) render on the SECONDARY monitor while the reconstructed
i860+GPU 3D renderer stays the main out-the-window view on the primary. The
window rects auto-fit to whatever the non-primary display is (detected via
System.Windows.Forms.Screen, so it survives a monitor rearrange), scaled to
~0.6 and offset into that display's virtual-desktop region; the DOSBox SVGA
screen parks clear of the cluster.

The cockpit windows live in vpxlog's rt_main thread, which only starts under
VPX_RENDER=1 -- the same flag whose DOSBox-thread scene-graph decode previously
stalled wire delivery (fixed earlier by removing it). Feared this would bite
again; MEASURED and it does NOT: with VPX_EXPLODE + VPX_NOMAIN the heavy main-
window 3D render (rt_draw, gated on the main window existing) is skipped, so
rt_main only does the cheap per-tick MFD/radar blits and the DOSBox thread keeps
up. Live-verified: all 6 cockpit windows placed on the secondary (DISPLAY1),
renderer on the primary, wire advancing normally to cmd 23k+ (no command-8
stall), frames flowing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:25:47 -05:00
CydandClaude Fable 5 11680bcb3e Phase 5.1: mapper attributes VERIFIED resolving (throttle/controlMode non-NULL)
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>
2026-07-20 11:14:34 -05:00
CydandClaude Opus 4.8 66cef329d1 Implement fog: source-derived depth formula, not a guess
BTDPL.INI's fog values for this mission (testarn.egg = arena1/day/clear,
[ardayclear] leaf: fog=200.0 1250.0 0.32 0.3 0.5, clip=0.25 1300.0) are exact,
verified ground truth. The depth term took two tries: `tz` (texz) looked like
the right candidate (BT411 says fog is linear on "W") but measured live it
DECREASES with distance -- the wrong direction/shape. Re-derived from the
actual firmware source instead of guessing again: AS860/XFPROJ.SS's own
comment on the projection code ("pz=wz*invz, where pz=1 at near clip, 0 at
infinity") establishes the z-buffer value as a normalized hither/distance
quantity, giving distance = hither * 2^20 / raw_zbuf_value.

gpu_raster.py: added a 10th shader output channel carrying the raw z-buffer
float (previously only a filled/empty flag existed), and the fog blend using
this formula + BTDPL.INI's exact color. Verified offline: distance climbs
smoothly (394->1051) across a receding ceiling, landing inside the authored
200-1250 range with no further fudging; visually the horizon shows the
authored blue-purple haze, correctly absent near the camera.

IG-SHADING-MODEL.md: documented the false start (tz) and the source-grounded
fix, with the honest caveats that remain (2^20 exact scale, hither as a fixed
per-mission constant vs. per-view from the wire).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:13:55 -05:00
CydandClaude Fable 5 1f2f8ac42a Phase 5.1: MechControlsMapper publishes its 20 control-input attributes
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>
2026-07-20 11:10:44 -05:00
CydandClaude Opus 4.8 9ebdfc4610 IG-SHADING-MODEL.md: document tonight's ground-truth findings for BT411
Extends the existing shading-model reference (already the established pattern
for this cross-check) with tonight's live-pod findings, explicitly framed for
the BT411 modernization team's consumption per the user's stated purpose for
this whole pipeline: a hardware-grounded source of truth where the software
renderer has open or hard-to-fix questions.

Added: a cross-reference index into BT411's context/ docs (rendering.md,
open-questions.md, combat-damage.md); the lit-color-plane finding (real
mechanism, confirmed values, wrong when used as a texture tint -- reverted
same session); fog (BT411's complete model, not yet ported here); and honest
open/uninvestigated status for HUD, weapon-fire rendering, and cockpit-cage,
each pointing at where BT411 has probably already solved the adjacent
question so it isn't re-derived from scratch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:02:48 -05:00
CydandClaude Opus 4.8 0527022ecd Revert texture tinting by the lit-color plane -- it double-colours
Found my own prior documentation (IG-SHADING-MODEL.md sec 2, written earlier
this session before compaction) that already establishes: the wire's 0x1a
texture uploads arrive ALREADY colourised (the material ramp is pre-baked on
this path, per the shipped libDPL header model cross-referenced against
BT411's context/rendering.md). Tinting them again with the lit-color plane
double-colours the result -- exactly the "more colors but neither correct"
outcome reported live (sky/ground now differ, but neither right).

Keep the lit-color plane for FLAT (untextured) polys only, where it replaces
a hardcoded placeholder with real per-polygon data; textured polys sample
unmodified, as documented. The underlying r24/g24/b24 mechanism itself is
still real and worth understanding precisely (see open-questions tracking),
but applying it as a texture tint was the wrong model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:01:27 -05:00
CydandClaude Opus 4.8 3473f5facf live_render: skip the per-frame PNG write once a window is showing
Image.fromarray(...).save() ran on EVERY frame regardless of whether a
window was already displaying it -- pure disk-I/O overhead adding to the
render-vs-game latency gap. Only write PNGs when there is no window (the
offline/headless case, where they are the actual output).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:43:30 -05:00
CydandClaude Opus 4.8 854ec0b216 Live-view fixes: stale-frame skip, mid-mission catch-up, lit-color plane
Three real issues found while watching the live-render session against the
actual running game and fixed:

1. STALE-FRAME SKIP (live_render.py): the renderer treated every single
   draw_scene as a frame to fully render+present. The game ticks at a
   measured ~28 draw_scene/s (matches the documented FAST 28Hz SOS clock),
   faster than our render, so a backlog built continuously -- we were
   perpetually rendering OLD content, which read as "frozen or 30s+ behind."
   Fix: SockSource.has_pending() detects an already-buffered next record;
   when backlogged, skip render+present (still consume/step normally) so we
   always show the freshest available state instead of crawling through
   history. pump() now drains aggressively so the backlog check is honest.

2. MID-MISSION CATCH-UP (live_render.py --catchup): reconnecting to an
   already-running pod produced a black screen. Root cause: draw_scene only
   means "render what's already loaded" -- it carries no geometry itself.
   The socket tee forwards only NEW traffic to a fresh client, so a from-
   scratch firmware boot has an empty scene graph. Fix: replay vpxlog's
   archival VPX_FIFODUMP (a second, independent sink of the same wire,
   written since pod boot) as the initial queue before going live -- exactly
   the same catch-up vpxlog's OWN native bridge already relies on for this
   same reason.

3. LIT-COLOR PLANE (gpu_raster.py): user-reported "textures are mostly
   grayscale" and "no fog effect." Found the actual mechanism in the real
   firmware source (sda4 DIVPXMAP.H + EOF.C): TREEclmpintoMEM (op 0x5a)
   writes dvpx_r24/g24/b24 -- a per-polygon lit color -- which EOF.C later
   copies into dvpx_eofr/g/b (the final screen color) via a straight 24-bit
   CPY in the simple (unfogged) case. Our renderer decoded this instruction
   but never used it, instead hardcoding flat polys to a placeholder
   (60,60,70) and sampling textures with no lighting modulation at all.
   Verified live on the wire: the value is a DIRECT 0-255-ish brightness
   (not a [0,1] float to rescale -- observed range ~49..395 with real
   per-polygon variation, only the brightest few clipping), consistent with
   a straight clamp-to-8-bit write. Now decoded (addr 118/126/134, empirically
   confirmed against DIVPXMAP.H's r24/g24/b24 spacing) and applied: flat polys
   get the real lit color instead of the placeholder; textured polys are
   tinted by it (texture * lit/255). Offline-tested (no crash, visibly
   plausible result: a lavender-tinted floor with real near/far gradient
   replacing the flat gray-blue placeholder) -- NOT yet cross-checked
   against a ground-truth screenshot, so treat the exact hue as provisional
   even though the underlying mechanism is verified real.

Honest scope note: this changes gpu_raster's rendered colors on purpose, so
it no longer bit-matches the OLDER frame_*.png CPU reference from before this
fix (that reference predates lit-color decode). The geometry/perspective/
raster correctness those bit-identity tests proved is unaffected -- only the
color post-process changed, layered on top.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:42:51 -05:00
CydandClaude Opus 4.8 ef7db9f2e4 Fix live-render lag: bulk memory read + masked texture compositing (~2.1x)
User-visible bug: connected the reconstructed renderer to the REAL running pod
for the first time (M4d demo) and the render fell increasingly behind the live
game, plus looked near-grayscale. Root-caused with direct profiling instead of
guessing:

1. VPX_RENDER=1 was set in launch_live.ps1 (copied from launch_pod.ps1's
   defaults without thinking it through) -- it starts vpxlog.cpp's OWN native
   GL render thread (rt_main), which contends with our renderer for the same
   CPU/GL resources. Live-caught: with it set, wire delivery stalled at wire
   command 8 for 90+s; removing it, the same mission reached command 21,488
   in the same window. This was the dominant cause of the visible lag.

2. gpu_raster.build_prims() scanned the whole per-draw program window
   (0x08158000-0x08170000, ~24k words) via a Python-level r32() call per word
   every frame. Added emu860c.dump_range(lo,hi)->bytes (one bulk C read) and
   rewrote build_prims to index a numpy array instead -- verified byte-identical
   dump_range output first, then reconfirmed the whole render is STILL
   bit-identical to the CPU reference (frames 0/5/11, differ>24 = 0.000%).

3. Profiling after (1) showed the per-texture compositing loop was the real
   remaining cost: 62ms/frame doing full-832x512-frame fancy-indexed texture
   sampling for EACH of 13 textures, regardless of how few pixels use each one.
   Added dpl_sampler.composite_masked (sample+composite only the pixels a
   texture actually covers; verified equivalent to composite() on a partial
   mask in dpl_sampler's own conformance test) and hoisted the invariant
    out of the loop.

Combined: render time 95.8ms -> 54.9ms/frame (~1.75x from this change alone,
~2.1x vs the original ~117ms/frame baseline), still bit-identical to the CPU
reference throughout.

launch_live.ps1: removed VPX_RENDER, renamed the automatic-variable shadow
 ->  (flagged by PSScriptAnalyzer), added -Pin <fifodump> to
pre-load the full texture set (bit-identical to offline) instead of the
incremental default, for a demo that wants correct color immediately rather
than waiting on the live mission's own upload order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:07:55 -05:00
CydandClaude Opus 4.8 bf234842cb Commit the texel-format fix + shared dpl_sampler module (was uncommitted)
These changes were made and verified earlier this session (the all-cyan bug
fix is already recorded as SOLVED in project memory) but never got committed --
render_final.py has referenced dpl_sampler as a module in its docstring since
ac4142e7, yet dpl_sampler.py itself was never added, leaving the repo unable
to run the M4b/M4c/M4d renderer scripts that all import it.

texstore.py: fix the texel word format. SVT texels are [pad,B,G,R] (ground
truth: dpl3-revive patha/vrboard.py do_texels + stage_assets.py); reading RGB
as bytes (0,1,2)=(pad,B,G) zeroed red, producing the all-cyan render. Correct
read is bytes (3,2,1).

dpl_sampler.py: the authentic IG-board texture-value model (wrap repeat/clamp,
near-black CUT keying) factored into its own tested module, distilled from the
shipped libDPL headers -- see IG-SHADING-MODEL.md (also added, referenced by
render_final.py's docstring but likewise never committed).

igc_exec.py: OP48/OP49 candidate opcodes (pixel global-X/Y seed into mem) added
to the CPU golden-model executor alongside the existing SCMEMA seed handling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:07:16 -05:00
CydandClaude Opus 4.8 a689bce660 Fix launch_live.ps1: don't enable Dave's native render thread
VPX_RENDER=1 was copied in from launch_pod.ps1's defaults without thinking it
through -- it starts vpxlog.cpp's OWN native GL render thread (rt_main),
which is redundant with (and contends against) our reconstructed renderer
riding the FIFOSOCK tee. Live-caught: with it set, wire delivery stalled at
command 8 for 90+s (window rendered, but slowly/poorly per live observation);
removing it, the same mission reached command 21,488 (10 frames) in the same
window. My two earlier successful ad-hoc bring-up tests had already removed
this env var by hand -- the script just hadn't caught up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:53:58 -05:00