Commit Graph
63 Commits
Author SHA1 Message Date
arcattackandClaude Fable 5 1436d27ac6 D1 phase 7: LAN auto-discovery (BT_RELAY=auto) + operator LAN scripts
LAN players now find the game with zero configuration -- the 90s arcade
model (cabinets locate the operator station) restored:

- Client (L4NET RelayDiscover): BT_RELAY=auto broadcasts 'BTR1DISC' on
  udp/15999 (255.255.255.255 AND 127.0.0.1 -- broadcast doesn't reliably
  loop back on Windows; covers same-box sessions), 5 x 1s attempts; the
  answering relay's SOURCE IP + its advertised console port become the
  relay address.  Explicit <host>:<port> path unchanged; mesh untouched.
- Relay (btconsole.py): best-effort discovery responder on udp/15999
  answers 'BTR1HERE' + <u16 consolePort>; degrades gracefully (logged) if
  the port is taken.
- Operator app: Export player scripts now writes a join_as_playerN_lan.bat
  pair (BT_RELAY=auto) next to each internet script -- LAN guests
  double-click and are found; internet guests use the public-host script.

Verified 2-node: both pods BT_RELAY=auto -> probe answered through a real
interface (not just loopback) -> discovered 172.19.x.x:1500 -> full session
to RunningMission.  KB: multiplayer.md D1 section updated (+ the operator
console entry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:12:01 -05:00
arcattackandClaude Fable 5 43fa53d542 Operator console app: PySide6 station + validated egg model (the lost 1995 console, recreated)
The operator station the archive never preserved: build missions with
authoritative dropdowns, run the console/relay, watch pods arrive, launch --
LAN mesh or internet relay -- plus one-click local instances for testing.

tools/eggmodel.py (headless, importable, self-tested 14/14):
- Order-preserving egg parse/emit: the editor manages [mission]/[pilots]/
  per-pilot pages; bitmap rasters, ordinals and role pages survive untouched.
- EVERY value set read LIVE from content/BTL4.RES (the resscan parser,
  importable): maps = MakeMessageStream(14) INTERSECT ExistanceBoxStream(26)
  (the game aborts unless both exist); colors/badges/patches from the
  VehicleTable resource (type 25, NUL-line NotationFile text with ;-comments);
  vehicles = ModelList names, known-good mechs (bhk1, madcat) surfaced first.
- validate(): the full required-key ruleset from MISSION.cpp/btl4mssn.cpp
  (missing map/time/weather/scenario, per-pilot hostType/vehicle/dropzone/
  color/patch/badge/experience/role, role page model=, illegal values,
  duplicate addresses) -- kills the hand-edited-egg crash class, incl. the
  color=Red-vs-Crimson gotcha that bit the dev eggs.

tools/btoperator.py (PySide6, dark Fusion theme):
- Mission form + pilot roster table (all dropdowns from eggmodel), relay-tag
  auto-numbering, add/remove pilots, egg New/Open/Save/Validate.
- Mode switch: Relay (internet; runs btconsole --relay) / Mesh (LAN legacy;
  dial-out console at address-port minus 1 per the +1 rule).
- Session panel: QProcess drives btconsole.py; stdout parsed live into
  per-pilot state lights (waiting/egg/registered/LAUNCHED) + relay stats.
- Local launcher: spawns btl4.exe per roster row with BT_RELAY/BT_SELF (or
  mesh -net port), BT_DEV_GAUGES/BT_START_INSIDE toggles.
- Export player scripts: per-pilot join_as_playerN.bat with the public
  relay hostname (a remote player's whole setup = run one .bat).
- Wire/protocol code stays in btconsole.py/eggmodel.py -- the GUI is only
  UI + process management; CLI + test harnesses share the same tested core.

Verified: eggmodel self-test 14/14; scripted end-to-end
(scratchpad/operator_e2e.py) drives the real window through its own methods --
start relay session -> launch 2 local instances -> monitor observes the
mission LAUNCH (user-confirmed live on screen: both pods in-mission).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 10:57:30 -05:00
arcattackandClaude Fable 5 bf8c2ffaae D1 phase 1: relay TCP core in btconsole.py (--relay mode)
The console emulator gains a relay/dispatcher mode for internet play (D1):
pods dial OUT to this process (NAT-friendly) instead of the console dialing
into each pod.  Legacy dial-out mode is untouched (verified: an unmodified
2-node mesh session still forms end-to-end through it).

--relay <consolePort> <egg> [--bind ADDR] [--udp-drop PCT], single
selectors-based event loop:
- console listener (consolePort): streams the NUL-line egg to each pod on
  connect; GLOBAL launch timer -- RunMission x2 to ALL pods at +20s/+4s after
  the LAST pod has its egg (fixes the per-pod skew of the legacy tool).
- game TCP listener (consolePort+1): envelope router {route i32, length u32}.
  First frame must be HELLO ('BTR1' + hostID validated against the egg
  [pilots] roster); PEER_UP/PEER_DOWN exchange; route>=2 unicast (route
  rewritten to sender), -1 broadcast-except-sender (the client sends each
  broadcast ONCE -- kills the N-1x upload duplication).  Frame cap 1600
  (NETWORKMANAGER_BUFFER_SIZE); protocol violations drop the connection.
- game UDP socket (consolePort+1/udp): endpoint learned per-datagram
  (NAT-rebind tolerant), HELLO->HELLO-ACK, verbatim forward by route,
  TCP-wrap fallback when the target's UDP endpoint is unknown; --udp-drop
  test hook for the robustness phase.

Verified: scratchpad/relay_stub_test.py (self-contained; spawns the relay +
stub clients) -- 24/24 checks pass: egg delivery, registration, PEER events,
unicast route-rewrite, broadcast fan-out (sender excluded), UDP ack/forward/
fallback, and all reject paths (bad magic, dup id, out-of-roster, oversize).

Plan: ~/.claude/plans/partitioned-snuggling-piglet.md (D1 relay + UDP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:44:01 -05:00
arcattackandClaude Opus 4.8 c581553a6c Audio (AUDIO_FIDELITY F14 residue): bake the authored static resonant
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>
2026-07-16 15:36:44 -05:00
arcattackandClaude Opus 4.8 691e88569a Audio Phase 2 (AUDIO_FIDELITY F1/F2/F13/F14): full-zone soundbank -- key-splits,
layers, baked tuning, loop regions, release fades

Extractor rewrite (tools/sf2extract.py): every sample-bearing instrument zone
becomes a SAMPLEINFO slot -- 603 zones / 241 presets (68/115 + 94/126 multi-
zone, matching the audit exactly).  Per zone: keyRange(43), sampleModes(54),
SBK samplePitch(55) + rootKey(58) + coarse/fineTune(51/52), attenuation(48,
INVERTED SBK scale, 0.375 dB/step [T3], baked into the PCM), releaseVolEnv(38),
pan(17), shdr loop region.

F2 pitch (algebraically exact): WAV rate = round(44100 * 2^(((6000 -
rootCents) + tune)/1200)) -- EMU8000 v1 base 44100.  Cross-checks: FootFall
17300 Hz (the audit's +4.2 st), MissileLoaded low zone 88200 (-24 st),
Warnings01 8-way klaxon split w/ 3 looped zones, Death01 162 Hz extremes.

F1 zone selection: SetupPatch/PlayNote take the authored note;
attach/play only zones whose [keyLo,keyHi] contains it (detach the rest so a
rewound source can't replay a stale buffer).  Live-verified: LaserLoaded/
MissileLoaded note 36 -> low clunk zone, note 84 -> high blip zone (pitch 4);
LaserCFire plays all 3 authored layers incl the looping sustain.  Stereo-pair
zones pan via listener-relative AL_POSITION (distance model is AL_NONE).
MAX_PRESET_SAMPLES=25 (AllExplosion); fixed PRESET_isImplemented's >=5 bound.

F13 loops + releases: authored [loopStart,loopEnd] applied via
AL_SOFT_loop_points at buffer load (0 rejections; kills the latent boom-loop
on MechExplosion's 1.5% sustain slice + the ~2.4 Hz LaserBSustain wrap tick);
StopNote now honors the authored releaseVolEnv (1.1-3.9 s on ~20 looping
presets) with a dB-linear fade serviced from AudioHead::Execute; restarts
reclaim fading sources.  One-shots keep the instant stop (faithful).

Regression (40s drive+fire): stable, loop points accepted, key-splits + layers
verified in the delivery trace, chirp still dead, footfalls fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:04:34 -05:00
arcattackandClaude Opus 4.8 e017f257a5 Tools: soundboard.py -- click-to-play UI for the 241-sample soundbank (task #50)
Stdlib-only (tkinter + winsound) sample browser: filter box, per-button
[bank:patch] tags parsed from audiopresets.cpp so an ear-identified sound maps
straight back to the game's (bankID, patchID), Play-All sweep, STOP/ESC.
For verifying sample identity/pitch (the 22050 Hz extraction guess) by ear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 08:01:27 -05:00
arcattackandClaude Opus 4.8 41c8ac120d Audio: fix runaway loops -- read SF2 sampleModes, not loop-point presence (task #50)
Weapon fire (and ~all one-shots) played FOREVER: sf2extract flagged a sample as
looping whenever the SF2 shdr had loop points -- but almost every SoundFont sample
has loop points; whether to actually loop is the instrument's sampleModes generator
(igen oper 54): 0/2 = one-shot, 1 = loop, 3 = loop-until-release.  The old heuristic
looped 239/241 samples, so SetupPatch set AL_LOOPING=1 on fire/explosion/step sounds.

Read sampleModes at the same igen zone as sampleID and loop only on 1/3.  Now
166 one-shot / 75 loop: LaserAFire/BigExplosion/BasicClick -> ForceStatic (one-shot),
EnginePower/EngineMotor/coolant -> LoopAtWill (correctly sustained).  Regenerated
audiopresets.cpp; the WAV PCM is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:56:38 -05:00
arcattackandClaude Opus 4.8 5b46655b82 Audio: ENABLE sound -- real OpenAL + in-tree WAV loader; both backend DLLs were no-op STUBS (task #50)
Root cause of "no sound, ever": the two audio backend DLLs shipped in the repo
are fakes. libsndfile-1.dll exports 15 funcs BY ORDINAL ONLY (no names) and
sf_open() always returns NULL; OpenAL32.dll (72KB) imports only KERNEL32 -- no
dsound/wasapi/winmm -- so it is a pure no-op that returns fake handles
(ctx=0x00000001, alGenSources->0) and never touches the hardware. The whole
render->device->buffer->source->play chain ran clean and silent.

Fixes:
- OpenAL32.dll: replace the stub with the real OpenAL Soft 1.25.2 Win32 build
  (imports AVRT/ole32/WINMM, real WASAPI backend). The exe imports the 25 AL
  funcs by NAME so it is a drop-in; alGenSources now yields a live source and
  alSourcePlay reaches AL_PLAYING.
- libsndfile: DROPPED entirely. It is replaced by LoadWavPCM() in L4AUDRES --
  a tiny RIFF/WAVE fmt+data reader that loads our soundbank WAVs (16-bit PCM)
  straight into the AL buffer. Removed the .lib/.dll from the link + copy and
  git-rm'd the stub. (This also kills the "ordinal 50 could not be located in
  libsndfile-1.dll" load-failure popup: adding an sf_strerror import bound to
  an ordinal the 2..16-only stub could not satisfy.)
- Soundbank: 241 samples cracked from AUDIO1/2.RES (SF2 v1.0) by
  tools/sf2extract.py into content/AUDIO/*.wav + the allPresets[2][128] table
  (audiopresets.cpp), replacing the zero-init btstubs stub. All 241 now load
  (alErr=0). BT_AUDIO_TEST plays buffer0 as proof-of-life; BT_AUDIO_LOG traces
  the chain.

Remaining: in-game triggering (AudioEntities on fire/step/engine/explosion)
so PlayNote fires during play -- next audio wave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:55:33 -05:00
arcattackandClaude Opus 4.8 49d73dc8e2 MP: peer shakiness ROOT-CAUSED as single-box packet jitter (rig, not game) + affinity launcher (task #50)
The residual RANDOM peer shakiness on accel/decel was proven (BT_RXJIT record
inter-arrival probe) to be TEST-RIG packet jitter, not the game: two Debug btl4
nodes on one box contend for CPU, so Windows batches their TCP delivery -- records
arrive in bursts (max 56-226ms gaps, burstiness 3-7x) instead of even ~17ms.  The
peer dead-reckons across the gaps then snaps -> random shake.  Pinning the nodes to
disjoint cores restored even ~17ms delivery (burstiness ~1.0) and the shakiness
vanished (user-confirmed 'that was it').  Real pods = dedicated machines, no
contention -> never see it.  So NO un-authentic jitter buffer -- the coupled
body-channel peer (96a896a/f094d78/23f1532) is the authentic + correct finish.

 - mech.cpp: BT_RXJIT record-arrival-jitter probe (env-gated).
 - tools/mp_launch.sh: 2-node launcher that pins nodes to disjoint cores (bakes in
   the fair-delivery condition; documents why).
 - context/multiplayer.md: the finding, so it is not re-litigated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:14:22 -05:00
arcattackandClaude Fable 5 8674bca2d8 btconsole: survive socket errors + flush output (the mid-session sync-freeze lead)
The console emulator's recv loop only caught socket.timeout -- any reset/abort killed
the thread UNHANDLED, its buffered stdout died with it, and with both threads gone the
process exited silently.  The 2-node session then froze replication BOTH ways (each pod
holding a dead ConsoleHost socket; the engine never logged a console disconnect).  Now:
OSErrors are caught + logged and the thread idles alive; prints flush.  Run with
python -u and > console.log to capture the death reason on any recurrence.  The engine-
side question (why a dead console freezes peer replication + the disconnect handler's
gameListenerSocket-vs-consoleListenerSocket close) is logged in open-questions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 09:30:47 -05:00
arcattackandClaude Opus 4.8 16f5f545ce gauge-complete P4b: AggregateHeatSink 0xBBE -> HeatSink/AmbientTemperature bound (last config NULL cleared)
The 0xBBE heat-sink BANK was built as a plain HeatSink, so the numeric-R
cockpit gauge binding HeatSink/AmbientTemperature (L4GAUGE.CFG:4552) had
no publisher -> the LAST unresolved config attribute.

Reconstruct AggregateHeatSink : HeatSink (ctor @4ae8d0, own GUID 0x50e590),
byte-exact + static_assert-locked (heatSinkCount@0x1D0, ambientTemperature
@0x1D4=300, helper@0x1D8 0xC link node, sizeof 0x1E4 == factory alloc @9993).
Publish HeatSinkCount + AmbientTemperature via a dense-prefix attribute table
chained to HeatSink::NextAttributeID (shared HeatSink table unchanged, so
CoolantMass/CoolantCapacity keep resolving). Move CreateHeatSinkBankSubsystem
into heatfamily_reslice.cpp (needs the class def) and build the real class at
factory case 0xBBE; mech.cpp unchanged.

DELIBERATE DEVIATION (documented in the class): keep the base HeatSinkSimulation
the HeatSink ctor installs; do NOT reimplement the authentic Performance @4ae73c
-- it derefs a raw self+0xE0 -> [+0x158] that does not map in our compiled layout
(AV/NaN, and runs for EVERY mech), and ambientTemperature is a frozen constant so
the gauge reads 300 either way. Authentic relaxation model deferred.

Verified: [attr] HeatSink/AmbientTemperature OK; all 50 config attribute
bindings resolve (0 NULL); no every-mech crash; combat TARGET DESTROYED,
FIRED #41+, un-regressed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:10:11 -05:00
arcattackandClaude Opus 4.8 c2c4ab1f67 docs: record the gauge attribute-wave (mechanism + 4 increments) + durable RE tools
Document the cockpit-gauge attribute-pointer system in GAUGE_COMPOSITE.md
(§Attribute wave) and CLAUDE.md §10: how a Subsystem/Attribute or bare-Attribute
binding resolves (ParseAttribute -> FindSubsystem/GetName -> GetAttributePointer
-> activeAttributeIndex), what a class must publish (AttributeID enum +
AttributePointers[] + GetAttributeIndex chained + DefaultData wiring), the
DENSE-TABLE HAZARD (Build leaves gap slots uninitialized, Find strcmps every
slot -> tables must be a dense prefix), and the base-primitive vs BT-specific
widget split.  Records the 4 committed increments (HeatSink table, Mech table,
vertBar, segmentArcRatio) and the follow-ups (oneOfSeveralPixInt/map/PlayerStatus/
vehicleSubSystems, the #if0'd HeatSink-bank AmbientTemperature, the Reservoir
coolantCapacity shadow).

Promote the two reverse-engineering helpers from the session scratchpad into
tools/ (durable): disas2.py (capstone + PE parse -- recovers x87 float math
Ghidra drops; FUN_004dcd94=round, FUN_004dcd00=fabs) and vtdump.py (dump a class
vtable to find an override the assert-anchored decomp never exported, e.g.
SegmentArcRatio::Execute).  Doc references updated scratchpad/ -> tools/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:52:53 -05:00
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00