Commit Graph
340 Commits
Author SHA1 Message Date
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 d26d0375ae Docs sweep: README + KB catch up with the full session's work
Coverage audit of this session's ~30 commits against the docs found
gaps; fixed:
- README §Multiplayer was stale (only the old mesh flow, 'combat in
  progress'): now leads with the relay + operator-console path
  (internet play, join.bat/play_solo.bat, patient wait) and lists what
  the mesh path has verified (4-pod, camera seat, callsigns, clock);
  README controls line updated (turn/twist/pitch, pad, CONTROLS.MAP)
- decomp-reference env table: added BT_CAM_LOG, BT_SPEC_LOG (were in
  code, not the hub); BT_INPUT_LOG note gains the twist/elev axis
- pod-hardware §input remap: the AXIS SIGN CONVENTION finding
  (positive sim = CCW/left; bridge negates once per channel;
  screenshot-forensics lesson) was only in commit cb82d8c -- now durable
- multiplayer.md: PATIENT WALK-UP + MISSION CLOCK sections (features
  shipped in f9f230c/f57d25f, only incidentally referenced before)

checkctx CLEAN (20 topics, 68 glossary keys).  All session features
now have topic-file coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:51:01 -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
arcattackandClaude Opus 4.8 e2c21c4db2 Fix the tester 'buttons crash the game' report: two keyboard killers
Reproduced by full-keyboard fuzz (every WM_CHAR + WM_KEYUP posted to the
game window, per-key liveness; delivery proven by the new BT_KEY_LOG
[keych] trace).  Two distinct issues:

1) '&' == Shift+7 is the engine's dev-console STOP-MISSION keystroke --
   one shifted-key slip while hunting unmapped panel keys (MAP zoom '+'
   is Shift+'=') instantly ended the session, indistinguishable from a
   crash.  Now env-gated (APP.cpp): default ignored with a log line;
   BT_KEY_STOP=1 restores the authentic stop (verified both ways live).
   Same hazard class as the arrow-release '&' alias already swallowed in
   L4CTRL.cpp:1516.

2) '\' (the developer fake-event key) was a REAL wild-jump crash:
   Entity::Dispatch STAMPS entityID/interestZoneID into the message at
   Entity::Message offsets (ENTITY.cpp:236), and the '\' case dispatched
   a bare Receiver-sized ReceiverDataMessageOf<ControlsButton> at the
   Mech -- the stamp wrote past the stack object and corrupted the frame
   (cdb: call to eip=1 out of Receiver::Receive).  The 1995 binary does
   the identical overwrite and survived on stack-layout luck.  Fixed with
   Entity::Message-sized placement-new backing (btl4mppr.cpp); the only
   such call site (grep-verified).

Verified: full fuzz (95 chars + F-keys + letter/digit keyups, 118 keys
delivered) survives end-to-end; '&' stops cleanly under BT_KEY_STOP=1.
KB: reconstruction-gotchas.md gains gotcha 19 (Entity::Dispatch message
stamping) + the '&' note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:06:46 -05:00
arcattackandClaude Opus 4.8 1ed8b05160 Radar view-wedge tracks the torso twist (Gitea issue #1)
The SECTOR radar's view cone read viewHorizontalRotation, wired by the
MapDisplay ctor from the mech's Torso -- but FindSubObject and
GetHorizontalRotation were NULL stubs, so the connection was never created
and the wedge sat at heading 0 regardless of twist.

Reconstructed from the binary:
- FindSubObject (FUN_0041f98c): a subsystem-ROSTER walk (count @+0x124,
  array @+0x128) matching the streamed subsystem name (sub+0xd4) with a
  tolower-strcmp (FUN_004d4b58) -- the 'Torso' sub-object IS the roster
  Torso subsystem.
- GetHorizontalRotation: torso+0x1D8 == Torso::currentTwist (layout-locked),
  via the existing task-#56 bridge BTGetTorsoTwistAddr (Radian is layout-
  identical to Scalar).

Verified live (MadCat, Standard mode Q/E): the wedge tracks the twist in
lockstep (rot 0 -> -2.21 rad), and the semantic test passes -- body turned
away, torso twisted back onto the enemy: reticle green + wedge pointing at
the enemy's blip on the body-fixed scope.  NB the Blackhawk's torso is FIXED
(+/-0.01 deg limits) -- its wedge authentically never moves; test with a
twisting mech.  Diag env: BT_RADAR_LOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:24:52 -05:00
arcattackandClaude Opus 4.8 8616b62405 LAST.EGG: pilot-2 -> crimson MadCat (Red->Crimson: vehicletable has no Red)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:25:17 -05:00
arcattackandClaude Opus 4.8 63250aee41 Fix two Jul-16 regressions: BT_DEV_GAUGES crash + -net dead controls mapper
Both surfaced today (dormant ~1 day) the moment the pod launch flags
(BT_DEV_GAUGES=1 BT_START_INSIDE=1, tools/mp_launch.sh) were used to drive in
multiplayer -- which made the same-day paint change look guilty.  Confirmed on
the pre-paint build too; the trigger was the Jul-16 audio attribute work.

1) BT_DEV_GAUGES crash (cdb: CoolingLoopConnection::Update, ~15s in as the gauge
   builds lazily).  The dev-gauge cooling-loop lamp reached the cooling master by
   RAW attribute index -- GetAttributePointer(3) + *(master+0x1d4).  The audio
   commits (cc2b109 ReportLeak etc.) inserted rows into the chained attribute
   tables, so index 3 shifted onto a scalar, the resolve walked garbage, and the
   background pass AV'd.  Fix: route through a complete-type bridge reading NAMED
   members (heat.cpp BTCoolingLoopFrame: linkedSinks.Resolve() +
   Condenser::condenserNumber) -- the databinding rule; never a raw numeric
   attribute index.  Removed the now-orphaned GetAttributePointer-by-index
   helper.  Name-keyed samplers (InputVoltage) were unaffected.

