Commit Graph
351 Commits
Author SHA1 Message Date
CydandClaude Fable 5 8cf2cc1f07 FE: the RP412 menu + lobby-room pattern; flat single-exe dist (per Cyd)
Menu (btl4fe, modeled on RP412 RP_L4/RPL4FE.cpp): full catalogs as
always-visible clickable LISTS in three columns with friendly names --
8 maps, the canonical 8 certified mechs, colors, times, weather, lengths,
modes -- bright/dim green with a selection chevron, double-buffered paint,
real EDIT controls for pilot name + LAN peers (green-on-black via
WM_CTLCOLOREDIT), framed LAUNCH / HOST STEAM / JOIN STEAM buttons, and the
loadout persisted across the per-mission relaunch (fe_last.ini, gitignored
implicitly by *.ini? no -- plain file beside the eggs).  ENTER/ESC kept.

Lobby room (RP412 RPL4LOBBY look): pilot roster with loadouts (mech, color),
framed L A U N C H  M I S S I O N (owner) + LEAVE LOBBY buttons, clickable.

Dist: FLAT layout (the RP412 convention) -- ONE btl4.exe entry point at the
folder root beside the content tree (the shape TeslaConsole manages), DLLs +
launchers at root, content flattened from git-tracked files.  Verified: the
staged flat tree boots the Mission Console zero-arg from its own root; the
click-driven menu launches a full marshaled mission (row click -> length=60
in the egg, LAUNCH click -> ladder -> 41+ ticks, fe_last.ini written).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:54:25 -05:00
CydandClaude Fable 5 ec3985e9da Panel: condensed layout -- keypads dropped, board columns centered + raised
Per Cyd: the two 4x4 hex keypads leave the glass panel (engine keypad units
stay reachable via bindings.txt); the four board columns (Thr/Sec/Scr/Joy)
move to the center between the lower MFD stacks and rise into the vacated
internal-keypad space -- 72 controls, ~5 rows shorter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:47:28 -05:00
CydandClaude Fable 5 f40ba58c60 Panel: drop the two hex keypads (per Cyd) -- 72 controls
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:46:15 -05:00
CydandClaude Fable 5 4c8544b6ce KB: merge-reconciliation note (btinput stand-down; one input system per mode)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:46:56 -05:00
CydandClaude Fable 5 f889e24ce0 Merge origin/master: the D1 relay/operator line + input remap meet the glass layer
33 master commits in (relay TCP/UDP + PySide6 operator console, CONTROLS.MAP
+XInput binding engine, camera seats, torso pitch aim, sign fixes, the 1995
manual, version stamping, 18-mech certification).  Conflicts: .gitignore +
CLAUDE.md router rows (combined).

SEMANTIC RECONCILIATION (the one real overlap): masters btinput binding
engine (ungated, CONTROLS.MAP) and the glass PadRIO (gated, bindings.txt)
would both read the keyboard/pad in a glass+PAD session.  btinput now joins
the stand-down convention: BTInputPoll yields (and BTInputSuppressKey claims
NOTHING, so authentic hotkeys flow) when an operational cockpit device owns
the input path -- BTRIODevicePresent, BT_KEY_BRIDGE force-override honored,
forced harness exempt.  One input system per mode: btinput on pod/dev
desktops, PadRIO on glass.  The mechmppr/mech4 bridge merges composed clean
(masters negate-once sign fix inside our device-gated bridge).  The D1 relay
keeps its own raw sockets by design (an alternative LAN wire; Steam and
relay are separate modes).

Verified post-merge: all 3 configs build; glass boots with [input] binding
engine standing down + PadRIO owning input (30 ticks); pod forced-walk
speedDemand=61.501 with btinput ACTIVE; 2-node loopback MP full 31/31
mission, 76/76 ticks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:46:41 -05:00
arcattackandClaude Fable 5 ff5260ce98 Coolant-loop cross-check vs the 1995 manual: structure strongly faithful
Dumped each mech's subsystem->coolant-loop (BT_SPEC_LOG in mech4: walks
the roster once, resolves each subsystem's linked-condenser number via
BTCoolingLoopFrame) and diffed all 6 manual mechs' COOLANT LOOPS tables.

RESULT -- the structure survives the 4.0->4.10 gap remarkably well:
- BACKBONE identical on ALL 6: Generator A/B/C/D on loops 1/2/3/5,
  Sensors(our Avionics) on loop 2, Myomers on loop 5, LRMs on 1&3,
  autocannon on 4, big energy weapon on 6 -- exact match
- weapon LOADOUT identical on 5 of 6 (Thor/Vulture/MadCat/Owens/
  Blackhawk); Loki is the one full rework (4.0 PPCx2/AFC100/SRM6 ->
  4.10 AFC50x2/ER Medium x2/SRM4)
- the consistent 4.0->4.10 change is a small-laser REDISTRIBUTION: the
  2 ER Small Lasers moved off the sensor/heavy loops onto the energy
  loops 4&6 (Thor/MadCat/Vulture); Owens near-perfect (loops 1&3 exact)
