9df7dff78767ffa62d8b3559afb36d8ef529f070
170
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
12d6862c1f |
keylight: unship it -- opt-in (BT_KEYLIGHT=1), gone from all player docs
Operator decision: the player base is not on Windows 11, so the RGB keyboard
mirror should not be shipped or promised. The feature is NOT deleted -- Cyd's
implementation stays intact behind the gate for whoever wants it -- it is
simply off by default and invisible:
* PadRIO gate flips from opt-out (BT_KEYLIGHT=0 disables) to OPT-IN
(BT_KEYLIGHT=1 enables). Default boots produce zero keylight lines, and a
stub build and a real build now behave identically for players: nothing.
* Removed from every user-facing artifact: the README paragraph, the
CONTROLS.md section, the CONTROLS.html footer line.
* environ.ini template: the entry stays (it is the discoverable home for the
opt-in) but now says OFF-unless-1, Windows 11 22H2+, and why it is
unshipped.
* mkdist's cut-time stub warning removed -- it existed to protect a README
promise that no longer exists.
* context/glass-cockpit.md records the decision, the opt-in, and the
build-time SDK stub gate in one place.
VERIFIED: default boot logs zero keylight lines; BT_KEYLIGHT=1 engages the
feature (the stub on this machine: its one honest log line + the key map).
All three console suites pass; checkctx CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8edce41f88 |
bindings: one-time migration to the board-2 layout -- the two-population fork is gone
Operator decision: "make this version devour their custom settings and re-output
the correct binding file with the updates -- I'd like to eliminate competing
code paths." Before this, the preserved-bindings.txt convention meant upgraders
stayed frozen on whatever layout their file was written under, forever: two
player populations, two sets of true documentation, and every future default
improvement reaching only fresh installs.
THE MIGRATION (PadBindingProfile::MigrateBindingsFile, run once at load):
* A file carrying the new "# bindings-board 2" marker is left alone -- the
check is the first thing that runs, so this is a no-op forever after.
* Otherwise: every row matching the UNION OF EVERY DEFAULT SET EVER SHIPPED
(85 canonical rows, harvested from c2ee729..1cda880 and embedded as a
table; comments stripped, whitespace collapsed, uppercased) is dropped --
those rows were never a player's choice, they were just the old board.
* Rows the player actually authored are CARRIED with their original text and
comments, and WIN over the new defaults: the losing default row is emitted
commented out as "# (yours wins) ...", keyed by the control ("KEY T",
"PAD A", "PADAXIS LX") so key rows only displace key rows.
* The joystick wizard's marker-delimited section is preserved VERBATIM (the
same markers L4JOY rewrites between), so HOTAS players migrate without
re-running the wizard.
* The previous file is saved as bindings.old.txt first. Restoring it over
bindings.txt just re-migrates next boot -- deliberate: the old WORLD is not
restorable, only the player's own rows persist. That is the point.
VERIFIED end-to-end with the real exe (three planted scenarios):
A. old-world file (8 old-default rows + 2 authored rows incl. a rate tweak +
a wizard section): both authored rows carried with comments, both
conflicting new defaults stood down with "(yours wins)", wizard section
byte-preserved, backup written, log line states all counts, and the
migrated file parses with ZERO malformed lines.
B. already-migrated file: byte-identical md5 after a full boot, no migration
line, no backup touched. (First attempt at this check clobbered its own
evidence by running case C first -- re-run properly.)
C. no file: fresh defaults now carry the marker as line 1, no migration.
The operator's real bindings.txt was stashed before testing and restored
untouched; it will migrate on their own next launch, as intended.
Docs flipped to the new reality: the README's "your keys DO NOT change"
upgrade promise is replaced by the migration story (customs + stick kept, old
stock keys replaced, bindings.old.txt escape hatch); CONTROLS.md gains the same
note; context/glass-cockpit.md records the mechanism, the 85-row table's
provenance, and why restoring the backup re-migrates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
150961176c |
merge fix-ups: the four review findings + a build portability gate the merge exposed
Applied on top of the glass-cockpit-refit merge, all from the pre-merge review: 1. README CONTROLS TABLE REWRITTEN for the new default board. It still taught "W/S throttle, A/D turn" while the defaults moved flight to the numpad and handed the letter rows to the MFD banks -- a fresh install would read controls that do not work. The new table teaches the numpad flight block, the panel-on-your-keys groups, F7 for control mode (was M), backtick for the camera (was V), the coolant valves on 1-3/QWE with the detent warning kept, and PgUp/PgDn volume. UPGRADERS ARE TOLD EXPLICITLY that their preserved bindings.txt keeps their old keys, and that deleting it opts into the board. 2. BACKTICK VIEW-TOGGLE GUARDED. The bindings-row-wins rule (KeyHasBinding) covered V and J/K/L but not backtick itself, which polled unconditionally -- and BACKTICK is a nameable, unbound key, so binding it to a button double-dispatched (button + camera). Both halves of the view poll are now guarded. 3. ENVIRON.INI REFUSES ONE-SHOTS. The loader applied ANY key=value line, and BT_JOYCONFIG in the file would defeat the #66 one-shot in a sneaky way: the wizard DELETES the process var after running, so in every relaunched child there is no real env var left to win over the file -- capture wizard at every mission start. BT_JOYCONFIG is now skipped with the reason in the template header, which also warns that test/debug hooks (BT_MP_FORCE_DMG and friends) do not belong in a file that silently persists forever. 4. VOLUME DOCUMENTED EVERYWHERE IT WAS MISSING. CONTROLS.md gains the PgUp/PgDn row (the old "- / = (see below)" cell pointed at a note that never mentioned volume) and the note that the keys moved so they no longer share a key with anything in any layout; the bindings template header reserves PgUp/PgDn informally. 5. L4KEYLIGHT BUILD GATE (found by building the merge on this machine). The C++/WinRT Dynamic Lighting TU does not compile against SDK 10.0.19041 -- its bundled cppwinrt fails inside winrt/base itself (C2039 'wait_for'). CMake now detects an SDK older than 10.0.22000 and builds a dormant stub with the same five-function scalar interface (logs once at Start, identical behaviour otherwise) -- extending the feature's own runtime philosophy ("no Dynamic Lighting -> log once, stay dormant") to build time. Dynamic Lighting is a Windows 11 22H2 feature, so nothing real is lost on an older build machine, and machines with a newer SDK build the real mirror unchanged. VERIFIED on the merged tree (4.11.572): * build clean -- 0 compile errors; the 40 /FORCE-tolerated unresolved externals are byte-identical to every pre-merge build (20 CreateStreamedSubsystem + 20 DefaultData; the earlier claim that all 40 were CSS was shorthand -- corrected here for the record). * solo boot: environ.ini template written + loaded, mode resolver announces the surround, historical bands L276/R276/T223/B336 confirmed, keylight stub logs its dormant line, BT_SHOT capture shows the under-glass button treatment and the corrected hat labels, 0 faults. * PgUp/PgDn volume PROVEN LIVE via injected keys: 5%->10%->15%->10%, saved to volume.cfg. (Side catch: content\volume.cfg had been sitting at 0.00 from an old test -- anyone using this tree had a silent game; reset.) * full relay cycle on the merged exe: 2 pods staged, launch pair, mission, StopMission, round RESET -- all clean. * all three console suites still pass; checkctx CLEAN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9d536294e4 |
Mouse buttons are bindable; mouse LOOK scoped (not built)
BUTTONS (shipped). Win32 hands mouse buttons out as virtual keys, and the
binding path already stores VKs and polls GetAsyncKeyState -- so this needed no
verb, no parser change and no new machinery, just names:
key MOUSE4 button 0x40 # side buttons
key MOUSE5 button 0x46
key MOUSEMIDDLE button 0x41
MOUSELEFT/MOUSERIGHT are named too, with a warning in the file header and the
docs: they are how the player presses cockpit buttons (left = press, right =
latch), and the poll is focus-guarded but not click-aware, so binding either
ALSO fires on every panel click. The side and middle buttons are the free
ones. Axes work as well (`key MOUSE4 axis Throttle slew + 0.7`).
One guard: the RGB lamp mirror skips mouse VKs when building its map -- a
keyboard lamp array has no mouse buttons to paint (BTPadBindingIsMouseKey).
Verified live: a bindings.txt carrying two mouse rows loads 76 keys (74 + 2),
and a real injected XBUTTON1 press produced `[emitter] FIRED #1`. Clean A/B --
the earlier run whose SendInput failed the struct-size check logged no fire at
all. Shipped commented-out examples in the default profile so the capability
is discoverable.
LOOK (scoped only). docs/MOUSELOOK_PLAN.md. Mouse MOVEMENT is a real feature,
not a table entry, and the hard part is not reading the device:
- The cursor is already spoken for. The glass cockpit's premise is that all
72 buttons are mouse targets; mouse-look wants a captured, hidden, recentred
cursor. Both cannot be true, so this is a MODE question first -- four
models compared, recommending hold-to-look (costs nothing when unbound,
cannot strand a player, behaves the same in surround and exploded layouts).
- The channels are POSITIONAL (JoystickX/Y are -1..1 deflections the mapper
reads per control mode); a mouse gives deltas. Accumulate-into-a-virtual-
stick reuses every existing rule; a rate model would need new mapper
semantics.
- Control mode changes what it MEANS: BASIC steers with the stick, MID/ADV
twist the torso -- so mouse-look steers in one and aims in the other.
Proposed grammar (new row type, so old builds skip it with a warning),
where the code goes, ~250 lines, and a verification plan whose load-bearing
item is "panel clicks still work when a mouselook binding exists but is not
engaged". Also flagged: the pod had NO mouse, so nothing here is
reconstruction -- every choice is design, judged on feel, and it must stay out
of the pod build's path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5f88a4eeb2 |
The keyboard becomes the button board (RP412 bindings port) + a controls page
Ported RP412's bindings design and made it the DEFAULT: the letter and number rows are the MFD button banks laid out WHERE THEY SIT ON THE PANEL, flight moves to the numpad so the board stays free, F1-F12 are the map's two columns, and G/B stay unbound as the physical gap between the lower clusters. Expressed in BT's OWN grammar (slew/deflect/set, the Turn channel) -- bindings.txt is a documented compatibility surface and was not touched. Coverage: 61 of 72 addresses on the keyboard, and the 11 absent ones are exactly those the pod never wired (0x16/0x17/0x1E/0x1F column gaps, 0x38-0x3E intercom/door). All 72 stay clickable on the panel. It lands on BT's addresses unreasonably well: 1-4 + QWER are the ENTIRE coolant system (Condensers 1-6, flush, balance), F6/F7 the display and control-mode cycles, F9-F12 Generators A-D, F4 the crouch button reconstructed earlier today. THE DELIBERATE COST. A bound key is removed from the authentic 1995 typed-hotkey channel so it cannot double-dispatch, and this board binds nearly everything -- so 5 (Quad page), z (Eng1), t/y/u/i/o (pilot select) and +/- (target zoom) are given up. Unbind a key to get its 1995 meaning back. Documented in the file header, the markdown and the page. NEW RULE: A BINDINGS ROW WINS OVER A BUILT-IN CONVENIENCE KEY (PadRIO::KeyHasBinding). V and J/K/L are board buttons now, and those polls read GetAsyncKeyState directly -- without this they would have fired BOTH the button and the built-in. View toggle lives on BACKTICK alone. CONTROLS.MAP rewritten to mirror the board so glass/pod/dev still feel identical (the 2026-07-21 settlement): 90 bindings, 0 parse complaints. HAT LABELS CORRECTED [T1]. INPUT_PATH_AUDIT flagged 0x41-0x44 and was right. Settled from the streamed mapping (BT_CTRLMAP_LOG): elem 66 -> subsys 17 attrID 14, i.e. 0x42 is the TORSO subsystem, not a look; 0x44/0x43/0x41 are the mapper's LookLeft/LookRight/LookBehind (attrID 10/11/12). The .RES has no "TORSO CENTER" string -- its names are LookBehind/Down/Forward/Left/Right -- so the audit's wording was loose but its substance correct. Swept L4GLASSWIN, L4PADPANEL and L4VB16. docs/CONTROLS.html: interactive keyboard (hover a key for its address and meaning, MFD clusters outlined), pad diagram, panel map, the under-glass press-target figure, radar placement thumbnails and the environ.ini table -- modelled on RP412's page, rewritten for BT's addresses and hazards. mkdist wraps the fragment in a doctype/charset shell and ships it beside CONTROLS.txt. Verified: bindings.txt regenerates and loads (74 keys, 0 errors); CONTROLS.MAP 90 bindings, 0 errors; 72/72 panel addresses still dispatch; surround / exploded / dock / pod / dev boot and simulate with zero faults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
56d9971b0e |
-fit: pin the backbuffer to the CANVAS, not the client (the display was wrong)
Field report with a screenshot: `btl4.exe -fit` renders tiny MFDs hugging the corners, an over-wide world view, and a horizontally squashed scene. CAUSE. L4VIDEO pins the windowed backbuffer to the CLIENT RECT (Gitea #56, so a device Reset cannot silently re-derive it). That is correct precisely while client == canvas -- true on a normal boot, and FALSE under -fit, which sizes the window to the whole monitor BEFORE the device exists. The backbuffer came out 3440x1440 against a 1452x1059 canvas, and everything downstream disagreed: - BTApplyWorldViewport and BTDrawCockpitPanels lay out from the BACKBUFFER, so the canvas they drew was 3440 wide: bands unchanged, MFDs still 320x240 (hence tiny), view rect 2888x881 instead of 900x500; - the world projection used the aspect of the INTENDED view (1.8) inside a 3.28 viewport, stretching the scene; - Present then scaled that oversized backbuffer into the 1974x1440 letterbox, squashing it horizontally by 0.574; - and every button was hit-tested through gBTCockpitCanvasW/H while being drawn in backbuffer space -- the exact #50 divergence the pinning comment warns about, unnoticed because nothing had clicked under -fit. FIX: when the cockpit surround is up, the backbuffer IS the canvas, whatever the window is doing. Pin to gBTCockpitCanvasW/H and let the letterbox do the scaling -- which is what the rest of the design already assumed. Drag-resize was always right for this reason (the device is created while client == canvas and a later resize does not recreate it); only -fit, which changes the client first, exposed it. MY EARLIER VERIFICATION WAS WRONG. I called -fit good from a screenshot that was centred and letterboxed, without checking that the backbuffer matched the canvas or that a single button could be clicked. Both are checked now. Verified: -fit on 3440x1440 renders correct proportions (captured), and 72/72 buttons dispatch under -fit; surround/exploded/dock/pod/dev all boot and simulate with zero faults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
02b1b50e45 |
RGB keyboard lamp mirror + the shipped controls reference
KEYBOARD LAMP MIRROR (L4KEYLIGHT). Ported from RP412, itself a port of vRIO's KeyboardLampMirror. Keys bound to a lamp address in bindings.txt glow with the panel palette through Windows Dynamic Lighting -- yellow for the map's side columns (0x10-0x1F), red for the rest -- flashing in step with the on-screen buttons (its LampLevel copy matches the FIXED BTLampBrightnessOf). Per-key boards light each bound key; zone-lit boards mirror the strongest lamp board-wide. All WinRT runs on a private worker thread: watcher, claim, and a 100 ms paint loop that repaints only on change. RP412's packing hazard does NOT transfer: it forces default struct packing there because its engine is /Zp1, which would break the WinRT ABI. BT411's BT_OPTS carries no /Zp, so only the dialect flags are needed -- /std:c++17 /permissive- per-file, since the project otherwise builds C++14 /permissive. The scalars-only interface is kept anyway so the isolation survives if packing is ever added. Wired in PadRIO: map built from bindings.keyBindings (ActionButton binds only, first-binding-wins per key), fed from PadRIO::SetLamp, stopped in the dtor (which hands the LEDs back to Windows). BT_KEYLIGHT=0 opts out; a machine without Dynamic Lighting logs once and stays dormant. Verified live: claimed this machine's 24-zone keyboard and mirrors 25 bound keys; BT_KEYLIGHT=0 and the pod profile produce zero keylight lines and run clean. SHIPPED CONTROLS REFERENCE. docs/CONTROLS.md carries the keyboard table plus the full 72-BUTTON POD MAP, grouped by the display each bank surrounds, with the coolant-valve detent warning (1-5-50-CLOSED, one press past max shuts the loop) and the jam/eject procedure. mkdist flattens it to ASCII as CONTROLS.txt at the zip root, the README's own idiom. players/README.txt gained pointers to it, to environ.ini, to -fit, and to the RGB mirror. KB: context/glass-cockpit.md and docs/GLASS_COCKPIT.md updated for the whole branch -- layout modes, L4RIOBANK and the under-glass rule, the letterbox fit and its ordering trap, player-tunable displays, the measured legend grid, the closed dead-button backlog, and this mirror. Verified: five-mode regression (surround/exploded/dock/pod/dev) boots and simulates with zero faults; mkdist writes a 5149-byte pure-ASCII CONTROLS.txt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
61563c9efe |
Glass cockpit refit: one mode resolver, one button geometry, and a cockpit that scales
Replicates the RP412 cockpit line into BT411's own architecture -- porting the geometry and keeping our renderers, so BT_SHOT single-frame verification stays intact. MODE RESOLVER. Where the secondary displays go was decided in TWO places with duplicated precedence, and the boot banner read NEITHER -- it announced "per-display cockpit windows" for every glass boot, surround included. The split had also broken BT_COCKPIT=0: documented as the dock-bottom opt-out, the profile block converted it to BT_GLASS_PANELS=1, so the docked strip was UNREACHABLE under the glass profile. One resolver now, consumed by the banner, the pad-panel decision and the window sizing; BT_GLASS_PANELS is explicit-only; dock/window modes auto-raise BT_PAD_PANEL so the button field always has a home. L4RIOBANK -- one button geometry. Both renderers carried their own copy and had drifted: an MFD button was 156x138 reaching under the glass in the exploded window and a 76x24 sliver entirely OUTSIDE the glass in the surround. One module owns it now, both are consumers, placement stays per-renderer. The pod's under-glass rule (RP412 L4MFDVIEW): reach half the glass in behind the display, leave a lamp strip clearing the edge, paint buttons first and imagery over -- so the lamp reads as a bar and practically the whole display is the press target. The strip scales off the display's SHORT axis (the map is portrait) with a readable floor. Retired L4GLASSWIN's three local placers and seven layout constants. LAMP FLASH DECODE was wrong [T1]. BTLampBrightnessOf returned max(state1, state2) and blanked on the alternate phase; RIO::LampState (L4RIO.h [T0]) says solid shows state 1 and flashing ALTERNATES the two. Agrees only when one state is Off -- true for the Panic lamp, which is why it survived -- but L4LAMP.cpp:252 commands flashFast + state1Dim + state2Bright, a dim->bright pulse that rendered as a hard bright->off blink. Three copies existed (l4vb16.h, L4GLASSWIN, L4PADPANEL), all three wrong; the two locals now forward to the one fixed inline. THE COCKPIT SCALES. The fixed canvas was stretched into whatever the client area was, so a window dragged to a different shape squashed the instruments (the projection was aspect-corrected in task #20; the panels never were). Now one uniform scale, centred, leftover black. D3D9 applies it as a Present destination rect, which DISCARD forbids -- so the WINDOWED swap effect becomes COPY when the surround is up and multisampling is off. The click mapping had to follow (mapping against the full client drifts the hit test off every button by the bar width), as did the world aspect (under a uniform scale it is the view rect's own). -fit / -windowed-fullscreen: borderless over the monitor. - ordering trap: the first WM_SIZE beats the device, so a -fit boot logged aspect=3.14 and applied it on frame 1. The letterbox INTENT is decided in btl4main; L4VIDEO only confirms or withdraws it. PLAYER-TUNABLE DISPLAYS. BT_MFD_SCALE (+ _UL/_UC/_UR/_LL/_LR), BT_RADAR_SCALE, BT_RADAR_POS (CENTER/LEFT/RIGHT/MIDLEFT/MIDRIGHT). The surround BANDS derive from the resolved sizes -- that is why the sizes could not stay constants: the band a display hangs in has to grow with it or the canvas clips it. 100% reproduces the historical L276 R276 T223 B336 exactly. A corner map goes flush to the CANVAS edge and the lower MFD slides beside it (measuring off the view edge overlapped them by 232px). MAP LEGEND GRID -- measured, not inherited. scratchpad/measurelegend.py over a native capture: top 3, cell 102, pitch 107 of 640. RP412's map is 13 + 6x102 @ 105 -- same cell height, different top and pitch, so its numbers do NOT transfer. Our old even division had the pitch right by luck and sat 3px high of the labels. environ.ini. It was read ~300 lines into WinMain, AFTER the platform-profile block had run its getenv()s -- so every setting the profile reads was silently ignored FROM THE FILE and only worked as a real env var. It also putenv()'d comments verbatim. Now loaded immediately after the first-breath line, comments skipped, the real environment WINS over the file, and a fully documented default is written on first run (the bindings.txt convention: untracked, so extract-over-top never clobbers a player's settings). VERIFICATION HARNESS (new, reusable): BT_RIOBANK_LOG=1 dumps every bank; checkbank.py proves no address is SHADOWED (an address whose rect is covered by earlier buttons is dead however big it looks -- the overlapping under-glass banks make that a live hazard); clickbank.py posts a real click at every button centre. Verified: 72/72 placed, 0 shadowed, 72/72 dispatched in BOTH modes, after a resize, at 150%/135%, and at 75%+BOTTOMRIGHT; wide/tall drags and -fit undistorted on a 3440x1440; exploded/dock/pod/dev un-regressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1d31af61d7 |
Gitea #48: arm the artifact tripwires BY DEFAULT + surface detections through the matchlog
The user's ask: if the translation-table fix did not quite catch it, will the
next playtest LOG the artifact? Before this commit, no -- BT_PLANE_AUDIT was
opt-in via env var, and playtest machines set none.
Changes (all engine L4VB16):
* BT_PLANE_AUDIT is now DEFAULT-ON, =0 opts out. Armed cost is a couple of
integer compares per DRAW CALL (not per pixel); confirmed zero log noise
on a clean default-env run post-fix.
* A SECOND trap class: out-of-bounds draw STARTS, checked always-on in
buildDestPointer (the funnel every primitive builds its pointer through).
Release builds compile Verify() to nothing, so a wild start previously
computed a silent rogue pointer into (or past) the shared buffer. Now
logged ([plane] OOB) and CLAMPED in-bounds.
* First detection of each class writes a GLITCH matchlog record
(kind=plane / kind=oob with position+color+mask), so the evidence arrives
with the round logs the operator already collects -- unattended.
* Both existing plane-leak trap levels (display primitives + the port-level
pixmap table scan) flipped to the same default-on gate.
Honest coverage note (in gauges-hud.md): an in-buffer EXTENT overrun from a
valid start (a blit walking past the right/bottom edge into later rows of the
SAME plane) is not trapped -- per-pixel walk checks are too hot. The plane
trap still catches any such overrun whose color leaks planes, which is the
class the night-3 artifacts belonged to.
Verified: full clean rebuild -- 40 pre-existing LNK2019 only (the
engine->game BTMatchLog linkage resolved, /FORCE-trap checked); default-env
probe run: 0 [plane] lines, game unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
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> |