Commit Graph
60 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 fa935777cc Relay: hold the mission start until every pod is READY (loaded)
Live finding (round 3): firing RunMission while a pod is still loading
drowns that pod's load in the running mission's update streams -- the
same event queue serves both, so the load starves (the operator, always
last to join via the mech menu, loaded 5+ minutes or crashed while
testers took ~20s; solo loads on the same box take ~60s).  The launch
timer now also waits for every registered pod's READY (route -10) so
everyone enters together, 1995-style; a held launch announces who's
still loading every 10s, and a 180s cap force-fires so a wedged pod
can't hold the night hostage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 22:36:18 -05:00
arcattackandClaude Opus 4.8 0360a26612 Console: local instances get BT_AFFINITY=0xF0 (upper cores)
The game self-pins to core 0 by default; on the operator box core 0 is
shared with the console/relay/desktop, and the mission load drains
frame-paced -- a starved frame rate multiplied the operator's load time
to 3-5 min while internet testers loaded in ~20s (live finding, round
2).  The local-instance launcher now spreads the game across the upper
cores unless BT_AFFINITY is explicitly set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 22:20:30 -05:00
arcattackandClaude Opus 4.8 117b92cb24 Relay: snapshot by_host in the PEER_UP/DOWN fanouts (end-of-round crash)
When every pod disconnects at once (normal end of a multiplayer round),
a failed send inside the PEER_DOWN fanout recursively drops that peer
and mutates by_host mid-iteration -> RuntimeError -> THE RELAY DIES,
taking the round reset and the matchlog uploads with it (live crash,
first 3-player internet round).  Iterate over snapshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 22:03:16 -05:00
arcattackandClaude Opus 4.8 2f4b2f9e91 Console: seat lines speak PLAYER ordinals, not raw host ids
'SEAT 2 READY' for the first (only) player read as an off-by-one (field
report) -- host 1 is the CONSOLE in the authentic 1995 numbering, so
pods start at host 2.  Human-facing lines now read PLAYER 1/2/... with
the host id in parentheses for cross-reference against the game[host=N]
wire lines; GUI monitor regexes updated (tag-keyed as before, verified
seated/ready/left in isolation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:54:06 -05:00
arcattackandClaude Opus 4.8 54fd4a4ab0 Timestamps for the launch-chain logs (operator request)
Every relay/console print now carries a wall-clock prefix (_StampedOut
stdout wrapper; the GUI monitor regexes use search() so the prefix is
transparent), and the pod's READY/pend lines carry wall time + tick --
post-mortems measure delays instead of guessing (the 3m40s load
reconstruction needed heartbeat-counting archaeology).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:10:38 -05:00
arcattackandClaude Opus 4.8 224eaff5f8 Console: RE_RELAY_READY name collision -- the new seat-ready regex
silently replaced the WAITING-FOR-OPERATOR matcher (class-body last
assignment wins), breaking the LAUNCH button re-arm for back-to-back
missions.  Renamed the old matcher RE_RELAY_WAITOP; both paths
regression-checked in isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:04:45 -05:00
arcattackandClaude Opus 4.8 9bec4b2050 READY light: pods report load-complete to the operator roster
The roster showed "registered" for a still-loading pod and a ready one
alike -- with multi-minute contended loads the operator couldn't tell
who they'd be waiting on.  When the app ladder reaches WaitingForLaunch
the pod sends one empty route -10 frame on its live relay game socket
(BTRelayNotifyReady; socket mirrored at HELLO); the relay announces
SEAT n READY (k/m loaded) and the console GUI lights the seat
bright-green with a "N pod(s) LOADED+READY" banner.

Verified LIVE by the operator: blue (seated) -> teal (registered) ->
green (ready) in order, twice.  Also resolved the load-speed mystery:
loads are ~30s uncontended; the 1-2+ minute cases were assistant
background test runs competing with the user's interactive sessions on
the same machine (memory note added -- no game-spawning tests while the
user is testing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:57:03 -05:00
arcattackandClaude Opus 4.8 3a9b35fc06 Round reset + device-creation retry + load-time pump (rejoin trilogy)
1. ROUND RESET (the "picked Hellbringer, got a Blackhawk" field bug):
   trim/prefs finalize the session egg for ONE round, and that was
   permanent for the relay's lifetime -- a rejoin after a mission or
   crash got the stale finalized egg and its new callsign/mech request
   was recorded but never applied.  _maybe_reset_round() (both drop
   paths) restores the original egg/roster/launch state once every pod
   is gone; live beacons re-key by tag position (conn.seat_tag).
   Verified two-round e2e: blkhawk mission -> exit -> RESET -> lok1
   request -> egg rewritten -> the Hellbringer spawned.

2. DEVICE-CREATION RETRY: CreateDevice(HARDWARE_VERTEXPROCESSING) fails
   transiently when a just-exited instance still holds the adapter
   (caught live; also the parked "exit 139" join crash).  The old
   double-failure path called PostQuitMessage(1) -- which does NOT stop
   execution -- and the NULL mDevice deref right after was the real
   crash.  Now: HW->SW retry with 500ms backoff and a 20s deadline,
   then a clean error box + ExitProcess.

3. LOAD-TIME PUMP: mission load is thousands of back-to-back
   synchronous model loads; the starved message pump ghosted the window
   "Not Responding" mid-load.  BTLoadPump() (hooked from
   d3d_OBJECT::LoadObject) pumps + repaints the wait screen at most
   every 150ms pre-run; no-op once the scene is live.

Also: round-reset originals captured after the roster parse (the first
cut crashed the relay constructor -- caught by the two-round test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:57:50 -05:00
arcattackandClaude Opus 4.8 2cccf877f5 Console: Seats spinner replaces Add/Remove pilot in relay mode
Rows are capacity + per-seat defaults now -- joiners auto-populate them
live and launch trims to whoever is seated, so hand-managing rows is
busywork.  A Seats spinner (1-8) maintains the row count in relay mode;
Add pilot / Remove selected remain for mesh mode, where explicit
addresses matter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:59:56 -05:00
arcattackandClaude Opus 4.8 3ef179d1a6 Console: retire --reserve for local seats (locked the operator's own
join out -- ROSTER FULL on every seat request, field report).  Local
instances seat-request like every joiner now; trim-on-launch makes
exact seat ownership moot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:54:28 -05:00
arcattackandClaude Opus 4.8 10eee05b20 Launch with whoever connects + live roster display in the console
The operator no longer has to match the roster row count to the exact
player count (or watch the "empty seats will STALL" warning).  Two
mechanisms:

1. PRESENCE BEACONS: the pod keeps its seat-request TCP connection open
   for the process lifetime; the relay reads a live beacon as "seated"
   and a pre-claim FIN frees the seat (no ghost seats -- a ghost stalls
   every pod at the connection gate).  Reservation expiry spares
   beaconed seats.

2. TRIM-ON-LAUNCH (manual/GUI mode): operator LAUNCH before the roster
   fills shrinks the session to the seats present -- egg [pilots]/pages
   trimmed, seat ids REMAPPED by position (pods re-derive their host id
   from their tag's roster position, so walk-up prefs and beacons re-key
   consistently), roster/expected_ids shrink, eggs release, the launch
   pair fires as ACKs land.

GUI: roster rows now update LIVE from the walk-up requests (SEAT n
PRESENT/FREED lines -> callsign/mech cells + a blue "seated" status
light), and LAUNCH MISSION enables from the first seated player with a
"starts with whoever is here" banner.

Verified 2-node: 2-of-4 trim ran the mission with the requested mechs;
and the mid-roster GAP case -- three joined, the middle player quit
pre-launch (beacon FIN freed the seat), trim remapped seat 4->3 with the
survivor's callsign/mech following, both survivors registered under the
remapped ids and ran the mission.  Also: stdin reader guarded against
sys.stdin=None spawn shapes (the GUI QProcess pipe is the supported
operator channel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:40:47 -05:00
arcattackandClaude Opus 4.8 95735db5ae Operator console: relay-mode local instances go through the JOIN menu
The Launch-local-instances path pinned BT_SELF, which claims a seat
directly and SKIPS the seat request -- the join menu (and the callsign/
mech choice it carries) never ran, so a local test instance couldn't
exercise the walk-up flow.  Relay-mode local instances now launch
zero-arg with BT_FE_JOIN=1: the JOIN-GAME menu opens, the relay assigns
the seat, and the choice lands in the egg like any join.bat player.
Each instance gets a distinct relaunch port via BT_FE_JOINPORT
(btl4main JoinRelay case; default 1501) -- two -net listeners can't
share a port on one box.  Mesh-mode locals keep the direct path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:14:19 -05:00
arcattackandClaude Opus 4.8 fcedc046cf Walk-up callsign/mech request: join menu -> relay -> the session egg
The 1995 front-desk conversation, over the internet.  join.bat/
join_lan.bat now open a JOIN-GAME menu (the FE in BT_FE_JOIN trim:
CALLSIGN edit + the 18-mech list + JOIN); the choice relaunches into the
normal join with BT_CALLSIGN/BT_MECH env, rides the relay SEAT_REQUEST
as {callsign NUL mech NUL} (empty payload = roster defaults, wire-
compatible), and the relay validates the mech tag, HOLDS egg delivery
until every pod's console pad is connected, rewrites the egg via
eggmodel (vehicle= + rasterized callsign bitmaps, graceful no-PySide6
fallback) and streams it to all pads -- so every player's egg copy
carries every player's callsign.

The hold is gated on CONSOLE-PAD count, not game-side registration: a
pod HELLOs only after parsing the egg's roster, so a registration gate
deadlocks (hit live in the first run; pods animate in WAITING FOR
MISSION ASSIGNMENT during the hold).

Verified 2-node e2e (env-injected requests): thor/vulture requested over
roster defaults bhk1/ava1 -> relay held 1/2, released at 2/2, rewrote
both seats (relay log), the delivered egg carries vehicle=thor/vulture +
name=VIPER/MONGOOSE, both pods launched and built [cyl] tables for both
requested mechs.  Join-menu layout screenshot-verified; the menu's
click-through relaunch awaits live human verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:58:51 -05:00
arcattackandClaude Opus 4.8 eb618fe9e6 BT_EXPIRE: tester builds stop working 14 days after build (compile gate)
Distributed test zips go stale and stray.  BT_EXPIRE (CMake option,
DEFAULT ON so a zip can never accidentally ship without it) makes the
exe refuse to start BT_EXPIRE_DAYS (default 14) after its build day:
boot logs the remaining window, an expired build logs + shows a
"test build expired -- ask the operator for the current build" box and
exits.  btversion.h now stamps BT_BUILD_UNIX (UTC-midnight-rounded so
the only-on-change header churns once per day, not every build) +
BT_BUILD_DATE.  Dev is unaffected: every build re-stamps the day.

-DBT_EXPIRE=OFF builds a non-expiring exe; mkdist reads the cache and
marks such a zip "-noexpire" + prints a warning, and the tester README
expiry note is {EXPIRE}-marker-gated like the steam lines.  Second
compile-gate exception after BT_STEAM (convention updated in
glass-cockpit.md).  Quality gate, not DRM -- a clock rollback defeats it.

Verified live: default build logs "test build window: 14 day(s) left";
a -DBT_EXPIRE_DAYS=0 build logs TEST BUILD EXPIRED and parks on the
explanation box (window title checked) without starting the game;
restored to 14 and rebuilt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:39:19 -05:00
arcattackandClaude Opus 4.8 753540de96 MP match forensics: per-peer matchlog + relay auto-upload + matchcheck
For the 8-player playtest.  Damage authority is VICTIM-side, so no single
peer sees the whole match: every -net instance now writes a compact
machine-parseable matchlog_<date>_<time>_<pid>.txt (auto-armed by -net;
BT_MATCHLOG=1/0 overrides; solo silent), and at mission end a relay-mode
pod dials the relay game port (the seat-request throwaway-dial pattern,
envelope route -9) and streams the file back -- the relay saves every
peer's copy under matchlogs/ on the operator's machine, so testers send
nothing by hand.  tools/matchcheck.py <dir> reconciles all of them:
damage claimed (FIRE/PROJ/SPLASH/RAM, shooter-side) vs applied (DMG,
victim-side authoritative), kill/death replication across observers,
PLAYER_DEAD counts, scores, version skew, replicant-application and
unattributed-damage anomalies, mid-mission disconnects.

Hooks at the authoritative sites: Mech::TakeDamageMessageHandler (DMG,
post-base, zone + post-application damageLevel), MechWeapon::
SendDamageMessage (FIRE), projectile impact/splash/collision dispatch
(PROJ/SPLASH/RAM), wreck entry (DEATH -- a ONE-SHOT latch at
MovementMode 9, NOT the transition: a replicant arrives at 9 straight
from the type-6 record header and never runs the transition branch),
BTPlayer vehicle/death/score handlers (VEHICLE/PLAYER_DEAD/SCORE), zone
crit cascade (CRIT), APP.cpp RunningMission (MISSION), L4NET host
handlers (PEER_UP/PEER_DOWN).  Every line t=/w=/st=-stamped + fflushed.

Verified 2-node: mesh run pairs A's 22 FIRE 1:1 with B's 22 DMG (same
amounts, cylinder-resolved zones, ~200ms latency); force-damage kill run
logs 103 DMG + 3 DEATH inst=M + 3 PLAYER_DEAD across three respawn
cycles victim-side and the kill SCOREs shooter-side; relay-mode run
auto-uploaded BOTH peers' files at the mission-clock stop and matchcheck
produced the full report with exactly one (correct) anomaly -- the force
hook's claim-less damage, the unattributed-damage detector working.
Relay gained a route-specific 8MB cap for the upload frame (game frames
stay capped at 1600).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:22:10 -05:00
arcattackandClaude Opus 4.8 18ce589f4c btoperator: local instances launch with BT_PLATFORM=glass
The console's one-click local-instance launcher predates the unified build
and never set the platform env -- instances booted the DEV profile (no
PadRIO), so the legacy arrow-drive bridge owned the keyboard instead of the
bindings.txt keymap (arrows drove fwd/back/turn, Alt-reverse dead).  The
join bats already set it; now the operator's QProcess env does too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:12:08 -05:00
arcattackandClaude Opus 4.8 04d50c9242 FE menu: BT_FE_SOLO=1 trims to a solo-only menu (play_solo.bat)
The user hit the full session menu from play_solo -- HOST/JOIN LAN modes,
NET PORT and the Steam buttons don't belong in the solo flow (LAN goes
through the join bats + operator; Steam through play_steam.bat).

BT_FE_SOLO=1 (set by play_solo.bat + the btoperator generated bat):
- the MODE cycle keeps only the two solo entries (kModes lists them first:
  SOLO MISSION, RAW SOLO) via the GroupSize clamp
- NET PORT row and the HOST/JOIN STEAM buttons are not laid out
- a stale fe_last.ini mode of hostlan/joinlan is rejected by the existing
  LoadPersisted GroupSize clamp, so the selection can never land on a
  hidden mode
play_steam.bat keeps the full menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:54:09 -05:00
arcattackandClaude Opus 4.8 aaa252a4a3 play_solo.bat: open the mission menu (mech/map/options pickers)
play_solo hardcoded -egg ARENA1.EGG, bypassing the miniconsole front end
that already carries the full pickers: SOLO/RAW-SOLO/HOST-LAN/JOIN-LAN
modes, 8 maps, all 18 mechs, 4 experience levels, colours/badges/patches,
drop zones, advanced damage, time-of-day, weather and mission length.

The bat now launches zero-arg (BT_PLATFORM=glass -> the menu); the menu
relaunches the exe with the generated egg and the cockpit/gauge env
(BT_START_INSIDE/BT_DEV_GAUGES) inherits into the mission child.  Menu-boot
verified with the bat's exact env; the solo menu->mission->menu cycle was
verified at FE landing.  README + the btoperator generated-bat template
updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:49:01 -05:00
arcattackandClaude Opus 4.8 d53fcf61ad BT_STEAM restored as THE compile gate (license) -- default OFF
The unification flattened BT_STEAM into the always-on exe; the user flagged
the constraint that makes it the one legitimate compile gate: the Steamworks
SDK license bars a public release with Steam inside, so a release build must
exclude it entirely.

- CMakeLists: option(BT_STEAM OFF) restored around the Steamworks block --
  an OFF configure touches no SDK headers, no steam_api.lib, no DLL copy.
  BT_GLASS stays retired (always defined); ONE build dir either way.
  Dev checkout: build/ configured once with -DBT_STEAM=ON.
- mkdist: reads BT_STEAM from build/CMakeCache.txt.  Steam-OFF zips exclude
  play_steam.bat + steam_api.dll (explicitly filtering a STALE DLL left by
  an earlier ON build), strip the README steam section ({STEAM} line
  markers), and take a -nosteam name suffix so the flavors never clobber
  (found live: both stamped 4.11.436 and the release zip overwrote the dev
  zip).

Verified both flavors:
  ON  (dev build):  steam smoke degrades to Winsock without the client;
      zip carries play_steam.bat + steam_api.dll + README steam lines.
  OFF (scratch build-relcheck, deleted after): 0 compile errors; exe dir =
      btl4.exe + OpenAL32.dll only; boots + simulates; BT_STEAM_NET=1 inert
      (transport not in the binary); end-to-end zip has zero steam bits and
      a clean README.

KB: context/glass-cockpit.md unified-build section records the license gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:40:03 -05:00
arcattackandClaude Opus 4.8 dd74d13120 mkdist: redist runtime DLLs + versioned tester README
The first zips (pre-mkdist) shipped d3dx9_43.dll / msvcp140.dll /
vcruntime140.dll next to the exe -- testers without the VC++ / DirectX
redistributables cannot load the exe otherwise.  mkdist missed them (the
clean-extract boot test passed only because this machine has them system-
wide).  Carried into redist/ (recovered from BT411_4.11.341.zip) and
packaged next to the exe.

Also: players/README.txt -- the tester instructions (updated for the
unified build: one exe, clickable cockpit surround, trigger-config regroup,
the settled keymap, play_steam.bat) -- stamped with the version and placed
at the zip root, matching the original zips' shape.

Clean-extract verified on BT411_4.11.435.zip: root has README + 4 bats,
exe dir carries all 4 DLLs, glass-profile boot simulates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:32:11 -05:00
arcattackandClaude Opus 4.8 10ed66bd7c UNIFY: one exe -- retire the pod/glass/steam compile split
The three build dirs (build\ pod, build-glass\, build-steam\) were compile-
time splits of the same game; the tax was real: a stale steam exe shipped 2
days behind, the felt keymap flipped with the boot flavor, and cockpit
clicks were silently dead in the pod build (the trigger-config incident).

Now everything compiles into the single build\ exe:
- CMakeLists: BT_GLASS/BT_STEAM options removed; the glass TUs (PadRIO,
  bindings, panel, glass windows, plasma) + FE/console + Steam transport
  compile unconditionally; both macros always defined (seam markers).
- Steam: steam_api.lib linked with /DELAYLOAD:steam_api.dll + delayimp --
  the DLL maps only when SteamAPI is first called, and every call site
  gates on BT_STEAM_NET=1 (BTSteamNet_* short-circuit on steamActive), so
  machines without the DLL run everything but Steam sessions.
- CABINET GUARD (btl4main): the miniconsole menu previously claimed any
  zero-arg launch -- unified, that would hijack the pod cabinet's boot
  shape.  The menu now requires BT_PLATFORM=glass (or BT_FE_MENU=1);
  platform parse moved above the fe_menu_mode computation.  A bare boot
  behaves exactly like the old pod build (DEV profile, wait for egg).
- Bats (play_solo/join/join_lan/play_steam) + the btoperator.py template:
  single build\ path + BT_PLATFORM=glass.  mkdist: one exe (+DLL scan
  picks up OpenAL32 + steam_api).  build-glass\/build-steam\/build-glass2\
  (a stale Jul-20 cache) deleted.

Verified matrix on the unified exe:
  bare zero-arg boot == old pod exe (same DEV profile + egg-load path;
    the segfault on a missing egg is pre-existing, baselined vs the zip exe)
  glass + BT_BTNTEST scripted click -> [cfgmap] ENTER/EXIT (trigger config)
  runs with steam_api.dll ABSENT (delay-load proven)
  glass zero-arg -> miniconsole menu
  2-node loopback MP: 145 ticks each, cross-connected
  BT_STEAM_NET=1 without Steam client -> "staying on Winsock", game runs

KB: context/glass-cockpit.md compile-gates section rewritten for the
unified model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:28:51 -05:00
arcattackandClaude Opus 4.8 d89ae71db5 mkdist: include the Steam build + play_steam.bat when present
Rebuilt build-steam fresh (was stale at Jul 20 -- predated today's gauge/
trigger-config/keymap fixes); the DLL scan picks up steam_api.dll
automatically.  Clean-extract verified: the steam exe boots to the front-end
menu (Steam client only needed for live lobbies).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:12:57 -05:00
arcattackandClaude Opus 4.8 8765ea3571 mkdist: name zips by the BT411_<version> convention (btversion.h)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:09:37 -05:00
arcattackandClaude Opus 4.8 f7e674d9cb tools/mkdist.py: tester-distribution zip builder
Packages the player bats (zip root), BOTH exes (build\ pod fallback +
build-glass\ preferred -- the bats auto-select glass = clickable cockpit /
trigger config / bindings.txt keymap), the runtime DLLs next to each exe
(OpenAL32.dll -- the first clean-extract boot test failed without it), and
the git-TRACKED content/ files only (the working tree carries dev junk that
must not ship).  Output: dist/BT411-<date>-<hash>.zip (dist/ gitignored).

Verified: clean-extract boot test -- glass exe boots the GLASS profile and
simulates; pod exe boots as fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:08:37 -05:00
arcattackandClaude Opus 4.8 3b17b60c19 Join bats: same glass-preference treatment as play_solo
join.bat + join_lan.bat + the btoperator.py generator template now prefer
build-glass\Release\btl4.exe when present and set BT_PLATFORM=glass with it
-- so MP sessions on a dev checkout get PadRIO (mouse-clickable cockpit
buttons: trigger config, MFD banks, radar rails) and the bindings.txt keymap,
identical to solo play.  Player zips (which ship only build\) fall back to
the pod build exactly as before.

Verified: 2-node loopback MP smoke on the GLASS build (the documented
procedure: A -egg MP.EGG -net 1501 + B -net 1601 + tools/btconsole.py) --
both nodes cross-connected (GameMachineHost 1502/1602), ~150 sim ticks
each, no errors.  btoperator.py syntax-checked; regenerated bats carry the
same guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:04:02 -05:00
arcattackandClaude Opus 4.8 cae616dc7c join_lan.bat: pin the operator's LAN IP (auto-discovery was unreliable)
Field report: LAN auto-discovery (BT_RELAY=auto) didn't work -- a joiner had to
hand-set BT_RELAY=10.0.0.46:1500 to reach the console every time.

- players/join_lan.bat: BT_RELAY=auto -> BT_RELAY=10.0.0.46:1500 (the operator PC's
  LAN IP), with a comment on updating it if the IP changes.
- tools/btoperator.py: the export now DETECTS the operator PC's LAN IP (UDP
  default-route probe, no packet sent) and stamps BT_RELAY=<lan-ip>:port into the
  generated join_lan.bat instead of "auto" -- so regeneration keeps it correct and
  doesn't revert to the broken auto-discovery.  Verified: detects 10.0.0.46 here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:27:12 -05:00
CydandClaude Fable 5 ec43e365fa 4.10 literal-source campaign: manifest DP upgrade, Route A closed, toolchain facts
- manifest410.py: monotone-DP TU labeling (data-cluster affinity + CLASSMAP
  seeds + recon @addr soft votes) replaces naive fill; BT region = 917 fn /
  ~230 KB, extents match every independently-known TU boundary.
- source-completeness: ROUTE A CLOSED (Nick holds no further assets);
  toolchain = BC++ 4.52 PROVEN by CW32.LIB byte-match, archived at
  TeslaRel410/BORLAND; OPT.MAK = the shipped binary's exact recipe; extender
  corrected to Borland PowerPack DPMI32 (was "Phar Lap TNT"); "all BT headers
  survive" corrected (17/36 bt-side; mech.hpp etc. reconstruction-only);
  engine gaps enumerated (vdata.hpp first, back-dated from BT412).
- phases/phase-03: rounds 1-3 of the source410 campaign - 6/10 surviving
  originals compile clean under BC4.52; console wire IDs recovered from the
  binary ctors (Killed=9 Damaged=10 ScoreUpdate=13 DWH=15 [T1], TeamScore
  guessed 12 [T4] - answers the TeslaSuite console-port spec's open item);
  round-3 finding: MECH.HPP is the capstone grown with the mech TU
  reconstructions, BTREG.CPP green = the header-family milestone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 07:33:59 -05:00
CydandClaude Fable 5 4e01e83563 KB: EXPERIENCE LEVELS decoded -- the 0x25c block is the simulation-mode flags
The egg's per-pilot 'experience' (novice/standard/veteran/expert) is the
pod's simulation-fidelity tier -- stored at BTMission+0xe4 and seeded into
the BTPlayer ctor's flag block @4c0bc8 [T1].  The old 'per-role display
toggles / returnFromDeath' reading was WRONG: the source is
mission->experienceLevel, and the neighboring pair copies
mission->advancedDamageOn (the egg's technician splash/collision switch),
not role floats.  Known consumers: 0x260 = the HEAT-MODEL master switch
(FUN_004ad7d4), 0x25c = the novice sim-lockout (jams/searchlight/powersub),
0x274 = the raw level (FUN_004ac9c8 ==0 novice predicate).

NEW established topic context/experience-levels.md (data path, binary flag
rows per level, manual cross-refs, corrections log) + router row; sweeps in
decomp-reference/gauges-hud/open-questions/pod-hardware/subsystems.
btplayer.cpp/.hpp re-annotated with the corrected semantics; the guarded
role-based seeding stays as a marked [T3] stand-in (slot drift vs the
binary switch documented inline) until the wiring task points it at
BTMission::ExperienceLevel().

(Research by the parallel context session; committed from the glass line.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:45:37 -05:00
arcattackandClaude Fable 5 1cb93d7b75 play_solo.bat: open gauges too (BT_DEV_GAUGES), matching join bats
Solo practice already started in-cockpit (BT_START_INSIDE) but didn't
open the MFD gauges window -- inconsistent with the join bats.  Add
BT_DEV_GAUGES=1 so single-player practice mirrors the pod experience.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:15:24 -05:00
arcattackandClaude Fable 5 2fb2f8a36d join bats: default to cockpit view + gauges (BT_START_INSIDE + BT_DEV_GAUGES)
Field report: a remote player had smooth gameplay but in 3RD-PERSON with
NO gauges.  Cause: view mode + gauges are CLIENT launch flags, not
something the relay/console can enforce -- and join.bat set neither, so
remote players booted in the game's desktop default (external camera, no
MFDs).  The operator's own local launch sets both (the app's 'Start in
cockpit' + 'Dev gauges' checkboxes, on by default), which is why the
operator saw the correct view but remote players didn't.

The pod is a first-person cockpit with MFD gauges and no '3rd person' at
all, so cockpit is the right default for players.  join.bat/join_lan.bat
now set BT_START_INSIDE=1 + BT_DEV_GAUGES=1.  (Players can still press V
to toggle the external camera.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:50:21 -05:00
arcattackandClaude Fable 5 9252ce92d4 Back-to-back missions: relay re-arms on rejoin (no Stop/Start needed)
Field report: after a mission timed out normally, relaunching a mission
did nothing.  The relay was single-mission -- once it fired a launch
(launches_sent==2) it never re-armed.

Fix (operator-side only; the BINARY needs nothing -- a rejoined pod is a
brand-new process at WaitingForLaunch):
- btconsole _check_launch_gate: when a previous mission finished
  (launches_sent==2) and ALL seats have RE-ACKED (every pod exited,
  re-ran join.bat, reconnected), reset the launch state and re-print
  'WAITING FOR OPERATOR LAUNCH'.  Only reachable on a pod ACK (never
  mid-mission).  Launch is CONSUMED after RunMission #2 (launch_at=None,
  launch_requested=False) so the next press is a deliberate new round.
- btoperator SessionMonitor: reset  on each new
  'WAITING FOR OPERATOR' so the LAUNCH button RE-ENABLES for mission 2
  (it stayed disabled forever before -- the other half of the bug).

Verified e2e: two full missions on ONE relay session -- mission 1 runs +
times out on the clock, pods rejoin, relay re-arms + re-announces ready,
operator LAUNCH -> mission 2's RunMission #1/#2 fire (total fires 2) ->
pods running.  BACK-TO-BACK PASS.  Friends' zip unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:45:47 -05:00
arcattackandClaude Fable 5 0b83d71578 Harden player bats: location guard for 'cannot find path specified'
Field report: a remote player ran join.bat from the wrong place (inside
the zip, or the .bat separated from content\/build\) and got 'The system
cannot find the path specified' + dumps -- the relative 'cd %~dp0content'
failed cryptically.

Now each bat: cd to its own dir, verify build\Release\btl4.exe +
content\<egg> exist, else jump to :badpath and print a clear message
('this file must sit in the extracted folder next to content and build;
right-click the zip - Extract All and run it from there').  Verified: run
from a dir with no content/build -> the guidance prints, no dump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:16:38 -05:00
arcattackandClaude Fable 5 9690043e57 btversion: also exclude content/OPERATOR.EGG (operator-app working file) from the dirty check
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:06:45 -05:00
arcattackandClaude Fable 5 954175e44e Diagnostics: pre-launch readiness warning + named friend logs (join.log)
Two additions after the live-session troubleshooting:

1. LAUNCH-SHORT is now VISIBLE (btconsole).  Pressing LAUNCH with an
   empty roster seat previously did NOTHING (eggs_done_at only sets when
   ALL seats ACK, so the manual-launch arming silently no-op'd -- the
   'I pressed launch and nothing happened' confusion).  Now the relay
   logs 'LAUNCH pressed but NOT all seats are filled' + a per-seat
   readiness line + a WARNING naming the empty seat(s) and the fix
   (reduce the roster).  When all seats DO fill it auto-arms (launch
   request persists).  Verified: 2/3 filled -> clear warning naming
   seat3.

2. Friend logs are now retrievable (btoperator exporter).  join.bat /
   join_lan.bat set BT_LOG=join.log and their exit message tells the
   player to send content\join.log to the operator if the game closed
   unexpectedly -- closes the friend-side-crash blind spot (their log
   was the generic btl4.log, un-named, easy to miss).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:05:34 -05:00
arcattackandClaude Fable 5 9f1bd8d6d4 Operator: tee relay/console output to content/operator_relay.log
Every field bug this session needed the relay's OWN view to diagnose,
but the app only shows it in the (post-mortem-unreadable) GUI log pane.
Now _console_output also appends to content/operator_relay.log (fresh
per session; stderr is merged so relay tracebacks are captured too).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:29:33 -05:00
arcattackandClaude Fable 5 196112f2dc Fix seat-collision: reserve the operator's LOCAL seats from auto-assign
FIELD BUG (2026-07-18): operator's local instance crashed + the mission
never launched.  Root cause (confirmed by code inspection + stub test,
NOT the earlier mis-read of stale logs): the relay assigns a no-BT_SELF
joiner 'the lowest free seat'.  The operator's local instance uses an
EXPLICIT seat (BT_SELF) but takes ~15s to boot; a remote join.bat
requests a seat immediately and gets assigned the operator's seat 1
first.  The operator's later HELLO for seat 1 -> 'already registered'
-> dropped ('closed by relay') -> its seat sits empty -> the
All-connections-completed gate never fires -> game never starts.

Fix: the operator's LOCAL seats are RESERVED.  btoperator passes the
Local roster tags as ; the
relay excludes those host_ids from seat ASSIGNMENT (an explicit HELLO
still claims them).  Verified by stub test: a racing joiner is assigned
host 3 (seat 1 skipped), the operator's HELLO for the reserved seat 1
registers fine.

Also fixed the UX bug that produced the EARLIER 4-window mess: Local
now defaults checked only on the operator's own seat (row 0), not every
relay roster row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:25:19 -05:00
arcattackandClaude Fable 5 f57d25f467 Mission clock reconstructed: the console ends the timed mission
User: 'isn't the game supposed to time out tho?'  Yes -- and it was
HALF-implemented: the egg's [mission] length drives the pods' countdown
and the final-30s ranking window, and the full StopMission -> fade ->
end chain exists, but nothing ever fired it: in 1995 the CONSOLE sent
StopMission at expiry.  Our console never did, so missions ran forever.

- btconsole relay: arms the clock when RunMission #2 fires; sends
  Application::StopMissionMessage (clientID 4, msgID 6 per APP.h:383,
  exitCode NullExitCodeID) at length + 2s grace; stdin 'stop' command
  ends the mission early (works in auto and manual modes).
- btoperator: END MISSION button (enabled once launched; resets per
  session).

Verified live: 40s test mission -- relay logged the armed clock, sent
StopMission on time, the pod ran the authentic end chain and exited
cleanly ('[boot] RunMissions returned').  Standard eggs carry
length=600, so real sessions are now authentic 10-minute pod missions
with the score display in the last 30 seconds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:17:42 -05:00
arcattackandClaude Fable 5 52ce7c58c8 play_solo.bat: single-player practice launcher in the player export
A zip recipient can dork around offline -- no -net argument means the
game self-launches a solo mission (all maps ship in content/).  Solo
gets its own exit message (btl4.log pointer, no server language).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:57:26 -05:00
arcattackandClaude Fable 5 dfd1894fb9 Unreachable-server UX: message boxes + bat guidance instead of a 'crash'
Field report (first internet-join attempt): both join bats 'hard
crash' on the remote machine.  Root cause was procedural -- the host
session was not running -- but the failure PRESENTATION was the bug:
Release Fail() is a bare abort(), so a dead relay = silent process
death + instantly-closing console window.

- L4NET: the two player-facing dead-ends (LAN discovery no-answer,
  seat request unreachable/full) now show a MessageBox saying what
  happened and what to do before exiting (verified live against a
  dead relay: 'BattleTech -- can't join the game' appears).
- join bats: echo + pause after exit so the window stays readable;
  join_lan.bat header now says it is LAN-only (the remote user ran
  both -- join_lan can never work over the internet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:23:29 -05:00
arcattackandClaude Fable 5 d173f088a6 Operator: loud warnings for the two silent launch-local failures
'Launch local instances' did nothing visible when no roster row had
Local checked (log pane quietly said 'launched 0') or when the session
was not started yet (instances would dial a dead relay and fail after
the bounded retry).  Both now pop a clear dialog saying what to do.
Found live: user pressed the button with both Local boxes unchecked
and no session running -- 'nothing happened'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:12:38 -05:00
arcattackandClaude Fable 5 7e49fb2284 Vehicle sweep: ALL 18 mech ModelList names certified playable
The egg generator's known-good list was just bhk1+madcat (the only two
ever exercised as egg vehicles); the operator roster therefore leaned on
those.  Swept every ModelList candidate as a solo egg vehicle: 18/18
boot a mission, spawn, and animate with no crash -- avatar, blkhawk,
loki, owens, sunder, thor, vulture and all short variants (ava1, lok1/2,
mad1/2, own1, snd1, thr1, vul1) join bhk1/madcat as first-class picks.
eggmodel now lists the canonical 8 first for the operator dropdown.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:46:55 -05:00
arcattackandClaude Fable 5 7c9958e8da btversion: exclude the game-rewritten content/LAST.EGG from the dirty check
The game rewrites LAST.EGG at every run, so every build stamped '+'
(false-dirty) even from a pristine checkout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:25:03 -05:00
arcattackandClaude Fable 5 fad962bec8 Version stamping: 4.11.<git commit count> (<hash>[+])
4.10 = the 1995 arcade release; 4.11 = the win32 reconstruction; the
build number is the git commit count -- monotonic, zero-maintenance,
and every exe pins to exact source via the short hash ('+' = built
from an uncommitted tree).  tools/btversion.cmake regenerates
build/btversion.h on every build (write-if-changed, no rebuild churn);
the stamp shows in the boot banner (btl4.log head, replacing the stale
'v4.10' line) and the window title (incl. the MP node tag).

Verified: build stamps 4.11.311 (980c9cd+), banner + title correct on
a live boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:22:15 -05:00
arcattackandClaude Fable 5 5e993de99d Operator-set callsigns: the console's name-bitmap generator recreated
The 1995 console let the operator type player callsigns; the pods render
them from 1bpp BITMAPS streamed in the egg ([largebitmap]/[smallbitmap]
lists + hex raster pages, bitmapindex per pilot; large 128x32 on the
score/ranking display via DPLRenderer name textures, small 64x16 on the
kill-feed and radar/target labels). The textual name= pilot key is dead
in the shipped binary -- its pilot parse reads bitmapindex only (decomp
part_014). Historical bug surfaced by this work: every egg in the repo
listed only ONE name bitmap (Aeolus) while pilot 2 pointed at index 2,
so player 2+'s callsign has never rendered (null-checked, silently
blank).

eggmodel.py: EggDoc.set_callsigns rewrites the lists + one raster page
per callsign per size + per-pilot name=/bitmapindex= (duplicate names
get disambiguated page tags); get_callsign_rows decodes pages back
(exact inverse of the engine's nibble-stream parse, GRAPH2D.cpp:340,
MSB-first bit order :558); rasterize_callsign/make_callsigns render
text via Qt (auto-shrink to fit, NoAntialias, 1bpp threshold) -- the
only PySide6-dependent corner of the module. validate() now catches
dangling bitmapindexes and missing raster pages.

btoperator.py: Callsign column in the roster (free text, PLAYERn
default); every save rasterizes the column into the egg exactly like
the original console did.

MP_BHMC.EGG template repaired: authentic Aeolus raster preserved
bit-exact, Boreas page generated and listed.

Verified: eggmodel self-test 20/20 (bit-exact raster round-trip, dup
disambiguation, dangling-index catch); ASCII-art decode of the
app-generated VIPER/MONGOOSE rasters reads correctly; live 2-node
relay session on the generated egg -- full ladder, launch, zero
name-bitmap load warnings; operator GUI e2e PASS after the column
shift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:29:57 -05:00
arcattackandClaude Fable 5 f04e8019c2 D1: relay-assigned seats -- players never need a player number
A pod launched with no BT_SELF asks the relay for a seat before joining:
new control frames SEAT_REQUEST (-6) -> relay reserves the lowest roster
seat not claimed or reserved (60s reservation so simultaneous joiners
can't race onto one seat) -> SEAT_ASSIGN (-7, int32 hostID + NUL tag)
becomes relaySelf; everything downstream (egg self-match, HELLO) runs
exactly as if BT_SELF had been set. Roster full -> SEAT_FULL (-8) ->
clean Fail(). A real HELLO pops the reservation; a pod drop frees the
seat. Explicit BT_SELF still claims a specific seat (the operator's
local launches use it).

Client: L4NetworkManager::RelayRequestSeat (throwaway TCP dial to the
relay game port, 10s reply window). Relay: SEAT_REQUEST branch in
_handle_game_frame. Operator exporter collapsed from per-seat
join_as_playerN.bat to ONE universal join.bat (+ join_lan.bat with
BT_RELAY=auto) -- every player gets the same file, first come first
served, the arcade walk-up-to-a-pod model. players/ regenerated.

Verified: 8/8 seat stub tests (distinct seats in roster order, FULL on
exhaustion, claimed+reserved stays FULL, HELLO claim accepted, duplicate
HELLO refused); 2-node localhost e2e with NO BT_SELF on either pod ->
both seated, full ladder, RunMission pair, UDP flowing post-launch;
regression smoke: explicit-BT_SELF relay session clean with zero seat
requests, classic mesh (no BT_RELAY) un-regressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:11:03 -05:00
arcattackandClaude Fable 5 efd440c64b Relay hardening: only ACKED pods count toward the launch gate
Found live during the port-forward verification: external port-checker nodes
(and by extension any internet scanner) that connect to the exposed console
port were streamed the egg AND counted as pods ('1/2 pods have it' from a
node in Turkey) -- a lingering scanner could hold a phantom roster slot or
nudge the ready/launch state.

Real pods send AcknowledgeEggFileMessage (clientID=0, msgID=4) after parsing
the egg (L4NET StartConnecting); scanners never speak the protocol.  The
launch gate (auto timer AND manual-launch READY) now counts only console
connections that ACKED:

- RelayConsoleConn.acked; _console_read detects the ACK, logs
  'pod ACK from <addr> (n/m ready)', drives _check_launch_gate.
- _eggs_out (egg_sent count) replaced by _pods_ready (acked count);
  gate logic extracted to _check_launch_gate (called on each ACK).
- btoperator monitor regex updated to the ACK line.

Verified: scanner-simulation (3 lingering non-protocol connections + 1 real
ACK on a 3-pilot roster -> gate NOT tripped, ACK counted 1/3); full operator
e2e PASS (registered -> READY held -> operator launch -> both pods LAUNCHED).

(The two intermediate e2e failures during this work were a stale port-check
relay squatting on 1500 -- a test-rig cleanup bug (Get-Process CommandLine
filter matches nothing; use Get-CimInstance Win32_Process), not the
hardening.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:49:58 -05:00
arcattackandClaude Fable 5 5df5c954ae Operator: manual launch gate + restart-session flow
The operator now holds the lobby: with 'Manual launch' checked (default),
the relay stops at 'all pods have the egg; WAITING FOR OPERATOR LAUNCH'
instead of auto-firing on a 20s timer -- the app shows 'ALL PODS READY --
press LAUNCH MISSION' with the roster lights, and the big green button
fires the mission when the operator is ready (everyone seated, voice chat
confirmed).  A 'Restart session' button cycles the same mission/roster for
the next round (players just re-run their join script).

- btconsole.py --relay --manual-launch: launch armed by the line 'launch'
  on stdin (a daemon reader thread; the app's Launch button writes it via
  QProcess).  The settle window is still honoured relative to egg delivery
  (max(now, eggs_done + 20s)) -- firing RunMission before the pods reach
  WaitingForLaunch Fail()s them, the same hazard the auto timer guards.
  Auto mode (no flag) unchanged -- CLI/test recipes unaffected.
- btoperator.py: Manual-launch checkbox (Network box, default on), LAUNCH
  MISSION button (enabled by the relay's READY line, disabled after fire),
  Restart session button, status headline shows the ready state.
- operator_e2e.py: now exercises the gate -- waits for READY, asserts the
  button enabled, presses it, verifies the operator-gated LAUNCH.

Verified: scripted e2e PASS -- registered -> READY (held) -> operator
launch -> settle honoured -> both pods LAUNCHED (operator-gated=True).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:32:54 -05:00
arcattackandClaude Fable 5 1436d27ac6 D1 phase 7: LAN auto-discovery (BT_RELAY=auto) + operator LAN scripts
LAN players now find the game with zero configuration -- the 90s arcade
model (cabinets locate the operator station) restored:

- Client (L4NET RelayDiscover): BT_RELAY=auto broadcasts 'BTR1DISC' on
  udp/15999 (255.255.255.255 AND 127.0.0.1 -- broadcast doesn't reliably
  loop back on Windows; covers same-box sessions), 5 x 1s attempts; the
  answering relay's SOURCE IP + its advertised console port become the
  relay address.  Explicit <host>:<port> path unchanged; mesh untouched.
- Relay (btconsole.py): best-effort discovery responder on udp/15999
  answers 'BTR1HERE' + <u16 consolePort>; degrades gracefully (logged) if
  the port is taken.
- Operator app: Export player scripts now writes a join_as_playerN_lan.bat
  pair (BT_RELAY=auto) next to each internet script -- LAN guests
  double-click and are found; internet guests use the public-host script.

Verified 2-node: both pods BT_RELAY=auto -> probe answered through a real
interface (not just loopback) -> discovered 172.19.x.x:1500 -> full session
to RunningMission.  KB: multiplayer.md D1 section updated (+ the operator
console entry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:12:01 -05:00
arcattackandClaude Fable 5 43fa53d542 Operator console app: PySide6 station + validated egg model (the lost 1995 console, recreated)
The operator station the archive never preserved: build missions with
authoritative dropdowns, run the console/relay, watch pods arrive, launch --
LAN mesh or internet relay -- plus one-click local instances for testing.

tools/eggmodel.py (headless, importable, self-tested 14/14):
- Order-preserving egg parse/emit: the editor manages [mission]/[pilots]/
  per-pilot pages; bitmap rasters, ordinals and role pages survive untouched.
- EVERY value set read LIVE from content/BTL4.RES (the resscan parser,
  importable): maps = MakeMessageStream(14) INTERSECT ExistanceBoxStream(26)
  (the game aborts unless both exist); colors/badges/patches from the
  VehicleTable resource (type 25, NUL-line NotationFile text with ;-comments);
  vehicles = ModelList names, known-good mechs (bhk1, madcat) surfaced first.
- validate(): the full required-key ruleset from MISSION.cpp/btl4mssn.cpp
  (missing map/time/weather/scenario, per-pilot hostType/vehicle/dropzone/
  color/patch/badge/experience/role, role page model=, illegal values,
  duplicate addresses) -- kills the hand-edited-egg crash class, incl. the
  color=Red-vs-Crimson gotcha that bit the dev eggs.

tools/btoperator.py (PySide6, dark Fusion theme):
- Mission form + pilot roster table (all dropdowns from eggmodel), relay-tag
  auto-numbering, add/remove pilots, egg New/Open/Save/Validate.
- Mode switch: Relay (internet; runs btconsole --relay) / Mesh (LAN legacy;
  dial-out console at address-port minus 1 per the +1 rule).
- Session panel: QProcess drives btconsole.py; stdout parsed live into
  per-pilot state lights (waiting/egg/registered/LAUNCHED) + relay stats.
- Local launcher: spawns btl4.exe per roster row with BT_RELAY/BT_SELF (or
  mesh -net port), BT_DEV_GAUGES/BT_START_INSIDE toggles.
- Export player scripts: per-pilot join_as_playerN.bat with the public
  relay hostname (a remote player's whole setup = run one .bat).
- Wire/protocol code stays in btconsole.py/eggmodel.py -- the GUI is only
  UI + process management; CLI + test harnesses share the same tested core.

Verified: eggmodel self-test 14/14; scripted end-to-end
(scratchpad/operator_e2e.py) drives the real window through its own methods --
start relay session -> launch 2 local instances -> monitor observes the
mission LAUNCH (user-confirmed live on screen: both pods in-mission).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 10:57:30 -05:00
arcattackandClaude Fable 5 bf8c2ffaae D1 phase 1: relay TCP core in btconsole.py (--relay mode)
The console emulator gains a relay/dispatcher mode for internet play (D1):
pods dial OUT to this process (NAT-friendly) instead of the console dialing
into each pod.  Legacy dial-out mode is untouched (verified: an unmodified
2-node mesh session still forms end-to-end through it).

--relay <consolePort> <egg> [--bind ADDR] [--udp-drop PCT], single
selectors-based event loop:
- console listener (consolePort): streams the NUL-line egg to each pod on
  connect; GLOBAL launch timer -- RunMission x2 to ALL pods at +20s/+4s after
  the LAST pod has its egg (fixes the per-pod skew of the legacy tool).
- game TCP listener (consolePort+1): envelope router {route i32, length u32}.
  First frame must be HELLO ('BTR1' + hostID validated against the egg
  [pilots] roster); PEER_UP/PEER_DOWN exchange; route>=2 unicast (route
  rewritten to sender), -1 broadcast-except-sender (the client sends each
  broadcast ONCE -- kills the N-1x upload duplication).  Frame cap 1600
  (NETWORKMANAGER_BUFFER_SIZE); protocol violations drop the connection.
- game UDP socket (consolePort+1/udp): endpoint learned per-datagram
  (NAT-rebind tolerant), HELLO->HELLO-ACK, verbatim forward by route,
  TCP-wrap fallback when the target's UDP endpoint is unknown; --udp-drop
  test hook for the robustness phase.

Verified: scratchpad/relay_stub_test.py (self-contained; spawns the relay +
stub clients) -- 24/24 checks pass: egg delivery, registration, PEER events,
unicast route-rewrite, broadcast fan-out (sender excluded), UDP ack/forward/
fallback, and all reject paths (bad magic, dup id, out-of-roster, oversize).

Plan: ~/.claude/plans/partitioned-snuggling-piglet.md (D1 relay + UDP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:44:01 -05:00