Adds a section resolving each screen's panel identity via the SAME Win32 API the game uses (EnumDisplayDevices on the display's MONITOR child) rather than the WmiMonitorID query above -- if probe and engine read different sources the printed fragments could fail to match what the engine tests. Verified on the dev box: probe and engine emit byte-identical identities. Answers the open [T3] multi-panel question WITHOUT deploying a build (pure OS data). When one EDID code appears on SEVERAL panels (identical MFD models) it detects the collision and emits the per-connector UID form instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
258 lines
12 KiB
PowerShell
258 lines
12 KiB
PowerShell
# podprobe.ps1 -- pod / crash-cart display + I/O topology probe.
|
|
#
|
|
# Run ON THE POD PC (PowerShell, no install, no admin):
|
|
# powershell -ExecutionPolicy Bypass -File podprobe.ps1
|
|
#
|
|
# Prints and writes podprobe.txt: GPUs, every monitor with its VIRTUAL DESKTOP
|
|
# rect (the coordinates glass_layout.cfg needs), EDID make/model (to identify
|
|
# the original pod panels), serial ports (the RIO board), and a PROPOSED
|
|
# glass_layout.cfg mapping the six pod surfaces onto the non-primary monitors.
|
|
#
|
|
# Then, in the game's working directory:
|
|
# copy the proposed glass_layout.cfg in
|
|
# set BT_GLASS=1 & set BT_GLASS_PANELS=1 & set BT_POD_SURFACES=1
|
|
# set BT_GLASS_LAYOUT=load (add =save to persist drags instead)
|
|
# btl4.exe -platform pod
|
|
# The log prints one "[glasswin] '<title>' ... -> monitor \\.\DISPLAYn" line per
|
|
# surface: that is the receipt that a picture landed on the right panel.
|
|
|
|
$ErrorActionPreference = 'Continue'
|
|
$out = New-Object System.Collections.ArrayList
|
|
function W($s) { [void]$out.Add($s); Write-Host $s }
|
|
|
|
W "==================== BT411 POD PROBE ===================="
|
|
W ("host : {0}" -f $env:COMPUTERNAME)
|
|
W ("when : {0}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))
|
|
W ("windows : {0}" -f (Get-CimInstance Win32_OperatingSystem).Caption)
|
|
W ""
|
|
|
|
W "---- WILL OUR BUILD EVEN RUN HERE? ----"
|
|
try {
|
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
$ver = [Version]$os.Version
|
|
W (" OS version : {0} (build {1})" -f $os.Version, $os.BuildNumber)
|
|
W (" architecture: {0}" -f $os.OSArchitecture)
|
|
if ($ver.Major -lt 6) {
|
|
W " *** STOP: this is Windows XP/2003-era. btl4.exe is built with a modern MSVC"
|
|
W " toolset and WILL NOT LAUNCH here (CRT + API requirements). Options:"
|
|
W " (a) put a newer Windows on the cart, or (b) rebuild with the v141_xp"
|
|
W " toolset (VS2017), or (c) drive the panels from a modern PC instead."
|
|
} elseif ($ver.Major -eq 6 -and $ver.Minor -lt 1) {
|
|
W " *** WARNING: Vista-era. Untested; Win7 SP1+ is the practical minimum."
|
|
} else {
|
|
W " OK: modern enough for the current build."
|
|
}
|
|
} catch { W " (OS query failed: $_)" }
|
|
W ""
|
|
|
|
W "---- GPUs ----"
|
|
try {
|
|
Get-CimInstance Win32_VideoController | ForEach-Object {
|
|
W (" {0}" -f $_.Name)
|
|
W (" driver {0} ({1}) mode {2}x{3} ram {4} MB" -f `
|
|
$_.DriverVersion, $_.DriverDate, $_.CurrentHorizontalResolution,
|
|
$_.CurrentVerticalResolution, [int]($_.AdapterRAM / 1MB))
|
|
}
|
|
} catch { W " (video controller query failed: $_)" }
|
|
W ""
|
|
|
|
W "---- MONITORS (virtual-desktop rects -- these are the layout coordinates) ----"
|
|
Add-Type -AssemblyName System.Windows.Forms | Out-Null
|
|
$screens = [System.Windows.Forms.Screen]::AllScreens
|
|
$i = 0
|
|
foreach ($s in $screens) {
|
|
$b = $s.Bounds
|
|
W (" [{0}] {1}{2} x={3} y={4} {5}x{6} bpp={7}" -f `
|
|
$i, $s.DeviceName, $(if ($s.Primary) { " *PRIMARY*" } else { "" }),
|
|
$b.X, $b.Y, $b.Width, $b.Height, $s.BitsPerPixel)
|
|
$i++
|
|
}
|
|
W (" total screens: {0}" -f $screens.Count)
|
|
W ""
|
|
|
|
W "---- EDID identity (which physical panel is which) ----"
|
|
try {
|
|
Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID | ForEach-Object {
|
|
$mk = -join ($_.ManufacturerName | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })
|
|
$nm = -join ($_.UserFriendlyName | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })
|
|
$sn = -join ($_.SerialNumberID | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })
|
|
W (" {0} | make={1} model='{2}' serial={3} year={4}" -f `
|
|
$_.InstanceName, $mk, $nm, $sn, $_.YearOfManufacture)
|
|
}
|
|
} catch { W " (EDID query failed -- normal over some remote sessions: $_)" }
|
|
W ""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BOOT-STABLE panel identity. Windows renumbers \\.\DISPLAYn and reorders the
|
|
# enumeration when a panel is power-cycled or re-cabled, so binding the pod by
|
|
# index or device name is fragile (Nick, 2026-08-07: "the order changed ... even
|
|
# if the visual desktop tool looks the same"). Bind by the panel's own EDID
|
|
# identity instead: glass_layout.cfg accepts `monitor:id:<fragment>`.
|
|
#
|
|
# This deliberately calls the SAME Win32 API the game does (EnumDisplayDevices
|
|
# on the display's MONITOR child) rather than the WmiMonitorID above -- if the
|
|
# probe and the engine read different sources, the fragments printed here might
|
|
# not be what the engine actually matches against.
|
|
# ---------------------------------------------------------------------------
|
|
W "---- BOOT-STABLE panel identity (paste these into glass_layout.cfg) ----"
|
|
try {
|
|
if (-not ("BTDisp" -as [type])) {
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
public class BTDisp {
|
|
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
|
|
public struct DISPLAY_DEVICE {
|
|
public int cb;
|
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string DeviceName;
|
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceString;
|
|
public int StateFlags;
|
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceID;
|
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceKey;
|
|
}
|
|
[DllImport("user32.dll", CharSet=CharSet.Ansi)]
|
|
public static extern bool EnumDisplayDevicesA(string dev, uint num, ref DISPLAY_DEVICE dd, uint flags);
|
|
public static string MonitorId(string display) {
|
|
DISPLAY_DEVICE dd = new DISPLAY_DEVICE();
|
|
dd.cb = Marshal.SizeOf(typeof(DISPLAY_DEVICE));
|
|
// 0x1 = EDD_GET_DEVICE_INTERFACE_NAME (richer path, includes connector UID)
|
|
if (!EnumDisplayDevicesA(display, 0, ref dd, 0x1)) {
|
|
dd = new DISPLAY_DEVICE();
|
|
dd.cb = Marshal.SizeOf(typeof(DISPLAY_DEVICE));
|
|
if (!EnumDisplayDevicesA(display, 0, ref dd, 0)) return "";
|
|
}
|
|
return dd.DeviceID;
|
|
}
|
|
}
|
|
'@
|
|
}
|
|
$codes = @{}
|
|
$rows = @()
|
|
foreach ($s in [System.Windows.Forms.Screen]::AllScreens) {
|
|
$sid = [BTDisp]::MonitorId($s.DeviceName)
|
|
# EDID PnP code = 3 letters + 4 hex digits (AUO10ED, DEL4231)
|
|
$code = ""
|
|
if ($sid -match '[\\#\?]([A-Za-z]{3}[0-9A-Fa-f]{4})[\\#\?]') { $code = $Matches[1] }
|
|
elseif ($sid -match '([A-Za-z]{3}[0-9A-Fa-f]{4})') { $code = $Matches[1] }
|
|
$rows += [pscustomobject]@{ Dev=$s.DeviceName; Prim=$s.Primary; Sid=$sid; Code=$code
|
|
X=$s.Bounds.X; Y=$s.Bounds.Y; W=$s.Bounds.Width; H=$s.Bounds.Height }
|
|
if ($code -ne "") { $codes[$code] = 1 + $(if ($codes.ContainsKey($code)) { $codes[$code] } else { 0 }) }
|
|
}
|
|
foreach ($r in $rows) {
|
|
W (" {0}{1} {2},{3} {4}x{5}" -f $r.Dev, $(if ($r.Prim) { " *PRIMARY*" } else { "" }), $r.X, $r.Y, $r.W, $r.H)
|
|
W (" stable-id : {0}" -f $(if ($r.Sid) { $r.Sid } else { "(unavailable)" }))
|
|
if ($r.Code -ne "" -and $codes[$r.Code] -gt 1) {
|
|
# Same model on more than one output: the EDID code alone is ambiguous.
|
|
# The connector UID in the tail is what separates them.
|
|
$uid = ""
|
|
if ($r.Sid -match '(UID[0-9]+)') { $uid = $Matches[1] }
|
|
if ($uid -ne "") {
|
|
W (" cfg form : monitor:id:{0} <-- code '{1}' is on {2} panels, so use the UID" -f $uid, $r.Code, $codes[$r.Code])
|
|
} else {
|
|
W (" cfg form : (AMBIGUOUS -- '{0}' appears on {1} panels and no UID found;" -f $r.Code, $codes[$r.Code])
|
|
W " use a longer unique substring of stable-id above)"
|
|
}
|
|
} elseif ($r.Code -ne "") {
|
|
W (" cfg form : monitor:id:{0}" -f $r.Code)
|
|
} else {
|
|
W " cfg form : (no EDID code parsed -- use a substring of stable-id)"
|
|
}
|
|
}
|
|
$dupes = ($codes.GetEnumerator() | Where-Object { $_.Value -gt 1 } | Measure-Object).Count
|
|
if ($dupes -gt 0) {
|
|
W ""
|
|
W (" NOTE: {0} EDID code(s) appear on more than one panel (identical models)." -f $dupes)
|
|
W " Those lines use the connector UID instead, which is per-output."
|
|
}
|
|
W ""
|
|
W " Run this again AFTER a reboot or a panel power-cycle: the device names"
|
|
W " and ordering above may move, but stable-id / cfg form must NOT."
|
|
} catch { W " (stable-identity probe failed: $_)" }
|
|
W ""
|
|
|
|
W "---- SERIAL PORTS (the RIO board lives on one of these) ----"
|
|
try {
|
|
$sp = Get-CimInstance Win32_SerialPort
|
|
if ($sp) { $sp | ForEach-Object { W (" {0} {1}" -f $_.DeviceID, $_.Name) } }
|
|
else {
|
|
$reg = 'HKLM:\HARDWARE\DEVICEMAP\SERIALCOMM'
|
|
if (Test-Path $reg) {
|
|
(Get-ItemProperty $reg).PSObject.Properties |
|
|
Where-Object { $_.Name -notlike 'PS*' } |
|
|
ForEach-Object { W (" {0} -> {1}" -f $_.Name, $_.Value) }
|
|
} else { W " (none found)" }
|
|
}
|
|
} catch { W " (serial query failed: $_)" }
|
|
W ""
|
|
|
|
W "---- REMOTE SESSION ----"
|
|
W (" SESSIONNAME={0} (a console session drives the real panels; an RDP" -f $env:SESSIONNAME)
|
|
W " session gets a VIRTUAL display and will NOT light the pod monitors."
|
|
W " Chrome Remote Desktop attaches to the CONSOLE -- that is what we want."
|
|
W ""
|
|
|
|
# ---------------- proposed layout ----------------
|
|
# Pod surface roles, in the order the cab reads them. Sizes are the native
|
|
# surface sizes the game creates in BT_POD_SURFACES mode.
|
|
$roles = @(
|
|
@{ title = 'Heat MFD'; w = 640; h = 480; note = 'upper LEFT (coolant)' },
|
|
@{ title = 'Engineering'; w = 640; h = 480; note = 'upper CENTER (engineering)' },
|
|
@{ title = 'Comm MFD'; w = 640; h = 480; note = 'upper RIGHT (hot box / comm)' },
|
|
@{ title = 'Left Weapons'; w = 640; h = 480; note = 'lower LEFT (weapons)' },
|
|
@{ title = 'Right Weapons'; w = 640; h = 480; note = 'lower RIGHT (weapons)' },
|
|
@{ title = 'Secondary / Radar'; w = 480; h = 640; note = 'secondary screen (radar, portrait)' }
|
|
)
|
|
|
|
$targets = @($screens | Where-Object { -not $_.Primary } | Sort-Object { $_.Bounds.Y }, { $_.Bounds.X })
|
|
W "---- PROPOSED glass_layout.cfg ----"
|
|
if ($targets.Count -eq 0) {
|
|
W " ! No non-primary monitors found. Either the pod panels are not attached to"
|
|
W " this PC, or this is a remote/virtual display session. Nothing to map yet."
|
|
} else {
|
|
W (" {0} non-primary monitor(s) available; assigning in top-to-bottom, left-to-right order." -f $targets.Count)
|
|
if ($targets.Count -lt $roles.Count) {
|
|
W (" ! Fewer panels than surfaces -- the last {0} surface(s) stay on the primary display." -f ($roles.Count - $targets.Count))
|
|
}
|
|
}
|
|
|
|
$cfg = New-Object System.Collections.ArrayList
|
|
[void]$cfg.Add('# BT411 glass cockpit window layout -- generated by tools/podprobe.ps1')
|
|
[void]$cfg.Add('# "<title>=x,y,w,h,noframe,bare" (bare = surface only, no desktop buttons)')
|
|
[void]$cfg.Add('# Coordinates are VIRTUAL DESKTOP pixels; run the probe again after any')
|
|
[void]$cfg.Add('# display rearrangement, because Windows renumbers the desktop origin.')
|
|
for ($r = 0; $r -lt $roles.Count; $r++) {
|
|
$role = $roles[$r]
|
|
if ($r -lt $targets.Count) {
|
|
$b = $targets[$r].Bounds
|
|
# centre the surface on the panel (exact fit when the panel is 640x480)
|
|
$x = $b.X + [int](($b.Width - $role.w) / 2)
|
|
$y = $b.Y + [int](($b.Height - $role.h) / 2)
|
|
$line = "{0}={1},{2},{3},{4},noframe,bare" -f $role.title, $x, $y, $role.w, $role.h
|
|
[void]$cfg.Add($line)
|
|
W (" {0,-18} -> {1} ({2}x{3}) at {4},{5} [{6}]" -f `
|
|
$role.title, $targets[$r].DeviceName, $b.Width, $b.Height, $x, $y, $role.note)
|
|
} else {
|
|
[void]$cfg.Add(("# {0}=<x>,<y>,{1},{2},noframe,bare # no panel assigned" -f $role.title, $role.w, $role.h))
|
|
}
|
|
}
|
|
|
|
$cfgPath = Join-Path (Get-Location) 'glass_layout.cfg.proposed'
|
|
$cfg | Set-Content -Path $cfgPath -Encoding ASCII
|
|
W ""
|
|
W (" wrote {0}" -f $cfgPath)
|
|
W " -> review it, rename to glass_layout.cfg in the game's working directory."
|
|
W ""
|
|
W "---- NOTES ----"
|
|
W " * The pod's main 3D view is the GAME's own window (not a glass panel); put it"
|
|
W " on the main-view monitor with the normal window/fullscreen controls."
|
|
W " * 640x480 panels: if Windows does not offer that mode, the surface still"
|
|
W " renders 640x480 inside whatever mode the panel runs -- centred, not scaled."
|
|
W " * The 1995 rig spanned the five MFDs as ONE 1280x480 surface via NVIDIA"
|
|
W " Horizontal Span, which modern drivers removed. This per-panel window path"
|
|
W " replaces it and needs no special driver."
|
|
|
|
$txt = Join-Path (Get-Location) 'podprobe.txt'
|
|
$out | Set-Content -Path $txt -Encoding ASCII
|
|
Write-Host ""
|
|
Write-Host ("full report written to {0}" -f $txt)
|