- Loop 0 = correctly UNCOOLED infrastructure (condensers, reservoir,
  gyro, torso, HUD, ammo bins, ...) -- not a coolant loop

CONCLUSION: the coolant-loop reconstruction is faithful; every diff is
4.0->4.10 balance tuning, not a bug.  BT_SPEC_LOG retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:37:14 -05:00
arcattackandClaude Fable 5 f4cf1e631a Spec cross-check vs the 1995 manual: reads CORRECT, numbers are 4.0-vs-4.10 drift
Audited our live streamed subsystem config against the manual's per-mech
stat sheets (new BT_SPEC_LOG dump: torso speed/limits, heat-sink count,
reservoir capacity, at subsystem ctor).

RESULT: structure + semantics align perfectly; the tuning NUMBERS
differ -- because the manual is Tesla 4.0 and our content is release
4.10.  Point-release balance drift, NOT a reconstruction bug:
- Owens + Blackhawk are the two fixed-torso mechs in BOTH (speed 0,
  limit ~0, torsoHorizontalEnabled=0); the other four twist-enabled --
  our reads correctly identify every case
- torso speed/limit + heat-sink count exist per-mech exactly as the
  manual documents; only the values were retuned 4.0->4.10 (e.g. torso
  speed MadCat 80->50, heat sinks Loki 38->15)
- reservoir coolantCapacity reads a flat 20 = the coolant THERMAL
  capacity (game units), NOT the manual's per-mech 'liters' (a
  display/flavor quantity with no single subsystem field) -- different
  measures, not a bug

CONCLUSION: do NOT retune content to the manual -- BTL4.RES is the
authentic 4.10 shipping data; the 4.0 manual's numbers are an earlier
pass.  The manual CONFIRMS our structural fidelity.

BT_SPEC_LOG retained for re-auditing.  Follow-up (structural, less
drift-prone): the per-mech coolant-loop weapon/generator assignments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:28:43 -05:00
arcattackandClaude Fable 5 cb82d8c1d0 Turn/twist/free-aim sign family: negate ONCE at the key bridge (user-verified)
User live-tested: 'if i push the left arrow the mech turns right' --
correct, and it invalidated my earlier screenshot-forensics 'D=right
verified' claim (the yaw telemetry actually agreed with the user all
along: D gave POSITIVE yaw = CCW = LEFT; the sim uses math convention,
positive = counter-clockwise, for turn AND twist).

On the pod the RIO Ranger owned the hardware sign; the desktop bridge
now negates in ONE place per channel family:
- key_turn = -gBTDrive.turn (forced/BT_GOTO harness demands stay
  sim-frame, un-negated)
- stickPosition.x = -gBTTwistAxis (Standard/Veteran torso stick)
- SetFreeAimSlew call-site negations REMOVED (the bridge negation now
  flows through; double-flip removed)

Matches the manual (p8): 'pulling your joystick to the right torso
twists your Mech to the right.'  User verified live: arrows steer
correctly (Blackhawk, basic mode) and MadCat torso twist is correct
in middle mode.

LESSON (recorded): large-rotation screenshot comparisons are AMBIGUOUS
(both directions put 'new scenery at an edge'); trust tracked
landmarks, numeric telemetry with an established convention, or the
user's live observation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:20:19 -05:00
arcattackandClaude Fable 5 02d5543c7f Archive the ORIGINAL 1995 player manual (from Nick) + alignment audit
reference/manual/Tesla40_BT_manual.pdf -- the genuine Tesla 4.0 BT
player manual, 34pp.  Primary-source alignment audit (pod-hardware.md
SManual): the reconstruction matches on EVERY checked control behavior:

- control modes are named BAS/MID/ADV in print (mechanics identical to
  our Basic/Standard/Veteran incl. the ADV no-auto-slow turn clamp)
