Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39144813a4 | ||
|
|
d213c980a9 | ||
|
|
6fcff95010 | ||
|
|
05d8164801 | ||
|
|
d96fa4f4e2 | ||
|
|
654277bb7b |
@@ -156,6 +156,26 @@ 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.
|
||||
@@ -183,6 +203,15 @@ 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
|
||||
@@ -214,7 +243,10 @@ 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.
|
||||
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
|
||||
|
||||
⚠ **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
|
||||
|
||||
+135
-7
@@ -217,12 +217,21 @@ 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. 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.
|
||||
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.
|
||||
|
||||
## 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
|
||||
@@ -249,7 +258,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=1 BT_GLASS_PANELS=1
|
||||
2. Drop the cfg in the game's working directory; run with `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 =
|
||||
@@ -262,6 +271,125 @@ 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:
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#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
|
||||
|
||||
//
|
||||
@@ -865,6 +869,29 @@ 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)
|
||||
//--------------------------------------------------
|
||||
@@ -1089,6 +1116,17 @@ 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();
|
||||
}
|
||||
|
||||
|
||||
+382
-12
@@ -123,6 +123,10 @@ 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];
|
||||
@@ -253,17 +257,26 @@ 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(RadarSurfW, RadarSurfH, &metrics);
|
||||
BTRioBankMetricsFor(surfW, surfH, &metrics);
|
||||
|
||||
BTRioBank bank;
|
||||
BTRioBankLayout(BTRioBankRadar, 0, 0, RadarSurfW, RadarSurfH,
|
||||
BTRioBankLayout(BTRioBankRadar, 0, 0, surfW, surfH,
|
||||
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 + RadarSurfW, dy + RadarSurfH);
|
||||
SetRect(&w.surfaceRect, dx, dy, dx + surfW, dy + surfH);
|
||||
w.clientW = bank.boundsW;
|
||||
w.clientH = bank.boundsH;
|
||||
}
|
||||
@@ -426,6 +439,116 @@ 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.
|
||||
@@ -609,6 +732,73 @@ 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.
|
||||
@@ -646,7 +836,34 @@ static void
|
||||
*end = '\0';
|
||||
|
||||
int x = 0, y = 0, w = 0, h = 0;
|
||||
if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
|
||||
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)
|
||||
continue;
|
||||
|
||||
// Trailing options after the numbers, comma-separated. Unknown ones
|
||||
@@ -666,11 +883,30 @@ static void
|
||||
GWin *gw = FindGWinByTitle(s);
|
||||
if (gw == NULL)
|
||||
continue;
|
||||
gw->wantX = x;
|
||||
gw->wantY = y;
|
||||
gw->noFrame = noframe;
|
||||
// "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;
|
||||
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;
|
||||
}
|
||||
@@ -841,6 +1077,19 @@ 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);
|
||||
|
||||
@@ -880,9 +1129,9 @@ static void
|
||||
if (ga != NULL && ga->GetEnableID() != L4GraphicsPort::BlankColor)
|
||||
gp = ga;
|
||||
}
|
||||
const int ch = gp->GetEnableID();
|
||||
const int ch = ResolvedChannelOf(w->groupPort[gi], gp);
|
||||
unsigned long chMask;
|
||||
switch (ch & 0x7F)
|
||||
switch (ch)
|
||||
{
|
||||
case L4GraphicsPort::RedChannel: chMask = 0x00FF0000ul; break;
|
||||
case L4GraphicsPort::GreenChannel: chMask = 0x0000FF00ul; break;
|
||||
@@ -1069,6 +1318,113 @@ 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,
|
||||
@@ -1205,11 +1561,23 @@ void
|
||||
if (gWinCount != 0)
|
||||
return; // already up
|
||||
|
||||
// BT_GAUGE_SEC_ROT: the portrait-CRT unrotation direction (shared with the
|
||||
// D3D dev path; default 3 = CW = user-verified upright).
|
||||
// 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.)
|
||||
{
|
||||
const char *rv = getenv("BT_GAUGE_SEC_ROT");
|
||||
gRadarRot = (rv != NULL && rv[0] == '1') ? 1 : 3;
|
||||
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;
|
||||
}
|
||||
|
||||
memset(latched, 0, sizeof(latched));
|
||||
@@ -1348,6 +1716,8 @@ 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;
|
||||
|
||||
@@ -90,6 +90,23 @@ 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
|
||||
|
||||
@@ -739,6 +739,34 @@ 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).
|
||||
int ow = h, oh = w;
|
||||
|
||||
+42
-1
@@ -407,6 +407,32 @@ 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)
|
||||
@@ -848,6 +874,18 @@ 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
|
||||
@@ -1145,8 +1183,11 @@ 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 (PadRIO + plasma window)"
|
||||
: gBTPlatformGlass ? "GLASS"
|
||||
: "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;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 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
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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 }
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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 } }
|
||||
@@ -0,0 +1,55 @@
|
||||
# ==== 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 ==================================================
|
||||
@@ -1,12 +1,22 @@
|
||||
@echo off
|
||||
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
|
||||
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
|
||||
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
|
||||
start "" ..\build\Release\btl4.exe -egg PODTEST.EGG
|
||||
exit /b
|
||||
:noinstall
|
||||
echo No BT411_* folder found under %ROOT%.
|
||||
exit /b 1
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@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
|
||||
@@ -0,0 +1,24 @@
|
||||
@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
|
||||
@@ -0,0 +1,24 @@
|
||||
@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
|
||||
@@ -0,0 +1,101 @@
|
||||
@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
|
||||
Reference in New Issue
Block a user