ca6718a8761240a7e576dbd40e3cd6ce9006a7e6
614
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca6718a876 |
turrets: correct the record -- the props are scenery, but PGN is a real turret that was cut
Amends what
|
||
|
|
0534e93eec |
the level turrets are scenery: TurretClassID is a red herring, 0xBDE is ThermalSight
Playtest question: "many levels have turrets/cannons that don't fire, aren't those supposed to shoot at players?" Chased it properly because the enum really does look like a smoking gun. engine/MUNGA/VDATA.h:209 declares TurretClassID as the LAST entry of the BT block -- right after MechTechClassID, immediately before the ND section -- which is exactly where a real BattleTech class would live. And the mech factory has a live `case 0xbde:` for it (part_012.c:10186). Both of those point the wrong way. That enum slot computes to 3038 = 0xBDE, and 0xBDE in the shipped runtime is ThermalSight: ctor @4b8718, already reconstructed and done in thermalsight.cpp. Same enum-vs-runtime label drift CLASSMAP already records for HUD and MechTech at 0xBD6/0xBDC, which is why the rule is to resolve the ctor address and never trust the factory case label. The case is also in the mech SUBSYSTEM factory (roster param_1[0x4a]), not an entity factory, so it could not spawn a world object even if the label were right. Everything else agrees. There is no `class Turret` anywhere in the engine or the port. BTL4OPT.EXE contains zero occurrences of turret, sentry, emplac, brain, patrol, aggro, hostile or npc (the apparent "ai"/"bot" hits are substrings of failureheat and verticallimitbottom). jointturret, which looked promising, is a skeleton joint on OWN/PGN/STI with own_tur.bgf -- a mech turret-torso, not a world gun. And the destructible world classes in CULTURAL.h -- Landmark, CulturalIcon, UnscalableTerrain -- have no weapon, fire or target members at all; they take damage and break, and that is the whole of it. So the turret and cannon models in the maps (TT1/TT2/TWR/TK1/APC, each with a D damaged variant) are scenery. They did not fire in 1995. Making them fire would be inventing a feature rather than reconstructing one, and it would need an entity class, a targeting model and a threat model that the binary has no trace of -- which is consistent with the standing T1 finding that BT shipped no AI at all and is PvP-only by design. Written up in combat-damage.md next to the No-AI section, since the enum is convincing enough that someone will find it again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1af2dfc769 |
back the audio cap off to opt-in: 7-23 already fixed the complaint, and the cap is a governor
Correcting my own framing in |
||
|
|
7e57816d39 |
audio cut out in firefights because nobody ever asked OpenAL for more than 256 voices
Field report from 2026-07-23 was "audio cutting in and out toward the end of the
match". The night-5 logs say it precisely: 2017 ACQUIRE FAILED lines in a single
match, and every last one of them at live=256.
It is not a leak, which is where I started and where the code comments still
pointed. The source census sits at a healthy 45-60 live, spikes during
firefights, and drains right back down afterwards -- 226 back to 42 in one
window -- so the engine's priority steal loop is doing exactly what it should:
AudioSourceStop/SuspendMaintenance -> ReleaseChannels -> ReleaseSourceSet ->
alDeleteSources, with the live counter following it down. The 2026-07-23 fix
(
|
||
|
|
819772f974 |
the rest of the Myomers wrapper: two live outputs recovered, and one branch that never fires
Follow-on to |
||
|
|
b70654dd11 |
torso twist died after a respawn: Myomers ran the inner integrator, never the wrapper
Gitea #70. Torso twist intermittently stops working after a death/respawn and stays stopped, with the generator at a full 10000V and the voltage link resolving fine. A stale state, not a live one. The Torso is a PowerWatcher that mirrors the electrical level of the subsystem it watches -- for the MadCat that is roster slot 15, Myomers -- and torso.cpp:570 holds effectiveTwistRate at 0 whenever that mirrored level is not Ready. So the question was never about the torso. It was: why does Myomers sit at NoVoltage forever? Because it never ran the state machine that would walk it back. The Myomers ctor @004b8fec stores [0x511620] into activePerformance, and that pointer resolves to 0x4b8b9c -- whose first instruction is `call 0x4b0bd0`, which is PoweredSubsystem::PoweredSubsystemSimulation. @004b8d18, the function this port had registered, is the INNER drive-heat integrator that wrapper goes on to run. We registered the inside of the onion. PoweredSubsystemSimulation is the only code that both enters NoVoltage (source == 0) and leaves it (NoVoltage -> Starting -> Ready), so a Myomers that lost power during the death/reset window had no way back out. Order is load-bearing: the base call precedes the heat-model gate in the binary. Gating first -- as this did -- also denied the electrical machine to every non-expert pilot, since OwnerAdvancedDamage() reads the +0x260 heat-model flag and that is off below veteran. Measured with BT_SELF_DAMAGE + BT_TORSO_LOG, before and after: pristine, before 48/48 Ready after 38/38 Ready post-respawn 95/97 Ready 89/89 Ready Two dead windows per respawn, now none, and the clean case stays clean. The wrapper has a tail this does not yet reconstruct: @004b8bb9-0x4b8c1e runs an effect when electricalStateAlarm == 1 and the myomer's damage zone is below 1.0, building a small vector (const 0xbc343958, about -0.011f) against the owner's localOrigin@0x100. Almost certainly the un-powered movement penalty. Flagged in the KB as T4 rather than guessed at. Worth generalizing, and noted as such in context/subsystems.md: when a subsystem's Performance address in a PTR_LAB_* slot does not match the function we reconstructed, suspect a wrapper that chains the base sim. The whole family does it -- Generator and PoweredSubsystem both lead with HeatSink::HeatSinkSimulation, and the Torso's own perf @004b5cf0 leads with UpdateWatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c5537a1247 |
BT_SELF_DAMAGE: kill your own pilot in solo, so the RESPAWN family is bench-testable at last
Nothing could kill the LOCAL pilot on a bench. BT_MP_FORCE_DMG only targets REPLICANTS (mech4.cpp:4884 skips anything else), a solo dummy never shoots back, and every other damage path needs a second live pod. So the entire respawn family -- #70 torso twist after respawn, #22 ammo/weapons not resetting, #55 coolant/heat/generator restore, #57 the respawn latch -- could only ever be tested by asking a playtester to die. That is why they are all still open or awaiting-verification. BT_SELF_DAMAGE=<amount/sec> dispatches an unaimed TakeDamage at our OWN mech through the same virtual Entity::Dispatch a real beam hit uses, so the cylinder hit-location lookup, the zone cascade, the vital-subsystem kill and the whole authentic death -> 5s -> DropZone -> Mech::Reset cycle all run for real. No state is poked. =60 kills a fresh MadCat in about a minute. It LATCHES OFF at the first death. That matters more than it sounds: live damage knocks the power bus down, which is exactly the signal a respawn test wants to read. The first run of this harness produced a torso electrical state flapping 4 <-> 1 and I nearly filed it as #70 -- it was the harness still shooting me. One death, then silence, then measure. FIRST RESULT -- #70 does NOT reproduce in solo, but something near it does. After the respawn the MadCat's torso recovers fully: rate back to 0.872665 (the authored 50 deg/s), elec=4 Ready. Twist is not broken by a respawn. However the post-respawn mission shows INTERMITTENT power dropouts that a pristine mission never shows: pristine control (no damage, no death): 48/48 samples elec=4, ZERO dips post-respawn (harness silent): 95/97 elec=4, 2 dips to elec=1 A dip zeroes effectiveTwistRate (torso.cpp:570), so a pilot mid-turn feels the twist cut out -- plausibly what "torso twist stops working after a respawn" actually is: not a permanent stop but a stutter. Both dips are transient and the tail of the run is steady, so it is not a stuck state. Cause not yet found; the watched subsystem's own electrical level (wElec) dips with it, while the generator holds a full 10000V against a 5000V brownout threshold -- so it is NOT the brownout path in PowerWatcher::UpdateWatch. Filed here as the next thread to pull, with the harness that makes it reproducible in one solo run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
034e55c7f5 |
prove generator FAILOVER, not just re-attach: BT_POWER_DETACH_TEST takes a subsystem name
The #62 verification so far only showed a subsystem detaching and re-attaching to the SAME generator it started on. That exercises the roster walk but never the case players actually hit: your generator dies and Auto has to find a DIFFERENT one. (Raised by the operator, who also pointed out solo has no way to damage a generator -- but it does not need one: a generator has its own on/off button.) BT_POWER_DETACH_TEST now accepts a NAME (=PPC_1, =ERSLaser_1, ...) instead of firing on whichever powered subsystem happens to tick first, so the scenario can be aimed at a real weapon. "1" keeps the old first-one behaviour. Verified end-to-end, everything through real paths -- the detach via DetachFromVoltageSource @004b0e30, the generator kill via its actual RIO button 0x1A through the click seam (EmitButton -> RIO queue -> manager drain -> Generator::ToggleGeneratorOnOff @004b1ed0), no state pokes: BT_POWER_DETACH_TEST=PPC_1 BT_BTNTEST=0x1a,300,320 BT_POWER_LOG=1 [power] TEST: detaching PPC_1 (forcing Auto) [power] AutoConnect RE-ATTACHED PPC_1 -> generator GeneratorA [btntest] PRESS 0x1a at poll 300 <- GeneratorA switched OFF [power] AutoConnect RE-ATTACHED PPC_1 -> generator GeneratorB So the hunt skips the dead generator (HasVoltage requires GeneratorStateOf()==2 plus real measured voltage, powersub.cpp:860-876) and finds a live one. Same result first observed on Avionics; the named form proves it on a WEAPON, which is the player-visible case. Second proof in the same output: EXACTLY ONE re-attach line, then silence. The hunt re-runs every frame while `mode==Auto && !HasVoltage()`, so an attached-but-dark weapon (the #21 symptom) would spam that line forever. One line then quiet means GeneratorB is really supplying it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
901cf1b459 |
four ENG-page lamps were invisible, not missing: the ctors dropped their colour parameters
Playtest report: "generators don't seem to have an Auto setting" / "no auto mode is ever displayed", plus the operator's own memory that the BUS MODE button once stepped through three states. The state machine was NEVER wrong: @004b0abc has exactly two branches (<2 -> Auto(2); ==2 -> detach + Off(0)), Manual(1) is set only by the four SelectGenerator buttons, and the auto-hunt gates on ==2 -- all byte-verified, and the #62 re-attach fires live. Manual is a one-way door out of BUS MODE by design: a single cycle button cannot know WHICH generator manual should mean. Verified on-screen by the operator this session: A-D returns to manual, BUS MODE toggles auto/off thereafter. What was actually broken: the connect-mode lamp -- and three siblings -- never drew a pixel. OneOfSeveralStates (@004c5470) and OneOfSeveralInt (@004c5148) forward caller-supplied background/foreground colours in the binary; the recon dropped both parameters and hardcoded 0,0 into the base. A BitMap-strip lamp draws SetColor(bg) + DrawBitMapOpaque(fg,...), so 0/0 painted colour-0 on colour-0: invisible. Affected: btemode (connect mode, 1x3 -- THE report), btecmode (coolant on/off, 1x2), and both bteseek gear-step lamps (1x4). The cluster call sites pass 0xff/0, byte-verified (@004c866c disasm; part_014.c:1479-1481, :2146-2147). Same dropped-element family as issue #42's MoveToAbsolute. THE TRAP THAT WAS NOT LANDED, recorded so it stays unlanded: with the lamp first made visible, the frames appeared inverted against the levels (art reads AUTO/MANUAL/OFF top-down; levels run OFF/MANUAL/AUTO), and a row=(rows-1)-selected "fix" was proposed. An adversarial workflow proved it wrong: the gauge blit addresses SOURCE rows BOTTOM-UP (Video16BitBuffered::DrawBitMapOpaque, L4VB16.cpp:3846-3850 -- sTop = map_max_y - sTop, rows walked upward), the 1995 blit @0046bdfc performs the identical flip, and the vertical strips are AUTHORED bottom-up to match. Identity level->row therefore draws the pod-correct display; the inversion would have created the very bug it claimed to cure. Two of five investigators (and the first human pass) assumed top-down; the engine source overruled all three. Convention + warning now recorded in context/gauges-hud.md. btecmode doubles as the standing tripwire: a second vertical strip that must show ON when coolant is available -- if it ever reads inverted, the bottom-up verdict is falsified. Also corrected: @004c552c is OneOfSeveralStates' EXECUTE override (clamp >= 0, chain the base draw) -- the port had the body on BecameActive under a wrong label; vtable-diffed against OneOfSeveral (0x518b24 vs 0x518bf0). BecameActive is inherited. Behaviorally inert today (the state source never goes negative), byte-faithful now. README: the "KNOWN ISSUE -- automatic re-attach is not working yet" text is replaced with how power routing actually works (A-D = manual, BUS MODE = auto then off, two presses off / one back to auto). That text shipped in 600, which already contained the #62 fix -- players were being TOLD auto was broken while it worked, which is half of how a painted-out lamp became "no auto mode". Diagnostic kept: BT_GENSEL_TEST=<id> now drives any of the five power-routing message ids (4..8, default 7) and pulses four times, so the whole mode cycle is observable headlessly under BT_FIRE_LOG ([gensel] lines). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c32d02b3cc |
the heat schematic was lit at every spawn: a dropped x87 tail, and the last #47 derivation
Playtest night 5: "heat damage @ launch is lit up like a Christmas tree",
confirmed by two players at ANY spawn -- so not the respawn-reset bug it was
first filed as. Two stacked causes, both fixed here.
1. THE RATIO WAS NEVER RECONSTRUCTED. HeatConnection::Transfer @004c3720 sets
the colour index a ColorMapper pushes into the palette slot. The port wrote
*currentColorIndex = HeatRound(heat->currentTemperature);
straight into an index whose range is 0..99. A stone-cold mech sits at ~77
and the generators idle near 260, so every one of the 24 cmHeat mappers in
GAUGE/L4GAUGE.CFG (GeneratorA-D, Condenser1-6, HUD, Avionics, Gyroscope,
Torso, GAUSS, the lasers, SRM6, Myomers) saturated at the hot end from the
first frame and stayed there.
The real computation was INVISIBLE in the decomp. Ghidra renders the tail as
`uVar1 = FUN_004dcd94();` -- an arg-less __ftol, exactly the carve artifact
reconstruction-gotchas §19 documents: the x87 expression that left the value
in ST0 is dropped from the export. The previous author reconstructed the
only thing visible and flagged the scaling as unreconciled in a comment.
Raw disasm @0x4c379a-0x4c37c1 recovers it:
fld [num] ; fdiv [den] ; ratio
fcomp 1.0f ; jbe -> ratio=1 ; clamp
fmul 99.0f ; call __ftol ; -> 0..99
i.e. "how close to failure am I", not a raw temperature. At spawn that is
77/2000 -> 4, and the schematic reads cold.
The two operands come from one of two branches, chosen by a class flag at
+0x14 that the port did not model at all (ctor disasm @0x4c3682-0x4c36c2):
HeatableSubsystem (0x50e3ec) -> own temp@0x114 / own failureTemperature@0x11C
HeatWatcher (0x50e604) -> WATCHED subsystem's temp@0x114
/ the WATCHER's failureTemperature@0x124
neither -> source NULLed, Transfer writes 100
Note the watcher asymmetry: temperature from the watched subsystem, reference
from the watcher itself. Bridged as BTHeatWatcherSample so the gauge TU need
not include the heat family's headers.
2. THE WATCHER BRANCH COULD NEVER BE SELECTED. With (1) fixed, HUD, Gyroscope
and Torso still pinned at 99 reading temp=1.6369e-35 failAt=0 -- uninitialised
bytes. HeatWatcher's C++ base had been re-based to MechSubsystem, but its
DERIVATION chain still said HeatableSubsystem, so every watcher answered "yes,
I am heat-bearing", took branch one, and read its own watchedLink@0x114 as a
temperature. This is the last surviving instance of the #47 bug -- in the
file that fix's own comment cites as already correct. The two families are
disjoint in the binary and the branch test depends on it.
VERIFIED LIVE (solo, F6 to the Heat schematic, BT_HEATGAUGE_LOG=1): mode mask
reached 0x510421 (ModeSecondaryHeat) and the gauge tracks per subsystem rather
than saturating -- GeneratorA 259.5->13, Condenser3 202.0->10, SRM6 182.8->9,
lasers 90-97->4-5, and AmmoBinSRM6_1 (a watcher) resolves its link and reads
182.7->9 through the branch that previously read 1.6369e-35. Nothing pinned at
99. Operator confirms the panel is blue, not red.
BT_HEATGAUGE_LOG kept as a permanent env-gated diagnostic: bind-time
classification plus per-sample temp/failAt/ratio/index. A fresh spawn reading
99 is this regression returning.
Not covered: ColorMapperCritical (cmCrit / ModeSecondaryCritical) already
computes a proper damageLevel*100 ratio and was read, not measured -- if the
Critical page specifically still misbehaves that is a separate lead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1777d5a62e |
one log per day, and every log says who it sent it -- rotation was eating the crash stacks
Conn Man crashed twice in an Owens on 2026-07-27, kept playing, and by the time
we asked for his logs they no longer existed. Not a mystery: every launcher did
if exist content\X.old.log del content\X.old.log
if exist content\X.log ren content\X.log X.old.log
which keeps TWO generations. He relaunched more than twice, so his own bat
deleted both crash sessions. The player who crashes is exactly the player who
relaunches immediately, so the retention window was aimed at the wrong case.
THE LOG. BT_LOG set still means "use this path verbatim" -- that contract is
load-bearing (mp_a.log/mp_b.log for 2-node runs from one cwd, btoperator's
operator_N.log, every scratchpad/mp_*.sh that greps the file it named) and is
untouched. Only when BT_LOG is UNSET does the exe now name the file itself:
content\<stem>_YYYYMMDD.log, appended, one per day, no rotation window to fall
out of. The stem (solo/join/steam/joyconfig) comes from the gates the bat
already sets, so solo still cannot overwrite the MULTIPLAYER log.
Self-named files append UNCONDITIONALLY. The default open mode is ios::out --
TRUNCATE -- and the operator GUI's exported bats never set BT_LOG_APPEND, so
they were truncating already; leaving append implicit would have let the second
launch of the day erase the morning.
IDENTITY, because these arrive from many players at once and Discord renames
half of them to message.txt (most of the night-5 evidence had to be
re-identified by hand):
===== BT411 SESSION build=4.11.603 (d64d75f+) machine=... user=...
callsign=Conn Man mode=solo pid=13192 local=... log=... =====
Also the session separator inside a per-day file, and it puts build+hash ABOVE
any [crash] block so btl4+0xNNNN offsets stay matchable to a PDB across builds.
SIZE. Never delete, but stay sendable: past 8 MB the next launch rolls to
<stem>_YYYYMMDD.1.log. Checked only at open, so a live session is never split.
CONSOLIDATION. launch_report.txt is gone, folded into lastrun_<stem>.txt (one
launch record: the exe appends its block at first breath, the bat appends the
exit line). Per-stem because one shared lastrun.txt let a second launcher's
`del` destroy the first launch's block. Its ABSENCE after a run is now the
#41 "never reached WinMain" probe -- the old "if not exist X.log" test cannot
work against a per-day file, which survives earlier launches, and would have
reported every healthy run as blocked by antivirus. marshal.log folded into
the day log too; it keeps its own handle and gained a CRITICAL_SECTION because
it runs on a worker thread while the engine log stream is single-threaded.
play_steam.bat gained a launch bracket it never had -- which is why a Steam
player killed before WinMain previously left no evidence at all.
_putenv_s, not SetEnvironmentVariableA, to pin the resolved name: MSVC's CRT
keeps its own environment copy, so getenv() in-process does NOT see a
SetEnvironmentVariableA write (measured). btl4console/btl4lobby resolve the
day log via getenv, so with the Win32-only call they silently fell back to
marshal.log and the fold never happened.
logfile.open() is now checked -- a failed open used to rebind cout to a dead
buffer, silently dropping the header, [boot] and any crash stack while
lastrun still claimed log=<name>.
Verified: append across sessions with distinct pids, crash block landing under
its own header, BT_LOG verbatim unmangled, an inherited BT_LOG cleared by the
bats, roll-over at 8 MB, and both forensic verdicts (exe ran / exe blocked).
Console auto-retrieval is unaffected -- it uploads the matchlog, a different
file, and a crashed round never reaches the upload call anyway.
Known gaps, deliberately not in this commit: the setlocal hoist for gate
leakage when two bats share one console (dev-only), and btoperator's exported
bats still lack the lastrun bracket -- do not press Export on a shipped zip.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d64d75fcf7 |
handoff: fold in the orphan investigation, the launcher fix, and what it means for the release
The tracker split moves to 26 open (adds #71/#72 filed today). Flags the one decision left for the road: the :btwait fix is at HEAD but NOT in the 600 zip, so it ships only if the zip is re-cut -- and names the single manual check that would close out the fix's verification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7afbda900e |
the terminal that never closes: every launcher waited for ANY btl4.exe on the machine
Operator report: "X-closing the game leaves the terminal open and leaves orphaned processes." Investigated on the rig against 4.11.600. The terminal half is real and is THIS: :btwait polled `tasklist /FI "IMAGENAME eq btl4.exe"`, which is machine-wide, so a bat that launched nothing at all keeps spinning while an unrelated instance lives -- proved with btwait_probe.ps1. A second client, the operator's own pod, or an orphan from a crash therefore hangs every join window, which reads as "the game never exited" and invites people to start killing processes. All four launchers carried the identical block. Fix: snapshot the btl4 PIDs alive BEFORE the launch; wait only on PIDs absent from that snapshot. The `if /I "%%P"=="btl4.exe"` guard is deliberately kept -- tokens=2 alone parses tasklist's "INFO: No tasks are running" line as a PID and spins forever with nothing running, which would be worse than the bug. Verified with the text lifted verbatim from the shipped play_solo.bat: nothing running -> signs off (the regression guard); someone else's instance -> signs off; our own generation -> keeps waiting; decoy gone -> signs off. The patched bat still launches (pid + launch_report.txt). NOT verified: the full handoff E2E, because the bat blocks on the FE menu waiting for a human. The orphan half did NOT reproduce on 600: closing the MAIN window exits cleanly in ~1s during solo model-load, in the relay join wait, and after a real console launch, with the relay logging the seat freed. The orphans the playtesters saw match 584 and earlier, where every close relaunched. Full write-up, including the aux windows that hide instead of closing and the WM_QUIT that BTLoadPump swallows, in phases/phase-12-orphan-processes.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
afbbb7c59c |
road handoff: the state of every live investigation, checked against the machine and the tracker
Written for picking the work back up on a laptop. Facts verified rather than recalled: the parked relay is NOT running (no listener on 1500/1501/1507), RDP is up, Tailscale is not installed, and the tracker splits 24 genuinely-open / 26 awaiting-verification. Also commits the tracker snapshot script so the issue split can be re-derived from the road instead of trusting the numbers frozen in the doc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
39cb8647c7 |
launcher logs: rotate, never delete -- and solo/joyconfig must stop destroying the MULTIPLAYER log
Prompted by "does the steam path still generate our logs?". Answer: yes -- play_steam.bat sets BT_LOG=steam.log with append, and matchlogs arm because both Steam launch paths pass -net (the default trigger). But the audit found two evidence-destroying bugs on the way: 1. play_steam.bat DELETED the previous steam.log at every launch -- the same pattern fixed in join.bat/join_lan.bat last night. A Steam player who crashes and relaunches loses the stack. Now rotates to steam.old.log. 2. WORSE: play_solo.bat and joyconfig.bat deleted content\join.log -- the MULTIPLAYER log, not theirs. A player who crashed in MP and then ran solo to investigate (or ran the joystick wizard) DESTROYED THEIR OWN CRASH EVIDENCE. That is a very plausible part of how Conn Man's owens-laser stack vanished (#35) -- he had every reason to poke around after crashing. Each bat now writes its OWN log (solo.log / joyconfig.log), rotates it, and never touches join.log. Their launch-forensics + "send the operator" lines are repointed to match. VERIFIED in the zip layout (the bats need build\ + content\ beside them, so a repo-relative test is meaningless -- my first two attempts hit the badpath guard and proved nothing): solo.log = [boot] btl4 4.11.599 ... (the NEW run) solo.old.log = OLD-SOLO-CONTENT (rotated, not deleted) join.log = SEEDED-MP-EVIDENCE (untouched by the solo run) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e26f4e6285 |
Gitea #62 FIXED: AutoConnect can re-attach again -- slot 16 is HasVoltage(source), not GetStatusFlags()
A weapon detached by two eng-page BUS MODE presses stayed dark for the REST OF
THE MISSION: no voltage, blank recharge arc, dead ready dot, will not fire.
The arcade recovers by pressing bus-mode back to Auto and letting the per-frame
auto-hunt re-tap a generator. In the port that hunt's body was UNREACHABLE.
THE DEFECT, visible in three lines of powersub.cpp:
if (modeAlarm == AutoConnect && GetStatusFlags() == 0) // outer: NOT damaged
for (...)
if (... && GetStatusFlags() != 0 && Attach(sub)) // inner: DAMAGED?!
Both call sites modelled the no-argument GetStatusFlags(), so the outer demanded
"no status flags" and the inner "some status flag", with nothing in between able
to change the value -- mutually exclusive, body never entered. The binary's
@004b0bd0 calls vtable slot +0x40 (slot 16) with TWO SHAPES: slot16(this, 0) == 0
("am I unpowered?") and slot16(this, candidate) != 0 ("would THIS generator
supply me?").
WHAT WAS ACTUALLY WRONG was narrower than the issue assumed -- the BODY was
already a faithful transcription of @004b0b5c (complete-type accessors, no raw
offsets). Only its NAME and its WIRING were wrong:
* it was called `IsSourceShorted`, asserting the INVERSE of what it computes:
state 2 is the generator's ON-LINE state (Generator::GeneratorReady, what
@004b215c's stateAlarm->2 sets) and the fabsf test requires meaningfully
non-zero output voltage. True means "live and supplying".
* it was declared NON-VIRTUAL, so it could not be the slot-16 body, and a weak
stand-in (`electricalStateAlarm == Ready`, no source arg, no voltage test)
occupied `HasVoltage()` instead.
FIX: rename to `virtual Logical HasVoltage(Subsystem *source = 0)`, delete the
stand-in, restore the two AutoConnect call shapes, and rename Myomers' slot-16
override (`HasAdequateVoltage` -> `HasVoltage`) so it actually overrides -- it
tightens the test to "at least the SELECTED SEEK voltage", which is why it
exists.
BLAST RADIUS (the issue's warning): GetStatusFlags ORs the BadPower bit 0x40 on
!HasVoltage(), and that bit feeds #47's annunciator -- so power, firing and
annunciation semantics moved together. Checked: a healthy solo mission shows
zero spurious BadPower, subsystems simulate normally, no faults.
VERIFIED with a new diagnostic hook in the codebase's existing family
(BT_POWER_DETACH_TEST=1, off by default): it reproduces the player's exact state
-- drop the voltage link and force Auto, i.e. what the second BUS MODE press
does -- so the fix can be tested without the eng-page UI. Live result:
[power] TEST: detaching Avionics (forcing Auto) -- the auto-hunt must recover it
[power] AutoConnect RE-ATTACHED Avionics -> generator GeneratorA
Before the fix that second line was impossible. BT_POWER_LOG=1 prints every
re-attach; both hooks stay for regression use.
(The file-static one-shot is deliberate: PoweredSubsystem's layout is byte-locked
by PoweredSubsystemLayoutCheck, so a test flag as an instance member would break
sizeof and every offset assert downstream.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ef8e449a17 |
#55 steps 0/1/2/4: the 1995 DEATH pass restored, the arg gate made real, and 5 subsystems the respawn sweep never reached
THE HEADLINE: the port's death sweep was a total no-op, and fixing it required
fixing the arg gate in the same commit -- otherwise corpses refill their ammo.
STEP 2 -- the authentic dispatch shape:
* engine/MUNGA/SUBSYSTM.h: Subsystem::DeathShutdown gains the binary's
universal base body { DeathReset(c) } (@004ad10e). It was empty, and NO
port class overrode it, so everything the 1995 game does at death was
skipped entirely.
* mech4.cpp death sweep now passes 0, not 1 (the binary's @0049fe0c arg) --
the wreck shape: alarms/state settle, nothing refills.
* ARG PROPAGATION (mandatory once the sweep runs): AmmoBin::DeathReset is now
arg-gated exactly as @004bd26c -- refill only when arg != 0, ammoAlarm
unconditional. Ignoring the arg was harmless only while the sweep was
dead; with it restored, every corpse would have re-armed. Also forwarded
in PoweredSubsystem / HeatSink / Condenser / HeatableSubsystem / Generator
(each binary body forwards it -- verified addresses in the plan).
STEP 4 -- coverage for 5 classes that had an authentic slot-10 body but no
DeathReset, so the respawn sweep fell through to the empty base:
Torso (this is Gitea #70 -- "loosing torso twist function after a death"),
Gyroscope, Myomers, HUD, Seeker.
STEPS 0/1 -- observability + the David chain:
* Three unconditional matchlog rows: DEAD_NOTIFY (mech + resolved link),
PLAYER_LINK (player <-> vehicle), RESPAWN (mode/alive/zones/subsys/pos).
A respawn was previously INVISIBLE in the matchlog -- night 3's analysis
had to infer them from ammo arithmetic.
* Mech::PlayerLinkMessageHandler + the death dispatch site: when the engine's
one-shot registry lookup misses (no null check, no retry -> the whole
death/respawn cycle silently swallowed), recover the SAME object via the
reverse link the binary's own respawn branch walks (player+0x1FC ==
playerVehicle). Complete-type TU, no raw offsets.
* deathPending cleared in the first-spawn branch (a latch carried in would
permanently kill every later respawn -- the #57 class).
VERIFIED on the 2-pod rig (madcat vs thor, forced kills, 5 death/respawn cycles
per pod): build clean, ZERO new /FORCE unresolved externs from the 5 new
overrides; refill lines appear ONLY immediately before a Mech::Reset and NEVER
between a death and the next respawn (the corpse-refill regression this commit
had to pre-empt); all three probe rows present in both pods' matchlogs; every
DEAD_NOTIFY carried a non-null link.
DELIBERATELY DEFERRED (documented, not forgotten): the mechsub.cpp rename pair
(ResetToInitialState -> GenerateFault, ClearStatus -> the root reset @004ac22c),
deleting HeatableSubsystem::ResetToInitialState, and deleting RespawnRepair.
Those need the HeatableSubsystem vtable-slot-10 pre-flight the plan calls for,
and we have no binary image here to dump the vtable from -- guessing at it
risks the vptr-alias trap. Next session with the decomp shards open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
26e678e570 |
#67 part 2, ROOT-CAUSED AND FIXED: 1995 latent uninitialized fields, exposed by the port's allocator
THE HUNT (the deterministic rig repro made it a two-hour arc):
1. cdb write-watch armed from the Torso ctor (bp plants `ba w4 this+0x21c`
per torso -- ASLR-proof). The poison reproduced (atUpd=-1250 this run;
-3750 and int-15-as-float before)... and the watch stayed SILENT. Nobody
writes the garbage. The field is never INITIALIZED.
2. Confirmed in our ctor reconstruction: it inits every neighbour but skips
targetTwist @0x218 and twistAtUpdate @0x21C (the function that zeroes
them is a death-reset handler, not the ctor).
3. Confirmed in the BINARY: the real ctor @004b6b0c contains no store to
either offset -- a genuine 1995 latent bug (uninitialized read on the
copy path).
4. Why the pod never showed it: MemoryBlock arenas carve fresh OS-zeroed
pages, one pool per type, low churn -- first allocations read as zero.
The engine's own DEBUG_NEW_ON NaN-fill proves the developers knew the
hazard class. Why WE show it: mechrecon.hpp's Memory::Allocate shim
("a plain heap allocation is behaviour-equivalent" -- false) recycles
dirty heap. Replicant torsos spawned with garbage twist targets; the
limit clamp turned any garbage into FULL TWIST -- rendered "twisted full
right while not using TT" to every peer (playtest night 4, 3 reporters).
THE FIX -- environmental, class-wide, byte-faithful to the binary's code:
Memory::Allocate / AllocateArray / Alloc now ZERO-FILL, reproducing the pod's
EFFECTIVE allocation semantics for every never-stored field in every
reconstructed factory at once (Torso, Reservoir, all of them). No ctor gains
stores the binary lacks.
PROVEN on the rig: the replicant thor now spawns cur=0 target=0 atUpd=0
(was cur=-3.31613 = hard against the limit). Teardown clean.
Ships with part 1 (the dead-reckoning clock fix) in the next zip. Remaining
on #67 for the next games night: live confirmation that twist TRACKING looks
right in play, and whether the fire-from-centre facet (very plausibly the same
never-stored-field class, now zeroed) is cured with it.
Tools kept: scratchpad/torso_watch.cdb + rig_watch.ps1 (the ctor-armed
write-watch pattern -- reusable for any "who wrote this field" hunt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a3329a64da |
#67 torso replication, part 1: the dead-reckoning clock fix + the bug REPRODUCED on the rig
THE CLOCK FIX (landed, verified live). The replicant torso's extrapolation
window was hardwired to ZERO: both shim accessors returned the same field
(torso.hpp, the "dormant in single-player" note). The engine maintains both
clocks exactly as the binary expects -- WriteUpdateRecord stamps
lastUpdate=lastPerformance on the master, ReadUpdateRecord stamps
lastUpdate=Now() on the replicant (SIMULATE.cpp:276/296, the 1995 "HACK"
comment intact) -- so the fix is one mapping: GetCreationTime (misnamed; now
GetLastUpdateTime) reads lastUpdate. ComputeTargetTwist's
`current - lastUpdate` window is real again. Why legs always replicated
while torsos did not: Mover is ENGINE code reading these fields natively; the
Torso is our reconstruction with the collapsed shim. Rig-verified: the
copy-side log now shows distinct lastUpd/now values.
THE FIELD BUG REPRODUCED (2-pod rig, thor vs thor -- thor because the first
attempt used FOGDAY's Black Hawk, whose torso is authentically FIXED like the
Owens: hEn=0, limits +/-0.01 deg):
[torso-copy] cur=-3.31613 target=-3.31613 atUpd=-3750
The replicant renders at EXACTLY its twist limit (thor: +/-3.31613 rad)
because twistAtUpdate holds garbage (-3750) and the limit clamp slams it to
the stop -- "twisted full right but was not using TT", on demand. An earlier
run poisoned it with 2.1e-44 (= int 15 as float, suspiciously the torso's
subsystem index).
NARROWED: the tx/rx probes added here (BT_TORSO_LOG prints every torso record
both directions, header + raw payload dwords) logged ZERO records in the
poisoned runs -- Torso::Read/WriteUpdateRecord never executed. The garbage
therefore arrives OUTSIDE the torso record path: a raw state write during
mech spawn/consolidation spraying the torso's fields (stream-walker framing
or a consolidation blit). That writer is part 2's hunt; the probes and the
deterministic repro make it a short one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9bb95d8580 |
steam-networking KB: the first real multi-player field verification (night 4)
Three back-to-back Steam lobbies, 3+ players, host-and-join, no operator console: launch worked, DAMAGE APPLIED (previously suspected dead over Steam), 2 kills, clean round end, next mission launched. Records what worked, the one genuinely Steam-side defect found (#68 silent exit on a failed join), and the triage lesson -- a bug found in a Steam lobby is only a Steam bug if it lives in the lobby/token/transport layer; the three replication symptoms that night produced (#67/#70/#55) are above the seam and would reproduce on the relay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
184121c597 |
kill-storm repro rig for #35 (owens laser crash): parameterized damage hook + honest NEGATIVE
BT_MP_FORCE_DMG=<n> now sets the per-tick probe damage (plain =1 keeps the original amount) -- kill-storm rigs need lethal ticks. rig_killstorm.ps1: offset-port relay + owens shooter under BT_AUTOFIRE vs a respawning victim. Result: NEGATIVE, twice. The respawn cycle (death anim + warp + handshake) caps the harvest at 1-2 kill-teardown windows per 4-minute round, and none crashed. With July's 254-volley negative the conclusion firms up: the window needs the reporter's slow-machine timing, not more attempts here. The field net (crash self-report + join.old.log rotation + the sweep guards) means the next real occurrence names its own site. Refined theory recorded in the ledger: six-beam volley vs a target dying mid-volley, 515-class teardown race. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
35bdb505df |
join bats keep the previous session's log as join.old.log
Conn Man's owens-laser crash stack (#35) was in his join.log -- and re-running join.bat deleted it before it could be sent. The bat now rotates instead: join.log -> join.old.log at launch. One generation of history, the exact insurance that would have closed #35 tonight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ecb9b338bd |
the X button means QUIT: a user-closed window exits for real instead of relaunching
Operator report (00:30): "my local instance auto-relaunches whenever I stop and
start a session, even if I close my window." Root cause is the pod's AUTHENTIC
immortality: an arcade cabinet's join loop never exits, so every RunMissions
return relaunches into the join wait -- and a human closing the window was
indistinguishable from a round ending. On a desktop that made the client
unkillable outside Task Manager, and explains every orphaned-plasma zombie
hunted this week (my own test teardowns included), plus tonight's "eventually
all I could find was my plasma display".
FIX, one flag + one gate:
* btl4main WndProc WM_CLOSE stamps gBTUserRequestedExit before DestroyWindow
(covers X, Alt-F4, taskbar close, Task Manager's polite phase).
* BTFE_RelaunchSelfAndExit -- the single choke point every relaunch path
funnels through (round end, console-pad lost, round abort, FE loop) --
exits for real when the flag is set, logging 'window closed by the user
-- exiting for real (no relaunch)' to the marshal log.
Round-end auto-rejoin is UNTOUCHED: the flag only sets on WM_CLOSE.
VERIFIED both directions on the built exe (4.11.591):
* WM_CLOSE posted to a live solo instance -> the whole process tree is GONE
8s later: no relaunched generation, no plasma orphan. [was: immortal]
* Offset-port rig, full launch -> StopMission -> both pods AUTO-REJOINED and
re-ACKed for the next round (2/2 ready, WAITING FOR OPERATOR); zero
exiting-for-real lines fired (nobody closed a window).
Requires the next zip for players; the operator's running instance keeps the
old behavior until restarted onto this build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
33487ab846 |
night ledger: post-night fix session status (guards verified, dup-pilot repro NEGATIVE, five fixes landed)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ef0ec48f9a |
map dropdown = the AUTHENTIC console catalog; the relay names a phantom-map stall instead of holding silently
THE 40-MINUTE OUTAGE'S REAL FIX. The GUI's map dropdown offered every RES
name passing the type-14+26 existence check -- which includes INTERNAL
FRAGMENTS: artrucks is an include-node of arena1 (PROGRESS_LOG map anatomy:
arena1 -> {arenall -> cavern, artrucks}), not a mission. Picking it stalled
every pod's mission load forever with no error anywhere, and End/Re-arm kept
restoring the poisoned egg -- 22:02..22:49, three "different" failures, one
cause.
* eggmodel.CONSOLE_MAPS: the Mac 4.10 operator console's own adventure-tree
catalog (Console.ini; the same 8 the solo menu ships in btl4fe.cpp kMaps):
cavern grass rav polar3 polar4 arena1 arena2 dbase. ValueSets.maps now
offers exactly that, catalog order, filtered to what the RES carries
(permissive fallback only if the intersection is empty -- a foreign RES
must not present zero maps). artrucks is the one fragment the old filter
let through; now hidden.
* The relay WARNS at egg load/reload when the mission names a non-catalog
map (the camo-color-warning precedent: hand-edited eggs still work, the
operator just cannot miss it).
* The launch hold gains the PHANTOM-MISSION SIGNATURE diagnostic: 0/N ready
for 60s means every pod is stalled in shared mission content -- one line
naming the egg's map and the recovery (End Mission -> fix map -> Re-arm),
instead of the silent 10s HELD drumbeat the operator stared at for 40
minutes. Hint re-arms per launch.
VERIFIED: new suite scratchpad/test_map_guard.py (5 checks: catalog exact +
ordered, artrucks hidden, warning fires on a doctored egg, silent on a real
one, the 60s hint names the map); all five console suites pass. Python-only.
(While placing the hint, a blind text-insert broke the launch-received block's
indentation -- caught by py_compile before anything ran, repaired, and the
whole area re-verified by the rearm suite.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
becaddb662 |
console logs ROTATE instead of truncate -- tonight's evidence loss cannot repeat
Two destroyers, both in btoperator.py:
* Start Session zeroed operator_relay.log -- took the whole 23:08 session
(the reservoir-crash round's relay trace) minutes after it happened;
* Launch local instances os.remove()'d operator_N.log -- took the pod's
[crash] self-report block before it could be copied.
Both now rotate the previous file to <name>.1 (btrelay_park's pattern: drop
the old .1, os.replace, never block on a locked file). One generation of
history is exactly what post-mortems needed twice tonight. All four console
suites pass; picks up on the console's next restart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b7e4837738 |
seat identity swap (SAURON played as 'Draco'): the LEFT handler ate the pref of a re-seating player
THE CHAIN (reconstructed from the night's logs, then REPRODUCED offline in
scratchpad/test_seat_identity.py before fixing):
1. At round end every pod relaunches and seat-requests again. SAURON's
request was walk-up-assigned the departed Draco's old seat, and his pref
(callsign/mech) was correctly written -- the assign line even printed
callsign='SAURON'.
2. His request conn then closed BY DESIGN (the pod re-dials to HELLO) --
and the beacon-death branch of _drop_game, added 2026-07-26 for the
departed-player roster fix, treated any beacon death on an unclaimed
seat as a LEAVE: it printed a false 'PLAYER n LEFT' and POPPED the pref
written milliseconds earlier.
3. The next egg release _reload_egg_file()'d the DISK egg -- where the GUI's
Start Session had saved the ADOPTED roster names, including 'Draco' on
that row -- and with no pref left to override it, Draco's callsign
shipped on SAURON's seat. His plasma (and that round's score
attribution) wore the wrong name. Per-seat HELLO-vs-close ordering
roulette explains why only one seat swapped.
THE FIX: a LEFT-grace window (SEAT_LEFT_GRACE_SECONDS=15). A beacon that
dies younger than the grace is the pod's designed post-assign re-dial: keep
the pref and the reservation, print nothing. A real join-menu leaver has
held the seat far longer, so the departed-player roster fix keeps working
(pop + LEFT exactly as before); claimed seats were already exempt.
REGRESSION SUITE (new, offline, drives the real Relay class -- no ports):
A. designed instant close: pref survives, seat stays protected [was FAIL]
B. real leave (aged beacon): pref popped + seat freed [unchanged]
C. claimed seat: pref survives an unrelated conn death [unchanged]
D. a different identity assigned onto a held seat overwrites
the held pref (the operator's original suspicion, locked in) [unchanged]
All four console suites pass (rearm 25, net 17, roster 22, identity 7).
Python-only: no client update needed; the running console picks it up on its
next Start Session. Awaiting live verification next games night.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e28dcbc161 |
the #35-class sweep + the games-night load crash: four lifecycle-window null-derefs guarded
THE CLASS (established by Gitea #35's 515 fix, confirmed growing tonight): code the binary could run bare because a pod's mission world always existed, crashing in the port's MP join/teardown/respawn windows where mission, player, or a roster link is briefly NULL. Swept every GetMissionPlayer()/ GetPlayerVehicle()/GetCurrentMission() chain in the tree (23+7 sites). GUARDED (the 515 pattern -- authentic behavior whenever the object exists, skip/degrade + loud log when mid-teardown): 1. DPLRenderer::SortAndReloadNameBitmaps -- runs in the round-STOP path (the end-of-round circle), raw mission+player+entity-manager derefs. 2. DPLRenderer::LoadOrdinalBitmaps -- called from (1); raw GetMissionPlayer()->GetInstance() evaluated on EVERY machine (the camera- director test derefs before it branches). 3. DPLRenderer::LoadNameBitmaps -- same window, raw GetPlayerCount() chain. 4. Reservoir::Reservoir master-gate block -- THE CAPTURED GAMES-NIGHT CRASH ([crash] btl4+0x4990d = this ctor inlined into CreateReservoirSubsystem, AV reading NULL+0x1d0): linkedSinks.Resolve() derefed raw; +0x1d0 is exactly the masterScale FILD of the resolved master heat-sink bank (heatSinkCount @0x1D0 -- the offset the crash named). Observed trigger: a DUPLICATE-PILOT egg (the seat-ghost bug put one identity in two seats) poisoning the ID registry, so Resolve() nulled during viewpoint-entity construction and at least two pods died loading the same egg. Now: loud '[spawn] FATAL-AVOIDED' + degrade to the inactive-copy shape. The duplicate-pilot ROOT CAUSE is the seat-ghost fix (tracked separately); this guard makes the failure survivable and self-identifying either way. CLASSIFIED SAFE, not touched: btl4mppr/btplayer/mechmppr/btl4gau3 chains (guarded or null-tolerant by design); APP.cpp's five Check(player) sites (mission state machine -- player exists by construction, original engine ordering); CAMMGR/CULTURAL GetCurrentMission chains (construction-time). VERIFIED: build clean (0 errors, no new unresolved externs); solo smoke on the guarded exe -- zero FATAL-AVOIDED lines (guards silent on the healthy path), subsystems simulating, no faults. STILL OWED: a full launch->stop rig cycle (exercises the guarded round-stop path live) and the duplicate-pilot-egg repro -- both blocked on port 1500 while the operator's console session is up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c7dcdf26eb |
games night 2026-07-26: full incident ledger (artrucks phantom map, reservoir load crash, seat ghost, owens laser suspect)
The night's evidence file, preserved against the log truncation that ate the primary sources twice. Highlights: map=artrucks (an art sub-node the dropdown offers as a mission) stalled every load for 40 minutes; the post-freeze reload crash resolved to CreateReservoirSubsystem+0x13d (null->0x1d0) with a duplicate-pilot egg as prime suspect; a held seat walk-up-assigned to a different player kept the old callsign (SAURON played as 'Draco'); and the 23:14 first domino was Conn Man in OWENS firing lasers -- matching an older verbal report never filed. Fix list + evidence-collection asks recorded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d3e724c254 |
cut 4.11.584 for tonight, and PROVE it still plays with the 554 everyone has
The merge brought Cyd's cockpit refit into the build testers will run tonight,
so the question that decides the evening is whether a 554 client can still join.
Rather than reason about it, tested it: extracted the real BT411_4.11.554.zip
and ran ONE 554 pod and ONE 584 pod against the same relay through a real
launch.
WIRE-COMPATIBLE. Both staged, both REGISTERED, the relay launched the mixed
round, and 30s in both were still registered AND still on the UDP fast path
(registered [2, 3] udp-known [2, 3]), zero drops, both processes alive.
=> NOBODY HAS TO UPDATE TONIGHT. The new zip is an upgrade, not a
flag day (unlike 554, which changed the scoreboard wire format).
Supporting evidence for why that held: the merge did NOT change the attribute
table's shape -- 36 ATTRIBUTE_ENTRY(Mech,...) rows before and after. Cyd's
crouch work POPULATED an existing pad slot (0x37 DuckState, previously
attrPad), so no index shifted.
The zip itself (dist/BT411_4.11.584.zip, 47.0 MB):
* built clean at HEAD
|
||
|
|
2b0506ddfd |
OPERATOR_GUIDE: fix the blockquote swallowing the secret/port paragraph
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6b0402badc |
park_relay.cmd: resolve a real interpreter -- a second admin account has NO working python
Setting up remote access exposed this: 'python' works for the operator's own account only. It resolves through pyenv SHIMS driven by PYENV/PYENV_HOME/ PYENV_ROOT, and all three are USER-scoped to that account; the single machine-wide PATH entry points at a Python39-32 folder that no longer exists. So a dedicated remote-admin login (the account just created for SSH/RDP) gets NO python at all -- parking the relay from the road would have failed with 'python is not recognized', at night, with players waiting. park_relay.cmd now resolves an interpreter explicitly and prints which one: BT_PYTHON override -> the project's pyenv 3.11.4 (calling account's profile, then the operator's -- Administrators can read it) -> the machine-wide py launcher -> whatever is on PATH -> a clear error naming the fix. Verified both as the operator and with a simulated second-account environment (machine PATH only, PYENV cleared): both resolve 3.11.4. Also dropped a delayed-expansion bug in the first cut (!CAND! without EnableDelayedExpansion would have silently evaluated to nothing). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
05fdd319d6 |
parked relay: a supervisor that keeps it alive + a REMOTE restart the operator can actually reach
Completes the travelling-operator deployment. The relay stays parked on the
home machine (so no player ever edits join.bat) and the console dials in from
anywhere -- but until now two things needed hands on that machine: bringing the
relay back when it died, and restarting it (the only way to clear a wedge or
pick up a roster resize). Both are covered now.
tools/btrelay_park.py -- the supervisor:
* runs the relay with cwd=content\ , which is what pins WHICH
operator_secret.txt is live (the trap documented last commit);
* relaunches on ANY exit, with 2/5/15/30/60s backoff when a relay dies inside
20s, so a permanent fault (port taken, missing egg) cannot become a spin;
* rotates content\parked_relay.log to .1 first, so the dead generation's
evidence survives the relaunch that replaces it;
* Ctrl-C stops it for good. NOT a Windows service on purpose: session 0
would hide the window an operator wants to tail.
tools/park_relay.cmd -- double-click to park; a shortcut to it in shell:startup
gives start-after-reboot with no admin rights.
`restart` on the control port (btconsole.py): sets restart_requested, the run
loop returns, relay_main exits RESTART_EXIT_CODE=42 and the supervisor relaunches
-- re-reading the egg. REFUSED while a mission is running (launches_sent >= 2
and not stop_sent): bouncing then would drop every pod out of a live round.
Local stdin gets the same command for parity.
btoperator.py: Restart Session in REMOTE mode now sends `restart` and reconnects
6s later instead of just tearing down our own link -- the old behaviour looked
like "Restart does nothing" against a parked relay. (QTimer had to be imported;
it was missing, which py_compile does not catch -- it would have been a runtime
NameError on the first click.)
VERIFIED by scratchpad/test_parked_supervisor.py, 12/12, and the full
test_remote_console_e2e.py re-run green afterwards (15/15):
* kill the relay -> a NEW pid is serving again, and the old log is kept as .1;
* remote `restart` -> ACKed, new pid, egg re-read, serving again;
* the supervisor distinguishes the two -- "operator requested a restart" vs
the crash/backoff path -- proven from its own log, not assumed;
* the mid-mission guard is present and wired to launches_sent/stop_sent.
Two harness defects fixed while proving it, both mine: a check written with
`or True` that could never fail (replaced with two real assertions against the
supervisor's log), and a recv that treated the relay's correct
close-after-restart as a ConnectionResetError failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
05f1ffb194 |
verify the parked-relay/remote-console path END TO END (it works; 15/15)
The operator asked whether the console is actually set for the parked-relay
deployment or whether I was just vouching for earlier work. Honest answer was
that prior verification was unit-level (relay log lines fed to an offscreen
widget) plus LOCAL-relay rig cycles -- the remote path had never been driven for
real. Now it has:
parked relay (standalone btconsole.py, cwd=content, all interfaces)
+ 2 real pods dialling in
+ the REAL operator GUI in REMOTE mode over a real TCP control socket
15/15, twice, with clean teardown. Proven: AUTH; the relay REGISTERS both pods;
the GUI adopts the roster and lights the seats; LAUNCH / END MISSION / Re-arm all
ENABLE over the remote link (the exact things that were broken before today);
LAUNCH really reaches the pods (RunMission pair) and END MISSION really stops
them; mission settings cross the wire and rewrite the relay's OWN egg
(map=cavern time=night weather=soup verified in the relay log); and the parked
relay SURVIVES the operator disconnecting -- the property the whole deployment
depends on.
A LAN address is used deliberately, not loopback: btoperator treats
localhost/127.0.0.1 in the relay-host field as LOCAL mode, so a loopback test
would have silently exercised the wrong code path and proved nothing.
Three defects found in my own harness on the way, all fixed here (none in the
product):
1. Pods announced BT_SELF on the LAN address while FOGDAY.EGG's roster lists
127.0.0.1:1502/1602 -- seat identity mismatch, so they took the egg and
closed. The relay was RIGHT to refuse: "LAUNCH pressed but NO players are
seated yet". Pods now announce the address the egg's roster lists.
2. The roster assertion counted EGG-CONFIGURED callsigns (always present) and
would have passed with zero live players. It now counts the relay's own
REGISTERED lines.
3. The relay's `set` really rewrites its egg -- pointed at a TRACKED file it
dirtied content\FOGDAY.EGG (caught by git status, restored with git
checkout). The test now runs on a throwaway copy and deletes it.
Also: cleanup now walks the PROCESS TREE, because btl4's front-end relaunches
itself for the mission, so the surviving pod is a CHILD of the spawned PID --
killing only our own handles left a live pod holding its log open. Still
PID-only, never by image name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9df7dff787 |
KB: the parked-relay/remote-console mode is COMPLETE -- record it so it stops being re-derived
Operator caught me re-investigating a feature we already shipped today. Root
cause was a self-contradicting knowledge base, not just my compaction: the
§Modes entry still said "Remote relay -- KNOWN BROKEN: the LAUNCH button can
never enable in this mode", while §Mode-specific traps 60 lines later documented
the same thing as FIXED. Reading the summary line first is exactly what a
future session does. Swept: that was the only stale copy.
Now recorded once, authoritatively (context/operator-console.md §Parked relay +
remote console, and a sysop recipe in docs/OPERATOR_GUIDE.md):
* THE PRODUCTION TOPOLOGY the operator actually wants: the relay parked
permanently on the home machine so no player ever edits join.bat, with the
console dialling in from anywhere (cell internet is fine -- outbound only).
* The exact standalone park command, and why the control port is what makes a
GUI-less relay usable at all (backgrounded, it has no stdin).
* The COMPLETE remote command set as a table, read out of _ctl_command /
_ctl_get_mission / _ctl_set_mission: launch, stop, rearm|newround, get,
set (the six CONTROL_SET_KEYS, written to the relay's OWN egg, effective
next round), ping, plus the log stream and the roster re-issued at AUTH.
Written down so the next session reads instead of re-deriving.
* The hard limits: remote mode ATTACHES -- it cannot start or restart the
relay, cannot resize the roster, cannot edit callsign/mech/colour.
* Multiple operators ARE accepted concurrently (ctl_conns is a list, each
AUTHing independently), so hand-off is seamless -- but nothing arbitrates
two people pressing LAUNCH.
Two live hazards found while verifying, both new:
1. THE SECRET'S CWD TRAP. control_secret() opens a bare relative
"operator_secret.txt" and SILENTLY GENERATES A NEW ONE if absent, so the
live secret depends on the relay's working directory. This repo really has
TWO secret files with DIFFERENT values (repo root and content\) -- a remote
operator holding the wrong one fails AUTH as a CONTROL_AUTH_TIMEOUT drop,
with no "wrong password" anywhere. Documented in both files.
2. operator_secret.txt was untracked but NOT ignored -- one `git add -A` from
publishing a live credential. Now in .gitignore.
Also corrected a nearby overreach: "the console must STAY CONNECTED" is about
the console channel to the PODS; an operator GUI dropping off a parked relay's
control port is harmless and the pods never notice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
12d6862c1f |
keylight: unship it -- opt-in (BT_KEYLIGHT=1), gone from all player docs
Operator decision: the player base is not on Windows 11, so the RGB keyboard
mirror should not be shipped or promised. The feature is NOT deleted -- Cyd's
implementation stays intact behind the gate for whoever wants it -- it is
simply off by default and invisible:
* PadRIO gate flips from opt-out (BT_KEYLIGHT=0 disables) to OPT-IN
(BT_KEYLIGHT=1 enables). Default boots produce zero keylight lines, and a
stub build and a real build now behave identically for players: nothing.
* Removed from every user-facing artifact: the README paragraph, the
CONTROLS.md section, the CONTROLS.html footer line.
* environ.ini template: the entry stays (it is the discoverable home for the
opt-in) but now says OFF-unless-1, Windows 11 22H2+, and why it is
unshipped.
* mkdist's cut-time stub warning removed -- it existed to protect a README
promise that no longer exists.
* context/glass-cockpit.md records the decision, the opt-in, and the
build-time SDK stub gate in one place.
VERIFIED: default boot logs zero keylight lines; BT_KEYLIGHT=1 engages the
feature (the stub on this machine: its one honest log line + the key map).
All three console suites pass; checkctx CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c4202d92ca |
zip-doc audit: last stale key claim + a keylight-stub warning at cut time
Full audit of every user-facing artifact in a freshly cut zip (33 checks, all
passing -- README substitutions/controls/migration story, CONTROLS.txt ASCII
flatten, CONTROLS.html loader + wrap, launchers byte-identical, exe carries the
migration):
* README's intro still said 'Press V any time to toggle the external camera'
-- the one key claim outside the rewritten controls block, stale for every
fresh/migrated install since the board took V. Backtick now.
* mkdist warns at cut time when the exe carries the KEYLIGHT STUB (build
machine's SDK < 10.0.22000), because the README promises the RGB feature
-- nobody should ship a stub build believing that promise ships with it.
The audit zip itself was deleted after verification: its version stamp predates
today's doc commits, and a release cut should rebuild the exe at final HEAD
first (the 554 procedure).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e46fc706a6 |
CONTROLS.html: load your bindings.txt and the board becomes YOUR board
The interactive controls page was a static picture of the stock layout -- a
player's carried migration rows, rebinds and HOTAS setup were invisible to it,
and the mismatch grew with every customization. Now a "Load your bindings.txt"
box (file picker + drag-drop) parses the real bindings grammar client-side and
rewrites the interactive keyboard in place:
* every key shows the loaded file's truth; keys that differ from the stock
board get a hazard outline (the legend says so); hover readouts follow
because they were already attribute-driven;
* the game's bindings-row-wins rule is modelled: the PgUp/PgDn volume and
backtick view BUILT-INS keep their face only while their key is unbound;
* rows that have no keyboard geometry (pad / padaxis / wizard joyaxis-
joybutton-joyhat / MOUSE keys) land in an "also in your file" list;
* one click restores the stock view. Nothing is uploaded -- FileReader on a
user-chosen local file, works from the extracted zip on file://.
Key-name registry walks the three key blocks in DOM order (Shift/Ctrl/Alt
resolve left-then-right; the numpad's U+2212 minus and its ASCII twin both map
to NUMPADMINUS), address meanings are harvested from the stock board itself
plus a small table for the slots the default board leaves off (unwired columns,
the cabinet-only throttle bank, PANIC).
Two self-inflicted bugs found by the harness and fixed before landing: the
address harvest invented a "BTN" label for the many stock keys that have none,
which made EVERY default row read as a customization; and generated axis labels
("AIM +") differed from the page's hand-authored vocabulary ("AIM UP"), custom-
marking the whole stock numpad. Both now speak the board's own labels.
VERIFIED headlessly (no browser extension needed): scratchpad/
test_controls_page.py injects a self-test harness into a copy of the page,
feeds it scratchpad/fixture_migrated_bindings.txt -- a REAL board-2 migrated
file (full defaults + two authored rows + a wizard section) -- and runs it
under chrome --headless=new --dump-dom. 11/11: the two authored rows carried
and marked, zero stray custom marks across the rest of the board, both
built-ins survive, wizard rows listed, status line honest, and reset restores
the stock board exactly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6eaed84fad |
CONTROLS.html: catch it up with the volume move + the bindings migration
The interactive controls page had drifted from the markdown fix-ups: PgUp/PgDn rendered as unbound keys with no meaning (the volume keys live there now), and the Rebinding section still promised 'an existing file is never overwritten' -- made false by the board-2 migration. Both corrected; the page and CONTROLS.txt now tell the same story. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ab91b5e7c1 |
clickbank: never target a degenerate window (the 0x0 Plasma window ate all 144 clicks)
The picker matched any visible window whose title contains the substring and took windows[0] -- which for a live game is 'BattleTech - Plasma', a 0x0-client window. Every posted click landed in it: 144 clicks, zero dispatches, and what looked exactly like the click/render alignment regression being checked for. Windows with a client smaller than 50x50 are skipped now; verified by re-running the full 72-button pass with the broad 'BattleTech' title straight through to 72/72 dispatches. (Found during the post-merge alignment regression check: boot geometry, a mid-session resize to an odd 1120x640 letterbox, and a minimize/restore cycle -- 72/72 dispatched in every round, 288 clicks total, zero Gitea #56 tripwire lines. The merge did NOT regress click-vs-render alignment.) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8edce41f88 |
bindings: one-time migration to the board-2 layout -- the two-population fork is gone
Operator decision: "make this version devour their custom settings and re-output
the correct binding file with the updates -- I'd like to eliminate competing
code paths." Before this, the preserved-bindings.txt convention meant upgraders
stayed frozen on whatever layout their file was written under, forever: two
player populations, two sets of true documentation, and every future default
improvement reaching only fresh installs.
THE MIGRATION (PadBindingProfile::MigrateBindingsFile, run once at load):
* A file carrying the new "# bindings-board 2" marker is left alone -- the
check is the first thing that runs, so this is a no-op forever after.
* Otherwise: every row matching the UNION OF EVERY DEFAULT SET EVER SHIPPED
(85 canonical rows, harvested from c2ee729..1cda880 and embedded as a
table; comments stripped, whitespace collapsed, uppercased) is dropped --
those rows were never a player's choice, they were just the old board.
* Rows the player actually authored are CARRIED with their original text and
comments, and WIN over the new defaults: the losing default row is emitted
commented out as "# (yours wins) ...", keyed by the control ("KEY T",
"PAD A", "PADAXIS LX") so key rows only displace key rows.
* The joystick wizard's marker-delimited section is preserved VERBATIM (the
same markers L4JOY rewrites between), so HOTAS players migrate without
re-running the wizard.
* The previous file is saved as bindings.old.txt first. Restoring it over
bindings.txt just re-migrates next boot -- deliberate: the old WORLD is not
restorable, only the player's own rows persist. That is the point.
VERIFIED end-to-end with the real exe (three planted scenarios):
A. old-world file (8 old-default rows + 2 authored rows incl. a rate tweak +
a wizard section): both authored rows carried with comments, both
conflicting new defaults stood down with "(yours wins)", wizard section
byte-preserved, backup written, log line states all counts, and the
migrated file parses with ZERO malformed lines.
B. already-migrated file: byte-identical md5 after a full boot, no migration
line, no backup touched. (First attempt at this check clobbered its own
evidence by running case C first -- re-run properly.)
C. no file: fresh defaults now carry the marker as line 1, no migration.
The operator's real bindings.txt was stashed before testing and restored
untouched; it will migrate on their own next launch, as intended.
Docs flipped to the new reality: the README's "your keys DO NOT change"
upgrade promise is replaced by the migration story (customs + stick kept, old
stock keys replaced, bindings.old.txt escape hatch); CONTROLS.md gains the same
note; context/glass-cockpit.md records the mechanism, the 85-row table's
provenance, and why restoring the backup re-migrates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
150961176c |
merge fix-ups: the four review findings + a build portability gate the merge exposed
Applied on top of the glass-cockpit-refit merge, all from the pre-merge review: 1. README CONTROLS TABLE REWRITTEN for the new default board. It still taught "W/S throttle, A/D turn" while the defaults moved flight to the numpad and handed the letter rows to the MFD banks -- a fresh install would read controls that do not work. The new table teaches the numpad flight block, the panel-on-your-keys groups, F7 for control mode (was M), backtick for the camera (was V), the coolant valves on 1-3/QWE with the detent warning kept, and PgUp/PgDn volume. UPGRADERS ARE TOLD EXPLICITLY that their preserved bindings.txt keeps their old keys, and that deleting it opts into the board. 2. BACKTICK VIEW-TOGGLE GUARDED. The bindings-row-wins rule (KeyHasBinding) covered V and J/K/L but not backtick itself, which polled unconditionally -- and BACKTICK is a nameable, unbound key, so binding it to a button double-dispatched (button + camera). Both halves of the view poll are now guarded. 3. ENVIRON.INI REFUSES ONE-SHOTS. The loader applied ANY key=value line, and BT_JOYCONFIG in the file would defeat the #66 one-shot in a sneaky way: the wizard DELETES the process var after running, so in every relaunched child there is no real env var left to win over the file -- capture wizard at every mission start. BT_JOYCONFIG is now skipped with the reason in the template header, which also warns that test/debug hooks (BT_MP_FORCE_DMG and friends) do not belong in a file that silently persists forever. 4. VOLUME DOCUMENTED EVERYWHERE IT WAS MISSING. CONTROLS.md gains the PgUp/PgDn row (the old "- / = (see below)" cell pointed at a note that never mentioned volume) and the note that the keys moved so they no longer share a key with anything in any layout; the bindings template header reserves PgUp/PgDn informally. 5. L4KEYLIGHT BUILD GATE (found by building the merge on this machine). The C++/WinRT Dynamic Lighting TU does not compile against SDK 10.0.19041 -- its bundled cppwinrt fails inside winrt/base itself (C2039 'wait_for'). CMake now detects an SDK older than 10.0.22000 and builds a dormant stub with the same five-function scalar interface (logs once at Start, identical behaviour otherwise) -- extending the feature's own runtime philosophy ("no Dynamic Lighting -> log once, stay dormant") to build time. Dynamic Lighting is a Windows 11 22H2 feature, so nothing real is lost on an older build machine, and machines with a newer SDK build the real mirror unchanged. VERIFIED on the merged tree (4.11.572): * build clean -- 0 compile errors; the 40 /FORCE-tolerated unresolved externals are byte-identical to every pre-merge build (20 CreateStreamedSubsystem + 20 DefaultData; the earlier claim that all 40 were CSS was shorthand -- corrected here for the record). * solo boot: environ.ini template written + loaded, mode resolver announces the surround, historical bands L276/R276/T223/B336 confirmed, keylight stub logs its dormant line, BT_SHOT capture shows the under-glass button treatment and the corrected hat labels, 0 faults. * PgUp/PgDn volume PROVEN LIVE via injected keys: 5%->10%->15%->10%, saved to volume.cfg. (Side catch: content\volume.cfg had been sitting at 0.00 from an old test -- anyone using this tree had a silent game; reset.) * full relay cycle on the merged exe: 2 pods staged, launch pair, mission, StopMission, round RESET -- all clean. * all three console suites still pass; checkctx CLEAN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
62e89018f5 |
Merge glass-cockpit-refit: cockpit scaling + one button geometry, the keyboard button board, RGB keylight, crouch, the cwd guard
Cyd's 9-commit branch, reviewed before merge (clean merge-tree, zero overlap with the console/relay work that landed after his fork point; his L4VB16 refit preserves the #48 plane-audit tripwires, and his lamp-decode fix corrects the #47 flash rendering). Four review findings are fixed in the follow-up commit: the stale README controls table, the unguarded backtick view-toggle, the environ.ini one-shot loophole, and volume-key documentation (the -/= -> PgUp/ PgDn move itself landed pre-merge so this branch's Comm-bank -/= bindings are collision-free). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9384c38c08 |
volume keys: -/= -> PgUp/PgDn, ending the zoom double-fire (prep for the cockpit-refit merge)
The issue #26 volume poll watched VK_OEM_MINUS/VK_OEM_PLUS -- the same physical keys the authentic 1995 typed-character target-zoom hotkeys ('+'/'-') live on, so a single press zoomed AND stepped the volume (operator-reported annoyance). The incoming glass-cockpit-refit branch also binds -/= as Comm-bank buttons, which would have made it a TRIPLE dispatch on fresh installs. PgUp/PgDn produce no WM_CHAR at all, so they cannot collide with any typed hotkey in any install; they are unbound in every bindings layout including the refit's 74-key board; and they exist on tenkeyless keyboards. Numpad +/- were considered and rejected: their typed characters feed the same zoom channel that made -/= wrong. README/CONTROLS documentation follows in the merge-fixes commit (the whole controls table is being redone there for the new default board). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a60ada782f |
operator-console KB: record what survives of the 1995 console (BTCNSL.CPP message defs; the Macintosh clue)
The changelog line in game/original/BT/BTCNSL.CPP -- '06/03/95 GAH Added
corresponding Macintosh message definitions' -- implies the 1995 sysop station
was a Mac application in a separate codebase, which explains its total absence
from the pod-side archives. Recorded so the provenance question ('is our
console a port or a recreation?') has a durable answer: wire-faithful
recreation, from the surviving parser side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c9e561d9ba |
console review round 2: six must-fixes from the adversarial pass (one of its fixes corrected)
The 4-dimension review of 4736cba..820caf8 returned 14 surviving findings. All six must-fixes plus the four deferables are in; one of the review's own prescriptions was wrong and is fixed differently (below). THE REAPER, FINAL DOCTRINE (blocker + major). Rev 2 -- "reap anything silent in the staging window" -- was still wrong twice over: a pad sends exactly ONE message in its life (the egg ACK, L4NET.CPP:1259) and is then silent FOREVER, so any manual-launch hold >180s reaped every healthy ACKed pad and force-relaunched their clients; and a REGISTERED game conn is quiet on TCP while loading, so with BT_RELAY_TCP_ONLY=1 (or blocked UDP) the reaper _abort_round()ed the whole night every ~3 minutes blaming a healthy player. The rule is now: an app-level deadline is valid only while a RESPONSE IS OWED. Only egg-sent-never-ACKed pads are reaped, with the debt clock starting at EGG SEND (a pad that sat through a long held-egg wait gets its full window -- an edge neither review round caught); game conns are never reaped at all. Half-open ghosts are TCP keepalive's job (~130s, faster than the deadline anyway). RE-ARM GUARDED ON EVERY CHANNEL (major). The launch_at guard lived only on the LAUNCH-press path; the ctl `rearm` command, stdin, and the always-lit GUI button reached _rearm_for_new_round unconditionally -- one press mid-mission zeroed the counters, permanently killing the mission clock AND making End Mission print "no mission is running": an unstoppable round, the exact class this feature exists to eliminate. Now refused (loudly) while a mission is running or a pair is in flight, and the button greys while launched. THE REVIEW'S OWN FIX WAS WRONG here: it prescribed refusing on `launches_sent >= 2`, but that stays 2 after a FINISHED round -- applying it verbatim resurrected the original dead-LAUNCH wedge, caught immediately by the regression suite. "Running" is `launches_sent >= 2 AND not stop_sent`. RE-ARM RE-KEYS SEATS (major). It restored the template roster but left seat_beacons/seat_prefs keyed by the trimmed round's positional ids, so round 2's trim minted a departed player's tag into the egg and trimmed a present player's out, shifting every callsign/mech a slot. The re-key block is factored out of _maybe_reset_round (_rekey_seats_to_roster) and shared. RESTART SESSION NO LONGER BAKES A WALK-UP'S NAME INTO THE EGG (major). _stop_session and _start_session now restore the stashed configured callsign/mech into the cells BEFORE _collect_egg can snapshot them (_restore_seat_defaults); previously the departed name went into the egg (name= plus rasterized bitmaps) and was then re-captured as the seat's permanent "configured default". LATE REMOTE OPERATOR GETS A ROSTER (major). The only line the GUI can adopt tags from was printed once at relay startup and aged out of the 400-line control history in ~33 minutes of stats chatter -- AUTH now re-issues the live roster line ahead of the replay, so a remote operator connecting at any point gets pilot lights and a working LAUNCH button. DEFERABLES, all four: _drop_game blames the tag stashed AT REGISTRATION (the live-roster resolve named the wrong tag for a trimmed-round conn dying after a re-arm restore -- and the tag-trusting GUI would clear the wrong seat); an operator-BLANKED callsign cell now restores (empty string is a real configured value; the falsy skip left the departed name up and re-captured it); a returning player displaces their own half-open registered ghost (same IP, no mission running) instead of eating ROSTER FULL until keepalive fires; udp_spoofed now rides the [relay-stats] line when non-zero and the warn set is capped at 256. VERIFIED: rearm suite grown to 25 checks (ACKed pad never reaped however silent; never-ACKed pad reaped; game conns never reaped; the egg-send debt clock; re-arm refused mid-mission, allowed after round end) -- plus net 17/17, roster 22/22, checkctx CLEAN. Live rig: mid-mission `rearm` refused with the message and the mission survived; End Mission -> re-arm -> full re-seat -> second mission launched. One bounded artifact observed and documented: each waiting pod bounces once (identity resync) after an explicit re-arm. Docs rewritten to the final doctrine (context/operator-console.md reaper + re-arm + roster-replay + udp_spoofed sections; OPERATOR_GUIDE + tooltip now say re-arm is between-rounds-only and warn about the one-bounce resync). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9d536294e4 |
Mouse buttons are bindable; mouse LOOK scoped (not built)
BUTTONS (shipped). Win32 hands mouse buttons out as virtual keys, and the
binding path already stores VKs and polls GetAsyncKeyState -- so this needed no
verb, no parser change and no new machinery, just names:
key MOUSE4 button 0x40 # side buttons
key MOUSE5 button 0x46
key MOUSEMIDDLE button 0x41
MOUSELEFT/MOUSERIGHT are named too, with a warning in the file header and the
docs: they are how the player presses cockpit buttons (left = press, right =
latch), and the poll is focus-guarded but not click-aware, so binding either
ALSO fires on every panel click. The side and middle buttons are the free
ones. Axes work as well (`key MOUSE4 axis Throttle slew + 0.7`).
One guard: the RGB lamp mirror skips mouse VKs when building its map -- a
keyboard lamp array has no mouse buttons to paint (BTPadBindingIsMouseKey).
Verified live: a bindings.txt carrying two mouse rows loads 76 keys (74 + 2),
and a real injected XBUTTON1 press produced `[emitter] FIRED #1`. Clean A/B --
the earlier run whose SendInput failed the struct-size check logged no fire at
all. Shipped commented-out examples in the default profile so the capability
is discoverable.
LOOK (scoped only). docs/MOUSELOOK_PLAN.md. Mouse MOVEMENT is a real feature,
not a table entry, and the hard part is not reading the device:
- The cursor is already spoken for. The glass cockpit's premise is that all
72 buttons are mouse targets; mouse-look wants a captured, hidden, recentred
cursor. Both cannot be true, so this is a MODE question first -- four
models compared, recommending hold-to-look (costs nothing when unbound,
cannot strand a player, behaves the same in surround and exploded layouts).
- The channels are POSITIONAL (JoystickX/Y are -1..1 deflections the mapper
reads per control mode); a mouse gives deltas. Accumulate-into-a-virtual-
stick reuses every existing rule; a rate model would need new mapper
semantics.
- Control mode changes what it MEANS: BASIC steers with the stick, MID/ADV
twist the torso -- so mouse-look steers in one and aims in the other.
Proposed grammar (new row type, so old builds skip it with a warning),
where the code goes, ~250 lines, and a verification plan whose load-bearing
item is "panel clicks still work when a mouselook binding exists but is not
engaged". Also flagged: the pod had NO mouse, so nothing here is
reconstruction -- every choice is design, judged on feel, and it must stay out
of the pod build's path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
820caf8765 |
console review follow-up: two edge cases in my own roster fix, found and fixed
Self-review of |
||
|
|
3a694485bf |
operator UI: a departed player no longer keeps their seat in the roster
Operator report: "if a player disconnects from the console, it leaves their name
up in the roster ... it should turn their seat back to empty." I had not noticed
or fixed this -- the earlier review raised adjacent roster issues (lights mapped
through the untrimmed tag list after a trim) but not this one.
CAUSE. The GUI only ever WROTE walk-up names into the table. On a disconnect
the relay pops seat_info, the write-back loop hit `if not info: continue`, and
the cell kept the departed callsign for the rest of the session -- while the
pilot light beside it correctly said "waiting". So the table and the light
disagreed and a free seat looked occupied.
Two changes, because the relay's two seat-clearing signals are NOT symmetric:
* SessionMonitor now pops seat_info when a pilot goes idle from EITHER signal.
"PLAYER n LEFT" is only printed inside the beacon-close branch gated on
`if seat not in self.by_host` (the relay's own comment: "never claimed:
player left"), so a player who was fully REGISTERED and then dropped
produced only `game[...] dropped` -- the common case, and the one that left
the name up. Keying "seat is empty" off LEFT alone could never have worked.
* _refresh_pod_status stashes the operator's configured callsign/mech PER
OCCUPANCY when a seat fills, and restores it when the seat empties.
Per-occupancy rather than once at egg load, so an edit the operator makes
while a seat is empty is respected instead of being overwritten by a stale
snapshot.
It restores the CONFIGURED PILOT, not a literal blank, deliberately: that cell is
editable and is what gets written to the egg on Save, so a placeholder like
"-- empty --" would end up in the mission file. The "unoccupied" signal is the
grey `waiting` light beside it.
NOT CHANGED, and explained instead: a vacated seat is held for its player for 90s
(seat_reclaim) so a crash or a reconnect returns them to the same seat and mech.
Assignment skips claimed/reserved/reclaim-held seats, so a brand-new joiner in
that window gets the next free seat, or ROSTER FULL if there wasn't one; after 90s
the seat is fair game. The operator said they were happy with players keeping
their seat/address, so this stays -- it is now documented in both docs rather than
silently removed.
VERIFIED: scratchpad/test_operator_roster.py (new, 14 checks) drives the REAL Qt
widget offscreen and feeds it the REAL relay log lines: a walk-up name appears;
after REGISTERED then `dropped` the light returns to waiting AND the seat shows
the configured pilot again; the seat is reusable by a different player and clears
for them too; the `PLAYER n LEFT` path clears it as well; an operator edit made
while the seat was empty survives an occupancy; and a neighbouring row is never
touched by another seat's traffic. Other suites still pass (rearm 19/19, net
17/17); checkctx CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ab5828a866 |
relay/console: fix the three remaining hardening items from the review
1. UDP ENDPOINT HIJACK. `from_host` in a UDP envelope is the sender's OWN claim,
and the relay used it directly to refresh that host's downstream endpoint --
so ANY datagram claiming host N silently stole host N's traffic (a zombie pod
from a previous round, a stale NAT mapping, or anyone who guessed a host id).
The victim simply stopped receiving on a channel that still looked healthy.
The claim is now bound to the identity we actually authenticated: the IP of
that host's live TCP game connection. The PORT is deliberately not checked --
it moves on a NAT rebind, which is the whole reason the endpoint map refreshes
per datagram -- and a genuine IP change cannot happen without the TCP
connection breaking and re-registering, so a legitimate pod is never rejected.
Rejections are counted (udp_spoofed) and logged once per offending (host, IP).
Known limit: two pods on one machine share an IP, so this cannot separate
them; same-machine trust is assumed.
2. THE EGG ACK COULD BE MISSED ENTIRELY -- a silent, unrecoverable wedge. The
pod's ACK is the launch gate's ONLY signal, and it was detected by parsing a
single recv() at fixed offset 0 behind a `len(data) >= 24` test. On a real
network (loopback hid both cases) the 28-byte ACK arriving SPLIT -- 16 bytes
then 12 -- was dropped by that test and never looked at again, and two
COALESCED messages meant only the first was read. A missed ACK means that
seat never counts toward the gate, so the round can never be released.
_console_read now buffers per connection and walks every complete frame
(16 + messageLength); an implausible length drops the connection WITH A REASON
rather than mis-reading that pod all night, since a stream protocol cannot be
resynced by guessing. Bounds: CONSOLE_MSG_MAX 64K, CONSOLE_INBUF_MAX 1 MiB.
3. REMOTE-OPERATOR MODE WAS HALF A CONSOLE. LAUNCH and END MISSION could never
enable (both conditions required `console_proc is not None`, which the remote
path never sets), the pilot lights were permanently blank (SessionMonitor was
built with an empty tag list, so `seated` was always 0), and Launch-local was
refused by the same guard even though the code below it already built
BT_RELAY from the remote host. All three enable conditions now accept EITHER
channel, and the monitor ADOPTS THE ROSTER from the relay's
"roster: N pilot(s) -> hostIDs [...]: [...]" line -- which the relay replays to
every newly AUTHed operator -- so a remote operator gets real lights and a
real seat count.
VERIFIED
* scratchpad/test_relay_net.py (new, 17 checks): spoofed datagram cannot move
an endpoint and is counted; a NAT rebind on the same IP still is honoured; an
unregistered host id still dropped; the ACK is found whole, split in two,
split three ways mid-header, and when hiding behind another message;
a garbage length drops the conn; the roster line is adopted, maps a
subsequent SEATED onto a real tag, lifts `seated` off 0, and re-feeding it
preserves known state.
* Live 2-pod rig: real pods ACKed through the new reassembly path (2/2 ready,
zero desync drops), the mission launched and ran, UDP flowed throughout
(93 rx / 85 tx, udp-known [2,3]) with ZERO false spoof rejections, then a
clean StopMission + round RESET.
* scratchpad/test_relay_rearm.py still 19/19; checkctx CLEAN.
Docs updated to match (context/operator-console.md gains reassembly and
anti-hijack sections; the guide no longer claims remote mode is crippled).
Remaining open items are now recorded in the topic's frontmatter: mesh mode's
operator buttons are inert by construction (no stdin reader in that path -- left
alone, mesh self-launches), _log_launch_readiness can cry STALL on a healthy
launch-with-whoever, and a straggler's late FIN is still misread as a pod dying
mid-load.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|