2) -net dead controls mapper (mech wouldn't walk; turning/weapons still worked).
   The RIO mapper is built from a stack SubsystemResource in btl4app.cpp that set
   only name/classID/modelSize -- leaving subsystemFlags as stack GARBAGE, which
   Subsystem::Subsystem copies into simulationFlags.  A stray DontExecuteFlag
   (0x2) froze the mapper (speedDemand stuck at 0).  Config-dependent: clean in
   SP, dirty in -net; the audio commits shifted the stack and flipped the bit.
   Fix: memset each hand-built control-mapper resource to 0 (flags 0 =
   AlwaysExecute, the faithful value -- a mapper must tick every frame).

Verified with the pod launch flags: SP alive 32s no crash; MP mech walks
(speedDemand 61.5 at full throttle, ~430u traveled), both nodes alive, no crash.

KB: reconstruction-gotchas.md gains gotcha 18 (uninitialized stack-resource
flags + the raw-attribute-index-into-a-growing-table variant) and a
verify-under-the-user's-launch-flags note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:58:32 -05:00
arcattackandClaude Fable 5 e0474ff92a Per-pilot mech paint: wire the color/badge/patch substitution end-to-end
- Mech::resourceNameA/B/C -> real CString members (the binary's 16-byte
  CStringRepresentation; deep-copy bind from the MakeMessage = FUN_00402a98,
  implicit member dtors) + paint-name accessors
- SetupMaterialSubstitutionList reads the real egg names ([paint] log);
  TearDown clears the callback first (FUN_004d11e8)
- dpl_SetMaterialNameCallback is real now (L4VIDEO registry); bgfload
  MaterialResolver::resolve() applies it to every material name -- the
  port analogue of the dpl board rewriting names at load
- MP_BHMC.EGG: color=Red -> Crimson (vehicletable has no Red; binary Fail()ed)

Verified live 2-node MP: crimson MadCat with hip hazard stripes + yellow VGL
leg emblems; white Blackhawk + emblems; replicants painted on both nodes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:50:23 -05:00
arcattackandClaude Fable 5 dda517b65c KB: mech paint audit -- per-pilot color/badge/patch substitution authored but unwired (the 'missing stripes/coloration' report)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:32:53 -05:00
arcattackandClaude Opus 4.8 46446f3870 KB: pod-hardware readiness gaps -> open-questions (Phase 8 assessment)
What's carried + plausibly functional vs what will need real work on modern
hardware: RIO serial chain / RGB-splitter channel packing / multi-output
selection / plasma driver all EXIST but are pod-untested; expected work =
multi-head D3D9 fullscreen modernization, the folded-away rear sound card
(4-speaker split), RIO protocol timing, and the undocumented pod bring-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 07:32:20 -05:00
arcattackandClaude Opus 4.8 abed41e711 gitignore: __pycache__
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 18:07:37 -05:00
arcattackandClaude Opus 4.8 224092aaff KB: reconcile the context system with the day's work
- decomp-reference.md: the binary ATTRIBUTE-TABLE section -- 16-byte
  {id,name,off+1,0} row format (alignment warning), the walker technique,
  and the three recovered tables (Mech ids 21-56 incl the corrected
  EyepointRotation@0x360 / RearFiring@0x410 / DistanceToMissile@0x400
  labels; HeatSink 3-12; Torso 3-15; the weapon RearFiring 'b' marker).
- combat-damage.md: the REAR-FIRE + look-view system (roster survey: the
  Blackhawk's ERMLaser_2/3 are the game's only rear weapons; every missile
  rack forward), the missile mount-frame launch truth, and the BANKED
  full Missile flight-model decode (three performances, proximity fuse).
- wintesla-port.md: post-Phase-4 closures (instability model live, F14
  static filter baked, the footstep warm-up bug + its trace-cap lesson);
  deferred list trimmed to F21/HRTF.
- AUDIO_FIDELITY.md: status block -- F14 FIXED, warm-up bug noted.
- open-questions.md: "which mechs used rear fire" answered from data;
  remaining look-view key bindings noted.
checkctx: CLEAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 18:07:15 -05:00
arcattackandClaude Opus 4.8 ebdfa40d95 HUD (task #68 follow-up): authentic reticle pip GROUPS -- front vs rear
The binary reticle registration resolves each weapon's RearFiring attribute
and registers its pip in group 1 (front) or 2 (rear) (part_014.c:5429-5434)
[T1]; the reticle draws the group selected by the mech's reticleElementMask
low bits (= binary mech+0x390, driven by the weapon-update view switch
part_013.c:5588-5595: forward |1&~2, look-back |2&~1).

Port: BTBuildReticle passes the real group (was hardcoded 1 on the disproven
"no BLH weapon is rear" belief); BTCommitLookState drives the mech's
reticleElementMask low bits per view (mech4's HUD tick already publishes
them as gBTHudGroupMask, and the reticle Draw already filters on it).

Resolves the user-reported "only 2 ERM dots but the mech has 3": ERMLaser_1
(front) and ERMLaser_2 (rear) both author pip position 1 -- with every pip
drawn in one group the two red dots sat exactly on top of each other.  Now:
forward view = 5 pips (2 PPC + ERM_1 + 2 SRM), look-back = the 2 rear
lasers' own group layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:45:24 -05:00