- stick right = torso right in MID/ADV (today's sign fix); Blackhawk
  and Owens named as THE fixed-torso exceptions; twist arc ~120
  (per-mech: Loki/Thor tables say 110 deg limit, 60 deg/s)
- continuous throttle lever; reverse = HOLD the red throttle button
  (our 0x3F hold-state model)
- Hot Box MFD = callsign buttons + CLOSEST (our hotbox button 8 ->
  ChooseNearestPilot); radar travel-oriented w/ 60-deg wedge sweeping
  on twist; map zoom +/- beside the secondary screen
- experience gates the hunting aids (standard sim only); expert adds
  movement heat
- per-mech stat sheets + coolant-loop tables = a systematic
  cross-check source for our streamed subsystem resources

NEW LEADS the manual surfaces: CROUCH (button by the secondary screen;
duckState attr + SQUAT clips exist, no input drives them), EJECT
(button beside the joystick; EjectButton01 sounds exist), the hot-box
viewscreen framing (deferred PNAME marker chain).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:04:16 -05:00
arcattackandClaude Fable 5 af80e62070 Fixed-torso free-aim left/right was reversed (tester report confirmed)
Field report: 'left/right is reversed'.  Investigation with screenshot
+ numeric verification found it channel-specific:

- A/D leg steering: CORRECT (D tap -> scenery slides left = turns
  right; verified via cockpit screenshots)
- Torso TWIST (turning-torso mechs, e.g. MadCat): CORRECT all along
  (E -> currentTwist +0.27 = right -- consistent with weeks of
  validated play; an interim blanket negation that broke this is
  reverted)
- Fixed-torso FREE-AIM slew (Blackhawk & friends): REVERSED -- E/right
  panned the view LEFT.  The slew consumer pans opposite the twist
  convention; on the pod the RIO layer owned the hardware sign.  Fix:
  negate stick_x ONLY at the two SetFreeAimSlew call sites
  (Standard + Veteran branches).

Verified after fix: MadCat E -> currentTwist +0.270 (right); Blackhawk
E -> view pans right.  New BT_INPUT_LOG diags: [input] twist
in/currentTwist (Standard branch) + mechYaw in the Basic elev line.

Note for testers: the tester was almost certainly flying a fixed-torso
mech -- 'reversed' reports should always record WHICH mech.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:59:09 -05:00
arcattackandClaude Fable 5 1271d3bc76 Torso elevation (pitch aim) wired: the pod stick's Y axis lives
The mechs could always tilt their aim up/down -- Torso models the full
vertical axis (currentElevation, rate, VerticalLimitTop/Bottom,
recenter) and EVERY 1995 control mode routes stickPosition.y into
Torso::SetAnalogElevationAxis -- but the desktop bridge hard-zeroed
stick Y, so the axis was dead on a keyboard rig.  The one pod control
the remap left unwired.

- btinput: JoystickY axis -> elevTarget/elevActive/elevAbsolute
- mech4 shim: sElev integrator (same walk/spring model as the twist;
  X recenters pitch too via gBTElevRecenter)
- mechmppr bridge: feeds stickPosition.y every bridged frame (the old
  unconditional zero removed); both mode branches covered
- CONTROLS.MAP (+ numpad profile + compiled default): R/F = aim
  up/down, pad LeftStickY = elevation (was unused)
- torso.hpp: CurrentElevation()/ElevationVelocity() accessors (diag)
- [mppr] trace gains stickY (note: the trace reads AFTER the next
  frame's device push re-zeroes the stick -- input flows regardless)

Verified live: R held -> torso elevation climbs at the authored rate
and clamps at 0.349066 rad = exactly 20.0 deg (the Blackhawk's
VerticalLimitTop); release holds the aim.  The eyepoint correctly
stays level -- pitch aims the GUNS and reads on the HUD's vertical
elevation tape, as in the pod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:44:22 -05:00
arcattackandClaude Fable 5 0a77d8e54b Camera-seat ranking window LIVE: the 1995 broadcast overlay reimplemented
The stubbed dpl-instance ranking display is reborn as screen-space
quads in CameraShipHUDRenderable::Render:
  - followed-player callsign banner (bottom center; the old direct
    index was correct -- Execute already converts to the 0-based
    texture slot)
  - the RANKING WINDOW: one row per scoring player, [ordinal][callsign]
    in rank order right of center; visibility = the Director's
    authentic flash logic (10s on / 15s off, solid final 30s); rows
    follow LIVE playerRank pointers so they re-sort as scores change
  - discovery: each 128x32 ordinal bitmap packs TWO ordinals side by
    side ('1st|2nd', '3rd|4th' -- why 4 bitmaps serve 8 players);
    draw = texture rank/2 with a u-half selected by rank parity
  - alpha-blended A4R4G4B4 white-on-transparent textures x green
    diffuse = the authentic green look

BT_SHOT moved AFTER the 2D pass -- it captured the backbuffer pre-HUD,
so overlays were on screen but invisible to screenshots (cost one
false debugging round).  GetOrdinalTexture accessor added.

Screenshot-verified: '1st MAVERICK' standings + MAVERICK banner over
live auto-directed coverage; 2-node mech smoke PASS (un-regressed;
the new render path only executes when a CameraDirector HUD exists).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:32:23 -05:00
arcattackandClaude Fable 5 4459262b9c Camera seat LIVE + Mech::PlayerLinkMessageHandler reconstructed (@0049f624)
The spectator/broadcast seat works end-to-end: a hostType=1 +
vehicle=camera pilot page boots a CameraShip whose 62-camera arena1
network loads from the BTL4.RES type-27 resource (the engine's
CreateStreamedCameraInstances existed all along -- the file probe falls
back to it), the BTCameraDirector locks onto the first live mech, and
the ship TRACKS it (sees=1, goal advancing) and CUTS between authored
camera positions (screenshot-verified: wide arena shot -> close
tracking shot).

The missing piece was the Mech override of PlayerLinkMessageHandler
(@0049f624), which the reconstruction never filled.  The engine base
resolves mech->player; BT's override adds:
  1. the REVERSE link player->playerVehicle = this on EVERY node
     (replicants included) -- how the camera director and scoreboard
     find a REMOTE player's mech; without it a spectator parks forever
     ('NO goal entity')
  2. clears NonScoringPlayerFlag (0x4000 = bit 14): a pilot with a
     vehicle is a SCORING player -- this admits REPLICATED players to
     the ranking pass, so cross-node rank/score displays work
  3. master only: seeds the heat bank's ambientTemperature (bank
     @0x1d4) from the mission [mission] temperature= (BTMission+0xf4)
     -- correcting the heat family's 'frozen 300' deviation note (it
     was never frozen; THIS is the writer)

