6bb03aed0bc4e0ba6cd6026d9224c074be897ff7
161
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6bb03aed0b |
Gitea #48 ROOT-CAUSED + FIXED: MFD "stray blocks / misplaced lamps" = uninitialized translation-table entries leaking pixels into other displays' bit-planes
The conviction was empirical, not theoretical: a new write-site trap
(BT_PLANE_AUDIT) in the seven L4VB16 drawing primitives logs any draw whose
color carries bits outside its port's plane mask -- the exact cross-display
corruption condition (Replace ORs an unmasked color; Or/Xor ignore the mask
entirely; And clears foreign planes). A quiet solo session produced 15-30
leaks per minute:
[plane] PORT 'sec' pixmap draw at(0,0) idx 217 entry=0xffffff00 mask=0x3f
[plane] LEAK DrawPixelMap8[table] at(639,0) color=0xffffff00 mask=0x3f leak=0xff00
THE DEFECT: L4GraphicsPort::translationTable[256] is never initialized in the
ctor, and BuildSecondaryTranslation fills only the entries its BitWrangler
reaches -- 2^numberOfBits: 64 for the sec plane (mask 0x3F), 4 for the overlay
(0xC0). Entries above that stay heap garbage. Every draw resolves color
through this table, and the PIXMAP path indexes it with raw pixel values
0..255: the 480x640 radar background carries pixel index 217, whose garbage
entry's high bits (0xFF00 = ALL EIGHT MFD planes) were stamped into the shared
640x480 buffer -- invisible on the culprit page (the in-plane bits happened
dark) and visible as bright fragments at the same coordinates on every OTHER
display. That is the operator's screenshot exactly: the same-position blips
on Mfd1+Mfd2 and the Heat-display block.
FIX (engine, both layers):
* zero translationTable in the L4GraphicsPort ctor (plane-neutral default);
* BuildSecondaryTranslation now cycles the in-plane pattern across entries
[2^bits..255] -- high-index art degrades to its (index mod 2^bits) colour
IN-PLANE and can never leak. The 1995 binary ships the SAME 64-entry fill
and survived on 6-bit art discipline; garbage is not a preservable
behaviour, so the cycle-fill is a guarded PORT deviation (documented).
A/B PROOF: same probe, 60s -- 0 leaks after the fix (15-30/min before).
sim3 3-pod regression: zero crashes.
SECOND real defect found + fixed en route: sessions configure the CAMERA seat
first ("cameraInit" -- which includes the MISSION-REVIEW context:
configure(0, sec, 0, 0x00FF, native, rgb, mrpal.pcc), a DirectColor context
whose translation tables legitimately hold full RGB565 values spanning all 16
plane bits), and the viewpoint swap to the mech re-configured WITHOUT tearing
that tree down (btl4app's s_gaugeTreeBuilt latch treats the mech build as the
first). The orphaned review gauges kept executing through stale DirectColor
ports. BTL4GaugeRenderer::ConfigureForModel now overrides (ConfigureForModel
made virtual in L4GREND.h) and tears the prior entity-bound tree down before
every rebuild -- safe on first build, and the review screen rebuilds the same
way when the seat returns to the camera.
Also logged (open-questions): the review-screen PlayerStatus panels read the
compiled player at RAW BINARY OFFSETS (+0x1FC vehicle / +0x1C8 score /
+0x1C4 alive-dead box) -- the databinding trap, dormant until the review
screen runs; bridge before enabling the review.
Ruled out along the way (with evidence): the pilot-list label/erase geometry
(erase box 128x32 covers the 64x16 name rasters), the radar name labels
(view-level ClipImage clips them), PlayerStatus in-game execution ([ps] probe:
never runs in-game), and the PNAME bitmap dimensions (egg generator emits
64x16 exactly).
Why it "started with the comms feature" (#43): that wave registered the new
gauge classes with the config interpreter, letting more of the authored page
furniture parse and draw than before -- the leaking high-index pixmaps rode in
with it. [T3 correlation detail; the leak + fix are T2 live-verified.]
Diagnostics kept (env-gated): BT_PLANE_AUDIT (the leak trap, now a permanent
regression tripwire), BT_PS_LOG, the port-level pixmap identity trap.
KB: gauges-hud.md (#48 section), decomp-reference.md (BT_PLANE_AUDIT),
open-questions.md (PlayerStatus databinding entry). checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6f5a264835 |
Gitea #47 COMPLETE: the ENG-button attention FLASH is live (jam / bay fire) -- MechTech status scan + BTL4GaugeAlarmManager + lamp chain, all from the binary
The pod behaviour Cyd described -- "on a jam or bay fire the display eng button
for the system flashes" -- is authored data + a five-stage chain, now running:
MechTech::TechnicalAssistance (@004ad33c, per frame)
edge-scans every monitored subsystem's GetStatusFlags() 7-bit condition
mask (TechStatusType: Destroyed 0, Damaged 1, CoolantLeaking 2,
Overheating 3, AmmoBurning 4, Jammed 5, BadPower 6)
-> Start/StopEntityAlarmMessage (@00436688 id 7 size 0x20 / @004366b8 id 8
size 0x1C; broadcast @004364e4; port shape: direct
Start/StopEntityAlarmImplementation calls on the gauge renderer)
-> GaugeAlarmManager::Activate(alarmModel) -- alarmModel = MechTech+0x100 =
the 'mechalrm' ModelList (id 83) -> SearchList(83, type 31) -> the baked
GaugeAlarmStream (id 331, 11 items {condition, lampCode}), decoded:
Destroyed -> gotoEngineering + engCooling + engBusMode
CoolantLeaking -> gotoEngineering + engCooling
AmmoBurning -> gotoEngineering + engEject
Jammed -> gotoEngineering + engEject
BadPower -> gotoEngineering + engBusMode
-> BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem -- THE REAL BODY, from
@004cc2fc + helpers @004cc108/148/1a0/264/27c + tables @0051cf1c..0x51d084
(gotoEngineering 0x80 = the subsystem's QUAD-SELECT bezel button via
lamp[aux]/mode[aux] tables; 0x81+ descend the eng-page bank; Condenser /
Generator specials on CoolantLeaking; <0x80 = the heat-bank fixed map).
btl4galm.cpp's old bodies were admitted fabrications and its provenance
note ("no override body exists in the image") was wrong -- corrected.
-> LampManager::FindLamp (@00444c80) -> Lamp::SetAlertState (@00444e64, a
COUNTER so stacked alarms hold the flash) -> L4Lamp::NotifyOfStateChange
emits RIO flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239) -> the
pod's physical lamps AND the glass panels (PadRIO IS rioPointer there).
FOUR load-bearing defects found and fixed en route -- each independently fatal
to the feature:
1. HeatableSubsystem's Derivation chained Subsystem directly, SKIPPING
MechSubsystem (written before the WAVE-1 re-basing) -- so EVERY subsystem
on both family branches failed IsDerivedFrom(MechSubsystem), which is
exactly MechTech's monitor filter: the status scan watched NOTHING.
2. MechSubsystem::GetStatusFlags was non-virtual (binary: vtable slot 12) --
the scan's MechSubsystem* call bound statically to the base tier and the
weapon bits could never surface.
3. ProjectileWeapon::GetStatusFlags sat in a Ghidra export gap -- raw-disasm
@004bbf88: base | 0x20 (weaponAlarm==5 Jammed) | 0x10 (linked bin
cookOffArmed@0x18C AmmoBurning). The port had "defer to base".
4. Mech::Reset's respawn sweep blanket-cast every roster entry to
MechSubsystem and called RespawnRepair -- wrong for the Subsystem-level
entries (MechTech 0xBDC, SubsystemMessageManager 0xBD3). On MechTech the
recon damageZone slot lands on its subsystemMonitors chain head: NULL
while the scan was broken (fix #1's bug MASKED this one), but the moment
the monitors populated, RespawnRepair virtual-called through a
SubsystemMonitor as if it were a DamageZone -> load-time crash in
StateIndicator::SetState (caught with cdb; call [edx+14h] on code bytes).
The sweep now filters IsDerivedFrom(MechSubsystem) before the cast.
Also: GUID identities pinned -- 0x50f4bc = PoweredSubsystem::ClassDerivations
(via @004b1208 = its TestInstance; btl4gau2's two "Generator" comments were
wrong, swept), 0x50fb60 = Generator's. A shadow-field instance documented for
the deferred de-shadow: MechSubsystem::damageZone re-declares the PUBLIC engine
Subsystem::damageZone (SUBSYSTM.h:159) -- mechtech's naive `sub->damageZone`
read the never-written engine member (0 monitors again); now reads the recon
member via GetDamageZoneProxy(). Logged in open-questions.
Wiring: BTL4GaugeRenderer now constructs + assigns the BTL4GaugeAlarmManager
(the base ctor NULLs it and Activate Check()s it); MechTech's Report*/
StatusMessageSink stubs are real; alarmModel confirmed baked (= 83) at runtime.
VERIFIED LIVE (scratchpad/lampflash.py, BT_LAMP_LOG chain trace):
[techstat] MechTech id 32 monitors 29 subsystems, alarmModel 83
[techstat] LRM15_1 condition 4 SET (alarmModel 83) <- bay fire armed
[galarm] condition 4 code 0x80 -> lamp 0xd mode 0x1 FLASH
[lamp] 0xd <- 0x37 (FLASHING) <- the select button
[galarm] condition 4 code 0x85 -> lamp 0xb mode 0x4 FLASH <- engEject
[techstat] LRM15_1 condition 4 CLEARED <- detonation clears
[techstat] AmmoBinLRM15_1 condition 0 SET <- bin Destroyed
Regressions: baytest PASS (bay fire kills), baypurge PASS (purge extinguishes),
sim3 3-pod brawl PASS with ZERO crashes (6 kills, organic heat-route bay fires,
respawns clean through the new sweep filter).
Env: BT_LAMP_LOG ([techstat]/[galarm]/[lamp]). KB: gauges-hud (the flash
section), decomp-reference (the GaugeAlarm closure + tables + GUIDs),
open-questions (built-note + the de-shadow deferred item), btl4gau2 comment
sweep. checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d39227ef39 |
Gitea #51 ROOT CAUSE: LoopAtWill was mapped to "loop forever", arming 224 of 603 samples as endless OpenAL sources
SAURON's "coolant flush sound glitched and perma" is one instance of a systemic
defect. SetupPatch (L4AUDLVL.cpp) decided looping from the SAMPLE flag alone:
AL_LOOPING = (info.loop != ForceStatic)
That armed every non-ForceStatic sample -- 224 of the 603 authored zones,
including unmistakable one-shots -- as an OpenAL source that never ends on its
own. Any such sound whose note-off never arrives plays forever. Measured 1946
LoopAtWill x Transient arming events in one short session.
The sample flag expresses PERMISSION; the SOURCE decides. Three facts [T1]:
* the enum's own comments (L4AUDLVL.h:14-19) -- LoopAtWill = "will play once
OR LOOP AS DESIRED", ForceStatic = "plays only once EVEN IF LOOPED" (a
veto), LoopAlways = "ramp up and then down";
* LoopAtWill is enum value 0, i.e. the DEFAULT an unauthored sample receives
(WTPresets.cpp:37) -- it cannot mean "loop forever";
* LoopAlways, the real always-loop, is used by ZERO shipped samples.
"As desired" is the source's authored AudioRenderType (Transient=one-shot vs
Sustained=held), streamed from AUDIO*.RES (AUDLVL.cpp:28) and already consulted
by the renderer (L4AUDRND.cpp:567, AUDREND.cpp:184). SetupPatch now takes it,
threaded from all three L4AudioSource call sites (Direct/Dynamic3D/Static3D).
New rule: LoopAlways->1, ForceStatic->0, LoopAtWill->the source's render type.
MEASURED before changing behaviour (BT_LOOP_AUDIT=1, scratchpad/loopaudit.py):
LoopAtWill x Transient -> loop=0 (was 1) 1946 events
LoopAtWill x Sustained -> loop=1 unchanged 65 events
ForceStatic x Transient -> loop=0 unchanged 292 events
The engine loops (EngineAccel07_z0..z2, EngineMotor01, EnginePower01) are all
LoopAtWill/Sustained and PRESERVED -- no regression there. All 24 reclassified
samples are genuine one-shots: laser charge/fire/explosion/loaded, missile
loading, engine shift, coolant pressure inc/dec.
A/B PROOF (scratchpad/loopab.py, same session both arms, only BT_LOOP_LEGACY
differs), asking the engine's own BT_AUDIO_DUMP what is still playing with
loop=1 long after every release:
[legacy] EnginePower01, CoolantPresInc03_z2, CoolantPresDcr03_z2
[fixed] EnginePower01
The two stuck coolant sources are the reported symptom. They were eventually
reclaimed when a later trigger reused the source, which is why the bug was
intermittent -- the "perma" case is when nothing else retriggers it. The fix
removes the mechanism.
BT_LOOP_LEGACY=1 restores the old rule for a field A/B without a rebuild. The
layers to listen to are the beam sustains: LaserA/CSustain* are authored
LoopAtWill on a TRANSIENT source, so they now end with the sample instead of
looping until note-off. The render type is T1 authored data; the interpretation
that LoopAtWill defers to it is a strong reading of the enum + defaults rather
than a disassembled statement, and is flagged as such in the KB.
Plausibly bears on #32 (audio cutting in/out late in a match): a stuck looping
source holds its pool slot for the rest of the round.
Rigs: scratchpad/flushsnd.py (flush start/stop pairing), flushsnd2.py (stuck-
source hunt), loopaudit.py (the classification matrix), loopab.py (the A/B).
KB: context/wintesla-port.md audio section, context/decomp-reference.md env
table (BT_LOOP_AUDIT / BT_LOOP_LEGACY / BT_AUDIO_DUMP / BT_AUD_TAIL),
docs/AUDIO_FIDELITY.md F38. checkctx.py CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
33ca99eb69 |
Input-audit cause #1: revive the Avionics power bank + add the unhandled-message trace
THE CLASS OF BUG. A default-constructed Receiver::MessageHandlerSet is a total blackhole: entryCount 0 and NO parent chain, so Find() returns NullHandler for every id and Receiver::Receive dropped the message with no log, no counter, nothing. docs/INPUT_PATH_AUDIT.md ranked this systemic cause #1; running its own lint finds SEVEN such sets (Sensor, Searchlight, ThermalSight, Gyroscope, MechTech, SubsystemMessageManager, Torso). It is also why generators cannot be switched off (#53) -- same shape. FIX 1 -- Sensor / Avionics (the big one). The streamed mappings aim 108 records across the 18 mechs at it: the MFD2 ENG1 power bank, 6 clickable buttons plus keys F5-F9 (route Avionics power to generators A-D, gen mode, coolant cut). All silently swallowed. Sensor needed NO handlers of its own -- ids 4-8 already live on PoweredSubsystem, which chains HeatSink for id 3, covering the whole 3-8 range those buttons send. It only ever needed to CHAIN, which is exactly why the byte-identical bank on ENG2 works (Myomers chains, myomers.cpp:88-100; Sensor did not). Added Sensor::GetMessageHandlers() with the same function-local-static idiom and pointed DefaultData at it instead of the empty set. VERIFIED LIVE (scratchpad/avionics.py -- pages MFD2 and presses the bank): [gensel] Avionics -> GeneratorA (tapped) ... B, C, D [gensel] Avionics mode -> 2 i.e. Avionics now behaves exactly like Myomers. 24 handler hits, 0 unhandled. FIX 2 -- the standing guard. Receiver::Receive now logs once per (class, message id) pair when no handler is found: '[msg] UNHANDLED: <class> has no handler for message id N -- silently dropped (is its MessageHandlerSet chained?)'. Once-per-pair on purpose, since some ids broadcast every frame. This one line would have printed Avionics, Searchlight, ThermalSight, crouch and all four generator buttons on the first boot instead of costing us months of player reports. Uses SharedData::derivedClasses -> Derivation::className. DEFERRED, with reasons (not guessed at): Searchlight's ToggleLamp body exists but is unreachable -- it needs a correctly-typed forwarder (Receiver::Handler is void(const Message*); ToggleLamp is Logical(Message&), so a cast would be UB that happens to work on x86) AND an id check, because its id 3 collides with HeatSink's ToggleCooling and I have not traced whether PowerWatcher's chain reaches HeatSink. #53 needs more than chaining: ToggleGeneratorOnOff @004b1ed0 has no body at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
5bbe070e9d |
Gitea #56 (+#50): pin the windowed backbuffer so a device reset cannot silently re-size it
ROOT CAUSE. mPresentParams.BackBufferWidth/Height were assigned ONLY in the fullscreen branch (L4VIDEO.cpp:3430-3434); windowed they stayed 0, which tells D3D9 to derive the backbuffer from the device window's client area at CreateDevice. Fine at startup -- but the device-lost recovery path saves and restores mPresentParams around Reset(), so it faithfully preserved those ZEROS and Reset re-derived the backbuffer from whatever the client area was at THAT moment, while gBTCockpitCanvasW/H still described the startup canvas. The cockpit layout is computed in backbuffer space and clicks are mapped client -> canvas (L4VB16.cpp BTCockpitMouseDown), so after that divergence panels are drawn for one geometry and clicks are mapped in another: buttons drift off their artwork, and at a large enough mismatch nothing is clickable at all -- which is #50 (SAURON: 'none of my MFDs clickable' while other players' worked). It needs BOTH a resize/maximise AND a device loss (alt-tab, Steam overlay, monitor sleep, UAC, RDP, driver hiccup), in that order. That is why it hits some players and never the operator, who does not resize the cockpit window -- and it matches the report exactly: the buttons 'become' misaligned rather than starting that way. FIX * windowed: record the client rect into BackBufferWidth/Height at CreateDevice. That is exactly what D3D would have derived, so startup behaviour is unchanged, but the save/restore now preserves a REAL size and Reset can never pick a different one. Deliberately NOT screenWidth/screenHeight -- those are the app's -res values (default 800x600) while the cockpit window is sized independently, so pinning to them would shrink the backbuffer. * BTVerifyCockpitCanvasAfterReset(): called at both device-lost recovery sites; compares the real backbuffer against the canvas globals, and if they ever disagree it says so and re-syncs instead of drifting silently. VERIFIED: solo cockpit run unchanged -- '[cockpit] view 900x440 canvas 1452x999', BT_SHOT still 1452x999, Heat panel renders correctly, no mismatch warning. NOTE: reverted an earlier scripted edit of this file that had corrupted an unrelated line -- L4VIDEO.cpp is CRLF and a Python join('\n') introduced a bare LF inside a string literal. Use the edit tool on this file, not line surgery. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
23229e573e |
Reverse thrust was dead on the desktop (user report): wire the 0x3F throttle-handle button
LALT -> button 0x3F was bound in both binding layers and PadRIO pushed the RIO ButtonPressed event correctly, but nothing ever set the mapper's ReverseThrust: - the authentic route is BTL4.RES 'L4' streamed record [2] (Button Throttle1 0x3F -> attr 6 ReverseThrust@0x124), but our streamed BUTTON mappings are not consumed at all -- MechControlsMapper::AddOrErase is still the unreconstructed Fail stub, so the event never reaches the attribute; - the ONLY writer of reverseThrust was the keyboard bridge's 'key_throttle < 0' test, which is bypassed whenever a RIO is operational (the pad RIO always is, so the bridge is OFF on the desktop) AND is unreachable regardless, because L4PADRIO clamps the Throttle channel to [0,1]. Net: Alt did nothing and the flag was re-zeroed every frame. FIX: publish the 0x3F hold state from PadRIO::EmitButton -- the one chokepoint every desktop source funnels through (keyboard, gamepad, DirectInput joystick, glass-panel clicks via SetScreenButton) -- and apply it in InterpretControls gated on BTPadRIOActive() so real pod hardware is untouched (there the streamed mapping owns the flag). Marked [T3]; delete once streamed button mappings land. Reverse is a HOLD (manual: hold the red throttle button, release = forward). Verified with scratchpad/revtest.py (keybd_event injection, BT_KEY_NOFOCUS): forward = rev=0 dmd +61.501; LALT held = rev=1 dmd -61.501; release edge logged. KB: pod-hardware.md now records the streamed-button-mapping gap + this seam. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
6c3fca2f67 |
Gitea #38 ROOT CAUSE + FIX: mech paint lost on geometry re-load (view toggle / respawn)
Found from the user's live 2-node demo: a Crimson MadCat went GREY in its own view after a V (inside/outside) toggle, with NO new [paint] or MakeMechRenderables line in the log -- so no rebuild, just a re-parse. ROOT CAUSE: the per-pilot colour/badge/patch is applied by rewriting MATERIAL NAMES while a BGF parses, and only while the substitution callback is installed (SetupMaterialSubstitutionList .. TearDown, bgfload.cpp:15-18). BTL4VideoRenderer::ApplyViewSkeleton re-parses every shown segment BGF on each view toggle AND on respawn (that is what its fresh graphic-state read is for), but did so OUTSIDE that bracket -> raw %color% placeholders -> unpainted materials -> grey. Nothing caches the geometry (d3d_OBJECT caches only textures), so every call re-parses. This is #38's mechanism: 'colors not preserved on respawn' was never a replication or teardown bug. FIX: re-install the substitution list around the reload, reusing the serial the mech was BUILT with -- SetupMaterialSubstitutionList advances the global %serno% per call, so a naive re-bracket would stamp a different serial and still resolve the wrong names. MechRenderTree gains paintSerno (captured before Setup at build); ApplyViewSkeleton installs/restores around the loop and logs it. Rig-verified: the crimson MadCat stays crimson (yellow patch intact) across repeated inside/outside toggles; each toggle now logs '[paint] color=red serno=0' + '(paint serno 0)'. ALSO -- corrections to yesterday's #44 name plate from the adversarial pass: - kPlateARGB 0xFF808080 -> 0x80FFFFFF. BMAP.BMF tag 0x0027 is dpfB_MATERIAL_OPACITY_TAG [T0 libDPL/dsys/PFBIZTAG.H], NOT a diffuse colour: the materials carry NO colour tag at all, so the plate is the unmodulated callsign raster at ~50% opacity, not a grey label. (Two independent workflows converged on this.) - the plate's K = 1/2.8 is NOT the hotbox's constant (2.8145) -- de-unified. - the Lock attribute is HUD attr id 10, an int/Logical*, not a Scalar*. - the PNAME pair split is the V half, not U (our per-player texture is one 128x32 cell, so the port quad samples v 0..1). - retracted bgf-format's 'tag 0x0027 likely SPECULAR [T4]'. KB: new reconstruction-gotchas section on the whole bug class (load-time-only state must be re-installed on every RE-load, incl. the serno trap). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
35c750dc7c |
Gitea #44: the reticle TARGET NAME PLATE (PNAME1-8) -- the target's callsign under the crosshair
Reconstructed the last deferred piece of the 1995 reticle, decoded from the Execute disassembly @004cdcf0 [T1]: - selection: target_mech+0x190 (owning BTPlayer) -> player+0x1e0 (playerBitmapIndex), 1-based into the mesh table at this+0x2e8 - placement: re-placed every frame the aim moves -- scale 0.12 uniform, translate (K*retPos.x, K*retPos.y - 0.08, -1.0), K = 1/2.8 (the x87 long double @0x4cee64) -- so the label TRACKS THE AIM POINT, not screen centre - visibility: the LOCK attribute (this+0x184, cached +0x188) AND an owning player; reticle-off / simple-X also hide it - art: the plate quad UV-addresses a shared 128x64 bitslice texmap whose texels the pod OVERWROTE each mission with the egg's callsign rasters (original source commented out at L4VIDEO.cpp:5682-5710) -- the baked 'PLAYER n' art in BMAP.BSL is only the shipped fallback. Material colour is a neutral (0.5,0.5,0.5): grey-white, not phosphor green. Port: same geometry in reticle units (0.224 below the aim point, 0.336x0.084) drawn as the target's egg callsign texture through the reticle's own MapX/MapY mapping, so it follows the world view in every layout and BT_SHOT captures it. New: dpl2d_DrawTexturedRect, BTReticleTargetPlate, BTGetPlayerNameTexture. Also: kRetCaret corrected 0.02f -> 0.025f (_DAT_004cd7f4 read from the binary; the old value was a T3 guess, 20% small). KB: gauges-hud + open-questions corrected (the chain was filed as deferred and mis-attributed to 'the 3D marker'); TWO PNAME consumers now distinguished (this plate, and CameraShipHUDRenderable's ranking window which shipped 2026-07-18); new bgf-format section on the plate atlas. Rig-verified: 2-node relay, BT_GOTO=enemy -> '[hud] name plate: bmp=2 tex=1' and 'Boreas' rendered under the crosshair on the locked target. Solo clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
210bdb05ee |
Remote operator control channel + THE teardown crash root-caused and fixed
REMOTE OPERATORS (dev-channel ask): the relay grows a control TCP listener (console port + 7) speaking a line protocol -- AUTH <secret> (auto-generated content\operator_secret.txt), then launch / stop / ping / get mission / set key=value;... The relay's timestamped log stream tees to the authenticated operator (bounded per-conn buffers -- a stalled operator can NEVER stall the relay; overflow = drop), with a 400-line history replay on connect so mid-session operators see current state. One operator at a time; a new AUTH displaces the old. stdin commands unchanged (rigs/GUI local mode untouched). The operator GUI gains a 'Relay host' field: blank = today's local flow byte-for-byte (child relay dies with the window); a hostname = drive that machine's relay remotely (launch/stop/apply-settings over the wire, roster rebuilt from the teed log, local-instance joins point at the remote relay). Only the relay host needs port forwarding. Scripted protocol test: auth reject/accept, takeover, get/set, ping, history replay, and a FULL 2-node mission launched and stopped entirely over the socket. CRASH FIX (the layout-shifting heisenbug -- and almost certainly issue #35): the new crash self-report finally captured its 24-frame stack: InterestManager::OrphanInterestOrigin -> RemoveUninterestingEntity -> DestroyEntityAudioObjects -> {Static,Dynamic}3DPatchSource:: IsAudioSourceClipped, AV reading NULL+0x38 -- the unguarded chain GetMissionPlayer()->GetPlayerVehicle()->GetSimulationState() (the authentic burning-mech death-silence check) hit a NULL vehicle during MP teardown windows. Both sites now null-guard; burning semantics preserved whenever the vehicle exists. Also live-confirms that interest-based entity teardown RUNS in MP (the #38 paint mechanism). Regression: full combat round + control-channel drive, zero crashes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef9a648c89 |
README: BASIC control mode does not steer with pedals (Ferret's 510 report)
Field report (new tester, 510): keyboard A/D + arrows 'not working' -- can twist and drive but not turn. Root-caused live via key injection: fresh spawns start in BASIC control mode, where steering rides the STICK (authentic rookie mode) and the pedal-bound keys are inert by design; one M press (MID) makes A/D steer. (A false regression-bisect chase established the metric noise came from an attached pad's stick drift nudging BASIC steering.) README now warns about the default mode; kept the env-gated [padwrite] diagnostic from the hunt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b2b7a45b5c |
Issues #39 + #40: gate input until the scene presents; cap the wait-screen idle loop
#39 (field: 'I was able to input before the game screen actually came up -- fired weapons, moving, could hear the audio'): the simulation runs during load/wait and PadRIO fed it live input. Poll now emits only the analog heartbeat until gBTSceneHasPresented -- keyboard, pad and joystick all stand down; edge state stays parked so keys held across the boundary press cleanly at first frame. BT_BTNTEST and the panel-click seam stay live (event injection, same as before). #40 (field: 'GPU usage during waiting screen is sometimes consuming up to 45-50%'): ExecuteIdle Cleared+Presented uncapped for the whole join wait. Now capped at ~40 Hz with a 5 ms sleep on skipped passes (the overlay's change gate is 12.5 Hz -- nothing visible changes faster). Regression: 2-node relay round through the wait screen -> load -> mission (135 in-mission ticks, autofire) -> clean. Awaiting the reporter's live confirm on both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f7daf03507 |
Crash self-report + round-completed latch + PDB archiving
CRASH SELF-REPORT (the #35/#41 heisenbug hunt): every unhandled exception now logs its code, address, and an EBP-walked stack as module-relative offsets (btl4+0xNNNN) into BT_LOG before dying -- any field crash on any machine hands us a resolvable stack. The walker is per-dereference guarded (a corrupt chain truncates). BT_CRASHTEST=1 exercises the path end-to-end (verified: forced AV logs code/addr/5-frame stack). mkdist archives each build's PDB next to its zip so offsets resolve per distributed version. Context: a flaky release-only crash in the mech build path surfaced during #38 rigs -- it moves with build layout, spares neither peer, and masks under a debugger heap (13 clean cdb runs vs 2/3 crashes without). A real latent memory bug; the self-report will catch it wherever it strikes next. ROUND-COMPLETED LATCH (issue #38, awaiting verification): relay-mode clients refuse RunMission after a round has ended -- the lobby-loop relaunch owns the next round (in-process round 2 rebuilt every player/mech entity on stale globals; matchlog caught player=3:1->3:36 in one generation). Latch sets ONLY when the stop ends a RUNNING mission (a stray pre-mission StopMission must not poison round 1 -- the first cut ate a legit launch, caught + fixed in rig). Regression verification pending (contaminated by the heisenbug above). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
390d4ddce7 |
Issue #36: direct-axis RELEASE EDGE -- centering an axis now releases its channel
Night-2 (RajelAran): 'the game is not seeing axis release for turn -- the mech may keep turning when control is released.' Root cause: the direct-mode axis write was gated on raw != 0, so an axis returning INSIDE the deadzone just stopped writing and the channel LATCHED at the last deflection -- fatal on channels with no keyboard spring (the Turn composite in the twin-stick profile). Fix: per-binding previous-value tracking (pad + joystick paths); the outside->inside transition writes 0 exactly once, then the centered axis leaves the channel to other inputs (composition preserved; slew bindings excluded by design -- a lever stays where slewed). Plus the #24 rule, axis edition: a pad/stick that DIES while deflected releases its direct channels (throttle keeps the last lever position -- safer than an all-stop mid-fight). Verified: build + 45s glass mission with an idle XInput pad (no spurious writes, no regression). The deflect-release cycle itself needs a human hand -- awaiting RajelAran's next session. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c2ee7299b2 |
Generic joystick / HOTAS / pedals support: DirectInput 8 layer + capture wizard
Tester ask: configure non-Xbox controllers. New L4JOY layer (DI8): enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling with range normalization, reacquire-on-loss, 3s hot-plug re-enum. XInput devices are excluded via the documented RawInput IG_ VID/PID check (live-verified skipping a real XBOX360 pad -- no double-feed). bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/ deadzone; hats as 4-direction buttons with diagonal chords). PadRIO runs them through the existing channel/event machinery -- per-binding edge arrays release automatically on detach (the #24 rule), and a direct throttle axis maps full lever travel onto the 0..1 channel while the gait detent still snaps a lever parked in the walk/run dead band. BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move each control when prompted (stick/twist/throttle/fire), invert derived from the move direction, hat look cluster auto-added, output written between markers in bindings.txt with the rest of the file preserved. Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched. Verified: grammar accept/reject per line, XInput-exclusion live, 30s in-mission no-device polling clean. Awaiting a tester with real stick hardware for axis verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a8a6da6760 |
Solo mission clock: end the mission when the game length expires (port shim)
Field 2026-07-23: a 20-minute solo mission was still running at 53 minutes. In the arcade the OPERATOR CONSOLE owned the game clock and sent StopMission at zero; the engine only computes the HUD countdown (secondsRemainingInGame) -- nothing consumes it. The desktop FE-solo mode has no console, so timed solo missions ran forever. Shim at the countdown site (Application::Execute): when no console/relay owns the session (BT_RELAY unset), post the console's own StopMissionMessage when the clock runs out -- the normal end flow (EndingMission -> review -> FE relaunch) takes over. length=0 (raw solo) stays endless; console-driven sessions untouched. One-shot latch resets outside RunningMission. Awaiting live verification (a 60s-length solo egg must end itself). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a999e5c49f |
Audio-dropout fix: OpenAL source-pool lifecycle (uninit aliasing, atomic-delete leak, stranded partials)
Field report: audio cutting in and out toward the end of the match. Three structural defects in the port's source-pool lifecycle: 1. SourceSet.sources[] was uninitialized heap garbage until first acquisition -- AL names are small ints, so a recycled-heap slot could alias a LIVE source owned by another sound; alIsSource() skipped generation, two sounds shared one source, and whichever released first deleted the other's mid-play. SourceSet ctor now zero-inits. 2. ReleaseSourceSet's bulk alDeleteSources(count, sources) is ATOMIC on invalid names (AL spec) -- one empty/-1 slot and NOTHING deleted; after the first pool exhaustion every partial release leaked and the pool never recovered. Now per-slot guarded delete, slots parked at 0. 3. Failed acquisitions stranded partial sets forever (dropped transients are never released by anything). ReleaseChannels() handback at both failure sites (StartRequest + dormant-resume) + a dtor backstop. Verified: 2-node MP smoke (mission + lobby-loop relaunch clean, census live/acquireFails 0/0) + 100s solo combat smoke (52 audio triggers, zero AL errors). Awaiting live playtest confirmation at scale. KB: context/wintesla-port.md audio-dropout section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cbebb0f1c1 |
Audio source-pool census + fix the unbuilt jam-log literal
Field report (playtest night + operator): audio cutting in and out
toward the end of the match -- the classic shape of source-pool
pressure (sources release only at entity teardown; long matches
accumulate looping occupants like wreck burn/smoke until transients
lose the voice fight; OpenAL mixing capacity is finite). L4AUDRND now
tracks live/deleted/failed sources: a 30s '[audio] source census' line
plus an ALWAYS-logged line on every acquisition failure (the smoking
gun). Soak test next to confirm the leak/occupation slope.
Also: the
|
||
|
|
12a1fea823 |
Score replication: peers' scores now reach every pod (rank fix)
Field report (playtest night, voice channel): everyone's HUD showed them in 1st place. Root cause: the engine's score-replication machinery was complete -- Player::WriteUpdateRecord ships currentScore, the replicant ReadUpdateRecord applies it, CalcRanking sums every scoring player -- but nothing marked the player's update record dirty on a score change (the engine only ForceUpdates at mission end for the fade sync). Every peer's replicant player therefore stayed at score 0 and each machine ranked its own (only-nonzero) player first. Fix: ForceUpdate() after every score application in the BTPlayer handlers (main, inflicted, and the severed-vehicle branch) -- the existing update-record path does the rest. Verified 2-node (force-damage kill scoring): the victim's machine ranks the killer's REPLICANT at its replicated score (24) above its own master (0); standings consistent on both nodes. BT_RANK_LOG=1 kept as the 1 Hz per-player ranking dump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c8cba8c764 |
Connection audit: every connect/disconnect scenario handled + verified
Systematic actor-death x lifecycle-phase sweep (operator request after the WaitingForEgg zombie). New handling, all live-verified: 1. PRE-EGG console-pad loss (the real zombie phase): before the egg the pod's only link is the console pad -- the game socket doesn't exist, so the earlier RelayGameDown hook could never fire (the verification run itself caught this). Detection now lives in HostDisconnectedMessageHandler ConsoleHostType (relayMode + pre-scene -> BTRelayRejoinNow); mesh keeps 1995 behavior. 2. MID-MISSION relay loss: the STOP is relay-sent (1995: the console owned the clock), so a pod losing its relay mid-match played a peer-less FOREVER-mission. RelayGameDown (scene presented) now posts StopMissionMessage at +15s -> normal end -> lobby relaunch. 3. POST-RELEASE pod death stalled the round (survivors wait forever on the dead peer's connection gate): relay _abort_round -> route -11 REJOIN to survivors -> everyone re-seats in seconds (reset-first ordering makes the drop cascade re-trigger-proof). 4. BTRelayRejoinNow carries BT_SEAT_CLAIM (mirrored tag): without it the rejoiner's own reclaim-hold blocked assignment -> ROSTER FULL loop for the 90s window (found by verification round 2). 5. Beacon NAT keepalive (SIO_KEEPALIVE_VALS 60s/10s) so long between-rounds waits can't be silently unseated. 6. BT_LOG_APPEND=1 preserves the log across lobby-loop generations (truncation erased round-1 verification evidence). Verified non-issues: relay pods never bind the -net listener (no double-launch collision on the internet path); mesh bind-fail exits cleanly. Full matrix + evidence: context/multiplayer.md; scratchpad/audit_verify.sh is the repeatable rig. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
56ca2f5c13 |
Mission loads 72s -> 4s: the wait screen was GPU-syncing every idle frame
The operator challenged the "inevitable ~minute" load (Task Manager showed idle CPU) and was right. Measured chain: - A/B same box/egg: dev 7s, +glass 30s, +BT_DEV_GAUGES 72s+. - Drain census: identical ~560-event make streams; 190 events/s in dev vs a metronomic 7/s in the console shape -- the loop, not the events. - Stack sampling: 4/6 samples inside ExecuteIdle's wait-screen paint -- GDI GetDC/FillRect on the D3DPOOL_DEFAULT offscreen surface forces a full GPU pipeline sync (~140ms/frame at glass window size). The 2026-07-22 flicker fix traded a cosmetic bug for a 25x load throttle, worst on the operator box (bigger window + gauge-window GPU work), with idle CPU because a GPU-sync stall isn't compute. Fix (ExecuteIdle): the staging surface is D3DPOOL_SYSTEMMEM (GetDC is pure CPU; survives Reset for free) uploaded via UpdateSurface, and a change gate skips the whole idle frame unless the spinner phase (12.5Hz) or state text changed -- between paints the loop runs free. Result: console-shape load 72s+ -> 4.1s (BEGIN -> READY), drain at 345 events/s; every pod benefits (same code), and the all-ready launch gate now waits on a seconds-scale slowest loader. Permanent pre-run diagnostics kept: the drain census + >50ms slow-event lines (EVENT.cpp, stand down at scene-live; slow-event identity captured BEFORE Process -- Receiver::Receive(Event*) deletes the event). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
76eaddc4a8 |
Issue #25: composite Turn channel -- twin-stick gamepad turning
Gamepad players expect stick X = turn; the pod-faithful default maps it
to torso twist, and the pedal pair couldn't take a single signed axis.
New bindings channel "Turn": a signed composite that decomposes into
the pedal pair at the RIO publish (+ = right pedal, - = left), ADDING
to direct pedal bindings so mixed rigs compose, clamped 0..1 per pedal.
The default bindings.txt stays pod-faithful and documents the
twin-stick alternative inline:
padaxis LX axis Turn
padaxis RX axis JoystickX
Also covers the HOTAS ask (any signed axis can now drive turning).
Verified: regenerated defaults carry the doc lines; a twin-stick-edited
bindings.txt parses and boots clean. Awaiting live pad verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4e0fcbf328 |
Issue #26: runtime master volume (-/= keys, persisted)
The game audio plays hot with no in-game control -- testers couldn't hear voice chat without the Windows mixer (playtest night). The -/= keys now step the OpenAL listener gain (the master scale every source inherits) in 5% steps, clamped 0..150%, edge-detected in the per-frame poll with a foreground guard; the value persists to content\volume.cfg and reloads at boot (BT_AUDIO_VOLUME env still wins when set; default stays 0.6). README updated. Verified: volume.cfg 0.25 -> boot log "master gain=0.25"; key stepping needs a live focused session (awaiting live verification). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
44f7c39ee1 |
Issue #24: release held pad buttons on controller disconnect (look latch)
XInput button emission is edge-based against previousPadButtons; the
disconnect path zeroed that memory WITHOUT emitting release edges, so
any button held when a controller (wrapper) died stayed pressed
game-side forever -- and the zeroing also erased what a reconnect would
have released. Field signature (playtest night): a PS3-pad-in-xbox-
mode wrapper died mid-hat-hold -> the look-view machine (hold-based,
mppr) latched a side view with the authored look pitch ("stuck side
view + pitch at ground, no recovery"); the keyboard-only run was clean.
On the connected->disconnected transition PadRIO now emits
EmitButton(address, 0) for every binding whose mask was held, then
forgets. Not reproducible headless (needs a real hardware unplug
mid-hold) -- awaiting the reporter's controller for live verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8368163b04 |
Lobby loop: auto-rejoin between rounds + seat reclaim + live mission edits
The between-rounds arcade flow (first-playtest-night field request): at
mission end every pod exited, seats dropped, and every round required
everyone to re-run join.bat with no way to adjust the mission.
1. AUTO-REJOIN: StopMission (operator END / mission clock -- NOT a
window close) sets gBTMissionStoppedByConsole; after the matchlog
upload btl4main relaunches the pod into the join wait with the same
cmdline (BT_CALLSIGN/BT_MECH ride the inherited environment).
2. SEAT RECLAIM: the assigned tag is mirrored file-scope
(BTRelaySelfTag) and re-presented as the 3rd SEAT_REQUEST payload
field (BT_SEAT_CLAIM); the relay holds a dropped seat for its tag
for 90s and a returning player gets their EXACT seat back -- static
ordering across rounds, the real-pod model. Non-claim joiners skip
reclaim-held seats.
3. LIVE MISSION EDITS: the console gains "Apply mission settings"
(enabled while a session runs) writing arena/time/weather/length
into the session egg; the relay re-reads the file at round reset and
egg release (roster shape locked mid-session).
Verified 2-pod 3-round: pods self-relaunched after each clock end; the
SECOND-to-rejoin still reclaimed its original seat (ordering held); the
between-rounds edit landed ("mission length now 75s"). Known edge
noted in the KB: a stale ready flag from a not-yet-exited old conn can
satisfy the launch gate mid-rejoin -- operators launch on green dots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7b3e76db38 |
Load-time attribution: the minute is the renderers' entity-make streams
Measured (loadphase markers + queue blocker dump): renderer LoadMission 31ms, model loads 0.2s -- the teal->green minute is the LoadingMission ready-gate (IsPriorityEmpty counts timed events too) waiting out the THREE renderers' entity-make streams: every entity/subsystem creation posts message 3 to each Renderer receiver (classIDs 8/30/36 = video/audio/gauge), hundreds deep per queue, draining across the whole window (the audio renderer's share is why sound starts mid-load). Instrumentation kept: [loadphase] markers (renderer span + busy-pass counter) always on; GeneralEventQueue::DumpBlockers + the class-named DumpData fallback behind BT_LOAD_DIAG=1 (hundreds of lines per load). Optimizing the drain (batching/budget) is an open perf item -- deliberately untouched before the 8-player night. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0ef5a3c4e1 |
Load-phase markers: attribute the teal->green minute
The measured 58s uncontended load has ~0.2s of model loads -- the rest is unattributed. Stamp the renderer LoadMission span and count the CheckLoad passes that find the event queue still busy (each delays the ready transition by its 1s re-post), so the next live run names the cost. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
54fd4a4ab0 |
Timestamps for the launch-chain logs (operator request)
Every relay/console print now carries a wall-clock prefix (_StampedOut stdout wrapper; the GUI monitor regexes use search() so the prefix is transparent), and the pod's READY/pend lines carry wall time + tick -- post-mortems measure delays instead of guessing (the 3m40s load reconstruction needed heartbeat-counting archaeology). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9bec4b2050 |
READY light: pods report load-complete to the operator roster
The roster showed "registered" for a still-loading pod and a ready one alike -- with multi-minute contended loads the operator couldn't tell who they'd be waiting on. When the app ladder reaches WaitingForLaunch the pod sends one empty route -10 frame on its live relay game socket (BTRelayNotifyReady; socket mirrored at HELLO); the relay announces SEAT n READY (k/m loaded) and the console GUI lights the seat bright-green with a "N pod(s) LOADED+READY" banner. Verified LIVE by the operator: blue (seated) -> teal (registered) -> green (ready) in order, twice. Also resolved the load-speed mystery: loads are ~30s uncontended; the 1-2+ minute cases were assistant background test runs competing with the user's interactive sessions on the same machine (memory note added -- no game-spawning tests while the user is testing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
53c86fa1dd |
Launch pend counter: no timeout, no queue entry (mid-load launch crash 2)
The take-1 deferral (re-post at +1s, capped at 120) turned a SLOW load into a crash: loads on a busy operator box measure 1-2+ minutes, the cap expired, and the original "Not ready to run" Fail/abort fired (field report: sound after ~1 min, crash ~2 min later; the log shows exactly 120 deferrals all in LoadingMission). A mid-load RunMission now just bumps gBTRunMissionPendCount -- nothing enters the event queue and there is NO timeout to outlive -- and CheckLoadMessageHandler drains the counter immediately after advancing LoadingMission -> WaitingForLaunch, re-posting that many stack RunMissionMessages (the engine's own no-console launch pattern at the same site, which also demonstrates Post spools stack messages safely). Verified (two-round rig, round 1): both relay launches pended through the load, fired at load completion, the requested mech spawned and the scene went live. Round reset + round-2 rejoin remain verified from the prior run; why loads take 1-2+ min on a busy box is left as an open perf question. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3a9b35fc06 |
Round reset + device-creation retry + load-time pump (rejoin trilogy)
1. ROUND RESET (the "picked Hellbringer, got a Blackhawk" field bug): trim/prefs finalize the session egg for ONE round, and that was permanent for the relay's lifetime -- a rejoin after a mission or crash got the stale finalized egg and its new callsign/mech request was recorded but never applied. _maybe_reset_round() (both drop paths) restores the original egg/roster/launch state once every pod is gone; live beacons re-key by tag position (conn.seat_tag). Verified two-round e2e: blkhawk mission -> exit -> RESET -> lok1 request -> egg rewritten -> the Hellbringer spawned. 2. DEVICE-CREATION RETRY: CreateDevice(HARDWARE_VERTEXPROCESSING) fails transiently when a just-exited instance still holds the adapter (caught live; also the parked "exit 139" join crash). The old double-failure path called PostQuitMessage(1) -- which does NOT stop execution -- and the NULL mDevice deref right after was the real crash. Now: HW->SW retry with 500ms backoff and a 20s deadline, then a clean error box + ExitProcess. 3. LOAD-TIME PUMP: mission load is thousands of back-to-back synchronous model loads; the starved message pump ghosted the window "Not Responding" mid-load. BTLoadPump() (hooked from d3d_OBJECT::LoadObject) pumps + repaints the wait screen at most every 150ms pre-run; no-op once the scene is live. Also: round-reset originals captured after the roster parse (the first cut crashed the relay constructor -- caught by the two-round test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
638cfca18d |
Defer RunMission until the mission load is ready (silent launch crash)
The relay fires the RunMission pair a fixed 20s after the egg ACK -- but the ACK happens at egg RECEIPT, and a long mission load (glass + dev gauges on a busy box measured ~50s) is still in LoadingMission when the launch lands. L4Application::RunMissionMessageHandler's default case Fail()ed on that state -- an abort() the 1995 code could afford because the real console launched operator-paced -- which presented as a SILENT crash at mission start (c0000409, no WER record; cdb stack: ucrtbase!abort <- RunMissionMessageHandler <- RoutePacket; 3/3 reproducible; matches the field report's 464 crash that died mid-LOD-load). The default case now DEFERS: re-post the message to ourselves at +1s (the VehicleDead re-post pattern) until the ladder reaches WaitingForLaunch, bounded at 120 deferrals so a truly wedged load still surfaces the original Fail. Verified: the crashing console-shaped run deferred 104x through its ~50s load, then launched, rendered (scene LIVE) and ran clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
66f22fb923 |
Wait screen: hard cutoff at first real scene present (handoff flicker)
The launch handoff (LoadingMission -> RunningMission) could interleave the idle wait-overlay presents with the first real game frames (user report: "the wait screen is still there when game graphics come up"). gBTSceneHasPresented latches at the render path's Present; ExecuteIdle stands down permanently once set -- the overlay can never paint over (or alternate with) live graphics. Permanent one-shot/1Hz [waitscreen] diagnostics record the active paint path, the scene-live moment, and idle paints. Verified: 70-frame 0.5s capture across the handoff -- 54 wait frames, then game frames with at most ONE ambiguous frame at the cut (was repeated interleaving); no black frames. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c421ec4af8 |
Wait screen: paint into the backbuffer, not after Present (load flicker)
The pre-mission overlay painted the window DC AFTER ExecuteIdle's Present -- every cycle flashed the black D3D frame before the GDI redraw, visible as flicker while the game loads (user report). The overlay now lands IN the presented frame: the backbuffer refuses GetDC on this driver (D3DERR_INVALIDCALL without LOCKABLE_BACKBUFFER, not worth forcing game-wide), so the text+spinner GDI-paint goes into a cached offscreen plain surface (GetDC always legal there) and StretchRects onto the backbuffer before Present. D3DPOOL_DEFAULT cache released at every device-Reset site; window-DC path remains for the pre-device seat wait and as the fallback; one-shot [waitscreen] boot log says which path is live. Verified: 14 rapid PrintWindow captures of the waiting state all carry the text (pre-fix runs alternated black frames). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
10eee05b20 |
Launch with whoever connects + live roster display in the console
The operator no longer has to match the roster row count to the exact player count (or watch the "empty seats will STALL" warning). Two mechanisms: 1. PRESENCE BEACONS: the pod keeps its seat-request TCP connection open for the process lifetime; the relay reads a live beacon as "seated" and a pre-claim FIN frees the seat (no ghost seats -- a ghost stalls every pod at the connection gate). Reservation expiry spares beaconed seats. 2. TRIM-ON-LAUNCH (manual/GUI mode): operator LAUNCH before the roster fills shrinks the session to the seats present -- egg [pilots]/pages trimmed, seat ids REMAPPED by position (pods re-derive their host id from their tag's roster position, so walk-up prefs and beacons re-key consistently), roster/expected_ids shrink, eggs release, the launch pair fires as ACKs land. GUI: roster rows now update LIVE from the walk-up requests (SEAT n PRESENT/FREED lines -> callsign/mech cells + a blue "seated" status light), and LAUNCH MISSION enables from the first seated player with a "starts with whoever is here" banner. Verified 2-node: 2-of-4 trim ran the mission with the requested mechs; and the mid-roster GAP case -- three joined, the middle player quit pre-launch (beacon FIN freed the seat), trim remapped seat 4->3 with the survivor's callsign/mech following, both survivors registered under the remapped ids and ran the mission. Also: stdin reader guarded against sys.stdin=None spawn shapes (the GUI QProcess pipe is the supported operator channel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fcedc046cf |
Walk-up callsign/mech request: join menu -> relay -> the session egg
The 1995 front-desk conversation, over the internet. join.bat/
join_lan.bat now open a JOIN-GAME menu (the FE in BT_FE_JOIN trim:
CALLSIGN edit + the 18-mech list + JOIN); the choice relaunches into the
normal join with BT_CALLSIGN/BT_MECH env, rides the relay SEAT_REQUEST
as {callsign NUL mech NUL} (empty payload = roster defaults, wire-
compatible), and the relay validates the mech tag, HOLDS egg delivery
until every pod's console pad is connected, rewrites the egg via
eggmodel (vehicle= + rasterized callsign bitmaps, graceful no-PySide6
fallback) and streams it to all pads -- so every player's egg copy
carries every player's callsign.
The hold is gated on CONSOLE-PAD count, not game-side registration: a
pod HELLOs only after parsing the egg's roster, so a registration gate
deadlocks (hit live in the first run; pods animate in WAITING FOR
MISSION ASSIGNMENT during the hold).
Verified 2-node e2e (env-injected requests): thor/vulture requested over
roster defaults bhk1/ava1 -> relay held 1/2, released at 2/2, rewrote
both seats (relay log), the delivered egg carries vehicle=thor/vulture +
name=VIPER/MONGOOSE, both pods launched and built [cyl] tables for both
requested mechs. Join-menu layout screenshot-verified; the menu's
click-through relaunch awaits live human verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
753540de96 |
MP match forensics: per-peer matchlog + relay auto-upload + matchcheck
For the 8-player playtest. Damage authority is VICTIM-side, so no single peer sees the whole match: every -net instance now writes a compact machine-parseable matchlog_<date>_<time>_<pid>.txt (auto-armed by -net; BT_MATCHLOG=1/0 overrides; solo silent), and at mission end a relay-mode pod dials the relay game port (the seat-request throwaway-dial pattern, envelope route -9) and streams the file back -- the relay saves every peer's copy under matchlogs/ on the operator's machine, so testers send nothing by hand. tools/matchcheck.py <dir> reconciles all of them: damage claimed (FIRE/PROJ/SPLASH/RAM, shooter-side) vs applied (DMG, victim-side authoritative), kill/death replication across observers, PLAYER_DEAD counts, scores, version skew, replicant-application and unattributed-damage anomalies, mid-mission disconnects. Hooks at the authoritative sites: Mech::TakeDamageMessageHandler (DMG, post-base, zone + post-application damageLevel), MechWeapon:: SendDamageMessage (FIRE), projectile impact/splash/collision dispatch (PROJ/SPLASH/RAM), wreck entry (DEATH -- a ONE-SHOT latch at MovementMode 9, NOT the transition: a replicant arrives at 9 straight from the type-6 record header and never runs the transition branch), BTPlayer vehicle/death/score handlers (VEHICLE/PLAYER_DEAD/SCORE), zone crit cascade (CRIT), APP.cpp RunningMission (MISSION), L4NET host handlers (PEER_UP/PEER_DOWN). Every line t=/w=/st=-stamped + fflushed. Verified 2-node: mesh run pairs A's 22 FIRE 1:1 with B's 22 DMG (same amounts, cylinder-resolved zones, ~200ms latency); force-damage kill run logs 103 DMG + 3 DEATH inst=M + 3 PLAYER_DEAD across three respawn cycles victim-side and the kill SCOREs shooter-side; relay-mode run auto-uploaded BOTH peers' files at the mission-clock stop and matchcheck produced the full report with exactly one (correct) anomaly -- the force hook's claim-less damage, the unattributed-damage detector working. Relay gained a route-specific 8MB cap for the upload frame (game frames stay capped at 1600). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f0918c66b0 |
Join wait: animate through EVERY pre-mission state
Two follow-ups to the waiting-screen work, both user-reported:
1. "Animation frozen after connecting": once the console seats the player
and sends the egg, the state advances LoadingMission -> WaitingForLaunch
-- where NOTHING presents until the operator's RunMission, so the last
idle frame froze on screen with the mission audio already running.
ExecuteForeground now keeps the idle clear + overlay alive through
LoadingMission/WaitingForLaunch/LaunchingMission (no early return -- the
load/replication work continues), and ExecuteIdle picks per-state text:
WaitingForEgg "WAITING FOR MISSION ASSIGNMENT"
Loading/Launching "LOADING MISSION / stand by"
WaitingForLaunch "MISSION READY / waiting for the operator to launch"
2. The roster-full retry pause was a raw Sleep(3000) -- with the relay up
and answering SEAT_FULL instantly, the process spent ~95% of its time
unpumped (IsHungAppWindow TRUE, spinner frozen). Now 30 pumped 100ms
slices with the reason on screen ("session is FULL -- waiting for a
seat to free up").
Verified live against the running operator relay: roster-full wait shows
the reason text, spinner advances across 500ms captures, IsHungAppWindow
false on every sample.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a1c9a71898 |
Join wait: non-blocking dial -- the spinner no longer stutters
User-reported after the waiting-screen fix: "the animation keeps hanging." Cause: RelayDialTcp used a BLOCKING connect() -- against a down/unreachable operator each attempt froze the window thread for the OS SYN-retry window (seconds), so the spinner ran only during the 2s sleep between attempts. RelayDialTcp now connects NON-blocking and select()s in 100ms slices, pumping the message loop + repainting the waiting screen every slice (both in-flight and between retry attempts). The success path already returned a non-blocking socket (FIONBIO), so caller semantics are unchanged; the other dial sites (console dial, game-socket dial) inherit the smooth behavior. Verified: 4 window captures at 400ms intervals during a dead-host dial all differ in the spinner region (uniform diff energy == continuous animation) and IsHungAppWindow reads false on every sample. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f5e0ffcd14 |
Join wait: animated waiting screen instead of "Not Responding"
The relay seat-wait loop (RelayRequestSeat -- the join/join_lan "patient
walk-up") blocked the window thread's message pump for its whole 30-minute
patience window: dial (2s timeout) -> Sleep(2000) -> repeat, with status
going only to the parent bat console. Windows flagged the frozen game
window "Not Responding" until the operator started the session
(user-reported).
- BTWaitScreenPaint (L4VIDEO.cpp): a GDI waiting screen on the main window
-- headline + detail line in the console green + a 12-segment marching
spinner phased on the tick clock. Pure GDI: flicker-free during the
seat wait (no D3D presenting yet), and re-painted after each
ExecuteIdle Present so the WaitingForEgg phase (previously a bare black
frame) shows "WAITING FOR MISSION ASSIGNMENT" too.
- RelayWaitTick (L4NET.CPP): PeekMessage pump + paint. The outer dial
loop's Sleep(2000) becomes 20 pumped 100ms slices ("WAITING FOR THE
OPERATOR'S SESSION" / "session at <ip:port> -- close this window to
cancel"); the inner seat-reply wait pumps per 200ms select slice.
Verified live against a dead relay address: window-only PrintWindow capture
shows the animated screen, and user32 IsHungAppWindow (the API behind "Not
Responding") reports FALSE throughout the wait.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4215b98655 |
Trigger config: root-cause the dead cockpit clicks (missing BT_PLATFORM=glass)
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>
|
||
|
|
9f0a7c52df |
Radar: drop the blip when a mech dies (restore the authentic interest-removal)
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> |
||
|
|
7052d9e5bb |
Cockpit: pull the MFD red lamps fully outside the screens; drop the flight blocks clear
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> |
||
|
|
129b27499d |
Cockpit surround: composite the gauges around the 3D view (default desktop layout)
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> |
||
|
|
5ba5697a08 |
Fix glass panel dead-button crash: null-init MessageHandlerSet gap slots (Gitea #18)
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> |
||
|
|
bc5ac3a0db |
Merge glass-per-display-windows: per-display cockpit windows (Cyd, BT_GLASS_PANELS)
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 |
||
|
|
70eea6c1a4 |
Boresight parallax FIXED + the #4/#5 verdict instrumentation (Gitea #16, #4, #5)
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> |
||
|
|
a0cec48e3f |
Death-crash FIXED: 25-voice explosion preset overflowed the 5-slot audio SourceSet (Gitea #12)
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> |
||
|
|
f338595685 |
Glass cockpit: per-display windows (BT_GLASS_PANELS)
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> |
||
|
|
6ad1e074cc |
Glass lever: port the pod-build GAIT DETENT to the PadRIO throttle slew
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>
|
||
|
|
5dd35365c7 |
Glass input audit: RIO mapper id fix (panel banks live), keyboard reconciliation
Root cause of the dead on-screen panel: MechRIOMapper's message-id enum
chained off L4MechControlsMapper::NextMessageID (=0x19), registering all 18
RIO override handlers at 0x19-0x2a while the streamed .CTL EventMapping
records carry the binary's ids -- the binary RIO table @0051dd30 re-registers
the BASE aux/zoom ids 3..0x13 (Hotbox 0x1a @0051de98). Every MFD-bank/zoom
press hit the base ConfigureMappableMessageHandler FAIL trap (26/72 buttons
dead; an abort() on a trap-armed pod). Ids now pinned to the binary
numbering + static_assert-locked (btl4mppr.hpp) -- panel goes 26 -> 52
working buttons (8 await handler reconstruction, filed on Gitea; 12 have no
streamed mapping authored = authentically inert).
Keyboard reconciliation (glass-only): the merged map -- keyboard hosts the
~20 core gameplay actions on the CONTROLS.MAP keys, the panel covers every
pod address by click (right-click = hold latch):
- W/S throttle lever, A/D pedals, Q/E twist, R/F elevation, X all-stop,
arrows drive; numpad flight cluster kept (Cyd)
- 1/2/3/4/Space/LCTRL/RCTRL fire, LALT reverse, B look-behind (0x41)
- M=0x18 mode cycle, N=0x15 display cycle, H=0x2C coolant flush (hold),
C=0x2F Condenser1 valve, G=0x0E weapon-1 configure (Mfd1-Quad-gated)
- F5-F9 = Mfd2 bank 0x27-0x24 + 0x22 (page-gated gen A-D / gen mode)
- hardcoded: ` or V = view toggle, J/K/L = Mfd1/2/3 preset-page cycle
(PadRIO edges -> gBTPresetCycle; no pod "cycle" button exists)
- PadRIO::SuppressKey: bound keys are swallowed from the typed 1995 channel
(the btinput suppression pattern, glass side) -- ends the glass double
dispatch ('w'=pilot select 0, 'a'..'g'=MFD2 presets, NUMPAD/F-key key-up
VK aliases like VK_F5=0x74='t'). Unbound keys keep their authentic
meanings (5/z presets, t-o pilot select, +/- zoom, F1/F2 align).
DISPLACED from Cyd's default bindings (each reachable on the panel or by a
bindings.txt edit -- flagging for Cyd's veto): number row -> secondary panel
(1-4 now fire), QWERTY row -> pilot keypad (dropped), arrows -> hat looks
(now drive), V/C/B -> fire 0x47/0x46/0x45 (now view/valve/look-behind),
LCTRL -> throttle-down (now fire). XInput pad section untouched.
Audit by-products (KB updated): the always-active msg-4 records identified
(0x2C = Reservoir flush, 0x29-0x2F = condenser valves, 0x1A-0x1D =
GeneratorA-D ToggleGeneratorOnOff @004b1ed0 unreconstructed); 0x13 = Mech
DuckRequest (crouch), 0x28 = BalanceCoolant; Searchlight/ThermalSight
ToggleLamp handler-sets are default-constructed EMPTY; weapon Eng-page msgs
0x3/0xb = ToggleCooling / ToggleSeekVoltage / EjectAmmo, all unwired;
unhandled messages are SILENT (no [FAIL] -- census = BT_CTRLMAP_LOG dump x
handler tables).
Verified live (build-glass2, ARENA1): panel presets/zoom/display/flush;
W drive (speedDemand 61.5, gait advances), Q turn (BAS), M -> MID, A turn
(MID), Space fires, N display, J/K/L presets, H flush drain, C valve 1->5;
suppression ('w' swallowed, 't' flows, F5-keyup swallowed); BT_SHOT frame.
Un-regression: pod build (gates OFF) compiles 0 errors, forced-walk drives,
streamed ids unchanged; CONTROLS.MAP/btinput untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|