Author SHA1 Message Date
Joe DiPrimaandClaude Fable 5 40c35946cd tester handout regenerated after Oracle's annotated sweep: 35 closed, 59 open, 883 headliners called out (#149 glass fix + #150 double-kill, first fielded build with both)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 15:26:31 -05:00
Joe DiPrimaandClaude Fable 5 47dcc62fc1 pod RIO investigation day-end: board mutes ~13s after each start (render-load/power-rail hypothesis for tomorrow); USB re-enum does NOT revive it; keep tools/probe_rio.ps1 (raw COM1 hand-poll); note the stall-detector keying fix needed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 13:40:53 -05:00
Joe DiPrimaandClaude Fable 5 9754fb1295 correct the record: the 2026-08-10 boot wedges were the USB-serial adapter hanging an unbounded overlapped write, NOT print-deadlock (confounded A/B, retracted); pod runbook gets the full COM1/FTDI diagnosis + remote minidump procedure
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 13:35:13 -05:00
Joe DiPrimaandClaude Fable 5 716e10e3fa serial write hardening: bound the overlapped-write wait (500ms + CancelIo + 3s sick window) -- a dropped USB-serial link parked the game forever in GetOverlappedResult (ALPHA-MR minidump: SendPacket <- RIO ctor); txTO= counter joins the [rio] health line
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 13:30:41 -05:00
Joe DiPrimaandClaude Fable 5 ab1b3439f4 [rio] telemetry FIX: printing from the serial poll path deadlocks against the PCSPAK RX thread (0-CPU park ~10s after every live-RIO boot, A/B-convicted on ALPHA-MR) -- GetNextEvent now only COUNTS; the printer (BTRioHealthReport) rides the glass tick beside [glassperf]
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 13:06:06 -05:00
Joe DiPrimaandClaude Fable 5 7cd58d3000 pod runbook: schtasks /end orphans the game (dead-RIO trap) + self-cleaning runpod.bat
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 12:35:52 -05:00
Joe DiPrimaandClaude Fable 5 a28878beaf [rio] link-health telemetry: req/rep/btn counters, reply age, the existing (never-printed) line-error/abandon/board counters, and STALL/RECOVER event lines -- default-on for hardware RIO, BT_RIO_LOG=0 opts out (encoder-dropout hunt on ALPHA-MR)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 12:34:10 -05:00
Joe DiPrimaandClaude Fable 5 a7722d9ca9 pod: EDID monitor:id binding DEPLOYED + verified on ALPHA-MR; runbook updates (Default_Monitor radar identity, cmd 1>> trap, tester-procedure deploy verified)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 11:56:26 -05:00
Joe DiPrimaandClaude Fable 5 bf04e1c079 port-layer papercuts for the playtest: #69 Denkou rename, #71 plasma X=minimize, #157 round-end UX (foreground handoff + lastrun clean-exit markers + bat sign-off rewording); tester-facing open-issues handout (docs/) + field-log anatomy notes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 11:29:01 -05:00
Joe DiPrimaandClaude Opus 5 46ff8dd262 #150 double kill FIXED (two binary-cited gates) + #151 measured AUTHENTIC to the point
#150 -- one kill credited twice (Rajel on Oracle, Oracle on Conn Man).
CAUGHT: one death -> two type-2 arms 1ms apart (awards 501.96/505.88 -- the
tally GREW between them), victim inst=R.  Mechanism: this port deliberately
applies damage on replicants for local visual response; the report tail ran
there too, and a replicant's movement mode cannot flip to wreck until the
victim's death record round-trips -- so each round of a killing VOLLEY inside
that window re-posted the kill.  Why missiles, why intermittent.

The binary forbids the whole situation: its handler OPENS with the assertion
  if ((flags & 0xc) == 4) Fail("Replicant Mech recieving takedamage",
                               MECH.CPP, 0x3da)          [part_012.c:14613]
-- TakeDamage on a replicant was a BUG in 1995; the tail only ever ran on the
victim's master.  Fix: (1) gate the REPORT TAIL master-only (local damage
application stays); (2) adopt the binary's once-latch -- the 0->1 edge of
IsMechDestroyed() across the handler call (local_14), replacing the
deathBlastArmed form for the report gate.

VERIFIED (scratchpad/night14/scorepair.sh): 3 victim deaths, 5 shooter
respawns -> exactly one type-2 per credited kill, kills 0->1->2, zero doubles
(was: kills 0->2 on one death).

#151 -- panic eject "-499 instead of 0" is AUTHENTIC 1995 SCORING, decoded
from EjectPilot @0049f854: the punch-out dispatches a SELF TakeDamage of
  damageAmount = role->killBonus (+0x1c, 500 in dfltrole), type Explosive,
  senderMechID = SELF, player->suppressConsole = 1, graphicAlarm -> 10