Bridge BTSetBankAmbientTemperature lives in heatfamily_reslice.cpp
(mech.cpp cannot include subsystem headers -- local-stub collision).
BT_CAM_LOG diagnostics: camera-network count, director pick + Players
group census, ship follow state, link dispatch/receive.

Verified live: 2-seat (mech + camera) relay session -- replicated
player shows +veh, director goal locked w/ 30s timer, ship tracking a
moving mech through camera cuts; standard 2-node mech smoke PASS
(un-regressed).  Lesson re-learned: a 'clean' build filtered on 'error
C' missed a linker file-lock failure -- one whole test cycle ran
against a stale exe (the /FORCE gotcha's cousin; grep -i error, not
error C).

Remaining (task open): ranking-window overlay draw (L4VIDRND stubs),
operator-app camera-seat row, shot polish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:10:29 -05:00
arcattackandClaude Fable 5 74d9d3b6ff Camera networks FOUND: BTL4.RES type 27, all 8 maps -- KB corrected
The 'cameras\ directory missing, ask Nick' entry (a03b16f, same day)
was WRONG: the file path is only the DEV OVERRIDE (the RP-style loose
.CAM files -- present for RP in the nick-games 410SRC archive, which
prompted the re-check).  The shipped binary falls back to a BTL4.RES
resource TYPE 27 (0x1b) named after the map (FUN_0042be3c probe ->
FUN_0042c078 res loader; per-cam binary records, parsers @0042ad54/
@0042b9f8) -- and our res has ALL 8 maps' networks (arena1 = 62
cameras; cavern/dbase/grass ~57KB each).  First arena1 record parses
clean (pos+quat, type 2).

The spectator/broadcast seat needs no external data: remaining work is
the type-27 loader in the WinTesla CAMMGR (file flavor only today),
the stubbed ranking-window draw, and a camera-vehicle boot test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:29:48 -05:00
arcattackandClaude Fable 5 a03b16fe6c KB: camera-seat/broadcast system investigated -- cameras\ data missing
The CameraShip spectator system is engine-complete (CameraDirector
auto-follows the score leader, CAMSHIP cuts between fixed map camera
positions like TV coverage, ranking window flashes 10s/15s + final
30s), but the per-map cameras\<mapname> notation files are absent from
the archive -- added to the get-from-Nick list with the reconstruction
alternatives (author our own / chase-cam fallback) and the two code
gaps (stubbed ranking-window draw, camera vehicle never boot-tested).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:24:51 -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 f9f230c62b Patient walk-up: a pod at a dead relay WAITS for the session
User request after the first internet join attempt: 'should we modify
the program so it sits and retries with a waiting-for-session?'  The
arcade model is a pod that waits for the operator, not one that
aborts.

- RelayRequestSeat: retry loop (2s dials, 30 min cap) with live status
  into the join.bat console window (BTRelayWaitStatus: AttachConsole to
  the parent cmd; silent without one) -- banner + progress dots, a
  distinct 'game is FULL, waiting for a free seat' state, and 'seat
  assigned -- joining!' on success.  The unreachable MessageBox now
  only fires at the 30-minute give-up.
- BT_RELAY=auto discovery: same patient loop ('searching for a game on
  this network...'), LAN-only guidance in the give-up box.

Verified live: pod launched against a dead relay, waited 20+s, relay
started, pod seat-requested/registered/UDP-up automatically with no
user action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:04:46 -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 0af0407534 MP4.EGG: mixed lance (MadCat/Thor/Vulture/Avatar) -- user-verified 4-pod session
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:00:22 -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 17176058cc First 4-pod session VERIFIED + the EntityID-cast diagnostic trap
4 pods, relay-assigned seats, 4-pilot MP4.EGG (eggmodel-generated incl.
callsign bitmaps): 4 distinct seats, full ladder, every node replicates
all 3 foreign mechs (host-qualified [repl] sets are exact complements),
4 distinct spawn positions (no dropzone stacking), UDP at 4-pod fan-out
(relay tx=3x rx, zero drops), all pods alive through the session.
Per-host [net-rx] census symmetric: 36 NewDynamicEntity makes (mech +
subsystem roster) from every host to every node.

DIAGNOSTIC TRAP that cost a false bug-hunt: EntityID::operator int()
returns localID ONLY, so the [repl] log's (long)GetEntityID() collapsed
different hosts' mechs onto one number and read as 'missing
replicants'.  The log now streams the EntityID object (host:local via
its operator<<).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:42:38 -05:00
arcattackandClaude Fable 5 7e269d6496 gitignore: dist/ (release zips)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:26:52 -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 980c9cd7e5 Input remap: CONTROLS.MAP binding engine + XInput gamepad support
The pod's controls reach the game through the RIO serial board; on a
desktop that hardware is a keyboard shim of hardcoded GetAsyncKeyState
reads.  New binding engine (game/reconstructed/btinput.cpp) maps PC keys
and an XInput pad onto the authentic channels through a user-editable
file (content/CONTROLS.MAP, community-suggested grammar, corrected):

  key/pad -> button <addr>   real buttonGroup emissions, mirroring the
                             RIO convention exactly (press a+1 w/ mode
                             mask saved, release -a-1 w/ saved mask;
                             L4CTRL.cpp:2470-2520) -- aux/preset banks,
                             hotbox, panic, reverse thrust all reachable
  key/pad -> axis            Throttle rate (lever), pedals/JoystickX
                             deflect (turn/twist) into the existing
                             virtual-controls integrators (feel, gait
                             detent, spring-centering all unchanged)
  keypad pilot|external <n>  keyboardGroup key values ('0'-'F')
  pckey <char>               any authentic 1995 typed hotkey
  action <name>              port dev controls (view, all-stop, ...)

