b7e4837738184cc9e07509d1746b5a6542d0ea88
90
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2b0506ddfd |
OPERATOR_GUIDE: fix the blockquote swallowing the secret/port paragraph
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6b0402badc |
park_relay.cmd: resolve a real interpreter -- a second admin account has NO working python
Setting up remote access exposed this: 'python' works for the operator's own account only. It resolves through pyenv SHIMS driven by PYENV/PYENV_HOME/ PYENV_ROOT, and all three are USER-scoped to that account; the single machine-wide PATH entry points at a Python39-32 folder that no longer exists. So a dedicated remote-admin login (the account just created for SSH/RDP) gets NO python at all -- parking the relay from the road would have failed with 'python is not recognized', at night, with players waiting. park_relay.cmd now resolves an interpreter explicitly and prints which one: BT_PYTHON override -> the project's pyenv 3.11.4 (calling account's profile, then the operator's -- Administrators can read it) -> the machine-wide py launcher -> whatever is on PATH -> a clear error naming the fix. Verified both as the operator and with a simulated second-account environment (machine PATH only, PYENV cleared): both resolve 3.11.4. Also dropped a delayed-expansion bug in the first cut (!CAND! without EnableDelayedExpansion would have silently evaluated to nothing). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
05fdd319d6 |
parked relay: a supervisor that keeps it alive + a REMOTE restart the operator can actually reach
Completes the travelling-operator deployment. The relay stays parked on the
home machine (so no player ever edits join.bat) and the console dials in from
anywhere -- but until now two things needed hands on that machine: bringing the
relay back when it died, and restarting it (the only way to clear a wedge or
pick up a roster resize). Both are covered now.
tools/btrelay_park.py -- the supervisor:
* runs the relay with cwd=content\ , which is what pins WHICH
operator_secret.txt is live (the trap documented last commit);
* relaunches on ANY exit, with 2/5/15/30/60s backoff when a relay dies inside
20s, so a permanent fault (port taken, missing egg) cannot become a spin;
* rotates content\parked_relay.log to .1 first, so the dead generation's
evidence survives the relaunch that replaces it;
* Ctrl-C stops it for good. NOT a Windows service on purpose: session 0
would hide the window an operator wants to tail.
tools/park_relay.cmd -- double-click to park; a shortcut to it in shell:startup
gives start-after-reboot with no admin rights.
`restart` on the control port (btconsole.py): sets restart_requested, the run
loop returns, relay_main exits RESTART_EXIT_CODE=42 and the supervisor relaunches
-- re-reading the egg. REFUSED while a mission is running (launches_sent >= 2
and not stop_sent): bouncing then would drop every pod out of a live round.
Local stdin gets the same command for parity.
btoperator.py: Restart Session in REMOTE mode now sends `restart` and reconnects
6s later instead of just tearing down our own link -- the old behaviour looked
like "Restart does nothing" against a parked relay. (QTimer had to be imported;
it was missing, which py_compile does not catch -- it would have been a runtime
NameError on the first click.)
VERIFIED by scratchpad/test_parked_supervisor.py, 12/12, and the full
test_remote_console_e2e.py re-run green afterwards (15/15):
* kill the relay -> a NEW pid is serving again, and the old log is kept as .1;
* remote `restart` -> ACKed, new pid, egg re-read, serving again;
* the supervisor distinguishes the two -- "operator requested a restart" vs
the crash/backoff path -- proven from its own log, not assumed;
* the mid-mission guard is present and wired to launches_sent/stop_sent.
Two harness defects fixed while proving it, both mine: a check written with
`or True` that could never fail (replaced with two real assertions against the
supervisor's log), and a recv that treated the relay's correct
close-after-restart as a ConnectionResetError failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9df7dff787 |
KB: the parked-relay/remote-console mode is COMPLETE -- record it so it stops being re-derived
Operator caught me re-investigating a feature we already shipped today. Root
cause was a self-contradicting knowledge base, not just my compaction: the
§Modes entry still said "Remote relay -- KNOWN BROKEN: the LAUNCH button can
never enable in this mode", while §Mode-specific traps 60 lines later documented
the same thing as FIXED. Reading the summary line first is exactly what a
future session does. Swept: that was the only stale copy.
Now recorded once, authoritatively (context/operator-console.md §Parked relay +
remote console, and a sysop recipe in docs/OPERATOR_GUIDE.md):
* THE PRODUCTION TOPOLOGY the operator actually wants: the relay parked
permanently on the home machine so no player ever edits join.bat, with the
console dialling in from anywhere (cell internet is fine -- outbound only).
* The exact standalone park command, and why the control port is what makes a
GUI-less relay usable at all (backgrounded, it has no stdin).
* The COMPLETE remote command set as a table, read out of _ctl_command /
_ctl_get_mission / _ctl_set_mission: launch, stop, rearm|newround, get,
set (the six CONTROL_SET_KEYS, written to the relay's OWN egg, effective
next round), ping, plus the log stream and the roster re-issued at AUTH.
Written down so the next session reads instead of re-deriving.
* The hard limits: remote mode ATTACHES -- it cannot start or restart the
relay, cannot resize the roster, cannot edit callsign/mech/colour.
* Multiple operators ARE accepted concurrently (ctl_conns is a list, each
AUTHing independently), so hand-off is seamless -- but nothing arbitrates
two people pressing LAUNCH.
Two live hazards found while verifying, both new:
1. THE SECRET'S CWD TRAP. control_secret() opens a bare relative
"operator_secret.txt" and SILENTLY GENERATES A NEW ONE if absent, so the
live secret depends on the relay's working directory. This repo really has
TWO secret files with DIFFERENT values (repo root and content\) -- a remote
operator holding the wrong one fails AUTH as a CONTROL_AUTH_TIMEOUT drop,
with no "wrong password" anywhere. Documented in both files.
2. operator_secret.txt was untracked but NOT ignored -- one `git add -A` from
publishing a live credential. Now in .gitignore.
Also corrected a nearby overreach: "the console must STAY CONNECTED" is about
the console channel to the PODS; an operator GUI dropping off a parked relay's
control port is harmless and the pods never notice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
e46fc706a6 |
CONTROLS.html: load your bindings.txt and the board becomes YOUR board
The interactive controls page was a static picture of the stock layout -- a
player's carried migration rows, rebinds and HOTAS setup were invisible to it,
and the mismatch grew with every customization. Now a "Load your bindings.txt"
box (file picker + drag-drop) parses the real bindings grammar client-side and
rewrites the interactive keyboard in place:
* every key shows the loaded file's truth; keys that differ from the stock
board get a hazard outline (the legend says so); hover readouts follow
because they were already attribute-driven;
* the game's bindings-row-wins rule is modelled: the PgUp/PgDn volume and
backtick view BUILT-INS keep their face only while their key is unbound;
* rows that have no keyboard geometry (pad / padaxis / wizard joyaxis-
joybutton-joyhat / MOUSE keys) land in an "also in your file" list;
* one click restores the stock view. Nothing is uploaded -- FileReader on a
user-chosen local file, works from the extracted zip on file://.
Key-name registry walks the three key blocks in DOM order (Shift/Ctrl/Alt
resolve left-then-right; the numpad's U+2212 minus and its ASCII twin both map
to NUMPADMINUS), address meanings are harvested from the stock board itself
plus a small table for the slots the default board leaves off (unwired columns,
the cabinet-only throttle bank, PANIC).
Two self-inflicted bugs found by the harness and fixed before landing: the
address harvest invented a "BTN" label for the many stock keys that have none,
which made EVERY default row read as a customization; and generated axis labels
("AIM +") differed from the page's hand-authored vocabulary ("AIM UP"), custom-
marking the whole stock numpad. Both now speak the board's own labels.
VERIFIED headlessly (no browser extension needed): scratchpad/
test_controls_page.py injects a self-test harness into a copy of the page,
feeds it scratchpad/fixture_migrated_bindings.txt -- a REAL board-2 migrated
file (full defaults + two authored rows + a wizard section) -- and runs it
under chrome --headless=new --dump-dom. 11/11: the two authored rows carried
and marked, zero stray custom marks across the rest of the board, both
built-ins survive, wizard rows listed, status line honest, and reset restores
the stock board exactly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6eaed84fad |
CONTROLS.html: catch it up with the volume move + the bindings migration
The interactive controls page had drifted from the markdown fix-ups: PgUp/PgDn rendered as unbound keys with no meaning (the volume keys live there now), and the Rebinding section still promised 'an existing file is never overwritten' -- made false by the board-2 migration. Both corrected; the page and CONTROLS.txt now tell the same story. 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> |
||
|
|
62e89018f5 |
Merge glass-cockpit-refit: cockpit scaling + one button geometry, the keyboard button board, RGB keylight, crouch, the cwd guard
Cyd's 9-commit branch, reviewed before merge (clean merge-tree, zero overlap with the console/relay work that landed after his fork point; his L4VB16 refit preserves the #48 plane-audit tripwires, and his lamp-decode fix corrects the #47 flash rendering). Four review findings are fixed in the follow-up commit: the stale README controls table, the unguarded backtick view-toggle, the environ.ini one-shot loophole, and volume-key documentation (the -/= -> PgUp/ PgDn move itself landed pre-merge so this branch's Comm-bank -/= bindings are collision-free). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c9e561d9ba |
console review round 2: six must-fixes from the adversarial pass (one of its fixes corrected)
The 4-dimension review of 4736cba..820caf8 returned 14 surviving findings. All six must-fixes plus the four deferables are in; one of the review's own prescriptions was wrong and is fixed differently (below). THE REAPER, FINAL DOCTRINE (blocker + major). Rev 2 -- "reap anything silent in the staging window" -- was still wrong twice over: a pad sends exactly ONE message in its life (the egg ACK, L4NET.CPP:1259) and is then silent FOREVER, so any manual-launch hold >180s reaped every healthy ACKed pad and force-relaunched their clients; and a REGISTERED game conn is quiet on TCP while loading, so with BT_RELAY_TCP_ONLY=1 (or blocked UDP) the reaper _abort_round()ed the whole night every ~3 minutes blaming a healthy player. The rule is now: an app-level deadline is valid only while a RESPONSE IS OWED. Only egg-sent-never-ACKed pads are reaped, with the debt clock starting at EGG SEND (a pad that sat through a long held-egg wait gets its full window -- an edge neither review round caught); game conns are never reaped at all. Half-open ghosts are TCP keepalive's job (~130s, faster than the deadline anyway). RE-ARM GUARDED ON EVERY CHANNEL (major). The launch_at guard lived only on the LAUNCH-press path; the ctl `rearm` command, stdin, and the always-lit GUI button reached _rearm_for_new_round unconditionally -- one press mid-mission zeroed the counters, permanently killing the mission clock AND making End Mission print "no mission is running": an unstoppable round, the exact class this feature exists to eliminate. Now refused (loudly) while a mission is running or a pair is in flight, and the button greys while launched. THE REVIEW'S OWN FIX WAS WRONG here: it prescribed refusing on `launches_sent >= 2`, but that stays 2 after a FINISHED round -- applying it verbatim resurrected the original dead-LAUNCH wedge, caught immediately by the regression suite. "Running" is `launches_sent >= 2 AND not stop_sent`. RE-ARM RE-KEYS SEATS (major). It restored the template roster but left seat_beacons/seat_prefs keyed by the trimmed round's positional ids, so round 2's trim minted a departed player's tag into the egg and trimmed a present player's out, shifting every callsign/mech a slot. The re-key block is factored out of _maybe_reset_round (_rekey_seats_to_roster) and shared. RESTART SESSION NO LONGER BAKES A WALK-UP'S NAME INTO THE EGG (major). _stop_session and _start_session now restore the stashed configured callsign/mech into the cells BEFORE _collect_egg can snapshot them (_restore_seat_defaults); previously the departed name went into the egg (name= plus rasterized bitmaps) and was then re-captured as the seat's permanent "configured default". LATE REMOTE OPERATOR GETS A ROSTER (major). The only line the GUI can adopt tags from was printed once at relay startup and aged out of the 400-line control history in ~33 minutes of stats chatter -- AUTH now re-issues the live roster line ahead of the replay, so a remote operator connecting at any point gets pilot lights and a working LAUNCH button. DEFERABLES, all four: _drop_game blames the tag stashed AT REGISTRATION (the live-roster resolve named the wrong tag for a trimmed-round conn dying after a re-arm restore -- and the tag-trusting GUI would clear the wrong seat); an operator-BLANKED callsign cell now restores (empty string is a real configured value; the falsy skip left the departed name up and re-captured it); a returning player displaces their own half-open registered ghost (same IP, no mission running) instead of eating ROSTER FULL until keepalive fires; udp_spoofed now rides the [relay-stats] line when non-zero and the warn set is capped at 256. VERIFIED: rearm suite grown to 25 checks (ACKed pad never reaped however silent; never-ACKed pad reaped; game conns never reaped; the egg-send debt clock; re-arm refused mid-mission, allowed after round end) -- plus net 17/17, roster 22/22, checkctx CLEAN. Live rig: mid-mission `rearm` refused with the message and the mission survived; End Mission -> re-arm -> full re-seat -> second mission launched. One bounded artifact observed and documented: each waiting pod bounces once (identity resync) after an explicit re-arm. Docs rewritten to the final doctrine (context/operator-console.md reaper + re-arm + roster-replay + udp_spoofed sections; OPERATOR_GUIDE + tooltip now say re-arm is between-rounds-only and warn about the one-bounce resync). 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>
|
||
|
|
3a694485bf |
operator UI: a departed player no longer keeps their seat in the roster
Operator report: "if a player disconnects from the console, it leaves their name
up in the roster ... it should turn their seat back to empty." I had not noticed
or fixed this -- the earlier review raised adjacent roster issues (lights mapped
through the untrimmed tag list after a trim) but not this one.
CAUSE. The GUI only ever WROTE walk-up names into the table. On a disconnect
the relay pops seat_info, the write-back loop hit `if not info: continue`, and
the cell kept the departed callsign for the rest of the session -- while the
pilot light beside it correctly said "waiting". So the table and the light
disagreed and a free seat looked occupied.
Two changes, because the relay's two seat-clearing signals are NOT symmetric:
* SessionMonitor now pops seat_info when a pilot goes idle from EITHER signal.
"PLAYER n LEFT" is only printed inside the beacon-close branch gated on
`if seat not in self.by_host` (the relay's own comment: "never claimed:
player left"), so a player who was fully REGISTERED and then dropped
produced only `game[...] dropped` -- the common case, and the one that left
the name up. Keying "seat is empty" off LEFT alone could never have worked.
* _refresh_pod_status stashes the operator's configured callsign/mech PER
OCCUPANCY when a seat fills, and restores it when the seat empties.
Per-occupancy rather than once at egg load, so an edit the operator makes
while a seat is empty is respected instead of being overwritten by a stale
snapshot.
It restores the CONFIGURED PILOT, not a literal blank, deliberately: that cell is
editable and is what gets written to the egg on Save, so a placeholder like
"-- empty --" would end up in the mission file. The "unoccupied" signal is the
grey `waiting` light beside it.
NOT CHANGED, and explained instead: a vacated seat is held for its player for 90s
(seat_reclaim) so a crash or a reconnect returns them to the same seat and mech.
Assignment skips claimed/reserved/reclaim-held seats, so a brand-new joiner in
that window gets the next free seat, or ROSTER FULL if there wasn't one; after 90s
the seat is fair game. The operator said they were happy with players keeping
their seat/address, so this stays -- it is now documented in both docs rather than
silently removed.
VERIFIED: scratchpad/test_operator_roster.py (new, 14 checks) drives the REAL Qt
widget offscreen and feeds it the REAL relay log lines: a walk-up name appears;
after REGISTERED then `dropped` the light returns to waiting AND the seat shows
the configured pilot again; the seat is reusable by a different player and clears
for them too; the `PLAYER n LEFT` path clears it as well; an operator edit made
while the seat was empty survives an occupancy; and a neighbouring row is never
touched by another seat's traffic. Other suites still pass (rearm 19/19, net
17/17); checkctx CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ab5828a866 |
relay/console: fix the three remaining hardening items from the review
1. UDP ENDPOINT HIJACK. `from_host` in a UDP envelope is the sender's OWN claim,
and the relay used it directly to refresh that host's downstream endpoint --
so ANY datagram claiming host N silently stole host N's traffic (a zombie pod
from a previous round, a stale NAT mapping, or anyone who guessed a host id).
The victim simply stopped receiving on a channel that still looked healthy.
The claim is now bound to the identity we actually authenticated: the IP of
that host's live TCP game connection. The PORT is deliberately not checked --
it moves on a NAT rebind, which is the whole reason the endpoint map refreshes
per datagram -- and a genuine IP change cannot happen without the TCP
connection breaking and re-registering, so a legitimate pod is never rejected.
Rejections are counted (udp_spoofed) and logged once per offending (host, IP).
Known limit: two pods on one machine share an IP, so this cannot separate
them; same-machine trust is assumed.
2. THE EGG ACK COULD BE MISSED ENTIRELY -- a silent, unrecoverable wedge. The
pod's ACK is the launch gate's ONLY signal, and it was detected by parsing a
single recv() at fixed offset 0 behind a `len(data) >= 24` test. On a real
network (loopback hid both cases) the 28-byte ACK arriving SPLIT -- 16 bytes
then 12 -- was dropped by that test and never looked at again, and two
COALESCED messages meant only the first was read. A missed ACK means that
seat never counts toward the gate, so the round can never be released.
_console_read now buffers per connection and walks every complete frame
(16 + messageLength); an implausible length drops the connection WITH A REASON
rather than mis-reading that pod all night, since a stream protocol cannot be
resynced by guessing. Bounds: CONSOLE_MSG_MAX 64K, CONSOLE_INBUF_MAX 1 MiB.
3. REMOTE-OPERATOR MODE WAS HALF A CONSOLE. LAUNCH and END MISSION could never
enable (both conditions required `console_proc is not None`, which the remote
path never sets), the pilot lights were permanently blank (SessionMonitor was
built with an empty tag list, so `seated` was always 0), and Launch-local was
refused by the same guard even though the code below it already built
BT_RELAY from the remote host. All three enable conditions now accept EITHER
channel, and the monitor ADOPTS THE ROSTER from the relay's
"roster: N pilot(s) -> hostIDs [...]: [...]" line -- which the relay replays to
every newly AUTHed operator -- so a remote operator gets real lights and a
real seat count.
VERIFIED
* scratchpad/test_relay_net.py (new, 17 checks): spoofed datagram cannot move
an endpoint and is counted; a NAT rebind on the same IP still is honoured; an
unregistered host id still dropped; the ACK is found whole, split in two,
split three ways mid-header, and when hiding behind another message;
a garbage length drops the conn; the roster line is adopted, maps a
subsequent SEATED onto a real tag, lifts `seated` off 0, and re-feeding it
preserves known state.
* Live 2-pod rig: real pods ACKed through the new reassembly path (2/2 ready,
zero desync drops), the mission launched and ran, UDP flowed throughout
(93 rx / 85 tx, udp-known [2,3]) with ZERO false spoof rejections, then a
clean StopMission + round RESET.
* scratchpad/test_relay_rearm.py still 19/19; checkctx CLEAN.
Docs updated to match (context/operator-console.md gains reassembly and
anti-hijack sections; the guide no longer claims remote mode is crippled).
Remaining open items are now recorded in the topic's frontmatter: mesh mode's
operator buttons are inert by construction (no stdin reader in that path -- left
alone, mesh self-launches), _log_launch_readiness can cry STALL on a healthy
launch-with-whoever, and a straggler's late FIN is still misread as a pod dying
mid-load.
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> |
||
|
|
a7bcf1cbab |
Proposal: ship the authentic single-folder dist layout (docs only, NOT implemented)
The bare-launch crash was a symptom; the cause is that BT411 hands players a developer tree. mkdist zips tracked repo paths verbatim, so the dist inherited build\Release\btl4.exe + content\ -- while the 1995 pod (BTL4OPT.EXE sits beside BTL4.RES to this day, in content\) and RP411/RP412 all ship ONE folder, where cwd is right by construction and a double-click just works. docs/DIST_LAYOUT_PLAN.md sets out the proposed tree, the file-by-file change (~50 lines total, no game code), what it buys, and the part that actually needs a decision: the upgrade story. Four migration options with a recommendation, plus the wrinkle that decides between them -- the zip root is VERSIONED, so every version may already land in its own folder, in which case the documented extract-over-top has never worked and the migration cost is near zero. Four open questions listed for the other author, including whether the .bat launchers still earn their keep and whether the pod cabinet's own install shape should be confirmed against Nick's notes before we call one folder authentic for the cabinet too. Not implemented pending that discussion. The 4.11.560 cwd guard stands either way and makes both layouts work. 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> |
||
|
|
1cda880c6d |
console/relay: document it properly + fix two regressions the review caught
DOCS (the ask: after a compaction this session lost track of how the console
works and launched the wrong program, twice).
* NEW context/operator-console.md -- the dedicated topic that was missing.
Leads with the thing I got wrong: btoperator.py is the PySide6 GUI the
operator uses; btconsole.py is the headless relay it spawns. Then ports,
the route table, the roster/seat/identity model, the full round lifecycle
with every launch gate, liveness, mode-specific traps, and log locations.
* NEW docs/OPERATOR_GUIDE.md -- sysop-facing: start the console, set up a
mission, watch pods arrive, launch, run back-to-back rounds, what to press
when LAUNCH looks dead, a troubleshooting table keyed on the exact log
lines, and what to save BEFORE restarting a session (Start Session
truncates operator_relay.log, so restarting to clear a problem destroys the
evidence of it).
* CLAUDE.md: two Quick Lookup rows + a DO-NOT entry naming the two programs,
so the distinction survives the next compaction.
* context/multiplayer.md: a pointer out of the scattered console notes to the
new topic (they were buried across ~8 places in a large file, which is
exactly why they evaporated).
FIXES -- both are regressions in my own previous commit, found by the review
pass, and one would have made a games night WORSE:
* THE REAPER WOULD HAVE KILLED HEALTHY PLAYERS. It was gated only on "no
mission running", which a round RESET satisfies -- so it was armed for the
whole BETWEEN-ROUNDS wait, and that is a period when a pod is legitimately
byte-silent: its seat beacon is write-only for the process lifetime
(L4NET.CPP: "the relay ignores its silence") and its console pad has no egg
yet so it cannot ACK. A real night showed 12-minute and 5-minute gaps; the
180s deadline would have dropped healthy pods and forced their clients to
relaunch. It now runs ONLY in the active staging window, skips pads with no
egg and conns that have not HELLO'd, and last_seen is also stamped from
inbound UDP (a pod streaming updates while its TCP idles was being counted
as silent). Half-open detection is keepalive's job; this is just a backstop.
* UnboundLocalError in the operator UI. My end_sent reset was an `elif` in
the chain that assigns `head`, so that branch left `head` unbound -- a crash
on the first status refresh after End Mission, which is exactly the path the
relay's "StopMission sent" line produces. Moved out of the chain.
Plus one pre-existing wedge with the same symptom as the reported bug, live-
proven in operator_relay.log (~5 minutes of a night lost): _abort_round clears
eggs_released BEFORE the survivors' sockets close, so _maybe_reset_round's own
`if not self.eggs_released: return` skips the template restore forever -- the
roster stays trimmed, the release gates can never be met, and walk-ups get
ROSTER FULL. _rearm_for_new_round's restore is therefore now UNCONDITIONAL (it
was gated on eggs_released, which made Re-arm useless in the one state that most
needs it) and it clears round_hold_until so an abort's settle window is not
inherited. Aborts are ordinary: nothing on the wire distinguishes a straggler's
late FIN from a pod dying mid-load.
scratchpad/test_relay_rearm.py now 19 checks, all passing, including the two new
regression guards (a 15-minute-silent waiting pod is NOT reaped; a pad that was
never sent an egg is NOT reaped) and the mid-pair no-re-arm guard.
Still open, recorded in the new topic's frontmatter: the UDP endpoint map trusts
the sender's self-declared fromHost; the egg-ACK is a fixed-offset parse of one
recv with no reassembly; remote-operator mode can never enable LAUNCH.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
48d47ef806 |
#45 review pass: correct three claims the fix's own comments got wrong
An adversarial review of the change returned NO BLOCKERS but caught three
statements that were wrong or unverifiable. All three are corrections to my own
comments/docs, not behaviour changes -- the verified binary is unchanged.
1. The phantom-kill note had the ORDERING backwards. It claimed the owner's
record "overwrites it on the next record -- the phantom stops being visible".
The engine's event priorities run the other way: an inbound update record is
posted at UpdateEventPriority == MaxEventPriority and drained by
ExecuteBackgroundTask, while the rerouted score message sits at
EntityManagerEventPriority == DefaultEventPriority and is popped later -- so
the correction usually lands FIRST and the phantom AFTER it. The artifact is
BOUNDED, not masked: on the killer's pod only, that victim's KILLS reads +1
until the next record, i.e. <= one 2s heartbeat instead of "until the victim
next scores or dies". Swept in btplayer.cpp, KD_SCOREBOARD_PLAN.md and
RECONCILE.md.
2. The corpus figures were not reproducible, and were mis-tiered. I cited
"125 of 125 SCORE type=2 over 255 node-logs" and "0 of 8800 DMG rows" -- true
when measured, but the corpus is append-only and this fix's OWN verification
runs grew it from ~255 to 425 files, so nobody can re-derive them. Replaced
everywhere with invariant RATIOS that hold at any corpus size, re-measured
here:
* 0 of 18818 DMG rows are inst=R (damage is applied master-side only)
* 439 of 439 DEATH inst=R rows read killer=0:0 killdmg=0.000, against
0 of 225 inst=M rows (sole lastInflictingID writer: mech.cpp:746)
Also re-tagged T1 -> T2: log-corpus field evidence is T2 per the CLAUDE.md
tier table. Lesson recorded in the banner: cite ratios, not counts, for a
corpus your own rigs append to.
3. The recordLength guard's justification was wrong (and the plan's risk 5 with
it). A short legacy record does NOT desync the stream -- both sides advance
by the WRITER's stamped recordLength and no reader asserts a length. The real
hazard is a read PAST THE ALLOCATION: an inbound update message lives in a
per-event heap block sized from messageLength, so on a TRAILING legacy record
the two new fields would be read off the end of it.
Plus: the record struct's own field comment still said "deathCount -- the DEATHS
column", the exact wrong-field belief #45 removed, sitting on the wire struct.
Filed the review's other finding on #60: a SECOND decomp export gap, index
jumping FUN_0049fe80 (ends 0x49ffc8) -> FUN_004a1232, 4714 unexported bytes
containing Mech::TakeDamageMessageHandler @0x4a0230 -- the producer of every
score and death message in the game. Verified independently against
functions_index.tsv before posting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a52207d779 |
#45 scoreboard: reclaim the binary's DEATHS field, add a heartbeat, sweep the false KB claims
Follow-up to
|
||
|
|
4fa7eee54f |
K/D scoreboard ROOT-CAUSED for real (#45): the kill credit was rerouted, not lost
Kills/deaths only ever appeared on ONE machine. The cause is not a missing
tally -- it is Entity::Dispatch:
if (GetInstance() == ReplicantInstance) // ENTITY.cpp:244-251
application->SendMessage(ownerID, EntityManagerClientID, message);
BTPostKillScore runs on the VICTIM's node (the only node whose mech carries a
populated lastInflictingID), resolves the killer's Player -- a REPLICANT there --
and Dispatch()es to it. The engine reroutes that to the owning host, so
++killCount lands on the killer's own PC and nowhere else, and nothing carried it
back out: Player__UpdateRecord is currentScore + dropZoneLocation only. Every
other pod's copy therefore read 0 all mission. playerLink was never NULL for
these kills -- the dispatch proves it resolved.
Field proof over the whole corpus (255 node-logs): 125 of 125 SCORE type=2 rows
credit the LOGGING node's own player, not one credits a remote pilot; and 0 of
8800 DMG rows target a replicant, so no other node can even know the killer.
This means the owner's counter is already the single authoritative copy,
incremented exactly once per kill. So the "design decision" the plan was blocked
on collapses: there is no second writer to reconcile, only a one-way
owner->replicant mirror to add.
* BTPlayer__UpdateRecord = Player::UpdateRecord + killTally + deathTally, with
Read/WriteUpdateRecord overrides. No new data member, no new virtual ->
sizeof(BTPlayer)==0x28c and every existing offset lock still holds.
* Both counter writes already ForceUpdate() (:508 death, :829/:840 score), so
no dirty-bit edit was needed (plan risk 6 avoided).
* recordLength guard in ReadUpdateRecord: a pod on a build without the
extension degrades to "remote counters don't move" instead of reading a
neighbouring record as a kill count.
* Size locks added per plan risk 2 (no false offsetof assert).
Rig-verified, 2-node loopback (scratchpad/rig45_up.ps1, BT_MP_FORCE_DMG):
owner 3:1 finished kills 3/deaths 2 and the peer read kills=3 deaths=2; owner 2:1
finished kills 2/deaths 3 and the peer read kills=2 deaths=3. Exact convergence
where a remote column previously never left 0. 3 respawns per side with intact
death sequences (deathCount is only overwritten on replicant copies; the
handshake runs on masters), no crash, no GLITCH rows.
Kept byte-faithful: the kill handler's partner increment (the binary's
inc [ebx+0x27c] / inc [edx+0x27c] wrong-column slip) is still reproduced. It
always lands on a replicant copy, so the owner's record now overwrites it -- the
rig caught the correction repeatedly (wasKills=3 -> kills=2). The phantom kill
stops being visible without silently deciding the fidelity question.
Still open and deliberately untouched: which field the LOCAL pod's DEATHS column
should read (+0x280 vs deathCount), last-hitter-takes-all/ram kills, and the
slip's fidelity. Awaiting live multi-pod verification by a human.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
373889760c |
#66: joyconfig.bat no longer hangs the game -- clear BT_JOYCONFIG once the wizard is done
Found while sweeping the shipped launchers for the PATH-shadowing bug class.
Previously recorded ONLY as row 10 of docs/INPUT_PATH_AUDIT.md and never
filed, so it was invisible on the tracker.
joyconfig.bat sets BOTH `BT_JOYCONFIG=1` and `BT_FE_SOLO=1`, so after the
wizard writes bindings.txt the boot continues into the FRONT END, which hands
off by CreateProcessW()ing the next generation with the CURRENT environment.
That clear-list (btl4console.cpp) covers only BT_FE_EGG/PODS/SECS/LOOP --
BT_JOYCONFIG is never cleared -- so the handed-off generation re-entered
BTJoyConfigWizard() and blocked on _getch() BEHIND the 3D window: an
unrecoverable hang, hitting exactly the players most likely to run that bat
(flight stick / HOTAS / pedals).
Fix: SetEnvironmentVariableA("BT_JOYCONFIG", NULL) immediately after the
wizard returns. Cleared in the WIN32 environment (what CreateProcess
inherits, matching the console's own pattern) rather than the CRT copy, so
exactly one generation ever runs it. Chosen over extending the console's
clear-list because it also covers any future relaunch path.
NOT YET REBUILT: the exe was locked by a live play session when this landed.
Must be rebuilt + verified (run joyconfig.bat, finish the wizard, confirm no
second "=== BattleTech joystick setup ===" banner and that the game proceeds
into the menu) before the next zip is cut.
Co-Authored-By: Claude Opus 5 (1M context) <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>
|
||
|
|
f3bdb3b85a |
Searchlight + ThermalSight ToggleLamp WIRED (#61) -- and a swapped table attribution ROOT-CAUSED, retracting a false "1995 latent bug"
Both classes' Receiver::MessageHandlerSet were default-constructed blackholes (input-path audit systemic cause #1): entryCount 0, no parent chain, Find() returns NullHandler for every id, Receive drops silently. The new unhandled- message trace printed it on the first press: [btntest] PRESS 0x14 at poll 400 [msg] UNHANDLED: Searchlight has no handler for message id 3 Each class now has a GetMessageHandlers() function-local static chained to PowerWatcher's (which name-resolves through HeatWatcher/MechSubsystem to Receiver's empty ROOT set, so id 3 does NOT collide with HeatSink's ToggleCooling), plus a correctly-typed forwarder -- Receiver::Handler is void(const Message*) while the decomp shape is Logical(Message&), so a cast would be UB that merely happens to work on x86 __thiscall. DefaultData re-pointed off the dead empty sets. MessageArg now reads the NAMED ReceiverDataMessageOf<int>::dataContents with a static_assert locking it to the binary's message+0xC (was a raw offset read -- databinding rule). ROOT CAUSE of a long-standing misattribution: @004b860c is **ThermalSight's** ToggleLamp (table @0x51120C), NOT Searchlight's. The two TUs emit parallel shared-data blocks with identical stride (msg entry, +0x5C attrs, +0x84 Performance triple); what pins each entry to its TU is that "ToggleLamp" is NOT pooled across them -- two copies exist, each emitted immediately before its own class-name string ("Searchlight"@0x51144B, "ThermalSight"@0x511475). [T1: reference/decomp/section_dump.txt:69661-69711] Searchlight's own handler is @004b838c, which sits in a Ghidra EXPORT GAP (#60). RECOVERED by raw disassembly of content/BTL4OPT.EXE (scratchpad/dis838c.py, the EjectAmmo technique) -- and it corrected two things I had inferred wrong: * NO ControlsAllowLights/+0x25C novice gate. Searchlight tests the press alone; that lock is ThermalSight-only. A NOVICE pilot CAN work the lamp. * `or word ptr [this+0x18],1` sits AT the jle target -> the graphics-dirty bit (updateModel == ForceUpdate(), per #59) is raised UNCONDITIONALLY. Consequence: the "ORIGINAL 1995 LATENT BUG -- the searchlight can never light" claim is RETRACTED. It compared Searchlight's Performance (@004b841c, reads requestedOn@0x1E0) against ThermalSight's toggle (0x1DC) -- two different classes -- and so invented a missing 0x1DC->0x1E0 bridge. Every sibling toggles the field its own Performance reads. The searchlight DID light in the arcade, the searchlight->fog swap was NOT inert there, and building PullFogRenderable is FAITHFUL rather than a designer-intent deviation (the 2026-07-13 "left as-is" decision is void; the sim needs no repair). Verified live, real click seam (BT_BTNTEST): 0x14 -> [light] requested ON -> reported lightState 0 -> 1 (lamp lights) 0x12 -> [light] thermal sight requested ON -> thermalActive 0 -> 1 UNHANDLED lines for both classes gone; 40 LNK2019 unchanged (the pre-existing CreateStreamedSubsystem + Entity__SharedData::DefaultData families only). Swept the misattribution out of searchlight.cpp/.hpp, thermalsight.cpp/.hpp, hud.cpp:282 (it had claimed @00511180/@004b838c as HUD's), btplayer.cpp's +0x25C consumer list, context/{decomp-reference,subsystems,rendering,open-questions, pod-hardware,experience-levels}.md, docs/{GLASS_COCKPIT,INPUT_PATH_AUDIT}.md. checkctx.py CLEAN. Still open (documented, not fixed): ThermalSight has no visible IR effect (ToggleGlobalThermalVision is a marked no-op, pvision unported, IsLocallyViewed returns False); ThermalSight publishes "LightState"->0x1D8 where the binary has "LightOn"->0x1D8 and "LightState"->0x1E0; Searchlight's commandedOn@0x1DC has no identified role and measured 21 live, hinting our SubsystemResource +0x28 differs from the binary's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5174c638ce |
K/D scoreboard ROOT-CAUSED (#45) + respawn latch fix (#57) + plate stage ops (#58)
FIXES (compile clean; exe link pending -- a game client still holds btl4.exe): #57 respawn latch: deathPending was released in ONE place (btplayer.cpp:343) and only if a LATER re-post happened to find the mech alive -- so if the +5s re-post landed before the drop-zone reply, or never arrived, the flag stayed set while the pilot respawned and fought on, and their NEXT death hit the dedup at :391 and was swallowed whole (no warp, no tally, no hunt) for the rest of the process. Tonight 3 of 3 deaths in the final round were swallowed, each by a player who had respawned successfully earlier in the same process. Clear moved to the actual completion point (after Mech::Reset at the drop zone) + the four abandon paths that also stranded it. Cycle logging promoted to always-on (START / RESET / SWALLOWED warning) + a RESPAWN matchlog record. #58 reticle callsign plate drew as a SOLID RECTANGLE on some maps: my dpl2d_DrawTexturedRect bound a texture but never set COLOROP/ALPHAOP, and L4D3D.cpp:1085 sets both to SELECTARG1 from TFACTOR (ignores the texture, fills with a constant). Which state was live depended on what the 3D pass drew last -- hence 'depends on the level'. Now sets MODULATE texture x diffuse for both channels and restores. Latent sibling noted (CameraHUDDrawQuad, same omission). ROOT-CAUSE DOC (no code): docs/KD_SCOREBOARD_PLAN.md. Headlines: - the port reads DEATHS from the WRONG FIELD: the binary's PilotList draws +0x27c/+0x280; we labelled +0x280 'pad_0x280', never write it, and substituted the engine's Player::deathCount (+0x200, the respawn identity number seeded to -2) -> remote rows read a clamped fake 0 by construction; - BTPlayer::VehicleDeadMessageHandler @0x4c05c4 is MISSING from our decomp export (functions_index.tsv jumps 4c052c -> 4c083c, verified) -- that silence is what produced the 'pad' belief. An absent function is not evidence of an absent behaviour; - neither counter rides an update record, so divergence never self-heals and bystanders show 0/0 all mission; BTPlayerCountObservedDeath is unreachable dead code; - 'a kill I didn't earn' = last-hitter-takes-all via lastInflictingID, and COLLISIONS count (5 of 47 deaths had a type=0 killing blow); - the phantom kill is authentic 1995 (the kill handler bumps the victim's killCount too); - btplayer.cpp:453 does simulationFlags |= 0x1 where the binary does ForceUpdate() -- bit 0 is DelayWatchersFlag and nothing clears it, so the first death of any pilot permanently disables ExecuteWatchers on that player's simulation. Filing separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
326023812a |
Respawn re-arm ROOT-CAUSED: David's death transition never started (#54) -- plan doc only
9-agent read-only investigation, every binary claim re-verified with capstone. VERDICT: respawn re-arm WORKS in 4.11.524 -- 21 own-deaths in tonight's matchlogs each have a 1:1 PLAYER_DEAD, except David's (1 death, 0 PLAYER_DEAD). So #22's fix is holding and is NOT implicated (my earlier claim retracted). His cycle never STARTED: PLAYER_DEAD is written unconditionally at btplayer.cpp:422 in the same straight-line block as ++deathCount, so its absence proves VehicleDeadMessageHandler never entered the deathCount==-1 branch -> no drop-zone hunt -> no DropZoneReply -> Mech::Reset never ran. Corroborated: a zone reads lvl=1.0000 11.18s AFTER death, which Reset would have healed. With no Reset movementMode stays 9, and ONE predicate -- Mech::IsDisabled() (mode 2||9) -- darkens all six panels via two authentic gates (emitter.cpp:431 currentLevel=0; projweap.cpp:707 recoil=rechargeRate + alarm 7). Ring and ready-disc are the SAME scalar, so six dark panels is one data fault. SEPARATE second bug: his AFC 100 went silent 123s BEFORE death holding 13 rounds, and one LRM 66s before while its twin kept firing -- that is the #21 family, untouched by this. Port divergences found on the way: mech4.cpp:1928 calls DeathShutdown(1) but the binary sweeps with 0 and forwards to DeathReset -- and our DeathShutdown is an empty base nobody overrides, so the whole death sweep is a no-op; only 7 of ~20 subsystem classes implement DeathReset; HeatSink::ResetToInitialState clobbers the startingTemperature it just wrote and stops short of the authentic terminus @004ac22c (which heals each subsystem's own crit zone + status alarm). UNKNOWN: why the -1 branch never ran. One survivor after static elimination (GetPlayerLink()==0 on his own master mech); section 6 'Check 0' settles it with no code change. Do that first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
b517c4cb30 |
Input-path audit: 206 paths traced, 22 dead -- plan doc only, no code changes
13-agent read-only audit (6 tracers -> adversarial verifiers -> triage), validated against 2,294 streamed control-mapping records decoded from all 18 mechs in BTL4.RES plus the live 121-record install dump. Verdict: 22 dead dispatch routes, 11 player-visible, 8 documentation defects, 2 entirely dormant input layers (CONTROLS.MAP and the whole DirectInput grammar). RETRACTS MY OWN DIAGNOSIS from the reverse-thrust fix earlier today: streamed BUTTON mappings ARE consumed (L4CTRL.cpp:2254-2262 / :2327-2336, and ctrlmap.log:32 shows reverse's record resolving to +0xec). The AddOrErase Fail bodies are the config-session base traps, overridden by MechRIOMapper. So 'reconstruct AddOrErase' would clear 0 of 2,294 records. The false claim is in pod-hardware.md:121-135 AND in mechmppr.cpp:894-919 -- both owed a sweep, and reverseThrust now has two writers (unknown #2 decides which to keep). The 'how did it break' question is therefore reopened; leading candidate is the unhandled WM_SYSKEYDOWN/SC_KEYMENU menu-modal loop on a held ALT. Biggest new finding: a default-constructed Receiver::MessageHandlerSet is a silent blackhole -- Sensor/Avionics alone kills 108 records (6 cockpit buttons + F5-F9). One accessor in sensor.cpp is the best findings-cleared/risk ratio. Worse-than-dead: target designation writes raw +0x284 on a modern-layout object, possibly out of bounds, reachable by pressing 't' -- flagged as a playtest safety note. Also lands scratchpad/keysweep.py: the empirical counterpart (presses every bound key, records what actually moves). Not yet run -- needs the machine idle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
6e810d99cc |
docs: mark the 'only .x meshes load' paragraph SUPERSEDED (native BGF reader exists)
Found during the input-path audit: RECONCILE.md's 'DEEPEST RENDER FAULT = ASSET PIPELINE GAP' paragraph is stale -- d3d_OBJECT::LoadObject falls through to LoadObjectBGF (L4D3D.cpp:98/230, bgfload.cpp LoadBgfFile) and BGF geometry loads natively today. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
66d1410118 |
Revolving-door mode: feasibility findings + two-phase implementation plan (docs only)
Phase 1 = console-only round carousel (relay door mode, no new zip); Phase 2 = true drop-in (engine make re-send on late PEER_UP), gated on a 2-node spike. No code modified -- plan for the next work session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
1e647e39d7 |
CREDITS: the printed credits page (via VGL Lynx) -- definitive roster
Lynx supplied a photo of the period credits page (preserved at reference/manual/credits_page.webp). It confirms, corrects and completes the oral-history identifications: - ECH = Eric Huffman (Audio Director / 3D audio framework) -- corrects the uncertain "Eric Nichols" guess - JMA = Joanna Mason (Simulation Code and Cameras) -- inferred mapping confirmed - CPB = Chris Brewer: "Secondary/Auxiliary Screens and Low Level I/O" -- the exact MFD gauge/mode layer this project reconstructs - GDU = Gabe Underwood: "Simulation Code and 'Mech Internal Systems" -- the heat/coolant/generator/seek economy - Full roster: Jordan Weisman (producer/designer), David McCoy (art director), J.M. Albertson (technical director), Bill Redmann (corporate director of technology), plus Corson, Edsall, Ciolek, Olsen, the artists (Burlington, Molitor, McCoy, Story; Allen Workshop: Bonilla, Peters) and sound (Bing McCoy, Effinger, Huffman). - 10 software engineers + 6 artists == Lynx's "maybe 8 dev, 6 art" memory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fe1cc0d79c |
docs/CREDITS.md: name the original 1995-96 VWE developers
Source archaeology (the per-file modification tables in the surviving headers) surfaced six programmer initials: JM, ECH, CPB, GDU, JMA, GAH. Read to VWE veterans on the project Discord (2026-07-21), VGL Lynx -- who watched the first multiplayer Thor walk terrain on a Red Planet map -- identified them: Jim Albertson, Eric Nichols (uncertain), Chris Brewer, Gabe Underwood, Joanna Mason (mapping inferred), Garth Hermanson; hardware initials MC = Marek Ciolek (AC unresolved), via Cyd. Team size per Lynx: dev ~8, art ~6, MUNGA by a smaller parallel internal team with crossovers, no outsourcing. Identifications marked T3 (oral history, as-typed spellings) against the T0 initials; a corrections invitation is included. Linked from context/project-overview.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
89b4f53046 |
Gauges: trigger-config joystick is LIVE -- @004c6ee0 is LinkToEntity, not SetColor
The weapon panel's btjoy trigger-config display (ConfigMapGauge) never rendered because the reconstruction labelled @004c6ee0 as "SetColor(int)" and, finding no caller, concluded the gauge was authentically dormant (task #6), gating it behind a BT_CONFIGMAP dev env. The user challenged this (the PROGRAM/TRIGGER CONFIG button implies the display visualizes your config). Re-verification found the old analysis failed twice: the "no caller" search looked for direct calls to a VIRTUAL, and its slot math used the wrong vtable copy. The real ConfigMapGauge vtable @0051a1b8, matched against the T0 GaugeBase virtual roster (GAUGE.h), pins @004c6ee0 at slot 9 (+0x24) == GaugeBase::LinkToEntity, and @0x94 is the linkedEntity -- the Execute gate is "not yet linked", not "disabled". The engine broadcasts LinkToEntity(viewpointEntity) to every gauge at viewpoint bind (APP.cpp:1277 -> GaugeRenderer::LinkToEntity GAUGREND.cpp:3011), arming the gate -- the joystick DOES render in the shipped game, showing per-trigger mapping state (solid = mapped, matching the DOSBox reference). Port fix: SetColor(int color) -> LinkToEntity(Entity*), color -> linkedEntity, BT_CONFIGMAP dev enable deleted (the authentic path lights it). Render-verified with no env override: the joystick + mapped-trigger lamp draw on weapon panels. KB swept per the correction mandate: gauges-hud.md, decomp-reference.md, open-questions.md, GAUGE_COMPOSITE.md, VEHICLE_SUBSYSTEMS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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>
|
||
|
|
1535e3e48d |
Glass 'keymap regression' triage: stale build-glass exe, not code — rebuild + full re-verify
The live-reported glass regressions (arrows dead / lever+detent missing / V+J/K/L+N inert / weapons into the ground) were build-glass/Release/btl4.exe built at 07:47 running the pre-5dd3536 image: no suppression tables, no V/J/K/L keys, unpinned RIO ids. content\bindings.txt already carried the merged map (arrows = W/S lever + A/D pedals) and parsed clean. Clean rebuild closed every symptom; SendInput re-verification: lever holds on release + stops at zero, LEFT/RIGHT turn in MID (wire-sign correct), V toggles with BT_SHOT pair, level boresight at spawn, R/F elevation drive; pod build rebuilt + smoked un-regressed. Code delta: BT_MPPR_TRACE [mppr-c] line enriched with mode/pedals/stick/turn/elev so one env-gated line proves the whole control surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4a38fba2c9 | Merge remote-tracking branch 'origin/master' into glass-cockpit | ||
|
|
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>
|
||
|
|
2cbfaef177 |
FE: the full authentic BattleTech mission-option catalog (TeslaSuite console)
The mission-control menu was missing player experience + many options. Added the complete set recovered from the Mac 4.10 operator console's BT:: adventure tree (TeslaSuite/410console/.../Console.ini) -- the tag= values are the egg fields; cross-checked vs the port spec + reference/cavern.egg: NEW groups: SCENARIO (Free For All / No Return respawn role), EXPERIENCE (novice/standard/veteran/expert), BADGE (7 team emblems), PATCH (8 badge colours), DROP ZONE (one..five), ADV. DAMAGE (on/off). CORRECTED: TIME (evening, not 'dusk'); COLOR (full 7: +Brown/Green/Grey/Tan, Red->Crimson, Gray->Grey); MECH (full 18 with console friendly names -> tags, was 8). btl4fe.hpp: BTFePilot gains experience/badge/patch/dropzone/advancedDamage; BTFeMission gains roleKey/roleModel. The egg writer emits the selected values (were hardcoded expert/VGL/Yellow/one/advancedDamage=1); No-Return role model = the game's 8.3 resource 'noretun'. 5-column layout (mission | mech | pilot | emblem/patch | session+controls); loadout persists across the relaunch (fe_last.ini, all 14 groups). Verified: menu drives (click Expert+Davion -> egg experience=expert badge=Davion, persisted), and the generated egg parses + runs a full mission (no egg errors, drive 61.501). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2215932016 |
KB: Gitea #12 incident findings -- @004bbd04 firing machine recovered, gauge-freeze root cause, dirty-bit gotcha
- decomp-reference: ProjectileWeaponSimulation @004bbd04 FULLY RECOVERED (capstone): fault gate reads simulationState@0x40==1 (not simulationFlags); ammo-bin gates; Loaded-case view/target denial = SetLevel(4);SetLevel(2) blip BEFORE FeedAmmo; updateModel@0x18|=1 is the replication dirty (== ForceUpdate); slot 17 @004b9c9c = rechargeLevel writer; FireWeapon @004bcc60 carries no gating. - gauges-hud: CORRECTION -- launcher recharge dial is NOT authentically static (slot 17 @004b9c9c animates it); MP gauge-window freeze root-caused = lobby-built gauge tree dangling over the launch mech re-stream + permanent SEH disable. - reconstruction-gotchas #20: updateModel@0x18 vs simulationFlags@0x28 bit 0 (DelayWatchersFlag) mis-mapping class (Emitter::SetDirty, mislanch salvo mark). - open-questions: #12 status ledger -- fixes pending a live-verify window; J/K/L preset hypothesis refuted for solo (scripted A/B); symptom-4 pip set unresolved. - GAUGE_COMPOSITE row 13 verdict corrected (WRONG-STATIC, fix pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
63b168cb92 |
SeekVoltageGraph: full reconstruction -- the eng-page POWER graph + top-box eraser (Gitea #11)
The #10 audit's one WRONG: the port Execute was a bring-up no-op, so the emitter/myomer engineering pages never erased their top data box -> stale sibling-page ghosts on the shared Eng bit-plane (SYSTEM 10 PPC showing the Streak ammo box, etc). Full faithful widget landed: - btl4gau2: ctor/BecameActive/Execute/clear/ticks/cursor recovered from the capstone disasm of @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/ @004c6c6c (Ghidra dropped every x87 arg). Plot: v=0..12000 step 1200, x=Round(response(v)*230), y=Round(v*(1/12000)*187) (ld80 @004c6bd0/@004c6d74 = exactly 1/12000); change-test samples the response at 12000V vs the 9999 activation sentinel; XOR op for tick/cursor move-by-redraw; destroyed branch centres edestryd.pcc and revives via own vtbl+0xC (BecameActive, slot 3 of PTR_0051a1fc -- vtable-dump verified). - The vtbl+0x3C sampler identified from the binary vtables: Emitter slot 15 @004bb42c = sqrt(P(v)/2.0e8), P @004bb3f4 = damageFraction*v^2*0.5* energyCoefficient; Myomers slot 15 @004b8f94 = sqrt(AvailableOutput(v)*3.6/ 350). FUN_004dd138 == sqrt (part_015.c:4026): myomers' fabs reading corrected, GetSpeedReading renamed SeekVoltageResponse. Port dispatch via complete-type bridges BTSeekVoltageSample/BTMyomersSeekSample + BTSubsystemDestroyed (databinding rule). - Emitter's AUTHENTIC attribute table recovered (binary @0x511dd4, ids 0x1D-0x25) and published: Laser*/Seek*/OutputVoltage@0x414 (currentLevel, RAW volts -- the live-cursor feed). Field renames per the table: 0x3F8 minSeekVoltageIndex / 0x3FC maxSeekVoltageIndex (static_assert-locked). The MechWeapon 0x1D OutputVoltage PORT ALIAS retired (binary table ends 0x1C; Find walks lowest-id-first, the alias shadowed the authentic row). PPC::DefaultData -> Emitter::GetAttributeIndex(). - mech4: BT_PRESET_HOLD=<n> (freeze the #9 preset cycler after n pulses) for steady-state pixel verification; BT_SEEK_LOG diag. Verified live (BLH, autofire): both #10 repro pairs held ghost-free for minutes (SYS09->SYS10 PPC, SYS02->SYS04 ERMed, SYS05->SYS06 Myomers); curves draw with live cursors; one replot per activation; quad panels/J-K-L/sec panel un-regressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f1a6ccbaaf |
Gitea #10: systematic gauge-data audit -- 45-row verdict table + findings
docs/GAUGE_COMPOSITE.md gains the AUDIT 2026-07-19 section: every widget on the 6 pod MFD surfaces + the secondary screen + the HUD reticle audited for binary-correct data (static pass vs decomp + two instrumented live sessions). Verdicts: 34 CORRECT / 1 WRONG (filed) / 6 AUTH-STATIC / 6 DEFERRED-FEED. Findings: A. The 'SYSTEM 10 PPC ammo readout' (entry a) root-caused: header/label/screen mapping are CORRECT ([vss] dump == authored sub+0x1dc); the ammo box is a GHOST -- stale pixels from the sibling ballistic/sensor page on the shared Eng plane, persisting because the port SeekVoltageGraph::Execute is a bring-up no-op. In the binary the graph IS the top-box eraser (view = (0x97,0x80)-(0x17d,0x13b) @004c6798; clear @004c6be4 on the @004c6920 activation sentinel) -- ghost-free on the pod. Full reconstruction FILED (4 Seek* attrs + vtbl+0x3C sampler + x87 recovery); no erase-only stand-in. B. HUD attr table @5110c0 conflict (entry b) RESOLVED -- see commit |
||
|
|
1d6339b226 |
Gitea #6: secondary MFD Damage/Critical/Heat cycling -- reconstruct the
NotifyOfDisplayModeChange override (vtbl+0x4C @4d1ae4) + wire desktop 'N'
The secondary screen's schematic selector was mislabeled: @004d1ae4 (the
bits-18..20 ModeSecondary* mask swap) was reconstructed as a non-virtual
"SetControlMode" that nothing called, so the desktop stayed pinned on the
Damage view. The binary's L4 vtable @0051e440 pins the truth:
+0x48 = @004d1acc <- CycleControlModeMessageHandler (FUN_004afbe0):
forwards to the base RET no-op @004b048c. A BAS/MID/ADV
control-mode change never touches the secondary view
(empirically confirmed: BT_MODECYCLE_TEST cycles the CONTROL
MODE lamp, mask bits 18-20 unchanged, schematic stays ARMOR).
+0x4C = @004d1ae4 <- CycleDisplayModeMessageHandler (FUN_004afcac):
THE Damage/Critical/Heat selector, indexed by displayMode
(table @0051dbe4 = ModeSecondaryDamage/Critical/Heat).
Authentic pod inputs (streamed type-6 .CTL EventMappings, dumped via the
new BT_CTRLMAP_LOG EVENT records): secondary-panel button 0x15 -> msg
0x15 CycleDisplayMode (manual p13, the "'Mech status Info center" bottom
left of the secondary screen), button 0x18 -> msg 0x14 CycleControlMode
(manual p6, top right), 0x10/0x11 -> ZoomIn/Out. The DOS keyboard
fallbacks (Keypress 0x13d/0x13e = extended F3/F4) are dead under the
WinTesla VK map, hence the desktop pin.
Port wiring (the M/ModeCycle pattern): key N / pad RightThumb -> action
DisplayCycle -> gBTDisplayCycle -> CycleDisplayModeNow() -- the same body
the pod console button message drives. Both .MAP profiles + the
compiled-in default updated.
Verified live (docked gauges + BT_SHOT, BT_VIEWCYCLE_TEST): the sec
panel cycles ARMOR DAMAGE silhouette -> CRITICAL DAMAGE subsystem list
-> HEAT DAMAGE colored list, mask 0x450421 -> 0x490421 -> 0x510421; M
control-mode cycling un-regressed (BAS/MID/ADV lamp cycles, view pinned).
Diags: BT_MODE_LOG, BT_VIEWCYCLE_TEST=<frame>, BT_MODECYCLE_TEST=<frame>,
BT_CTRLMAP_LOG now dumps EVENT records. KB: gauges-hud secondary-view
section rewritten, CLASSMAP +0x48/+0x4C slots, decomp-reference env
gates, GAUGE_COMPOSITE phase-4 entry resolved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4c8544b6ce |
KB: merge-reconciliation note (btinput stand-down; one input system per mode)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
75f136e9ff |
KB: step-4 closeout -- Steam wire documented, live-test procedure, router
docs/GLASS_COCKPIT.md gains the step-4 ledger (seam/transport/lobby + THE FAKEIP LESSON); NEW context/steam-networking.md topic (+ router row) and docs/STEAM_TEST.md (the 2-machine live procedure = the step-4 exit criterion on user hardware). context/glass-cockpit.md: steps 3+4 status. Final purity re-check: pod build clean, forced-walk mission runs, ZERO glass/steam tags in the pod log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6c9edc8de3 |
KB: step-3 closeout -- miniconsole verification + SDK vendor note
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
82b53b7ad7 |
KB: step-2 closeout -- verification matrix + ledger detail (glass cockpit)
docs/GLASS_COCKPIT.md gains the 2b-2e detail + the full verification matrix (purity build + seam audit, pod forced-walk, 2-node loopback MP via btconsole relay on the POD build -- both nodes reach the running mission, PadRIO authentic-path input, panel-click survival). context/glass-cockpit.md status updated: step 2 DONE [T2]; steps 3-4 pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b26e8205e3 |
Controls: the .CTL positional-id off-by-one -- CONFIRMED live and FIXED (step 2a)
The streamed L4 mapping resource carries the binary's positional attribute ids (stick=3, throttle=4) but MechControlsMapper chained from Subsystem::NextAttributeID == 2 -- every streamed record resolved ONE MEMBER LATE (attr 4 -> pedalsPosition, verified via the new permanent BT_CTRLMAP_LOG diagnostic in CreateStreamedMappings). A latent real-pod bug: the serial RIO throttle would drive the pedals member; the dev keyboard bridge masked it. Fix per the mechweap/mech attrPad idiom: ids pinned to the binary numbering, id-2 gap padded, static_assert-locked. Torso + weapon chains verified already aligned. Regression: BT_FORCE_THROTTLE headless walk clean (speedDemand=61.501 through authentic InterpretControls, gait cycles, no faults). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |