vPOD: pod power vs game split; install bar finishes; command column

- 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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-09 21:40:27 -05:00
co-authored by Claude Fable 5
parent e0a3c72370
commit e87a8a22f1
2 changed files with 124 additions and 43 deletions
+9 -3
View File
@@ -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
+115 -40
View File
@@ -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();
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// 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.)
/// </summary>
private void PowerOffClicked(object sender, EventArgs e)
{
mRestartTimer.Stop();
mRebootTimer.Stop();
mServer.Stop();
StopGame();
StopManagement();
SetPoweredState(on: false);
}
/// <summary>Starts the emulated game client (Munga listener) on a powered-on pod.</summary>
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);
}
/// <summary>
/// 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.
/// </summary>
private void StopGameClicked(object sender, EventArgs e)
{
StopGame();
}
private void StopGame()
{
mRestartTimer.Stop();
mServer.Stop();
SetGameState(running: false);
}
/// <summary>
/// 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
}
}
/// <summary>Reflects the on/off state in the buttons and, when off, the status labels.</summary>
/// <summary>Reflects the pod's (machine-level) on/off state in the buttons and,
/// when off, the status labels. Game-level state is <see cref="SetGameState" />.</summary>
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
}
}
/// <summary>Reflects whether the emulated game client is running; the pod
/// itself (launcher/site management) may still be up with the game stopped.</summary>
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 ----
/// <summary>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)