Keys claimed by a binding are SUPPRESSED from the legacy WM_CHAR/KEYUP
feed -- ends the historic double-dispatch ('w' drove AND selected pilot
0; F5's key-up value 0x74 aliased to the 't' hotkey; letter key-ups fed
the developer fake-event dispatcher).  Unbound keys keep their authentic
meaning.  The dual-use 'V' is split: V = view toggle, B = look behind.

Default profile = WASD classic (compiled-in twin; delete the file to
restore).  CONTROLS_NUMPAD.MAP ships the corrected community layout
(keypads are NOT buttons 0x50-0x6F -- that space doesn't exist; no
clickable cockpit exists so everything needs a binding; keyboard fire
buttons added; 0x36/0x37 are hotbox not 'config'; missiles moved off
Ctrl).  XInput loads dynamically (1_4 -> 9_1_0), disconnected-pad
probing rate-limited; pad sticks write the axes as absolute positions
(the spring is physical), triggers/buttons per the file.

Verified live: bindings load (43), W and NumPad8 drive (speedDemand 0 ->
61.5), X all-stop (spd -> 0), aux-bank emission (D1 -> [input] 0x2f
PRESS/release under the numpad profile), suppression both directions
(posted 'r' reaches [keych], posted 'w' swallowed), full-keyspace
WM_CHAR+WM_KEYUP fuzz x3 survived with sim advancing, XInput graceful
with no pad, 2-node relay session un-regressed (full ladder + UDP).
Pad-in-hand testing still needs a physical controller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:09:36 -05:00
arcattackandClaude Fable 5 0fcd0c3106 KB sweep: env-gate hub table catches up (BT_RELAY/BT_SELF/BT_KEY_STOP/...); stale MP remaining-work line updated; gauges-hud callsign cross-ref
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:36:53 -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 dbb9af2dfa D1 phase 5+6: UDP unreliable channel (restores the 1995 reliable/unreliable split)
The ~60Hz entity update records now ride UDP in relay mode, so a lost/late
datagram is dropped (dead reckoning absorbs the gap) instead of head-of-line-
blocking the reliable TCP stream -- the fix for internet rubber-banding.
Reliable traffic (make/damage/death/control/egg/launch) stays on TCP.

