The egg's per-pilot 'experience' (novice/standard/veteran/expert) is the
pod's simulation-fidelity tier -- stored at BTMission+0xe4 and seeded into
the BTPlayer ctor's flag block @4c0bc8 [T1]. The old 'per-role display
toggles / returnFromDeath' reading was WRONG: the source is
mission->experienceLevel, and the neighboring pair copies
mission->advancedDamageOn (the egg's technician splash/collision switch),
not role floats. Known consumers: 0x260 = the HEAT-MODEL master switch
(FUN_004ad7d4), 0x25c = the novice sim-lockout (jams/searchlight/powersub),
0x274 = the raw level (FUN_004ac9c8 ==0 novice predicate).
NEW established topic context/experience-levels.md (data path, binary flag
rows per level, manual cross-refs, corrections log) + router row; sweeps in
decomp-reference/gauges-hud/open-questions/pod-hardware/subsystems.
btplayer.cpp/.hpp re-annotated with the corrected semantics; the guarded
role-based seeding stays as a marked [T3] stand-in (slot drift vs the
binary switch documented inline) until the wiring task points it at
BTMission::ExperienceLevel().
(Research by the parallel context session; committed from the glass line.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reported: the reticle's missile pips no longer momentarily disappear when
firing. Traced it: the pip's 'loaded' flag has read MechWeapon::rechargeLevel
(>= 0.999) since the reticle Execute was recovered (task #37). That's a port
APPROXIMATION of the binary's authentic attr 0x1C (WeaponState @0x350). It
works for emitters -- rechargeLevel is charge-driven and tops off at 1.0 when
Loaded -- but projectile weapons (MissileLauncher/autocannon) never write
rechargeLevel (it's authentically static at 1.0; the recharge DIAL draws full
and never moves). So a missile pip's 'loaded' was permanently true and the pip
never blinked. NOT an audio regression -- it never worked in the port; the
audio wave (ea85554) is actually what made the projectile fire-cycle state
(weaponAlarm Loaded/Loading/Firing) correct, i.e. the very signal the pip
should have been reading.
Fix (faithful to the binary): the pip now reads attr 0x1C = the weaponAlarm
StateIndicator level (new MechWeapon::WeaponStatePtr -> GaugeAlarm54::LevelPtr)
and compares it '== stateConst2' -- the loaded constant (2) the binary itself
already stores in AddWeapon (param_10). The state cycles
Loaded(2)<->Firing(0)/Loading(3)/Jammed(5) for BOTH weapon families, so every
pip momentarily drops on fire. The authentically-static rechargeLevel dial is
untouched.
Verified live (Blackhawk solo, BT_AUTOFIRE=1 BT_AF_MISSILE=1): both SRM6
missile pips toggle loaded 2<->0/5 on each salvo (emitter pips unchanged);
game boots + runs clean. KB: context/gauges-hud.md pip entry corrected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Solo practice already started in-cockpit (BT_START_INSIDE) but didn't
open the MFD gauges window -- inconsistent with the join bats. Add
BT_DEV_GAUGES=1 so single-player practice mirrors the pod experience.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The crash (any typed key in a glass session; found via the backtick report):
the merge-reconciliation stand-down declared extern BTRIODevicePresent at
BLOCK scope inside the extern-C BTInputSuppressKey -- the declaration
inherited C linkage, _BTRIODevicePresent went UNRESOLVED, and /FORCE bound
the call to garbage (cdb: wild call into Sensor::DefaultData from
LBE4ControlsManager::Execute's suppression check). The tolerated-LNK2019
batch hid the new unresolved external -- the exact CLAUDE.md /FORCE trap.
Fix: the extern moved to file scope (C++ linkage); link verified clean of
_BTRIODevicePresent.
Backtick feature (per Cyd: backtick = 1st/3rd person): PadRIO edge-detects
VK_OEM_3 in its poll (focus-guarded, message-path-free) and the mech4 view
block consumes it beside the V action (which stands down with the binding
engine in glass mode). Verified live: two real presses -> [view] COCKPIT
eyepoint -> [view] external chase, game alive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
User-reported: glass left/right inverted. Closed with live sign algebra:
the joystick-group wire push was traced end to end (new env-gated
BT_CTRLMAP_LOG push trace + record mode-mask in the install log) --
measured wire stickX=-1 -> turnDemand=-1, the SAME demand the
user-verified dev D-key-RIGHT produces (stickX=+1 -> turnDemand=-1 via the
bridge's negate-once, cb82d8c). The mapper interprets the authentic RIO
WIRE convention: stick right = NEGATIVE JoystickX (the vRIO/RIOJoy
calibration convention). PadRIO now publishes X negated (all sources --
keyboard deflect, pad LX, panel -- uniformly); Y stays screen-sign;
L4PADFLIP still flips both. Also: PadRIO honors BT_KEY_NOFOCUS=1 (the
btinput harness override) for automation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Steam-host report ('no glass panel, plasma yes'): a direct btl4.exe start ran
the MENU under the DEV profile, whose putenv(L4CONTROLS=KEYBOARD) leaked into
the relaunched mission child -- the child's glass preset (sets-when-unset)
could then never select PAD: no PadRIO, no panel; the plasma appeared only
because DEV never touches L4PLASMA. Menu-mode is now detected FIRST and the
front-end process skips the profile putenvs entirely (profiles belong to game
processes); boot line reads 'MENU (front end -- no profile applied)'.
Verified: zero-arg start -> menu -> launch -> child boots GLASS, PadRIO up,
72-control panel present, plasma present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First dist feedback ('there is no glass panel'): both windows opened at the
top-left with no-activate and the game window then covered them. The pod's
physical panel is always visible -- so is the glass one now: the button
panel parks at the screen's right edge, the plasma bottom-right, both
WS_EX_TOPMOST. Verified live: panel left=2616 top=12 topmost, plasma
bottom-right topmost, game window below.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
'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>
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>
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>
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>
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>
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>
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>
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>