The 1995 engine tolerates late HOST attach but never finished late ENTITY
sync (NotifyOfReplacementEntityCreation = Fail stub, makes broadcast-once).
Routes to docs/REVOLVING_DOOR_PLAN.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
Phase 1 = console-only round carousel (relay door mode, no new zip);
Phase 2 = true drop-in (engine make re-send on late PEER_UP), gated on a
2-node spike. No code modified -- plan for the next work session.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
The same careful tester hit the BASIC-mode trap twice because 'expert/
veteran modes' (menu experience) sound like control modes (M key).
Spelled out the distinction + the numpad NumLock dependency.
REMOTE OPERATORS (dev-channel ask): the relay grows a control TCP
listener (console port + 7) speaking a line protocol -- AUTH <secret>
(auto-generated content\operator_secret.txt), then launch / stop /
ping / get mission / set key=value;... The relay's timestamped log
stream tees to the authenticated operator (bounded per-conn buffers --
a stalled operator can NEVER stall the relay; overflow = drop), with a
400-line history replay on connect so mid-session operators see
current state. One operator at a time; a new AUTH displaces the old.
stdin commands unchanged (rigs/GUI local mode untouched).
The operator GUI gains a 'Relay host' field: blank = today's local
flow byte-for-byte (child relay dies with the window); a hostname =
drive that machine's relay remotely (launch/stop/apply-settings over
the wire, roster rebuilt from the teed log, local-instance joins point
at the remote relay). Only the relay host needs port forwarding.
Scripted protocol test: auth reject/accept, takeover, get/set, ping,
history replay, and a FULL 2-node mission launched and stopped
entirely over the socket.
CRASH FIX (the layout-shifting heisenbug -- and almost certainly issue
#35): the new crash self-report finally captured its 24-frame stack:
InterestManager::OrphanInterestOrigin -> RemoveUninterestingEntity ->
DestroyEntityAudioObjects -> {Static,Dynamic}3DPatchSource::
IsAudioSourceClipped, AV reading NULL+0x38 -- the unguarded chain
GetMissionPlayer()->GetPlayerVehicle()->GetSimulationState() (the
authentic burning-mech death-silence check) hit a NULL vehicle during
MP teardown windows. Both sites now null-guard; burning semantics
preserved whenever the vehicle exists. Also live-confirms that
interest-based entity teardown RUNS in MP (the #38 paint mechanism).
Regression: full combat round + control-channel drive, zero crashes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The advertised upgrade flow was delete-and-unzip, which wipes the two
per-player files (key/joystick bindings incl. joyconfig output, saved
volume) every version. The zip carries neither file, so extract-over-
top preserves them by construction -- now the documented flow, with the
copy-out fallback for clean-delete preferrers.
Field report (new tester, 510): keyboard A/D + arrows 'not working' --
can twist and drive but not turn. Root-caused live via key injection:
fresh spawns start in BASIC control mode, where steering rides the
STICK (authentic rookie mode) and the pedal-bound keys are inert by
design; one M press (MID) makes A/D steer. (A false regression-bisect
chase established the metric noise came from an attached pad's stick
drift nudging BASIC steering.) README now warns about the default
mode; kept the env-gated [padwrite] diagnostic from the hunt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
David's failures leave a 0-byte (or absent) join.log -- the exe can't
testify about a death before WinMain. The bats now bracket it from
OUTSIDE: launch_report.txt records the launch timestamp, the exe's
EXIT CODE (fingerprints the failure class: 0xC0000135 DLL-not-found,
0xC0000005 AV, AV-kill signatures, our clean exits), whether join.log
was ever created (never-created = blocked before our first
instruction), and its size. Verified with a synthetic -1073741819
exit. The 'exited' message now asks players to send BOTH files.
With this + the 507 first-breath line + the crash self-report, every
failure mode now leaves evidence somewhere: bat report (pre-WinMain),
first-breath (WinMain reached), [crash] stack (anything after).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#39 (field: 'I was able to input before the game screen actually came
up -- fired weapons, moving, could hear the audio'): the simulation
runs during load/wait and PadRIO fed it live input. Poll now emits
only the analog heartbeat until gBTSceneHasPresented -- keyboard, pad
and joystick all stand down; edge state stays parked so keys held
across the boundary press cleanly at first frame. BT_BTNTEST and the
panel-click seam stay live (event injection, same as before).
#40 (field: 'GPU usage during waiting screen is sometimes consuming up
to 45-50%'): ExecuteIdle Cleared+Presented uncapped for the whole join
wait. Now capped at ~40 Hz with a 5 ms sleep on skipped passes (the
overlay's change gate is 12.5 Hz -- nothing visible changes faster).
Regression: 2-node relay round through the wait screen -> load ->
mission (135 in-mission ticks, autofire) -> clean. Awaiting the
reporter's live confirm on both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The binary's ConfigMapGauge dirty-redraw (@004c6f1c) is SetColor(0xFF)
-> MoveToAbsolute(0,0) -> DrawBitMapOpaque; the transcription dropped
the reposition. The FIRST draw worked by accident (fresh view cursor
at the origin); every forced redraw -- the weapon panel's DISPLAY
round-trip (main -> ENG DATA -> main, BecameActive sets dirty) --
blitted btjoy.pcc at the state-lamp loop's leftover cursor (0xc, 0x55),
painting an offset ghost joystick over the original on every MFD.
Repro pinned live by the operator (initial draw correct; doubles on
the DISPLAY round-trip, every MFD). LIVE-VERIFIED fixed by the
operator: round-trips redraw cleanly. NOT a regression -- present
since the joystick went live (89b4f53; bisect-confirmed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CRASH SELF-REPORT (the #35/#41 heisenbug hunt): every unhandled
exception now logs its code, address, and an EBP-walked stack as
module-relative offsets (btl4+0xNNNN) into BT_LOG before dying --
any field crash on any machine hands us a resolvable stack. The
walker is per-dereference guarded (a corrupt chain truncates).
BT_CRASHTEST=1 exercises the path end-to-end (verified: forced AV
logs code/addr/5-frame stack). mkdist archives each build's PDB
next to its zip so offsets resolve per distributed version.
Context: a flaky release-only crash in the mech build path surfaced
during #38 rigs -- it moves with build layout, spares neither peer,
and masks under a debugger heap (13 clean cdb runs vs 2/3 crashes
without). A real latent memory bug; the self-report will catch it
wherever it strikes next.
ROUND-COMPLETED LATCH (issue #38, awaiting verification): relay-mode
clients refuse RunMission after a round has ended -- the lobby-loop
relaunch owns the next round (in-process round 2 rebuilt every
player/mech entity on stale globals; matchlog caught player=3:1->3:36
in one generation). Latch sets ONLY when the stop ends a RUNNING
mission (a stray pre-mission StopMission must not poison round 1 --
the first cut ate a legit launch, caught + fixed in rig). Regression
verification pending (contaminated by the heisenbug above).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Respawn-paint investigation: 10 forced respawns on the i22 rig showed
paint STABLE across in-place respawns on both master and replicant --
the in-place reset does NOT repaint. Two real findings landed:
1. 'Red' is NOT a camo color (the vehicle table authors Crimson=red for
camo; Red exists only as a PATCH color) -- an egg with color=Red
paints the mech silent gray from first spawn (the engine tolerates
the table miss by design). The relay now WARNS at egg load on any
color= outside the camo set; our MPSHORT test egg had exactly this
bug (fixed).
2. [paint] log now carries the entity id, so the one-time-observed
nondeterministic video-model rebuilds (sernos 2-5 with swapped
colors, run 1 only) will be attributable when they recur.
The reported color/style change on respawn remains unreproduced in the
deterministic rig -- needs the observer detail (whose mech, which view)
from the next field sighting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Found during the #35 (Owens laser crash) hunt: FindResourceDescription
returns NULL for an unknown vehicle name and release builds (Check
compiled out) segfaulted on Lock() -- a typo'd or corrupted vehicle
tag in a served egg would crash EVERY pod at drop. Now logs
'[spawn] FATAL: unknown vehicle' + Fail() with a clean exit.
Verified: vehicle=nosuch -> message + exit 127 (was a segfault).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Night-2 (RajelAran): 'the game is not seeing axis release for turn --
the mech may keep turning when control is released.' Root cause: the
direct-mode axis write was gated on raw != 0, so an axis returning
INSIDE the deadzone just stopped writing and the channel LATCHED at
the last deflection -- fatal on channels with no keyboard spring (the
Turn composite in the twin-stick profile).
Fix: per-binding previous-value tracking (pad + joystick paths); the
outside->inside transition writes 0 exactly once, then the centered
axis leaves the channel to other inputs (composition preserved; slew
bindings excluded by design -- a lever stays where slewed). Plus the
#24 rule, axis edition: a pad/stick that DIES while deflected releases
its direct channels (throttle keeps the last lever position -- safer
than an all-stop mid-fight).
Verified: build + 45s glass mission with an idle XInput pad (no
spurious writes, no regression). The deflect-release cycle itself
needs a human hand -- awaiting RajelAran's next session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Night-2 capture: one flapping pod cascaded 3 ROUND ABORTs in 8s; every
client relaunch-churned (~1 gen/s) until the D3D device-creation 20s
deadline fired on every machine ('could not connect direct 3d'), and
claim-less restarts minted duplicate roster seats.
Console (relay):
- Round abort clears stale console-conn ACK flags and HOLDS the next
egg release for an 8s settle window (_tick_settle re-releases when
quiet) -- the abort->instant-re-release->drop->abort cycle is broken.
- HELLO from a stale identity gets a REJOIN reply (identity resync via
the seat-request path, 5s/IP rate limit) instead of a bare drop.
- STATIC SEATS: seats remember (IP + callsign); a claim-less restart
from the same machine+name RECLAIMS its own seat (displacing its
zombie beacon) instead of minting a duplicate -- the pod contract:
same box, same seat, always.
- Operator local instances log with BT_LOG_APPEND (the night-2 crash
loop left a 2-line file; generations now leave a full trail).
Client:
- Relaunch storm damper: a generation that lived <15s sleeps 5s before
respawning (storm decays to a gentle cadence; normal multi-minute
round relaunches untouched; menu clicks explicitly never delayed).
Rig-verified: normal round + mid-MISSION drop = no abort (correct);
mid-LOAD kill = exactly ONE abort + 8s hold + seat reclaimed + no
cascade. The night-2 'host 7' identity-derivation subtlety remains
under forensics (append logs + first-breath line will capture it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field 2026-07-23 (second night): a player's join.bat failed 3x in a row
with a 0-byte join.log -- indistinguishable from 'never ran'. Two
causes of evidence loss fixed:
1. The first flushed log line now happens the INSTANT the log opens
(version, pid, args, first-process vs relaunched-generation). An
empty log after this build means the exe was killed before WinMain
-- an external cause (antivirus/loader), not ours.
2. The menu->game relaunch chain truncated join.log each generation
(BT_LOG_APPEND was unset in the player bats): the crashed
generation's evidence was erased by the next attempt. The bats now
delete the log once per bat run and append across generations.
Also explains the confusing 'The game has exited' bat message on
SUCCESSFUL launches (the menu process exits after relaunching the game
detached) -- the log now records the whole chain.
The 1-5-50-0 cycle's closed detent one press past max is what cooked
both SRM6s in the 2026-07-23 field session -- players need the warning
and the shared-supply note.
Tester ask: configure non-Xbox controllers. New L4JOY layer (DI8):
enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling
with range normalization, reacquire-on-loss, 3s hot-plug re-enum.
XInput devices are excluded via the documented RawInput IG_ VID/PID
check (live-verified skipping a real XBOX360 pad -- no double-feed).
bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots
by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/
deadzone; hats as 4-direction buttons with diagonal chords). PadRIO
runs them through the existing channel/event machinery -- per-binding
edge arrays release automatically on detach (the #24 rule), and a
direct throttle axis maps full lever travel onto the 0..1 channel
while the gait detent still snaps a lever parked in the walk/run dead
band.
BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move
each control when prompted (stick/twist/throttle/fire), invert derived
from the move direction, hat look cluster auto-added, output written
between markers in bindings.txt with the rest of the file preserved.
Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched.
Verified: grammar accept/reject per line, XInput-exclusion live,
30s in-mission no-device polling clean. Awaiting a tester with real
stick hardware for axis verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Measured A/B/C (Blackhawk, sustained autofire, 150s soaks): SRM6 on
Condenser2 plateaus ~531 and COOLS at share 0.91 (boost 2), climbs to
the jam band at balanced (0.167), and runs at the 2000 failure line
starved (0.018). MoveValve reruns the redistribute every press;
veteran passes the novice guard. The field 'boosted loop 2 still
cooked' therefore means the valve was NOT at the 50 detent during the
fight -- the 1->5->50->0 cycle turns the loop OFF one press past max.
MoveValve presses now log always-on ('[valve] <name> -> detent N' +
a CLOSED warning) so the next field session records the detents.
Also: solo mission clock verified live (60s egg self-ends,
'[mission] solo game clock expired' -> RunMissions returns).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field 2026-07-23: a 20-minute solo mission was still running at 53
minutes. In the arcade the OPERATOR CONSOLE owned the game clock and
sent StopMission at zero; the engine only computes the HUD countdown
(secondsRemainingInGame) -- nothing consumes it. The desktop FE-solo
mode has no console, so timed solo missions ran forever.
Shim at the countdown site (Application::Execute): when no
console/relay owns the session (BT_RELAY unset), post the console's
own StopMissionMessage when the clock runs out -- the normal end flow
(EndingMission -> review -> FE relaunch) takes over. length=0 (raw
solo) stays endless; console-driven sessions untouched. One-shot
latch resets outside RunningMission.
Awaiting live verification (a 60s-length solo egg must end itself).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field capture 2026-07-23: right SRM6 tripped FailureHeat and went dark
with no indication -- reported as a suspected bug. Decomp @004c9a38:
the gauge cluster's only weapon-state read is ==5 (jam lamp); state 7
has no presentation anywhere. EJECT tap revives it while rounds
remain. (Same session: first live verification of the #31 eject
wiring -- jammed left SRM6 cleared by a tap.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Disasm-faithful transcription of the export-gap handler onto
ProjectileWeapon (MESSAGE_ENTRY id 0xb, the #19 pattern). PRESS arms
the ~3s UpdateEject countdown (bin alarm -> Ejecting(3); gate 2 parks
the weapon offline mid-eject -- authentic). HOLD to completion =
DumpAmmo jettisons the bay -> NoAmmo. TAP (release early) cycles ONE
round out via FeedAmmo and returns the weapon to Loading -- clears a
jam at the cost of a round (and recovers a FailureHeat 7 while rounds
remain). Binary structure kept exactly incl. the press-no-bin
fall-through into the release block.
AmmoBin: Ejecting(3)/Ejected(4) alarm levels decoded + named
SetAmmoState accessor (databinding rule). Always-on forensics:
'[weap] EJECT tap' + '[ammo] bay DUMPED overboard (N rounds)'.
Rig-verified live (BT_EJECTTEST=1 / =hold on LRM15): taps eject one
round each and return the weapon to service (15->8 across cycles);
hold dumps all 16 -> NoAmmo. Awaiting a human press of the actual
ENG-page button on a jammed weapon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Old-timer testimony (tap = eject one round, hold = eject the bay, eject
clears a jam) checked against the binary: raw disasm of the export-gap
handler @004bb9b8 confirms it byte-for-byte. PRESS arms the ~3s
UpdateEject countdown (hold-to-completion = DumpAmmo whole bay ->
NoAmmo); RELEASE before completion (TAP) ejects ONE round (@004bd4f4)
and, with ammo remaining, sets weaponAlarm 3 + restarts the reload --
the weapon returns to service. A tap even recovers a FailureHeat
level-7 launcher while rounds remain (empty-bin gate 2 re-pins 7 only
when the bay is dry).
The earlier 'jams are mission-permanent / no unjam exists' claim read
only the HOLD path -- corrected + swept: projweap.cpp case-5/jam-log/
CheckForJam comments, players/README.txt, decomp-reference.md
(message-tables entry), open-questions.md (state-7 recovery note).
Gitea #31 updated; wiring the handler now restores the pod's actual
jam-recovery mechanic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field report: audio cutting in and out toward the end of the match.
Three structural defects in the port's source-pool lifecycle:
1. SourceSet.sources[] was uninitialized heap garbage until first
acquisition -- AL names are small ints, so a recycled-heap slot could
alias a LIVE source owned by another sound; alIsSource() skipped
generation, two sounds shared one source, and whichever released
first deleted the other's mid-play. SourceSet ctor now zero-inits.
2. ReleaseSourceSet's bulk alDeleteSources(count, sources) is ATOMIC on
invalid names (AL spec) -- one empty/-1 slot and NOTHING deleted;
after the first pool exhaustion every partial release leaked and the
pool never recovered. Now per-slot guarded delete, slots parked at 0.
3. Failed acquisitions stranded partial sets forever (dropped transients
are never released by anything). ReleaseChannels() handback at both
failure sites (StartRequest + dormant-resume) + a dtor backstop.
Verified: 2-node MP smoke (mission + lobby-loop relaunch clean, census
live/acquireFails 0/0) + 100s solo combat smoke (52 audio triggers,
zero AL errors). Awaiting live playtest confirmation at scale.
KB: context/wintesla-port.md audio-dropout section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field report (playtest night + operator): audio cutting in and out
toward the end of the match -- the classic shape of source-pool
pressure (sources release only at entity teardown; long matches
accumulate looping occupants like wreck burn/smoke until transients
lose the voice fight; OpenAL mixing capacity is finite). L4AUDRND now
tracks live/deleted/failed sources: a 30s '[audio] source census' line
plus an ALWAYS-logged line on every acquisition failure (the smoking
gun). Soak test next to confirm the leak/occupation slope.
Also: the ffa3933 jam-entry log had a raw newline in a string literal
(committed unbuilt -- process violation, caught by this build).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HandleMessage @004bcabc: message 2 -> SetLevel(weaponAlarm, 5) -- a
malfunction INDUCER, not an unjam; the state-machine comment had it
backwards. There is NO in-mission unjam in the binary (jams clear only
at ResetToInitialState = mission reset/respawn), and the EJECT cycle is
the anti-cook-off ammo jettison (DumpAmmo -> NoAmmo), not an unjam.
The old 'unjam delay' follow-up suggestion is retired as unauthentic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live verification closed the loop on issue #30: heat-jammed SRMs light
the authentic BTEJAM lamp on the weapon panel's ENG DATA page; the
default view shows nothing (authentic). Testers get told where to look.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Solo field report: both SRM launchers went inert with ammo remaining and
the default log could not answer whether they jammed or heat-failed --
every weapon-state transition log was gated behind BT_AMMO_LOG. The
edges are rare and mission-permanent, so they now log unconditionally:
[weap] JAMMED with temperature/alarm evidence at the CheckForJam hit,
and both NoAmmo edge logs (gate 1 destroyed/failHeat/disabled, gate 2
dry bin) ungated. The missing MFD presentation for these states is
issue #30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field report (playtest night, voice channel): everyone's HUD showed
them in 1st place. Root cause: the engine's score-replication
machinery was complete -- Player::WriteUpdateRecord ships currentScore,
the replicant ReadUpdateRecord applies it, CalcRanking sums every
scoring player -- but nothing marked the player's update record dirty
on a score change (the engine only ForceUpdates at mission end for the
fade sync). Every peer's replicant player therefore stayed at score 0
and each machine ranked its own (only-nonzero) player first.
Fix: ForceUpdate() after every score application in the BTPlayer
handlers (main, inflicted, and the severed-vehicle branch) -- the
existing update-record path does the rest.
Verified 2-node (force-damage kill scoring): the victim's machine ranks
the killer's REPLICANT at its replicated score (24) above its own
master (0); standings consistent on both nodes. BT_RANK_LOG=1 kept as
the 1 Hz per-player ranking dump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Systematic actor-death x lifecycle-phase sweep (operator request after
the WaitingForEgg zombie). New handling, all live-verified:
1. PRE-EGG console-pad loss (the real zombie phase): before the egg the
pod's only link is the console pad -- the game socket doesn't exist,
so the earlier RelayGameDown hook could never fire (the verification
run itself caught this). Detection now lives in
HostDisconnectedMessageHandler ConsoleHostType (relayMode +
pre-scene -> BTRelayRejoinNow); mesh keeps 1995 behavior.
2. MID-MISSION relay loss: the STOP is relay-sent (1995: the console
owned the clock), so a pod losing its relay mid-match played a
peer-less FOREVER-mission. RelayGameDown (scene presented) now
posts StopMissionMessage at +15s -> normal end -> lobby relaunch.
3. POST-RELEASE pod death stalled the round (survivors wait forever on
the dead peer's connection gate): relay _abort_round -> route -11
REJOIN to survivors -> everyone re-seats in seconds (reset-first
ordering makes the drop cascade re-trigger-proof).
4. BTRelayRejoinNow carries BT_SEAT_CLAIM (mirrored tag): without it
the rejoiner's own reclaim-hold blocked assignment -> ROSTER FULL
loop for the 90s window (found by verification round 2).
5. Beacon NAT keepalive (SIO_KEEPALIVE_VALS 60s/10s) so long
between-rounds waits can't be silently unseated.
6. BT_LOG_APPEND=1 preserves the log across lobby-loop generations
(truncation erased round-1 verification evidence).
Verified non-issues: relay pods never bind the -net listener (no
double-launch collision on the internet path); mesh bind-fail exits
cleanly. Full matrix + evidence: context/multiplayer.md;
scratchpad/audit_verify.sh is the repeatable rig.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The operator challenged the "inevitable ~minute" load (Task Manager
showed idle CPU) and was right. Measured chain:
- A/B same box/egg: dev 7s, +glass 30s, +BT_DEV_GAUGES 72s+.
- Drain census: identical ~560-event make streams; 190 events/s in dev
vs a metronomic 7/s in the console shape -- the loop, not the events.
- Stack sampling: 4/6 samples inside ExecuteIdle's wait-screen paint --
GDI GetDC/FillRect on the D3DPOOL_DEFAULT offscreen surface forces a
full GPU pipeline sync (~140ms/frame at glass window size). The
2026-07-22 flicker fix traded a cosmetic bug for a 25x load throttle,
worst on the operator box (bigger window + gauge-window GPU work),
with idle CPU because a GPU-sync stall isn't compute.
Fix (ExecuteIdle): the staging surface is D3DPOOL_SYSTEMMEM (GetDC is
pure CPU; survives Reset for free) uploaded via UpdateSurface, and a
change gate skips the whole idle frame unless the spinner phase
(12.5Hz) or state text changed -- between paints the loop runs free.
Result: console-shape load 72s+ -> 4.1s (BEGIN -> READY), drain at 345
events/s; every pod benefits (same code), and the all-ready launch gate
now waits on a seconds-scale slowest loader. Permanent pre-run
diagnostics kept: the drain census + >50ms slow-event lines (EVENT.cpp,
stand down at scene-live; slow-event identity captured BEFORE Process
-- Receiver::Receive(Event*) deletes the event).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field report (2026-07-23): with open seats the roster stays blue until
LAUNCH -- correct (loading cannot start before the roster is finalized,
and the LAUNCH press is what finalizes a partial roster) -- but the
operator lost all visibility into the post-launch staging, and worse,
the LAUNCH button's re-arm rode the "WAITING FOR OPERATOR" line that
only prints on a FULL-roster ACK, so in partial mode the button stayed
dead after round 1.
- The relay's 10s "launch HELD -- still loading: PLAYER n (k/m ready)"
lines now surface as the status banner ("STAGING -- PLAYER 2 still
loading (1/2 ready)") while the per-seat lights walk teal->green.
- "round RESET" re-arms the console (launched/ready/held cleared) so
every round's LAUNCH works regardless of roster fullness.
- The standby banner explains the mode: seated players are STANDING BY,
LAUNCH starts their loading, the mission fires itself at all-green.
- The seated count includes registered/ready players (it undercounted
once pods advanced past blue).
Monitor states unit-tested (held -> launch -> clear -> reset -> re-arm).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Playtest matchlog forensics: across 2,591 applied-damage events in two
rounds, Ballistic (type 1) damage NEVER appeared -- Collision/Explosive/
Laser/Energy only -- despite 136 projectile impacts and autocannon/Gauss
mechs. The weapon parses its authentic damageData.damageType correctly
(mechweap.cpp:316 maps "BallisticDamage" -> 1), but BTPushProjectile
never carried the type and every projectile-impact site in mech4.cpp
hardcoded ExplosiveDamageType. So autocannon/Gauss rounds were scaled
against armor by damageScale[Explosive] instead of damageScale[Ballistic]
(the model indexes a distinct scale cell per damage type) -- every
ballistic weapon did the wrong damage all night.
Lasers/PPCs were unaffected (their SendDamageMessage path carries the
weapon's own damageData); missiles happen to BE Explosive so only
autocannon & Gauss were mis-scaled.
BTProjectile gains a damageType field, threaded from the firing weapon
(damageData.damageType) at launch through to the impact dispatch;
missiles resolve Explosive from their own streamed type, autocannon/
Gauss now resolve Ballistic. The missile-splash template stays
Explosive. Enum is anonymous so the field casts to Enumeration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gamepad players expect stick X = turn; the pod-faithful default maps it
to torso twist, and the pedal pair couldn't take a single signed axis.
New bindings channel "Turn": a signed composite that decomposes into
the pedal pair at the RIO publish (+ = right pedal, - = left), ADDING
to direct pedal bindings so mixed rigs compose, clamped 0..1 per pedal.
The default bindings.txt stays pod-faithful and documents the
twin-stick alternative inline:
padaxis LX axis Turn
padaxis RX axis JoystickX
Also covers the HOTAS ask (any signed axis can now drive turning).
Verified: regenerated defaults carry the doc lines; a twin-stick-edited
bindings.txt parses and boots clean. Awaiting live pad verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The game audio plays hot with no in-game control -- testers couldn't
hear voice chat without the Windows mixer (playtest night). The -/=
keys now step the OpenAL listener gain (the master scale every source
inherits) in 5% steps, clamped 0..150%, edge-detected in the per-frame
poll with a foreground guard; the value persists to content\volume.cfg
and reloads at boot (BT_AUDIO_VOLUME env still wins when set; default
stays 0.6). README updated.
Verified: volume.cfg 0.25 -> boot log "master gain=0.25"; key stepping
needs a live focused session (awaiting live verification).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
XInput button emission is edge-based against previousPadButtons; the
disconnect path zeroed that memory WITHOUT emitting release edges, so
any button held when a controller (wrapper) died stayed pressed
game-side forever -- and the zeroing also erased what a reconnect would
have released. Field signature (playtest night): a PS3-pad-in-xbox-
mode wrapper died mid-hat-hold -> the look-view machine (hold-based,
mppr) latched a side view with the authored look pitch ("stuck side
view + pitch at ground, no recovery"); the keyboard-only run was clean.
On the connected->disconnected transition PadRIO now emits
EmitButton(address, 0) for every binding whose mask was held, then
forgets. Not reproducible headless (needs a real hardware unplug
mid-hold) -- awaiting the reporter's controller for live verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ROOT CAUSE [T1]: Mech::Reset's subsystem sweep (@0049fb74 loop-2) calls
vtable slot +0x28, which the DATA-section vtables prove is
ResetToInitialState (0050f9d8/00511d2c/00512078/0051242c all carry the
class RTIS at +0x28) -- our reconstruction mislabeled it DeathReset,
the EMPTY engine base virtual, so the whole per-subsystem reset was a
silent no-op ("ammo and weapons do not reset on respawn", playtest
night; matchlogs: 3 of 7 players fired zero projectiles after their
first death).
SECOND LAYER: the binary never refilled ammo at all -- its respawn
severed the vehicle and DropZoneReply built a NEW mech (fresh ctor =
full bins, pristine subsystems: the pod behavior testers remember).
Our port deliberately reuses the entity (the new-mech path was the
two-mech/camera-glitch family), so Reset now EMULATES ctor-freshness:
- the sweep starts at i=2 (binary-faithful: skips mapper + voltage
bus) and dispatches the engine DeathReset virtual as the vehicle;
per-class overrides chain each class's own authentic slot-10 RTIS
body (the recon RTIS namesakes are statically bound -- no common
virtual): MechWeapon @004b96ec, Emitter @004ba4d0 (charge cycle
restarts from Loading), ProjectileWeapon @004bbaf8 (unjams).
- MechSubsystem::RespawnRepair (PORT): heals the subsystem's PRIVATE
crit damage zone + status alarm -- the mech-zone heal never touched
those, which kept destroyed weapons dead across respawns.
- AmmoBin::DeathReset (PORT): refills to initialAmmoCount (@0x220, the
ctor value) + Loaded alarm + idle feed + cook-off disarmed;
ResetToInitialState stays authentically EMPTY per AMMOBIN.TCP.
Verified (2-node force-damage kill/respawn rig, BT_DEATH_LOG): every
respawn logs [respawn] ammo bin refills for both bins to streamed
capacity across 4 death cycles; mission un-regressed. KB corrected +
swept (multiplayer.md task #52 notes). Awaiting live tester
verification of depleted-bin refill + revived weapons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The between-rounds arcade flow (first-playtest-night field request): at
mission end every pod exited, seats dropped, and every round required
everyone to re-run join.bat with no way to adjust the mission.
1. AUTO-REJOIN: StopMission (operator END / mission clock -- NOT a
window close) sets gBTMissionStoppedByConsole; after the matchlog
upload btl4main relaunches the pod into the join wait with the same
cmdline (BT_CALLSIGN/BT_MECH ride the inherited environment).
2. SEAT RECLAIM: the assigned tag is mirrored file-scope
(BTRelaySelfTag) and re-presented as the 3rd SEAT_REQUEST payload
field (BT_SEAT_CLAIM); the relay holds a dropped seat for its tag
for 90s and a returning player gets their EXACT seat back -- static
ordering across rounds, the real-pod model. Non-claim joiners skip
reclaim-held seats.
3. LIVE MISSION EDITS: the console gains "Apply mission settings"
(enabled while a session runs) writing arena/time/weather/length
into the session egg; the relay re-reads the file at round reset and
egg release (roster shape locked mid-session).
Verified 2-pod 3-round: pods self-relaunched after each clock end; the
SECOND-to-rejoin still reclaimed its original seat (ordering held); the
between-rounds edit landed ("mission length now 75s"). Known edge
noted in the KB: a stale ready flag from a not-yet-exited old conn can
satisfy the launch gate mid-rejoin -- operators launch on green dots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live finding (round 3): firing RunMission while a pod is still loading
drowns that pod's load in the running mission's update streams -- the
same event queue serves both, so the load starves (the operator, always
last to join via the mech menu, loaded 5+ minutes or crashed while
testers took ~20s; solo loads on the same box take ~60s). The launch
timer now also waits for every registered pod's READY (route -10) so
everyone enters together, 1995-style; a held launch announces who's
still loading every 10s, and a 180s cap force-fires so a wedged pod
can't hold the night hostage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The game self-pins to core 0 by default; on the operator box core 0 is
shared with the console/relay/desktop, and the mission load drains
frame-paced -- a starved frame rate multiplied the operator's load time
to 3-5 min while internet testers loaded in ~20s (live finding, round
2). The local-instance launcher now spreads the game across the upper
cores unless BT_AFFINITY is explicitly set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When every pod disconnects at once (normal end of a multiplayer round),
a failed send inside the PEER_DOWN fanout recursively drops that peer
and mutates by_host mid-iteration -> RuntimeError -> THE RELAY DIES,
taking the round reset and the matchlog uploads with it (live crash,
first 3-player internet round). Iterate over snapshots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'SEAT 2 READY' for the first (only) player read as an off-by-one (field
report) -- host 1 is the CONSOLE in the authentic 1995 numbering, so
pods start at host 2. Human-facing lines now read PLAYER 1/2/... with
the host id in parentheses for cross-reference against the game[host=N]
wire lines; GUI monitor regexes updated (tag-keyed as before, verified
seated/ready/left in isolation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Measured (loadphase markers + queue blocker dump): renderer LoadMission
31ms, model loads 0.2s -- the teal->green minute is the LoadingMission
ready-gate (IsPriorityEmpty counts timed events too) waiting out the
THREE renderers' entity-make streams: every entity/subsystem creation
posts message 3 to each Renderer receiver (classIDs 8/30/36 =
video/audio/gauge), hundreds deep per queue, draining across the whole
window (the audio renderer's share is why sound starts mid-load).
Instrumentation kept: [loadphase] markers (renderer span + busy-pass
counter) always on; GeneralEventQueue::DumpBlockers + the class-named
DumpData fallback behind BT_LOAD_DIAG=1 (hundreds of lines per load).
Optimizing the drain (batching/budget) is an open perf item --
deliberately untouched before the 8-player night.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The measured 58s uncontended load has ~0.2s of model loads -- the rest
is unattributed. Stamp the renderer LoadMission span and count the
CheckLoad passes that find the event queue still busy (each delays the
ready transition by its 1s re-post), so the next live run names the
cost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every relay/console print now carries a wall-clock prefix (_StampedOut
stdout wrapper; the GUI monitor regexes use search() so the prefix is
transparent), and the pod's READY/pend lines carry wall time + tick --
post-mortems measure delays instead of guessing (the 3m40s load
reconstruction needed heartbeat-counting archaeology).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>