From e87a8a22f1f1bef8e601cc533fe1c0f0f1841cdb Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 9 Jul 2026 21:40:27 -0500 Subject: [PATCH] vPOD: pod power vs game split; install bar finishes; command column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pod Power (new group) is the whole machine: Power On boots the launcher / site-management side and auto-starts the game (a real pod's boot-time autoRestart launch); Power Off darkens everything. Start Game / Stop Game control just the emulated game exe, so "machine up, game not running" is now representable — Manage Site keeps seeing a healthy pod while the console's game connection is down. Watchdog stays game-level and no-ops if the pod is powered off during its window; console Shutdown/Restart stays a pod-level power cycle. - The install progress bar snaps to 100% when the transfer completes (the wire still reports 99 — the console retries on anything else). - Installed-apps list shows the full command line (exe + arguments). Co-Authored-By: Claude Fable 5 --- Console/vPOD/README.md | 12 ++- Console/vPOD/VPodForm.cs | 155 +++++++++++++++++++++++++++++---------- 2 files changed, 124 insertions(+), 43 deletions(-) diff --git a/Console/vPOD/README.md b/Console/vPOD/README.md index fb0ebc5..0fb409e 100644 --- a/Console/vPOD/README.md +++ b/Console/vPOD/README.md @@ -28,9 +28,15 @@ end-to-end with no cockpit and **no console changes**. See `WaitingForEgg`. This is the real pod's per-game cycle (`autoRestart`); the *Restart game after mission ends (watchdog)* checkbox (on by default) toggles it — unchecked, the pod just returns to `WaitingForEgg` without exiting. -- **Power On / Power Off** — Power Off closes the TCP listener so the console - cannot connect, mimicking a pod with no game client running; Power On reopens - it. **Reset** returns a live pod to `WaitingForEgg`. +- **Pod Power (Power On / Power Off)** — the whole virtual machine: Power Off + darkens the game listener AND the launcher / site-management side; Power On + boots the launcher (or provisioning beacons) and auto-starts the game, like a + real pod booting with an `autoRestart` product installed. +- **Start Game / Stop Game** — just the emulated game "exe", separate from pod + power: Stop Game closes the Munga listener (the console's game connection + drops) while the pod and its launcher service stay up — the state of a pod + whose game client crashed or was killed. **Reset** returns a running game to + `WaitingForEgg`. - **Reassembles and shows the egg** the console streams (the `EggFileMessage` chunks), one field per line, with a summary line (adventure / map / scenario / pilot count). The last egg is **kept** across missions/restarts (so it can be diff --git a/Console/vPOD/VPodForm.cs b/Console/vPOD/VPodForm.cs index 2ba32c3..3485589 100644 --- a/Console/vPOD/VPodForm.cs +++ b/Console/vPOD/VPodForm.cs @@ -35,6 +35,8 @@ internal sealed class VPodForm : Form private RadioButton mBattleTechRadio; private Button mPowerButton; private Button mPowerOffButton; + private Button mStartGameButton; + private Button mStopGameButton; private Button mResetButton; private CheckBox mRestartCheckbox; private Timer mRestartTimer; @@ -142,7 +144,22 @@ internal sealed class VPodForm : Form Text = "—" }; - // game toggle + // pod power: the whole virtual machine (launcher/site management included), + // distinct from starting/stopping the emulated game client below. + GroupBox powerGroup = new GroupBox + { + Text = "Pod Power", + Location = new Point(400, 20), + Size = new Size(150, 130) + }; + mPowerButton = new Button { Text = "Power On", Location = new Point(12, 28), Size = new Size(126, 30) }; + mPowerOffButton = new Button { Text = "Power Off", Location = new Point(12, 66), Size = new Size(126, 30) }; + mPowerButton.Click += PowerOnClicked; + mPowerOffButton.Click += PowerOffClicked; + powerGroup.Controls.Add(mPowerButton); + powerGroup.Controls.Add(mPowerOffButton); + + // game toggle + game-process controls (the "exe", not the machine) GroupBox gameGroup = new GroupBox { Text = "Mimicking Game", @@ -165,16 +182,16 @@ internal sealed class VPodForm : Form }; mRedPlanetRadio.CheckedChanged += GameToggleChanged; mBattleTechRadio.CheckedChanged += GameToggleChanged; - mPowerButton = new Button { Text = "Power On", Location = new Point(12, 90), Size = new Size(94, 28) }; - mPowerOffButton = new Button { Text = "Power Off", Location = new Point(112, 90), Size = new Size(94, 28) }; + mStartGameButton = new Button { Text = "Start Game", Location = new Point(12, 90), Size = new Size(94, 28) }; + mStopGameButton = new Button { Text = "Stop Game", Location = new Point(112, 90), Size = new Size(94, 28) }; mResetButton = new Button { Text = "Reset", Location = new Point(212, 90), Size = new Size(94, 28) }; - mPowerButton.Click += PowerOnClicked; - mPowerOffButton.Click += PowerOffClicked; + mStartGameButton.Click += StartGameClicked; + mStopGameButton.Click += StopGameClicked; mResetButton.Click += (s, e) => mSimulator.Reset(); gameGroup.Controls.Add(mRedPlanetRadio); gameGroup.Controls.Add(mBattleTechRadio); - gameGroup.Controls.Add(mPowerButton); - gameGroup.Controls.Add(mPowerOffButton); + gameGroup.Controls.Add(mStartGameButton); + gameGroup.Controls.Add(mStopGameButton); gameGroup.Controls.Add(mResetButton); mRestartCheckbox = new CheckBox @@ -192,6 +209,7 @@ internal sealed class VPodForm : Form statusGroup.Controls.Add(stateCaption); statusGroup.Controls.Add(mStateLabel); statusGroup.Controls.Add(mRestartCheckbox); + statusGroup.Controls.Add(powerGroup); statusGroup.Controls.Add(gameGroup); // ---- egg viewer + log ---- @@ -351,7 +369,7 @@ internal sealed class VPodForm : Form mAppsView.Columns.Add("Installed App", 150); mAppsView.Columns.Add("PID", 48); mAppsView.Columns.Add("Auto", 42); - mAppsView.Columns.Add("Exe", 200); + mAppsView.Columns.Add("Command", 320); siteGroup.Controls.Add(mAppsView); siteGroup.Controls.Add(siteTop); @@ -371,16 +389,7 @@ internal sealed class VPodForm : Form UpdateConnectionLabel(null); UpdateStateLabel(mSimulator.State); RefreshAppsList(); - if (StartServer()) - { - mSimulator.PowerOn(); - SetPoweredState(on: true); - StartManagement(); - } - else - { - SetPoweredState(on: false); - } + PowerOnClicked(this, EventArgs.Empty); } private void OnFormClosing(object sender, FormClosingEventArgs e) @@ -389,34 +398,68 @@ internal sealed class VPodForm : Form StopManagement(); } + /// + /// Pod power = the whole virtual machine. Powering on boots the + /// launcher/site-management side and auto-starts the game client (a real + /// pod's boot-time autoRestart launch); the game can then be stopped and + /// started on its own while the pod stays up. + /// private void PowerOnClicked(object sender, EventArgs e) { - mRestartTimer.Stop(); // cancel any pending watchdog restart mRebootTimer.Stop(); - if (!mServer.IsListening && !StartServer()) - { - return; // couldn't bind the port; stay powered off - } - mSimulator.PowerOn(); SetPoweredState(on: true); StartManagement(); + StartGameClicked(sender, e); } /// - /// Powers the pod off by closing the TCP listener, so the console can no - /// longer connect — the same condition as a pod with no game client running. - /// The launcher/provisioning side goes dark too (the whole machine is "off", - /// unlike an end-mission game exit where the launcher service survives). + /// Powers the whole pod off: the game listener AND the launcher/provisioning + /// side go dark, like unplugging the machine. (To mimic just the game exe + /// exiting while the pod stays up, use Stop Game.) /// private void PowerOffClicked(object sender, EventArgs e) { - mRestartTimer.Stop(); mRebootTimer.Stop(); - mServer.Stop(); + StopGame(); StopManagement(); SetPoweredState(on: false); } + /// Starts the emulated game client (Munga listener) on a powered-on pod. + private void StartGameClicked(object sender, EventArgs e) + { + if (!mPoweredOn) + { + return; + } + mRestartTimer.Stop(); // cancel any pending watchdog restart + if (!mServer.IsListening && !StartServer()) + { + SetGameState(running: false); + return; // couldn't bind the port; pod stays up, game stays stopped + } + mSimulator.PowerOn(); + SetGameState(running: true); + } + + /// + /// Stops just the emulated game "exe": the Munga listener closes (the + /// console's game connection drops) but the pod — and its launcher / + /// site-management side — stays up, like killing the game process on a + /// real pod. + /// + private void StopGameClicked(object sender, EventArgs e) + { + StopGame(); + } + + private void StopGame() + { + mRestartTimer.Stop(); + mServer.Stop(); + SetGameState(running: false); + } + /// /// The game exited gracefully on the console's end-mission command. Simulate /// the process exit by closing the listener (the console's connection drops), @@ -433,8 +476,8 @@ internal sealed class VPodForm : Form UpdateConnectionLabel(null); mStateLabel.Text = "Restarting..."; mStateLabel.ForeColor = Color.DarkOrange; - mPowerButton.Enabled = true; - mPowerOffButton.Enabled = false; + mStartGameButton.Enabled = true; + mStopGameButton.Enabled = false; mResetButton.Enabled = false; mRestartTimer.Stop(); mRestartTimer.Start(); @@ -444,15 +487,19 @@ internal sealed class VPodForm : Form private void OnRestartTimerTick(object sender, EventArgs e) { mRestartTimer.Stop(); + if (!mPoweredOn) + { + return; // pod was powered off while the watchdog was pending + } if (StartServer()) { OnLog("Watchdog restarted the game."); mSimulator.PowerOn(); - SetPoweredState(on: true); + SetGameState(running: true); } else { - SetPoweredState(on: false); + SetGameState(running: false); } } @@ -477,16 +524,19 @@ internal sealed class VPodForm : Form } } - /// Reflects the on/off state in the buttons and, when off, the status labels. + /// Reflects the pod's (machine-level) on/off state in the buttons and, + /// when off, the status labels. Game-level state is . private void SetPoweredState(bool on) { mPoweredOn = on; mPowerButton.Enabled = !on; mPowerOffButton.Enabled = on; - mResetButton.Enabled = on; mReprovisionButton.Enabled = on && !mOptions.NoManage; if (!on) { + mStartGameButton.Enabled = false; + mStopGameButton.Enabled = false; + mResetButton.Enabled = false; UpdateListeningLabel(false); UpdateConnectionLabel(null); mStateLabel.Text = "Powered Off"; @@ -499,6 +549,25 @@ internal sealed class VPodForm : Form } } + /// Reflects whether the emulated game client is running; the pod + /// itself (launcher/site management) may still be up with the game stopped. + private void SetGameState(bool running) + { + mStartGameButton.Enabled = mPoweredOn && !running; + mStopGameButton.Enabled = mPoweredOn && running; + mResetButton.Enabled = mPoweredOn && running; + if (!running) + { + UpdateListeningLabel(false); + UpdateConnectionLabel(null); + if (mPoweredOn) + { + mStateLabel.Text = "Game Not Running"; + mStateLabel.ForeColor = Color.Gray; + } + } + } + // ---- virtual launcher / site management lifecycle ---- /// Brings the launcher side up for a powered-on pod: the RPC server @@ -739,7 +808,7 @@ internal sealed class VPodForm : Form app.LaunchPair.DisplayName, pids.ToString(), app.AutoRestart ? "yes" : "", - app.ExeFile + string.IsNullOrEmpty(app.Arguments) ? app.ExeFile : app.ExeFile + " " + app.Arguments }); mAppsView.Items.Add(item); } @@ -751,7 +820,14 @@ internal sealed class VPodForm : Form if (IsDisposed) return; BeginInvoke((Action)(() => { - mInstallProgressBar.Value = Math.Max(0, Math.Min(100, progress.PercentComplete)); + int percent = Math.Max(0, Math.Min(100, progress.PercentComplete)); + if (progress.IsCompleted && percent >= 99) + { + // The wire deliberately tops out at 99 (the console retries on + // 100) — but the local bar can still show a finished install. + percent = 100; + } + mInstallProgressBar.Value = percent; mInstallStatusLabel.Text = progress.IsCompleted ? "Install: " + progress.Status : $"Installing: {progress.Status} ({progress.PercentComplete}%)"; @@ -765,9 +841,8 @@ internal sealed class VPodForm : Form if (IsDisposed) return; BeginInvoke((Action)(() => { - mRestartTimer.Stop(); mRebootTimer.Stop(); - mServer.Stop(); + StopGame(); StopManagement(); SetPoweredState(on: false); if (restart)