The live-reported glass regressions (arrows dead / lever+detent missing /
V+J/K/L+N inert / weapons into the ground) were build-glass/Release/btl4.exe
built at 07:47 running the pre-5dd3536 image: no suppression tables, no V/J/K/L
keys, unpinned RIO ids. content\bindings.txt already carried the merged map
(arrows = W/S lever + A/D pedals) and parsed clean. Clean rebuild closed every
symptom; SendInput re-verification: lever holds on release + stops at zero,
LEFT/RIGHT turn in MID (wire-sign correct), V toggles with BT_SHOT pair, level
boresight at spawn, R/F elevation drive; pod build rebuilt + smoked un-regressed.
Code delta: BT_MPPR_TRACE [mppr-c] line enriched with mode/pedals/stick/turn/elev
so one env-gated line proves the whole control surface.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pre-launch host drop/rejoin re-stream re-creates every mech at mission
launch; the LOBBY-built gauge tree kept attribute pointers into the freed
first-stream mechs, every widget Execute AV'd, and the BT_DEV_GAUGES SEH
guard (Gauge::GuardedExecute, GAUGE.cpp:618) disabled each one PERMANENTLY
(rate=0, no rebind) -- the frozen dev-gauges window of incident #12. The
authentic engine flow only tears the tree down at mission transitions
(Application::Shutdown -> gaugeRenderer->Shutdown() -> ShutdownImplementation
-> Remove(0), APP.cpp:787 / GAUGREND.cpp:3264); the re-stream bypassed it.
Fix: BTL4GaugeRenderer::TearDownForViewpointRestream() performs the
ENTITY-BOUND half of that same ShutdownImplementation sequence, in order
(gaugeAlarmManager->RemoveAllAlarms() -> Remove(0) -> moving/static entity
grid Clear), keeping the mission-scoped state (warehouse, graphics ports,
interpreter, controls-owned L4Lamps -- RemoveAllLamps would delete them
behind the buttonGroup's &lamp->automaticValue registrations). Called from
BTL4Application::MakeViewpointEntity before ConfigureForModel("Init") on any
viewpoint RE-make -- that handler runs once per stream (the incident log's
[ctrlmap] installing x2) -- so the tree rebuilds bound to the NEW mech via
the renderer's existing lazy build. Sentinel: "[gauge] viewpoint re-stream:
tearing the gauge tree down for rebuild".
Verified with the 2-instance relay harness (port 15600, MP_RELAY.EGG,
the incident sequence): join A (dev gauges) + B, kill B by exact PID at
WAITING FOR OPERATOR LAUNCH, rejoin B, operator launch. Pod A re-streamed
(2x ctrlmap install, 4x zonebuild), logged the sentinel, and post-launch the
docked composite is fully live (mission clock/heading/radar advancing between
BT_SHOT frames) with ZERO [gauge-fault] DISABLED lines (the incident had 11).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the RivetGun-modeled ProjectileWeaponSimulation body with the fully
recovered binary machine (capstone disasm, every branch address-cited):
- gate 1 @4bbd36: simulationState@0x40==1 (destroyed) || heatAlarm==FailureHeat
|| owner disabled -> recoil=rechargeRate + alarm 7 (was: simulationFlags==1)
- gate 2 @4bbd71: bin alarm 2/3 or bin destroyed -> alarm 7 re-pinned per frame
- Loaded @4bbec2: DENIED shots (viewFireEnable off / no owner target) blip
SetLevel(4);SetLevel(2) and STAY LOADED, no ammo pull (the old FireWeapon
early-returns under an unconditionally-cycling caller faked a full firing
pip cycle -- the "missiles cycle but never launch" incident mechanic);
the ammo pull is bin->FeedAmmo @4bbee6 in the CALLER; both updateModel|=1
marks (@4bbf05/@4bbf4c = ForceUpdate) bracket FireWeapon; recoil set @4bbf51
- Loading @4bbdd2: recoil bleeds ONLY here at electrical Ready and CLAMPS at 0
(it ran to -114 before); bin Loaded -> weapon Loaded
- 7 @4bbe4d: binary-faithful roach motel (re-asserted unconditionally;
recovery = ResetToInitialState only)
- slot 17 @004b9c9c reconstructed as MechWeapon::ComputeOutputVoltage
(rechargeLevel=(rechargeRate-recoil)/rechargeRate, Emitter overrides with
@4ba738) and called from Loading/7 -- the launcher recharge dial ANIMATES
(the old "authentically static" claim was wrong)
FireWeapon bodies stripped to heat+spawn only per @004bcc60 (no view gate, no
ConsumeRound, no recoil); ConsumeRound retired (not a binary method).
Gotcha #20 sweep (all sites disasm-verified): simulationFlags|=0x1 == engine
DelayWatchersFlag (audio watchers muted forever) removed everywhere --
projweap/mislanch fire marks, ProjectileWeapon::ResetToInitialState (@4bbb47
= updateModel|=1); Emitter::SetDirty retired and split per binary site into
ForceUpdate() (@4bafaa/@4ba55d) vs ExecuteOnUpdate() (@4ba99a/@4ba943);
Emitter fault gate GetFlags()==1 -> simulationState==1 (@4baab9).
AmmoBin::GetAmmoState() accessor added (named-member read of ammoAlarm@0x1A8).
BT_LOOK_TEST=<frame> scripted verify added (mech4.cpp): holds the rear-view
button 300f on/300f off to drive viewFireEnable headlessly.
Verified solo ARENA1 (bhk1, autofire): 54-60 [projectile] PUSH + impact smoke;
look-back denial window = trigger pulses, state stays 2, zero launches,
resumes on release; enemy (disabled) mech launchers pinned 7 + full recoil;
STREAK-6 recharge dial sweeps across BT_SHOT frames; lasers fire throughout;
zero gauge faults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #10 audit's one WRONG: the port Execute was a bring-up no-op, so the
emitter/myomer engineering pages never erased their top data box -> stale
sibling-page ghosts on the shared Eng bit-plane (SYSTEM 10 PPC showing the
Streak ammo box, etc). Full faithful widget landed:
- btl4gau2: ctor/BecameActive/Execute/clear/ticks/cursor recovered from the
capstone disasm of @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/
@004c6c6c (Ghidra dropped every x87 arg). Plot: v=0..12000 step 1200,
x=Round(response(v)*230), y=Round(v*(1/12000)*187) (ld80 @004c6bd0/@004c6d74
= exactly 1/12000); change-test samples the response at 12000V vs the 9999
activation sentinel; XOR op for tick/cursor move-by-redraw; destroyed branch
centres edestryd.pcc and revives via own vtbl+0xC (BecameActive, slot 3 of
PTR_0051a1fc -- vtable-dump verified).
- The vtbl+0x3C sampler identified from the binary vtables: Emitter slot 15
@004bb42c = sqrt(P(v)/2.0e8), P @004bb3f4 = damageFraction*v^2*0.5*
energyCoefficient; Myomers slot 15 @004b8f94 = sqrt(AvailableOutput(v)*3.6/
350). FUN_004dd138 == sqrt (part_015.c:4026): myomers' fabs reading
corrected, GetSpeedReading renamed SeekVoltageResponse. Port dispatch via
complete-type bridges BTSeekVoltageSample/BTMyomersSeekSample +
BTSubsystemDestroyed (databinding rule).
- Emitter's AUTHENTIC attribute table recovered (binary @0x511dd4, ids
0x1D-0x25) and published: Laser*/Seek*/OutputVoltage@0x414 (currentLevel,
RAW volts -- the live-cursor feed). Field renames per the table:
0x3F8 minSeekVoltageIndex / 0x3FC maxSeekVoltageIndex (static_assert-locked).
The MechWeapon 0x1D OutputVoltage PORT ALIAS retired (binary table ends
0x1C; Find walks lowest-id-first, the alias shadowed the authentic row).
PPC::DefaultData -> Emitter::GetAttributeIndex().
- mech4: BT_PRESET_HOLD=<n> (freeze the #9 preset cycler after n pulses) for
steady-state pixel verification; BT_SEEK_LOG diag.
Verified live (BLH, autofire): both #10 repro pairs held ghost-free for
minutes (SYS09->SYS10 PPC, SYS02->SYS04 ERMed, SYS05->SYS06 Myomers); curves
draw with live cursors; one replot per activation; quad panels/J-K-L/sec
panel un-regressed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/GAUGE_COMPOSITE.md gains the AUDIT 2026-07-19 section: every widget on the
6 pod MFD surfaces + the secondary screen + the HUD reticle audited for
binary-correct data (static pass vs decomp + two instrumented live sessions).
Verdicts: 34 CORRECT / 1 WRONG (filed) / 6 AUTH-STATIC / 6 DEFERRED-FEED.
Findings:
A. The 'SYSTEM 10 PPC ammo readout' (entry a) root-caused: header/label/screen
mapping are CORRECT ([vss] dump == authored sub+0x1dc); the ammo box is a
GHOST -- stale pixels from the sibling ballistic/sensor page on the shared
Eng plane, persisting because the port SeekVoltageGraph::Execute is a
bring-up no-op. In the binary the graph IS the top-box eraser (view =
(0x97,0x80)-(0x17d,0x13b) @004c6798; clear @004c6be4 on the @004c6920
activation sentinel) -- ghost-free on the pod. Full reconstruction FILED
(4 Seek* attrs + vtbl+0x3C sampler + x87 recovery); no erase-only stand-in.
B. HUD attr table @5110c0 conflict (entry b) RESOLVED -- see commit 904c75a.
C. Entry c: no [T3] tags remain in gauges-hud.md; the single [T4]
(reticlePosition writer) stays flagged.
D. BT_SHOT dock-capture regression found + fixed (commit 904c75a).
Live re-verifications this audit: heat digits climb (515->680 under autofire),
reservoir flush 6->0 (~1s held) + PFX cloud, ammo counter moves (24->19 after
missile autofire), heading 089->120 + speed 0->182 under BT_GOTO, secondary
dama->crit->heat cycle (masks 0x450421/0x490421/0x510421), preset cycles visit
exactly the populated page set (MFD1:1,2,4 MFD2:1-4 MFD3:1,2), mission clock,
Comm 0/0, pips/compass/tape/caret render.
Audit performed by the agent; findings await human review, fixes await human
verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
table re-decoded to the ModeMFD bits + desktop J/K/L page cycle
The preset system was unwired by ONE defect in the message layer: the
SetPresetMode @004d1b24 table @0051dbf0 had been transcribed from the
section dump as BIG-endian dwords ({0x1e,0x01000000} instead of
{0x1e,0x01}), so a preset press set a garbage high bit -- and for group 1
items 3-4 / group 2 items 0-2 stomped the LIVE NonMapping / Intercom /
ModeSecondary* bits -- while the real page bits never moved. Ground
truth (section_dump.txt:72901-72908, little-endian + BTL4MODE.HPP [T0]):
set = ModeMFD{1,2,3}{Quad,Eng1-4} = 1<<(group*5+item), bits 0-14, fully
disjoint from the #6 secondary trio (bits 18-20); group 2 is MFD3, NOT a
duplicate of the secondary views.
What the 15 presets show (l4gauge.cfg): group = the MFD (Mfd1 lower left
/ Mfd2 upper center / Mfd3 lower right), item 0 = the btquad.pcx Quad
overview (up to 4 vehicleSubSystems cluster panels), items 1-4 = the
full-screen engineering-detail pages (bteng.pcx + prepEngr screens
group*4+1..4 + the cluster eng children: GENERATOR SELECT A-D, POWER
graph, COOLING loop, DAMAGE, ammo). Empty screens are authored per mech
(Blackhawk: 3/11/12 empty).
Authentic dispatch (streamed "L4" .CTL EventMappings, BT_CTRLMAP_LOG
dump): each MFD owns the 8-button RIO bank around it, MODE-MASK-gated --
Mfd1 = 0x08-0x0F, Mfd2 = 0x20-0x27, Mfd3 = 0x00-0x07. Quad page ->
direct-select buttons for the POPULATED eng pages (mapper msgs
Aux1Eng1-4 0x4-0x7 / Aux2* 0x9-0xC / Aux3* 0xE-0x11); eng page -> one
back-to-Quad button (0x3/0x8/0xD) + per-subsystem controls. These
records already install and fire on desktop (btinput passes the live
manager mask), so the NUMPAD profile's 0x20-0x27 keys page MFD2
authentically.
Port wiring (the #6 pattern): keys J/K/L -> actions Mfd1/2/3Cycle ->
gBTPresetCycle -> L4MechControlsMapper::CyclePresetModeNow(group) -- a
documented desktop shim (24 mode-dependent pod buttons don't fit a
keyboard) that cycles Quad -> populated Eng pages -> Quad, visiting
exactly the pod-reachable set; the body is the authentic SetPresetMode.
Dev-composite: BTDrawGaugeSurfaces now draws the Eng1-3 planes at their
sibling cells and skips any mono plane whose channel is BlankColor,
honoring the mode-driven reconfigure (RemapGraphicsPort) -- each dev
cell shows the ACTIVE page like the pod monitor. This supersedes and
removes the 2026-07-12 GAUGREND "frozen-dial" scaffold (forced all 15
page bits active under BT_DEV_GAUGES; it pinned the shared Eng plane on
the highest screen and ate the page flips).
Pixel-verified (BT_PRESET_TEST + BT_DEV_GAUGES_DOCK + BT_SHOT,
Blackhawk): all three MFDs page Quad -> SYSTEM NN eng details -> back to
Quad in lockstep with the [mode] preset mask log; group 0 skips the
authored-empty screen 3, group 2 skips 11/12. Un-regressed: N display
cycle (0x450421->0x490421->0x510421, page bits intact), M control mode,
CONTROLS.MAP 52 bindings parse clean.
Diags: BT_MODE_LOG ([mode] preset), BT_PRESET_TEST=<frame>.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NotifyOfDisplayModeChange override (vtbl+0x4C @4d1ae4) + wire desktop 'N'
The secondary screen's schematic selector was mislabeled: @004d1ae4 (the
bits-18..20 ModeSecondary* mask swap) was reconstructed as a non-virtual
"SetControlMode" that nothing called, so the desktop stayed pinned on the
Damage view. The binary's L4 vtable @0051e440 pins the truth:
+0x48 = @004d1acc <- CycleControlModeMessageHandler (FUN_004afbe0):
forwards to the base RET no-op @004b048c. A BAS/MID/ADV
control-mode change never touches the secondary view
(empirically confirmed: BT_MODECYCLE_TEST cycles the CONTROL
MODE lamp, mask bits 18-20 unchanged, schematic stays ARMOR).
+0x4C = @004d1ae4 <- CycleDisplayModeMessageHandler (FUN_004afcac):
THE Damage/Critical/Heat selector, indexed by displayMode
(table @0051dbe4 = ModeSecondaryDamage/Critical/Heat).
Authentic pod inputs (streamed type-6 .CTL EventMappings, dumped via the
new BT_CTRLMAP_LOG EVENT records): secondary-panel button 0x15 -> msg
0x15 CycleDisplayMode (manual p13, the "'Mech status Info center" bottom
left of the secondary screen), button 0x18 -> msg 0x14 CycleControlMode
(manual p6, top right), 0x10/0x11 -> ZoomIn/Out. The DOS keyboard
fallbacks (Keypress 0x13d/0x13e = extended F3/F4) are dead under the
WinTesla VK map, hence the desktop pin.
Port wiring (the M/ModeCycle pattern): key N / pad RightThumb -> action
DisplayCycle -> gBTDisplayCycle -> CycleDisplayModeNow() -- the same body
the pod console button message drives. Both .MAP profiles + the
compiled-in default updated.
Verified live (docked gauges + BT_SHOT, BT_VIEWCYCLE_TEST): the sec
panel cycles ARMOR DAMAGE silhouette -> CRITICAL DAMAGE subsystem list
-> HEAT DAMAGE colored list, mask 0x450421 -> 0x490421 -> 0x510421; M
control-mode cycling un-regressed (BAS/MID/ADV lamp cycles, view pinned).
Diags: BT_MODE_LOG, BT_VIEWCYCLE_TEST=<frame>, BT_MODECYCLE_TEST=<frame>,
BT_CTRLMAP_LOG now dumps EVENT records. KB: gauges-hud secondary-view
section rewritten, CLASSMAP +0x48/+0x4C slots, decomp-reference env
gates, GAUGE_COMPOSITE phase-4 entry resolved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The playtest report (sound plays, no gauge drop, no cloud) traced to the
Reservoir's InjectCoolant chain being dead in three places:
- The handler was never REGISTERED: the binary's Reservoir handler table
@0x50e680 has one entry {4, "InjectCoolant", @4aee70}; added
Reservoir::GetMessageHandlers + the press/release handler (press starts
the flush gated on coolantLevel@0x12C > 0 -- the old body misread +0x12C
as currentTemperature; release stops it; novice lockout via FUN_004ac9c8).
- Reservoir::InjectCoolant (@4aefa4, 1019 bytes) was an empty stub -- the
drain the coolant gauge reads. Reconstructed in full: work list =
condenser/weapon/heatable chains (+0x7cc/+0x7bc/+0x7ac, roster-walk
emulation with the binary's duplicate-visit weighting; HeatSink-filtered
[T2 guarded]) + the linked master sink; per sink with flowScale != 0 move
squirtMass x flowScale x dt (clamped to the tank / sink capacity) and
credit pendingHeat with the negative carried-heat delta capped at
sinkMass x reservoir startingTemperature -- the set%-biased flush of the
manual (p24), riding the existing heat model.
- Two latent ctor decode bugs surfaced and fixed: the master gate @4af408
(and @4aeb40 HeatWatcher, swept) reads the OWNER MECH's simulationFlags
(*(param_2+0x28)), not the resource's subsystemFlags (the misread left
the CoolantSimulation Performance unregistered); and the capacity scale
FILDs the bank's INTEGER HeatSinkCount ((float10)*(int*)(link+0x1d0)) --
the float reinterpret gave ~1e-44 -> a permanently empty tank.
Capacity = 0.05 x heatSinkCount x streamed CoolantCapacity (BLH: 6.0).
Visual: the binary's mode-1 coolant-effect renderable (FUN_00456a68, built
for classID 0xBC0 on "ReservoirState", part_014.c:5439; tick @part_007.c:
8780) starts psfx 19 = FLUSH.PFX ("Coolant flush", BTDPL.INI) when the
state changes to 1 -- BTSpawnFlushCloud (mech4.cpp) spawns it on the same
alarm edge as an attached emitter at torso height.
Input: new CONTROLS.MAP action "Flush" on 'H' (HELD; press+release both
dispatch, the held-button payload). Diags: BT_FLUSH_LOG, BT_FLUSH_TEST.
Verified live (FOGDAY): [flush] Reservoir level 6 -> 0.13 -> 0 over ~0.6 s
held (= the manual's 3-4 punches to empty), the coolant vertBar source
Reservoir/CoolantMass drains, and the bluish condensation cloud rises from
the mech (scratchpad/flush_cloud.png vs flush_before.png).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User + tester reports: pitch does not work. Root cause: the Torso sim
integrated the R/F / stick-Y axis into currentElevation (authored limits
and rates all correct) but NOTHING consumed it -- eyepointRotation's only
writer was the look-state switch, so neither the cockpit view nor the aim
ray (camera basis) ever pitched. Shots always flew level; low targets
(trucks) were unhittable.
Fix (faithful: the pod's stick-Y pitches the EYE; the torso geometry does
not tilt): per-frame compose in the HUD tick --
eyepointRotation = EulerAngles(lookPitch + torsoElevation, lookYaw, 0);
BTCommitLookState now stores its look component in gBTLookPitch/Yaw; new
complete-type bridge BTGetTorsoElevation (torso.cpp, mirrors
BTGetTorsoTwist). DPLEyeRenderable consumes it every frame, so the view
and the boresight aim pitch together (crosshair stays screen-centred).
Sign PIXEL-CALIBRATED via new diag BT_FORCE_ELEV=<-1..1> (pins the axis
headless): screenshots confirm axis +1 (key R) = aim UP, -1 (F) = down.
X still recenters. Awaiting human verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(b) The 'tiny explosion' on the ram kill is AUTHORED: crunch res 31 = 'stephit'
(one video object, effect 1008 = ddam5 damage smoke) -- a step/ram squash is
small by design. The FULL explosion res 32 = 'trkdead' = psfx 15 dtrkboom
(fiery omni burst) + psfx 16 dtrkburn (the burning-wreck fire), reached by
WEAPON kills -- which the 1995 binary dispatches [T1]:
- MechWeapon::SendDamageMessage @004b9728 (part_013.c:6765) gates only
"target NOT derived from Mech@0x50bdb4 OR aimed zone set": a non-Mech
boresight target takes the zone=-1 damage UNCONDITIONALLY;
- Missile contact @004be078 dispatches at the struck solid's OWNER entity
with no class test at all.
Port wiring: Mech::WorldStructurePick returns the struck solid's owning entity
(BoxedSolid::GetOwningSimulation; TERRAIN.cpp:107/246 build every static solid
with its Terrain/CulturalIcon/Door entity as owner); the mech4 pick block
designates IT instead of the gBTTerrainEntity sentinel (sentinel = fallback);
the projectile contact path grew the non-mech damage-zoned else-branch
(@004be078 mirror, direct Dispatch). Plain terrain ignores the damage
(ENTITY.cpp:885 zone==-1 guard); an icon's handler maps -1 -> 0 and dies.
Truck armor is WeaponDamagePoints=1 (TRK.DMG): one laser = one dead truck.
(c) The burning fire is dtrkburn.pfx, authored INTO the death package (every
icon family carries 1016: trkdead/bigdead/meddead/msldead/twrdead). There is
NO looping BurningState fire in the binary: the damage-zone effect watcher
ctor @0042a984 has exactly one call site (the Mech ctor, part_012.c:10405), so
icon ExplosionTables are inert, and CulturalIcon has no Performance.
Documented as authentic -- nothing invented.
Verified live (BT_SHOT pixel capture): 26 laser kills, [cult] TakeDamage
type=3 -> DYING res=32 -> DPLIndependantEffect 1015+1016 at the icon origin;
frames show the orange dtrkboom fireball, dtrkburn flames on the fresh wreck,
and the rubble aftermath; ram harness re-run still fires crunch res 31.
Diags: BT_FIRE_AT_ICON (designate nearest ahead icon; live-icon census in
CULTURAL.cpp), BT_FX_TEST="1015,1016".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MECHMD's scorch base (basev:bvx9_mtl, bexp9_tex = BEXP.BSL RGBA4444
slice 8, ramp cdusty) drew as a hard-edged dark square for two stacked
reasons:
1. The blanket 'truecolor BSL slice (channel >= 6) never ramps' gate
blocked its cdusty ramp. Corpus scan: only 4 shipped textures use
truecolor slices; bexp9/bdet9 are grayscale in RGB (100% / 98.8%
r==g==b) and their materials author ramps -- only bdam8 (damage
sheet) is truly coloured. bgfload rampableSlice() now probes the
decoded slice: a truecolor slice ramps iff effectively gray (>=95%),
keeping bdam8's colour protected.
2. The RGBA4444 authored alpha channel -- a binary 0/240 cutout mask
(the splat silhouette, 78.5% transparent) -- was never alpha-tested,
so the quad's transparent surround rendered as an opaque rectangle.
New BgfDrawBatch.texAlpha (channel >= 8) routes these batches
through the PUNCH alpha-test draw states, WITHOUT the black-texel
keying (which would hole the near-black charred centre). Side
benefit: tree9 (tree/leaf cards) + bdet9 (trans-rail lattice) get
their authored cutouts too.
Pixel-verified (ram-kill run, BT_SHOT): irregular char splat, dark
brown (71,44,34) lifting to near-terrain tan (115,100,91 vs terrain
131,119,108), no rectangle; pre-death frames unregressed (vehicles
stay lit/diffuse -- hasNormals gate untouched). Awaiting human
verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Human-verified: wreck swap works live; small ram explosion visible.
Remaining detail recorded for the next session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Human playtest (2026-07-19): ramming a truck makes it disappear entirely --
no rubble, no smoke/fire, no visible explosion. The [cultvis] log evidence
verified the STATE MACHINE (pointer swaps), not pixels: the rubble child
component (DCSInstanceRenderable/DPLStaticChildRenderable) is itself
2007-stubbed and never draws, and the routed psfx 1008 produces nothing
visible. rendering.md section corrected from 'verified live' to PARTIAL
with the three remaining gaps (rubble draw, visible explosion, burning
fire). Gitea #3 reinstated as open/wip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playtest report: trucks play a collision sound on impact but never change
state -- no explosion swap, no destroyed model, collision volume persists.
Investigation (BT_CULT_LOG census + BT_GOTO ram harness): the SIM was never
broken. ARENA1 map-streams 124 CulturalIcon entities, each with a damage
zone, explosion resource and removeOnDeath; the mech crunch dispatch
(ProcessCollision -> BTDispatchCollisionDamage) reaches them; ~19 walking
bumps burn the zone -> CulturalIcon::TakeDamageMessageHandler spawns the
Explosion (psfx effect 1008 observed), posts the delayed BurningState and
deletes the collision boxes. All of that ran correctly and invisibly.
The ACTUAL bug: the 2007 WinTesla port fully stubbed
StateInstanceSwitchRenderable ('STUBBED: DPL RB 1/14/07') -- ctor never
registered on the state dial, Execute never toggled anything -- so the
intact->rubble visual swap on BurningState NEVER fired, for every cultural
icon in the game. The destroyed truck kept its intact model: exactly the
reported 'no state change'.
Revival (D3D9-native, 1995 semantics verbatim):
- StateInstanceSwitchRenderable now controls the draw COMPONENT via the
SetDrawObj in-place drawable swap (the mech RemakeEntity mechanism):
visible = captured d3d_OBJECT, hidden = NULL. Initial state in the ctor,
AddVideoWatcher on the SimulationState dial (SetState fires video
watchers), toggle only on a real change.
- L4VIDEO cultural case passes the draw component + object (the dead
dpl_INSTANCE param is gone); [video] Object hides at BurningState,
Rubble shows.
- REQUIRED: cultural objects stay OUT of static-mesh consolidation
(RecurseStaticObject, same exclusion class as banded LODs/shadows) --
a merged static draws forever regardless of DrawObj.
- Permanent env diag BT_CULT_LOG: creation census (zones/explosion/flags/
pos), TakeDamage trace, death transition, [cultvis] switch actions.
Verified live (BT_GOTO=-620,-328 ram, ARENA1): 19 crunches -> DYING ->
explosion 1008 at the icon -> [cultvis] HIDE x2 (intact) + SHOW (rubble)
-> contacts cease, mech walks into the former footprint. Boot + render
un-regressed (statics consolidate as before minus the 124 icons).
KB: context/rendering.md new section (the full chain + the consolidation
gotcha). MP replicant path shares the same SetState watcher (cross-pod
verification pending -- noted in the issue).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- manifest410.py: monotone-DP TU labeling (data-cluster affinity + CLASSMAP
seeds + recon @addr soft votes) replaces naive fill; BT region = 917 fn /
~230 KB, extents match every independently-known TU boundary.
- source-completeness: ROUTE A CLOSED (Nick holds no further assets);
toolchain = BC++ 4.52 PROVEN by CW32.LIB byte-match, archived at
TeslaRel410/BORLAND; OPT.MAK = the shipped binary's exact recipe; extender
corrected to Borland PowerPack DPMI32 (was "Phar Lap TNT"); "all BT headers
survive" corrected (17/36 bt-side; mech.hpp etc. reconstruction-only);
engine gaps enumerated (vdata.hpp first, back-dated from BT412).
- phases/phase-03: rounds 1-3 of the source410 campaign - 6/10 surviving
originals compile clean under BC4.52; console wire IDs recovered from the
binary ctors (Killed=9 Damaged=10 ScoreUpdate=13 DWH=15 [T1], TeamScore
guessed 12 [T4] - answers the TeslaSuite console-port spec's open item);
round-3 finding: MECH.HPP is the capstone grown with the mech TU
reconstructions, BTREG.CPP green = the header-family milestone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings Cyd's experience-level / simulation-mode research onto master (the
KB-decode portion of glass-cockpit 4e01e83; the bundled BT410 source
manifest is left for the full glass-cockpit merge).
- context/experience-levels.md (NEW): the egg per-pilot 'experience' field
(novice/standard/veteran/expert) is the pod's SIMULATION-FIDELITY tier,
decoded end-to-end. FUN_004c0bc8 reads btMission->experienceLevel(+0xe4)
and fans it into the +0x25c/+0x260/+0x26c/+0x270/+0x274 flag block:
+0x25c 'sim live' (novice lockout: jams/searchlight/powersub), +0x260 the
HEAT-MODEL master switch (veteran+expert; FUN_004ad7d4), +0x274 raw level
(FUN_004ac9c8 = ==0 novice predicate; the valve/advanced-cockpit lockout).
4.0->4.10 drift: the viewscreen hunting-aid gate is GONE in 4.10 (HUD reads
none of these flags), and movement heat is veteran+expert not expert-only.
- btplayer.cpp/.hpp: CORRECTED comments/labels -- the +0x25c..+0x274 block is
seeded from btMission experienceLevel/advancedDamageOn, NOT the scenario
role's returnFromDeath; the 'roleClassIndex/showKills' names are scoring-era
mislabels. Code logic UNCHANGED (comment-only): the ctor still reads
scenarioRole (STAND-IN, defaults 2 = veteran) pending the wiring task.
- Corrections swept into gauges-hud.md (ROOKIE->novice lockout),
open-questions.md (player+0x260/0x274 semantics now PINNED),
pod-hardware.md, subsystems.md, decomp-reference.md; CLAUDE.md router row.
No code-logic change -> no rebuild needed (comment + markdown only). The
runtime wiring (seed from BTMission::ExperienceLevel()) remains TODO.
Co-Authored-By: Cyd <cyd@falloutshelterarcade.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The egg's per-pilot 'experience' (novice/standard/veteran/expert) is the
pod's simulation-fidelity tier -- stored at BTMission+0xe4 and seeded into
the BTPlayer ctor's flag block @4c0bc8 [T1]. The old 'per-role display
toggles / returnFromDeath' reading was WRONG: the source is
mission->experienceLevel, and the neighboring pair copies
mission->advancedDamageOn (the egg's technician splash/collision switch),
not role floats. Known consumers: 0x260 = the HEAT-MODEL master switch
(FUN_004ad7d4), 0x25c = the novice sim-lockout (jams/searchlight/powersub),
0x274 = the raw level (FUN_004ac9c8 ==0 novice predicate).
NEW established topic context/experience-levels.md (data path, binary flag
rows per level, manual cross-refs, corrections log) + router row; sweeps in
decomp-reference/gauges-hud/open-questions/pod-hardware/subsystems.
btplayer.cpp/.hpp re-annotated with the corrected semantics; the guarded
role-based seeding stays as a marked [T3] stand-in (slot drift vs the
binary switch documented inline) until the wiring task points it at
BTMission::ExperienceLevel().
(Research by the parallel context session; committed from the glass line.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reported: the reticle's missile pips no longer momentarily disappear when
firing. Traced it: the pip's 'loaded' flag has read MechWeapon::rechargeLevel
(>= 0.999) since the reticle Execute was recovered (task #37). That's a port
APPROXIMATION of the binary's authentic attr 0x1C (WeaponState @0x350). It
works for emitters -- rechargeLevel is charge-driven and tops off at 1.0 when
Loaded -- but projectile weapons (MissileLauncher/autocannon) never write
rechargeLevel (it's authentically static at 1.0; the recharge DIAL draws full
and never moves). So a missile pip's 'loaded' was permanently true and the pip
never blinked. NOT an audio regression -- it never worked in the port; the
audio wave (ea85554) is actually what made the projectile fire-cycle state
(weaponAlarm Loaded/Loading/Firing) correct, i.e. the very signal the pip
should have been reading.
Fix (faithful to the binary): the pip now reads attr 0x1C = the weaponAlarm
StateIndicator level (new MechWeapon::WeaponStatePtr -> GaugeAlarm54::LevelPtr)
and compares it '== stateConst2' -- the loaded constant (2) the binary itself
already stores in AddWeapon (param_10). The state cycles
Loaded(2)<->Firing(0)/Loading(3)/Jammed(5) for BOTH weapon families, so every
pip momentarily drops on fire. The authentically-static rechargeLevel dial is
untouched.
Verified live (Blackhawk solo, BT_AUTOFIRE=1 BT_AF_MISSILE=1): both SRM6
missile pips toggle loaded 2<->0/5 on each salvo (emitter pips unchanged);
game boots + runs clean. KB: context/gauges-hud.md pip entry corrected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coverage audit of this session's ~30 commits against the docs found
gaps; fixed:
- README §Multiplayer was stale (only the old mesh flow, 'combat in
progress'): now leads with the relay + operator-console path
(internet play, join.bat/play_solo.bat, patient wait) and lists what
the mesh path has verified (4-pod, camera seat, callsigns, clock);
README controls line updated (turn/twist/pitch, pad, CONTROLS.MAP)
- decomp-reference env table: added BT_CAM_LOG, BT_SPEC_LOG (were in
code, not the hub); BT_INPUT_LOG note gains the twist/elev axis
- pod-hardware §input remap: the AXIS SIGN CONVENTION finding
(positive sim = CCW/left; bridge negates once per channel;
screenshot-forensics lesson) was only in commit cb82d8c -- now durable
- multiplayer.md: PATIENT WALK-UP + MISSION CLOCK sections (features
shipped in f9f230c/f57d25f, only incidentally referenced before)
checkctx CLEAN (20 topics, 68 glossary keys). All session features
now have topic-file coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
33 master commits in (relay TCP/UDP + PySide6 operator console, CONTROLS.MAP
+XInput binding engine, camera seats, torso pitch aim, sign fixes, the 1995
manual, version stamping, 18-mech certification). Conflicts: .gitignore +
CLAUDE.md router rows (combined).
SEMANTIC RECONCILIATION (the one real overlap): masters btinput binding
engine (ungated, CONTROLS.MAP) and the glass PadRIO (gated, bindings.txt)
would both read the keyboard/pad in a glass+PAD session. btinput now joins
the stand-down convention: BTInputPoll yields (and BTInputSuppressKey claims
NOTHING, so authentic hotkeys flow) when an operational cockpit device owns
the input path -- BTRIODevicePresent, BT_KEY_BRIDGE force-override honored,
forced harness exempt. One input system per mode: btinput on pod/dev
desktops, PadRIO on glass. The mechmppr/mech4 bridge merges composed clean
(masters negate-once sign fix inside our device-gated bridge). The D1 relay
keeps its own raw sockets by design (an alternative LAN wire; Steam and
relay are separate modes).
Verified post-merge: all 3 configs build; glass boots with [input] binding
engine standing down + PadRIO owning input (30 ticks); pod forced-walk
speedDemand=61.501 with btinput ACTIVE; 2-node loopback MP full 31/31
mission, 76/76 ticks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dumped each mech's subsystem->coolant-loop (BT_SPEC_LOG in mech4: walks
the roster once, resolves each subsystem's linked-condenser number via
BTCoolingLoopFrame) and diffed all 6 manual mechs' COOLANT LOOPS tables.
RESULT -- the structure survives the 4.0->4.10 gap remarkably well:
- BACKBONE identical on ALL 6: Generator A/B/C/D on loops 1/2/3/5,
Sensors(our Avionics) on loop 2, Myomers on loop 5, LRMs on 1&3,
autocannon on 4, big energy weapon on 6 -- exact match
- weapon LOADOUT identical on 5 of 6 (Thor/Vulture/MadCat/Owens/
Blackhawk); Loki is the one full rework (4.0 PPCx2/AFC100/SRM6 ->
4.10 AFC50x2/ER Medium x2/SRM4)
- the consistent 4.0->4.10 change is a small-laser REDISTRIBUTION: the
2 ER Small Lasers moved off the sensor/heavy loops onto the energy
loops 4&6 (Thor/MadCat/Vulture); Owens near-perfect (loops 1&3 exact)
- Loop 0 = correctly UNCOOLED infrastructure (condensers, reservoir,
gyro, torso, HUD, ammo bins, ...) -- not a coolant loop
CONCLUSION: the coolant-loop reconstruction is faithful; every diff is
4.0->4.10 balance tuning, not a bug. BT_SPEC_LOG retained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reference/manual/Tesla40_BT_manual.pdf -- the genuine Tesla 4.0 BT
player manual, 34pp. Primary-source alignment audit (pod-hardware.md
SManual): the reconstruction matches on EVERY checked control behavior:
- control modes are named BAS/MID/ADV in print (mechanics identical to
our Basic/Standard/Veteran incl. the ADV no-auto-slow turn clamp)
- stick right = torso right in MID/ADV (today's sign fix); Blackhawk
and Owens named as THE fixed-torso exceptions; twist arc ~120
(per-mech: Loki/Thor tables say 110 deg limit, 60 deg/s)
- continuous throttle lever; reverse = HOLD the red throttle button
(our 0x3F hold-state model)
- Hot Box MFD = callsign buttons + CLOSEST (our hotbox button 8 ->
ChooseNearestPilot); radar travel-oriented w/ 60-deg wedge sweeping
on twist; map zoom +/- beside the secondary screen
- experience gates the hunting aids (standard sim only); expert adds
movement heat
- per-mech stat sheets + coolant-loop tables = a systematic
cross-check source for our streamed subsystem resources
NEW LEADS the manual surfaces: CROUCH (button by the secondary screen;
duckState attr + SQUAT clips exist, no input drives them), EJECT
(button beside the joystick; EjectButton01 sounds exist), the hot-box
viewscreen framing (deferred PNAME marker chain).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mechs could always tilt their aim up/down -- Torso models the full
vertical axis (currentElevation, rate, VerticalLimitTop/Bottom,
recenter) and EVERY 1995 control mode routes stickPosition.y into
Torso::SetAnalogElevationAxis -- but the desktop bridge hard-zeroed
stick Y, so the axis was dead on a keyboard rig. The one pod control
the remap left unwired.
- btinput: JoystickY axis -> elevTarget/elevActive/elevAbsolute
- mech4 shim: sElev integrator (same walk/spring model as the twist;
X recenters pitch too via gBTElevRecenter)
- mechmppr bridge: feeds stickPosition.y every bridged frame (the old
unconditional zero removed); both mode branches covered
- CONTROLS.MAP (+ numpad profile + compiled default): R/F = aim
up/down, pad LeftStickY = elevation (was unused)
- torso.hpp: CurrentElevation()/ElevationVelocity() accessors (diag)
- [mppr] trace gains stickY (note: the trace reads AFTER the next
frame's device push re-zeroes the stick -- input flows regardless)
Verified live: R held -> torso elevation climbs at the authored rate
and clamps at 0.349066 rad = exactly 20.0 deg (the Blackhawk's
VerticalLimitTop); release holds the aim. The eyepoint correctly
stays level -- pitch aims the GUNS and reads on the HUD's vertical
elevation tape, as in the pod.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stubbed dpl-instance ranking display is reborn as screen-space
quads in CameraShipHUDRenderable::Render:
- followed-player callsign banner (bottom center; the old direct
index was correct -- Execute already converts to the 0-based
texture slot)
- the RANKING WINDOW: one row per scoring player, [ordinal][callsign]
in rank order right of center; visibility = the Director's
authentic flash logic (10s on / 15s off, solid final 30s); rows
follow LIVE playerRank pointers so they re-sort as scores change
- discovery: each 128x32 ordinal bitmap packs TWO ordinals side by
side ('1st|2nd', '3rd|4th' -- why 4 bitmaps serve 8 players);
draw = texture rank/2 with a u-half selected by rank parity
- alpha-blended A4R4G4B4 white-on-transparent textures x green
diffuse = the authentic green look
BT_SHOT moved AFTER the 2D pass -- it captured the backbuffer pre-HUD,
so overlays were on screen but invisible to screenshots (cost one
false debugging round). GetOrdinalTexture accessor added.
Screenshot-verified: '1st MAVERICK' standings + MAVERICK banner over
live auto-directed coverage; 2-node mech smoke PASS (un-regressed;
the new render path only executes when a CameraDirector HUD exists).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The spectator/broadcast seat works end-to-end: a hostType=1 +
vehicle=camera pilot page boots a CameraShip whose 62-camera arena1
network loads from the BTL4.RES type-27 resource (the engine's
CreateStreamedCameraInstances existed all along -- the file probe falls
back to it), the BTCameraDirector locks onto the first live mech, and
the ship TRACKS it (sees=1, goal advancing) and CUTS between authored
camera positions (screenshot-verified: wide arena shot -> close
tracking shot).
The missing piece was the Mech override of PlayerLinkMessageHandler
(@0049f624), which the reconstruction never filled. The engine base
resolves mech->player; BT's override adds:
1. the REVERSE link player->playerVehicle = this on EVERY node
(replicants included) -- how the camera director and scoreboard
find a REMOTE player's mech; without it a spectator parks forever
('NO goal entity')
2. clears NonScoringPlayerFlag (0x4000 = bit 14): a pilot with a
vehicle is a SCORING player -- this admits REPLICATED players to
the ranking pass, so cross-node rank/score displays work
3. master only: seeds the heat bank's ambientTemperature (bank
@0x1d4) from the mission [mission] temperature= (BTMission+0xf4)
-- correcting the heat family's 'frozen 300' deviation note (it
was never frozen; THIS is the writer)
Bridge BTSetBankAmbientTemperature lives in heatfamily_reslice.cpp
(mech.cpp cannot include subsystem headers -- local-stub collision).
BT_CAM_LOG diagnostics: camera-network count, director pick + Players
group census, ship follow state, link dispatch/receive.
Verified live: 2-seat (mech + camera) relay session -- replicated
player shows +veh, director goal locked w/ 30s timer, ship tracking a
moving mech through camera cuts; standard 2-node mech smoke PASS
(un-regressed). Lesson re-learned: a 'clean' build filtered on 'error
C' missed a linker file-lock failure -- one whole test cycle ran
against a stale exe (the /FORCE gotcha's cousin; grep -i error, not
error C).
Remaining (task open): ranking-window overlay draw (L4VIDRND stubs),
operator-app camera-seat row, shot polish.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'cameras\ directory missing, ask Nick' entry (a03b16f, same day)
was WRONG: the file path is only the DEV OVERRIDE (the RP-style loose
.CAM files -- present for RP in the nick-games 410SRC archive, which
prompted the re-check). The shipped binary falls back to a BTL4.RES
resource TYPE 27 (0x1b) named after the map (FUN_0042be3c probe ->
FUN_0042c078 res loader; per-cam binary records, parsers @0042ad54/
@0042b9f8) -- and our res has ALL 8 maps' networks (arena1 = 62
cameras; cavern/dbase/grass ~57KB each). First arena1 record parses
clean (pos+quat, type 2).
The spectator/broadcast seat needs no external data: remaining work is
the type-27 loader in the WinTesla CAMMGR (file flavor only today),
the stubbed ranking-window draw, and a camera-vehicle boot test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CameraShip spectator system is engine-complete (CameraDirector
auto-follows the score leader, CAMSHIP cuts between fixed map camera
positions like TV coverage, ranking window flashes 10s/15s + final
30s), but the per-map cameras\<mapname> notation files are absent from
the archive -- added to the get-from-Nick list with the reconstruction
alternatives (author our own / chase-cam fallback) and the two code
gaps (stubbed ranking-window draw, camera vehicle never boot-tested).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User: 'isn't the game supposed to time out tho?' Yes -- and it was
HALF-implemented: the egg's [mission] length drives the pods' countdown
and the final-30s ranking window, and the full StopMission -> fade ->
end chain exists, but nothing ever fired it: in 1995 the CONSOLE sent
StopMission at expiry. Our console never did, so missions ran forever.
- btconsole relay: arms the clock when RunMission #2 fires; sends
Application::StopMissionMessage (clientID 4, msgID 6 per APP.h:383,
exitCode NullExitCodeID) at length + 2s grace; stdin 'stop' command
ends the mission early (works in auto and manual modes).
- btoperator: END MISSION button (enabled once launched; resets per
session).
Verified live: 40s test mission -- relay logged the armed clock, sent
StopMission on time, the pod ran the authentic end chain and exited
cleanly ('[boot] RunMissions returned'). Standard eggs carry
length=600, so real sessions are now authentic 10-minute pod missions
with the score display in the last 30 seconds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The egg generator's known-good list was just bhk1+madcat (the only two
ever exercised as egg vehicles); the operator roster therefore leaned on
those. Swept every ModelList candidate as a solo egg vehicle: 18/18
boot a mission, spawn, and animate with no crash -- avatar, blkhawk,
loki, owens, sunder, thor, vulture and all short variants (ava1, lok1/2,
mad1/2, own1, snd1, thr1, vul1) join bhk1/madcat as first-class picks.
eggmodel now lists the canonical 8 first for the operator dropdown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 pods, relay-assigned seats, 4-pilot MP4.EGG (eggmodel-generated incl.
callsign bitmaps): 4 distinct seats, full ladder, every node replicates
all 3 foreign mechs (host-qualified [repl] sets are exact complements),
4 distinct spawn positions (no dropzone stacking), UDP at 4-pod fan-out
(relay tx=3x rx, zero drops), all pods alive through the session.
Per-host [net-rx] census symmetric: 36 NewDynamicEntity makes (mech +
subsystem roster) from every host to every node.
DIAGNOSTIC TRAP that cost a false bug-hunt: EntityID::operator int()
returns localID ONLY, so the [repl] log's (long)GetEntityID() collapsed
different hosts' mechs onto one number and read as 'missing
replicants'. The log now streams the EntityID object (host:local via
its operator<<).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.10 = the 1995 arcade release; 4.11 = the win32 reconstruction; the
build number is the git commit count -- monotonic, zero-maintenance,
and every exe pins to exact source via the short hash ('+' = built
from an uncommitted tree). tools/btversion.cmake regenerates
build/btversion.h on every build (write-if-changed, no rebuild churn);
the stamp shows in the boot banner (btl4.log head, replacing the stale
'v4.10' line) and the window title (incl. the MP node tag).
Verified: build stamps 4.11.311 (980c9cd+), banner + title correct on
a live boot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pod's controls reach the game through the RIO serial board; on a
desktop that hardware is a keyboard shim of hardcoded GetAsyncKeyState
reads. New binding engine (game/reconstructed/btinput.cpp) maps PC keys
and an XInput pad onto the authentic channels through a user-editable
file (content/CONTROLS.MAP, community-suggested grammar, corrected):
key/pad -> button <addr> real buttonGroup emissions, mirroring the
RIO convention exactly (press a+1 w/ mode
mask saved, release -a-1 w/ saved mask;
L4CTRL.cpp:2470-2520) -- aux/preset banks,
hotbox, panic, reverse thrust all reachable
key/pad -> axis Throttle rate (lever), pedals/JoystickX
deflect (turn/twist) into the existing
virtual-controls integrators (feel, gait
detent, spring-centering all unchanged)
keypad pilot|external <n> keyboardGroup key values ('0'-'F')
pckey <char> any authentic 1995 typed hotkey
action <name> port dev controls (view, all-stop, ...)
Keys claimed by a binding are SUPPRESSED from the legacy WM_CHAR/KEYUP
feed -- ends the historic double-dispatch ('w' drove AND selected pilot
0; F5's key-up value 0x74 aliased to the 't' hotkey; letter key-ups fed
the developer fake-event dispatcher). Unbound keys keep their authentic
meaning. The dual-use 'V' is split: V = view toggle, B = look behind.
Default profile = WASD classic (compiled-in twin; delete the file to
restore). CONTROLS_NUMPAD.MAP ships the corrected community layout
(keypads are NOT buttons 0x50-0x6F -- that space doesn't exist; no
clickable cockpit exists so everything needs a binding; keyboard fire
buttons added; 0x36/0x37 are hotbox not 'config'; missiles moved off
Ctrl). XInput loads dynamically (1_4 -> 9_1_0), disconnected-pad
probing rate-limited; pad sticks write the axes as absolute positions
(the spring is physical), triggers/buttons per the file.
Verified live: bindings load (43), W and NumPad8 drive (speedDemand 0 ->
61.5), X all-stop (spd -> 0), aux-bank emission (D1 -> [input] 0x2f
PRESS/release under the numpad profile), suppression both directions
(posted 'r' reaches [keych], posted 'w' swallowed), full-keyspace
WM_CHAR+WM_KEYUP fuzz x3 survived with sim advancing, XInput graceful
with no pad, 2-node relay session un-regressed (full ladder + UDP).
Pad-in-hand testing still needs a physical controller.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 1995 console let the operator type player callsigns; the pods render
them from 1bpp BITMAPS streamed in the egg ([largebitmap]/[smallbitmap]
lists + hex raster pages, bitmapindex per pilot; large 128x32 on the
score/ranking display via DPLRenderer name textures, small 64x16 on the
kill-feed and radar/target labels). The textual name= pilot key is dead
in the shipped binary -- its pilot parse reads bitmapindex only (decomp
part_014). Historical bug surfaced by this work: every egg in the repo
listed only ONE name bitmap (Aeolus) while pilot 2 pointed at index 2,
so player 2+'s callsign has never rendered (null-checked, silently
blank).
eggmodel.py: EggDoc.set_callsigns rewrites the lists + one raster page
per callsign per size + per-pilot name=/bitmapindex= (duplicate names
get disambiguated page tags); get_callsign_rows decodes pages back
(exact inverse of the engine's nibble-stream parse, GRAPH2D.cpp:340,
MSB-first bit order :558); rasterize_callsign/make_callsigns render
text via Qt (auto-shrink to fit, NoAntialias, 1bpp threshold) -- the
only PySide6-dependent corner of the module. validate() now catches
dangling bitmapindexes and missing raster pages.
btoperator.py: Callsign column in the roster (free text, PLAYERn
default); every save rasterizes the column into the egg exactly like
the original console did.
MP_BHMC.EGG template repaired: authentic Aeolus raster preserved
bit-exact, Boreas page generated and listed.
Verified: eggmodel self-test 20/20 (bit-exact raster round-trip, dup
disambiguation, dangling-index catch); ASCII-art decode of the
app-generated VIPER/MONGOOSE rasters reads correctly; live 2-node
relay session on the generated egg -- full ladder, launch, zero
name-bitmap load warnings; operator GUI e2e PASS after the column
shift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pod launched with no BT_SELF asks the relay for a seat before joining:
new control frames SEAT_REQUEST (-6) -> relay reserves the lowest roster
seat not claimed or reserved (60s reservation so simultaneous joiners
can't race onto one seat) -> SEAT_ASSIGN (-7, int32 hostID + NUL tag)
becomes relaySelf; everything downstream (egg self-match, HELLO) runs
exactly as if BT_SELF had been set. Roster full -> SEAT_FULL (-8) ->
clean Fail(). A real HELLO pops the reservation; a pod drop frees the
seat. Explicit BT_SELF still claims a specific seat (the operator's
local launches use it).
Client: L4NetworkManager::RelayRequestSeat (throwaway TCP dial to the
relay game port, 10s reply window). Relay: SEAT_REQUEST branch in
_handle_game_frame. Operator exporter collapsed from per-seat
join_as_playerN.bat to ONE universal join.bat (+ join_lan.bat with
BT_RELAY=auto) -- every player gets the same file, first come first
served, the arcade walk-up-to-a-pod model. players/ regenerated.
Verified: 8/8 seat stub tests (distinct seats in roster order, FULL on
exhaustion, claimed+reserved stays FULL, HELLO claim accepted, duplicate
HELLO refused); 2-node localhost e2e with NO BT_SELF on either pod ->
both seated, full ladder, RunMission pair, UDP flowing post-launch;
regression smoke: explicit-BT_SELF relay session clean with zero seat
requests, classic mesh (no BT_RELAY) un-regressed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LAN players now find the game with zero configuration -- the 90s arcade
model (cabinets locate the operator station) restored:
- Client (L4NET RelayDiscover): BT_RELAY=auto broadcasts 'BTR1DISC' on
udp/15999 (255.255.255.255 AND 127.0.0.1 -- broadcast doesn't reliably
loop back on Windows; covers same-box sessions), 5 x 1s attempts; the
answering relay's SOURCE IP + its advertised console port become the
relay address. Explicit <host>:<port> path unchanged; mesh untouched.
- Relay (btconsole.py): best-effort discovery responder on udp/15999
answers 'BTR1HERE' + <u16 consolePort>; degrades gracefully (logged) if
the port is taken.
- Operator app: Export player scripts now writes a join_as_playerN_lan.bat
pair (BT_RELAY=auto) next to each internet script -- LAN guests
double-click and are found; internet guests use the public-host script.
Verified 2-node: both pods BT_RELAY=auto -> probe answered through a real
interface (not just loopback) -> discovered 172.19.x.x:1500 -> full session
to RunningMission. KB: multiplayer.md D1 section updated (+ the operator
console entry).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ~60Hz entity update records now ride UDP in relay mode, so a lost/late
datagram is dropped (dead reckoning absorbs the gap) instead of head-of-line-
blocking the reliable TCP stream -- the fix for internet rubber-banding.
Reliable traffic (make/damage/death/control/egg/launch) stays on TCP.
This RESTORES the authentic 1995 NETNUB split, it doesn't invent one:
Receiver::Message defaults messageFlags=ReliableFlag; the update path clears
it (flags=0, ENTITY.cpp:590); Mode(UnreliableMode) fires at LoadingMission
(APP.cpp:704 -- the 2007 port's Mode() was a no-op, now it STORES the mode).
Routing gate = (mode==UnreliableMode && !(flags & ReliableFlag)) -- flag AND
mode, so the map-stream creation messages (also flags=0 but flowing during the
still-Reliable CreatingMission window) ride TCP for free.
Client (L4NET):
- Mode() stores currentNetworkMode (was ignored).
- ConnectRelayUdp: UDP socket connect()ed to the relay game port; HELLO
(outbound punch-through, relay learns our endpoint); udpUp on HELLO-ACK.
BT_RELAY_TCP_ONLY=1 disables the channel (UDP-blocked nets degrade to the
pure-TCP checkpoint automatically -- no ACK => everything stays on TCP).
- RelayUdpSendFrame: {route, fromHost, seq} envelope + frame, best-effort.
- RelayUdpKeepalive: ~15s NAT-hold + ~1s HELLO retry until acked.
- CheckRelayUdp: HELLO-ACK sets udpUp; per-sender seq gate drops stale/
out-of-order datagrams; frames handed up envelope-stripped like TCP.
- Send / ExclusiveBroadcast: route unreliable-window traffic to UDP, else TCP.
- CheckBuffers polls the UDP channel first, then TCP; keepalive tick.
(The relay-side UDP forwarder + endpoint learning + --udp-drop hook landed in
phase 1.)
Verified 2-node localhost relay: during a drive the update stream flows on UDP
(relay udp tx 2->190+, TCP frozen at 53) and the peer's replicant tracks the
master to sub-2u; 15% forced drop (--udp-drop 15) stays coherent (tracked to
0.1u); TCP-only fallback confirmed (udp-known drops the TCP-only pod); no
crashes; mesh mode un-regressed. KB: context/multiplayer.md gains the D1
RELAY MODE section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/GLASS_COCKPIT.md gains the step-4 ledger (seam/transport/lobby + THE
FAKEIP LESSON); NEW context/steam-networking.md topic (+ router row) and
docs/STEAM_TEST.md (the 2-machine live procedure = the step-4 exit criterion
on user hardware). context/glass-cockpit.md: steps 3+4 status. Final purity
re-check: pod build clean, forced-walk mission runs, ZERO glass/steam tags in
the pod log.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/GLASS_COCKPIT.md gains the 2b-2e detail + the full verification matrix
(purity build + seam audit, pod forced-walk, 2-node loopback MP via btconsole
relay on the POD build -- both nodes reach the running mission, PadRIO
authentic-path input, panel-click survival). context/glass-cockpit.md status
updated: step 2 DONE [T2]; steps 3-4 pending.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The streamed L4 mapping resource carries the binary's positional attribute ids
(stick=3, throttle=4) but MechControlsMapper chained from Subsystem::NextAttributeID
== 2 -- every streamed record resolved ONE MEMBER LATE (attr 4 -> pedalsPosition,
verified via the new permanent BT_CTRLMAP_LOG diagnostic in CreateStreamedMappings).
A latent real-pod bug: the serial RIO throttle would drive the pedals member; the
dev keyboard bridge masked it. Fix per the mechweap/mech attrPad idiom: ids pinned
to the binary numbering, id-2 gap padded, static_assert-locked. Torso + weapon
chains verified already aligned. Regression: BT_FORCE_THROTTLE headless walk clean
(speedDemand=61.501 through authentic InterpretControls, gait cycles, no faults).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reproduced by full-keyboard fuzz (every WM_CHAR + WM_KEYUP posted to the
game window, per-key liveness; delivery proven by the new BT_KEY_LOG
[keych] trace). Two distinct issues:
1) '&' == Shift+7 is the engine's dev-console STOP-MISSION keystroke --
one shifted-key slip while hunting unmapped panel keys (MAP zoom '+'
is Shift+'=') instantly ended the session, indistinguishable from a
crash. Now env-gated (APP.cpp): default ignored with a log line;
BT_KEY_STOP=1 restores the authentic stop (verified both ways live).
Same hazard class as the arrow-release '&' alias already swallowed in
L4CTRL.cpp:1516.
2) '\' (the developer fake-event key) was a REAL wild-jump crash:
Entity::Dispatch STAMPS entityID/interestZoneID into the message at
Entity::Message offsets (ENTITY.cpp:236), and the '\' case dispatched
a bare Receiver-sized ReceiverDataMessageOf<ControlsButton> at the
Mech -- the stamp wrote past the stack object and corrupted the frame
(cdb: call to eip=1 out of Receiver::Receive). The 1995 binary does
the identical overwrite and survived on stack-layout luck. Fixed with
Entity::Message-sized placement-new backing (btl4mppr.cpp); the only
such call site (grep-verified).
Verified: full fuzz (95 chars + F-keys + letter/digit keyups, 118 keys
delivered) survives end-to-end; '&' stops cleanly under BT_KEY_STOP=1.
KB: reconstruction-gotchas.md gains gotcha 19 (Entity::Dispatch message
stamping) + the '&' note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SECTOR radar's view cone read viewHorizontalRotation, wired by the
MapDisplay ctor from the mech's Torso -- but FindSubObject and
GetHorizontalRotation were NULL stubs, so the connection was never created
and the wedge sat at heading 0 regardless of twist.
Reconstructed from the binary:
- FindSubObject (FUN_0041f98c): a subsystem-ROSTER walk (count @+0x124,
array @+0x128) matching the streamed subsystem name (sub+0xd4) with a
tolower-strcmp (FUN_004d4b58) -- the 'Torso' sub-object IS the roster
Torso subsystem.
- GetHorizontalRotation: torso+0x1D8 == Torso::currentTwist (layout-locked),
via the existing task-#56 bridge BTGetTorsoTwistAddr (Radian is layout-
identical to Scalar).
Verified live (MadCat, Standard mode Q/E): the wedge tracks the twist in
lockstep (rot 0 -> -2.21 rad), and the semantic test passes -- body turned
away, torso twisted back onto the enemy: reticle green + wedge pointing at
the enemy's blip on the body-fixed scope. NB the Blackhawk's torso is FIXED
(+/-0.01 deg limits) -- its wedge authentically never moves; test with a
twisting mech. Diag env: BT_RADAR_LOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both surfaced today (dormant ~1 day) the moment the pod launch flags
(BT_DEV_GAUGES=1 BT_START_INSIDE=1, tools/mp_launch.sh) were used to drive in
multiplayer -- which made the same-day paint change look guilty. Confirmed on
the pre-paint build too; the trigger was the Jul-16 audio attribute work.
1) BT_DEV_GAUGES crash (cdb: CoolingLoopConnection::Update, ~15s in as the gauge
builds lazily). The dev-gauge cooling-loop lamp reached the cooling master by
RAW attribute index -- GetAttributePointer(3) + *(master+0x1d4). The audio
commits (cc2b109 ReportLeak etc.) inserted rows into the chained attribute
tables, so index 3 shifted onto a scalar, the resolve walked garbage, and the
background pass AV'd. Fix: route through a complete-type bridge reading NAMED
members (heat.cpp BTCoolingLoopFrame: linkedSinks.Resolve() +
Condenser::condenserNumber) -- the databinding rule; never a raw numeric
attribute index. Removed the now-orphaned GetAttributePointer-by-index
helper. Name-keyed samplers (InputVoltage) were unaffected.
2) -net dead controls mapper (mech wouldn't walk; turning/weapons still worked).
The RIO mapper is built from a stack SubsystemResource in btl4app.cpp that set
only name/classID/modelSize -- leaving subsystemFlags as stack GARBAGE, which
Subsystem::Subsystem copies into simulationFlags. A stray DontExecuteFlag
(0x2) froze the mapper (speedDemand stuck at 0). Config-dependent: clean in
SP, dirty in -net; the audio commits shifted the stack and flipped the bit.
Fix: memset each hand-built control-mapper resource to 0 (flags 0 =
AlwaysExecute, the faithful value -- a mapper must tick every frame).
Verified with the pod launch flags: SP alive 32s no crash; MP mech walks
(speedDemand 61.5 at full throttle, ~430u traveled), both nodes alive, no crash.
KB: reconstruction-gotchas.md gains gotcha 18 (uninitialized stack-resource
flags + the raw-attribute-index-into-a-growing-table variant) and a
verify-under-the-user's-launch-flags note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>