Author SHA1 Message Date
CydandClaude Opus 4.8 ee84e14f25 PPC scramble: animate it as a collapse -> hold-roll+shake -> recover -> lock
Reworks the flat per-row shear into the authentic sync-loss-and-relock
transition the playtesters described, on both render paths (surround
DrawDevSurface + glass ExpandPlaneToBGRA, native + rotated radar):

  1. COLLAPSE -- the image squeezes to a thin centred vertical line.
  2. HOLD (~0.8s, the window the card held the detuned CRTC) -- the line's
     content scrolls WILDLY fast and decelerates, plus a violent per-frame
     SHAKE (LCG jitter: vertical row bounce + horizontal scroll jitter).
  3. RECOVERY (~0.5s) -- the line broadens back to full while the scroll,
     tear and shake settle to zero.
  4. LOCK -- clean full display.

Mechanism: FunkyVideo(on, dur) records start+hold; ScrambleParams is a
two-phase envelope (hold: deep collapse + decelerating scroll = integral of
v(t)=rollMax*(1-t/hold); recovery: smoothstep broaden + settle).  The read
loops map the source through it -- `scale` sets a black-bordered collapse
band, `rollOff` SCROLLS (wraps) within it, `shear` is a per-row diagonal,
`shakeRow` bounces the source row.  The SVGA16 owns the full hold+recovery
clock, so FunkyVideo(False) is a no-op (the card restoring sync is where the
recovery begins) -- the recovery isn't cut off when the 0.8s latch clears.

Tunable by eye (pod-monitor PLL look isn't recoverable from the binary):
BT_SCRAMBLE_COLLAPSE (0.03), _ROLL (9000 px/s), _SHAKE (10 px), _SHEAR (3),
_DUR (hold), _RECOVER (0.5s).  BT_SCRAMBLE_TEST loops the transition for
tuning; BT_SCRAMBLE_CYCLE loops it stepping the roll speed.

Verified: Release links clean; surround loops the effect at slowed + true
full speed, all secondaries + radar collapse/roll/shake/recover together,
main view clean, no crash.  SHIPPING with these defaults for playtester
feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 17:19:19 -05:00
CydandClaude Opus 4.8 74a0db4edd PPC hit scrambles the secondary displays (phase-14, all three work items)
Restores the PPC's authentic secondary-display effect: an EnergyDamageType hit
scrambles every secondary cockpit display for 0.8s while the main out-the-window
view stays clean.  Was fully specced (phase-14) with the engine half already
present under the original VWE names; only the trigger and the visual were
missing (the visual STUBBED since 2007).

A -- TRIGGER (game/reconstructed/mech.cpp): in TakeDamageMessageHandler, at the
  binary's @0x4a03f3 position (after the cylinder resolve, before the burst
  loop, so ONCE per damage message) fire
  GetGaugeRenderer()->SpecialEffect(scrambleVideo, damageType*0.2f) on
  EnergyDamageType (==4).  That type is authored on exactly the 14 PPC/ERPPC
  records, so the branch is structurally PPC-exclusive -- no weapon-class check.
  Duration DERIVED from the ordinal (4*0.2==0.8s), not a literal.  BT_DMG_LOG
  prints [ppc-scramble].

B -- VISUAL (SVGA16::FunkyVideo, was the 2007 stub): FunkyVideo now arms
  scrambleActive; new SVGA16::ScrambleRowShift is a per-source-row horizontal
  shear+roll, read by BOTH DrawDevSurface (surround/dock) and ExpandPlaneToBGRA
  (glass windows, native + rotated radar), so all secondary surfaces shear
  together in source space and the main 3D view (separate timing chain) is
  untouched -- the modern stand-in for the VGA CRTC Horizontal-Total detune.
  Tunable: BT_SCRAMBLE_SHEAR (px/line, def 4), BT_SCRAMBLE_ROLL (px/sec, def
  220); BT_SCRAMBLE_TEST=1 forces it on for tuning by eye.

