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:
@@ -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
|
||||
|
||||
@@ -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" %*
|
||||
@@ -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() } }
|
||||
Reference in New Issue
Block a user