diff --git a/.gitignore b/.gitignore index a6d1401..95c5335 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/assets/RP411/startrp-800x600.bat b/assets/RP411/startrp-800x600.bat deleted file mode 100644 index 8c6f583..0000000 --- a/assets/RP411/startrp-800x600.bat +++ /dev/null @@ -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 connect to the pod network / Tesla console (TCP) -rem -egg 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" %* diff --git a/assets/RP411/startrp-800x600.ps1 b/assets/RP411/startrp-800x600.ps1 deleted file mode 100644 index 7846d6a..0000000 --- a/assets/RP411/startrp-800x600.ps1 +++ /dev/null @@ -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 GetProcessWindows(uint pid) { - var list = new List(); - 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 , 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() } }