C -- NON-STACKING LATCH (L4GaugeRenderer::SpecialEffect): ignore the re-arm while
  scrambleVideoFlag is set (matches the binary's `modified` latch) -- a second
  PPC during the window no longer extends it.  Was a divergence.

Verified: Release links clean; surround boots + runs with the effect forced on,
every secondary MFD + the radar shear while the out-the-window view stays clean,
no crash (screenshot).  Open (for the playtesters who filed the report): live
PPC-fire confirmation + by-eye shear tuning -- k/roll are not recoverable from
the binary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 16:19:10 -05:00
CydandClaude Opus 4.8 1164098d61 docs: PPC-hit CRTC sync-distortion finding + phase-14 port spec
Disassembly of BTL4OPT.EXE (2026-08-06) recovered the PPC's authentic
secondary-display effect: an EnergyDamageType (==4) hit calls the gauge
renderer's SpecialEffect(scrambleVideo, damageType*0.2f), which detunes the
VGA CRTC Horizontal Total by -9 for 0.8s -- every secondary cockpit display
loses horizontal sync ("looks like the CRTs are being degaussed"), the main
VPX view is untouched.  damageType 4 is authored on exactly the 14 PPC/ERPPC
records, so the branch is structurally PPC-exclusive.

  - phases/phase-14-ppc-sync-distortion.md: the port spec (trigger + visual +
    fidelity constraints + verification).
  - context/gauges-hud.md: full disasm chain + the FlashPalette non-confusion.
  - context/combat-damage.md: the damageType==4 branch in the damage handler.

NOT YET IMPLEMENTED -- this commit is the spec; the effect is the next work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 16:01:47 -05:00
24 changed files with 664 additions and 1050 deletions
+10
View File
@@ -637,6 +637,16 @@ Benches: `scratchpad/night12/scorekill.sh` (cross-node kill: killer `kills=1 awa
victim respawns, death #1 single-cycle) + `scoreself.sh` (#134 negation). Collision-death tail
fallthrough is inspection-tier [T3] — shares the benched tail code; field wall-deaths exercise it.
**The handler's OTHER damageType branch — `damageType==4` (Energy) = the PPC cockpit-sync glitch
[T1, 2026-08-06].** Between the collision divert and the burst loop sits a second type test
@`0x4a03f3`: `cmp [esi+0x2c],4 / jne 0x4a0423`. On a match it calls the gauge renderer's vtable
slot 19 with `((float)damageType × 0.2, 0)` = `(0.8f, 0)`, which detunes the **VGA CRTC Horizontal
Total by 9** for 0.8 s — every secondary cockpit display loses horizontal sync, the main VPX view
is untouched. `EnergyDamageType` is authored on **exactly the 14 PPC/ERPPC records and nothing
else**, so this is structurally PPC-exclusive. It fires **once per damage message** (outside the
burst loop). Full chain + addresses: [[gauges-hud]] §"PPC HIT = a deliberate CRTC horizontal-sync
DETUNE"; port spec: `phases/phase-14-ppc-sync-distortion.md`. **Implemented 2026-08-06** (branch `ppc-sync-distortion`).
## (HISTORICAL — the gap as found 2026-07-29, superseded above) [T1]
The authored crit machinery exists and is reconstructed — `Mech__DamageZone::CriticalHit @0049ccc4`
(half the damage to armour, half to ONE critical subsystem chosen by `criticalWeight`, capped by
+84
View File
@@ -13,6 +13,7 @@ open_questions:
- "Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired"
- "Always-active msg-4 records IDENTIFIED 2026-07-20 (glass input audit): 0x2C = Reservoir InjectCoolant (the flush button), 0x2F/0x2E/0x2D/0x2B/0x2A/0x29 = Condenser1-6 MoveValve, 0x1A-0x1D = GeneratorA-D ToggleGeneratorOnOff (@0050fb90; wired 2026-07-25, powersub.cpp); plus 0x13 = Mech DuckRequest (CROUCH -- COMPLETE 2026-08-06, [[locomotion]]), 0x28 = Mech BalanceCoolant, 0x12/0x14 = ThermalSight/Searchlight toggles (searchlight visuals done 2026-08-05) -- see pod-hardware.md + docs/GLASS_COCKPIT.md; statuses re-swept 2026-08-06"
- "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)"
- "PPC `scrambleVideo` IMPLEMENTED 2026-08-06 (branch ppc-sync-distortion): a PPC hit scrambles every secondary display for 0.8 s (modern per-scanline shear stand-in for the CRTC Horizontal-Total detune). Trigger in Mech::TakeDamageMessageHandler (damageType==4), visual in SVGA16::FunkyVideo/ScrambleRowShift (DrawDevSurface + ExpandPlaneToBGRA), non-stacking latch in L4GaugeRenderer::SpecialEffect. Screenshot-verified; live PPC-fire confirmation + by-eye shear tuning (BT_SCRAMBLE_SHEAR/ROLL) left to playtesters. Spec: phases/phase-14-ppc-sync-distortion.md"
---
# Cockpit Gauges / MFD HUD
@@ -755,6 +756,89 @@ pooling fix) — an alarm that cannot acquire a source is silent.
⚠ The bench cannot confirm audibility: it runs with no audio device (`live=0
pooled=0`), so the control chain is verified but final playback is not.
## PPC HIT = a deliberate CRTC horizontal-sync DETUNE on every secondary display (2026-08-06) [T1 disasm-verified]
**✅ IMPLEMENTED 2026-08-06** (branch `ppc-sync-distortion`; screenshot-verified —
every secondary MFD + the radar shear together, the out-the-window view stays
clean). Trigger + visual + non-stacking latch — see
`phases/phase-14-ppc-sync-distortion.md` for the port details and the
`BT_SCRAMBLE_*` tuning envs. Reported by playtesters as "being hit by a PPC makes
it look like all of the secondary CRTs were being degaussed" — main (VPX) view
unaffected, PPC strikes only. Both observations are exactly what the binary does.
The disasm chain below is the ground truth the port was built from.
**The gate is the damage TYPE, and only the PPC has it.** A `BTL4.RES`
subsystem census gives `damageType` 4 (`EnergyDamageType`) = **14 records,
every one PPC or ERPPC**; everything else is Ballistic (16), Explosive (30),
Laser (78). So a branch keyed on type 4 is structurally PPC-exclusive.
The chain, on the **VICTIM's** machine (all `@` from `BTL4OPT.EXE`,
md5 `a97075bcb5634d13263e9ad5a2b96fd0`):
1. `Mech::TakeDamageMessageHandler` @`0x4a0230`, branch @**`0x4a03f3`** — sits
between the collision divert and the burst loop, so it runs **once per
damage message**, not per burst:
```
004a03f3 mov ecx,[esi+0x2c] ; damage.damageType
004a03f6 cmp ecx,4 ; EnergyDamageType
004a03f9 jne 0x4a0423 ; everything else -> burst loop
004a03fb mov eax,[0x4efc94] ; the global `application`
004a0400 mov eax,[eax+0x4c] ; -> gauge renderer
004a0405 je 0x4a0423 ; null-guarded
004a0407 fild dword [esi+0x2c] ; (float)damageType == 4.0
004a040a fld xword [0x4a0c08] ; long double 0.2
004a0410 fmulp st(1) ; => 0.8
004a041d call dword [edx+0x4c] ; vtable slot 19, args (0.8f, 0)
```
⚠ The duration is **derived, not constant**: `(float)damageType × 0.2`.
2. Gauge-renderer vtable @`0x51cebc`, slot 19 (`+0x4c`) = @**`0x46ffcc`**
(an `L4GaugeRenderer` method — the 0x46xxxx MUNGA_L4 range, so the
capability is shared-engine; BT is what wires it to Energy damage).
Second arg must be 0 (`sub eax,1; jae ret`). Body: `svga16 = this[+0x1c52c]`
(null-guarded — same member RP fetches for its `FlashPalette`), then
`this[+0x1c534] = 1` (active) and `this[+0x1c538] = now + 0.8 s` in ticks.
3. @`0x46d840` — thin wrapper, drops `this`, forwards the flag.
4. @**`0x47d76d`** — the payload, straight VGA CRTC I/O:
```
out(0x3D4,0x11); v=in(0x3D5); out(0x3D5, v & 0x7F) ; unlock CRTC regs 0-7
out(0x3D4,0x00) ; CRTC 0 = HORIZONTAL TOTAL
if (arg==0) { out(0x3D5, saved); modified=0; } ; restore
else if (!modified) { modified=1; saved=in(0x3D5);
out(0x3D5, saved-9); } ; <<< shorten the scanline
out(0x3D4,0x11); out(0x3D5, v) ; restore write-protect
```
Globals: `modified` @`0x4fe0fe`, `saved` @`0x4fe0ff`. The `modified` latch
makes it **idempotent** — overlapping PPC hits do NOT stack, and a second
hit does not re-save an already-detuned value.
5. @**`0x47003c`** (per frame): `if (active && now >= expiry) { active = 0;
SVGADistortSync(svga16, 0); }` — restores the saved Horizontal Total.
**Why it reads as a degauss, and why only the secondaries.** CRTC register 0 is
the character-clock count per scanline — it *is* the horizontal scan frequency.
9 drives every attached monitor's horizontal oscillator off frequency: the
image shears/rolls/wobbles until it re-locks, then snaps back 0.8 s later. All
six secondary displays are derived by the VDB from that one VGA's timing, so
they glitch **together**; the main view comes off the Division VPX card on an
independent timing chain and is untouched. No relay, no VDB register, no
palette work — the VDB just propagates a deliberately corrupted sync. (The
`LampTesla1/2/3` "solid-state relays" in `L4CTRL.HPP` are NOT involved and are
driven by nothing in the surviving tree.)
⚠ **Do not confuse this with the `SVGA16::FlashPalette` pixel-mask cycler**
(`flashRate`/`mask[4]`, ports `0x302/0x30A/0x312`). That machinery is linked and
its per-frame cycler runs in BT, but `FlashPalette` @`0x46d5f4` has **zero call
sites and zero address-of references** in `BTL4OPT.EXE` — BT never arms it.
**RP does**: `RPL4OPT.EXE` @`0x4addce` calls `FlashPalette(palette 1 =
SecondaryPalette, rate 2.0, masks {FF,BF,7F,3F})` from its gauge-renderer ctor —
hardware-assisted alarm blinking by masking off the top two pixel bits. Same
pods, so it is an easy source of cross-game misattribution.
Scope note: callers of vtable slot 19 were not exhaustively enumerated (virtual
dispatch); the PPC site was found via the three `application+0x4c` uses
(`0x4a03fb` here, `0x4cc3be` / `0x4d1559` unrelated). The low-level path IS
exhaustive — @`0x47d76d` has exactly one caller, and @`0x46d840` exactly two
(set @`0x47002b`, restore @`0x470076`).
## Key Relationships
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
+1 -33
View File
@@ -156,26 +156,6 @@ want bare. From then on those windows are frameless **and pinned** — with no c
nothing to drag them by, which is the point: the arrangement is finished. Delete the flag to get
the frame back.
### `monitor:<name|index>` — bind a window to a DISPLAY, not to pixels (2026-08-06) [T2 live]
Instead of an `x,y,w,h` rect, a line may name a display device:
```
Secondary / Radar=monitor:DISPLAY4,bare
VGA Port A=monitor:DISPLAY2,bare
```
The resolver (`ResolveMonitorSpec` + `MonScanProc`, `L4GLASSWIN`) matches the full device name OR
its tail, so `monitor:DISPLAY4` works without typing `\\.\`; a bare integer is a 0-based index in
enumeration order. The surface is CENTRED on that monitor. This is what makes a fixed rig
survivable: **desktop rects move** when a display is re-detected, a USB graphics adapter
re-enumerates or someone changes a resolution, and a pixel rect then silently puts a panel on the
wrong glass — a device binding does not. Required for [[pod-hardware]] §ALPHA-MR, where a
mis-bound window means a picture on the wrong CRT with no error anywhere.
`,bare` **implies frameless** (it used to need `,noframe` too, and a "bare" window came back
656×519 WITH a caption — a real bug found on the cab).
Options are comma-separated after the numbers and **unknown ones are ignored**, so an older build
reading a newer file loses the option but never the line — the `bindings.txt` grammar rule
applied here.
@@ -203,15 +183,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
the panels it has no `WM_TIMER` focus-throttle to worry about.
**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
`PlasmaDisplay` on that serial port), and the GLASS profile force-defaults it to `SCREEN` — so on
a rig with no marquee wired, leaving the variable unset is NOT enough; the profile fills it back
in and a plasma window lands on the cab's glass. The `NONE`/`OFF`/`0` spelling nulls the string
before the branch, so no external display is constructed at all. The boot banner reads
`GLASS (PadRIO; plasma off [L4PLASMA])` when it takes, and the absence of the
`[plasmawin] desktop plasma display up` line is the runtime proof.
**Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects;
`scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a
point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every
@@ -243,10 +214,7 @@ window.
- **World aspect** under the letterbox is the view rect's OWN aspect — the client no longer
enters into it.
- **`-fit`** (alias `-windowed-fullscreen`): borderless `WS_POPUP` over the monitor, canvas
letterboxed inside. Verified on a 3440×1440 ultrawide. **`BT_FIT=1` is the env spelling**
(2026-08-06) — same flag, settable from `environ.ini`, so a fixed rig keeps its borderless main
view however it is launched instead of depending on one launcher's command line. Targets
`MONITOR_DEFAULTTOPRIMARY`, i.e. the PRIMARY display. [[pod-hardware]] §ALPHA-MR
letterboxed inside. Verified on a 3440×1440 ultrawide.
**Ordering trap found live:** the letterbox flag was first set at device creation, but the
first `WM_SIZE` arrives BEFORE the device exists — a `-fit` boot logged `aspect=3.14` (the
+7 -135
View File
@@ -217,21 +217,12 @@ colour channel of one shared palettized framebuffer. Mechanism, end to end:
mono-tinted window on its own Windows display (see §MFD PANELS ON REAL HARDWARE), which is right
when every panel has its own output. **On splitter-wired glass it is wrong** — three monitors
would share one Windows display and each would show only its channel's share of a single tinted
image.
**`BT_POD_RGB=1` — the CHANNEL-COMPOSITE mode (built + field-verified 2026-08-06) [T2].** It
collapses the six panel windows into **three**: two VGA-port windows plus the radar. Each port
window composites its group's planes into ONE 640×480 BGRA image, each member ORed into its own
channel mask (`0x00FF0000` / `0x0000FF00` / `0x000000FF`), so the analog splitter downstream hands
each mono CRT exactly its picture. Grouping follows `L4GAUGE.CFG`: *VGA Port A* = Heat + Comm +
Mfd2, *VGA Port B* = Mfd1 + Mfd3; the radar keeps real colour on its own port.
`SVGA16::ExpandPlaneToBGRA` does the per-plane extraction, and `BT_POD_RGB` implies
`BT_POD_SURFACES`. Two companion gates came out of the bring-up: **`BT_POD_CHANMAP=<Port>=<colour>,…`**
remaps a surface's colour line when a cab's harness disagrees with the 1995 config (it did — see
§ALPHA-MR), and **`BT_POD_IDENT`** / **`BT_POD_CHANTEST`** paint identifying/cycling test patterns
so you can tell which physical CRT is which without trusting the desktop layout.
**SETTLED:** Nick's cart IS splitter-wired — its three 640×480 "displays" are VGA outputs, each
feeding a three-monitor splitter, and the composite is what lit the glass correctly.
image. Driving original splitter hardware needs a CHANNEL-COMPOSITE mode: extract three planes
into ONE 640x480 RGB image with pure (255,0,0)/(0,255,0)/(0,0,255) tints, one window per VGA
output. `SVGA16::ExpandPlaneToBGRA` already does the per-plane extraction with a tint, so the
composite is a small addition. OPEN: which way Nick's crash cart is wired (Windows shows three
separate 640x480 displays there, which suggests per-panel outputs via the Trigger 6 USB adapter,
not a splitter) — settle it by eye before building.
## MFD PANELS ON REAL HARDWARE — the bring-up path (2026-08-06) [T2 local / T4 on-pod]
Nick's crash cart (pod hardware + Chrome Remote Desktop on a burner account) is the first chance
@@ -258,7 +249,7 @@ desktops. Two pieces were added for the cab:
1. Probe: GPUs, every monitor's virtual-desktop rect, EDID make/model (identifies the original
panels), serial ports (the RIO board), session type, and a PROPOSED `glass_layout.cfg` that
assigns the six surfaces to the non-primary monitors top-to-bottom/left-to-right, centred.
2. Drop the cfg in the game's working directory; run with `BT_GLASS_PANELS=1
2. Drop the cfg in the game's working directory; run with `BT_GLASS=1 BT_GLASS_PANELS=1
BT_POD_SURFACES=1 BT_GLASS_LAYOUT=load` (`=save` to persist drags instead).
3. Read the receipts; re-assign titles to monitors in the cfg until each picture is on its panel.
Surface→panel roles: Heat MFD = upper left (coolant), Engineering = upper centre, Comm MFD =
@@ -271,125 +262,6 @@ at native size CENTRED, not scaled — a scale-to-fit option is the obvious foll
Chrome Remote Desktop holds the CONSOLE session (it should — an RDP session would get a virtual
display and light nothing); and the RIO serial input, which is a separate task from the displays.
## ALPHA-MR — the verified cart config, FROZEN (2026-08-06) [T2, field-verified by eye]
The bring-up above landed: real pictures on real pod glass, then user-confirmed correct
("ok this is correct"). What that took, and where it now lives permanently.
**The rig.** Win10 IoT Enterprise LTSC on the cart; Intel HD630 + a "Trigger 6 External Graphics"
USB adapter. Four console displays: **DISPLAY3 800×600 PRIMARY at 0,0** (main view), plus
**DISPLAY4 @800,122**, **DISPLAY2 @1440,123**, **DISPLAY1 @2080,124** (each 640×480). Physical
cab: five mono MFD CRTs (two left stacked, two right stacked, one centre loose on the desk) plus
the colour radar LCD in the yellow-button frame.
**The mapping** (`glass_layout.cfg`, `monitor:<name>` binding — see [[glass-cockpit]]):
```
Secondary / Radar = monitor:DISPLAY4,bare # colour LCD, yellow-button frame
VGA Port A = monitor:DISPLAY2,bare # RGB triple: Heat + Comm + Mfd2
VGA Port B = monitor:DISPLAY1,bare # RGB triple: Mfd1 + Mfd3
```
Three windows, not six — under `BT_POD_RGB` each VGA-port window carries three MFDs on its three
colour planes (§THE RGB SPLIT). Desktop-coordinate order says NOTHING about physical cab position:
the first assignment, made by sorting monitors left-to-right, was wrong. Only eyes on the glass
settle it.
**Two deviations from the 1995 config, both real, both this cab's wiring:**
- **`BT_POD_CHANMAP=Comm=blue,Heat=red`** — kills/deaths and coolant loops came up on each
other's panel. `L4GAUGE.CFG` has Comm on red and Heat on blue; THIS cab's splitter harness
wires them the other way. The env override remaps a port's colour channel without touching the
authentic config file. [T2]
- **`BT_GAUGE_SEC_ROT=0`** — the radar here is a LANDSCAPE LCD, so the 640×480 source needs no
rotation. The 1995 pod's 270° rotation existed because its secondary CRT was mounted PORTRAIT;
it is a mounting fact, not a format fact. (En route: 180° looked plausible and was upside down.)
**Frozen in `content/environ.ini`**, not in a launcher — `scratchpad/pod/podprofile.ini` is the
block, `scratchpad/pod/mergeprofile.ps1` merges it idempotently between markers. environ.ini is
read before anything touches the environment ([[build-and-run]]), and the real environment still
WINS, so a .bat can override any of it for a one-off. The cart's `runpod.bat` now sets only
`BT_LOG`/`BT_GLASS_LOG` and the `-egg`; `runpod_env.bat` keeps the all-inline version as the
fallback. Two gates were added for this:
- **`BT_FIT=1`** — the env spelling of `-fit`, so the borderless main view survives however the
game is started (shortcut, scheduled task, autostart) instead of riding on one launcher's
command line. It fits `MONITOR_DEFAULTTOPRIMARY` = DISPLAY3, which is the main-view panel.
- **`L4PLASMA=NONE`** (also `OFF`/`0`) — no marquee at all. Needed because the GLASS profile
force-defaults `L4PLASMA=SCREEN`, so simply leaving it unset still puts a desktop plasma window
on the pod's glass. The boot banner reports `plasma off [L4PLASMA]` when it takes.
Verified on build 4.11.813 with the launcher carrying none of it: `[boot] environ.ini: 9
setting(s) applied`, `[cockpit] -fit: borderless 800x600`, `[glasswin] radar rotation 0 (none)`,
and all three surfaces on their intended `\\.\DISPLAYn`.
**Surviving a NEW BUILD, not just a reboot.** Both frozen files live INSIDE the versioned install
(`content\environ.ini`, `content\glass_layout.cfg`) and NEITHER ships in the zip — environ.ini is
generated on first run. So extracting a new build gives a cab that comes up wrong with no error
anywhere. The masters therefore live at the stable `C:\bt411\` (`podprofile.ini`,
`glass_layout.cfg`, `podkit.ps1`), and **`setup_pod.bat` pushes them into the newest `BT411_*`
folder** — run it once after each extract. `runpod.bat` resolves the newest install and applies
the kit itself before launching, so the remote path needs no hand-editing per build. Re-tuning the
cab means editing the MASTER at `C:\bt411\`, not the install's copy: the apply overwrites it, on
purpose, so there is one place to look when a panel moves. Repo copies: `scratchpad/pod/`.
**The tester launchers work on the cab unchanged (verified 2026-08-06) [T2].** `play_solo.bat`,
`join.bat`, `join_lan.bat`, `play_steam.bat` and `joyconfig.bat` set `BT_PLATFORM=glass`,
`BT_START_INSIDE=1`, `BT_DEV_GAUGES=1` plus their own mode flag — and **not one pod key** between
them. So each inherits the whole rig from environ.ini without knowing the pod exists. This is the
argument against a separate pod-only settings file: a second file would need every launcher to opt
in, which is the shape of the "competing keymaps" bug that made glass the default profile.
Observed: `play_solo.bat` on the cart logs `environ.ini: 7 setting(s) applied` — 7 not 9, because
the bat sets `BT_PLATFORM` and `BT_START_INSIDE` itself and the real environment wins.
Steam MP needs the **client installed and logged in** (AppID 480/Spacewar, so any account, no
purchase); installed on the cart 2026-08-06. ⚠ **#68: a failed JOIN/HOST exits the process
SILENTLY** — "Steam not running" and a real crash look identical to a player.
### The HARDWARE RIO is live on the cart (2026-08-06) [T2]
`L4CONTROLS=RIO:COM1,KEYBOARD` in the frozen profile — the real cockpit board instead of PadRIO.
The whole stock path above the seam (mapper, lamps, streamed `.CTL` mappings) runs unchanged; the
109-mapping `L4` control table installs exactly as it does on a desktop. **This is independent of
the platform profile** — the cab keeps the GLASS display stack (panels + RGB split) and only the
input device changes, which is what the glass/PadRIO seam was built for. Do NOT reach for
`BT_PLATFORM=pod` to get it.
- **Wiring:** `RIO:COMn``\\.\COMn` at **9600 8N1** (`L4SERIAL`); bare `RIO` defaults to COM1.
Reads are non-blocking (`ReadIntervalTimeout=MAXDWORD`, totals 0), so an absent or dead board
cannot hang the game — it just goes quiet.
- **RIO and PAD are MUTUALLY EXCLUSIVE** — both assign `rioPointer` in the `L4CONTROLS` parser, so
the last token wins. Selecting the hardware RIO turns the XInput pad OFF on that machine.
`KEYBOARD` is safe alongside: it only sets `flags.keyboardExists` and never touches
`primaryControlType`.
- **The board's own self-test is the diagnostic.** `RIO::RIO` runs it (`perform_tests = True` by
default) and writes `content\FAILURE.LOG` pages `RIOBoardErrors` / `RIODeadLamps` / `RIOErrors`,
deleting them first so the file is always current. On the cart, reproducibly: **4 missing boards**
(`Slot=3:Address=0`, `3:2`, `4:0`, `5:0`) and **16 dead lamps** (`AuxUpperRight1-8`,
`AuxUpperCenter1`, `TeslaRelay1/3`, `PanicButton`, `IcomAmpEnableRelay`, `IcomIncRelay`,
`FloorEntry`, `undefined_0x17`) — the crash cart is a PARTIAL cab, and those are the boards it
does not have. **A specific 4-of-many inventory is itself the proof the link is good**: a dead
serial line reports the whole address space missing, not four slots.
- **Input reaching the mech** was seen live as `[ctrlmap] push stick x=0.0595238 y=0` with nobody
at the cab — the physical stick sitting just outside the 5% deadband. NB when the stick is
centred this reads `x=0`, which is indistinguishable from no data: a centred-stick zero proves
nothing either way. ⚠ **Not yet checked by hand:** that stick/throttle/pedal travel and the
physical buttons drive the mech correctly, and whether the cab's pots match the `Ranger`
calibration baked into `RIO::RIO` (`JoystickX -96..96`, `JoystickY -97..108`, `Throttle 0..800`,
pedals `0..470`). A mismatch shows up as drift or short travel, not as an error.
- The boot banner names the resolved device — `GLASS (hardware RIO; plasma off [L4PLASMA])`. It
used to hardcode "PadRIO", which is a lie on a wired cab and exactly the line you read to check.
-**THE COUPLING BUG this exposed (fixed 2026-08-06) [T2].** `BTGlassPanels_Create()` was called
ONLY from the end of the **PadRIO constructor** — the panels began life as the frames around the
on-screen RIO button banks, so "the buttons ride the device". Selecting the hardware RIO means
PadRIO is never constructed, so **every MFD window silently failed to appear**: the mission came
up on the main view with all the pod glass dark, and not one `[glasswin]` line in the log. The
MFD panels are a DISPLAY concern, so creation moved to `LBE4ControlsManager` after the
`L4CONTROLS` parse, where every device branch converges — plus a symmetric `BTGlassPanels_Destroy()`
in its destructor, because `~PadRIO` tore them down and `~RIO` knows nothing about them (windows
outliving the surfaces they blit would crash on the next mission cycle). Both calls are
idempotent (`Create` returns on `gWinCount != 0`), so the PadRIO path is unchanged and simply
arrives first.
**Working on the cart, remotely.** SSH over Tailscale lands in **session 0**, which has a dummy
"WinDisc" display and CANNOT see or enumerate session 1's windows — GUI work must go through
`schtasks /run /tn BT411Run` (task registered `/IT`), and window enumeration over SSH silently
returns nothing rather than failing. Kill `btl4.exe` BEFORE scp'ing a new exe or the file is
locked. The receipts in `podrun.log` are the remote eyes; a missing `DEBUG_STREAM` line in an
otherwise-logging run is real evidence that code path did not execute.
## 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
every checked control behavior:
-38
View File
@@ -9,10 +9,6 @@
#include "dxutils.h"
#ifdef BT_GLASS
#include "l4padrio.h"
#ifdef BT_GLASS
#include "l4glasswin.h" // the per-display MFD windows -- created below for
// EITHER RIO (they used to ride the PadRIO ctor)
#endif
#endif
//
@@ -869,29 +865,6 @@ LBE4ControlsManager::LBE4ControlsManager():
while (temp[0] != '\0');
}
//--------------------------------------------------
// The per-display MFD windows (2026-08-06)
//--------------------------------------------------
// These used to be created ONLY at the end of the PadRIO constructor,
// because they began life as the frames around the on-screen RIO button
// banks -- "the buttons ride the device". On a real cab that coupling is
// backwards: selecting the HARDWARE RIO means PadRIO is never built, so
// the MFD panels never appeared at all. Found on the crash cart, where
// the mission came up on the main view with every MFD dark.
//
// The windows are a DISPLAY concern, so create them here -- after the
// L4CONTROLS parse, where every device branch converges -- and let them
// exist whichever RIO won. In pod surface mode there are no on-screen
// buttons on them at all. Create() is idempotent (`gWinCount != 0`
// returns), so the PadRIO call site stays valid and simply arrives first.
//
#ifdef BT_GLASS
if (rioPointer != NULL && BTGlassPanelsActive())
{
BTGlassPanels_Create();
}
#endif
//--------------------------------------------------
// Clear all lamps (and reset analog I/O)
//--------------------------------------------------
@@ -1116,17 +1089,6 @@ LBE4ControlsManager::~LBE4ControlsManager()
rioPointer = NULL;
}
#ifdef BT_GLASS
//
// Symmetric with the Create above (2026-08-06). ~PadRIO tears the panels
// down itself, so this is a no-op there -- but the HARDWARE RIO's dtor
// knows nothing about them, and windows that outlive the surfaces they
// blit are a crash waiting for the next mission cycle. Destroy is safe
// if they were never created.
//
BTGlassPanels_Destroy();
#endif
Check_Fpu();
}
+12 -382
View File
@@ -123,10 +123,6 @@ struct GWin
const char *groupPort[3];
const char *groupAlt[3]; // the Eng<n> twin, or NULL
int groupCount;
char monitorName[40]; // the PHYSICAL monitor this window landed on
// (\.\DISPLAYn), stamped at creation; shown
// by BT_POD_IDENT so the cab can be mapped.
};
static GWin gWins[8];
@@ -257,26 +253,17 @@ static void
// (0x15 sits on the left column -- Cyd listed 0x10-0x14; move it if wanted.)
static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E };
// The SURFACE SIZE follows the rotation: a quarter turn (1 or 3) transposes
// the 640x480 source to 480x640 portrait -- the pod's rotated CRT -- while
// 0 (none) and 2 (180) keep it LANDSCAPE. Sizing this independently of the
// rotation squashed the picture into the wrong aspect on a landscape
// replacement panel (crash cart, 2026-08-06).
const int quarterTurn = (gRadarRot == 1 || gRadarRot == 3);
const int surfW = quarterTurn ? RadarSurfW : RadarSurfH; // 480 : 640
const int surfH = quarterTurn ? RadarSurfH : RadarSurfW; // 640 : 480
BTRioBankMetrics metrics;
BTRioBankMetricsFor(surfW, surfH, &metrics);
BTRioBankMetricsFor(RadarSurfW, RadarSurfH, &metrics);
BTRioBank bank;
BTRioBankLayout(BTRioBankRadar, 0, 0, surfW, surfH,
BTRioBankLayout(BTRioBankRadar, 0, 0, RadarSurfW, RadarSurfH,
0x10, 0x18, &metrics, bottomAddrs, &bank);
BTRioBankDump("Secondary / Radar", &bank);
int dx = -bank.boundsX, dy = -bank.boundsY;
AdoptBank(w, bank, ClrYellow, dx, dy);
SetRect(&w.surfaceRect, dx, dy, dx + surfW, dy + surfH);
SetRect(&w.surfaceRect, dx, dy, dx + RadarSurfW, dy + RadarSurfH);
w.clientW = bank.boundsW;
w.clientH = bank.boundsH;
}
@@ -439,116 +426,6 @@ static const char *layoutFileName = "glass_layout.cfg";
// The Flight Controls window (no surface at all) is not created.
// BT_POD_SURFACES=1 -> every display window goes bare
// glass_layout.cfg "<title>=x,y,bare" -> just that one (mixed rigs)
// BT_POD_CHANMAP -- per-cart channel override, e.g.
// BT_POD_CHANMAP=Comm=blue,Heat=red
// The 1995 config authors which colour line each surface rides (L4GAUGE.CFG),
// but a rebuilt cab can have its RGB splitter wired to different panels than
// the original -- on Nick's cart the coolant (Heat) and hot-box (Comm) screens
// came out swapped, which is a WIRING difference, not a software bug. Rather
// than edit authentic config, override the channel here, per rig.
// Returns 0/1/2 (red/green/blue) for a named port, or -1 if unmapped.
static int
PodChannelOverride(const char *portName)
{
static int loaded = 0;
static char names[8][20];
static int chans[8];
static int count = 0;
if (!loaded)
{
loaded = 1;
const char *e = getenv("BT_POD_CHANMAP");
while (e != NULL && *e && count < 8)
{
while (*e == ' ' || *e == ',') ++e;
const char *eq = strchr(e, '=');
if (eq == NULL) break;
int n = 0;
for (const char *p = e; p < eq && n < 19; ++p) names[count][n++] = *p;
names[count][n] = 0;
const char *v = eq + 1;
int ch = -1;
if (_strnicmp(v, "red", 3) == 0) ch = L4GraphicsPort::RedChannel;
else if (_strnicmp(v, "green", 5) == 0) ch = L4GraphicsPort::GreenChannel;
else if (_strnicmp(v, "blue", 4) == 0) ch = L4GraphicsPort::BlueChannel;
if (ch >= 0)
{
chans[count] = ch;
DEBUG_STREAM << "[glasswin] channel override: " << names[count]
<< " -> " << (ch == L4GraphicsPort::RedChannel ? "red"
: ch == L4GraphicsPort::GreenChannel ? "green" : "blue")
<< std::endl << std::flush;
++count;
}
const char *nx = strchr(v, ',');
if (nx == NULL) break;
e = nx + 1;
}
}
for (int i = 0; i < count; ++i)
if (portName != NULL && _stricmp(names[i], portName) == 0)
return chans[i];
return -1;
}
// The channel a surface actually rides on THIS rig: the override if given,
// else what the pod config authored.
static int
ResolvedChannelOf(const char *portName, L4GraphicsPort *port)
{
int ov = PodChannelOverride(portName);
if (ov >= 0)
return ov;
return (port != NULL) ? (port->GetEnableID() & 0x7F) : -1;
}
// BT_POD_IDENT=1 -- draw each surface's identity ON the glass (title, monitor,
// and in RGB mode the channel->surface map). The only reliable way to learn
// which physical cab panel a Windows display drives.
static int
PodIdentMode()
{
static int m = -1;
if (m < 0)
{
const char *e = getenv("BT_POD_IDENT");
m = (e != NULL && e[0] != 0 && e[0] != '0') ? 1 : 0;
}
return m;
}
// BT_POD_CHANTEST=<seconds> -- CHANNEL WALK, the instrument for mapping
// splitter-wired glass. On a cab, three mono CRTs share one VGA port through
// its R/G/B lines, so "which panel is which" cannot be read off Windows: the
// desktop sees ONE display for three monitors. This drives ONE channel at a
// time -- all-red, then all-green, then all-blue, N seconds each, with the
// channel name drawn huge -- so the answer is simply: watch which panel lights
// up, and when. Readable even over a rough livestream.
// Returns the dwell in seconds (0 = off).
static int
PodChanTestSeconds()
{
static int s = -1;
if (s < 0)
{
const char *e = getenv("BT_POD_CHANTEST");
s = (e != NULL && e[0] != 0) ? atoi(e) : 0;
if (s < 0) s = 0;
if (e != NULL && e[0] != 0 && s == 0) s = 3; // "1"/"on" -> 3s default
}
return s;
}
// Which channel is lit right now (0=red 1=green 2=blue), from the wall clock.
static int
PodChanTestPhase()
{
int dwell = PodChanTestSeconds();
if (dwell <= 0)
return -1;
return (int)((GetTickCount() / (DWORD)(dwell * 1000)) % 3);
}
// BT_POD_RGB=1 -- the pod's REAL video wiring (see pod-hardware.md §THE RGB
// SPLIT): one VGA port per window, its R/G/B analog lines split to three
// monochrome MFD monitors. Implies pod surface mode.
@@ -732,73 +609,6 @@ void
}
// Restore saved positions over the just-computed pod-faithful defaults. Called
//---------------------------------------------------------------------------
// MONITOR BINDING (2026-08-06) -- "monitor:<name-or-index>" instead of x,y.
//
// Absolute desktop coordinates do not survive a reboot on a pod: Windows
// renumbers the virtual desktop whenever adapters enumerate differently (a USB
// display adapter is especially prone), so a layout pinned to 800,122 can land
// on the wrong glass -- or off-screen -- after a power cycle. Binding to the
// MONITOR instead and resolving its rect at startup is stable across boots.
// Heat MFD=monitor:\.\DISPLAY4,bare (device name -- what the receipts print)
// Heat MFD=monitor:2,bare (index into the enumeration order)
// The surface is centred on that monitor; an exact-size panel fills it.
//---------------------------------------------------------------------------
struct MonScan { int index; const char *want; int wantIndex; RECT rect; int found; };
static BOOL CALLBACK
MonScanProc(HMONITOR mon, HDC, LPRECT, LPARAM param)
{
MonScan *sc = (MonScan *)param;
MONITORINFOEXA mi;
memset(&mi, 0, sizeof(mi));
mi.cbSize = sizeof(mi);
if (GetMonitorInfoA(mon, (MONITORINFO *)&mi))
{
int hit = 0;
if (sc->want != NULL)
{
// Match the full device name OR just its tail, so a hand-written
// cfg can say `monitor:DISPLAY4` and skip the \\.\ prefix entirely
// (backslashes in a config file are a foot-gun -- every shell,
// editor and copy-paste path mangles them differently).
size_t dl = strlen(mi.szDevice), wl = strlen(sc->want);
if (_stricmp(mi.szDevice, sc->want) == 0)
hit = 1;
else if (wl > 0 && wl <= dl
&& _stricmp(mi.szDevice + (dl - wl), sc->want) == 0)
hit = 1;
}
if (!hit && sc->wantIndex >= 0 && sc->index == sc->wantIndex)
hit = 1;
if (hit && !sc->found)
{
sc->rect = mi.rcMonitor;
sc->found = 1;
}
}
++sc->index;
return TRUE;
}
// Resolve "monitor:<spec>" to a rect. Returns 1 on success.
static int
ResolveMonitorSpec(const char *spec, RECT *out)
{
while (*spec == ' ' || *spec == ' ') ++spec;
MonScan sc;
memset(&sc, 0, sizeof(sc));
sc.wantIndex = -1;
if (spec[0] >= '0' && spec[0] <= '9')
sc.wantIndex = atoi(spec);
else
sc.want = spec;
EnumDisplayMonitors(NULL, NULL, MonScanProc, (LPARAM)&sc);
if (sc.found)
*out = sc.rect;
return sc.found;
}
// from Create AFTER ComputeLayout, so any window not named in the file keeps its
// computed spot. Restored windows are flagged so the WM_TIMER re-snap (which
// re-runs ComputeLayout once the main window appears) leaves them alone.
@@ -836,34 +646,7 @@ static void
*end = '\0';
int x = 0, y = 0, w = 0, h = 0;
int haveXY = (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) >= 2);
int viaMonitor = 0;
if (!haveXY)
{
// "monitor:<name|index>" -- boot-stable binding (see the resolver).
const char *v = eq + 1;
while (*v == ' ' || *v == '\t') ++v;
if (_strnicmp(v, "monitor:", 8) == 0 || _strnicmp(v, "mon:", 4) == 0)
{
char spec[64];
const char *p2 = strchr(v, ':') + 1;
int n2 = 0;
while (*p2 && *p2 != ',' && n2 < (int)sizeof(spec) - 1) spec[n2++] = *p2++;
spec[n2] = 0;
RECT mr;
if (ResolveMonitorSpec(spec, &mr))
{
x = mr.left; y = mr.top;
w = mr.right - mr.left; h = mr.bottom - mr.top;
haveXY = 1;
viaMonitor = 1;
}
else
DEBUG_STREAM << "[glasswin] monitor '" << spec
<< "' not found -- leaving computed placement\n" << std::flush;
}
}
if (!haveXY)
if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
continue;
// Trailing options after the numbers, comma-separated. Unknown ones
@@ -883,30 +666,11 @@ static void
GWin *gw = FindGWinByTitle(s);
if (gw == NULL)
continue;
// "bare" is a pod panel: it always implies frameless. (Without this a
// cfg line saying only ",bare" silently re-framed a window that pod
// surface mode had already cropped -- caught on the bench 2026-08-06.)
gw->noFrame = (noframe || bare) ? 1 : 0;
gw->wantX = x;
gw->wantY = y;
gw->noFrame = noframe;
if (bare && gw->buttonCount > 0)
MakeBareSurface(*gw); // per-window pod panel (mixed rigs)
if (viaMonitor)
{
// x,y,w,h is the MONITOR rect -- centre this surface on it (an
// exact-size pod panel fills it edge to edge).
int sw = gw->clientW > 0 ? gw->clientW : (gw->surfaceRect.right - gw->surfaceRect.left);
int sh = gw->clientH > 0 ? gw->clientH : (gw->surfaceRect.bottom - gw->surfaceRect.top);
gw->wantX = x + ((w - sw) / 2);
gw->wantY = y + ((h - sh) / 2);
DEBUG_STREAM << "[glasswin] '" << gw->title
<< "' bound to monitor " << x << "," << y << " " << w << "x" << h
<< " -> window at " << gw->wantX << "," << gw->wantY
<< std::endl << std::flush;
}
else
{
gw->wantX = x;
gw->wantY = y;
}
gw->restored = 1;
++restored;
}
@@ -1077,19 +841,6 @@ static void
mask = 0xFF; // index the low (palette) byte
palId = SVGA16::SecondaryPalette;
}
// RGB mode: the base surface rides a CHANNEL, so its tint is that pure
// primary -- resolved live (override first), never the build-time guess.
if (w->groupCount > 0 && tint >= 0)
{
switch (ResolvedChannelOf(w->portPrimary, port))
{
case L4GraphicsPort::RedChannel: tint = (int)0x00FF0000; break;
case L4GraphicsPort::GreenChannel: tint = (int)0x0000FF00; break;
case L4GraphicsPort::BlueChannel: tint = (int)0x000000FF; break;
default: break;
}
}
int ow = 0, oh = 0;
svga->ExpandPlaneToBGRA(mask, palId, tint, w->rotate, gStage, &ow, &oh);
@@ -1129,9 +880,9 @@ static void
if (ga != NULL && ga->GetEnableID() != L4GraphicsPort::BlankColor)
gp = ga;
}
const int ch = ResolvedChannelOf(w->groupPort[gi], gp);
const int ch = gp->GetEnableID();
unsigned long chMask;
switch (ch)
switch (ch & 0x7F)
{
case L4GraphicsPort::RedChannel: chMask = 0x00FF0000ul; break;
case L4GraphicsPort::GreenChannel: chMask = 0x0000FF00ul; break;
@@ -1318,113 +1069,6 @@ static void
// (NO full-face flash overlay here either -- same wrongness as the
// L4VB16 one, removed 2026-08-03: the buttons are big rects tucked
// under the surface by design, and the protruding edge is the lamp.)
// ---- CHANNEL WALK (BT_POD_CHANTEST) ---------------------------------
// Overwrite the whole surface with ONE channel at full intensity, so
// exactly one monitor of a splitter-fed trio lights. Cycles R->G->B.
{
const int phase = PodChanTestPhase();
if (phase >= 0)
{
static const COLORREF chColor[3] =
{ RGB(255, 0, 0), RGB(0, 255, 0), RGB(0, 0, 255) };
static const char *chName[3] = { "RED", "GREEN", "BLUE" };
HBRUSH fill = CreateSolidBrush(chColor[phase]);
FillRect(dc, &w->surfaceRect, fill);
DeleteObject(fill);
// Name the channel AND what the pod authors onto it here, so a
// lit panel identifies itself completely: "GREEN = Mfd2".
const char *who = "-";
GaugeRenderer *cgr = BTResolveGaugeRenderer();
if (cgr != NULL)
{
const char *cand[4]; int nc = 0;
cand[nc++] = w->portPrimary;
for (int gi = 0; gi < w->groupCount && nc < 4; ++gi)
cand[nc++] = w->groupPort[gi];
for (int i = 0; i < nc; ++i)
{
L4GraphicsPort *pp =
static_cast<L4GraphicsPort*>(cgr->GetGraphicsPort(cand[i]));
if (pp != NULL && (pp->GetEnableID() & 0x7F) == phase)
{ who = cand[i]; break; }
}
}
char big[96];
sprintf(big, "%s %s = %s", w->monitorName, chName[phase], who);
RECT br = w->surfaceRect;
br.top += (br.bottom - br.top) / 3;
SetBkMode(dc, TRANSPARENT);
DrawLabelText(dc, big, titleFont, &br, RGB(0, 0, 0),
DT_CENTER | DT_SINGLELINE | DT_VCENTER);
}
}
// ---- PANEL IDENTIFY (BT_POD_IDENT=1) --------------------------------
// Pod bring-up: nobody can tell which PHYSICAL panel a Windows display
// is, and desktop coordinate order says nothing about where a panel
// sits in the cab -- guessing that mapping is exactly how it came out
// wrong. So label each surface ON ITS OWN GLASS with what it is, which
// monitor it came from, and (in RGB mode) the channel->surface map.
// Walk the pod, read the panels, write the layout from what you SAW.
if (PodIdentMode())
{
char line1[96], line2[160];
sprintf(line1, "%s", w->title);
if (w->groupCount > 0)
{
// name the channels so a splitter-fed trio is self-describing
char parts[128];
parts[0] = 0;
GaugeRenderer *igr = BTResolveGaugeRenderer();
const char *names[4]; int chans[4]; int nn = 0;
names[nn] = w->portPrimary; chans[nn] = -1; ++nn;
for (int gi = 0; gi < w->groupCount && nn < 4; ++gi)
{ names[nn] = w->groupPort[gi]; chans[nn] = -1; ++nn; }
for (int i = 0; i < nn; ++i)
{
const char *chName = "?";
if (igr != NULL)
{
L4GraphicsPort *pp =
static_cast<L4GraphicsPort*>(igr->GetGraphicsPort(names[i]));
if (pp != NULL)
switch (pp->GetEnableID() & 0x7F)
{
case L4GraphicsPort::RedChannel: chName = "RED"; break;
case L4GraphicsPort::GreenChannel: chName = "GREEN"; break;
case L4GraphicsPort::BlueChannel: chName = "BLUE"; break;
case L4GraphicsPort::AllChannels: chName = "RGB"; break;
default: chName = "blank"; break;
}
}
char one[48];
sprintf(one, "%s%s=%s", (i ? " " : ""), chName, names[i]);
if (strlen(parts) + strlen(one) < sizeof(parts) - 1)
strcat(parts, one);
}
sprintf(line2, "%s %s", w->monitorName, parts);
}
else
{
sprintf(line2, "%s surface=%s", w->monitorName,
w->portPrimary ? w->portPrimary : "-");
}
RECT lr = w->surfaceRect;
lr.bottom = lr.top + 64;
HBRUSH back = CreateSolidBrush(RGB(0, 0, 0));
FillRect(dc, &lr, back);
DeleteObject(back);
SetBkMode(dc, TRANSPARENT);
RECT t1 = lr; t1.top += 4; t1.bottom = t1.top + 30;
DrawLabelText(dc, line1, titleFont, &t1, RGB(255, 255, 0),
DT_LEFT | DT_SINGLELINE | DT_VCENTER);
RECT t2 = lr; t2.top += 32; t2.bottom = t2.top + 28;
DrawLabelText(dc, line2, buttonFont, &t2, RGB(0, 255, 255),
DT_LEFT | DT_SINGLELINE | DT_VCENTER);
}
}
BitBlt(winDC, 0, 0, client.right - client.left, client.bottom - client.top,
@@ -1561,23 +1205,11 @@ void
if (gWinCount != 0)
return; // already up
// BT_GAUGE_SEC_ROT: how far to turn the secondary/radar surface.
// 0 = none, 1 = 90 CCW, 2 = 180, 3 = 90 CW (default -- the pod's
// portrait CRT, user-verified upright).
// 0 and 2 keep the picture LANDSCAPE, which a modern replacement panel
// needs. (Until 2026-08-06 this read only '1' and forced everything else
// to 3, so BT_GAUGE_SEC_ROT=2 was silently ignored -- caught on the cart
// when a 180 request kept coming back portrait.)
// BT_GAUGE_SEC_ROT: the portrait-CRT unrotation direction (shared with the
// D3D dev path; default 3 = CW = user-verified upright).
{
const char *rv = getenv("BT_GAUGE_SEC_ROT");
if (rv != NULL && rv[0] >= '0' && rv[0] <= '3')
gRadarRot = rv[0] - '0';
else
gRadarRot = 3;
DEBUG_STREAM << "[glasswin] radar rotation " << gRadarRot
<< (gRadarRot == 3 ? " (90 CW)" : gRadarRot == 1 ? " (90 CCW)"
: gRadarRot == 2 ? " (180)" : " (none)")
<< std::endl << std::flush;
gRadarRot = (rv != NULL && rv[0] == '1') ? 1 : 3;
}
memset(latched, 0, sizeof(latched));
@@ -1716,8 +1348,6 @@ void
if (mon != NULL && GetMonitorInfoA(mon, (MONITORINFO *)&mi))
{
devName = mi.szDevice;
strncpy(w.monitorName, mi.szDevice, sizeof(w.monitorName) - 1);
w.monitorName[sizeof(w.monitorName) - 1] = 0;
mx = mi.rcMonitor.left; my = mi.rcMonitor.top;
mw = mi.rcMonitor.right - mi.rcMonitor.left;
mh = mi.rcMonitor.bottom - mi.rcMonitor.top;
+7 -19
View File
@@ -90,23 +90,6 @@ L4GaugeRenderer::L4GaugeRenderer(bool windowed, int *secondaryIndex, int *aux1In
char
*plasma_string = getenv("L4PLASMA");
//
// L4PLASMA=NONE / OFF / 0 -- no plasma marquee at all (2026-08-06).
// A real cab drives the 128x32 marquee over serial; a rebuilt cart may
// not have one wired, and the GLASS profile force-defaults L4PLASMA to
// SCREEN, which puts a desktop plasma window on the pod's glass where
// nothing should be. Leaving the variable UNSET is not an option
// there (the profile fills it in), so give "off" an explicit spelling.
//
if (plasma_string != NULL
&& (_stricmp(plasma_string, "NONE") == 0
|| _stricmp(plasma_string, "OFF") == 0
|| strcmp(plasma_string, "0") == 0))
{
Tell("Plasma display disabled (L4PLASMA=none)\n");
plasma_string = NULL;
}
if (plasma_string != NULL)
{
//Win32 Serial support: ADB 06/30/07
@@ -829,13 +812,18 @@ void
switch (type)
{
case scrambleVideo:
if (graphicsDisplay != NULL)
// NON-STACKING LATCH (phase-14 fidelity): the binary latches on `modified`
// @0x4fe0fe -- a second PPC hit while the scramble is ALREADY live does NOT
// re-save the (already detuned) value and does NOT extend the window. This
// original unconditionally overwrote scrambleVideoTimeout, so rapid PPC fire
// STACKED the effect -- a divergence from the binary. Ignore re-arm while live.
if (graphicsDisplay != NULL && !scrambleVideoFlag)
{
scrambleVideoFlag = True;
scrambleVideoTimeout = ((Scalar)Now()) + duration;
Check(graphicsDisplay);
((SVGA16*) graphicsDisplay)->FunkyVideo(True);
((SVGA16*) graphicsDisplay)->FunkyVideo(True, duration); // duration -> the recovery envelope
}
break;
+234 -40
View File
@@ -615,17 +615,41 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
Word *source = pixelBuffer.Data.MapPointer;
Word *dest = (Word*)rect.pBits;
int postRowIncrement = (rect.Pitch / 2) - w;
// PPC sync-scramble (phase-14): map the SOURCE read through the recovery
// envelope (collapse toward a line -> broaden -> lock). Identity when not
// armed (ScrambleParams returns False). Output columns outside the collapsed
// band read index 0 (black), so the picture squeezes to a line and grows back.
double scl, shr, roff; int cx, shake;
Logical scr = ScrambleParams(w, &scl, &shr, &roff, &cx, &shake);
double invS = scr ? (1.0 / scl) : 1.0;
if (monoTint < 0)
{
// PALETTE surface (sec/radar) -- palette-LUT expand (== SVGA16::Update case 0).
SVGA16Palette *pal = &palette[paletteID];
for (int y = 0; y < h; y++)
{
int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1;
Word *srcRow = source + sry * w; // vertical shake bounces the row
double rowScroll = roff + shr * (double)y; // scroll + diagonal, per row
for (int x = 0; x < w; x++)
{
PaletteTriplet *pe = &(pal->paletteData.Color[*source & mask]);
Word px;
if (scr)
{
double srcBase = ((double)x - cx) * invS + cx; // collapse band
if (srcBase < 0.0 || srcBase >= w)
px = 0; // black outside the line
else
{
long sx = (long)(srcBase + rowScroll + 0.5);
sx %= w; if (sx < 0) sx += w; // roll SCROLLS within source
px = srcRow[sx];
}
}
else px = srcRow[x];
PaletteTriplet *pe = &(pal->paletteData.Color[px & mask]);
*dest = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3);
dest++; source++;
dest++;
}
dest += postRowIncrement;
}
@@ -637,10 +661,27 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
Word tint = (Word) monoTint;
for (int y = 0; y < h; y++)
{
int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1;
Word *srcRow = source + sry * w; // vertical shake bounces the row
double rowScroll = roff + shr * (double)y;
for (int x = 0; x < w; x++)
{
*dest = (*source & mask) ? tint : 0;
dest++; source++;
Word px;
if (scr)
{
double srcBase = ((double)x - cx) * invS + cx;
if (srcBase < 0.0 || srcBase >= w)
px = 0;
else
{
long sx = (long)(srcBase + rowScroll + 0.5);
sx %= w; if (sx < 0) sx += w;
px = srcRow[sx];
}
}
else px = srcRow[x];
*dest = (px & mask) ? tint : 0;
dest++;
}
dest += postRowIncrement;
}
@@ -712,16 +753,38 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
Word *base = pixelBuffer.Data.MapPointer;
SVGA16Palette *pal = &palette[paletteID];
// PPC sync-scramble (phase-14) -- the recovery envelope (collapse -> broaden ->
// lock), mapped in SOURCE space so it survives the rotation. Identity when not
// armed (ScrambleParams False). Reads outside the collapsed band -> index 0 (black).
double scl, shr, roff; int cx, shake;
Logical scr = ScrambleParams(w, &scl, &shr, &roff, &cx, &shake);
double invS = scr ? (1.0 / scl) : 1.0;
if (rotateQuadrant == 0)
{
// Native orientation, top-down straight copy.
for (int y = 0; y < h; y++)
{
Word *src = base + y * w;
int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1;
Word *src = base + sry * w; // vertical shake bounces the row
unsigned long *d = dst + y * w;
double rowScroll = roff + shr * (double)y;
for (int x = 0; x < w; x++)
{
Word s = src[x];
Word s;
if (scr)
{
double srcBase = ((double)x - cx) * invS + cx; // collapse band
if (srcBase < 0.0 || srcBase >= w)
s = 0; // black outside the line
else
{
long sx = (long)(srcBase + rowScroll + 0.5);
sx %= w; if (sx < 0) sx += w; // roll SCROLLS within source
s = src[sx];
}
}
else s = src[x];
if (monoTint < 0)
{
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
@@ -739,36 +802,10 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
return;
}
// 180 degrees (rotate 2): dimensions UNCHANGED -- the case a landscape
// replacement panel needs when the 1995 glass was a portrait CRT mounted
// the other way up. Added 2026-08-06 from the crash cart, whose radar is a
// landscape LCD: 90 CW left the picture portrait and overhanging its
// monitor, and a further 90 CW is this.
if (rotateQuadrant == 2)
{
for (int oy = 0; oy < h; oy++)
{
unsigned long *d = dst + oy * w;
for (int ox = 0; ox < w; ox++)
{
Word s = base[(h - 1 - oy) * w + (w - 1 - ox)];
if (monoTint < 0)
{
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
d[ox] = ((unsigned long)pe->Red << 16) |
((unsigned long)pe->Green << 8) | (unsigned long)pe->Blue;
}
else
d[ox] = (s & mask) ? (unsigned long)monoTint : 0UL;
}
}
if (outW) *outW = w;
if (outH) *outH = h;
return;
}
// 90-degree rotation: output is transposed (ow = h, oh = w). rotate 3 = CW,
// rotate 1 = CCW (the DrawDevSurface convention; BT_GAUGE_SEC_ROT picks it).
// The scramble maps the SOURCE column (sx keyed on source row sy) through the
// same envelope, so the radar recovers the same way the landscape MFDs do.
int ow = h, oh = w;
for (int oy = 0; oy < oh; oy++)
{
@@ -786,7 +823,21 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
sx = w - 1 - oy;
sy = ox;
}
Word s = base[sy * w + sx];
int syS = sy + shake; if (syS < 0) syS = 0; else if (syS >= h) syS = h - 1; // shake
Word s;
if (scr)
{
double srcBase = ((double)sx - cx) * invS + cx; // collapse band (source col)
if (srcBase < 0.0 || srcBase >= w)
s = 0;
else
{
long msx = (long)(srcBase + roff + shr * (double)sy + 0.5);
msx %= w; if (msx < 0) msx += w; // roll SCROLLS within source
s = base[syS * w + msx];
}
}
else s = base[syS * w + sx];
if (monoTint < 0)
{
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
@@ -5423,6 +5474,9 @@ SVGA16::SVGA16(
BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index);
for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw
mDevSurfaceTex[_i] = NULL;
scrambleActive = False; // PPC sync-scramble (phase-14) -- armed by FunkyVideo
scrambleStartMs = 0;
scrambleDurationS = 0.0;
//STUBBED: VIDEO RB 1/15/07
# if defined(DEBUG)
Tell("SVGA16::SVGA16()\n");
@@ -6382,13 +6436,153 @@ void
Check_Fpu();
}
//
// PPC sync-scramble (phase-14), animated as a RECOVERY. The original
// (`//STUBBED: VIDEO RB 1/15/07`) detuned the VGA CRTC Horizontal Total so every
// secondary monitor lost horizontal lock for ~0.8 s. There is no CRTC here, so
// FunkyVideo records the start + duration and the per-surface expansions
// (DrawDevSurface / ExpandPlaneToBGRA) map the pixelBuffer read through the
// ScrambleParams envelope: at the hit the image COLLAPSES horizontally toward a
// line, then BROADENS back out as the tear + roll DECAY, and LOCKS to the full
// clean display at the end. The 0.8 s window + non-stacking latch stay in
// L4GaugeRenderer; the shape lives here.
//
void
SVGA16::FunkyVideo(Logical on_off)
SVGA16::FunkyVideo(Logical on_off, Scalar duration)
{
//STUBBED: VIDEO RB 1/15/07
//Check(this);
//SVGAFunkyVideo(on_off);
//Check_Fpu();
if (on_off)
{
scrambleActive = True;
scrambleStartMs = GetTickCount();
scrambleDurationS = (duration > 0.0f) ? (double)duration : 0.8; // the HOLD
}
// FunkyVideo(False) is a NO-OP: the card restoring the sync (at HOLD end) is
// where the RECOVERY begins, so ScrambleParams keeps running past the flag,
// self-terminating at hold+recovery. scrambleActive stays armed; the clock gates.
}
//
// Two-phase envelope (per the "hold the bad sync, THEN recover" direction):
// HOLD [0, hold] -- the card holds the detuned CRTC. Deep collapse (a line),
// and a WILDLY fast horizontal roll that decelerates over
// the hold (content scrolls, wrapping, so it can't be read).
// RECOVERY [hold, +] -- the sync is restored and the monitor re-locks: the line
// BROADENS back to full while the residual scroll + tear
// settle to zero, then LOCKS (returns False = clean).
// Fills the read-loop parameters: scale (collapse band width fraction), rollOff
// (SCROLL offset in px, wrapped within the source), shear (px/row diagonal),
// centerX. Tunables (read once): BT_SCRAMBLE_COLLAPSE (min band, deeper=smaller),
// BT_SCRAMBLE_ROLL (initial scroll px/sec -- "wildly high"), BT_SCRAMBLE_SHEAR
// (px/row), BT_SCRAMBLE_RECOVER (recovery seconds), BT_SCRAMBLE_DUR (hold seconds
// override). BT_SCRAMBLE_TEST loops it; BT_SCRAMBLE_CYCLE loops stepping the roll.
//
Logical
SVGA16::ScrambleParams(int width, double *scale, double *shear,
double *rollOff, int *centerX, int *shakeRow) const
{
*shakeRow = 0;
if (width <= 0)
return False;
static int sInit = 0, sTest = 0, sCycle = 0;
static double shearMax = 3.0, rollMax = 9000.0, collapse = 0.03,
recover = 0.5, holdEnv = 0.0, shakeAmp = 10.0;
if (!sInit)
{
sInit = 1;
sTest = (getenv("BT_SCRAMBLE_TEST") != NULL) ? 1 : 0;
sCycle = (getenv("BT_SCRAMBLE_CYCLE") != NULL) ? 1 : 0;
const char *e;
if ((e = getenv("BT_SCRAMBLE_SHEAR")) != NULL) shearMax = atof(e);
if ((e = getenv("BT_SCRAMBLE_ROLL")) != NULL) rollMax = atof(e);
if ((e = getenv("BT_SCRAMBLE_COLLAPSE")) != NULL) collapse = atof(e);
if ((e = getenv("BT_SCRAMBLE_RECOVER")) != NULL) recover = atof(e);
if ((e = getenv("BT_SCRAMBLE_DUR")) != NULL) holdEnv = atof(e);
if ((e = getenv("BT_SCRAMBLE_SHAKE")) != NULL) shakeAmp = atof(e);
if (collapse < 0.004) collapse = 0.004;
if (collapse > 1.0) collapse = 1.0;
if (recover < 0.05) recover = 0.05;
}
double hold = (holdEnv > 0.0) ? holdEnv
: (scrambleDurationS > 0.0 ? scrambleDurationS : 0.8);
double total = hold + recover;
double rlMax = rollMax;
double elapsed;
if (sTest || sCycle)
{
// Tuning: loop the whole effect (hold+recovery) + a short locked pause.
// CYCLE steps the initial roll speed each loop.
double loop = total + 0.6;
double t = (double)GetTickCount() * 0.001;
long n = (long)(t / loop);
double ph = t - (double)n * loop;
if (ph >= total)
return False; // locked pause between loops
elapsed = ph;
if (sCycle)
{
static const double kR[] = { 3000.0, 6000.0, 9000.0, 14000.0, 20000.0 };
int nS = (int)(sizeof(kR) / sizeof(kR[0]));
rlMax = kR[(int)(n % nS)];
static long lastN = -1;
if (n != lastN) { lastN = n;
DEBUG_STREAM << "[scramble-cycle] loop " << n << " rollMax=" << rlMax
<< "px/s (hold " << hold << "s + recover " << recover << "s)\n"
<< std::flush; }
}
}
else
{
if (!scrambleActive)
return False;
elapsed = ((double)(GetTickCount() - scrambleStartMs)) * 0.001;
if (elapsed >= total)
return False; // LOCKED -- clean display
}
if (elapsed < 0.0) elapsed = 0.0;
double sScale, sScroll, sShear;
if (elapsed < hold)
{
// HOLD: bad sync held. Deep collapse; roll starts wild and decelerates to a
// stop by the hold end. scroll = integral of v(t)=rlMax*(1-t/hold): the
// content scrolls fast then coasts to rest (rlMax*hold/2 accumulated).
sScale = collapse;
sScroll = rlMax * elapsed * (1.0 - elapsed / (2.0 * hold));
sShear = shearMax;
}
else
{
// RECOVERY: sync restored, monitor re-locks. Broaden line->full and settle
// the residual scroll + tear to zero (smoothstep), then it hits total = lock.
double tr = (elapsed - hold) / recover;
double b = tr * tr * (3.0 - 2.0 * tr);
sScale = collapse + (1.0 - collapse) * b;
sScroll = (rlMax * hold * 0.5) * (1.0 - b);
sShear = shearMax * (1.0 - b);
}
// SHAKE: a per-FRAME jitter (LCG, so all surfaces shake together within a frame
// but re-roll each frame), violent at the hit and fading through the recovery.
// Horizontal folds into the scroll; vertical bounces the source row (shakeRow).
double shakeEnv = (elapsed < hold)
? (1.0 - 0.5 * (elapsed / hold)) // 1.0 at impact -> 0.5 at hold end
: (0.5 * (1.0 - (elapsed - hold) / recover)); // 0.5 -> 0 through recovery
if (shakeEnv < 0.0) shakeEnv = 0.0;
unsigned int fr = (unsigned int)(GetTickCount() / 16); // ~per-frame index
unsigned int r = fr * 1103515245u + 12345u;
double jX = ((double)((r >> 16) & 0x7FFF) / 16384.0) - 1.0; // [-1,1)
r = r * 1103515245u + 12345u;
double jY = ((double)((r >> 16) & 0x7FFF) / 16384.0) - 1.0;
*scale = sScale;
*shear = sShear;
*rollOff = sScroll + shakeAmp * shakeEnv * jX; // horizontal shake -> scroll
*centerX = width / 2;
*shakeRow = (int)(shakeAmp * shakeEnv * jY + (jY >= 0.0 ? 0.5 : -0.5)); // vertical bounce
return True;
}
//########################################################################
+21 -1
View File
@@ -332,7 +332,27 @@ public:
UnflashPalette(int palette_number);
void
FunkyVideo(Logical on_off);
FunkyVideo(Logical on_off, Scalar duration = 0.0f);
// PPC sync-scramble (phase-14): the modern stand-in for the VGA CRTC
// Horizontal-Total detune, animated as a RECOVERY over the effect window -- at
// the hit the image collapses horizontally toward a line, then broadens back
// out as the tear + roll decay, and LOCKS to the full clean display at the end.
// FunkyVideo(on, dur) records the start + duration; ScrambleParams derives the
// per-frame envelope (returns True while active + fills scale/shear/rollOff/
// centerX); DrawDevSurface (surround) + ExpandPlaneToBGRA (glass) map the
// pixelBuffer read through it, so all secondary surfaces recover together and
// the main 3D view is untouched. Tunable by eye: BT_SCRAMBLE_SHEAR (max
// px/line), BT_SCRAMBLE_ROLL (max px/sec), BT_SCRAMBLE_COLLAPSE (min width
// fraction 0..1), BT_SCRAMBLE_DUR (anim seconds override). BT_SCRAMBLE_TEST
// loops the transition for tuning; BT_SCRAMBLE_CYCLE loops it stepping the
// max shear. (Exact look depended on each pod monitor's PLL -- not recoverable.)
Logical
ScrambleParams(int width, double *scale, double *shear,
double *rollOff, int *centerX, int *shakeRow) const;
Logical scrambleActive;
unsigned long scrambleStartMs;
double scrambleDurationS;
protected:
+1 -42
View File
@@ -407,32 +407,6 @@ static const char *gBTCwdFixNote = NULL;
//
int gBTFitDisplay = 0;
//
// L4PLASMA=NONE/OFF/0 means "no marquee at all" (L4GREND.cpp reads the same
// spellings). Only used to keep the boot banner honest.
//
static bool BTPlasmaDisabled(void)
{
const char *p = getenv("L4PLASMA");
return p != NULL && (_stricmp(p, "NONE") == 0 ||
_stricmp(p, "OFF") == 0 ||
strcmp(p, "0") == 0);
}
//
// Which control device the GLASS profile actually resolved to. The banner
// used to hardcode "PadRIO", which is a lie on a cab wired to the real board
// -- and precisely the log line someone reads to check that (2026-08-06).
//
static const char *BTControlsSummary(void)
{
const char *c = getenv("L4CONTROLS");
if (c == NULL) return "PadRIO";
if (_strnicmp(c, "RIO", 3) == 0) return "hardware RIO";
if (_strnicmp(c, "PAD", 3) == 0) return "PadRIO";
return c;
}
static void BTEnsureContentDirectory(void)
{
if (GetFileAttributesA("BTL4.RES") != INVALID_FILE_ATTRIBUTES)
@@ -874,18 +848,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
{
gBTFitDisplay = 1;
}
//
// BT_FIT=1 -- the env spelling of -fit, so a POD can freeze it in
// content\environ.ini alongside the rest of its rig config instead of
// depending on every launcher passing the flag (2026-08-06, crash cart:
// the main view has to fill its 800x600 panel on every boot, however the
// game gets started -- shortcut, autostart or scheduled task).
//
{
const char *fitEnv = getenv("BT_FIT");
if (fitEnv != NULL && fitEnv[0] != '\0' && fitEnv[0] != '0')
gBTFitDisplay = 1;
}
// BT_JOYCONFIG=1: the generic-joystick capture wizard (flight sticks /
// HOTAS / pedals -- L4JOY.h). Console prompts detect which device/axis
@@ -1183,11 +1145,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
std::cout << "[boot] platform profile: "
<< (fe_menu_mode ? "MENU (front end -- no profile applied)"
: gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)"
: gBTPlatformGlass ? "GLASS"
: gBTPlatformGlass ? "GLASS (PadRIO + plasma window)"
: "DEV (single window + keyboard)");
if (gBTPlatformGlass && !fe_menu_mode)
std::cout << " (" << BTControlsSummary()
<< (BTPlasmaDisabled() ? "; plasma off [L4PLASMA])" : " + plasma window)");
if (!fe_menu_mode)
std::cout << " [secondary displays: " << kGlassLayoutName[glassLayout] << "]";
std::cout << std::endl << std::flush;
+26
View File
@@ -82,6 +82,7 @@
// The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp)
// for the same reason.
#include "dmgtable.hpp"
#include <GAUGREND.hpp> // GaugeRenderer::SpecialEffect (virtual) -- PPC scramble trigger
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
// heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete
// there) -- mech.cpp cannot include the subsystem headers (local-stub collision)
@@ -1122,6 +1123,31 @@ void
}
}
reportZone = message->damageZone; // post-resolve (@0x4a0396 write)
//
// @0x4a03f3 [T1] -- PPC/ERPPC SECONDARY-DISPLAY SCRAMBLE. EnergyDamageType
// (==4) is authored on exactly the 14 PPC/ERPPC subsystem records and nothing
// else, so this branch is structurally PPC-exclusive -- NO explicit weapon
// class check (the data authorship IS the gate). It sits OUTSIDE the burst
// loop below, so it fires ONCE per damage message, not per burst. The
// duration is DERIVED from the type ordinal, not a literal: (Scalar)4 * 0.2 ==
// 0.8s (binary loads long double 0.2 @0x4a0c08, fmulp). SpecialEffect ->
// L4GaugeRenderer scrambles every SECONDARY cockpit display (SVGA16 sync
// detune, FunkyVideo) for that window; the main 3D view is on an independent
// timing chain and is left clean. Spec: phases/phase-14-ppc-sync-distortion.md.
//
if (message->damageData.damageType == Damage::EnergyDamageType)
{
GaugeRenderer *gauges =
(application != 0) ? application->GetGaugeRenderer() : 0; // APP.h:355 -- named, not application+0x4c
if (gauges != 0) // the binary null-guards the renderer too (@0x4a0405)
gauges->SpecialEffect(GaugeRenderer::scrambleVideo,
(Scalar)message->damageData.damageType * 0.2f);
if (BTEnvOn("BT_DMG_LOG", 0))
DEBUG_STREAM << "[ppc-scramble] EnergyDamageType hit -> scrambleVideo for "
<< ((Scalar)message->damageData.damageType * 0.2f) << "s\n" << std::flush;
}
//
// #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which
// REPLACES the engine-base single application. Three things the base
+252
View File
@@ -0,0 +1,252 @@
# Phase 14 — PPC hit = `scrambleVideo`, the cockpit-CRT sync detune
**Goal:** restore the PPC's authentic secondary effect — a PPC strike scrambles
**every secondary cockpit display for 0.8 s**, leaving the main view untouched.
**Status:** ✅ IMPLEMENTED 2026-08-06 (branch `ppc-sync-distortion`) [T2 —
visual screenshot-verified, trigger by construction]. Discovered 2026-08-06 by
disassembly of the shipped `BTL4OPT.EXE` (md5 `a97075bcb5634d13263e9ad5a2b96fd0`)
after playtesters reported *"being hit by a PPC makes it look like all of the
secondary CRTs were being degaussed."* Full findings: `context/gauges-hud.md`
§"PPC HIT = a deliberate CRTC horizontal-sync DETUNE"; cross-ref in
`context/combat-damage.md`.
**What shipped (all three work items):**
- **A — trigger:** `game/reconstructed/mech.cpp`, `Mech::TakeDamageMessageHandler`
@0x4a03f3 position (after the cylinder resolve, before the burst loop) — fires
`GetGaugeRenderer()->SpecialEffect(scrambleVideo, damageType*0.2f)` once per
EnergyDamageType message. `BT_DMG_LOG` prints `[ppc-scramble]`.
- **B — visual (animated recovery):** `SVGA16::FunkyVideo(on, dur)` (was the 2007
stub) records the start + hold; `SVGA16::ScrambleParams` (new) is a two-phase
envelope read by BOTH `DrawDevSurface` (surround/dock) and `ExpandPlaneToBGRA`
(glass windows, native + rotated radar), so all secondary surfaces recover
together and the main 3D view (separate timing chain) is untouched. The sequence
per hit: **collapse** to a thin centred line → **HOLD** for the ~0.8 s the card
held bad sync (content scrolls wildly fast + decelerating, plus a violent
**shake**) → **RECOVERY** (~0.5 s: the line broadens back to full while
scroll/tear/shake settle) → **LOCK** (clean). The read loops map the source
through the envelope: horizontal `scale` sets a black-bordered collapse band,
`rollOff` scrolls (wraps) within it, `shear` is a per-row diagonal, and a
per-frame LCG `shake` bounces the row + jitters the scroll. Tunables (by eye —
the pod-monitor PLL look is not recoverable): `BT_SCRAMBLE_COLLAPSE` (min band
fraction, 0.03), `BT_SCRAMBLE_ROLL` (initial scroll px/s, 9000),
`BT_SCRAMBLE_SHAKE` (px, 10), `BT_SCRAMBLE_SHEAR` (px/row, 3), `BT_SCRAMBLE_DUR`
(hold s), `BT_SCRAMBLE_RECOVER` (recovery s, 0.5). **`BT_SCRAMBLE_TEST=1` loops
the whole transition** for tuning without a hit; `BT_SCRAMBLE_CYCLE=1` loops it
stepping the roll speed.
- **C — non-stacking latch:** `L4GaugeRenderer::SpecialEffect` now ignores the
re-arm while `scrambleVideoFlag` is set (matches the binary's `modified` latch);
a second PPC during the window no longer extends it. NB the SVGA16 animation runs
the full hold+recovery on its own clock (`FunkyVideo(False)` is a no-op — the
card restoring sync is where the recovery *begins*), so the recovery isn't cut
off when the 0.8 s latch clears.
- Verified [T2]: Release links clean; surround boots + runs with the effect looped
(`BT_SCRAMBLE_TEST`) at both slowed and true full speed — every secondary MFD +
the radar collapse/roll/shake/recover together while the out-the-window view
stays clean, no crash (screenshots). **SHIPPING with the current defaults for
playtester feedback.** Open: live PPC-fire confirmation (one arm per hit,
non-stacking across two hits) + by-eye tuning of the envelope constants — none
recoverable from the binary, so the testers who filed the report are the ground
truth.
**Good news up front:** the engine half already exists in our tree under the
**original VWE names** (`SpecialEffect` / `scrambleVideo` / `FunkyVideo`). Only
two things are missing: the **trigger** (never ported) and the **visual**
(stubbed out in 2007). This is a small, well-bounded job.
---
## 1. What the original did [T1 — disasm-verified]
On the **victim's** machine, `Mech::TakeDamageMessageHandler` @`0x4a0230` tests
the damage type between the collision divert and the burst loop:
```
004a03f3 mov ecx,[esi+0x2c] ; damage.damageType
004a03f6 cmp ecx,4 ; EnergyDamageType
004a03f9 jne 0x4a0423 ; everything else -> burst loop
004a03fb mov eax,[0x4efc94] ; global `application`
004a0400 mov eax,[eax+0x4c] ; -> gauge renderer
004a0405 je 0x4a0423 ; null-guarded
004a0407 fild dword [esi+0x2c] ; (float)damageType == 4.0
004a040a fld xword [0x4a0c08] ; long double 0.2
004a0410 fmulp st(1) ; => 0.8
004a041d call dword [edx+0x4c] ; vtable slot 19 == SpecialEffect(0, 0.8f)
```
`L4GaugeRenderer::SpecialEffect(scrambleVideo, 0.8f)` @`0x46ffcc`
`SVGA16::FunkyVideo(True)` @`0x47d76d`, which reprograms the **VGA CRT
controller**:
```
out(0x3D4,0x11); v=in(0x3D5); out(0x3D5, v & 0x7F) ; unlock CRTC regs 0-7
out(0x3D4,0x00) ; CRTC 0 = HORIZONTAL TOTAL
saved = in(0x3D5); out(0x3D5, saved - 9) ; shorten every scanline
out(0x3D4,0x11); out(0x3D5, v) ; restore write-protect
```
A per-frame timer @`0x47003c` writes `saved` back 0.8 s later.
**Why only the PPC:** a `BTL4.RES` census gives `damageType` 4 (`Energy`) =
**14 subsystem records, every one PPC or ERPPC**. Everything else is Ballistic
(16), Explosive (30), Laser (78). The branch is structurally PPC-exclusive —
no extra gating needed.
**Why only the secondaries:** all six secondary displays are derived by the VDB
from that one VGA's timing, so they break together. The main out-the-window
view comes off the Division VPX card on an independent timing chain and is
unaffected. Playtesters confirm both halves.
---
## 2. What our tree already has
| Piece | Where | State |
|---|---|---|
| `enum VideoEffectType { scrambleVideo }` (value **0**) | `engine/MUNGA/GAUGREND.h:482` | ✅ present |
| `virtual void GaugeRenderer::SpecialEffect(VideoEffectType, Scalar) {}` | `engine/MUNGA/GAUGREND.h:488` | ✅ base no-op |
| `L4GaugeRenderer::SpecialEffect` — sets `scrambleVideoFlag`, `scrambleVideoTimeout = Now()+duration`, calls `FunkyVideo(True)` | `engine/MUNGA_L4/L4GREND.cpp:806` | ✅ implemented |
| `L4GaugeRenderer::ProcessVideoEffects()` — on timeout, `FunkyVideo(False)` | `engine/MUNGA_L4/L4GREND.cpp:832` | ✅ implemented |
| …called every frame from `ExecuteForeground` | `engine/MUNGA_L4/L4GREND.cpp:370` | ✅ live |
| `SVGA16::FunkyVideo(Logical)` | `engine/MUNGA_L4/L4VB16.cpp:6358` | ❌ **STUBBED** (`//STUBBED: VIDEO RB 1/15/07`, body commented out) |
| Any caller of `SpecialEffect` | — | ❌ **NONE** |
The base-class declaration carries its original comment:
> `// Quick and dirty hack to allow calling L4GaugeRenderer::SpecialEffect`
> `// from non L4 level. GDU 2/28/96`
That hack exists **because the damage handler (non-L4 level) had to call it**
independent corroboration that the trigger belonged in `mech.cpp`, and the
reason you can call it through the base pointer without dragging L4 headers
into a game-layer TU.
---
## 3. Work item A — the trigger (`game/reconstructed/mech.cpp`)
In `Mech::TakeDamageMessageHandler`, **after** the `damageType==0` collision
divert and **before** the burst loop, add the type-4 branch.
```cpp
// @0x4a03f3 [T1] -- PPC/ERPPC only: EnergyDamageType is authored on exactly
// the 14 PPC/ERPPC subsystem records and nothing else. Duration is DERIVED
// from the type ordinal, not a constant: (float)damageType * 0.2 == 0.8f.
if (damage.damageType == Damage::EnergyDamageType) // == 4
{
GaugeRenderer *gauges = (application != 0)
? application->GetGaugeRenderer() : 0; // APP.h:355
if (gauges != 0) // binary null-guards too
{
gauges->SpecialEffect(
GaugeRenderer::scrambleVideo,
(Scalar)damage.damageType * 0.2f); // long double 0.2 @0x4a0c08
}
}
```
**Placement matters.** The binary's branch is *outside* the burst loop, so it
fires **once per damage message**, not once per burst. Putting it inside the
loop would re-arm it `burstCount` times.
Use the named accessor — do not raw-read `application+0x4c` (databinding rule,
`context/reconstruction-gotchas.md`).
---
## 4. Work item B — the visual (`SVGA16::FunkyVideo`)
There is no CRTC to detune, so reproduce the **look**, applied to the gauge
composite only.
What the original did physically: Horizontal Total sets character clocks per
scanline. Subtracting 9 shortens every line by roughly **9%**, far outside any
monitor's sync lock range, so the picture breaks into a rolling diagonal tear
until the value is restored. VWE's own name for it — `scrambleVideo` — is the
best description of the intended result.
Suggested model (per-scanline horizontal displacement of the gauge buffer):
```
shift(y, t) = ( y * k + roll(t) ) mod width
```
- `k` — per-line shear, the fraction of a line lost. ~9% of width is the
physically-derived starting point; **tune by eye** against the playtester
description rather than treating it as exact, because the on-screen result
depended on how each pod monitor's H-sync PLL misbehaved — that is not
recoverable from the binary.
- `roll(t)` — a time-varying offset so the tear drifts rather than sitting
static. The original rolled because the monitor never re-locked.
Constraints:
- **Gauge composite only.** Apply to the `SVGA16` `pixelBuffer`
(`engine/MUNGA_L4/l4vb16.h:243`) or at the point the strip/surfaces are
presented — *never* the main 3D view. The main view being clean is a
confirmed observation, not an assumption.
- **All secondary surfaces together.** They are bit-planes of one shared
buffer, so a single buffer-level effect is authentic by construction; do not
implement it per-MFD.
- **Keep the existing timing path.** `SpecialEffect` / `ProcessVideoEffects`
already own the flag and the 0.8 s timeout and are already called per frame.
`FunkyVideo` should only set/clear state — no timing logic of its own.
---
## 5. Fidelity constraints (do not "improve" these)
1. Duration is `damageType * 0.2f`, **not** a literal `0.8f`.
2. Fires on `EnergyDamageType` — never add an explicit PPC class check; the
data authorship *is* the gate.
3. **Idempotent, non-stacking.** The binary latches on `modified` @`0x4fe0fe`:
a second PPC hit while the effect is live does **not** re-save the (already
detuned) value and does **not** extend or double the effect. Our
`SpecialEffect` currently *does* overwrite `scrambleVideoTimeout`, which
extends the effect on a second hit — **that is a divergence.** Match the
binary: ignore re-arm while `scrambleVideoFlag` is set. (The original's
latch was in `FunkyVideo`; ours must go in `SpecialEffect` or `FunkyVideo`,
but it must exist.)
4. Victim-side only. The shooter sees nothing; this runs in the victim's
damage handler.
5. Null-guard the gauge renderer — the binary does, and headless/bench runs
have none.
---
## 6. Verification
- **Headless:** add a one-line log in the new branch; fire a PPC at a dummy
with `BT_DMG_LOG`. Expect exactly one arm per PPC message, zero for laser /
autocannon / missile / Gauss.
- **Non-stacking:** two PPC hits ~0.2 s apart must produce one 0.8 s effect
measured from the *first* hit, not 1.0 s or two effects.
- **Live:** confirm every secondary MFD scrambles together and the main view
stays clean. Have the playtesters who filed the report compare — they are
the only ground truth for `k` and `roll`.
- **Against the original (optional, decisive):** in the DOSBox-X fork, log
writes to CRTC index 0 via `0x3D4`/`0x3D5` during a real BT mission.
Prediction: exactly one write of `saved-9` per PPC strike, one restore 0.8 s
later, zero for every other weapon.
---
## 7. Gotchas
- Do **not** confuse this with `SVGA16::FlashPalette` (the pixel-mask cycler on
ports `0x302/0x30A/0x312`). That machinery is linked and its per-frame cycler
runs, but `FlashPalette` @`0x46d5f4` has **zero call sites and zero
address-of references** in `BTL4OPT.EXE` — BT never arms it. **RP does**
(`RPL4OPT.EXE` @`0x4addce`, palette 1, rate 2.0, masks `{FF,BF,7F,3F}`) for
alarm blinking. Wrong mechanism, wrong game.
- The `LampTesla1/2/3` "solid-state relays" in `L4CTRL.HPP` are **not**
involved and are driven by nothing in the surviving tree.
- Scope note: callers of gauge-renderer vtable slot 19 were not exhaustively
enumerated (virtual dispatch), so another arming site may exist. The
low-level path *is* exhaustive — @`0x47d76d` has one caller, @`0x46d840` two
(set @`0x47002b`, restore @`0x470076`).
## References
- `context/gauges-hud.md` §"PPC HIT = a deliberate CRTC horizontal-sync DETUNE"
- `context/combat-damage.md` §`Mech::TakeDamageMessageHandler` (Energy branch)
- `HISTORY.md` (TeslaRel410) §"Anatomy of a surviving weapon — the PPC"
-11
View File
@@ -1,11 +0,0 @@
# ALPHA-MR cab layout -- VERIFIED BY EYE 2026-08-06 (do not re-guess from
# desktop coordinates; they say nothing about physical cab position).
# DISPLAY4 -> the COLOUR RADAR LCD in the yellow-button frame, so the colour
# surface goes there. (An RGB composite landed here first and
# showed all three channels overlaid.)
# DISPLAY2 -> the VGA port feeding the Heat + Comm + Mfd2 CRT group.
# DISPLAY1 -> the VGA port feeding the Mfd1 + Mfd3 CRT group.
# monitor: binding, not pixel rects -- the USB graphics adapter re-enumerates.
Secondary / Radar=monitor:DISPLAY4,bare
VGA Port A=monitor:DISPLAY2,bare
VGA Port B=monitor:DISPLAY1,bare
-8
View File
@@ -1,8 +0,0 @@
# IDENTIFY PASS -- assignment here is arbitrary on purpose. Each panel prints
# its own name + monitor, so we map the cab from what the GLASS says, not from
# desktop coordinate order (which is what got it wrong the first time).
#
# Bound by MONITOR IDENTITY, not x,y: survives reboots and adapter renumbering.
VGA Port A=monitor:DISPLAY4,bare
VGA Port B=monitor:DISPLAY2,bare
Secondary / Radar=monitor:DISPLAY1,bare
-8
View File
@@ -1,8 +0,0 @@
# Corrected from the cab photo (2026-08-06): DISPLAY4 feeds the COLOUR RADAR
# LCD -- the RGB composite was landing there and showing all three channels
# overlaid. Colour surface to the colour panel; the RGB composites to the
# displays that feed the mono CRT groups. Which CRT group is DISPLAY2 vs
# DISPLAY1 is still unknown -- the channel test settles it.
Secondary / Radar=monitor:DISPLAY4,bare
VGA Port A=monitor:DISPLAY2,bare
VGA Port B=monitor:DISPLAY1,bare
-25
View File
@@ -1,25 +0,0 @@
Add-Type @"
using System;using System.Text;using System.Runtime.InteropServices;
public class W{
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb,IntPtr p);
public delegate bool EnumWindowsProc(IntPtr h,IntPtr p);
[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h,StringBuilder s,int c);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h,out uint p);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h,out R r);
public struct R{public int L,T,Rr,B;}
}
"@
$target = (Get-Process btl4 -ErrorAction SilentlyContinue).Id
if (-not $target) { Write-Output "btl4 NOT RUNNING"; exit }
[W]::EnumWindows({ param($h,$l)
$o = 0
[W]::GetWindowThreadProcessId($h, [ref]$o) | Out-Null
if ($o -eq $target -and [W]::IsWindowVisible($h)) {
$sb = New-Object System.Text.StringBuilder 256
[W]::GetWindowText($h, $sb, 256) | Out-Null
$r = New-Object 'W+R'
[W]::GetWindowRect($h, [ref]$r) | Out-Null
Write-Output ("WIN '" + $sb.ToString() + "' at " + $r.L + "," + $r.T + " " + ($r.Rr-$r.L) + "x" + ($r.B-$r.T))
}
return $true }, [IntPtr]::Zero) | Out-Null
-21
View File
@@ -1,21 +0,0 @@
# Merge the POD PROFILE block into the cart's content\environ.ini, idempotently.
# Any previous block between the two markers is replaced, so re-running is safe.
$ini = 'C:\bt411\BT411_4.11.801\content\environ.ini'
$prof = 'C:\bt411\BT411_4.11.801\content\podprofile.ini'
$block = Get-Content $prof
$keep = @()
if (Test-Path $ini) {
$inBlock = $false
foreach ($line in (Get-Content $ini)) {
if ($line -match '^# ==== BT411 POD PROFILE') { $inBlock = $true; continue }
if ($line -match '^# ==== END BT411 POD PROFILE') { $inBlock = $false; continue }
if (-not $inBlock) { $keep += $line }
}
}
# Trim trailing blanks so repeated merges do not grow the file.
while ($keep.Count -gt 0 -and $keep[-1].Trim() -eq '') { $keep = $keep[0..($keep.Count-2)] }
($keep + @('') + $block) | Set-Content $ini -Encoding ascii
Write-Output "environ.ini: $((Get-Content $ini).Count) lines, profile keys:"
Get-Content $ini | Select-String '^(BT_|L4)' | ForEach-Object { $_.Line }
-48
View File
@@ -1,48 +0,0 @@
# Apply the ALPHA-MR pod kit to a BT411 install's content directory.
#
# WHY THIS EXISTS: the frozen rig config lives in content\environ.ini and
# glass_layout.cfg -- both INSIDE the versioned install folder, and neither is
# shipped in the zip (environ.ini is generated on first run). So extracting a
# new build gives you a cab that comes up wrong with no error anywhere. The
# MASTERS live beside this script at a stable path; this pushes them into
# whichever install you point it at. Idempotent -- safe to re-run.
param([Parameter(Mandatory=$true)][string]$Content)
$here = Split-Path -Parent $MyInvocation.MyCommand.Definition
$prof = Join-Path $here 'podprofile.ini'
$lay = Join-Path $here 'glass_layout.cfg'
$ini = Join-Path $Content 'environ.ini'
if (-not (Test-Path $Content)) { Write-Output "NO SUCH CONTENT DIR: $Content"; exit 1 }
if (-not (Test-Path $prof)) { Write-Output "MISSING MASTER: $prof"; exit 1 }
# --- the env block: replace anything between the markers, keep the rest ------
$block = Get-Content $prof
$keep = @()
if (Test-Path $ini) {
$inBlock = $false
foreach ($line in (Get-Content $ini)) {
if ($line -match '^# ==== BT411 POD PROFILE') { $inBlock = $true; continue }
if ($line -match '^# ==== END BT411 POD PROFILE') { $inBlock = $false; continue }
if (-not $inBlock) { $keep += $line }
}
while ($keep.Count -gt 0 -and $keep[-1].Trim() -eq '') { $keep = $keep[0..($keep.Count-2)] }
}
($keep + @('') + $block) | Set-Content $ini -Encoding ascii
# --- the layout: the master always wins -------------------------------------
# Re-tuning the cab means editing the MASTER beside this script, not the copy
# in the install -- a BT_GLASS_LAYOUT=save drag inside an install is overwritten
# on the next apply, on purpose. One place to look when a panel moves.
if (Test-Path $lay) { Copy-Item $lay (Join-Path $Content 'glass_layout.cfg') -Force }
# --- the pod test mission ----------------------------------------------------
# PODTEST.EGG is NOT in the repo -- it was made on the cart, so a fresh extract
# has no mission for runpod.bat to launch (found the first time a new build was
# dropped: the launcher ran and nothing came up). Carry it in the kit.
$egg = Join-Path $here 'PODTEST.EGG'
if (Test-Path $egg) { Copy-Item $egg (Join-Path $Content 'PODTEST.EGG') -Force }
Write-Output "pod kit applied to $Content"
Get-Content $ini | Select-String '^(BT_|L4)' | ForEach-Object { " " + $_.Line }
if (Test-Path $lay) { Get-Content (Join-Path $Content 'glass_layout.cfg') | Select-String '^[A-Z]' | ForEach-Object { " " + $_.Line } }
-55
View File
@@ -1,55 +0,0 @@
# ==== BT411 POD PROFILE (ALPHA-MR crash cart) ================================
# Frozen 2026-08-06 from the verified-good bring-up. Everything a launcher
# used to have to pass now lives here, so the rig comes up correct however
# the game is started -- shortcut, scheduled task or autostart.
# Physical mapping + the RGB-split decode: context/pod-hardware.md.
#
# The layout itself (which surface sits on which monitor) is in
# glass_layout.cfg beside this file, loaded by BT_GLASS_LAYOUT=load:
# Secondary / Radar = monitor:DISPLAY4 (colour LCD in the yellow frame)
# VGA Port A = monitor:DISPLAY2 (RGB triple: Heat + Comm + Mfd2)
# VGA Port B = monitor:DISPLAY1 (RGB triple: Mfd1 + Mfd3)
# Platform PROFILE. NB "glass", not "pod": BT_PLATFORM=pod selects the 1995
# multi-surface gauge path (exclusive-fullscreen D3D per adapter + the NVIDIA
# horizontal span), which no modern driver can do. What lights this cab is the
# GLASS profile plus the three switches below. (Glass is already the default
# when nothing is set -- named here so the rig never depends on that default.)
BT_PLATFORM=glass
# One window per surface -- this is the switch that makes panels at all.
BT_GLASS_PANELS=1
# ONE VGA port's R/G/B lines drive THREE mono MFDs -- composite the planes.
BT_POD_RGB=1
# THIS cab's splitter wires Comm and Heat to different colour lines than the
# 1995 L4GAUGE.CFG assumed (kills/deaths and coolant loops came up swapped).
BT_POD_CHANMAP=Comm=blue,Heat=red
# The radar panel here is a LANDSCAPE LCD; the 1995 pod rotated only because
# its CRT was mounted portrait. 0 = leave the 640x480 source alone.
BT_GAUGE_SEC_ROT=0
BT_GLASS_LAYOUT=load
BT_START_INSIDE=1
# Main view fills its 800x600 panel (DISPLAY3, the primary) with no border.
BT_FIT=1
# THE REAL COCKPIT BOARD. RIO:COM1 -> \\.\COM1 at 9600 8N1 (L4SERIAL); the
# stock control path above the seam (mapper, lamps, streamed .CTL mappings)
# runs unchanged -- PadRIO was only ever standing in for this.
# * RIO and PAD are MUTUALLY EXCLUSIVE: both assign rioPointer in the
# L4CONTROLS parser, so the last token wins. This turns the XInput pad
# off on this machine. To get it back for a session, override in the
# launcher: set L4CONTROLS=PAD,KEYBOARD (the real environment wins).
# * KEYBOARD stays in the list as the fallback. It does NOT demote the RIO
# (it only sets a flag), and a silent total loss of input would be
# indistinguishable from a hang.
L4CONTROLS=RIO:COM1,KEYBOARD
# No 128x32 marquee wired on this cart -- and the glass profile would
# otherwise default L4PLASMA to SCREEN and drop a plasma window on the glass.
L4PLASMA=NONE
# ==== END BT411 POD PROFILE ==================================================
+9 -19
View File
@@ -1,22 +1,12 @@
@echo off
REM BT411 pod -- ALPHA-MR. Resolves the NEWEST BT411_* install, applies the
REM frozen rig config (so a fresh zip drop needs no hand-editing), and launches
REM straight into a mission. The rig itself is frozen in content\environ.ini;
REM this carries only the log switches and the mission.
REM Fallback if the kit is ever lost: runpod_env.bat sets everything inline.
setlocal
set ROOT=C:\bt411
set INSTALL=
for /f "delims=" %%d in ('dir /b /ad /o-d "%ROOT%\BT411_*" 2^>nul') do (
if not defined INSTALL set INSTALL=%ROOT%\%%d
)
if not defined INSTALL goto noinstall
powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\podkit.ps1" -Content "%INSTALL%\content" >nul
cd /d %INSTALL%\content
REM BT411 pod launch -- console session (started via schtasks /IT).
cd /d C:\bt411\BT411_4.11.801\content
set BT_GLASS=1
set BT_GLASS_PANELS=1
set BT_POD_RGB=1
set BT_GLASS_LAYOUT=load
set BT_GLASS_LOG=1
set BT_START_INSIDE=1
set BT_FE_SOLO=1
set BT_LOG=podrun.log
start "" ..\build\Release\btl4.exe -egg PODTEST.EGG
exit /b
:noinstall
echo No BT411_* folder found under %ROOT%.
exit /b 1
start "" ..\build\Release\btl4.exe
-16
View File
@@ -1,16 +0,0 @@
@echo off
REM BT411 pod -- ALPHA-MR working config.
REM layout : radar on DISPLAY4 (the colour LCD), RGB composites on 2 and 1
REM fix 1 : Comm/Heat ride swapped colour lines on THIS cab's splitter
REM fix 2 : radar panel is landscape -> UNROTATED landscape, not the pod portrait CRT
cd /d C:\bt411\BT411_4.11.801\content
set BT_GLASS=1
set BT_GLASS_PANELS=1
set BT_POD_RGB=1
set BT_POD_CHANMAP=Comm=blue,Heat=red
set BT_GAUGE_SEC_ROT=0
set BT_GLASS_LAYOUT=load
set BT_GLASS_LOG=1
set BT_START_INSIDE=1
set BT_LOG=podrun.log
start "" ..\build\Release\btl4.exe -egg PODTEST.EGG
-24
View File
@@ -1,24 +0,0 @@
@echo off
REM BT411 pod -- HARDWARE RIO test. Same frozen glass/RGB rig, but the input
REM device is the real cockpit board on COM1 instead of PadRIO.
REM L4CONTROLS=RIO:COM1 -> RIO opens \.\COM1 at 9600 8N1 (L4SERIAL).
REM KEYBOARD stays in the list so the cab is still playable if the board is
REM dead -- a silent loss of all input would be indistinguishable from a hang.
setlocal
set ROOT=C:\bt411
set INSTALL=
for /f "delims=" %%d in ('dir /b /ad /o-d "%ROOT%\BT411_*" 2^>nul') do (
if not defined INSTALL set INSTALL=%ROOT%\%%d
)
if not defined INSTALL goto noinstall
powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\podkit.ps1" -Content "%INSTALL%\content" >nul
cd /d %INSTALL%\content
set L4CONTROLS=RIO:COM1,KEYBOARD
set BT_CTRLMAP_LOG=1
set BT_GLASS_LOG=1
set BT_LOG=podrio.log
start "" ..\build\Release\btl4.exe -egg PODTEST.EGG
exit /b
:noinstall
echo No BT411_* folder found under %ROOT%.
exit /b 1
-24
View File
@@ -1,24 +0,0 @@
@echo off
REM ============================================================================
REM RUN THIS AFTER EXTRACTING A NEW BT411 BUILD.
REM It finds the newest BT411_* folder under C:\bt411 and pushes the cab's
REM frozen rig config into it (environ.ini block + glass_layout.cfg).
REM Without it a fresh extract comes up with the MFDs wrong and no error.
REM ============================================================================
setlocal
set ROOT=C:\bt411
set INSTALL=
for /f "delims=" %%d in ('dir /b /ad /o-d "%ROOT%\BT411_*" 2^>nul') do (
if not defined INSTALL set INSTALL=%ROOT%\%%d
)
if not defined INSTALL goto noinstall
echo Newest install: %INSTALL%
powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\podkit.ps1" -Content "%INSTALL%\content"
echo.
echo Done. play_solo.bat / join.bat / play_steam.bat in that folder will now
echo all come up on the pod glass -- they inherit it from environ.ini.
pause
exit /b
:noinstall
echo No BT411_* folder found under %ROOT%. Extract the zip there first.
pause
-101
View File
@@ -1,101 +0,0 @@
@echo off
REM ===========================================================================
REM podbringup.bat -- ONE double-click pod bring-up. BT411.
REM
REM 1. self-elevates (accept the UAC prompt -- that is the only interaction)
REM 2. installs the built-in Windows OpenSSH SERVER
REM 3. authorizes ONE key (the laptop's bt411 key) and locks its ACL
REM 4. opens SSH on private/domain profiles ONLY (never the public internet)
REM 5. probes the display/GPU/monitor/serial topology
REM 6. sends the results BACK over Tailscale -- no copying by hand
REM
REM Undo (admin cmd):
REM sc stop sshd & sc config sshd start= disabled
REM del C:\ProgramData\ssh\administrators_authorized_keys
REM netsh advfirewall firewall delete rule name="BT411 SSH"
REM ===========================================================================
REM ---- 1. elevate -----------------------------------------------------------
net session >nul 2>&1
if %errorlevel% neq 0 (
echo Requesting administrator rights -- click YES on the prompt...
powershell -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
exit /b
)
setlocal enabledelayedexpansion
set LAPTOP=desktop-ae1su9u
set TS=C:\PROGRA~1\Tailscale\tailscale.exe
set REPORT=%~dp0podbringup_report.txt
set DXOUT=%~dp0podbringup_dxdiag.txt
echo ==================== BT411 POD BRING-UP ====================> "%REPORT%"
echo host=%COMPUTERNAME% user=%USERNAME% session=%SESSIONNAME%>> "%REPORT%"
ver >> "%REPORT%"
echo.>> "%REPORT%"
REM ---- 2. OpenSSH server ----------------------------------------------------
echo [1/5] installing OpenSSH server (this can take a minute)...
echo ---- OpenSSH install ---->> "%REPORT%"
dism /online /quiet /norestart /add-capability /capabilityname:OpenSSH.Server~~~~0.0.1.0 >> "%REPORT%" 2>&1
sc config sshd start= auto >> "%REPORT%" 2>&1
net start sshd >> "%REPORT%" 2>&1
sc query sshd | findstr /i "STATE" >> "%REPORT%" 2>&1
REM ---- 3. authorize the laptop key -----------------------------------------
echo [2/5] authorizing the BT411 key...
if not exist C:\ProgramData\ssh mkdir C:\ProgramData\ssh
set AKF=C:\ProgramData\ssh\administrators_authorized_keys
findstr /c:"bt411-claude-code@laptop" "%AKF%" >nul 2>&1
if errorlevel 1 (
echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICmGkinDbZMwQ/UhwqU/oECUIeAN+pBjLgsekj41di7W bt411-claude-code@laptop>> "%AKF%"
)
icacls "%AKF%" /inheritance:r /grant "SYSTEM:F" /grant "BUILTIN\Administrators:F" >> "%REPORT%" 2>&1
echo ---- authorized_keys ---->> "%REPORT%"
type "%AKF%" >> "%REPORT%" 2>&1
REM ---- 4. firewall (private/domain only) ------------------------------------
echo [3/5] firewall rule (private/tailnet only)...
netsh advfirewall firewall show rule name="BT411 SSH" >nul 2>&1
if errorlevel 1 (
netsh advfirewall firewall add rule name="BT411 SSH" dir=in action=allow protocol=TCP localport=22 profile=private,domain >> "%REPORT%" 2>&1
)
REM ---- 5. topology probe ----------------------------------------------------
echo [4/5] probing displays / GPUs / monitors / serial ports...
echo.>> "%REPORT%"
echo ---- OS ---->> "%REPORT%"
wmic os get Caption,Version,BuildNumber,OSArchitecture /format:list >> "%REPORT%" 2>&1
echo ---- GPUs ---->> "%REPORT%"
wmic path Win32_VideoController get Name,DriverVersion,DriverDate,CurrentHorizontalResolution,CurrentVerticalResolution /format:list >> "%REPORT%" 2>&1
echo ---- Monitors ---->> "%REPORT%"
wmic path Win32_DesktopMonitor get DeviceID,Name,ScreenWidth,ScreenHeight,Availability /format:list >> "%REPORT%" 2>&1
echo ---- Serial ports (RIO) ---->> "%REPORT%"
wmic path Win32_SerialPort get DeviceID,Name /format:list >> "%REPORT%" 2>&1
echo ---- Tailscale ---->> "%REPORT%"
"%TS%" ip -4 >> "%REPORT%" 2>&1
echo ---- CONNECT WITH ---->> "%REPORT%"
echo ssh -i ~/.ssh/bt411_pod %USERNAME%@bt411-pod>> "%REPORT%"
echo [5/5] dxdiag (monitor rects + D3D caps, ~30s)...
dxdiag /whql:off /t "%DXOUT%"
REM ---- 6. send it all back --------------------------------------------------
echo Sending results back to %LAPTOP% over Tailscale...
"%TS%" file cp "%REPORT%" "%DXOUT%" %LAPTOP%:
if errorlevel 1 (
echo.
echo ! Taildrop send failed -- right-click these two files and use
echo "Send with Tailscale" instead:
echo %REPORT%
echo %DXOUT%
) else (
echo ...sent.
)
echo.
echo ================= DONE =================
echo ssh user: %USERNAME% host: bt411-pod
type "%REPORT%" | findstr /i "STATE ssh-ed25519 Caption"
echo.
pause