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)