which flows through the report tail as a SUICIDE kill: award = -CalcKillScore
(your own death), plus the type-1 death cost.  1000 - ~999 - 500 = -499:
the binary predicts Oracle's exact number.  Our port already used
role->killBonus for the blast (mech.cpp:652) -- faithful end to end; bench
shows each eject charged exactly once (-900/-1050 across three ejects).
Panic eject = scored as suicide.  Whether that is fun is a design question;
it is not a porting defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-10 10:30:12 -05:00
21 changed files with 578 additions and 90 deletions
+1
View File
@@ -82,6 +82,7 @@ precise than anything you can infer.
| Multiplayer, replication, netcode | `context/multiplayer.md` | | Multiplayer, replication, netcode | `context/multiplayer.md` |
| **The OPERATOR CONSOLE + RELAY** — how to launch it, ports, seats, the round/launch lifecycle, re-arm | `context/operator-console.md` | | **The OPERATOR CONSOLE + RELAY** — how to launch it, ports, seats, the round/launch lifecycle, re-arm | `context/operator-console.md` |
| Running a session as sysop (operator-facing how-to) | `docs/OPERATOR_GUIDE.md` | | Running a session as sysop (operator-facing how-to) | `docs/OPERATOR_GUIDE.md` |
| The tester-facing OPEN-ISSUES handout (for Discord) | `docs/OPEN_ISSUES_FOR_TESTERS.txt` — regenerate from the tracker + update the header date before each playtest |
| Pod hardware, monitors, RIO, MFD surfaces, input remap (CONTROLS.MAP/XInput) | `context/pod-hardware.md` | | Pod hardware, monitors, RIO, MFD surfaces, input remap (CONTROLS.MAP/XInput) | `context/pod-hardware.md` |
| Glass cockpit desktop dev layer (BT_GLASS/BT_STEAM gates, PadRIO, miniconsole, Steam) | `context/glass-cockpit.md` | | Glass cockpit desktop dev layer (BT_GLASS/BT_STEAM gates, PadRIO, miniconsole, Steam) | `context/glass-cockpit.md` |
| Steam internet MP (the wire seam, identity tokens, lobby) | `context/steam-networking.md` | | Steam internet MP (the wire seam, identity tokens, lobby) | `context/steam-networking.md` |
+1 -1
View File
@@ -1384,7 +1384,7 @@ cmHeat(K,ModeSecondaryHeat,58,heatpal.pcc,heatpal2.pcc,Myomers);
} }
#======================================================================= #=======================================================================
# Sunder V1 # Denkou (was: Sunder V1)
#======================================================================= #=======================================================================
Snd1Init Snd1Init
{ {
+17
View File
@@ -69,6 +69,23 @@ run\run.cmd [EGG] # default DEV.EGG; cd's to content\ and runs btl4.exe -eg
**Keep the `if /I "%%P"=="btl4.exe"` guard** — parsing tasklist with `tokens=2` alone reads its **Keep the `if /I "%%P"=="btl4.exe"` guard** — parsing tasklist with `tokens=2` alone reads its
*"INFO: No tasks are running"* line as a PID and spins forever with nothing running. Full *"INFO: No tasks are running"* line as a PID and spins forever with nothing running. Full
investigation, including which windows exit vs merely hide: `phases/phase-12-orphan-processes.md`. investigation, including which windows exit vs merely hide: `phases/phase-12-orphan-processes.md`.
- **Field-log anatomy — reading a steam night (#157 forensics, 2026-08-10) [T2].** A night's log is
a CHAIN of `===== BT411 SESSION` blocks: menu stubs (`args=''`, "first process") alternating with
glass generations (`-net 1501 -platform glass`, "relaunched generation"); one glass block = one
drop, and boundaries are synchronized across every machine to ±2 s (mission-clock expiry relaunches
all peers' menus in the same second). **Stub anatomy:** the steam lobby is CLICK-driven
(`steamAction`, btl4fe.cpp), so a menu closed without a click logs exactly **4 boot lines** and
exits 0 — a 4-line stub at end-of-night is a player going to bed, NOT a crash (all four machines
ended night-14 with identical 4-line stubs at 00:31:2x). 78-line stubs = a lobby cycle
(`[steamnet] up` + `[lobby] join` + map). `BTCrashFilter` is armed before the first log line and
writes a `[crash]` block + module-relative stack on ANY unhandled exception — a silent log end
means quit/kill/hang, never an unlogged AV. **`lastrun_<stem>.txt` caveats:** appends stop silently
if the file gets locked (the `is_open` guard skips) — a lastrun ending mid-night while the log
continues means the file was captured/locked during live troubleshooting, not that the chain died.
And the bat sign-off ("The game has exited. If it closed unexpectedly, send…") prints after ANY
chain end including a normal quit — it primes players to report quits as crashes; #157 ("did not
exit cleanly") was exactly this echo. The arcade-authentic round-end (mission process exits →
menu relaunch) itself reads as a crash to first-night players.
- **Why this is a BT411-only hazard [T1].** The 1995 pod shipped ONE folder — `BTL4OPT.EXE` sits - **Why this is a BT411-only hazard [T1].** The 1995 pod shipped ONE folder — `BTL4OPT.EXE` sits
next to `BTL4.RES`/`VIDEO\`/`GAUGE\`/`AUDIO\` (it is still there in `content\`) — and RP411/RP412 next to `BTL4.RES`/`VIDEO\`/`GAUGE\`/`AUDIO\` (it is still there in `content\`) — and RP411/RP412
keep that shape (`pack-dist.ps1` copies the exe and every asset dir into one dist root). BT411's keep that shape (`pack-dist.ps1` copies the exe and every asset dir into one dist root). BT411's
-26
View File
@@ -203,32 +203,6 @@ appears in the cfg like any panel and honours `,noframe`. Verified: a drag wrote
at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike
the panels it has no `WM_TIMER` focus-throttle to worry about. the panels it has no `WM_TIMER` focus-throttle to worry about.
## Glass-panel repaint pump — perf + dirty-skip (2026-08-09/10) [T2 measured]
The exploded per-display windows are pure CPU/GDI (`ExpandPlaneToBGRA` +
`StretchDIBits`), repainted synchronously on the MAIN render thread by
`BTGlassPanels_Tick` (~16 Hz). Three layered changes after a playtester reported
~20 fps in panels mode vs ~130 fps in the surround:
1. **HALFTONE → COLORONCOLOR** (`f3d27f5`, in master): the `HALFTONE` stretch
(GDI's per-output-pixel resample) × 7 windows per pump was the primary sink;
nearest is crisper for the low-res MFD pixels anyway. `BT_GLASS_SMOOTH=1` restores.
2. **Per-window dirty-skip** (`c9e25e5`, in master): each window carries a change
token = FNV over `SVGA16::PlaneChecksum(mask)` (the shared pixelBuffer masked to
every port that can feed the window) + each button's RENDERED lamp brightness +
held/latch. The pump re-blits only windows whose token moved: ~31 pumps/2s →
4-15 window repaints vs 217-224 always-on. `BT_GLASS_DIRTY=1` logs the tally.
3. **Palette generation in the token** (branch `glass-palette-token`): the
ColorMapper family (armor rosette tints, adpal/adpal2 damage flash) animates by
CLUT writes with ZERO pixel churn — invisible to a pixel checksum.
`SVGA16::paletteGeneration` is bumped by the palette writers
(`BuildSecondaryColor` only on a REAL entry change, since the flash alternates
palettes every Execute writing identical RGB at zero damage; full rebuilds
unconditionally) and folded into the token for palette-expanding windows
(`monoTint < 0`, or all under `BT_GLASS_MFD_PAL`). Without it the glass radar
held stale armor tints between pixel repaints while the D3D surround (which
re-expands every frame) tracked live.
**Turning it OFF: `L4PLASMA=NONE` (also `OFF`/`0`, 2026-08-06) [T2].** `L4GREND` creates a **Turning it OFF: `L4PLASMA=NONE` (also `OFF`/`0`, 2026-08-06) [T2].** `L4GREND` creates a
marquee whenever `L4PLASMA` is set at all (`SCREEN` → the desktop window, anything else → a real marquee whenever `L4PLASMA` is set at all (`SCREEN` → the desktop window, anything else → a real
`PlasmaDisplay` on that serial port), and the GLASS profile force-defaults it to `SCREEN` — so on `PlasmaDisplay` on that serial port), and the GLASS profile force-defaults it to `SCREEN` — so on
+77 -3
View File
@@ -254,7 +254,22 @@ desktops. Two pieces were added for the cab:
once, and over CRD you cannot see the panels at all: the log IS the confirmation that a picture once, and over CRD you cannot see the panels at all: the log IS the confirmation that a picture
landed on the right glass. landed on the right glass.
### Boot-STABLE panel binding — `monitor:id:` (2026-08-08, staged, pod-untested) ### Boot-STABLE panel binding — `monitor:id:` (DEPLOYED + POD-VERIFIED 2026-08-10)
**Live on ALPHA-MR since 2026-08-10** — mapping eye-verified over livestream, then the master
`C:\bt411\glass_layout.cfg` flipped to the id form (old cfg saved as
`glass_layout.cfg.displaynbak`) and the relaunch bound all three windows to the SAME rects with
zero mismatch shouts [T2]. The shipped binding:
```
Secondary / Radar=monitor:id:Default_Monitor,bare # colour radar LCD, USB adapter
VGA Port A=monitor:id:UID224795,bare # RAR0005: Heat + Comm + Mfd2 CRTs
VGA Port B=monitor:id:UID200195,bare # RAR0005: Mfd1 + Mfd3 CRTs
```
⚠ **The radar LCD's USB display adapter presents NO EDID** — its only stable identity is the
`Default_Monitor` placeholder (`#Default_Monitor#…&UID256#`). Unique today; a second EDID-less
panel would collide, and the per-boot `[glassid]` dump (BT_GLASS_IDS=1, now PERMANENT in
`podprofile.ini`) plus the mismatch shout are the diagnosis tools. The Dell (`DEL4025`) is the
PRIMARY main-view panel (BT_FIT owns it; not in the layout cfg) — the 08-08 note calling it a
"1908FP" service monitor mis-guessed its role.
**Windows renumbers displays.** Nick, after re-cabling + a reboot: *"the order changed … sometimes **Windows renumbers displays.** Nick, after re-cabling + a reboot: *"the order changed … sometimes
they change when one gets turned off and back on, at least how windows SEEs them, even if the they change when one gets turned off and back on, at least how windows SEEs them, even if the
visual desktop tool looks the same."* So the two original binding forms are both boot-fragile — visual desktop tool looks the same."* So the two original binding forms are both boot-fragile —
@@ -480,8 +495,67 @@ instantly and did nothing" actually means "it is still hung". Recover with
without a GUI):** `DISPLAY\RAR0005\…UID224795`, `DISPLAY\DEL4025\…UID249395` (DELL 1908FP), without a GUI):** `DISPLAY\RAR0005\…UID224795`, `DISPLAY\DEL4025\…UID249395` (DELL 1908FP),
`DISPLAY\RAR0005\…UID200195`. **The two RAR panels share one EDID code and have blank serials**, so `DISPLAY\RAR0005\…UID200195`. **The two RAR panels share one EDID code and have blank serials**, so
`monitor:id:RAR0005` is ambiguous — they must be bound by the per-connector form `monitor:id:RAR0005` is ambiguous — they must be bound by the per-connector form
(`monitor:id:UID224795` / `monitor:id:UID200195`). The Dell's code is unique. The cab's shipped (`monitor:id:UID224795` / `monitor:id:UID200195`). **RESOLVED 2026-08-10:** the full map is
`glass_layout.cfg` still uses the fragile `monitor:DISPLAY4` device-name form. UID224795 = VGA Port A (Heat+Comm+Mfd2), UID200195 = VGA Port B (Mfd1+Mfd3), `Default_Monitor`
(EDID-less USB adapter, UID256) = the colour radar LCD, DEL4025 = the PRIMARY main-view panel
(not a service monitor). The master cfg now ships the id form (see the binding section above);
`glass_layout.cfg.displaynbak` preserves the old DISPLAYn cfg.
**7. ⚠ cmd redirect trap when scripting over SSH:** `echo VAR=1>> file` appends `VAR=` — cmd
parses the digit-adjacent `1>>` as a stream-handle redirect and eats the value (and MSVC putenv
treats an EMPTY value as UNSET, so the flag silently never arms). Space it (`echo VAR=1 >>file`,
trailing-space payload) or use PowerShell `Add-Content`. Bit us arming BT_GLASS_IDS 2026-08-10.
**8. Tester-procedure deploy VERIFIED end-to-end (2026-08-10, build 876):** scp zip →
`Expand-Archive` → podkit → `schtasks /run` came up clean on the first try — rig config survived,
newest-install auto-resolve worked, no hand-edits. The cab genuinely upgrades like a tester box.
**9a. ⚠ THE RIO's "COM1" IS A USB-SERIAL ADAPTER — the axis-dropout / frozen-boot fault
(2026-08-10, minidump-proven) [T1].** `\Device\VCP0` = an FTDI adapter (FTE7PGA5A); COM2 is a
second FTDI (A995JTOYA); **two ghost Prolific PL2303 entries also claim "COM1"** from earlier
plug-ins (clean them in Device Manager). When the adapter's **USB side** drops mid-operation:
brief drop = analog polls go mute (the field "encoder dropouts", revived by hand-jostle /
re-enumeration — NOT by button protocol); drop with a write in flight = the 1995 code's
**unbounded `GetOverlappedResult` wait in `PCSerialPacket::SendPacket` parked the whole game at
0 CPU forever** (three boots on 2026-08-10; stack: NtWaitForSingleObject ← GetOverlappedResult ←
SendPacket ← RIO ctor). **Hardened in 4.11.881**: 500 ms bounded write + CancelIo + 3 s sick
window (fast-drop, game keeps frame rate, keyboard fallback lives); `txTO=` in the `[rio]` health
line counts the events — proven the same evening (`txTO=1 rRetry=4` on a boot that SURVIVED).
USB **selective suspend disabled** system-wide on the cab the same day (powercfg) — prime suspect
for the random-period idle dropouts. Hardware to-do: FTDI adapter into a REAR motherboard USB
port, reseat its USB end. ⚠ A first "print-deadlock" diagnosis of the boot wedge was WRONG
(confounded A/B) and is retracted — the dump is the authority.
**Day-end state (2026-08-10 evening) + the pick-up plan.** The 881 soak measured the fault
precisely: the board answers for **~13 s after each fresh game start** (254 replies at 20 Hz),
then goes MUTE on analog for minutes while host→board writes keep completing (`req=3846 rep=254
age=240s`, txTO frozen at 1, abandon=0). Direct hand-poll (`tools/probe_rio.ps1`, also at
`C:\bt411\probe_rio.ps1` — raw COM1 CHECK/VERSION/ANALOG with correct [cmd][sum&0x7F] framing,
DTR/RTS raised): **zero bytes**. USB re-enumeration of BOTH FTDI converters + immediate re-poll:
**still zero** — which argues AGAINST the adapter (a wedged adapter RX clears on re-enum) and AT
the board. ⚠ [T4 hypothesis for tomorrow]: 13 s ≈ when the mission render load lights up — a
**power rail sagging under the cab's full load** would also explain the original mid-combat
dropouts (explosion/load transients) and the button "revival" (load dips + harness jostle).
Tomorrow, with hands: (1) loopback plug on the FTDI (TX→RX jumper, probe echoes = adapter fully
exonerated); (2) 5 V/12 V at the RIO board while a mission loads (the 13 s correlation is the
test); (3) swap in one of the spare Prolific adapters as cheap elimination; (4) rear-mobo USB
port + ghost-COM cleanup regardless. ⚠ My STALL/RECOVER edge detector in 881 keys on
request-age, which the 50 ms poll keeps resetting — it never fires; the `age=` field carries the
real signal (fix the keying to reply-age at the next build).
**Remote minidump procedure (worked over SSH, no tools install):** 32-bit dump of the wedged pid
via `C:\Windows\SysWOW64\rundll32.exe C:\Windows\SysWOW64\comsvcs.dll, MiniDump <pid>
C:\bt411\w.dmp full` → scp home → local x86 cdb (`where.exe cdb` resolves the bt411-tools copy)
with `-y C:\git\bt411\dist` (the archived per-build PDBs) → `~*k`. A 64-bit comsvcs dump of the
WOW64 process shows only 64-bit frames — use the SysWOW64 pair.
**9. ⚠ `schtasks /end` ORPHANS the game — the dead-RIO trap (2026-08-10):** `/end` kills the
batch, but `runpod.bat` `start ""`s the exe DETACHED, so the old btl4.exe survives, keeps COM1
open, and the NEXT launch boots with a dead cockpit board: `InitSerialPort() failed
[GetLastError() = 5]` (access denied) → `RIO initialization failed!` → buttons dead while the
banner still says "hardware RIO" (the banner is the PROFILE, not the init result). Diagnosis:
`tasklist /FI "IMAGENAME eq btl4.exe"` showing ≥2 instances. FIXED at the root the same day:
`runpod.bat` now `taskkill /F /IM btl4.exe`s FIRST (original saved as `runpod.bat.bak`), making
the BT411Run task an idempotent restart — but any hand-rolled launcher on a future cab needs the
same line, and a mid-mission `/run` now RESTARTS the game rather than failing quietly.
## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source] ## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source]
`reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on `reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on
+1 -1
View File
@@ -339,7 +339,7 @@ cavern.egg`. This is every option the original console exposed for a BattleTech
- experience (4 [T1 game decode]): novice, standard, veteran, expert ← was hardcoded `expert` - experience (4 [T1 game decode]): novice, standard, veteran, expert ← was hardcoded `expert`
- vehicle (18, friendly→tag): Black Hawk=blkhawk, Loki=loki, Nova=bhk1, Mad Cat=madcat, Thor=thor, - vehicle (18, friendly→tag): Black Hawk=blkhawk, Loki=loki, Nova=bhk1, Mad Cat=madcat, Thor=thor,
Owens=owens, Kotori=own1, Summoner=thr1, Hellbringer=lok1, Timber Wolf=mad1, Avatar=avatar, Owens=owens, Kotori=own1, Summoner=thr1, Hellbringer=lok1, Timber Wolf=mad1, Avatar=avatar,
Satyr=ava1, Sunder=sunder, Sunder V1=snd1, Vulture=vulture, Mad Dog=vul1, Mischief=lok2, Satyr=ava1, Sunder=sunder, Denkou=snd1 (#69), Vulture=vulture, Mad Dog=vul1, Mischief=lok2,
Zanin Neko=mad2 (the 18 ModelList names; matches the 2026-07-18 vehicle sweep) Zanin Neko=mad2 (the 18 ModelList names; matches the 2026-07-18 vehicle sweep)
- color/camo (7, display→tag): White=White, Black=Black, Brown=Brown, Red=**Crimson**, Green=Green, - color/camo (7, display→tag): White=White, Black=Black, Brown=Brown, Red=**Crimson**, Green=Green,
Gray=**Grey**, Tan=Tan ← was White/Black/Crimson only Gray=**Grey**, Tan=Tan ← was White/Black/Crimson only
+135
View File
@@ -0,0 +1,135 @@
=============================================================================
BT411 -- ALL OPEN TRACKER ISSUES (59), SORTED FOR PLAYTESTERS
Updated 2026-08-10 (evening) -- after Oracle's full annotated sweep, which
closed 35 issues. Thank you Oracle: that is what this list is for.
=============================================================================
HOW TO REPORT: drop a line in Discord with the issue number and PASS /
FAIL / what you saw. Screenshots and rough times help; logs get collected
as usual.
=============================================================================
-----------------------------------------------------------------------------
A. FIXED -- PLEASE VERIFY (headliners first: new fixes fielded THIS build)
-----------------------------------------------------------------------------
#149 Glass cockpit stutter / hitching ** HEADLINER **
First fielded build with the fix. Glass users -- especially Oracle:
mission load and play should feel smooth again (repaints now skip
windows whose gauges did not change).
#150 Double kill credit (one kill counted twice) ** HEADLINER **
First fielded build with the fix (868's double-kills were pre-fix).
TEST: kill someone with a missile volley -- scoreboard +1, not +2.
#69 "Sunder V1" renamed to DENKOU
TEST: vehicle picker shows Denkou -- era witnesses, confirm spelling.
#71 Closing the Plasma window lost it until restart
TEST: X the plasma window -- it minimizes to the taskbar; click restores.
#25 Gamepad turn profile (stick X could not turn the mech)
TEST: RajelAran -- bind an axis to "Turn" in content\bindings.txt.
#27 Autocannon/Gauss dealt the wrong damage type
Oracle asked for a RETEST before closing: ballistics should feel/score
like solid shot (no blast splash).
#38 Mech colors reverted on respawn
TEST: custom paint, die, respawn -- colors stay.
#39 Controls were live before the game screen appeared
TEST: mash fire during load -- nothing happens until the scene shows.
#41 (David/Auric) launcher killed before it could run -- forensics armed
TEST: if join.bat ever fails again, send content\lastrun_join.txt.
#56 MFD buttons drift away from where they are drawn
TEST: alt-tab / focus changes mid-round -- buttons stay aligned.
#147 Range caret vanished for the session (ticks stayed)
Monitoring: if it happens, the log traps the culprit -- just report it.
#158 (Sauron) could not move after respawn until throttle cycled
TEST when it happens: does cycling the throttle free you?
YES -> the #146 release mechanic (expected). NO -> map + spawn spot.
-----------------------------------------------------------------------------
B. OPEN -- HELP US REPRODUCE / NARROW (what to note when it happens)
-----------------------------------------------------------------------------
#24 Controller hat/look view sticks (recovers after wiggling)
#37 Mech torso appears BACKWARDS to other players (transient)
#74 8BitDo gamepad works solo, dead in Steam MP (VexUbiquity)
#102 Owens: full-throttle run -> hold reverse = animation/audio hang
REPRO known (exterior cam, savannah); self-clears on a torso twist.
#125 Peers do not see the FIRST destroyed-zone graphic (2nd hit shows it)
#126 Peer weapon fire invisible AND silent from some positions
#128 Ramming terrain = INSTANT coolant leak -- Oracle re-confirms STILL LIVE
#130 Vulture "skating" (gliding without walk animation)
#132 (Conn Man) persistent yellow line on the HUD in every mech
#135 Coolant leak with NO flashing annunciators -- Oracle re-confirms LIVE
#136 A SHUT leaking loop starts/stops leaking with COMPONENT toggles
(Oracle marked "Clarify" -- one more description pass requested.)
#143 One panel hit -> smoke erupts from MULTIPLE places at once
#153 Basic-mode elevation limit only applies after cycling modes (fix pending)
#154 Generator Out warning fires on the WRONG EDGE (at recovery, not trip)
Refined by Oracle: the stuck-forever gen did NOT reproduce; era rule =
gens return at HALF-cooled (red bar portion gone). Threshold audit due.
#155 PANIC lit: Eng + Weapon MFD banks dead; Score/Radar/Coolant stay live
(Oracle mapped the exact bank split; #50 closed into this ticket.
Flicker half = authentic. Next: is the bank capture authentic too?)
#159 (Conn Man) lag during drops -- missiles / fire exploding in empty space
NOTE: shooter + target + rough time; match receipts measure the rest.
#160 Pod cab: RIO axis dropouts + boot wedges (hardware investigation)
Cab-side only. Bounded-write hardening is in this build; the cage
hardware work continues with Nick.
-----------------------------------------------------------------------------
C. STILL-OPEN ERA QUESTIONS
-----------------------------------------------------------------------------
#103 Mech-to-mech RAM: armor damage or internals only? (Oracle: don't know;
Lynx / Ronin / Draco -- your call. Binary does internals-only.)
#133 Generator X-out on the MFD: Oracle recalls an X "under certain
circumstances" -- WHICH circumstances? (shutdown / overheat / destroyed)
ALSO (#152 rider): did Mid/Adv auto-recentre the torso in the pod, or only
Basic? (Oracle: don't know. Binary says: no auto-recentre.)
-----------------------------------------------------------------------------
D. DESIGN CALLS -- group opinions wanted
-----------------------------------------------------------------------------
#151 Panic eject costs -499: verified REAL 1995 scoring (your death scored
as suicide + death penalty). Keep authentic, or house-rule a flat cost?
#23 Non-glass screen layout: Oracle proposes widescreen-aware sizing --
world view wider + taller, moved up, allowed to overlap the radar.
Opinions welcome before anyone prototypes it.
#77 Resizable Secondary/Radar window (touch-panel cockpits). Want it?
-----------------------------------------------------------------------------
E. KNOWN GAPS -- confirmed and on the list, no need to re-report
-----------------------------------------------------------------------------
#49 Radar/map does not draw the arena WALL geometry
#76 Glass layout save/restore rework (some windows not saved)
#83 INCOMING MISSILE alarm not implemented yet
#90 Flames/smoke from serious damage: less than the original (tuning)
#104 Mad Dog "NARC 15" label: NARC postdates our 4.10 binary (Oracle: added
in a late 4.x release; zero NARC in our decomp) -- content rename due.
#105 Sensor damage effects: Oracle's era spec captured (HUD flicker/loss,
radar loss) -- implementation pending.
#111 Peers see a dying mech's blue warp bubble (should see explosion only)
#112 AFC rounds shotgun across zones (should land as one projectile)
#113 LRM burn/hit state lasts too long
#115 Hot box / target select not connected (Oracle era note: hotbox was a
STANDARD-mode feature; absent in Expert -- gating to reconcile)
#121 Sensor HEAT has no consequences yet
#123 Searchlight: the visible LIGHT + night-fog swap still to come
#145 IR "predator vision": decode done, display not implemented
#156 Fade-to-black at mission end not happening
-----------------------------------------------------------------------------
F. INTERNAL ENGINEERING BACKLOG (nothing to test)
-----------------------------------------------------------------------------
#14 Glass panel unreconstructed button handler backlog (census)
#17 Cavern upper-butte halves lack a collision stream (authenticity TBD)
#21 Weapon recharge arc dark after eng-page use (symptom trail -> #62)
#28 Confirm the vital-subsystem-crit death path can fire (machinery in)
#30 Weapon "unavailable" (level-7) display verification + accessor cleanup
#63 PlayerStatus reads raw 1995 offsets (databinding trap, dormant)
#64 damageZone shadow field (half-fixed; consequences disarmed)
#65 HandleMessage non-virtual across 10 classes (latent dispatch trap)
#72 BTLoadPump swallows WM_QUIT during load (latent)
#107 Destroyed HeatSink annunciates nothing (no lamp for condition 0)
#117 KB note: per-chassis cockpit bounce was deliberate (Ronin) -- FYI
=============================================================================
End of list. 59 open issues total:
12 verify (2 headliners), 17 reproduce, 3 era questions, 3 design calls,
14 known gaps, 11 internal.
35 issues closed 2026-08-10 on Oracle's annotated sweep.
=============================================================================
+9 -12
View File
@@ -2104,19 +2104,7 @@ static unsigned long
if (svga == NULL) svga = static_cast<SVGA16*>(p->graphicsDisplay); if (svga == NULL) svga = static_cast<SVGA16*>(p->graphicsDisplay);
} }
if (svga != NULL && combined != 0) if (svga != NULL && combined != 0)
{
token ^= svga->PlaneChecksum(combined); token ^= svga->PlaneChecksum(combined);
// PALETTE-ANIMATED content (2026-08-10): a palette-expanding window
// (the radar; MFDs under BT_GLASS_MFD_PAL) changes colour with ZERO
// pixel writes -- the ColorMapper family (armor rosette tints, the
// adpal/adpal2 damage flash) writes CLUT entries. Fold the palette
// write generation in so those changes repaint; mono-tint windows
// don't read the palette and keep their pixel-only token.
static int sPalTok = -1;
if (sPalTok < 0) sPalTok = getenv("BT_GLASS_MFD_PAL") ? 1 : 0;
if (w->monoTint < 0 || sPalTok)
token = (token ^ svga->PaletteGeneration()) * 16777619UL;
}
} }
// Lamps: each button's RENDERED brightness + held/latched, so a flash toggle or a // Lamps: each button's RENDERED brightness + held/latched, so a flash toggle or a
@@ -2133,12 +2121,21 @@ static unsigned long
return token; return token;
} }
// [rio] health printer (L4RIO.cpp): the serial poll path only COUNTS (kept
// I/O-free by design; the 2026-08-10 wedges once blamed on prints there were
// really the USB-serial adapter hanging an unbounded overlapped write --
// see PCSerialPacket::SendPacket); the report rides this tick, which
// already streams [glassperf] every second.
extern "C" void BTRioHealthReport();
void void
BTGlassPanels_Tick() BTGlassPanels_Tick()
{ {
if (gWinCount == 0) if (gWinCount == 0)
return; return;
BTRioHealthReport();
static unsigned long sLastPaint = 0; static unsigned long sLastPaint = 0;
unsigned long now = GetTickCount(); unsigned long now = GetTickCount();
if (now - sLastPaint < (unsigned long)RepaintMilliseconds) if (now - sLastPaint < (unsigned long)RepaintMilliseconds)
+49 -4
View File
@@ -410,6 +410,20 @@ int PCSerialPacket::ReceivePacket(BYTE *destPtr) // returns len (zero if none)
// Returns: // Returns:
// void // void
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
//
// Serial-write health (see the bounded wait below): after a write timeout
// the link is SICK for 3 s and writes fast-drop -- one stall, not one per
// poll. BTPcspakTxTimeouts feeds the [rio] telemetry.
//
static unsigned gPcspakTxTimeout = 0;
static DWORD gPcspakTxSickUntil = 0;
extern "C" unsigned
BTPcspakTxTimeouts()
{
return gPcspakTxTimeout;
}
void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet
{ {
//-------------------------------------------------------------- //--------------------------------------------------------------
@@ -418,6 +432,11 @@ void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet
if(!enabled) //if disabled, don't even try if(!enabled) //if disabled, don't even try
return; return;
if (gPcspakTxSickUntil != 0 && GetTickCount() < gPcspakTxSickUntil)
{
return; // adapter sick: drop, don't stall
}
BYTE cmd = *srcPtr; //get the command byte BYTE cmd = *srcPtr; //get the command byte
cmd &= 0x7F; //remove MSB cmd &= 0x7F; //remove MSB
int length = txLengthPtr[cmd]; //get the length in al int length = txLengthPtr[cmd]; //get the length in al
@@ -459,11 +478,37 @@ void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet
return; return;
} }
} }
DWORD bytesWritten; //
GetOverlappedResult(hComm, &overlapSend, &bytesWritten, TRUE); // BOUNDED completion wait (2026-08-10, ALPHA-MR field minidump). The
if(bytesWritten != length + 1) // RIO's "COM1" is a USB-serial adapter; when its USB link drops with a
// write in flight the overlapped write NEVER completes, and the original
// GetOverlappedResult(..., TRUE) parked the whole game at 0 CPU forever
// (cdb-proven: NtWaitForSingleObject <- GetOverlappedResult <-
// SendPacket <- RIO ctor). Now: 500 ms bound (9600 baud needs ~21 ms),
// CancelIo on expiry, and a 3 s SICK window that fast-drops writes so a
// dead adapter costs one stall, not one per 50 ms poll -- the game stays
// at frame rate with a degraded RIO and the keyboard fallback alive.
// gPcspakTxTimeout feeds the [rio] health line (txTO=).
//
DWORD bytesWritten = 0;
DWORD wait_result = WaitForSingleObject(overlapSend.hEvent, 500);
if (wait_result == WAIT_OBJECT_0)
{ {
DEBUG_STREAM << "Not all data was written to serial port!\n" << std::flush; GetOverlappedResult(hComm, &overlapSend, &bytesWritten, FALSE);
gPcspakTxSickUntil = 0;
if (bytesWritten != (DWORD)(length + 1))
{
DEBUG_STREAM << "Not all data was written to serial port!\n"
<< std::flush;
}
}
else
{
// No print here -- serial-path discipline; the counter surfaces in
// the [rio] health line (txTO=) from the glass-tick printer.
CancelIo(hComm);
++gPcspakTxTimeout;
gPcspakTxSickUntil = GetTickCount() + 3000;
} }
delete buffer; delete buffer;
} }
+5 -1
View File
@@ -25,7 +25,11 @@ static LRESULT CALLBACK
return 0; return 0;
case WM_CLOSE: case WM_CLOSE:
ShowWindow(window, SW_HIDE); // hide only; the renderer owns it // Minimize, never hide (#71): SW_HIDE left no way back short of a
// restart. A minimized window keeps its taskbar button (both framed
// and ,noframe styles), so the player can recover the display. The
// renderer still owns the window either way.
ShowWindow(window, SW_MINIMIZE);
return 0; return 0;
} }
return DefWindowProcW(window, message, wparam, lparam); return DefWindowProcW(window, message, wparam, lparam);
+148
View File
@@ -1240,6 +1240,105 @@ RIO::~RIO()
Check_Fpu(); Check_Fpu();
} }
//
// [rio] telemetry state (see the tracking block in GetNextEvent). File-scope
// statics on purpose: one hardware RIO per process, no header/layout churn --
// and the PRINTER (BTRioHealthReport, called from the glass tick) reads ONLY
// these statics, never RIO members, so it needs no instance and touches
// nothing the serial layer locks.
//
static int gRioPresent = 0;
static unsigned gRioReqCount = 0;
static unsigned gRioRepCount = 0;
static unsigned gRioBtnCount = 0;
static int gRioReqPending = 0;
static DWORD gRioLastReqTick = 0;
static DWORD gRioLastReplyTick = 0;
static int gRioStallActive = 0;
static DWORD gRioStallStart = 0;
static unsigned gRioStallBtnBase = 0;
static unsigned gRioStallCount = 0;
static DWORD gRioLastStallMs = 0;
static unsigned gRioLastStallBtns = 0;
static unsigned gRioLineErrMirror = 0;
static unsigned gRioOverrunMirror = 0;
static unsigned gRioAbandonMirror = 0;
static unsigned gRioRRetryMirror = 0;
static unsigned gRioRAbandonMirror = 0;
static unsigned gRioRFullMirror = 0;
extern "C" unsigned BTPcspakTxTimeouts(); // L4PCSPAK.cpp: bounded-write stat
//
// The printer half -- called from BTGlassPanels_Tick beside the [glassperf]
// report (a stream site that prints every second without incident), NEVER
// from the serial path. Edge lines land at the tick's ~1 s granularity,
// ample for the multi-second field dropouts this exists to catch.
// BT_RIO_LOG=0 opts out; silent when no hardware RIO ever came up.
//
extern "C" void
BTRioHealthReport()
{
static int sLog = -1;
static DWORD sLastLine = 0;
static unsigned sSeenStalls = 0;
static int sSawActive = 0;
if (!gRioPresent)
{
return;
}
if (sLog < 0)
{
const char *e = getenv("BT_RIO_LOG");
sLog = (e != 0 && *e == '0') ? 0 : 1;
}
if (!sLog)
{
return;
}
DWORD now = GetTickCount();
if (gRioStallActive && !sSawActive)
{
sSawActive = 1;
DEBUG_STREAM << "[rio] STALL analog silent "
<< (now - gRioStallStart) << "ms and counting req="
<< gRioReqCount << " rep=" << gRioRepCount
<< "\n" << std::flush;
}
else if (!gRioStallActive && sSawActive)
{
sSawActive = 0;
}
if (sSeenStalls != gRioStallCount)
{
sSeenStalls = gRioStallCount;
DEBUG_STREAM << "[rio] RECOVER stall#" << gRioStallCount
<< " lasted " << gRioLastStallMs
<< "ms btnDuring=" << gRioLastStallBtns
<< "\n" << std::flush;
}
if (now - sLastLine >= 5000)
{
sLastLine = now;
DEBUG_STREAM << "[rio] req=" << gRioReqCount
<< " rep=" << gRioRepCount
<< " btn=" << gRioBtnCount
<< " age=" << (gRioLastReplyTick != 0
? (now - gRioLastReplyTick) : 0)
<< "ms stalls=" << gRioStallCount
<< " txTO=" << BTPcspakTxTimeouts()
<< " lineErr=" << gRioLineErrMirror
<< " overrun=" << gRioOverrunMirror
<< " abandon=" << gRioAbandonMirror
<< " rRetry=" << gRioRRetryMirror
<< " rAbandon=" << gRioRAbandonMirror
<< " rFull=" << gRioRFullMirror
<< "\n" << std::flush;
}
}
Logical Logical
RIO::TestInstance() const RIO::TestInstance() const
{ {
@@ -1264,6 +1363,36 @@ Logical
Logical reply, looping; Logical reply, looping;
Word errors; Word errors;
//
// [rio] link-health TRACKING -- pure arithmetic, no I/O here by design.
// CORRECTED HISTORY (2026-08-10): the boot wedges first blamed on prints
// from this path were actually the USB-serial adapter dropping its link
// with a write in flight -- the unbounded GetOverlappedResult in
// PCSerialPacket::SendPacket parked the game (minidump-proven; the
// print-deadlock A/B was confounded by the adapter behaving that boot).
// The count-here / print-from-the-glass-tick split stays anyway: it
// keeps this hot polled path I/O-free and the printer at one known
// stream site (BTRioHealthReport below, beside [glassperf]).
//
if (operational)
{
DWORD now = GetTickCount();
if (!gRioStallActive && gRioReqPending
&& now - gRioLastReqTick > 500)
{
gRioStallActive = 1;
gRioStallStart = gRioLastReqTick;
gRioStallBtnBase = gRioBtnCount;
}
gRioPresent = 1;
gRioLineErrMirror = (unsigned) lineErrorCount;
gRioOverrunMirror = (unsigned) overrunCount;
gRioAbandonMirror = (unsigned) abandonCount;
gRioRRetryMirror = (unsigned) remoteRetryCount;
gRioRAbandonMirror = (unsigned) remoteAbandonCount;
gRioRFullMirror = (unsigned) remoteFullBufferCount;
}
//PCSPAKState(this); //PCSPAKState(this);
// //
//cout << flush; //cout << flush;
@@ -1377,6 +1506,20 @@ Logical
break; break;
case AnalogReply: case AnalogReply:
++gRioRepCount;
gRioReqPending = 0;
gRioLastReplyTick = GetTickCount();
if (gRioStallActive)
{
// The reply that ended the drought -- record only; the
// glass-tick printer emits the RECOVER line. btnDuring
// is the field theory's verdict: >0 on every recover =
// button traffic resyncs the link; 0 = it self-heals.
gRioStallActive = 0;
++gRioStallCount;
gRioLastStallMs = gRioLastReplyTick - gRioStallStart;
gRioLastStallBtns = gRioBtnCount - gRioStallBtnBase;
}
destination->Type = RIO::AnalogEvent; destination->Type = RIO::AnalogEvent;
// //
// NOTE: no data is sent in this packet. // NOTE: no data is sent in this packet.
@@ -1423,11 +1566,13 @@ Logical
break; break;
case ButtonPressed: case ButtonPressed:
++gRioBtnCount;
destination->Type = RIO::ButtonPressedEvent; destination->Type = RIO::ButtonPressedEvent;
destination->Data.Unit = receive_buffer[1]; destination->Data.Unit = receive_buffer[1];
break; break;
case ButtonReleased: case ButtonReleased:
++gRioBtnCount;
destination->Type = RIO::ButtonReleasedEvent; destination->Type = RIO::ButtonReleasedEvent;
destination->Data.Unit = receive_buffer[1]; destination->Data.Unit = receive_buffer[1];
break; break;
@@ -1568,6 +1713,9 @@ void
if (operational && !TestModeActive) if (operational && !TestModeActive)
{ {
SendPacket((Byte *) request_analog_string); SendPacket((Byte *) request_analog_string);
++gRioReqCount;
gRioReqPending = 1;
gRioLastReqTick = GetTickCount();
} }
Check_Fpu(); Check_Fpu();
} }
-23
View File
@@ -5445,7 +5445,6 @@ SVGA16::SVGA16(
BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index); BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index);
for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw
mDevSurfaceTex[_i] = NULL; mDevSurfaceTex[_i] = NULL;
paletteGeneration = 0; // GLASS dirty-skip palette tracking
//STUBBED: VIDEO RB 1/15/07 //STUBBED: VIDEO RB 1/15/07
# if defined(DEBUG) # if defined(DEBUG)
Tell("SVGA16::SVGA16()\n"); Tell("SVGA16::SVGA16()\n");
@@ -7191,7 +7190,6 @@ void
svga_palette->paletteData.Valid = True; svga_palette->paletteData.Valid = True;
svga_palette->modified = True; svga_palette->modified = True;
((SVGA16 *) graphicsDisplay)->paletteGeneration++; // glass dirty-skip (full rebuild)
Check_Fpu(); Check_Fpu();
} }
@@ -7236,13 +7234,6 @@ void
SVGA16Palette SVGA16Palette
*svga_palette(&((SVGA16 *) graphicsDisplay)->palette[paletteID]); *svga_palette(&((SVGA16 *) graphicsDisplay)->palette[paletteID]);
// GLASS dirty-skip: track whether this write CHANGES anything -- the
// ColorMapper family calls this every Execute (the adpal/adpal2 flash
// alternates palettes even at zero damage, usually writing identical RGB),
// so bump the generation only on a real change or every glass window that
// palette-expands would repaint every pump for nothing.
int palette_changed = 0;
//------------------------------------------- //-------------------------------------------
// If any of the ...TransparentZero modes are used, // If any of the ...TransparentZero modes are used,
// leave color zero undefined for this bit group by // leave color zero undefined for this bit group by
@@ -7284,31 +7275,21 @@ void
{ {
case RedChannel: case RedChannel:
case RedChannelTransparentZero: case RedChannelTransparentZero:
if (destination_triplet->Red != source_triplet->Red)
palette_changed = 1;
destination_triplet->Red = source_triplet->Red; destination_triplet->Red = source_triplet->Red;
break; break;
case GreenChannel: case GreenChannel:
case GreenChannelTransparentZero: case GreenChannelTransparentZero:
if (destination_triplet->Green != source_triplet->Green)
palette_changed = 1;
destination_triplet->Green = source_triplet->Green; destination_triplet->Green = source_triplet->Green;
break; break;
case BlueChannel: case BlueChannel:
case BlueChannelTransparentZero: case BlueChannelTransparentZero:
if (destination_triplet->Blue != source_triplet->Blue)
palette_changed = 1;
destination_triplet->Blue = source_triplet->Blue; destination_triplet->Blue = source_triplet->Blue;
break; break;
case AllChannels: case AllChannels:
case AllChannelsTransparentZero: case AllChannelsTransparentZero:
if (destination_triplet->Red != source_triplet->Red
|| destination_triplet->Green != source_triplet->Green
|| destination_triplet->Blue != source_triplet->Blue)
palette_changed = 1;
*destination_triplet = *source_triplet; *destination_triplet = *source_triplet;
break; break;
} }
@@ -7329,8 +7310,6 @@ void
svga_palette->paletteData.Valid = True; svga_palette->paletteData.Valid = True;
svga_palette->modified = True; svga_palette->modified = True;
if (palette_changed)
((SVGA16 *) graphicsDisplay)->paletteGeneration++; // glass dirty-skip
Check_Fpu(); Check_Fpu();
} }
@@ -7434,7 +7413,6 @@ void
svga_palette->paletteData.Valid = True; svga_palette->paletteData.Valid = True;
svga_palette->modified = True; svga_palette->modified = True;
((SVGA16 *) graphicsDisplay)->paletteGeneration++; // glass dirty-skip (full rebuild)
# if defined(TESTPALETTE) # if defined(TESTPALETTE)
std::cout << "L4GraphicsPort::BuildAuxiliaryPalette for port " << std::cout << "L4GraphicsPort::BuildAuxiliaryPalette for port " <<
@@ -7545,7 +7523,6 @@ void
svga_palette->paletteData.Valid = True; svga_palette->paletteData.Valid = True;
svga_palette->modified = True; svga_palette->modified = True;
((SVGA16 *) graphicsDisplay)->paletteGeneration++; // glass dirty-skip (full rebuild)
Check_Fpu(); Check_Fpu();
} }
-14
View File
@@ -382,15 +382,6 @@ protected:
SVGA16Palette SVGA16Palette
palette[PaletteCount]; palette[PaletteCount];
// GLASS dirty-skip (2026-08-10): bumped by the palette writers
// (L4GraphicsPort::BuildSecondaryColor on a REAL entry change; the full
// palette rebuilds unconditionally) so the glass repaint token can see
// palette-only animation -- the ColorMapper family (armor rosette tints,
// damage flash) changes COLORS with zero pixel writes, which the pixel
// checksum alone can never catch.
unsigned long
paletteGeneration;
private: private:
int NUMGAUGEWINDOWS; int NUMGAUGEWINDOWS;
HWND *gaugeWindows; HWND *gaugeWindows;
@@ -434,11 +425,6 @@ public:
// glass repaint pump re-blit ONLY the windows whose plane actually changed // glass repaint pump re-blit ONLY the windows whose plane actually changed
// (idle MFDs / static panels skip; the sweeping radar keeps updating). // (idle MFDs / static panels skip; the sweeping radar keeps updating).
unsigned long PlaneChecksum(int mask) const; unsigned long PlaneChecksum(int mask) const;
// GLASS dirty-skip: the palette write generation (see paletteGeneration).
// Folded into the repaint token of palette-expanding windows so ColorMapper
// palette animation (armor tints / damage flash) repaints without pixel churn.
unsigned long PaletteGeneration() const { return paletteGeneration; }
}; };
//######################################################################## //########################################################################
+32
View File
@@ -234,6 +234,36 @@ static LONG WINAPI
return EXCEPTION_EXECUTE_HANDLER; // die (after the evidence is out) return EXCEPTION_EXECUTE_HANDLER; // die (after the evidence is out)
} }
//
// lastrun_<stem>.txt clean-exit markers (#157, 2026-08-10): the launch record
// only ever said "exe reached WinMain", so a night that ended with the player
// closing the menu was indistinguishable from a silent death -- and the bat's
// sign-off taught players to report normal quits as crashes. The deliberate
// exit paths stamp the record themselves now, so the file answers "crash or
// quit?" on its own. Name captured at breadcrumb time (cwd is content\ by
// then -- the same file the bats bracket).
//
static char gBTLastrunName[MAX_PATH] = "";
void
BTLastrunNote(const char *note)
{
if (gBTLastrunName[0] == 0)
{
return;
}
SYSTEMTIME lt;
GetLocalTime(&lt);
std::ofstream f(gBTLastrunName, std::ios::out | std::ios::app);
if (f.is_open())
{
char stamp[64];
wsprintfA(stamp, "[%04d-%02d-%02d %02d:%02d:%02d] ",
lt.wYear, lt.wMonth, lt.wDay, lt.wHour, lt.wMinute, lt.wSecond);
f << stamp << note << "\n";
}
}
//===========================================================================// //===========================================================================//
// environ.ini -- the player's settings file (cwd = content\). One KEY=VALUE // environ.ini -- the player's settings file (cwd = content\). One KEY=VALUE
// per line; the real environment always WINS, so a launcher .bat or a shell // per line; the real environment always WINS, so a launcher .bat or a shell
@@ -833,6 +863,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
GetComputerNameA(machine, &machine_len); GetComputerNameA(machine, &machine_len);
char lastrun_name[MAX_PATH]; char lastrun_name[MAX_PATH];
wsprintfA(lastrun_name, "lastrun_%s.txt", logStem); wsprintfA(lastrun_name, "lastrun_%s.txt", logStem);
lstrcpynA(gBTLastrunName, lastrun_name, MAX_PATH);
std::ofstream breadcrumb(lastrun_name, std::ios::out | std::ios::app); std::ofstream breadcrumb(lastrun_name, std::ios::out | std::ios::app);
if (breadcrumb.is_open()) if (breadcrumb.is_open())
{ {
@@ -1209,6 +1240,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
BTFeLaunchSpec fe_spec; BTFeLaunchSpec fe_spec;
if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone) if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone)
{ {
BTLastrunNote("clean exit: player quit from the menu");
return 0; // quit from the menu return 0; // quit from the menu
} }
char fe_arguments[192]; char fe_arguments[192];
+12
View File
@@ -174,6 +174,8 @@ void
{ {
MarshalLog("window closed by the user -- exiting for real " MarshalLog("window closed by the user -- exiting for real "
"(no relaunch)"); "(no relaunch)");
extern void BTLastrunNote(const char *);
BTLastrunNote("clean exit: player closed the game window");
ExitProcess(0); ExitProcess(0);
} }
} }
@@ -257,6 +259,16 @@ void
if (CreateProcessW(exe_path, command_line, NULL, NULL, FALSE, if (CreateProcessW(exe_path, command_line, NULL, NULL, FALSE,
0, NULL, NULL, &startup, &process)) 0, NULL, NULL, &startup, &process))
{ {
// Hand the child our foreground right BEFORE we die (#157): Windows
// refuses SetForegroundWindow to a process spawned by an exiting
// background parent, so the round-end menu could open BEHIND the
// desktop clutter -- which is how a normal between-rounds relaunch
// read as "the game crashed" from the cockpit seat.
if (!AllowSetForegroundWindow(process.dwProcessId))
{
MarshalLog("foreground handoff denied -- the next window may "
"open behind");
}
CloseHandle(process.hThread); CloseHandle(process.hThread);
CloseHandle(process.hProcess); CloseHandle(process.hProcess);
} }
+1 -1
View File
@@ -412,7 +412,7 @@ static const CatalogEntry kVehicles[] =
{ "avatar", "Avatar" }, { "avatar", "Avatar" },
{ "ava1", "Satyr" }, { "ava1", "Satyr" },
{ "sunder", "Sunder" }, { "sunder", "Sunder" },
{ "snd1", "Sunder V1" }, { "snd1", "Denkou" }, // #69: the 4.x-era name (two era witnesses)
{ "vulture", "Vulture" }, { "vulture", "Vulture" },
{ "vul1", "Mad Dog" }, { "vul1", "Mad Dog" },
{ "lok2", "Mischief" }, { "lok2", "Mischief" },
+27 -1
View File
@@ -1032,6 +1032,12 @@ void
{ {
Check(message); Check(message);
// #150 -- the binary's ONCE-PER-DEATH latch (local_14 @0x4a0230 body,
// part_012.c:14625-14629): IsMechDestroyed() sampled AT ENTRY. The kill
// report posts only on the 0->1 edge (!destroyedAtEntry && destroyed-now);
// a later salvo of the killing volley takes the inflicted-only path.
const int destroyedAtEntry = IsMechDestroyed();
// MP DIAGNOSTIC (task #47): confirm the handler runs on the OWNING MASTER // MP DIAGNOSTIC (task #47): confirm the handler runs on the OWNING MASTER
// for a cross-pod (network-delivered) TakeDamage + what state it carries. // for a cross-pod (network-delivered) TakeDamage + what state it carries.
if (getenv("BT_MP_NET")) if (getenv("BT_MP_NET"))
@@ -1242,11 +1248,31 @@ void
// vital-wreck flag, and the loop-ENTRY zone. This retires // vital-wreck flag, and the loop-ENTRY zone. This retires
// BTPostDamageScore/BTPostKillScore -- see the btplayer.cpp tombstone. // BTPostDamageScore/BTPostKillScore -- see the btplayer.cpp tombstone.
// //
// #150 DOUBLE-KILL FIX, two binary-grounded gates (2026-08-10):
//
// (1) MASTER ONLY. The binary OPENS this handler with the assertion
// `if ((flags & 0xc) == 4) Fail("Replicant Mech recieving takedam...",
// MECH.CPP, 0x3da)` (part_012.c:14613-14616) -- in 1995 a replicant
// executing TakeDamage was a BUG, so the report tail could only ever
// run on the victim's master, once. This port deliberately applies
// damage on replicants too (local visual responsiveness), so the tail
// ALSO ran there -- and the replicant's movement mode can't flip to
// wreck until the death record round-trips, so a multi-round killing
// volley posted a kill report PER ROUND inside that window. Measured:
// one death -> two type-2 arms 1ms apart, kills 0->2 (the field's
// "one kill counted twice", #150). Gate the REPORTS to the master;
// the local damage application stays.
//
// (2) ENTRY LATCH. The binary's newly-destroyed test is the 0->1 edge of
// IsMechDestroyed() across THIS handler call (local_14), not the
// movement-mode-derived deathBlastArmed -- on the master both agree,
// but the binary's form is the spec.
if (GetInstance() != ReplicantInstance)
{ {
extern void BTMechPostCombatReports(void *, void *, float, int, int, int); extern void BTMechPostCombatReports(void *, void *, float, int, int, int);
BTMechPostCombatReports((void *)this, (void *)message, BTMechPostCombatReports((void *)this, (void *)message,
(float)damageTally, zoneDestroyed, reportZone, (float)damageTally, zoneDestroyed, reportZone,
deathBlastArmed && IsMechDestroyed()); !destroyedAtEntry && IsMechDestroyed());
} }
} }
+3 -2
View File
@@ -93,8 +93,9 @@ if defined BT_STILL_RUNNING (
goto btwait goto btwait
) )
echo. echo.
echo The game has exited. If it closed unexpectedly, send the operator echo The game has exited. (Quitting from the menu lands here -- normal.)
echo BOTH of these from the content folder: echo If it CRASHED or would not start, send the operator BOTH of these
echo from the content folder:
echo content\lastrun_solo.txt (tiny -- send it EVEN IF there is echo content\lastrun_solo.txt (tiny -- send it EVEN IF there is
echo no solo_*.log at all: that combination echo no solo_*.log at all: that combination
echo is how we spot a blocked launch) echo is how we spot a blocked launch)
+4 -1
View File
@@ -97,7 +97,10 @@ if defined BT_STILL_RUNNING (
goto btwait goto btwait
) )
echo. echo.
echo The game has exited. If it closed unexpectedly, send the operator BOTH echo The game has exited.
echo (Normal exits land here too: round ends return everyone to the menu,
echo and closing the menu window quits -- neither is a crash.)
echo If it CRASHED or would not start, send the operator BOTH:
echo content\lastrun_steam.txt (tiny -- send it EVEN IF there is no echo content\lastrun_steam.txt (tiny -- send it EVEN IF there is no
echo steam_*.log at all: that is how we spot echo steam_*.log at all: that is how we spot
echo a launch blocked before it ran) echo a launch blocked before it ran)
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# #150/#151: count type-2 arm executions per kill + measure the eject score arithmetic
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3
rm -f sc_a.log sc_b.log sc_r.log
mkdir -p /c/git/bt411/scratchpad/night14/mlbak && mv matchlog_*.txt /c/git/bt411/scratchpad/night14/mlbak/ 2>/dev/null
bt_expert_egg MP.EGG SC.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SC.EGG
# B: unarmed slow target, respawns on death
( export BT_GOTO=enemy BT_GOTO_STOP=80 BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 BT_MP_LOG=1
bt_launch sc_b.log SC.EGG 0x0C -net 1601 )
sleep 2
# A: kills B with autofire, then panic-ejects late
( export BT_GOTO=enemy BT_GOTO_STOP=100 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4
export BT_EJECT_AT=9000
export BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 BT_MP_LOG=1
bt_launch sc_a.log SC.EGG 0x03 -net 1501 )
sleep 5
python ../tools/btconsole.py SC.EGG 127.0.0.1:1501 127.0.0.1:1601 > sc_r.log 2>&1 &
R=$!; sleep 300; kill $R 2>/dev/null; sleep 2
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1
echo RESULT
echo "--- B deaths (kills A should have earned) ---"
grep -ac "Mech::Reset" sc_b.log
echo "--- A: kill-arm executions + score sequence (matchlog) ---"
grep -ah "SCORE\|KILL\|EJECT\|DEATH" matchlog_*.txt 2>/dev/null | head -25
echo "--- A: scorepost receipts (producer side) ---"
grep -a "scorepost" sc_a.log | head -10
grep -a "scorepost" sc_b.log | head -10
+26
View File
@@ -0,0 +1,26 @@
# Hand-poll the RIO board over COM1 -- no game involved. Wire format per
# PCSerialPacket::SendPacket: [cmd][checksum = sum & 0x7F].
$p = New-Object System.IO.Ports.SerialPort COM1,9600,None,8,One
$p.DtrEnable = $true
$p.RtsEnable = $true
$p.ReadTimeout = 200
$p.Open()
'PORT OPEN CTS=' + $p.CtsHolding + ' DSR=' + $p.DsrHolding
function Send-Cmd([byte[]]$bytes, [string]$name) {
$p.DiscardInBuffer()
$p.Write($bytes, 0, $bytes.Length)
Start-Sleep -Milliseconds 700
$got = @()
while ($p.BytesToRead -gt 0) { $got += [byte]$p.ReadByte() }
$hex = ($got | ForEach-Object { $_.ToString('X2') }) -join ' '
"$name -> $($got.Count) byte(s): $hex"
}
Send-Cmd @([byte]0x80, [byte]0x00) 'CHECK 0x80'
Send-Cmd @([byte]0x81, [byte]0x01) 'VERSION 0x81'
Send-Cmd @([byte]0x82, [byte]0x02) 'ANALOG 0x82'
Start-Sleep -Seconds 2
Send-Cmd @([byte]0x82, [byte]0x02) 'ANALOG again'
$p.Close()
'DONE'