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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
The user's tip ("some mechs actually do fire backward") checks out end to
end. Binary ground truth [T1]:
- weapon+0x334 (attr 0x1B "RearFiring"): the ctor tests the MOUNT SEGMENT's
page name for the marker 'b' (@0x511aa2, part_013.c:6913-6930) -- the back
gun ports sitelbgunport/siterbgunport. The old reading ("EXT in the
weapon model name") was a double misread (wrong string, wrong name).
- mech+0x410 (attr id 50 "RearFiring"; the old `stateFlags` label): the ctor
ORs every weapon's flag (part_012.c:10220) -- "carries a rear arsenal".
- The mapper's five-state LOOK machine (part_013.c:396-459): on a state
change it re-aims the eyepoint (mech+0x360 = EyepointRotation -- consumed
by DPLEyeRenderable, already live in the port) and re-arms each weapon's
viewFireEnable(+0x3E0): FORWARD view = the non-rear weapons, LOOK-BACK
(yaw pi + lookBackAngle pitch) = the REAR-mounted ones, side/down = none.
+0x3E0 is the same flag the emitter's Loaded->Firing gate reads (the old
`useConfiguredPip` label) -- pips and fire permission both follow the view.
Port: rearFiring derived from the mount segment name (BTWeaponMountIsRear);
mech rearFiring ORed in the roster pass (the old SubProxy::IsDerivedFrom
stub returned 0 -- that loop never ran; now bridged through
BTWeaponIsRearFiring, which also fixes the weaponRoster fill); the look
commit is LIVE (BTCommitLookState: eyepoint EulerAngles from the authored
per-mech look angles -- now real members, were Wword scratch parks -- +
per-weapon view enables); MissileLauncher/ProjectileWeapon FireWeapon gate
on viewFireEnable like the emitter; RearFiring attrs (mech + weapon) bind
real members. Keyboard: HOLD 'V' = the pod's rear-view button.
Live-verified on the default blackhawk: ERMLaser_2 -> siterbgunport rear=1,
ERMLaser_3 -> sitelbgunport rear=1, PPCs/SRMs/torso mounts forward -- the
blackhawk authors TWO REAR LASERS (owens also has back ports). [rearfire]
trace under BT_PROJ_LOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
backward-firing rack
User report (madcat): missiles sometimes leave the mech BACKWARD; lasers
always fine. Root cause: only missiles fly the authored MuzzleVelocity
vector, and BTPushProjectile rotated it through the mech BODY basis
(localToWorld) -- but the madcat's racks ride the TORSO. The muzzle POINT
was segment-resolved (tracked the twist) while the launch DIRECTION followed
the LEGS: twist the torso far enough and rounds left the back. Lasers/ACs
aim straight at the designated pick (no launch vector) -- unaffected, exactly
as observed.
Binary ground truth [T1]: the fire builder FUN_004bcc60 spawns the missile
with the FULL muzzle-segment frame (the real GetMuzzlePoint fills an
AffineMatrix into the descriptor, :8762-8764), composes the authored MV with
the z-NEGATION (:8759-8761 -- confirming the 2026-07-12 telemetry finding)
onto the mech's own localVelocity (FUN_004b9cbc = owner+0x1c4), and the dumb
seeker re-aims 100u ahead of the MISSILE's own frame each frame (0x4be9a0
disasm) -- the mount orientation IS the launch direction.
Port: BTPushProjectile takes muzzle_seg and rotates the launch vector through
the mount segment's world frame (segment-to-entity x localToWorld, the same
matrix the muzzle point already used), keeping the z-negation convention, and
inherits the shooter's world velocity; body-basis fallback for callers
without a segment. All four call sites pass GetSegmentIndex().
Bonus decode banked for the full Missile-entity revival (KB-worthy): the
three authentic performances -- Seeker 0x4be9a0 (aim = target-frame offset /
100u-ahead dumb + loft/lead 0x4beae4), Thruster 0x4be474 (quaternion-slerp
BODY TURN toward the aim at turnRate deg/s), MoveAndCollide 0x4bef78
(velocity aerodynamically aligned to thrust via signed-square per-axis gains,
ballistic droop as fuel burns, PROXIMITY FUSE on seeker rangeToTarget,
max-range + altitude-floor retire).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Byte-decoded the un-exported master-perf block @0x4aad3d-0x4aaf14 (capstone;
the same gap that held the F5 footstep code) [T1]:
instab = (min(|AccelerationLastFrame| / maxUnstableAcceleration, 1)
x unstableAccelerationEffect)^2
instab += clamp((demand - legCycleSpeed)/demand, <=1) # "gun the engine"
x unstableGunTheEngineEffect # (demand clamped to
# [gimpStride(neg), runMax2])
if (legAnimationState == 4 /*turn-in-place*/) instab += unstableStopedTurnEffect
clamp to 1 -> mech+0x3F0 -> gyro->swayBias (gyro+0x3A8)
Every piece resolves to already-reconstructed structure: the model-record
tuning block rec+0x80..0x94 (ctor copy @0x4a2593 -- the six authored
Unstable* fields, previously parked in the Wword scratch bank, now real
members); AccelerationLastFrame@0x82c = the snapshot of the ring-derived
localAcceleration (copied at the perf tail @0x4ab142); CurrentSpeed@0x348 =
legCycleSpeed; the trn state 4 gate; and the gyro feed lands in the
EXISTING swayBias member + GyroscopeSimulation consumer (task #56) --
GyroFrameJointWrite's "0.0f model TBD" argument is now the live value.
UnstablePercentage (binary id 52 @0x3F0) binds the real member: the LAST
dead audio attribute is live -- the authored instability alarm (start
thresh 0.01 + volume = the fraction) sounds under hard maneuvers, and the
cockpit ambient sway now scales with reckless driving.
Live verification (30s, 0.6-throttle run): instab 0.38 during the walk->run
chase (cyc 22.2 vs demand 33.6), settling to 0.002-0.12 per-stride ripple at
steady run; attribute binds with live float values; [instab] trace under
BT_GYRO_TRACE. unstableSuperStopEffect/unstableHighVelocityEffect writers
remain unlocated (plausibly the airborne perf variant) -- noted on members.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
low-pass into the zone WAVs
232 zones author initialFilterFc(8)/initialFilterQ(9) (SBK 0..127) -- the
fixed resonant low-pass the EMU8000 applied in hardware; the port played
them unfiltered (flagship: the LaserLoaded charge hum, fc=57 Q=87, much
brighter/harsher than the arcade). The extractor now applies an RBJ 2-pole
low-pass per zone: cutoff = the AWE NRPN curve (100 + fc*7900/127 Hz),
resonance = SBK Q -> 0..+12 dB peak [T3 curve, endpoints exact], designed at
the zone's baked rate so note-60 playback reproduces the hardware's absolute
cutoff (pitch-shifted notes carry the filter -- same limitation as the
rate-baked tuning). Synthetic sweep verified: flat lows, +5.4 dB at the
authored 3.6 kHz cutoff, -22 dB @10 kHz, -46 dB @14 kHz.
Whole bake pipeline (filter -> attenuation) now runs in float with ONE int16
conversion + peak normalization: an int-per-stage draft hard-clipped 165 of
232 zones (resonance overshoot, worst 3% of samples); now only pre-existing
source-material clipping remains (14 files). 160 WAVs re-baked; the preset
table is unchanged (no engine rebuild).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User report: footfalls silent for the first 10-20 s of every mission (all
mechs), then solid. Root cause was three interlocking layers, each measured
with timestamped traces:
1. The authored footstep volume chain (LocalAcceleration [0,10]->ctl100 +
LocalVelocity [0,0.6]->ctl101 through authored N=30/N=15
AudioControlSmoothers, fill 0) hangs off the SOURCE's watcher chain
(scale watches smoother watches mixer watches source), and an idle
source's chain executes only at Start attempts -- one smoother sample
per stride.
2. Each hop is frame-gated (AudioComponent::ExecuteWatchers,
DefaultAudioFrameDelay), so any burst collapses to one execution.
3. The transient drop gate (vol < 0.3, AUDREND) rejected every Start while
the smoother average crawled up 1/30th per attempt -> ~25 dropped strides
before the first audible step, then per-frame execution while playing
kept it warm forever ("solid after that").
Fixes (engine-level, each documented in place):
- AudioScaleOf<T>::Execute now sends EVERY poll (scales are continuous
value-feeders; the base bitwise change-gate -- Motion::operator== is
memcmp -- froze on our deterministic gait math, where the original's
noisy physics floats never bit-repeated. Triggers/matchers keep the
change gate: their semantics are edge-based).
- Component/AudioComponent::PrimeWatchers(passes): recursive, GATE-FREE
watcher pump; AUDREND runs 30 passes on every transient Start request so
the authored smoothers evaluate at their true steady state before the
drop gate reads the volume.
- localAcceleration derives via the binary's exact structure: 15-sample
ring buffers of the raw position derivative + dt (ctor part_012.c:9836,
derive :15169-15195), in the PerformAndWatch tail so it runs every frame.
- AttributeWatcherOf::GrabCurrentValue private -> protected (the scale
override calls it).
Verified (30 s walk from cold start): drops 25 -> 3 (the survivors are
authentic quiet-stride gating: first gentle strides at vol ~0.28 vs the 0.3
gate), footfalls deliver from the first stride, 43 delivered with live
per-stride gain variation. Diag traces added: [accwatch]/[fsscale]/
[smooth]/[smoothcfg]/[motionscalecfg] + timestamps on DROP/volset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The binary's per-frame perf (disasm @0x4a9e80-0x4a9eb6 leg / @0x4aba86-
0x4abab9 body) computes footStep@0x394 = (jointlocal.y <= *footStepThreshold)
with the threshold pointer = SequenceController+0x20 = &ANI hdr[2] -- the
header word the engine's own AnimationInstance captures (JMOVER.cpp:1415)
and our SelectSequence skipped. States 0/1 retain the value.
Port: SelectSequence captures hdr[2]; PerformAndWatch evaluates the contact
level per frame from the cached jointlocal root joint (leg channel drives
the pose); the 150 ms clip-transition pulse + decay are RETIRED (steps fired
at clip boundaries with a fixed width; clips whose root crosses twice or
never counted wrong).
Live verification (30s walk): the authored threshold decodes real
(-0.232 root height); rootY oscillates across it per stride (-0.26 contact /
-0.19 swing) with clean 0->1->0 transitions per leg state (5/6/7); 34
footfall deliveries at stride rate with per-stride varying gains.
Also closes F23(2): the 17 authored AnimationState trigger states fire
empirically -- the EngineShiftFwd/Rev heard during the gait dead-band hunt
WERE states 10/11/14/15; the runtime clip numbering is correct.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F17 CollisionSpeed (binary id 24 @0x4B4): real member captured as
|worldLinearVelocity| when the contact accumulator arms (0->1) -- the authored
AttackVolume [0.9,1] / Brightness [0.7,1] scales over impact speed [0,25] now
make harder hits sound louder and brighter. ReduceButton (id 46 @0x340): real
watchable member (the keyboard rig never presses it). UnstablePercentage
stays deferred: its sway/overspeed model @0x3F0 is the known gyro-ledger gap
(live writer unexported); binding without the model would be a stand-in.
F19 footstep feed (the invention is dead, long live the authored chain):
new [motionscalecfg/motiontrigcfg] traces recovered the authored configs --
EVERY motion watcher extracts |linearMotion| (motionValue=3); the footstep
volume mixer is fed by LocalAcceleration [0,10] -> ctl100 (per-stride kick)
+ LocalVelocity [0,0.6] -> ctl101 (0.4 base while moving). The port never
wrote Mover::localAcceleration, so ctl100 read 0 and the old mech2.cpp
step-intensity broadcast (patch-sniffing, invented curve) fought the live
authored scale. Now: localAcceleration.linear = d(published velocity)/dt --
EXACTLY the binary's derivation ((avgVel - prev)/avgDt into +0x1e4,
part_012.c:15186-15195) -- and the broadcast is REMOVED.
Regression (30s, walk throttle): stable; footfalls deliver through the
wholly-authored chain with per-stride VARYING gains (0.61/0.72/0.62 -- real
step dynamics, impossible under the old constant-curve invention).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
publishes its real attribute table
Recovered the binary Torso attribute table [T1]: dense ids 3..15 from
MechSubsystem::NextAttributeID (HeatWatcher/PowerWatcher publish nothing --
binary parity), all 13 rows land on already-reconstructed members:
RotationOfTorsoVertical/Horizontal @0x1E4/0x1D8, HorizontalLimitRight/Left
@0x1DC/0x1E0, SpeedOfTorsoVertical/Horizontal @0x1EC/0x1E8 (the |rate| the
binary abs's -- our derive already did), StickPosition @0x1F0, TorsoUp/Down/
Left/Right/Center @0x1F8..0x208, MotionState @0x20C (statusFlags; the binary
writes 2 on the limit-hit frame -- the authored ==2 matcher is the twist-stop
clunk, settling the audit's [T4] guess).
Torso's AttributeIndex was default-constructed EMPTY -- TorsoTwistInt01/
Ext01/Stop01 were unreachable. Now the authored chain (pitch -200..+200
cents over twist speed 0.5..0.9, start/stop gate at 0.25, stop clunk on
MotionState==2) drives them unchanged.
Regression (25s): stable; all three bind real; **attrnull count = 0 -- every
authored audio attribute in the game now binds a real member.**
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F6 ReportLeak (the largest dead authored block -- 38 of 54 match watchers):
recovered the binary HeatSink attribute table (16-byte {id,name,off+1} rows
@0x50e438..0x50e4c8, ids 3..12) [T1] -- it confirms EVERY existing heat
binding (CurrentTemperature@0x114 .. CoolantMassLeakRate@0x130, HeatSink@
0x164) and adds the one row we never published: ReportLeak (id 12) ->
+0x138 = coolantActive, the INT leak hysteresis flag UpdateCoolant
(@004adbf8) drives 1/0 around draw 0.003/0.0025. PoweredSubsystem derives
from HeatSink, so the single row serves all 19 authored leak watchers
through the chained index, exactly like the binary. MechWeapon's pinned-id
pad absorbed the +1 chain shift (0x0E -> 0x0F, tripwire fired as designed).
F7 IncomingLock/DistanceToMissile (missile alarm): binary Mech table walked
in full (ids 21..56 [T1] -- also settles FootStep@0x394, CollisionSpeed@
0x4B4, UnstablePercentage@0x3F0, ReduceButton@0x340 for the next findings).
IncomingLock id 54 @0x3fc, DistanceToMissile id 56 @0x400; the old
"maxSpeed @0x400 = FLT_MAX" member was a MISREAD of the far default and is
retired (its 1000.0f "override" was RadarRange id 47 @0x404, already
published). Real members + accumulators: Missile::MoveAndCollide reports
target + range each tick (BTReportIncomingMissile bridge); PerformAndWatch
latches per frame. The authored beeper (match 1/0) + range->TEMPO scale
(100..800 -> 600..10, accelerating as the missile closes) read them
unchanged. Drive is intent-level [T3]; init 0/FLT_MAX matches the binary
reset (part_012.c:9446-9447).
Regression (30s): stable; ReportLeak binds real on every subsystem (0 pad
redirects); DistanceToMissile binds with FLT_MAX live; attrnull 41 -> 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New L4AUDEFX bridge (OpenAL Soft ALC_EXT_EFX): one EAXReverb aux slot at the
authentic AUDIO.INI global_reverb_scale (0.3) + a scratch AL_FILTER_LOWPASS
(params copied at attach).
F9 filters (was: computed then thrown away -- everything full-bright at all
distances): Dynamic3D ExecuteModel drives GAINHF from highFreqCutoffScale x
brightnessScale x maxMIDIFilterCutoff, UNGATED (decomp part_008.c:7496,
7589-7604 -- every moving 3D sound dulls with distance per the AUDIO.INI
knee-60/exp-2.0 model); Static3D from brightness x max (:7831-7884); Direct
inside its existing gated NRPN-rate block. AWE 100-8000 Hz curve -> EFX
5 kHz-reference gainhf via a 2-pole approximation [T3 curve, endpoints exact].
F11 reverb (was: bone-dry everywhere): 3D patch sources attach an aux send at
Start, exactly where the original sent CC91 = global_reverb_scale
(part_008.c:7278-7394); Direct cockpit sources keep CC91=0 -- dry. The
wet-exterior vs dry-cockpit contrast is back.
F12 placement (was: every cockpit sound dead-center): DirectPatchSource
Start places sources by the authored 6-value position enum (front/rear card
+ pan CC10, decomp @00463848/@004638a8) as listener-relative directions,
composed with the zone L/C/R pan. New PatchResource GetBankID/GetPatchID
pass-throughs (the LOD accessor is protected).
Regression (35s drive+fire): EFX READY, stable, deliveries unregressed, no AL
errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Byte-decoded finding [T1]: the gait SM has no stable state for a demand in
(walkStrideLength@0x534, reverseSpeedMax@0x538). Cap semantics settled from
LoadLocomotionClips @0x4a80d4: 0x538 is the walk->run TRANSITION CLIP's exit
speed (the run ENGAGE threshold -- not a reverse max) and 0x34c (run-cycle avg
root speed) is the mapper's continuous demand multiplier (FUN_004afd10). The
finished-callbacks up-shift when tgt > 0x534 but enter/sustain run only when
tgt >= 0x538, so a demand parked between them (Blackhawk: 22.02-30.87 =
throttle 36-50%) hunts walk -> shift-up -> shift-down forever, firing the
authored EngineShiftFwd/Rev sounds each swing -- the user's repro, faithfully
reproduced by all-authentic logic + data. Likely masked on the pod by
MECHANICAL throttle-quadrant detents ("5 speeds" lore; the software path is
notch-free) [T4] -- added to get-from-Nick.
Accommodation (keyboard = our stand-in lever): at key REST, snap the lever to
the nearer dead-band edge (walk cap, or run engage + margin since the cont
check is >=). Sweeping THROUGH the band while held stays continuous -- an
authentic moving lever; the single shift it fires is the authentic shift.
BT_GAIT_TRACE logs [gaitdetent] snaps.
KB: locomotion.md gait-dead-band section, pod-hardware.md mechanical-notch
hypothesis, open-questions.md Nick item.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F3 distance: alDistanceModel(AL_NONE); restored the commented authored
distance multiply in Dynamic3D::CalculateSourceVolumeScale, added the same
override to Static3D, dropped the (now inert) AL_MAX_DISTANCE writes. Far
battle audio follows the authored knee/rolloff curve again, and the volume
cull / voice steal / ducking chains are distance-aware.
F4 volume law: AL_GAIN = volume_scale^2 at all 3 per-frame sites (Direct/
Dyn3D/Static3D) -- the GM CC7 squared curve; linear was ~+6 dB at mids.
F10 doppler: alDopplerFactor(0) (AL's model ran wrong constants + sign-
inverted velocity: approaching sources pitched DOWN); the AUTHORED
dopplerCents (AUDIO.INI range/speed-of-sound model, decomp-proven consumer
part_008.c:7466) now adds into the Dynamic3D pitch chain.
F15 ConfigureActivePress: published at the MechSubsystem BASE (binary id 2,
descriptor @0x50de5c -> +0x110); renamed the misnamed vitalSubsystemIndex
member (the weapon ConfigureMappables handler already drives it 0/-1).
Removed the invented Sensor/Myomers duplicates and RESTORED their byte-
exact layouts (0x328/0x358 allocs + asserts). MechWeapon's pinned-id pad
absorbed the +1 chain shift -- which matches the binary's own numbering.
F18 cook-off warning: AmmoBin FireCountdownStarted -> the existing
cookOffArmed @0x18C (binary table @0x512600); the countdown klaxon can fire.
F20 zoom blip: L4MechControlsMapper publishes TargetRangeExponent -> the live
@0x1a4 zoom member (own table chained to MechControlsMapper).
KB: replaced the bogus divisionParameters+0x10 rate read with
SystemClock::GetTicksPerSecond() (FUN_0044e19c is GetFrameRate -- original
audio frames were CLOCK TICKS).
Regression (35s drive+fire): stable; ConfigureActivePress binds real on all 9
subsystems (idle -1, zero pad redirects); FireCountdownStarted +
TargetRangeExponent bind live members; attrnull 53 -> 41; chirp still dead;
footfalls still fire (gain now correctly squared).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'footsteps disappear when running' report traced to the authored interior-
footfall volume chain: an AudioScalarScale on LocalVelocity with the INVERTED
curve aB=[0,20] -> cB=[1,0] (through a smoother) drives the FootFallInt source
volume -- full at standstill, fading with speed, ZERO at >=20 (run). The
drop-gate evaluation (CalculateSourceVolumeScale -> ExecuteWatchers) pulls this
authored value and overrides the game-side mixer feed, so run-speed footfalls
are dropped BY DESIGN: at running speed the pod cockpit is engine roar; the
1995 sound design fades the interior footfall out. Timeline evidence:
volset(1,1 from the mixer) -> matchfire Start -> volset(0 from the authored
chain during drop evaluation) -> DROP, exactly once per stride at speed >= 20.
No code change -- this commit records the finding. User-verified state:
chirp gone, per-step static gone (velocity smoothing), footfalls audible at
walk, authentically absent at run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User pinpointed it: the per-stride 'static' was NOT the footstep sample -- it
was the ambient engine hum being interrupted in time with the footfalls.
Diagnosis (live tape): EnginePower01 was caught in a rapid Play->Stop cycle
(80x/session). The idle-hum is gated by authored LocalVelocity triggers (hum
ON in speed [1,20], OFF above 20) -- and the port's localVelocity is a RAW
per-frame position derivative that pulses with every stride (gait acceleration
+ ground-snap bob). At walking speed |v| straddles the 20 band edge, so the
trigger Start/Stop-flapped the hum on every foot plant; at run |v| >> 20 keeps
it off (matching 'goes away at higher speeds').
Fix: ~0.25s exponential filter on the PUBLISHED localVelocity.linearMotion
(fwd + vertical) -- kills the stride ripple, tracks real speed changes within
a couple strides. The physics worldLinearVelocity (StaticBounce, collision
damage pricing) is untouched; update records + the impact gate get the
smoother value (both benefit).
VERIFIED: EnginePower 1 play / 0 stops for the whole run (was 80 stop-cycles);
FootFall cycles normally per stride; gear shifts/windup lifecycle intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous 'chirp killed' verification was WRONG: it checked the 1/s playing
snapshot (which misses 0.12s pulses) instead of delivery counts -- the run had
186 ProgramButton deliveries. Corrected metric + trigcfg now prints its target
component, revealing NINE thresh=-1 Start triggers on the configure-ticker
sequence: ConfigureActivePress on Avionics + Myomers (backed real, -1) AND
SEVEN more subsystems still on the inert pad (0 = "button 0 held" -> ticker on).
Fix: a TYPED pad in the missing-attribute redirect -- ConfigureActivePress
resolves to a shared static int = -1 (the authored none-held idle) instead of
the zero pad; every other missing attr keeps the zero pad. Covers all
configurable subsystems at once with no layout changes; self-clears per
subsystem as each gets a real driven member.
VERIFIED BY DELIVERY COUNT: ProgramButton01 deliveries = 0 (was 31-186/run);
7 attrs took the -1 pad; the rest of the soundscape intact (weapon cycles,
loops, translocate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The obnoxious always-on clicking was the cockpit CONFIGURE-MODE ticker: an
authored looped sequence gated by an AudioIntegerTrigger with threshold -1 on
<subsystem>.ConfigureActivePress (the held-configure-button index; -1 = none).
The attribute was one of the inert-pad redirects -- the pad reads 0 = "button 0
held forever" -> the ticker Start-fired at load and pulsed 2.7/s eternally.
Found via [sendcfg]/[seqstart] traces: the first seq start immediately follows
the trigcfg(thresh=-1, onID=Start) construction on the pad address.
Fix: real int configureActivePress = -1 APPENDED to Sensor ("Avionics") and
Myomers, registered in their attribute tables. Both classes are byte-exact
factory allocs, so the sizeof locks AND the placement-new alloc constants are
bumped TOGETHER (Sensor 0x328->0x32C, Myomers 0x358->0x35C) -- the tripwire
caught a mid-layout insert (seekVoltage@0x330 shift) and a fixed-alloc overrun
before they shipped; member moved to the true tail. Wiring the live value
(EnterConfiguration/ExitConfiguration sets/clears the index) restores the
authored hold-to-configure tick later; idle -1 is the correct silent state.
VERIFIED: ProgramButton01 plays ZERO times (was the 2.7/s chirp);
ConfigureActivePress binds real (vtbl=FFFFFFFF = the -1); FootFallInt at 0.98
gain; stable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The output-filtered broadcast still fed EVERY volume/brightness mixer -- the
always-on ambience pulse's own volume mixer got cranked to step intensity on
every stride ('a stuck button', 3/s, loud). The send now identifies footstep
sources by authored patch (FootFallInt/Ext: bank2 37-39, bank1 106/107) through
mixer->GetTargetComponent() -> AudioSource -> PatchResource LOD bank/patch
(new accessors: GetTargetComponent, GetBankID/GetPatchID,
AudioResource::PeekAudioLevelOfDetail).
Also: step intensity curve raised (speed/25, floor 0.55 -- a mech stomp is
never subtle; it was firing at ~0.4 gain and masked under 0.9-gain layers,
heard as an unrecognizable 'orchestral hit').
Verified: FootFallInt at 0.78 gain in the live tape; the ambience pulse back
to its brief authored form (absent from 1/s snapshots).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>