Network-agnostic, air-gapped pod install for the two DOSBox titles (BT/RP 4.10), fitting the existing TeslaConsole/TeslaLauncher pod-bay architecture. emulator/DEPLOYMENT-PLAN.md Full design: bridge on the one NIC (pods form a source-proven P2P TCP mesh, so NAT/slirp is out); launcher keeps the bay IP, the DOSBox guest bridges at bayIP+100 (egg/mesh/game endpoint); two-edit +100 convention (postinstall + console DOSBox flag); static, air-gapped, <=32 pods. emulator/pod-launch/ (C# net8 supervising entry-point) Creates a Job Object (KILL_ON_JOB_CLOSE), launches DOSBox-X + the render bridge into it and blocks -- kill this process and both die (kernel-enforced, even on a hard TerminateProcess of a hung session; verified). Mode dispatch: bt/rp wired (also serve camera + live mission-review via egg hostType); review + diagnostics recognized but fail clean until their DOS launch args are known. emulator/deploy/ (install side) postinstall.bat (thin elevated wrapper) + configure.ps1 (NIC detect, realnic bind as a contiguous letter-leading GUID fragment, game IP = bayIP+100, stable MAC, render net_*.conf templates, stamp WATTCP.CFG my_ip) + tokenized conf templates + package.ps1 (assemble the zip). Render/bind/stamp + packaging verified against a scratch tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
7.8 KiB
C#
170 lines
7.8 KiB
C#
// Argument parsing + path resolution. Paths resolve relative to --root (default:
|
|
// the exe's own directory, i.e. the deployed game dir), with dev fallbacks so
|
|
// the same exe runs against the emulator/ source tree via `--root <emulator>`.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
namespace VwePod
|
|
{
|
|
internal enum LayoutMode { Cockpit, Explode }
|
|
|
|
internal sealed class Options
|
|
{
|
|
public Mode Mode;
|
|
public LayoutMode Layout = LayoutMode.Cockpit;
|
|
|
|
public string Root;
|
|
public string Work;
|
|
public string DosBox;
|
|
public string DosBoxDir;
|
|
public string Conf;
|
|
public string BridgeScript; // dev: live_bridge.py via pyw
|
|
public string RendererExe; // deploy: frozen renderer exe (else null)
|
|
public string AweRom;
|
|
|
|
public bool NoBridge;
|
|
public bool NoSound;
|
|
public bool Mipmap;
|
|
public bool NoFocus;
|
|
public bool DryRun;
|
|
|
|
public string BridgePos = "0,0"; // main out-the-window head
|
|
public int BridgeW = 800;
|
|
public int BridgeH = 600;
|
|
public int DosBoxX = 10;
|
|
public int DosBoxY = 10;
|
|
|
|
// Cockpit head rects (deployment variant of pod_deploy.ps1's RECTS).
|
|
public string RadarRect = "800,0,640,480"; // VPX_WIN0 radar (HEAD C)
|
|
public string Mfd3Rect = "1440,0,640,480"; // VPX_WIN4 3-color MFD (HEAD B)
|
|
public string Mfd2Rect = "2080,0,640,480"; // VPX_WIN3 2-color MFD (HEAD A)
|
|
|
|
public static Options Parse(string[] args)
|
|
{
|
|
var o = new Options();
|
|
string modeName = null, confArg = null, rootArg = null, dosboxArg = null,
|
|
bridgeScriptArg = null, rendererArg = null, aweArg = null;
|
|
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
string a = args[i];
|
|
string Next(string name)
|
|
{
|
|
if (i + 1 >= args.Length) throw new ArgumentException(name + " needs a value");
|
|
return args[++i];
|
|
}
|
|
if (!a.StartsWith("-")) { modeName = a; continue; } // positional mode
|
|
switch (a)
|
|
{
|
|
case "--mode": modeName = Next(a); break;
|
|
case "--conf": confArg = Next(a); break;
|
|
case "--root": rootArg = Next(a); break;
|
|
case "--work": o.Work = Next(a); break;
|
|
case "--dosbox": dosboxArg = Next(a); break;
|
|
case "--bridge-script": bridgeScriptArg = Next(a); break;
|
|
case "--renderer": rendererArg = Next(a); break;
|
|
case "--awe-rom": aweArg = Next(a); break;
|
|
case "--layout": o.Layout = Next(a).Equals("explode", StringComparison.OrdinalIgnoreCase) ? LayoutMode.Explode : LayoutMode.Cockpit; break;
|
|
case "--bridge-pos": o.BridgePos = Next(a); break;
|
|
case "--bridge-size": ParseWH(Next(a), out o.BridgeW, out o.BridgeH); break;
|
|
case "--dosbox-xy": ParseWH(Next(a), out o.DosBoxX, out o.DosBoxY); break;
|
|
case "--no-bridge": o.NoBridge = true; break;
|
|
case "--no-sound": o.NoSound = true; break;
|
|
case "--mipmap": o.Mipmap = true; break;
|
|
case "--no-focus": o.NoFocus = true; break;
|
|
case "--dry-run": o.DryRun = true; break;
|
|
case "-h": case "--help": Usage(); Environment.Exit(0); break;
|
|
default: throw new ArgumentException("unknown arg: " + a);
|
|
}
|
|
}
|
|
|
|
o.Root = rootArg ?? AppContext.BaseDirectory;
|
|
|
|
o.Mode = Modes.Find(modeName ?? "bt");
|
|
if (o.Mode == null)
|
|
throw new ArgumentException("unknown mode '" + modeName + "'; valid: " + Modes.Names());
|
|
// Unsupported modes: return with only the Mode set; Program prints the
|
|
// reason and exits without touching the launch env.
|
|
if (!o.Mode.Supported) return o;
|
|
|
|
o.DosBox = dosboxArg ?? FirstExisting(
|
|
Path.Combine(o.Root, "dosbox-x.exe"),
|
|
Path.Combine(o.Root, "src", "src", "dosbox-x.exe"))
|
|
?? throw new ArgumentException("dosbox-x.exe not found under root; pass --dosbox");
|
|
o.DosBoxDir = Path.GetDirectoryName(o.DosBox);
|
|
|
|
o.Conf = confArg ?? FirstExisting(Path.Combine(o.Root, o.Mode.ConfBase + ".conf"))
|
|
?? throw new ArgumentException(o.Mode.ConfBase + ".conf not found under root; pass --conf");
|
|
|
|
o.RendererExe = rendererArg ?? FirstExisting(
|
|
Path.Combine(o.Root, "renderer.exe"),
|
|
Path.Combine(o.Root, "render-bridge", "renderer.exe"));
|
|
o.BridgeScript = bridgeScriptArg ?? FirstExisting(
|
|
Path.Combine(o.Root, "live_bridge.py"),
|
|
Path.Combine(o.Root, "render-bridge", "live_bridge.py"));
|
|
if (!o.NoBridge && o.RendererExe == null && o.BridgeScript == null)
|
|
throw new ArgumentException("no renderer found; pass --renderer, --bridge-script, or --no-bridge");
|
|
|
|
o.AweRom = aweArg ?? FirstExisting(
|
|
Path.Combine(o.Root, "roms", "awe32.raw"),
|
|
Path.Combine(o.Root, "..", "roms", "awe32.raw"));
|
|
|
|
o.Work = o.Work ?? Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"Temp", "vwe-pod");
|
|
|
|
return o;
|
|
}
|
|
|
|
private static void ParseWH(string s, out int a, out int b)
|
|
{
|
|
var parts = s.Split(',');
|
|
if (parts.Length != 2 || !int.TryParse(parts[0], out a) || !int.TryParse(parts[1], out b))
|
|
throw new ArgumentException("expected 'x,y': " + s);
|
|
}
|
|
|
|
private static string FirstExisting(params string[] paths)
|
|
{
|
|
foreach (var p in paths)
|
|
{
|
|
if (p == null) continue;
|
|
var full = Path.GetFullPath(p);
|
|
if (File.Exists(full)) return full;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static void Usage()
|
|
{
|
|
Console.Error.WriteLine(@"pod-launch [mode] [options]
|
|
Supervising entry-point: launches DOSBox-X + the render bridge into a job
|
|
object and blocks; killing this process kills them too (no orphans).
|
|
|
|
mode bt | rp (default bt). bt/rp also serve the camera ship
|
|
+ live mission-review roles -- the console picks the
|
|
role via the egg hostType, so no separate mode. Also
|
|
recognized (not yet wired): review, test-plasma,
|
|
reset-rio, audio-test.
|
|
--mode <name> same as the positional mode
|
|
--conf <path> explicit conf (overrides the mode's conf)
|
|
--root <dir> base dir for dosbox/conf/bridge (default: exe dir)
|
|
--work <dir> runtime/work dir (default: %LOCALAPPDATA%\Temp\vwe-pod)
|
|
--layout cockpit|explode window layout (default cockpit)
|
|
--dosbox <exe> explicit dosbox-x.exe
|
|
--renderer <exe> frozen renderer exe (else falls back to pyw live_bridge)
|
|
--bridge-script <py> live_bridge.py (dev)
|
|
--awe-rom <raw> AWE32 SoundFont ROM
|
|
--bridge-pos x,y main render window position (default 0,0)
|
|
--bridge-size w,h main render window size (default 800,600)
|
|
--dosbox-xy x,y DOSBox window position + focus (default 10,10)
|
|
--no-bridge pod only, no GL render window
|
|
--no-sound skip the ~4-min SoundFont upload
|
|
--mipmap VRVIEW_MIPMAP=1 (RP floor shimmer fix; non-authentic)
|
|
--no-focus skip the window layout/focus pass
|
|
--dry-run resolve paths + print the launch plan, then exit");
|
|
}
|
|
}
|
|
}
|