This RESTORES the authentic 1995 NETNUB split, it doesn't invent one:
Receiver::Message defaults messageFlags=ReliableFlag; the update path clears
it (flags=0, ENTITY.cpp:590); Mode(UnreliableMode) fires at LoadingMission
(APP.cpp:704 -- the 2007 port's Mode() was a no-op, now it STORES the mode).
Routing gate = (mode==UnreliableMode && !(flags & ReliableFlag)) -- flag AND
mode, so the map-stream creation messages (also flags=0 but flowing during the
still-Reliable CreatingMission window) ride TCP for free.

Client (L4NET):
- Mode() stores currentNetworkMode (was ignored).
- ConnectRelayUdp: UDP socket connect()ed to the relay game port; HELLO
  (outbound punch-through, relay learns our endpoint); udpUp on HELLO-ACK.
  BT_RELAY_TCP_ONLY=1 disables the channel (UDP-blocked nets degrade to the
  pure-TCP checkpoint automatically -- no ACK => everything stays on TCP).
- RelayUdpSendFrame: {route, fromHost, seq} envelope + frame, best-effort.
- RelayUdpKeepalive: ~15s NAT-hold + ~1s HELLO retry until acked.
- CheckRelayUdp: HELLO-ACK sets udpUp; per-sender seq gate drops stale/
  out-of-order datagrams; frames handed up envelope-stripped like TCP.
- Send / ExclusiveBroadcast: route unreliable-window traffic to UDP, else TCP.
- CheckBuffers polls the UDP channel first, then TCP; keepalive tick.
(The relay-side UDP forwarder + endpoint learning + --udp-drop hook landed in
phase 1.)

Verified 2-node localhost relay: during a drive the update stream flows on UDP
(relay udp tx 2->190+, TCP frozen at 53) and the peer's replicant tracks the
master to sub-2u; 15% forced drop (--udp-drop 15) stays coherent (tracked to
0.1u); TCP-only fallback confirmed (udp-known drops the TCP-only pod); no
crashes; mesh mode un-regressed.  KB: context/multiplayer.md gains the D1
RELAY MODE section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 10:00:26 -05:00
arcattackandClaude Fable 5 c029df6e6b D1 phase 2+3: client TCP relay mode (L4NET) + 2-node verification
The pod can now run entirely over the relay: one outbound TCP connection
carries both the console protocol (egg/launch) and all game traffic
(envelope-multiplexed), so internet play works behind NAT with no port
forwarding.  All new code is behind 'if (relayMode)' (BT_RELAY unset => mesh
mode byte-for-byte untouched; verified un-regressed).

Env gates (parsed once in the ctor, after WSAStartup so gethostbyname works):
- BT_RELAY=<host>:<consolePort>  (host may be a DNS name; game port = +1)
- BT_SELF=<exact [pilots] entry> (NIC matching can't identify us across NAT)

L4NET changes:
- CreateConsoleHost: relay branch dials OUT to the relay console port
  (bounded retry; console-less continue on a re-dial) instead of listening.
- StartConnecting: relay self-match by BT_SELF string; peers become VIRTUAL
  hosts (INVALID_SOCKET, NoNetworkConnectionStatus) flipped online by the
  relay's PEER_UP; then ConnectRelayGame dials the game port + HELLOs.  HostID
  assignment / remoteHostCount / the connection gate / app ladder unchanged.
- CheckRelay (new): the receive seam -- drains {route,length} envelopes,
  synthesizes HostConnected/Disconnected from PEER_UP/DOWN (same messages the
  mesh accept path routes), returns game frames envelope-stripped (they
  self-identify via NetworkPacketHeader.fromHost; consumers route by payload).
- Send: game-host traffic -> relay unicast envelope (console host excluded --
  its legacy protocol is relay-TERMINATED, not routed).
- ExclusiveBroadcast: build ONCE, send ONCE with the broadcast route -- the
  relay fans out, killing the mesh's (N-1)x upload duplication.
- RelaySendAll (new): partial-send-safe transmit (required on the multiplexed
  socket -- a partial write would desync framing for all peers).
- CheckBuffers: polls CheckRelay first; skips recv on virtual game hosts.
- RelayGameDown: relay-loss synthesizes all-peers-disconnected (match
  continues peer-less; pod never exits mid-match).
- Mode(): now STORES the reliable/unreliable mode (was ignored) for the UDP
  phase's authentic mode+flag routing.

Verified 2-node localhost relay (MP_RELAY.EGG, tagged [pilots]):
- both pods reach 'All connections completed!' via PEER_UP, then
  RunningMission; both mechs' MakeMessages cross the relay (paint x2 each) and
  bidirectional 148-byte update records flow (net-tx/net-rx traces; relay
  stats tcp rx==tx, registered [2,3]); cockpit/HUD render; no crashes.
- driving one node transmits pose/damage/death frames through the relay.
- mesh smoke (no BT_RELAY): 2-node session still forms + simulates unchanged.

Plan: ~/.claude/plans/partitioned-snuggling-piglet.md.  Next: UDP channel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 09:01:54 -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
CydandClaude Fable 5 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>
2026-07-18 00:05:51 -05:00
CydandClaude Fable 5 f703bb1d56 Steam: the lobby + IDENTITY-TOKEN roster -- internet MP code-complete (step 4c)
NEW gated (BT_STEAM) game/glass/btl4lobby: an ISteamMatchmaking room replacing
the arcade Site-Management screen -- host creates (lobby data btl4=1), members
join by filter, pilots publish name/mech/color, the host ENTER mints the
roster and signals GO.

THE FAKEIP LESSON (live-verified, prior-art assumption DISPROVEN): a fresh
process gets a DIFFERENT FakeIP, so menu-time fake addresses go stale by
mission time -- the first cycle timed out connecting to its own stale address.
Redesign: roster addresses are opaque ipv4-shaped TOKENS (169.254.77.N with
ports 1501/1502) minted by the host at GO, mapped to Steam IDENTITIES;
connections ride ConnectP2P(identity, channel) with console=0/game=1 virtual
ports; the map reaches mission processes via env (BT_FE_MYFAKE +
BT_FE_STEAMMAP); the GetMyAddress seam feeds the pod its own token for roster
self-match; CheckSocket reports peers by token.  L4STEAMNET reworked (no
FakeIP allocation at all); the marshal gained the Steam-wire branch.

Verified single-machine (ON/ON, live Steam): menu -> HOST STEAM LOBBY ->
room -> GO -> token map minted -> egg roster carries the token -> mission
process up with 1 roster token incl. self -> pod self-matches -> stock
console ladder -> mission RUNS (46 ticks).  Remaining: the live 2-account
cross-machine session (docs/STEAM_TEST.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 00:03:17 -05:00
CydandClaude Fable 5 48ede2f7d7 Net: the Steam transport -- FakeIP + SDR behind the wire seam (step 4b)
NEW gated (BT_STEAM) TU L4STEAMNET: ISteamNetworkingSockets with FakeIP --
pseudo-SOCKET handles (0x5EA0xxxx) behind the L4NET BTNet* seam; the engine
TCP byte stream rides reliable-NoNagle Steam messages re-assembled into
per-connection rings (nonblocking recv semantics preserved: empty ->
WSAEWOULDBLOCK, peer-closed -> 0); two FakeIP ports mirror the arcade
console/game channel convention; listen sockets + accept queues via the
global status-changed callback; graceful degrade to Winsock at every step.
Seam completions in L4NET.CPP: CheckSocket reports a pseudo-socket peer as
its FAKE ipv4:port (the lobby egg roster matches unchanged); OpenConnection
TCP_OPEN delegates FakeIP targets to BTSteamNet_Connect.  WinMain installs
on BT_STEAM_NET=1 (lobby launch path / by hand).  CMake: gated SDK include/
lib/dll-copy.  Both pumps (SteamAPI_RunCallbacks + sockets RunCallbacks)
drive it -- the FakeIP result rides the GENERAL dispatch (first attempt with
only the sockets pump timed out).

Verified live (ON/ON build, Steam client running): SteamAPI_Init OK,
steam_appid.txt(480) auto-written, FakeIP allocated
(169.254.36.58, console 54464 / game 54465); without BT_STEAM_NET or
without Steam the game stays pure Winsock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 23:47:47 -05:00
CydandClaude Fable 5 e79e8faeed Net: the wire seam -- file-local Winsock primitive wrappers (step 4a)
BTNetSend/Recv/Accept/Close wrap the 9 raw wire sites in L4NET.CPP (send
@x3, recv, accept @x2, closesocket @x2).  OFF builds are PURE CODE MOTION
(bodies are the original calls; the statics inline back); under BT_STEAM
each wrapper first offers the op to the Steam transport (BTSteamNet_*,
lands next) -- handles stay SOCKET-typed so L4Host and every call site
above the seam are identical on both wires.  Connection-ESTABLISHMENT
delegates (OpenConnection/listener arms) land with the transport.  The 3
BT_NET_TRACE blocks stay at call-site level, untouched.

Regression (pod build, 2-node loopback MP + btconsole relay): mesh forms
through BTNetAccept (console + peer accepts logged), both nodes reach the
full 31/31-subsystem running mission, 71/76 ticks -- byte-identical wire
behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 23:41:28 -05:00
CydandClaude Fable 5 6c9edc8de3 KB: step-3 closeout -- miniconsole verification + SDK vendor note
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 23:37:39 -05:00
CydandClaude Fable 5 23c001938b Extern: vendor Steamworks SDK 1.64 (headers + redistributables) for BT_STEAM
The official Valve SDK zip (provided by Cyd), trimmed to what the gated
build consumes: public/ (steam_api.h, ISteamNetworkingSockets,
ISteamMatchmaking, the full public header set) + redistributable_bin/
(steam_api.lib/.dll all platforms) + Readme.  glmgr/steamworksexample/
tools dropped.  Used ONLY under -DBT_STEAM=ON (step 4); the pod and plain
glass builds never reference it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 23:34:28 -05:00
CydandClaude Fable 5 c4ca87b808 Miniconsole: the front end + LocalConsole marshal -- self-launched missions (step 3)
NEW gated dir game/glass/: btl4fe (green-on-black GDI catalog menu -- map/
time/weather/length/mech/color/pilot/mode/port/peers; egg writer emitting the
FULL console egg shape, golden-tested vs MP.EGG: identical section sequence
at identical line offsets, the 4 static ordinal pages verbatim, GDI-rendered
128x32/64x16 pilot-name plasma bitmaps in MP.EGG framing) and btl4console
(the marshal: a worker-thread console CLIENT speaking the btconsole.py wire
protocol verbatim -- 1040-byte chunked egg, 20s settle, RunMission x2,
STAY CONNECTED, own the clock, StopMission at expiry; logs marshal.log).
The engine sees a real connected console and runs the 100% stock ladder --
NO engine hooks (less invasive than planned: the L4APP.H setters proved
unnecessary; launches go through real argv on relaunch).

WinMain (one gated block): zero-mission-arg glass launch -> menu -> relaunch
self with the mission argv (per-mission process relaunch; BT_FE_* env arms
the marshal); post-RunMissions BT_FE_LOOP tail returns to the menu.  Menu
relaunches CLEAR the BT_FE_* handoff (found live: the inherited env re-armed
a marshal instead of showing the menu).

Verified end-to-end (synthetic menu drive): menu -> frontend.egg -> relaunch
-> marshal feeds self over loopback -> stock console ladder -> mission RUNS
(58+ ticks) -> 60s clock -> StopMission -> clean menu return, no env leak.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 23:33:39 -05:00
CydandClaude Fable 5 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>
2026-07-17 22:34:20 -05:00
CydandClaude Fable 5 418acf07e4 Panel: the vRIO layout + coloration, plasma flip fix, button-trap guard
Per Cyd: the on-screen panel now mirrors vRIO (C:\VWE\vrio -- CockpitLayout.cs
+ PanelCanvas.cs, itself the RIOJoy profile-editor layout from the original
Win32 RIO mockup): five 4x2 MFD clusters (addresses descending), four 1x8
board columns (Throttle/Secondary/Screen/Joystick), the two 4x4 hex keypads
(0x50/0x60 -- cells emit real keypad KeyEvents: internal=pilot unit, external=
operator unit), physical names (Panic/Main/Hat*/Pinky...), vRIO colors: red
lamp cells + yellow Secondary/Screen + neutral-blue keypads, flash half-
periods 500/250/125ms, right-click latch (gold outline).  104 controls.

PlasmaWindow: the gauge renderer writes the 128x32 buffer TOP-DOWN -- the
bottom-up flip rendered the marquee upside down (user-reported); copy straight.

Button-trap guard (the e2c21c4 pattern): the authentic base
ConfigureMappableMessageHandler FAIL trap is abort() under DEBUGOFF -- any
aux/zoom button without its L4 override reconstructed KILLED the game on
press.  Default now: loud [FAIL] log + ignore; BT_BUTTON_TRAP=1 restores the
hard trap.  Verified live: synthetic click on a Secondary cell -> 2 [FAIL]
lines (press+release through panel->PadRIO->ProcessRIOEvent->streamed
mapping), game survives and keeps ticking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:30:35 -05:00
CydandClaude Fable 5 db9ac947f7 Displays: on-screen cockpit buttons + desktop plasma + the glass preset (steps 2d/2e)
NEW gated TUs: L4PADPANEL (GDI top-level window, all-W APIs: the pod button
banks -- upper/lower aux, 12 secondary, specials/hat/handle, 63 buttons --
clickable via PadRIO::SetScreenButton with lamp-lit faces from GetLampState,
10 Hz repaint; created by the PadRIO ctor on BT_PAD_PANEL=1) and L4PLASMAWIN
(PlasmaWindow : Video8BitBuffered -- the gauge renderer draws the 128x32
plasma into its pixelBuffer through the SAME code path as the serial device;
Update() blits orange-on-black at L4PLASMASCALE, default x4).  Gated seams:
L4GREND.cpp L4PLASMA=SCREEN branch; btl4main.cpp -platform glass preset
(PAD,KEYBOARD + BT_DEV_GAUGES + SCREEN plasma + panel; env always overrides;
non-glass builds log+fall back to DEV); run.cmd glass token.  MFD surfaces
need NO new code: the existing dock-bottom / BT_DEV_GAUGES_WINDOW=1 /
BT_DEV_GAUGES_DOCK=1 modes are the display story.

Verified live: -platform glass boots GLASS profile, panel + plasma windows
up, pad detected, dev gauges awake, mission loop clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:21:22 -05:00
CydandClaude Fable 5 9f35a8034a Controls: the keyboard bridges STAND DOWN for a live device (step 2c)
New engine query BTRIODevicePresent() (L4CTRL.cpp, ungated -- pod-correct for
the serial RIO too): device exists AND IsOperational().  Both dev input
bridges -- the mech4.cpp mapper-attr writes and the mechmppr.cpp BT_KEY_BRIDGE
block -- now gate their WRITES on it: BT_KEY_BRIDGE unset = auto (bridge only
when no device), 0 = force off (the documented pod setting, honored), else
force on.  The BT_FORCE_THROTTLE headless harness always rides the bridge.
The mapper demand READ (turn = turnDemand) always runs.

Verified live (glass build, PAD): OS-injected LSHIFT hold slews the PadRIO
throttle 0 -> 0.33 -> 1.0 with pre==thr every frame -- the value arrives via
the ENGINE push through the streamed .CTL binding, not the bridge -- and
speedDemand=61.501 comes out of the authentic InterpretControls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:14:37 -05:00
CydandClaude Fable 5 ac57a474ef Input: the RIOBase seam + PadRIO -- the glass cockpit gets hands (step 2b)
L4RIO.h splits the abstract cockpit-control surface (RIOBase: enums, the five
analog Scalars, GetNextEvent/SetLamp, no-op serial ops, NEW IsOperational) out
of the serial RIO (RIO : PCSerialPacket, RIOBase -- byte-for-byte behavior kept,
ctor assigns as before); LBE4ControlsManager holds a RIOBase* and gains the
gated L4CONTROLS=PAD factory arm (BT_GLASS; OFF build logs+ignores the token).
NEW gated TUs: L4PADRIO (XInput+keyboard synthesize the surface; 3s hot-plug
re-probe; focus-guarded keys; per-poll AnalogEvent heartbeat; lampState[] +
static SetScreenButton/GetLampState for the panel) and L4PADBINDINGS
(content\bindings.txt profile, self-documenting default written on first run;
deflect/slew/set axis model; addresses validated against ButtonCount).
Verified live (glass build, L4CONTROLS=PAD): bindings written+parsed 44/10/5,
XInput pad detected, 121 streamed mappings install via stock PrimaryRIO path,
2157 frames clean. Pod build (gates OFF) compiles the split with zero
behavioral delta.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:11:16 -05:00
CydandClaude Fable 5 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>
2026-07-17 21:59:51 -05:00