Untrack the local cockpit-launcher wrappers

startrp-800x600.bat/.ps1 rode into the previous commit unintentionally;
remove them from tracking (local copies untouched) and gitignore them so
a bulk add cannot sweep them back in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-12 12:53:34 -05:00
co-authored by Claude Fable 5
parent d6745353b1
commit e4afc5ca19
3 changed files with 4 additions and 531 deletions
+4
View File
@@ -53,6 +53,10 @@ Thumbs.db
desktop.ini
.DS_Store
# Local cockpit-launcher experiments — deliberately untracked
assets/RP411/startrp-800x600.bat
assets/RP411/startrp-800x600.ps1
# Bundled game working copy: committed wholesale as runtime data (see
# docs/ASSETS.md). Re-include the binaries the global rules above would skip.
!assets/**/*.exe
-54
View File
@@ -1,54 +0,0 @@
@echo off
rem ============================================================================
rem Red Planet pod-cockpit launcher (wrapper around rpl4opt.exe)
rem ============================================================================
rem
rem Starts the game windowed with an 800x600 main view and rebuilds the full
rem 7-display pod cockpit on the desktop:
rem
rem [ 1: main 3D view ] [ 3: MFD W3.R ] [ 4: MFD W3.G ] [ 5: MFD W3.B ]
rem [ 6: MFD W4.R ] [ 2: map, 90CW ] [ 7: MFD W4.G ]
rem
rem The game itself only creates 4 windows. The five monochrome green MFDs are
rem packed into the color channels of game windows 3 and 4 (window 3 carries
rem three MFDs in R/G/B, window 4 two more in R/G). The wrapper hides those
rem source windows behind the main view and shows live green-screen mirrors of
rem each channel, plus the map display rotated 90 degrees clockwise (portrait,
rem as mounted in the pod). Mirror windows have title bars and can be dragged.
rem
rem All arguments are passed to startrp-800x600.ps1. Wrapper options:
rem
rem -Width N -Height N main view size (default 800x600)
rem -TileOnly no mirrors - just resize + tile the 4 real windows
rem -SaveLayout record the window arrangement on exit: launch with
rem this, drag windows where you want them, quit the
rem game -> positions saved to cockpit-layout.json and
rem restored automatically on every later launch
rem -ResetLayout delete the saved layout, return to the default grid
rem -TopMost keep the mirror displays above all other windows
rem
rem Anything else replaces the game's own arguments (default: -net 1501).
rem Useful game switches (see RedPlanet-CommandLine-and-Environment.md):
rem -net <port> connect to the pod network / Tesla console (TCP)
rem -egg <file> standalone: run a local mission, e.g. -egg TEST.EGG
rem -lc Live Cam spectator station -mr Mission Review
rem
rem Examples:
rem startrp-800x600.bat
rem normal pod: -net 1501, cockpit mirrors, saved layout if present
rem startrp-800x600.bat -egg TEST.EGG
rem offline test mission, no console needed
rem startrp-800x600.bat -SaveLayout -TopMost
rem arrange your cockpit, quit, and the layout sticks for future runs
rem startrp-800x600.bat -Width 1024 -Height 768 -net 1501
rem bigger main view on the pod network
rem
rem Lifecycle: PowerShell stays running hidden to drive the mirrors and exits
rem by itself when the game quits. Closing any mirror window shuts down all
rem mirrors and brings the real game windows back out. Note: the mirrors are
rem required because the game renders with D3D9 - the main window is always
rem created at full screen size by the exe (the -res switch only sets the
rem backbuffer), and windows parked off-screen stop rendering entirely, so
rem the sources are hidden behind the main view instead.
rem ============================================================================
start "" powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0startrp-800x600.ps1" %*
-477
View File
@@ -1,477 +0,0 @@
# Red Planet launcher wrapper: starts rpl4opt.exe and reconstructs the 7-display pod
# cockpit on a desktop monitor:
#
# [ 1: main 3D view ] [ 3: MFD W3.R ] [ 4: MFD W3.G ] [ 5: MFD W3.B ]
# [ 6: MFD W4.R ] [ 2: map, 90CW ] [ 7: MFD W4.G ]
# (centered)
#
# Background: the game creates 4 windows - main 3D view, the tactical/map display, and
# two "packed" gauge windows. A real pod has 7 displays: main, map (portrait-mounted),
# and five monochrome green MFDs (Multi Function Displays). The game packs the five MFD
# images into the COLOR CHANNELS of its 3rd and 4th windows: window 3 carries three
# MFDs in its R, G and B channels; window 4 carries two more in R and G. This wrapper
# splits them back out: each MFD view captures its source window, extracts one channel
# and renders it as a green screen. The map is shown rotated 90 degrees clockwise
# (portrait, as mounted in the pod).
#
# Implementation notes:
# - rpl4opt.exe always creates its main window at full screen size (hardcoded
# GetSystemMetrics at 0x49fe4b); -res only sets the D3D backbuffer. We resize the
# main window's client area ourselves.
# - Windows cannot rotate or channel-split a live window, so the map and MFD views are
# live mirrors: the three source windows are hidden BEHIND the main window and
# captured with BitBlt each tick. Sources must stay on-screen - the game's D3D9
# device stops presenting to fully off-screen windows and captures read black;
# occluded-but-on-screen keeps presenting.
# - Window identity is by CREATION ORDER (stable across runs; verified by content
# fingerprinting over repeated launches - window handle values shuffle per run):
# 1 = main, 2 = tactical/map, 3 = MFD pack 1 (red/blue), 4 = MFD pack 2 (BOOST/CHUTE).
#
# Layout persistence: launch with -SaveLayout, arrange the windows how you like, and
# the positions (main + all mirrors) are recorded to cockpit-layout.json next to this
# script when you exit. Every later launch restores that arrangement automatically.
# Use -ResetLayout to discard it and return to the default grid.
#
# Usage:
# startrp-800x600.ps1 -> rpl4opt.exe -net 1501 -windowed -res 800 600
# startrp-800x600.ps1 -Width 1024 -Height 768 -> bigger main window
# startrp-800x600.ps1 -TileOnly -> no mirrors, just resize + tile the 4 windows
# startrp-800x600.ps1 -SaveLayout -> record the window arrangement on exit
# startrp-800x600.ps1 -ResetLayout -> forget the saved window layout
# startrp-800x600.ps1 -TopMost -> keep the mirror displays above other windows
# startrp-800x600.ps1 -egg TEST.EGG -> replace default game args (standalone mission)
param(
[int]$Width = 800,
[int]$Height = 600,
[switch]$TileOnly,
[switch]$SaveLayout,
[switch]$ResetLayout,
[switch]$TopMost,
[Parameter(ValueFromRemainingArguments = $true)][string[]]$GameArgs
)
$ErrorActionPreference = 'Stop'
if (-not $GameArgs -or $GameArgs.Count -eq 0) { $GameArgs = @('-net', '1501') }
$GameArgs = @($GameArgs) + @('-windowed', '-res', "$Width", "$Height")
$gameDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$exe = Join-Path $gameDir 'rpl4opt.exe'
if (-not (Test-Path $exe)) { throw "rpl4opt.exe not found in $gameDir" }
$script:layoutPath = Join-Path $gameDir 'cockpit-layout.json'
if ($ResetLayout -and (Test-Path $script:layoutPath)) {
Remove-Item $script:layoutPath -Force
Write-Host "Saved layout discarded; using the default grid."
}
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
public static class RPWin {
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int L, T, R, B; }
delegate bool EnumProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")] static extern bool EnumWindows(EnumProc cb, IntPtr lParam);
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
[DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr hWnd, out RECT r);
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int w, int h, uint flags);
[DllImport("user32.dll")] public static extern int GetSystemMetrics(int index);
[DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr hWnd, IntPtr hdc);
[DllImport("gdi32.dll")] public static extern bool BitBlt(IntPtr dst, int dx, int dy, int w, int h, IntPtr src, int sx, int sy, uint rop);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassName(IntPtr hWnd, StringBuilder sb, int max);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max);
// EnumWindows returns windows top-of-z-order first.
public static List<IntPtr> GetProcessWindows(uint pid) {
var list = new List<IntPtr>();
EnumWindows((h, l) => {
uint wpid; GetWindowThreadProcessId(h, out wpid);
if (wpid == pid && IsWindowVisible(h)) list.Add(h);
return true;
}, IntPtr.Zero);
return list;
}
public static string ClassOf(IntPtr h) { var sb = new StringBuilder(256); GetClassName(h, sb, 256); return sb.ToString(); }
public static string TitleOf(IntPtr h) { var sb = new StringBuilder(256); GetWindowText(h, sb, 256); return sb.ToString(); }
}
'@
# --- launch -------------------------------------------------------------
Write-Host "Starting: rpl4opt.exe $($GameArgs -join ' ')"
$proc = Start-Process -FilePath $exe -WorkingDirectory $gameDir -ArgumentList $GameArgs -PassThru
$pidOfGame = [uint32]$proc.Id
# --- constants / helpers -------------------------------------------------
$SWP_RESIZE = 0x0234 # SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED
$SWP_MOVE = 0x0015 # SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE
$SWP_MOVE_Z = 0x0011 # SWP_NOSIZE | SWP_NOACTIVATE (allows z-order change)
$HWND_BOTTOM = [IntPtr]1
$script:lockedOrder = $null
function Get-OrderedWindows([uint32]$targetPid, [IntPtr]$hMain) {
# Order = main window first, then the gauge windows in CREATION order. EnumWindows
# returns top-of-z-order first, and at startup (before anything is clicked) newer
# windows sit above older ones, so reversing the enumeration gives creation order.
# The order is locked the first time all four windows exist so later z-order
# changes (clicks) can't renumber them.
if ($script:lockedOrder) { return $script:lockedOrder }
$wins = [RPWin]::GetProcessWindows($targetPid)
$others = @($wins | Where-Object { $_ -ne $hMain })
[array]::Reverse($others)
$ordered = @($hMain) + $others
if ($wins.Count -ge 4) { $script:lockedOrder = $ordered }
return $ordered
}
function Get-MainWindow([uint32]$targetPid) {
$best = [IntPtr]::Zero; $bestArea = -1
foreach ($h in [RPWin]::GetProcessWindows($targetPid)) {
$r = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($h, [ref]$r)
$area = ($r.R - $r.L) * ($r.B - $r.T)
if ([RPWin]::ClassOf($h) -eq 'MainWndClass') { $area += 1000000000 }
if ($area -gt $bestArea) { $bestArea = $area; $best = $h }
}
return $best
}
function Set-ClientSize([IntPtr]$hWnd, [int]$w, [int]$h) {
# Resize by measured client-vs-outer delta rather than AdjustWindowRectEx, whose
# metrics don't match the game window's real frame under Win11 DPI scaling.
for ($i = 0; $i -lt 3; $i++) {
$wr = New-Object RPWin+RECT; $cr = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($hWnd, [ref]$wr)
[void][RPWin]::GetClientRect($hWnd, [ref]$cr)
$dw = $w - ($cr.R - $cr.L); $dh = $h - ($cr.B - $cr.T)
if ($dw -eq 0 -and $dh -eq 0) { return }
[void][RPWin]::SetWindowPos($hWnd, [IntPtr]::Zero, 0, 0, ($wr.R - $wr.L + $dw), ($wr.B - $wr.T + $dh), $SWP_RESIZE)
}
}
function Set-WindowTiling([uint32]$targetPid, [IntPtr]$hMain) {
# Simple non-overlapping row (wraps at the desktop edge). Used while the game loads
# and as the fallback arrangement when the mirrors are not running.
$ordered = Get-OrderedWindows $targetPid $hMain
$screenW = [RPWin]::GetSystemMetrics(78) # SM_CXVIRTUALSCREEN
if ($screenW -le 0) { $screenW = [RPWin]::GetSystemMetrics(0) } # SM_CXSCREEN
$x = 0; $y = 0; $rowH = 0
foreach ($h in $ordered) {
$r = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($h, [ref]$r)
$w = $r.R - $r.L; $ht = $r.B - $r.T
if (($x + $w) -gt $screenW -and $x -gt 0) { $x = 0; $y += $rowH; $rowH = 0 }
if ($r.L -ne $x -or $r.T -ne $y) {
[void][RPWin]::SetWindowPos($h, [IntPtr]::Zero, $x, $y, 0, 0, $SWP_MOVE)
}
$x += $w
if ($ht -gt $rowH) { $rowH = $ht }
}
}
function Get-ClientSizeOf([IntPtr]$hWnd) {
$c = New-Object RPWin+RECT
[void][RPWin]::GetClientRect($hWnd, [ref]$c)
return @(($c.R - $c.L), ($c.B - $c.T))
}
# --- wait for the main window, then pin size + tiling for a few seconds --
$hMain = [IntPtr]::Zero
$deadline = (Get-Date).AddSeconds(60)
while ((Get-Date) -lt $deadline) {
if ($proc.HasExited) { Write-Host "Game exited before a window appeared."; exit 1 }
$hMain = Get-MainWindow $pidOfGame
if ($hMain -ne [IntPtr]::Zero) { break }
Start-Sleep -Milliseconds 250
}
if ($hMain -eq [IntPtr]::Zero) { Write-Host "Timed out waiting for the game window."; exit 1 }
Write-Host ("Main window found (class '{0}', title '{1}') - resizing client area to {2}x{3}" -f `
[RPWin]::ClassOf($hMain), [RPWin]::TitleOf($hMain), $Width, $Height)
$pinUntil = (Get-Date).AddSeconds(10)
while ((Get-Date) -lt $pinUntil) {
if ($proc.HasExited) { break }
$c = New-Object RPWin+RECT
[void][RPWin]::GetClientRect($hMain, [ref]$c)
if (($c.R - $c.L) -ne $Width -or ($c.B - $c.T) -ne $Height) { Set-ClientSize $hMain $Width $Height }
Set-WindowTiling $pidOfGame $hMain
Start-Sleep -Milliseconds 500
}
$ordered = Get-OrderedWindows $pidOfGame $hMain
if ($TileOnly -or $ordered.Count -lt 4) {
if ($ordered.Count -lt 4) { Write-Host "Fewer than 4 game windows found; leaving plain tiling." }
Write-Host "Done. Game is running (PID $pidOfGame); windows tiled without overlap."
exit 0
}
# --- pod cockpit mirrors --------------------------------------------------
# Sources: 2 = map, 3 = MFD pack 1 (R,G,B), 4 = MFD pack 2 (R,G).
$script:hMap = $ordered[1]
$script:hMfd1 = $ordered[2]
$script:hMfd2 = $ordered[3]
$script:gameProc = $proc
$mapSize = Get-ClientSizeOf $script:hMap
$mfd1Size = Get-ClientSizeOf $script:hMfd1
$mfd2Size = Get-ClientSizeOf $script:hMfd2
$cellW = [int]$mfd1Size[0]; $cellH = [int]$mfd1Size[1]
# Hide all three source windows behind the main window (bottom of z-order at 0,0,
# where the larger main window covers them). They must remain on-screen or the game's
# D3D9 device stops presenting and captures read black.
foreach ($h in @($script:hMap, $script:hMfd1, $script:hMfd2)) {
[void][RPWin]::SetWindowPos($h, $HWND_BOTTOM, 0, 0, 0, 0, $SWP_MOVE_Z)
}
# ImageAttributes that render one source channel (0=R,1=G,2=B) as a green screen:
# output G = input <channel>, output R = B = 0.
function New-GreenChannelAttrs([int]$channel) {
$rows = [System.Array]::CreateInstance([single[]], 5)
for ($i = 0; $i -lt 5; $i++) { $rows[$i] = New-Object 'single[]' 5 }
$rows[$channel][1] = 1.0 # source channel -> green output
$rows[3][3] = 1.0 # alpha passthrough
$rows[4][4] = 1.0
$cm = New-Object System.Drawing.Imaging.ColorMatrix -ArgumentList @(,$rows)
$ia = New-Object System.Drawing.Imaging.ImageAttributes
$ia.SetColorMatrix($cm)
return $ia
}
function Get-WindowBitmap([IntPtr]$hWnd, [int]$w, [int]$h) {
$bmp = New-Object System.Drawing.Bitmap($w, $h)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$hdcDst = $g.GetHdc()
$hdcSrc = [RPWin]::GetDC($hWnd)
[void][RPWin]::BitBlt($hdcDst, 0, 0, $w, $h, $hdcSrc, 0, 0, 0x00CC0020) # SRCCOPY
[void][RPWin]::ReleaseDC($hWnd, $hdcSrc)
$g.ReleaseHdc($hdcDst)
$g.Dispose()
return $bmp
}
# Layout (see banner at the top of this file). Grid sits to the right of the main
# window; map is portrait (rotated 90CW) centered under the middle (green) MFD.
# The mirrors get title bars and fixed borders so the user can drag them around, so
# positions are computed from OUTER window sizes after each form's chrome is applied.
$mr = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($hMain, [ref]$mr)
$gx = $mr.R - $mr.L # grid origin = right edge of main window
$mapRotW = [int]$mapSize[1]; $mapRotH = [int]$mapSize[0]
$viewDefs = @(
@{ Name = 'MFD 1 (W3 red)'; Src = 'mfd1'; Chan = 0; W = $cellW; H = $cellH }
@{ Name = 'MFD 2 (W3 green)'; Src = 'mfd1'; Chan = 1; W = $cellW; H = $cellH }
@{ Name = 'MFD 3 (W3 blue)'; Src = 'mfd1'; Chan = 2; W = $cellW; H = $cellH }
@{ Name = 'MFD 4 (W4 red)'; Src = 'mfd2'; Chan = 0; W = $cellW; H = $cellH }
@{ Name = 'Map (90CW)'; Src = 'map'; Chan = -1; W = $mapRotW; H = $mapRotH }
@{ Name = 'MFD 5 (W4 green)'; Src = 'mfd2'; Chan = 1; W = $cellW; H = $cellH }
)
$script:views = @()
$script:forms = @()
foreach ($def in $viewDefs) {
$form = New-Object System.Windows.Forms.Form
$form.FormBorderStyle = 'FixedSingle' # title bar + border: movable, not resizable
$form.MaximizeBox = $false
$form.StartPosition = 'Manual'
$form.Text = "RPL4 " + $def.Name
$form.BackColor = [System.Drawing.Color]::Black
$form.ClientSize = New-Object System.Drawing.Size($def.W, $def.H)
$form.ShowInTaskbar = $false
$form.TopMost = [bool]$TopMost
$pic = New-Object System.Windows.Forms.PictureBox
$pic.Dock = 'Fill'
$pic.BackColor = [System.Drawing.Color]::Black
$form.Controls.Add($pic)
$attrs = $null
if ($def.Chan -ge 0) { $attrs = New-GreenChannelAttrs $def.Chan }
$script:views += [pscustomobject]@{ Src = $def.Src; Chan = $def.Chan; Pic = $pic; Attrs = $attrs; W = $def.W; H = $def.H }
$script:forms += $form
}
# Position the grid using outer sizes (client size + title bar/border chrome).
$fW3R = $script:forms[0]; $fW3G = $script:forms[1]; $fW3B = $script:forms[2]
$fW4R = $script:forms[3]; $fMap = $script:forms[4]; $fW4G = $script:forms[5]
$fW3R.Location = New-Object System.Drawing.Point($gx, 0)
$fW3G.Location = New-Object System.Drawing.Point(($gx + $fW3R.Width), 0)
$fW3B.Location = New-Object System.Drawing.Point(($gx + $fW3R.Width + $fW3G.Width), 0)
$rowY = $fW3R.Height
$fW4R.Location = New-Object System.Drawing.Point($gx, $rowY)
$fMap.Location = New-Object System.Drawing.Point(($fW3G.Left + [int](($fW3G.Width - $fMap.Width) / 2)), $rowY)
$fW4G.Location = New-Object System.Drawing.Point($fW3B.Left, $rowY)
# --- saved layout: restore user-arranged window positions -----------------
$script:formsByName = @{}
for ($i = 0; $i -lt $viewDefs.Count; $i++) { $script:formsByName[$viewDefs[$i].Name] = $script:forms[$i] }
function Get-ClampedPos([int]$x, [int]$y, [int]$w, [int]$h) {
# Keep at least part of the window reachable on the current virtual desktop
# (saved layouts can reference monitors that are no longer attached).
$vx = [RPWin]::GetSystemMetrics(76); $vy = [RPWin]::GetSystemMetrics(77)
$vw = [RPWin]::GetSystemMetrics(78); $vh = [RPWin]::GetSystemMetrics(79)
if ($vw -le 0) { return @($x, $y) }
$x = [Math]::Max(($vx - $w + 60), [Math]::Min($x, ($vx + $vw - 60)))
$y = [Math]::Max($vy, [Math]::Min($y, ($vy + $vh - 40)))
return @($x, $y)
}
$script:savedLayout = $null
if (Test-Path $script:layoutPath) {
try {
$json = Get-Content $script:layoutPath -Raw | ConvertFrom-Json
$script:savedLayout = @{}
foreach ($prop in $json.psobject.Properties) { $script:savedLayout[$prop.Name] = $prop.Value }
} catch { $script:savedLayout = $null }
}
if ($script:savedLayout) {
foreach ($name in @($script:formsByName.Keys)) {
if ($script:savedLayout.ContainsKey($name)) {
$f = $script:formsByName[$name]
$p = $script:savedLayout[$name]
$pos = Get-ClampedPos ([int]$p.X) ([int]$p.Y) $f.Width $f.Height
$f.Location = New-Object System.Drawing.Point($pos[0], $pos[1])
}
}
if ($script:savedLayout.ContainsKey('Main')) {
$p = $script:savedLayout['Main']
$pos = Get-ClampedPos ([int]$p.X) ([int]$p.Y) ($mr.R - $mr.L) ($mr.B - $mr.T)
[void][RPWin]::SetWindowPos($hMain, [IntPtr]::Zero, $pos[0], $pos[1], 0, 0, $SWP_MOVE)
}
Write-Host "Restored saved window layout from cockpit-layout.json"
}
$script:lastMainPos = $null
function Save-Layout {
# Snapshot current positions (skipping minimized windows) so the arrangement
# survives the next launch. Only invoked on exit when -SaveLayout was given.
try {
$data = [ordered]@{}
if ([RPWin]::IsWindowVisible($script:hMain)) {
$r = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($script:hMain, [ref]$r)
if ($r.L -gt -10000) { $data['Main'] = @{ X = $r.L; Y = $r.T } }
} elseif ($script:lastMainPos) {
# game already exited; use the position seen on the last mirror tick
$data['Main'] = @{ X = $script:lastMainPos[0]; Y = $script:lastMainPos[1] }
}
foreach ($name in $script:formsByName.Keys) {
$f = $script:formsByName[$name]
if (-not $f.IsDisposed -and $f.WindowState -eq 'Normal') {
$data[$name] = @{ X = $f.Location.X; Y = $f.Location.Y }
}
}
if ($data.Count -gt 0) {
($data | ConvertTo-Json) | Set-Content -Path $script:layoutPath -Encoding UTF8
Write-Host "Window layout recorded to cockpit-layout.json"
}
} catch { }
}
$script:srcSizes = @{ map = $mapSize; mfd1 = $mfd1Size; mfd2 = $mfd2Size }
$script:srcWnds = @{ map = $script:hMap; mfd1 = $script:hMfd1; mfd2 = $script:hMfd2 }
$script:closing = $false
$script:wantSaveLayout = [bool]$SaveLayout
function Restore-Sources {
# Bring the parked windows back into a visible row (used if mirrors are closed
# while the game is still running).
if ($script:gameProc.HasExited) { return }
$mr2 = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($script:hMain, [ref]$mr2)
$x = $mr2.R - $mr2.L
foreach ($h in @($script:hMap, $script:hMfd1, $script:hMfd2)) {
$r = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($h, [ref]$r)
[void][RPWin]::SetWindowPos($h, [IntPtr]::Zero, $x, 0, 0, 0, $SWP_MOVE_Z) # HWND_TOP
$x += ($r.R - $r.L)
}
}
$script:hMain = $hMain
$script:timer = New-Object System.Windows.Forms.Timer
$script:timer.Interval = 50 # ~20 fps
$script:timer.Add_Tick({
if ($script:gameProc.HasExited) {
$script:timer.Stop()
[System.Windows.Forms.Application]::Exit()
return
}
# keep the parked sources tucked under the main window even if the user drags it
# (skip while minimized - GetWindowRect then reports -32000 and chasing it would
# push the sources off-screen, which stops the game presenting into them)
$mrT = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($script:hMain, [ref]$mrT)
if ($mrT.L -gt -10000) {
$script:lastMainPos = @($mrT.L, $mrT.T) # remembered for Save-Layout after the game exits
foreach ($h in @($script:hMap, $script:hMfd1, $script:hMfd2)) {
$sr = New-Object RPWin+RECT
[void][RPWin]::GetWindowRect($h, [ref]$sr)
if ($sr.L -ne $mrT.L -or $sr.T -ne $mrT.T) {
[void][RPWin]::SetWindowPos($h, $HWND_BOTTOM, $mrT.L, $mrT.T, 0, 0, $SWP_MOVE_Z)
}
}
}
# capture each source once per tick
$caps = @{}
foreach ($key in @('map', 'mfd1', 'mfd2')) {
$sz = $script:srcSizes[$key]
$caps[$key] = Get-WindowBitmap $script:srcWnds[$key] ([int]$sz[0]) ([int]$sz[1])
}
foreach ($v in $script:views) {
$srcBmp = $caps[$v.Src]
if ($v.Chan -lt 0) {
# map view: rotate 90 degrees clockwise, full color
$out = $srcBmp.Clone()
$out.RotateFlip([System.Drawing.RotateFlipType]::Rotate90FlipNone)
} else {
# MFD view: render one channel as a green screen
$out = New-Object System.Drawing.Bitmap($v.W, $v.H)
$g = [System.Drawing.Graphics]::FromImage($out)
$rect = New-Object System.Drawing.Rectangle(0, 0, $v.W, $v.H)
$g.DrawImage($srcBmp, $rect, 0, 0, $srcBmp.Width, $srcBmp.Height, [System.Drawing.GraphicsUnit]::Pixel, $v.Attrs)
$g.Dispose()
}
$old = $v.Pic.Image
$v.Pic.Image = $out
if ($old) { $old.Dispose() }
}
foreach ($bmp in $caps.Values) { $bmp.Dispose() }
})
foreach ($form in $script:forms) {
$form.Add_FormClosed({
if ($script:closing) { return }
$script:closing = $true
$script:timer.Stop()
if ($script:wantSaveLayout) { Save-Layout }
Restore-Sources
[System.Windows.Forms.Application]::Exit()
})
$form.Show()
}
Write-Host "Pod cockpit layout active: main + 5 green-screen MFDs + rotated map."
Write-Host "Sources for windows 2-4 are hidden behind the main window; closing any mirror restores them."
Write-Host "Wrapper stays running to drive the mirrors; it exits when the game does."
$script:timer.Start()
[System.Windows.Forms.Application]::Run()
# If we fell out of the message loop because the game exited, record the layout (when
# requested) and close any open forms.
if ($script:wantSaveLayout -and -not $script:closing) { Save-Layout }
$script:closing = $true
foreach ($form in $script:forms) { if (-not $form.IsDisposed) { $form.Close() } }