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:
@@ -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
|
`WaitingForEgg`. This is the real pod's per-game cycle (`autoRestart`); the
|
||||||
*Restart game after mission ends (watchdog)* checkbox (on by default) toggles
|
*Restart game after mission ends (watchdog)* checkbox (on by default) toggles
|
||||||
it — unchecked, the pod just returns to `WaitingForEgg` without exiting.
|
it — unchecked, the pod just returns to `WaitingForEgg` without exiting.
|
||||||
- **Power On / Power Off** — Power Off closes the TCP listener so the console
|
- **Pod Power (Power On / Power Off)** — the whole virtual machine: Power Off
|
||||||
cannot connect, mimicking a pod with no game client running; Power On reopens
|
darkens the game listener AND the launcher / site-management side; Power On
|
||||||
it. **Reset** returns a live pod to `WaitingForEgg`.
|
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`
|
- **Reassembles and shows the egg** the console streams (the `EggFileMessage`
|
||||||
chunks), one field per line, with a summary line (adventure / map / scenario /
|
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
|
pilot count). The last egg is **kept** across missions/restarts (so it can be
|
||||||
|
|||||||
+115
-40
@@ -35,6 +35,8 @@ internal sealed class VPodForm : Form
|
|||||||
private RadioButton mBattleTechRadio;
|
private RadioButton mBattleTechRadio;
|
||||||
private Button mPowerButton;
|
private Button mPowerButton;
|
||||||
private Button mPowerOffButton;
|
private Button mPowerOffButton;
|
||||||
|
private Button mStartGameButton;
|
||||||
|
private Button mStopGameButton;
|
||||||
private Button mResetButton;
|
private Button mResetButton;
|
||||||
private CheckBox mRestartCheckbox;
|
private CheckBox mRestartCheckbox;
|
||||||
private Timer mRestartTimer;
|
private Timer mRestartTimer;
|
||||||
@@ -142,7 +144,22 @@ internal sealed class VPodForm : Form
|
|||||||
Text = "—"
|
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
|
GroupBox gameGroup = new GroupBox
|
||||||
{
|
{
|
||||||
Text = "Mimicking Game",
|
Text = "Mimicking Game",
|
||||||
@@ -165,16 +182,16 @@ internal sealed class VPodForm : Form
|
|||||||
};
|
};
|
||||||
mRedPlanetRadio.CheckedChanged += GameToggleChanged;
|
mRedPlanetRadio.CheckedChanged += GameToggleChanged;
|
||||||
mBattleTechRadio.CheckedChanged += GameToggleChanged;
|
mBattleTechRadio.CheckedChanged += GameToggleChanged;
|
||||||
mPowerButton = new Button { Text = "Power On", Location = new Point(12, 90), Size = new Size(94, 28) };
|
mStartGameButton = new Button { Text = "Start Game", 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) };
|
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) };
|
mResetButton = new Button { Text = "Reset", Location = new Point(212, 90), Size = new Size(94, 28) };
|
||||||
mPowerButton.Click += PowerOnClicked;
|
mStartGameButton.Click += StartGameClicked;
|
||||||
mPowerOffButton.Click += PowerOffClicked;
|
mStopGameButton.Click += StopGameClicked;
|
||||||
mResetButton.Click += (s, e) => mSimulator.Reset();
|
mResetButton.Click += (s, e) => mSimulator.Reset();
|
||||||
gameGroup.Controls.Add(mRedPlanetRadio);
|
gameGroup.Controls.Add(mRedPlanetRadio);
|
||||||
gameGroup.Controls.Add(mBattleTechRadio);
|
gameGroup.Controls.Add(mBattleTechRadio);
|
||||||
gameGroup.Controls.Add(mPowerButton);
|
gameGroup.Controls.Add(mStartGameButton);
|
||||||
gameGroup.Controls.Add(mPowerOffButton);
|
gameGroup.Controls.Add(mStopGameButton);
|
||||||
gameGroup.Controls.Add(mResetButton);
|
gameGroup.Controls.Add(mResetButton);
|
||||||
|
|
||||||
mRestartCheckbox = new CheckBox
|
mRestartCheckbox = new CheckBox
|
||||||
@@ -192,6 +209,7 @@ internal sealed class VPodForm : Form
|
|||||||
statusGroup.Controls.Add(stateCaption);
|
statusGroup.Controls.Add(stateCaption);
|
||||||
statusGroup.Controls.Add(mStateLabel);
|
statusGroup.Controls.Add(mStateLabel);
|
||||||
statusGroup.Controls.Add(mRestartCheckbox);
|
statusGroup.Controls.Add(mRestartCheckbox);
|
||||||
|
statusGroup.Controls.Add(powerGroup);
|
||||||
statusGroup.Controls.Add(gameGroup);
|
statusGroup.Controls.Add(gameGroup);
|
||||||
|
|
||||||
// ---- egg viewer + log ----
|
// ---- egg viewer + log ----
|
||||||
@@ -351,7 +369,7 @@ internal sealed class VPodForm : Form
|
|||||||
mAppsView.Columns.Add("Installed App", 150);
|
mAppsView.Columns.Add("Installed App", 150);
|
||||||
mAppsView.Columns.Add("PID", 48);
|
mAppsView.Columns.Add("PID", 48);
|
||||||
mAppsView.Columns.Add("Auto", 42);
|
mAppsView.Columns.Add("Auto", 42);
|
||||||
mAppsView.Columns.Add("Exe", 200);
|
mAppsView.Columns.Add("Command", 320);
|
||||||
|
|
||||||
siteGroup.Controls.Add(mAppsView);
|
siteGroup.Controls.Add(mAppsView);
|
||||||
siteGroup.Controls.Add(siteTop);
|
siteGroup.Controls.Add(siteTop);
|
||||||
@@ -371,16 +389,7 @@ internal sealed class VPodForm : Form
|
|||||||
UpdateConnectionLabel(null);
|
UpdateConnectionLabel(null);
|
||||||
UpdateStateLabel(mSimulator.State);
|
UpdateStateLabel(mSimulator.State);
|
||||||
RefreshAppsList();
|
RefreshAppsList();
|
||||||
if (StartServer())
|
PowerOnClicked(this, EventArgs.Empty);
|
||||||
{
|
|
||||||
mSimulator.PowerOn();
|
|
||||||
SetPoweredState(on: true);
|
|
||||||
StartManagement();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SetPoweredState(on: false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFormClosing(object sender, FormClosingEventArgs e)
|
private void OnFormClosing(object sender, FormClosingEventArgs e)
|
||||||
@@ -389,34 +398,68 @@ internal sealed class VPodForm : Form
|
|||||||
StopManagement();
|
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)
|
private void PowerOnClicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
mRestartTimer.Stop(); // cancel any pending watchdog restart
|
|
||||||
mRebootTimer.Stop();
|
mRebootTimer.Stop();
|
||||||
if (!mServer.IsListening && !StartServer())
|
|
||||||
{
|
|
||||||
return; // couldn't bind the port; stay powered off
|
|
||||||
}
|
|
||||||
mSimulator.PowerOn();
|
|
||||||
SetPoweredState(on: true);
|
SetPoweredState(on: true);
|
||||||
StartManagement();
|
StartManagement();
|
||||||
|
StartGameClicked(sender, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Powers the pod off by closing the TCP listener, so the console can no
|
/// Powers the whole pod off: the game listener AND the launcher/provisioning
|
||||||
/// longer connect — the same condition as a pod with no game client running.
|
/// side go dark, like unplugging the machine. (To mimic just the game exe
|
||||||
/// The launcher/provisioning side goes dark too (the whole machine is "off",
|
/// exiting while the pod stays up, use Stop Game.)
|
||||||
/// unlike an end-mission game exit where the launcher service survives).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void PowerOffClicked(object sender, EventArgs e)
|
private void PowerOffClicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
mRestartTimer.Stop();
|
|
||||||
mRebootTimer.Stop();
|
mRebootTimer.Stop();
|
||||||
mServer.Stop();
|
StopGame();
|
||||||
StopManagement();
|
StopManagement();
|
||||||
SetPoweredState(on: false);
|
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>
|
/// <summary>
|
||||||
/// The game exited gracefully on the console's end-mission command. Simulate
|
/// The game exited gracefully on the console's end-mission command. Simulate
|
||||||
/// the process exit by closing the listener (the console's connection drops),
|
/// the process exit by closing the listener (the console's connection drops),
|
||||||
@@ -433,8 +476,8 @@ internal sealed class VPodForm : Form
|
|||||||
UpdateConnectionLabel(null);
|
UpdateConnectionLabel(null);
|
||||||
mStateLabel.Text = "Restarting...";
|
mStateLabel.Text = "Restarting...";
|
||||||
mStateLabel.ForeColor = Color.DarkOrange;
|
mStateLabel.ForeColor = Color.DarkOrange;
|
||||||
mPowerButton.Enabled = true;
|
mStartGameButton.Enabled = true;
|
||||||
mPowerOffButton.Enabled = false;
|
mStopGameButton.Enabled = false;
|
||||||
mResetButton.Enabled = false;
|
mResetButton.Enabled = false;
|
||||||
mRestartTimer.Stop();
|
mRestartTimer.Stop();
|
||||||
mRestartTimer.Start();
|
mRestartTimer.Start();
|
||||||
@@ -444,15 +487,19 @@ internal sealed class VPodForm : Form
|
|||||||
private void OnRestartTimerTick(object sender, EventArgs e)
|
private void OnRestartTimerTick(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
mRestartTimer.Stop();
|
mRestartTimer.Stop();
|
||||||
|
if (!mPoweredOn)
|
||||||
|
{
|
||||||
|
return; // pod was powered off while the watchdog was pending
|
||||||
|
}
|
||||||
if (StartServer())
|
if (StartServer())
|
||||||
{
|
{
|
||||||
OnLog("Watchdog restarted the game.");
|
OnLog("Watchdog restarted the game.");
|
||||||
mSimulator.PowerOn();
|
mSimulator.PowerOn();
|
||||||
SetPoweredState(on: true);
|
SetGameState(running: true);
|
||||||
}
|
}
|
||||||
else
|
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)
|
private void SetPoweredState(bool on)
|
||||||
{
|
{
|
||||||
mPoweredOn = on;
|
mPoweredOn = on;
|
||||||
mPowerButton.Enabled = !on;
|
mPowerButton.Enabled = !on;
|
||||||
mPowerOffButton.Enabled = on;
|
mPowerOffButton.Enabled = on;
|
||||||
mResetButton.Enabled = on;
|
|
||||||
mReprovisionButton.Enabled = on && !mOptions.NoManage;
|
mReprovisionButton.Enabled = on && !mOptions.NoManage;
|
||||||
if (!on)
|
if (!on)
|
||||||
{
|
{
|
||||||
|
mStartGameButton.Enabled = false;
|
||||||
|
mStopGameButton.Enabled = false;
|
||||||
|
mResetButton.Enabled = false;
|
||||||
UpdateListeningLabel(false);
|
UpdateListeningLabel(false);
|
||||||
UpdateConnectionLabel(null);
|
UpdateConnectionLabel(null);
|
||||||
mStateLabel.Text = "Powered Off";
|
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 ----
|
// ---- virtual launcher / site management lifecycle ----
|
||||||
|
|
||||||
/// <summary>Brings the launcher side up for a powered-on pod: the RPC server
|
/// <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,
|
app.LaunchPair.DisplayName,
|
||||||
pids.ToString(),
|
pids.ToString(),
|
||||||
app.AutoRestart ? "yes" : "",
|
app.AutoRestart ? "yes" : "",
|
||||||
app.ExeFile
|
string.IsNullOrEmpty(app.Arguments) ? app.ExeFile : app.ExeFile + " " + app.Arguments
|
||||||
});
|
});
|
||||||
mAppsView.Items.Add(item);
|
mAppsView.Items.Add(item);
|
||||||
}
|
}
|
||||||
@@ -751,7 +820,14 @@ internal sealed class VPodForm : Form
|
|||||||
if (IsDisposed) return;
|
if (IsDisposed) return;
|
||||||
BeginInvoke((Action)(() =>
|
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
|
mInstallStatusLabel.Text = progress.IsCompleted
|
||||||
? "Install: " + progress.Status
|
? "Install: " + progress.Status
|
||||||
: $"Installing: {progress.Status} ({progress.PercentComplete}%)";
|
: $"Installing: {progress.Status} ({progress.PercentComplete}%)";
|
||||||
@@ -765,9 +841,8 @@ internal sealed class VPodForm : Form
|
|||||||
if (IsDisposed) return;
|
if (IsDisposed) return;
|
||||||
BeginInvoke((Action)(() =>
|
BeginInvoke((Action)(() =>
|
||||||
{
|
{
|
||||||
mRestartTimer.Stop();
|
|
||||||
mRebootTimer.Stop();
|
mRebootTimer.Stop();
|
||||||
mServer.Stop();
|
StopGame();
|
||||||
StopManagement();
|
StopManagement();
|
||||||
SetPoweredState(on: false);
|
SetPoweredState(on: false);
|
||||||
if (restart)
|
if (restart)
|
||||||
|
|||||||
Reference in New Issue
Block a user