Clicking a per-display glass Engineering panel button (0x21 on an Eng page)
hard-crashed via a wild call through an uninitialised pointer (eip=cdcdcdcd
debug / 0x01048748 release), zero btl4 frames above Receiver::Receive.
Root cause is the dense message-handler-table gap hazard (reconstruction-
gotchas #11), the message-side twin of the attribute-table one -- NOT a /FORCE
unresolved external (the glass link log carried only the known-benign mech3.obj
CreateStreamedSubsystem set). Receiver::MessageHandlerSet::Find(id) returns
messageHandlers[id-1].entryHandler for any id <= entryCount with no populated-
check, and Receive() calls it when != NullHandler. The streamed Eng-page .CTL
dispatches subsystem msg id 3/0xb to whatever subsystem is shown; a weapon
(Emitter) registers only PoweredSubsystem 4-8 + MechWeapon 9-10, so id 3 is a
gap below entryCount 10. Build did new HandlerEntry[entryCount] and left gaps
uninitialised -> (this->*garbage)(msg). The 1995 binary has the identical non-
zeroing new[] (part_002.c Build) and survived only on fresh-heap-zero luck: a
weapon receiving id 3 was always meant to IGNORE it (id 3 = a Condenser/
Reservoir action in a different subsystem branch; the uniform Eng-page button
template makes buttons for unimplemented actions authentically inert). The new
per-display windows just made the id reachable live.
Fix (engine, class-wide, faithful): Build now null-inits every slot
(entryID=0/entryName=""/entryHandler=NullHandler) before copying inherited /
placing supplied, so a gap is deterministically NullHandler -> Receive drops it
(the authentic "Receiver ignores unhandled messages"), with an empty never-NULL
name so the name-based Find() strcmp can't deref a gap. Correct dispatch
contract, not a glass-path guard -> protects the whole dead-button class (#14).
Also lands the [glasswin] CLICK crash-forensics log (names the button before
dispatch).
Verified under cdb: click-soaked every MFD bank (Engineering/L+R Weapons/Heat/
Comm, ~130 clicks through Quad<->Eng page cycles) -> zero AVs, reconstructed
mapper preset selects still fire ([mode] preset (1,1)/(2,1)...); pod build +
solo mission un-regressed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One window per pod display (Heat/Engineering/Comm/Weapons x2/radar +
Flight Controls), each carrying its surface with its RIO button bank
arranged pod-faithfully; CPU-expanded surfaces, buttons under the
imagery. Runtime gate BT_GLASS_PANELS (=0 = the legacy single panel).
Merged on top of today's landed fix pile (audio-crash, experience,
parallax); the L4PADRIO overlap with the gait-detent commit resolved
preserving BOTH (his window refactor + our at-rest band snap).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
# context/glass-cockpit.md
PARALLAX (#16): the pick/fire ray was anchored at mech.y+5.0 (a port
improvisation) while the sight line ran from the eyepoint (y=6.23) --
two parallel rays whose constant offset grew into the reported low-miss
as range closed (measured ry +0.072 @50u -> +1.54 @2.7u). The decomp's
sight and pick share the eye origin (HudSimulation @4b7830 chain). Fix:
the viewpoint mech's cockpit eye owns the aim-camera publish in BOTH
views, origin = its own eye translation; leveling + deliberate elevation
untouched; chase view now converges to cockpit ballistics (V cannot
change where shots land). After: pick pinned to the crosshair (ry <=
5e-6) from 50u to point-blank; 26 laser + 8 missile center-mass hits at
3/4-screen. Awaiting the reporters' approach-test.
VERDICT INSTRUMENTATION (#4 closed authentic, #5 verdict posted):
BT_RANGE_LOG per-frame pick tracing + BTGroundRayHitExact analytic
cross-check (0/8000 arena fall-throughs; cavern 6/8400 single-frame
grazes -- the 'crazy sliding' is the authentic world-pick + 500 m/s
slide over depth discontinuities); BT_AUD_TAIL StopNote/fade timing +
BT_FIRE_PULSE single-shot driver (the energy 'buzz' is the AUTHORED
charging loop: crescendo through recharge, 1.309s authored release,
measured within one frame). CAVERN.EGG: solo cavern test egg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BTPlayer's master ctor now seeds the flag block from
btMission->ExperienceLevel() + AdvancedDamageOn() (accessors from the
SURVIVED 1995 BTMSSN.HPP [T0]) instead of the NULL-stubbed scenario
role -- the egg experience knob works for the first time. Both switch
slot errors fixed vs the binary @4c0bc8; phantom btMission duplicate
member removed (+0x1f8 IS Player::playerMission).
SEVEN gate stubs unified onto the real flags (two found empirically):
HeatModelActive, OwnerAdvancedDamage, LiveFireEnabled,
ControlsAllowLights, powersub @4b0efc, emitter's file-local FUN_004ad7d4
+ FUN_004ac9c8 stubs. FUN_004ac9c8 family swept (incl. a live raw-offset
databinding trap in MechSubsystem::IsDamaged). Flag block renamed to its
true semantics (simLive/heatModelOn/...), scoring-era names kept as
comments. Includes the #5 [aud-tail] diag in emitter.cpp.
ALL 19 shipped eggs say experience=expert -> default behavior UNCHANGED
(per-tier verified: novice/standard = no heat/jams, authentic; veteran/
expert = today's exact behavior). Real delta: egg advancedDamage=1 now
reaches player+0x264/0x268 (was hardwired 0). Follow-up flagged: the
glass FE defaults empty experience to veteran (one tier below shipped).
Awaiting human verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The weekend's crash family root-caused under cdb: SourceSet.sources[5]
receives the AllExplosion death preset's 25 streamed voices --
RequestAudioChannels wrote 20 OpenAL source ids past the array, smashing
the neighbouring heap object (Release: a vtable overwritten with a source
id -> delayed silent AV at AudioControlEvent::Send; Debug: CRT heap-
corruption abort in the death Explosion's audio teardown). Explains all
three #12 crash flavors (solo enemy kill -- stack-confirmed; MP self-
death; MP peer PEER_DOWN cascade -- same generic teardown).
Fix: sources[] sized to AUDIO_SOURCESET_CAPACITY=25, static_assert
lockstep with MAX_PRESET_SAMPLES, + defence-in-depth clamps at the ctor
and acquisition sites. Soak: 26+ deaths across glass AND pod builds
under cdb, zero faults, full wreck lifecycle every time. Gotchas S21:
the fixed-array-vs-streamed-count overflow class + sweep note.
Awaiting human verification: the MP death-and-survive session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Break the desktop glass cockpit's secondary displays out of the single
combined pad panel + D3D gauge strip into ONE window per pod display, each
carrying that display's surface with its RIO button bank, arranged
pod-faithfully around the main view. New TU engine/MUNGA_L4/L4GLASSWIN.*
(BT_GLASS-only), selected by the -platform glass preset via the new runtime
gate BT_GLASS_PANELS (=0 falls back to the legacy single panel + dock).
- Surfaces are CPU-expanded from the shared gauge buffer
(SVGA16::ExpandPlaneToBGRA, no D3D) and StretchDIBits'd in -- the D3D
dev-composite path (dock / window / overlay) stands down while active.
- Buttons sit UNDER the imagery: big hidden click targets that reach into
the display, with only a small protruding edge showing as a lamp light.
Red MFD banks tile the top/bottom; radar rails run the sides + a bottom
row; a Flight Controls window hosts the no-display banks.
- Windows: Heat / Engineering / Comm / Left Weapons / Right Weapons / radar
+ Flight Controls; edge-to-edge (no in-client margin/title; OS caption
labels each).
- Fix blank surfaces: application/ghWnd are duplicate-defined and bind
non-deterministically under /FORCE, so the fresh L4GLASSWIN TU read NULL.
Reach the renderer/window via BTResolveGaugeRenderer/BTResolveMainWindow
defined in btl4main.cpp off the real btl4App/hWnd pointers. New gotcha:
reconstruction-gotchas.md S6 duplicate-GLOBAL corollary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Human re-test on the fresh build: zero-stop OK, but the lever could PARK inside
the walk/run hysteresis band (walkStrideLength@0x534 .. reverseSpeedMax@0x538)
where the gait SM has no stable state -- walk<->run hunt, EngineShiftFwd/Rev
retriggering ("mixed walk/run mode + repeated acceleration sample"). The
pod-build keyboard lever already snaps out of the band to the NEARER edge at
key-rest (mech4.cpp GAIT DETENT); the PadRIO slew channel lacked it.
Faithful port, not an invention: mech4.cpp publishes the band in lever units
every driven frame (gBTGaitDetentLo/Hi seam, same guards + the same +0.002
engage margin); L4PADRIO Poll() applies the identical nearer-edge snap to the
Throttle channel when the slew rests (no slew key held, no pad slew axis
deflected). Sweeping through the band while held stays continuous (the
authentic moving lever, one authentic shift).
Verified (DEV.EGG glass, graduated parks each ~0.10 of the sweep,
BT_GAIT_TRACE+BT_MPPR_TRACE): band [0.358026,0.50388) = demand 22.02-30.87;
in-band parks snap both directions (0.4137->lo, 0.465126->hi); no at-rest
sample in the band; exactly ONE walk->run clip (state 10) up and ONE run->walk
(state 14) down across the whole sweep; zero-stop unaffected; pod build
rebuilt + BT_AUTODRIVE smoke clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Zero-mission-arg launch of the build-steam exe -> lands in the glass FE
menu, where Steam sessions actually form (host or join an ISteamMatchmaking
lobby; there is no address to dial, so this is a LAUNCHER not a joiner --
unlike join.bat/join_lan.bat). Sets BT_PLATFORM=glass + BT_STEAM_NET=1,
logs to content\steam.log. Guarded like the sibling bats, with the two
failure modes spelled out (zip without build-steam\; Steam client not
running = menu works, no lobbies).
Requires distributing the build-steam variant (BT_GLASS+BT_STEAM) alongside
content\ -- a different zip shape; the pod-build zips are unaffected. For
dev testers to exercise the Steam path further (prior tests were buggy);
NOT for general players yet: the live 2-machine exit criterion
(docs/STEAM_TEST.md) is still open and the AppID is the 480 test id.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Human-verified (epilectrik, live): the panel is steady on the SAME monitor
as the game. Two root causes, both cured:
1. D3D-vs-GDI presentation contention -- the game's ~60Hz presents fought
the panel's GDI redraws on a shared display (single-monitor users had
no escape; moving the panel to another monitor cleaned it, the tell).
WS_EX_LAYERED + opaque SetLayeredWindowAttributes: DWM now composites
the panel independently of the D3D swap chain.
2. Sampling beat -- the 100ms repaint timer sampled the authentic RIO lamp
flash half-periods (125/250/500ms) into an irregular 'broken lamp'
strobe. Timer -> 62ms (~half the fastest half-period): flashing lamps
now blink at their even, authored tempo.
(Stacked on the earlier double-buffer/erase-suppression cleanup.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The panel repainted control-by-control straight onto the window DC on a
100ms timer. Compose to a memory bitmap + single BitBlt; erase suppressed.
NOT the whole fix: the reporter's flicker is monitor-dependent (clean on a
display without the game window) = D3D-vs-GDI presentation contention, plus
a 100ms-timer-vs-125ms-lamp-flash sampling beat -- directions filed on #13.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mission-control menu was missing player experience + many options. Added
the complete set recovered from the Mac 4.10 operator console's BT:: adventure
tree (TeslaSuite/410console/.../Console.ini) -- the tag= values are the egg
fields; cross-checked vs the port spec + reference/cavern.egg:
NEW groups: SCENARIO (Free For All / No Return respawn role), EXPERIENCE
(novice/standard/veteran/expert), BADGE (7 team emblems), PATCH (8 badge
colours), DROP ZONE (one..five), ADV. DAMAGE (on/off).
CORRECTED: TIME (evening, not 'dusk'); COLOR (full 7: +Brown/Green/Grey/Tan,
Red->Crimson, Gray->Grey); MECH (full 18 with console friendly names ->
tags, was 8).
btl4fe.hpp: BTFePilot gains experience/badge/patch/dropzone/advancedDamage;
BTFeMission gains roleKey/roleModel. The egg writer emits the selected values
(were hardcoded expert/VGL/Yellow/one/advancedDamage=1); No-Return role model
= the game's 8.3 resource 'noretun'. 5-column layout (mission | mech | pilot |
emblem/patch | session+controls); loadout persists across the relaunch
(fe_last.ini, all 14 groups).
Verified: menu drives (click Expert+Davion -> egg experience=expert
badge=Davion, persisted), and the generated egg parses + runs a full mission
(no egg errors, drive 61.501).
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>
- hud.hpp/hud.cpp/CLASSMAP.md: the binary HUD AttributePointers table starts at
@5110b8 with a FULL id-3 FlickerRate record (the old transcription read it as a
label and shifted every offset one slot). Re-based member names/offsets:
rotationOfTorsoHorizontal@0x1DC (<- Torso+0x1D8 twist), limits@0x1E0/0x1E4,
speed@0x1E8, rangeToTarget@0x1EC, @0x1F0 unbound, Visible@0x1F4 (init 1),
Lock@0x1F8 (init 0), HotBoxVector P3D@0x1FC, ThreatVector P3D@0x208,
CompassHeading Scalar@0x214 (= yaw euler[0] + twist, HudSimulation :5676).
Cross-checked vs ctor @004b7f94 + HudSimulation @004b7830 (Lock rule writes
@0x1F8 :5622/:5633; range default 1200 + 500 m/s slide @0x1EC). SetCompassHeading
renamed SetThreatVector (@004b7810 writes @0x208 = the threat vector; no port
caller). No behavioral change: the reticle feeds via port globals and the CFG
never binds HUD attrs. Resolves the gauges-hud open question (Gitea #10 entry b).
- L4VIDEO.cpp: BT_SHOT capture moved AFTER BTDrawGaugeInset -- the 2026-07-18
reorder had left it before the dock blit, so BT_DEV_GAUGES_DOCK screenshots
silently omitted the gauge panel.
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>
Follow-through on the pitch fix: the view pitched but shots still flew
level (user report). Cause: the task-#48 AIM BORESIGHT leveling in the
eye's aim-camera publish (L4VIDRND) strips ALL pitch from the pick-ray
direction -- correct for the TERRAIN body pitch it was built against, but
it also erased the pilot's deliberate R/F elevation.
Fix: mech4's per-frame eye compose publishes the torso elevation
(gBTEyeElev); the publisher re-applies it onto the LEVELED forward
(fwd = (cos e * level, sin e)) before building the basis -- terrain pitch
stays stripped, the deliberate aim survives into BTGetAimRay. Sign
matches the pixel-calibrated view compose (+ = up).
Harness evidence: pinned-down elevation visibly pitches the chase eye
into the terrain (the ray basis moves); a full headless aimed-kill could
not be driven (the autofire gate needs a designated target and the
random spawns wedged short of the truck row) -- aim-at-truck kill needs
the human pass. Awaiting human verification (issue #8).
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>
The opaque BLACK rectangle under a destroyed truck was basev:bvx9_mtl (the
MECHMD wreck's 4-vtx scorch base): the material authors NO diffuse, ambient
(0,0,0), texture bexp9_tex (MAP 'bexp' slice 8, BEXP.BSL) + ramp cdusty.
collectMaterials' AMBIENT fallback took the all-zero ambient as a REAL
colour (hasDiffuse=true, black) and tinted the whole quad black regardless
of its texture. An all-zero ambient means UNSET, not 'tint black' (a black
tint renders geometry invisible -- never authored intent): the fallback now
requires r+g+b > 0.001. Pixel-verified (BT_SHOT ram run): the scorch base
draws as the authored charred burn patch under the wreck debris.
Also: cross-library TEXTURE registry (globalTexMaps, BT_TEX_XLIB=0
disables) mirroring the existing globalRamps -- material libs reference
textures DEFINED in other libs (bexp9_tex is defined in BTARENA/BTFX, not
BASEV.BMF); a per-file texMaps miss left such batches untextured. Same
first-wins sweep of the indexed BMFs on first miss.
Regression scope: only materials authoring a zero ambient AND no diffuse
change (they now ramp/texture instead of black); cross-lib refs that
previously resolved keep their same-file definitions (checked first).
Awaiting human verification in a live session (issue #3 stays open).
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>
The playtest falsified the first revival: the truck vanished with no wreck.
Cause: the rubble (and any additional cultural object) was built as a
DPLStaticChildRenderable / DCSInstanceRenderable added at TOP LEVEL of
mRenderables -- those classes read the bare matrix stack (identity at top
level), so the rubble drew at the WORLD ORIGIN, not at the icon (and before
the consolidation exclusion it was consolidated there permanently).
Fix: every cultural video object (intact AND rubble) is a RootRenderable
(Static) -- placed from myEntity->localToWorld, registered into the frame
pass lists by the same per-frame Execute path the intact model draws
through. The revived state switch then genuinely swaps them.
PIXEL-VERIFIED via BT_SHOT screenshot timeline (full-speed BT_GOTO ram,
ARENA1 MECHMOVR truck): intact truck visible -> ram/death -> the MECHMD
wreck debris renders scattered at the truck's exact spot, intact model
gone, mech walks the footprint. ARENA1 census: 57 rubble-typed objects
across the 124 icons (MECHMD/bpip1d/ab07d_FR...).
NOT yet closed (open on #3): (a) the wreck's scorch base quad draws as an
opaque BLACK rectangle (13 plain ops on MECHMD -- material/ramp resolve,
not a blend flag); (b) the death explosion (psfx 1008) still not visibly
confirmed; (c) the authored burning-fire state visual unreconstructed.
Diag added: [cultobj] census (BT_CULT_LOG) -- per-object file/type/op flags.
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>
Solo practice already started in-cockpit (BT_START_INSIDE) but didn't
open the MFD gauges window -- inconsistent with the join bats. Add
BT_DEV_GAUGES=1 so single-player practice mirrors the pod experience.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The crash (any typed key in a glass session; found via the backtick report):
the merge-reconciliation stand-down declared extern BTRIODevicePresent at
BLOCK scope inside the extern-C BTInputSuppressKey -- the declaration
inherited C linkage, _BTRIODevicePresent went UNRESOLVED, and /FORCE bound
the call to garbage (cdb: wild call into Sensor::DefaultData from
LBE4ControlsManager::Execute's suppression check). The tolerated-LNK2019
batch hid the new unresolved external -- the exact CLAUDE.md /FORCE trap.
Fix: the extern moved to file scope (C++ linkage); link verified clean of
_BTRIODevicePresent.
Backtick feature (per Cyd: backtick = 1st/3rd person): PadRIO edge-detects
VK_OEM_3 in its poll (focus-guarded, message-path-free) and the mech4 view
block consumes it beside the V action (which stands down with the binding
engine in glass mode). Verified live: two real presses -> [view] COCKPIT
eyepoint -> [view] external chase, game alive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field report: a remote player had smooth gameplay but in 3RD-PERSON with
NO gauges. Cause: view mode + gauges are CLIENT launch flags, not
something the relay/console can enforce -- and join.bat set neither, so
remote players booted in the game's desktop default (external camera, no
MFDs). The operator's own local launch sets both (the app's 'Start in
cockpit' + 'Dev gauges' checkboxes, on by default), which is why the
operator saw the correct view but remote players didn't.
The pod is a first-person cockpit with MFD gauges and no '3rd person' at
all, so cockpit is the right default for players. join.bat/join_lan.bat
now set BT_START_INSIDE=1 + BT_DEV_GAUGES=1. (Players can still press V
to toggle the external camera.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field report: after a mission timed out normally, relaunching a mission
did nothing. The relay was single-mission -- once it fired a launch
(launches_sent==2) it never re-armed.
Fix (operator-side only; the BINARY needs nothing -- a rejoined pod is a
brand-new process at WaitingForLaunch):
- btconsole _check_launch_gate: when a previous mission finished
(launches_sent==2) and ALL seats have RE-ACKED (every pod exited,
re-ran join.bat, reconnected), reset the launch state and re-print
'WAITING FOR OPERATOR LAUNCH'. Only reachable on a pod ACK (never
mid-mission). Launch is CONSUMED after RunMission #2 (launch_at=None,
launch_requested=False) so the next press is a deliberate new round.
- btoperator SessionMonitor: reset on each new
'WAITING FOR OPERATOR' so the LAUNCH button RE-ENABLES for mission 2
(it stayed disabled forever before -- the other half of the bug).
Verified e2e: two full missions on ONE relay session -- mission 1 runs +
times out on the clock, pods rejoin, relay re-arms + re-announces ready,
operator LAUNCH -> mission 2's RunMission #1/#2 fire (total fires 2) ->
pods running. BACK-TO-BACK PASS. Friends' zip unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-reported: glass left/right inverted. Closed with live sign algebra:
the joystick-group wire push was traced end to end (new env-gated
BT_CTRLMAP_LOG push trace + record mode-mask in the install log) --
measured wire stickX=-1 -> turnDemand=-1, the SAME demand the
user-verified dev D-key-RIGHT produces (stickX=+1 -> turnDemand=-1 via the
bridge's negate-once, cb82d8c). The mapper interprets the authentic RIO
WIRE convention: stick right = NEGATIVE JoystickX (the vRIO/RIOJoy
calibration convention). PadRIO now publishes X negated (all sources --
keyboard deflect, pad LX, panel -- uniformly); Y stays screen-sign;
L4PADFLIP still flips both. Also: PadRIO honors BT_KEY_NOFOCUS=1 (the
btinput harness override) for automation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field report: a remote player ran join.bat from the wrong place (inside
the zip, or the .bat separated from content\/build\) and got 'The system
cannot find the path specified' + dumps -- the relative 'cd %~dp0content'
failed cryptically.
Now each bat: cd to its own dir, verify build\Release\btl4.exe +
content\<egg> exist, else jump to :badpath and print a clear message
('this file must sit in the extracted folder next to content and build;
right-click the zip - Extract All and run it from there'). Verified: run
from a dir with no content/build -> the guidance prints, no dump.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Steam-host report ('no glass panel, plasma yes'): a direct btl4.exe start ran
the MENU under the DEV profile, whose putenv(L4CONTROLS=KEYBOARD) leaked into
the relaunched mission child -- the child's glass preset (sets-when-unset)
could then never select PAD: no PadRIO, no panel; the plasma appeared only
because DEV never touches L4PLASMA. Menu-mode is now detected FIRST and the
front-end process skips the profile putenvs entirely (profiles belong to game
processes); boot line reads 'MENU (front end -- no profile applied)'.
Verified: zero-arg start -> menu -> launch -> child boots GLASS, PadRIO up,
72-control panel present, plasma present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>