Third instance of the silent-swallow pattern (unregistered handler id ->
Receiver NullHandler). The Mech handler table @0x50BDF8 binds
{0x16, "BalanceCoolant" -> @0049f728}; the reconstruction never registered it.
Disasm-verified body (@0049f728): press-only guard (msg+0xc > 0; the binary
has NO novice lockout on this one), set EVERY condenser's valveState@0x1D0
to 1 (equal weights), then the shared redistribute @0049f788 -- which the
port ALREADY had faithfully as BTRecomputeCondenserValves (flow =
valve/total + condenserAlarm pulse; MoveValve calls it). So the fix is:
- mech.hpp/mech.cpp: BalanceCoolantMessageID=0x16 + handler + MESSAGE_ENTRY
- heatfamily_reslice.cpp: BTBalanceCondenserValves bridge (Condenser is a
complete type there; sets valves to 1 + redistributes)
- mech4.cpp: BT_BALTEST scripted harness (MoveValve press alternating with
a Balance press)
VERIFIED headless (BT_BALTEST=1 BT_VALVE_LOG=1):
boot -> all valves 1, flows 0.1667 each
MoveValve-> condenser#1 valve 5, flows 0.5 / 0.1x5 (the priority boost)
BALANCE -> all valves 1, flows 0.1667 each (equalized)
Awaiting the tester's live ADV-mode confirmation.
KB: decomp-reference.md Mech-table row marked wired.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause -- NOT issue #2 (the experience mis-seed pins everyone to VETERAN,
which PASSES the novice lockout; the gates were never the blocker). Two
unwired handlers, the same silent-swallow pattern ToggleCooling had
(Receiver::Receive -> NullHandler for an unregistered id):
- Myomers (handler table @0x51158C {9,"ToggleSeekVoltage"@004b8a48}): the
reconstruction had an EMPTY stub (the address falls in the untagged decomp
gap 0x4b8837..0x4b8a8c) and never registered the id. Body reconstructed
faithfully from the raw disassembly (tools/disas2.py 0x4b8a48): novice
lockout (@4ac9c8) -> heat-model gate (@4ad7d4) -> press-only (msg+0xc>0) ->
currentSeekVoltageIndex@0x320 = (idx+1) % (maxSeekVoltageIndex@0x32C + 1).
Modulo from zero, min index not consulted, no ResetFiringState.
- Emitter (energy-weapon table @0x511DB8 {0xb,"ToggleSeekVoltage"@004ba478}):
the faithful body already existed as the orphaned "AdvanceSeekVoltage"
(ZERO callers) -- converted to the registered message handler (same guards;
cycle + ResetFiringState when not wrapping, disasm re-verified). PPC and
GaussRifle copy-inherit the set; the ammo branch keeps its own id 0xb ==
EjectAmmo (still unwired, tracked separately).
Both registered via MESSAGE_ENTRY on top of the inherited chains. Permanent
diagnostics: [seek] gear log (BT_SEEK_LOG) + BT_SEEKTEST scripted dispatch
harness (mech4.cpp, the cooltoggle pattern).
VERIFIED headless: [seek] Myomers -> 3,0,1,2,3... and [seek] PPC_1 ->
3,0,1,2,3... (modulo-4 wrap, matching the binary's arithmetic). Awaiting
the tester's live ADV-mode confirmation for the real MFD button route (the
routing layer is the same streamed-EventMapping path ToggleCooling verified).
KB: decomp-reference.md message-table rows marked wired.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The user hit the full session menu from play_solo -- HOST/JOIN LAN modes,
NET PORT and the Steam buttons don't belong in the solo flow (LAN goes
through the join bats + operator; Steam through play_steam.bat).
BT_FE_SOLO=1 (set by play_solo.bat + the btoperator generated bat):
- the MODE cycle keeps only the two solo entries (kModes lists them first:
SOLO MISSION, RAW SOLO) via the GroupSize clamp
- NET PORT row and the HOST/JOIN STEAM buttons are not laid out
- a stale fe_last.ini mode of hostlan/joinlan is rejected by the existing
LoadPersisted GroupSize clamp, so the selection can never land on a
hidden mode
play_steam.bat keeps the full menu.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
play_solo hardcoded -egg ARENA1.EGG, bypassing the miniconsole front end
that already carries the full pickers: SOLO/RAW-SOLO/HOST-LAN/JOIN-LAN
modes, 8 maps, all 18 mechs, 4 experience levels, colours/badges/patches,
drop zones, advanced damage, time-of-day, weather and mission length.
The bat now launches zero-arg (BT_PLATFORM=glass -> the menu); the menu
relaunches the exe with the generated egg and the cockpit/gauge env
(BT_START_INSIDE/BT_DEV_GAUGES) inherits into the mission child. Menu-boot
verified with the bat's exact env; the solo menu->mission->menu cycle was
verified at FE landing. README + the btoperator generated-bat template
updated to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The steam bat missed the two client flags the join bats set -- steam-launched
missions booted 3rd-person (the same field bug fixed for join.bat earlier).
Env inherits through the menu's mission relaunch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unification flattened BT_STEAM into the always-on exe; the user flagged
the constraint that makes it the one legitimate compile gate: the Steamworks
SDK license bars a public release with Steam inside, so a release build must
exclude it entirely.
- CMakeLists: option(BT_STEAM OFF) restored around the Steamworks block --
an OFF configure touches no SDK headers, no steam_api.lib, no DLL copy.
BT_GLASS stays retired (always defined); ONE build dir either way.
Dev checkout: build/ configured once with -DBT_STEAM=ON.
- mkdist: reads BT_STEAM from build/CMakeCache.txt. Steam-OFF zips exclude
play_steam.bat + steam_api.dll (explicitly filtering a STALE DLL left by
an earlier ON build), strip the README steam section ({STEAM} line
markers), and take a -nosteam name suffix so the flavors never clobber
(found live: both stamped 4.11.436 and the release zip overwrote the dev
zip).
Verified both flavors:
ON (dev build): steam smoke degrades to Winsock without the client;
zip carries play_steam.bat + steam_api.dll + README steam lines.
OFF (scratch build-relcheck, deleted after): 0 compile errors; exe dir =
btl4.exe + OpenAL32.dll only; boots + simulates; BT_STEAM_NET=1 inert
(transport not in the binary); end-to-end zip has zero steam bits and
a clean README.
KB: context/glass-cockpit.md unified-build section records the license gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first zips (pre-mkdist) shipped d3dx9_43.dll / msvcp140.dll /
vcruntime140.dll next to the exe -- testers without the VC++ / DirectX
redistributables cannot load the exe otherwise. mkdist missed them (the
clean-extract boot test passed only because this machine has them system-
wide). Carried into redist/ (recovered from BT411_4.11.341.zip) and
packaged next to the exe.
Also: players/README.txt -- the tester instructions (updated for the
unified build: one exe, clickable cockpit surround, trigger-config regroup,
the settled keymap, play_steam.bat) -- stamped with the version and placed
at the zip root, matching the original zips' shape.
Clean-extract verified on BT411_4.11.435.zip: root has README + 4 bats,
exe dir carries all 4 DLLs, glass-profile boot simulates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The three build dirs (build\ pod, build-glass\, build-steam\) were compile-
time splits of the same game; the tax was real: a stale steam exe shipped 2
days behind, the felt keymap flipped with the boot flavor, and cockpit
clicks were silently dead in the pod build (the trigger-config incident).
Now everything compiles into the single build\ exe:
- CMakeLists: BT_GLASS/BT_STEAM options removed; the glass TUs (PadRIO,
bindings, panel, glass windows, plasma) + FE/console + Steam transport
compile unconditionally; both macros always defined (seam markers).
- Steam: steam_api.lib linked with /DELAYLOAD:steam_api.dll + delayimp --
the DLL maps only when SteamAPI is first called, and every call site
gates on BT_STEAM_NET=1 (BTSteamNet_* short-circuit on steamActive), so
machines without the DLL run everything but Steam sessions.
- CABINET GUARD (btl4main): the miniconsole menu previously claimed any
zero-arg launch -- unified, that would hijack the pod cabinet's boot
shape. The menu now requires BT_PLATFORM=glass (or BT_FE_MENU=1);
platform parse moved above the fe_menu_mode computation. A bare boot
behaves exactly like the old pod build (DEV profile, wait for egg).
- Bats (play_solo/join/join_lan/play_steam) + the btoperator.py template:
single build\ path + BT_PLATFORM=glass. mkdist: one exe (+DLL scan
picks up OpenAL32 + steam_api). build-glass\/build-steam\/build-glass2\
(a stale Jul-20 cache) deleted.
Verified matrix on the unified exe:
bare zero-arg boot == old pod exe (same DEV profile + egg-load path;
the segfault on a missing egg is pre-existing, baselined vs the zip exe)
glass + BT_BTNTEST scripted click -> [cfgmap] ENTER/EXIT (trigger config)
runs with steam_api.dll ABSENT (delay-load proven)
glass zero-arg -> miniconsole menu
2-node loopback MP: 145 ticks each, cross-connected
BT_STEAM_NET=1 without Steam client -> "staying on Winsock", game runs
KB: context/glass-cockpit.md compile-gates section rewritten for the
unified model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuilt build-steam fresh (was stale at Jul 20 -- predated today's gauge/
trigger-config/keymap fixes); the DLL scan picks up steam_api.dll
automatically. Clean-extract verified: the steam exe boots to the front-end
menu (Steam client only needed for live lobbies).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Packages the player bats (zip root), BOTH exes (build\ pod fallback +
build-glass\ preferred -- the bats auto-select glass = clickable cockpit /
trigger config / bindings.txt keymap), the runtime DLLs next to each exe
(OpenAL32.dll -- the first clean-extract boot test failed without it), and
the git-TRACKED content/ files only (the working tree carries dev junk that
must not ship). Output: dist/BT411-<date>-<hash>.zip (dist/ gitignored).
Verified: clean-extract boot test -- glass exe boots the GLASS profile and
simulates; pod exe boots as fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
join.bat + join_lan.bat + the btoperator.py generator template now prefer
build-glass\Release\btl4.exe when present and set BT_PLATFORM=glass with it
-- so MP sessions on a dev checkout get PadRIO (mouse-clickable cockpit
buttons: trigger config, MFD banks, radar rails) and the bindings.txt keymap,
identical to solo play. Player zips (which ship only build\) fall back to
the pod build exactly as before.
Verified: 2-node loopback MP smoke on the GLASS build (the documented
procedure: A -egg MP.EGG -net 1501 + B -net 1601 + tools/btconsole.py) --
both nodes cross-connected (GameMachineHost 1502/1602), ~150 sim ticks
each, no errors. btoperator.py syntax-checked; regenerated bats carry the
same guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
context/glass-cockpit.md: record the BT_PLATFORM=glass/PadRIO click gating
(the dead-clicks root cause), the verified live regroup loop, the [cfgmap]/
BT_BTNTEST diagnostics, and the one-keymap settlement (babf5b1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The user standardized on the "newer" layout (numpad aim cluster, arrows =
throttle/pedals, Alt = reverse thrust). That layout lived only in
bindings.txt (PadRIO, GLASS-profile boots); CONTROLS.MAP (btinput, DEV/pod
boots) lacked the numpad cluster entirely and had NO reverse binding -- so
the felt keymap flip-flopped with the boot flavor.
Added to CONTROLS.MAP + the byte-identical compiled-in default profile
(btinput.cpp sDefaultProfile):
Alt -> button 0x3F (throttle-head REVERSE THRUST)
NumPad8/2 -> JoystickY aim up/down
NumPad4/6 -> JoystickX torso twist
NumPad7/9 -> pedals (turn)
NumPad5 -> AllStop
Shift -> throttle up (alt)
Parse-verified: [input] loaded 61 binding(s) from CONTROLS.MAP, no warnings.
Both engines now carry the same layout, so DEV/pod boots (MP join bats) and
GLASS boots (play_solo) feel identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The user held the MFD PROGRAM button and nothing happened (weapons kept
firing). Diagnosis, verified end-to-end:
- The cockpit mouse handler DID register the click ([cockpit] CLICK addr=0x8
press) and 0x8 IS a streamed PROGRAM element (EVENT msg 0x9 -> weapon,
Mfd1 quad page mask).
- But every launch ran the DEV platform profile (no BT_PLATFORM) ->
L4CONTROLS=KEYBOARD -> no PadRIO instance -> PadRIO::SetScreenButton
no-ops (activeInstance NULL) -> the press never entered the RIO queue ->
ConfigureMappables (id 9) never dispatched -> the mode never flipped ->
fire keys kept firing. The config machinery itself was never broken.
- With BT_PLATFORM=glass (the GLASS profile: L4CONTROLS=PAD -> PadRIO), the
full chain now proves out headlessly:
[btntest] PRESS 0x8 -> [cfgmap] ENTER session on ERMLaser_1
mode 0x450421 -> 0x448421 (NonMapping 0x10000 -> Mapping 0x8000)
[btntest] RELEASE 0x8 -> [cfgmap] EXIT (mode restored)
Landed:
- mechweap.cpp: [cfgmap] BT_FIRE_LOG diagnostics in the real
ConfigureMappables/ChooseButton handlers (they were silent -- the G-key
harness had logs but the authentic button path had none).
- L4PADRIO.cpp: BT_BTNTEST="addr,pressPoll,releasePoll" scripted screen-
button harness through the REAL click seam (EmitButton -> RIO queue ->
manager drain -> buttonGroup mapping) for headless verification.
- play_solo.bat: prefer the glass build when present AND set
BT_PLATFORM=glass so PadRIO exists and cockpit clicks work.
Side finding (the user's keymap "flip-flop"): CONTROLS.MAP (btinput, DEV/pod
profile) and bindings.txt (PadRIO, GLASS profile) are two parallel binding
engines; which one runs depends on the platform profile, so the felt keymap
changes with the boot flavor. Unification pending (user decision).
Awaiting live verification of the full hold-PROGRAM + tap-fire regroup flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The weapon panel's btjoy trigger-config display (ConfigMapGauge) never rendered
because the reconstruction labelled @004c6ee0 as "SetColor(int)" and, finding no
caller, concluded the gauge was authentically dormant (task #6), gating it
behind a BT_CONFIGMAP dev env.
The user challenged this (the PROGRAM/TRIGGER CONFIG button implies the display
visualizes your config). Re-verification found the old analysis failed twice:
the "no caller" search looked for direct calls to a VIRTUAL, and its slot math
used the wrong vtable copy. The real ConfigMapGauge vtable @0051a1b8, matched
against the T0 GaugeBase virtual roster (GAUGE.h), pins @004c6ee0 at slot 9
(+0x24) == GaugeBase::LinkToEntity, and @0x94 is the linkedEntity -- the
Execute gate is "not yet linked", not "disabled". The engine broadcasts
LinkToEntity(viewpointEntity) to every gauge at viewpoint bind (APP.cpp:1277 ->
GaugeRenderer::LinkToEntity GAUGREND.cpp:3011), arming the gate -- the joystick
DOES render in the shipped game, showing per-trigger mapping state (solid =
mapped, matching the DOSBox reference).
Port fix: SetColor(int color) -> LinkToEntity(Entity*), color -> linkedEntity,
BT_CONFIGMAP dev enable deleted (the authentic path lights it). Render-verified
with no env override: the joystick + mapped-trigger lamp draw on weapon panels.
KB swept per the correction mandate: gauges-hud.md, decomp-reference.md,
open-questions.md, GAUGE_COMPOSITE.md, VEHICLE_SUBSYSTEMS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
context/gauges-hud.md: record [this+0xA0]=fillColor(green)/[this+0xA4]=
backgroundColor(black), the zone colours, and the swapped-colour bug that
rendered the remainder green. Commit 4fbc911.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the tiled-render fix: the two colour params were swapped. The
binary caller (@004c8269) pushes the colours 0xff then 0; they land at
[this+0xA0]=fillColor (0xff green) and [this+0xA4]=backgroundColor (0 black).
The binary uses the GREEN for zone 1 (tile SetColor / dots) and zone 2 (the
over-degrade fill), and BLACK only for zone 3 (the unfilled remainder
[valPix,width]).
The port had them reversed -- zone 3 used fillColor -- so the whole remainder
of the bar (most of it, since valPix is small when cold) rendered solid green
instead of black. Swapped: zone 1 + zone 2 = fillColor (green), zone 3 =
backgroundColor (black). Render-verified: black background + green dotted tick
scale + a green hatch fill growing from the left, matching the reference.
Both builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
context/gauges-hud.md: new section on the TEMP/STATUS bar's three-zone tiled
render (@004c4340) and the port bug where Execute solid-filled from warnPix and
ignored the tile image. Commit 4d4436e.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-weapon TEMP/STATUS bar (HorizTwoPartBar) rendered as a solid block
starting at the warn-threshold pixel -- it "started in the middle" and was
never striped. The port's Execute had been rewritten with DrawFilledRectangle
and never used the interned tile image, unlike the (correct) VertTwoPartBar
which tiles via DrawTiledBitmap.
Restored the binary's three-zone render (disasm @004c4340), along X:
[0, warnPix) DrawTiledBitmap(tileImage) -- the striped/dotted pattern
[warnPix, valPix) backgroundColor solid (only when value > low)
[valPix, width) fillColor solid
So the bar now fills the striped tile from the beginning and matches the
reference (hatched fill block on the left + dotted tick scale). Render-
verified vs the DOSBox reference capture. Both builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
context/gauges-hud.md: new section on BallisticWeaponCluster::DrawWarningLamp
(@004c9b50) swapping the ammo NumericDisplayInteger colours by the fire-ready
dot state (green-on-black when absent, black-cut-out when present), and the
port bug where only the non-virtual base existed. Commit 66dcdf2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The base-page missile/round count sat on the fire-ready "dot" (the cluster
image blitted green when charged) as green digits on an opaque black box --
a messy dark rectangle punched into the solid green dot.
The binary swaps the ammo count's colours with the dot state. Verified from
the decomp: BallisticWeaponCluster overrides DrawWarningLamp (@004c9b50) --
it chains the base (@004c932c, which blits the dot in on-colour 0xff /
off-colour 0) and then calls the ammo NumericDisplayInteger's SetColors:
dot ABSENT -> SetColors(0, 0xff) == green digits on black
dot PRESENT -> SetColors(0xff, 0) == BLACK digits cut out of the green dot
so the count is always legible (a stencil against the solid dot -- the same
black-on-green look as the loop/generator squares). SetColors ForceUpdate()s
the numeric so it repaints over the freshly drawn dot.
The port had only the base WeaponCluster::DrawWarningLamp (non-virtual, no
swap), so the count stayed green-on-black and clashed with the green dot.
Made DrawWarningLamp virtual and added the BallisticWeaponCluster override.
Render-verified (STREAK "0024" now black-cut-out of the solid dot); energy
weapons (no ammo count) unaffected. Both builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The eng-page generator-voltage bar (evolt.pcc, ScalarBarGauge @004c721c) and
the MyomerCluster seek-voltage graph were fed by GeneratorVoltageConnection,
which walked the same dead path as the generator-letter lamp:
ResolveLink(AttributePointerOf(subsystem,"InputVoltage")) -> read resolved
+0x1DC == Generator::outputVoltage. The BT_DEV_GAUGES audio attribute rows
shifted the chained attribute ids so AttributePointerOf no longer lands on
voltageSource@0x1D0 -> the bar always read 0.
New BTGeneratorVoltage(subsystem) bridge (powersub.cpp) reads the NAMED member
via PoweredSubsystem::ResolveVoltageSource()->Generator::MeasuredVoltage()
(outputVoltage@0x1DC), bypassing the attribute table -- same pattern as
BTPowerSourceFrame/BTCoolingLoopFrame. Both GeneratorVoltageConnection callers
(the ScalarBarGauge param + the MyomerCluster seekValue) now pass subsystem_in.
[voltfeed]-verified: Myomers -> 10000V from GeneratorD (was 0 on the dead
path). Both pod + glass builds clean. Eng-page render awaiting live playtest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User confirmed live (pod build, solo ARENA1 cockpit) that the loop-number
and generator-letter boxes render correctly on the weapon MFD panels.
Fix commit e634709.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
context/gauges-hud.md: new section recording the color-drop + shadow-field +
attribute-table-shift fixes for the btploop/btpbus lamps (commit e634709),
with the lesson that gauge value feeds must read named members through a
complete-type bridge, not AttributePointerOf+ResolveLink. Notes the eng-page
GeneratorVoltageConnection as the remaining sibling on the old path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SubsystemCluster per-weapon MFD panels render two image-strip lamps in
the TEMP/STATUS area: the cooling-loop number (btploop, 1..6) and the
generator letter (btpbus, A..D). Both drew as completely empty boxes.
Three stacked databinding bugs, all now fixed:
1. Color drop (btl4gau2.cpp): AnimatedSubsystemLamp/AnimatedSourceLamp
ctors dropped the bg/fg color params the binary passes (bg=0xff,fg=0,
same as the neighboring temp bar) and hardcoded 0,0 -> OneOfSeveral::
Execute did SetColor(0)+DrawBitMapOpaque(0), i.e. black-on-black.
Restored 0xff,0 (verified vs @004c70a4 / caller SubsystemCluster
@004c8140).
2. Shadow-field trap (btl4gau2.hpp, gotcha #2): both lamp classes
re-declared `int selected;` while already inheriting it from
OneOfSeveral (@0xAC). The CoolingLoop/PowerSource connection wrote the
derived shadow copy while OneOfSeveral::Execute read the base @0xAC
(always 0) -> every lamp stuck on frame 0 ("OFF"). Removed the
redeclarations so &selected binds the inherited member. Loop numbers
now render (verified: PPC->4, Myomers->5, ERMLaser->1/6, etc.).
3. Attribute-table shift (powersub.cpp + btl4gau2.cpp, gotcha #8): the
generator-letter lamp resolved its source via
ResolveLink(AttributePointerOf(subsystem,"InputVoltage")), but the
BT_DEV_GAUGES audio attribute rows shifted the chained attribute ids so
AttributePointerOf no longer landed on voltageSource@0x1D0 -> the link
always resolved to 0 (OFF) even though the master PoweredSubsystem ctor
DID bind voltageSource to its Generator. New BTPowerSourceFrame bridge
reads the NAMED member via PoweredSubsystem::ResolveVoltageSource() and
returns Generator::generatorNumber (@0x1E0), bypassing the attribute
table -- the same pattern as the BTCoolingLoopFrame fix. Generator
letters now render (verified: PPC_1->GeneratorA="A", Myomers->
GeneratorD="D", SRM6->GeneratorB="B").
Result: the "4 A / 1 B / 5 D" boxes render with both the loop number and
generator letter, matching the reference. Kept BT_LOOP_LOG-gated
[loopfeed]/[busfeed] feed diagnostics; both pod + glass builds clean.
Awaiting live playtest verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #2 audit (BT_CTRLMAP_LOG) proves the Eng-page button->msg-3 route was always
authored (121-mapping L4 ControlMappingsList, ~a dozen EVENT msg 0x3 entries;
weapons via Eng-page aux elem 0x21 mask-gated per ModeMFD page). It was "silently
dead" ONLY for lack of the id-3 handler, which the prior commit wired -- so coolant
priority via the MFD "Display -> Coolant" button now works end-to-end. (The
mask-0xffffffff msg-3 entries at 0x12/0x14 go to ThermalSight/Searchlight on the
PowerWatcher branch -- separate, untouched, no collision.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coolant priority (Lynx: weapon MFD -> Display -> Coolant) was unreachable: the
ToggleCooling handler (@004ad6f8, HeatableSubsystem table @0x50E41C id 3) was
undefined AND the handler chain skipped it -- PoweredSubsystem::GetMessageHandlers
chained straight to the Receiver root, bypassing HeatSink/HeatableSubsystem, so a
weapon's dispatch never saw id 3.
Reconstructed ToggleCooling from the disassembly (tools/disas2.py 0x4ad6f8): a
per-subsystem coolant on/off TOGGLE -- novice-locked (owner->BTPlayer->
roleClassIndex+0x274==0, the same guard as MoveValve), press-only, then flip
coolantAvailable(+0x134) 0<->1 and coolantFlowScale(+0x15C) 0.0f<->1.0f. Cutting a
system's cooling frees the shared loop for the rest -- the emergent "coolant
priority" (the mechanism is a toggle, not a multi-level cycle).
- heat.hpp / heatfamily_reslice.cpp: HeatSink::ToggleCoolingMessageHandler (id 3)
+ HeatSink::GetMessageHandlers. Registered at HeatSink (the abstract
HeatableSubsystem never instantiates; every concrete heatable subsystem is a
HeatSink, where the coolant fields live) -- identical coverage to the binary's
base-table registration, no downcast.
- powersub.cpp + Condenser/Reservoir: chain their handler sets through
HeatSink::GetMessageHandlers so id 3 reaches every heatable subsystem.
- mech4.cpp: BT_COOLTOGGLE_TEST scripted inject (dispatch id 3 to the first weapon)
for verification.
Verified (BT_COOLTOGGLE_TEST + BT_COOL_LOG, expert egg): "[cool] PPC_1 ToggleCooling
reached" then coolant OFF(flow 0.0) <-> ON(flow 1.0) each press -- chain routes,
handler toggles, novice guard honored. Both build/ + build-glass/ compile clean;
existing ids 4-8 unaffected (found before id 3). NEXT: the #2 MFD button routing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Field report: LAN auto-discovery (BT_RELAY=auto) didn't work -- a joiner had to
hand-set BT_RELAY=10.0.0.46:1500 to reach the console every time.
- players/join_lan.bat: BT_RELAY=auto -> BT_RELAY=10.0.0.46:1500 (the operator PC's
LAN IP), with a comment on updating it if the IP changes.
- tools/btoperator.py: the export now DETECTS the operator PC's LAN IP (UDP
default-route probe, no packet sent) and stamps BT_RELAY=<lan-ip>:port into the
generated join_lan.bat instead of "auto" -- so regeneration keeps it correct and
doesn't revert to the broken auto-discovery. Verified: detects 10.0.0.46 here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playtest: "after I destroyed the mech the red blip was still on my radar." The
authentic engine adds/removes radar contacts through the interest feed
(GaugeRenderer::NotifyOfNewInterestingEntity / NotifyOfBecomingUninterestingEntity),
and the game decides what's a contact -- a dying mech becoming uninteresting is
removed, so the blip drops. The port DROPPED that feed and rebuilds the radar grid
every frame from EVERY dynamic mover (RebuildEntityGrid), so it never removed the
dead one and the red blip lingered on the frozen wreck (the wreck stays as an entity
by design; only its model sinks/buries).
Fix: in RebuildEntityGrid, skip a destroyed/disabled mover (GetSimulationState() ==
2 || 9 -- the movementMode wreck latch, unified with simulationState) so it leaves
the moving/red-contact grid. Live movers unaffected; the blip drops on death.
Verified (BT_MAP_LOG): the killed enemy (ent cls=0xbb9) is drawn ONCE at the
death-transition frame (the 1-frame lag before movementMode latches to 9) then never
again for the rest of the session, while the owner keeps drawing every cycle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playtest: "lost missiles mid-fight, no pips, sad noise -- out of ammo?" The default
build didn't trace ammo, so it couldn't be answered from the log. Added a [ammo]
trace (env BT_AMMO_LOG) at the four ProjectileWeaponSimulation decision points --
FIRED+rounds-left, gate1/gate2 dry-transition edge, dry-fire blip, and denial -- plus
a public AmmoBin::GetAmmoCount() const getter.
Reproduced the actual cause: NOT ammo depletion. The SRM6 trips gate 1 FailureHeat
(heatLvl=2) after ~5 volleys and latches into the permanent NoAmmo roach-motel with
19 rounds still in the bin:
[ammo] SRM6_2 FIRED, rounds left=19
[ammo] SRM6_2 -> NoAmmo (gate1): destroyed=0 failHeat=1 heatLvl=2 mechDisabled=0
Logged as the concrete instance of the heat-economy open question (open-questions.md).
Trace-only; no behavior change (all output gated on BT_AMMO_LOG).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playtest feedback: the red MFD lamp buttons were obstructed (only a thin sliver
showed under the surface, crammed against the DISPLAY/PROGRAM legends), and the blue
THROTTLE/JOYSTICK flight blocks overlapped the bottom MFDs.
- Red lamps now sit FULLY OUTSIDE the MFD (kCkRedGap=3 off the edge, kCkRedH=24 tall)
instead of reaching kCkRedCell into the display -- nothing occludes them and they
read as real buttons, clear of the DISPLAY/PROGRAM legends. topBand grows to 223.
- Flight blocks drop below the MFD's bottom red lamps (belowLamp) -- no overlap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under BT_DEV_GAUGES the DEFAULT is now the pod-faithful cockpit surround: the 3D
world CENTERED with the six gauge surfaces composited AROUND it in the single main
window at 1/2 native scale, plus clickable RIO button lamps. Iterated as a visual
reference with the user, then landed in the engine.
- L4VB16.cpp/.h: BTCockpitLayout + BTCockpitComputeLayout (layout single source of
truth, computed from the backbuffer canvas); BTDrawCockpitPanels (band fill ->
button lamp quads -> 9-entry surface loop, phosphor-green mono / amber palette
sec, Eng-sibling slot map -> GDI-atlas flight labels; D3DSBT_ALL state-block
isolation = the floating-rocks trap); BTCockpitMouseDown/Up (client->bb hit-test
+ glass press/release/right-latch contract); BTApplyWorldViewport + DevGaugeDocked
cockpit branches; shared BTLampBrightnessOf inline. Hooked at the top of
BTDrawGaugeInset (before BT_SHOT, so shots include it).
- btl4main.cpp: glass preset (cockpit default, per-display panels opt-in); mode
resolution -> gBTGaugeCockpit with work-area-clamped window sizing (-res = view
size); WndProc WM_L/RBUTTON routing.
- L4VIDEO.cpp: BTWorldAspectOf cockpit branch (view rect on-screen aspect).
- L4VIDRND.cpp: CameraShipHUDRenderable::Render made viewport-relative.
Buttons = the L4GLASSWIN address banks x0.5 (Heat 0x2F, Mfd2 0x27, Comm 0x37, Mfd1
0x0F, Mfd3 0x07 red; radar rails 0x10-0x1B yellow; flight 0x38-0x47 blue, labeled).
Full rect is the hit target; the surface draws over it so only the lamp edge shows.
Precedence: BT_GLASS_PANELS=1 stands cockpit down > BT_COCKPIT=1 > _WINDOW/_DOCK
opt-out > cockpit default; BT_COCKPIT=0 = dock-bottom. Green tunable via
BT_COCKPIT_TINT. Renders in ALL builds; only the PadRIO click/lamp seam is
BT_GLASS-gated.
Verified (awaiting playtest): both build/ + build-glass/ compile (0 error C);
glass BT_SHOT matches the mockup; 500-click storm during a live mission survived
(Gitea #18 Receiver-gap fix holds); BT_COCKPIT=0 dock-bottom unregressed;
BT_GLASS_PANELS=1 stands cockpit down; pod build renders the panels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>