using System;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Munga.Net;
using Tesla;
using Tesla.Net;
namespace VPod;
///
/// The vPOD window: a live view of the simulated pod. Top panel shows the
/// listening/connection status and the current ApplicationState (colour-coded),
/// with the game toggle (Red Planet / BattleTech) that changes which
/// ApplicationID the pod reports. Below, the left pane is the egg viewer (the
/// last egg the console streamed, one field per line) and the right pane is a
/// scrolling protocol log. The right column is the virtual launcher — the pod
/// side of the console's Site Management (provisioning, product installs,
/// launch/kill/volume/shutdown), see LauncherRpcServer / PodProvisioning.
///
internal sealed class VPodForm : Form
{
private readonly PodArguments mOptions;
private readonly MungaPodServer mServer;
private readonly PodSimulator mSimulator;
private readonly VirtualLauncher mLauncher;
private readonly LauncherRpcServer mRpcServer;
private readonly PodProvisioning mProvisioning;
private Label mListeningLabel;
private Label mConnectionLabel;
private Label mStateLabel;
private Label mEggSummaryLabel;
private RadioButton mRedPlanetRadio;
private RadioButton mBattleTechRadio;
private Button mPowerButton;
private Button mPowerOffButton;
private Button mStartGameButton;
private Button mStopGameButton;
private Button mResetButton;
private CheckBox mRestartCheckbox;
private Timer mRestartTimer;
private Timer mRebootTimer;
private SplitContainer mSplit;
private TextBox mEggBox;
private TextBox mLogBox;
// Virtual launcher / site-management column.
private Label mProvisionStatusLabel;
private Panel mPassphrasePanel;
private Label mRequestIdValueLabel;
private Label mPassphraseValueLabel;
private Label mNetConfigLabel;
private Label mRpcStatusLabel;
private Label mInstallStatusLabel;
private ProgressBar mInstallProgressBar;
private Button mReprovisionButton;
private CheckBox mRealLaunchCheckbox;
private ListView mAppsView;
private bool mPoweredOn;
// How long the "watchdog" waits before relaunching the exited game.
private const int WatchdogRestartMs = 1500;
// How long a console-requested Shutdown(restart) keeps the pod dark.
private const int RebootRestartMs = 4000;
public VPodForm(PodArguments options)
{
mOptions = options;
mServer = new MungaPodServer(options.Port);
mSimulator = new PodSimulator(mServer, options.Application, options.HostId);
mLauncher = new VirtualLauncher();
mRpcServer = new LauncherRpcServer(mLauncher);
mProvisioning = new PodProvisioning(PodProvisioning.MacForHost(options.HostId));
BuildUi();
mServer.Log += OnLog;
mServer.ConnectionChanged += OnConnectionChanged;
mServer.MessageReceived += OnMessageReceived;
mSimulator.Log += OnLog;
mSimulator.StateChanged += OnStateChanged;
mSimulator.EggReceived += OnEggReceived;
mSimulator.EggProgress += OnEggProgress;
mSimulator.EndMissionExit += OnEndMissionExit;
mLauncher.Log += OnSiteLog;
mLauncher.AppsChanged += OnAppsChanged;
mLauncher.InstallProgressChanged += OnInstallProgress;
mLauncher.ShutdownRequested += OnShutdownRequested;
mLauncher.ReprovisionRequested += OnReprovisionRequested;
mRpcServer.Log += OnSiteLog;
mRpcServer.ConnectionsChanged += OnRpcConnectionsChanged;
mProvisioning.Log += OnSiteLog;
mProvisioning.ConfigReceived += OnConfigReceived;
mProvisioning.Provisioned += OnProvisioned;
mRestartTimer = new Timer { Interval = WatchdogRestartMs };
mRestartTimer.Tick += OnRestartTimerTick;
mRebootTimer = new Timer { Interval = RebootRestartMs };
mRebootTimer.Tick += OnRebootTimerTick;
Load += OnFormLoad;
FormClosing += OnFormClosing;
}
private void BuildUi()
{
Text = $"vPOD - virtual pod (port {mOptions.Port}, host {mOptions.HostId})";
ClientSize = new Size(1280, 560);
MinimumSize = new Size(1100, 500);
Font = new Font("Segoe UI", 9f);
// ---- status panel ----
GroupBox statusGroup = new GroupBox
{
Text = "Pod Status",
Dock = DockStyle.Top,
Height = 176,
Padding = new Padding(10)
};
mListeningLabel = new Label { AutoSize = true, Location = new Point(16, 26) };
mConnectionLabel = new Label { AutoSize = true, Location = new Point(16, 50) };
Label roleLabel = new Label
{
AutoSize = true,
Location = new Point(16, 74),
Text = "Role: " + RoleText(mOptions.HostType)
};
Label stateCaption = new Label
{
AutoSize = true,
Location = new Point(16, 106),
Text = "Application State:"
};
mStateLabel = new Label
{
AutoSize = true,
Location = new Point(130, 100),
Font = new Font("Segoe UI", 15f, FontStyle.Bold),
Text = "—"
};
// 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(740, 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",
Location = new Point(400, 20),
Size = new Size(330, 130)
};
mRedPlanetRadio = new RadioButton
{
Text = "Red Planet (RPL4)",
Location = new Point(18, 28),
AutoSize = true,
Checked = mOptions.Application == ApplicationID.RPL4
};
mBattleTechRadio = new RadioButton
{
Text = "BattleTech (BTL4)",
Location = new Point(18, 56),
AutoSize = true,
Checked = mOptions.Application == ApplicationID.BTL4
};
mRedPlanetRadio.CheckedChanged += GameToggleChanged;
mBattleTechRadio.CheckedChanged += GameToggleChanged;
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) };
mStartGameButton.Click += StartGameClicked;
mStopGameButton.Click += StopGameClicked;
mResetButton.Click += (s, e) => mSimulator.Reset();
gameGroup.Controls.Add(mRedPlanetRadio);
gameGroup.Controls.Add(mBattleTechRadio);
gameGroup.Controls.Add(mStartGameButton);
gameGroup.Controls.Add(mStopGameButton);
gameGroup.Controls.Add(mResetButton);
mRestartCheckbox = new CheckBox
{
Text = "Restart game after mission ends (watchdog)",
Location = new Point(16, 146),
AutoSize = true,
Checked = true
};
mRestartCheckbox.CheckedChanged += (s, e) => mSimulator.RestartOnEndMission = mRestartCheckbox.Checked;
statusGroup.Controls.Add(mListeningLabel);
statusGroup.Controls.Add(mConnectionLabel);
statusGroup.Controls.Add(roleLabel);
statusGroup.Controls.Add(stateCaption);
statusGroup.Controls.Add(mStateLabel);
statusGroup.Controls.Add(mRestartCheckbox);
statusGroup.Controls.Add(powerGroup);
statusGroup.Controls.Add(gameGroup);
// ---- egg viewer + log ----
// Split the egg viewer (left) and protocol log (right). The 50/50 default
// is applied in OnFormLoad once the control has its real width (setting it
// here would clamp to the control's default size).
mSplit = new SplitContainer
{
Dock = DockStyle.Fill,
Orientation = Orientation.Vertical
};
SplitContainer split = mSplit;
GroupBox eggGroup = new GroupBox { Text = "Current Egg", Dock = DockStyle.Fill, Padding = new Padding(8) };
// The egg is kept after a mission/restart (not auto-cleared) so it can be
// copied for dev use; the Clear button empties the viewer on demand.
Panel eggHeader = new Panel { Dock = DockStyle.Top, Height = 30 };
Button clearEggButton = new Button { Text = "Clear", Dock = DockStyle.Right, Width = 70 };
mEggSummaryLabel = new Label
{
Dock = DockStyle.Fill,
Text = "No egg loaded.",
TextAlign = ContentAlignment.MiddleLeft
};
clearEggButton.Click += (s, e) =>
{
mEggBox.Clear();
mEggSummaryLabel.Text = "No egg loaded.";
};
eggHeader.Controls.Add(mEggSummaryLabel);
eggHeader.Controls.Add(clearEggButton);
mEggBox = new TextBox
{
Dock = DockStyle.Fill,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both,
WordWrap = false,
Font = new Font("Consolas", 9f),
BackColor = Color.White
};
eggGroup.Controls.Add(mEggBox);
eggGroup.Controls.Add(eggHeader);
GroupBox logGroup = new GroupBox { Text = "Protocol Log", Dock = DockStyle.Fill, Padding = new Padding(8) };
mLogBox = new TextBox
{
Dock = DockStyle.Fill,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both,
WordWrap = false,
Font = new Font("Consolas", 9f),
BackColor = Color.FromArgb(24, 24, 24),
ForeColor = Color.Gainsboro
};
logGroup.Controls.Add(mLogBox);
split.Panel1.Controls.Add(eggGroup);
split.Panel2.Controls.Add(logGroup);
// ---- virtual launcher / site management column ----
GroupBox siteGroup = new GroupBox
{
Text = "Launcher / Site Management",
Dock = DockStyle.Right,
Width = 360,
Padding = new Padding(8)
};
Panel siteTop = new Panel { Dock = DockStyle.Top, Height = 234 };
mProvisionStatusLabel = new Label
{
AutoSize = true,
Location = new Point(4, 4),
Font = new Font("Segoe UI", 9f, FontStyle.Bold),
Text = "Provisioning: —"
};
// Stand-in for the real pod's TeslaPasscodeDisplay: the operator reads
// these two values into the console's Configure dialog.
mPassphrasePanel = new Panel { Location = new Point(4, 28), Size = new Size(336, 62), Visible = false };
Label requestIdCaption = new Label { AutoSize = true, Location = new Point(0, 6), Text = "Request ID:" };
mRequestIdValueLabel = new Label
{
AutoSize = true,
Location = new Point(90, 0),
Font = new Font("Segoe UI", 15f, FontStyle.Bold),
Text = "—"
};
Label passphraseCaption = new Label { AutoSize = true, Location = new Point(0, 38), Text = "Passphrase:" };
mPassphraseValueLabel = new Label
{
AutoSize = true,
Location = new Point(90, 32),
Font = new Font("Segoe UI", 15f, FontStyle.Bold),
ForeColor = Color.Firebrick,
Text = "—"
};
mPassphrasePanel.Controls.Add(requestIdCaption);
mPassphrasePanel.Controls.Add(mRequestIdValueLabel);
mPassphrasePanel.Controls.Add(passphraseCaption);
mPassphrasePanel.Controls.Add(mPassphraseValueLabel);
mNetConfigLabel = new Label
{
Location = new Point(4, 94),
Size = new Size(336, 32),
ForeColor = Color.Gray,
Text = ""
};
mRpcStatusLabel = new Label
{
AutoSize = true,
Location = new Point(4, 130),
Text = "Launcher RPC: not running"
};
mInstallStatusLabel = new Label
{
Location = new Point(4, 154),
Size = new Size(336, 18),
Text = "No install in progress."
};
mInstallProgressBar = new ProgressBar
{
Location = new Point(4, 176),
Size = new Size(240, 22),
Minimum = 0,
Maximum = 100
};
mReprovisionButton = new Button
{
Text = "Reprovision",
Location = new Point(252, 174),
Size = new Size(88, 26)
};
mReprovisionButton.Click += ReprovisionClicked;
// Off = LaunchApp records simulated PIDs; on = start/kill real processes
// (the entries point into the real C:\Games, so this runs what the
// console deployed — exactly like the Agent).
mRealLaunchCheckbox = new CheckBox
{
Text = "Actually launch apps (real processes)",
Location = new Point(4, 206),
AutoSize = true,
Checked = false
};
mRealLaunchCheckbox.CheckedChanged += (s, e) =>
{
mLauncher.RealLaunch = mRealLaunchCheckbox.Checked;
OnSiteLog(mRealLaunchCheckbox.Checked
? "Launch/Kill now start and terminate REAL processes."
: "Launch/Kill now simulate PIDs (no real processes).");
};
siteTop.Controls.Add(mProvisionStatusLabel);
siteTop.Controls.Add(mPassphrasePanel);
siteTop.Controls.Add(mNetConfigLabel);
siteTop.Controls.Add(mRpcStatusLabel);
siteTop.Controls.Add(mInstallStatusLabel);
siteTop.Controls.Add(mInstallProgressBar);
siteTop.Controls.Add(mReprovisionButton);
siteTop.Controls.Add(mRealLaunchCheckbox);
mAppsView = new ListView
{
Dock = DockStyle.Fill,
View = View.Details,
FullRowSelect = true,
HeaderStyle = ColumnHeaderStyle.Nonclickable
};
mAppsView.Columns.Add("Installed App", 150);
mAppsView.Columns.Add("PID", 48);
mAppsView.Columns.Add("Auto", 42);
mAppsView.Columns.Add("Command", 320);
siteGroup.Controls.Add(mAppsView);
siteGroup.Controls.Add(siteTop);
Controls.Add(split);
Controls.Add(statusGroup);
Controls.Add(siteGroup);
}
private void OnFormLoad(object sender, EventArgs e)
{
// Now that the split has its real width, give the log half the window.
if (mSplit.Width > mSplit.Panel1MinSize + mSplit.Panel2MinSize)
{
mSplit.SplitterDistance = mSplit.Width / 2;
}
UpdateConnectionLabel(null);
UpdateStateLabel(mSimulator.State);
RefreshAppsList();
PowerOnClicked(this, EventArgs.Empty);
}
private void OnFormClosing(object sender, FormClosingEventArgs e)
{
mServer.Stop();
StopManagement();
mLauncher.KillAllApps(); // don't orphan real launched processes when the tool exits
}
///
/// 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)
{
mRebootTimer.Stop();
SetPoweredState(on: true);
StartManagement();
StartGameClicked(sender, e);
}
///
/// 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)
{
mRebootTimer.Stop();
StopGame();
StopManagement();
mLauncher.KillAllApps(); // the machine is "off": launched apps (real or simulated) die with it
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),
/// then let the watchdog timer relaunch it.
///
private void OnEndMissionExit()
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
OnLog("Game exited gracefully (end mission); watchdog will restart it...");
mServer.Stop();
UpdateListeningLabel(false);
UpdateConnectionLabel(null);
mStateLabel.Text = "Restarting...";
mStateLabel.ForeColor = Color.DarkOrange;
mStartGameButton.Enabled = true;
mStopGameButton.Enabled = false;
mResetButton.Enabled = false;
mRestartTimer.Stop();
mRestartTimer.Start();
}));
}
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();
SetGameState(running: true);
}
else
{
SetGameState(running: false);
}
}
/// Starts the listener and updates the status label. Returns false (with a message) if the port is unavailable.
private bool StartServer()
{
try
{
mServer.Start();
UpdateListeningLabel(true);
return true;
}
catch (Exception ex)
{
UpdateListeningLabel(false);
OnLog("FAILED to listen on port " + mOptions.Port + ": " + ex.Message);
MessageBox.Show(this,
"Could not listen on TCP port " + mOptions.Port + ".\n\n" + ex.Message +
"\n\nIs another pod or vPOD already using it?",
"vPOD", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
}
/// 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;
mReprovisionButton.Enabled = on && !mOptions.NoManage;
if (!on)
{
mStartGameButton.Enabled = false;
mStopGameButton.Enabled = false;
mResetButton.Enabled = false;
UpdateListeningLabel(false);
UpdateConnectionLabel(null);
mStateLabel.Text = "Powered Off";
mStateLabel.ForeColor = Color.Gray;
mProvisionStatusLabel.Text = "Provisioning: pod powered off";
mProvisionStatusLabel.ForeColor = Color.Gray;
mPassphrasePanel.Visible = false;
mRpcStatusLabel.Text = "Launcher RPC: pod powered off";
mRpcStatusLabel.ForeColor = Color.Gray;
}
}
/// 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
/// when a session key is stored, otherwise the provisioning beacons.
private void StartManagement()
{
if (mOptions.NoManage)
{
mProvisionStatusLabel.Text = "Provisioning: disabled (-nomanage)";
mProvisionStatusLabel.ForeColor = Color.Gray;
mRpcStatusLabel.Text = "Launcher RPC: disabled (-nomanage)";
mRpcStatusLabel.ForeColor = Color.Gray;
return;
}
byte[] key = mLauncher.LoadKey();
if (key != null)
{
StartRpcServer(key);
}
else
{
StartProvisioning();
}
}
private void StopManagement()
{
mProvisioning.Stop();
mRpcServer.Stop();
}
private void StartRpcServer(byte[] key)
{
try
{
mRpcServer.Start(key);
}
catch (Exception ex)
{
OnSiteLog($"FAILED to listen on TCP {LauncherRpcServer.ManagePort}: {ex.Message}");
mRpcStatusLabel.Text = $"Launcher RPC: TCP {LauncherRpcServer.ManagePort} unavailable";
mRpcStatusLabel.ForeColor = Color.Firebrick;
return;
}
mProvisionStatusLabel.Text = "Provisioned — key " + KeyFingerprint(key);
mProvisionStatusLabel.ForeColor = Color.ForestGreen;
mPassphrasePanel.Visible = false;
mRpcStatusLabel.Text = $"Launcher RPC: listening on TCP {LauncherRpcServer.ManagePort}";
mRpcStatusLabel.ForeColor = Color.ForestGreen;
}
private void StartProvisioning()
{
mProvisioning.Start();
mProvisionStatusLabel.Text = "Unprovisioned — beaconing to console...";
mProvisionStatusLabel.ForeColor = Color.DarkGoldenrod;
mRequestIdValueLabel.Text = mProvisioning.RequestId;
mPassphraseValueLabel.Text = mProvisioning.Passphrase;
mPassphrasePanel.Visible = true;
mNetConfigLabel.Text = "Use Manage Site's \"Configure\" button and enter the passphrase above.";
mRpcStatusLabel.Text = "Launcher RPC: waiting for provisioning";
mRpcStatusLabel.ForeColor = Color.Gray;
}
/// Drops the session key, clears the installed-apps store and
/// re-enters provisioning — from the UI button, or when the console's
/// Reconfigure clears our store. (Fresh-pod state; extracted C:\Games
/// files are left on disk.)
private void DropKeyAndReprovision()
{
mLauncher.WipeApps();
mLauncher.DeleteKey();
mRpcServer.Stop();
mProvisioning.Stop();
mNetConfigLabel.Text = "";
if (mPoweredOn && !mOptions.NoManage)
{
StartProvisioning();
}
}
private void ReprovisionClicked(object sender, EventArgs e)
{
OnSiteLog("Reprovision requested from the vPOD window.");
DropKeyAndReprovision();
}
private static string KeyFingerprint(byte[] key)
{
return key.Length >= 4 ? $"{key[0]:X2}{key[1]:X2}{key[2]:X2}{key[3]:X2}…" : "?";
}
private void GameToggleChanged(object sender, EventArgs e)
{
if (sender is RadioButton rb && !rb.Checked)
{
return; // only act on the newly-checked one
}
mSimulator.ApplicationId = mBattleTechRadio.Checked ? ApplicationID.BTL4 : ApplicationID.RPL4;
}
// ---- event handlers (marshal to UI thread) ----
private const int MaxLogLines = 500;
private void OnLog(string message)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
// Newest first: prepend, and cap the buffer so it can't grow without bound.
string line = $"[{DateTime.Now:HH:mm:ss}] {message}";
string existing = mLogBox.Text;
string combined = existing.Length > 0 ? line + "\r\n" + existing : line;
string[] lines = combined.Split(new[] { "\r\n" }, StringSplitOptions.None);
if (lines.Length > MaxLogLines)
{
combined = string.Join("\r\n", lines, 0, MaxLogLines);
}
mLogBox.Text = combined;
}));
}
private void OnConnectionChanged(string remote)
{
if (IsDisposed) return;
BeginInvoke((Action)(() => UpdateConnectionLabel(remote)));
}
private void OnMessageReceived(MungaPodServer.Incoming incoming)
{
// The simulator drives all protocol behaviour; the UI only logs
// non-query traffic (StateQuery is once-a-second noise).
if (!(incoming.Message is StateQueryMessage) && incoming.Message != null)
{
OnLog("<- " + incoming.Message.GetType().Name);
}
mSimulator.HandleMessage(incoming);
}
private void OnStateChanged(ApplicationState state)
{
if (IsDisposed) return;
// The egg viewer is intentionally NOT cleared here — the last egg stays
// visible (copyable) across missions/restarts until the Clear button.
BeginInvoke((Action)(() => UpdateStateLabel(state)));
}
private void OnEggReceived(string eggText)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
mEggBox.Text = eggText.Replace("\n", "\r\n");
mEggSummaryLabel.Text = SummarizeEgg(eggText);
}));
}
private void OnEggProgress()
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
int pct = mSimulator.EggPercent;
if (pct < 100 && pct > 0)
{
mEggSummaryLabel.Text = $"Receiving egg... {pct}%";
}
}));
}
// ---- virtual launcher event handlers (marshal to UI thread) ----
private void OnSiteLog(string message)
{
OnLog("[SITE] " + message);
}
private void OnProvisioned(byte[] key)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
mLauncher.SaveKey(key);
mProvisioning.Stop();
StartRpcServer(key);
}));
}
private void OnConfigReceived(BasicConfigResponse config)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
mNetConfigLabel.Text = $"Assigned (display only): IP {config.Address} / {config.Mask}" +
(string.IsNullOrEmpty(config.HostName) ? "" : $"\r\nhost \"{config.HostName}\", gw {config.Gateway}, dns {config.Dns}");
}));
}
private void OnRpcConnectionsChanged(int count)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
if (!mRpcServer.IsListening)
{
return; // Stop() already updated the label
}
mRpcStatusLabel.Text = count > 0
? $"Launcher RPC: {count} console session(s) on TCP {LauncherRpcServer.ManagePort}"
: $"Launcher RPC: listening on TCP {LauncherRpcServer.ManagePort}";
mRpcStatusLabel.ForeColor = Color.ForestGreen;
}));
}
private void OnAppsChanged()
{
if (IsDisposed) return;
BeginInvoke((Action)RefreshAppsList);
}
private void RefreshAppsList()
{
LaunchData[] installed = mLauncher.GetInstalledApps();
LaunchedAppData[] launched = mLauncher.GetLaunchedApps();
mAppsView.BeginUpdate();
mAppsView.Items.Clear();
foreach (LaunchData app in installed)
{
StringBuilder pids = new StringBuilder();
foreach (LaunchedAppData proc in launched)
{
if (proc.LaunchKey == app.LaunchPair.LaunchKey)
{
if (pids.Length > 0) pids.Append(", ");
pids.Append(proc.ProcessId);
}
}
ListViewItem item = new ListViewItem(new[]
{
app.LaunchPair.DisplayName,
pids.ToString(),
app.AutoRestart ? "yes" : "",
string.IsNullOrEmpty(app.Arguments) ? app.ExeFile : app.ExeFile + " " + app.Arguments
});
mAppsView.Items.Add(item);
}
mAppsView.EndUpdate();
}
private void OnInstallProgress(Guid callId, OutOfBandProgress progress)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
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}%)";
}));
}
/// The console asked the pod to shut down or restart: go fully dark
/// (Munga + launcher), and come back after a "reboot" pause when restarting.
private void OnShutdownRequested(bool restart)
{
if (IsDisposed) return;
BeginInvoke((Action)(() =>
{
mRebootTimer.Stop();
StopGame();
StopManagement();
mLauncher.KillAllApps(); // launched apps die with the rebooting/halting machine
SetPoweredState(on: false);
if (restart)
{
mStateLabel.Text = "Rebooting...";
mStateLabel.ForeColor = Color.DarkOrange;
mRebootTimer.Start();
}
}));
}
private void OnRebootTimerTick(object sender, EventArgs e)
{
mRebootTimer.Stop();
OnLog("[SITE] Pod rebooted.");
PowerOnClicked(this, EventArgs.Empty);
}
/// The console's Reconfigure cleared our store — drop the key and go
/// back to beaconing so the "Configure" button reappears in Manage Site.
private void OnReprovisionRequested()
{
if (IsDisposed) return;
BeginInvoke((Action)DropKeyAndReprovision);
}
// ---- label helpers ----
private void UpdateListeningLabel(bool listening)
{
mListeningLabel.Text = listening
? $"Listening on TCP {mOptions.Port} ●"
: $"Not listening on TCP {mOptions.Port}";
mListeningLabel.ForeColor = listening ? Color.ForestGreen : Color.Firebrick;
}
private void UpdateConnectionLabel(string remote)
{
if (string.IsNullOrEmpty(remote))
{
mConnectionLabel.Text = "Console: not connected";
mConnectionLabel.ForeColor = Color.Gray;
}
else
{
mConnectionLabel.Text = "Console: connected from " + remote;
mConnectionLabel.ForeColor = Color.ForestGreen;
}
}
private void UpdateStateLabel(ApplicationState state)
{
mStateLabel.Text = Prettify(state);
mStateLabel.ForeColor = ColorFor(state);
}
private static string RoleText(HostType hostType)
{
switch (hostType)
{
case HostType.MissionReviewHostType:
return "Camera / Mission Review";
case HostType.ConsoleHostType:
return "Console";
default:
return "Game Machine (player)";
}
}
private static string Prettify(ApplicationState state)
{
switch (state)
{
case ApplicationState.InitializingState: return "Initializing";
case ApplicationState.WaitingForEgg: return "Waiting For Egg";
case ApplicationState.LoadingMission: return "Loading Mission";
case ApplicationState.WaitingForLaunch: return "Waiting For Launch";
case ApplicationState.LaunchingMission: return "Launching Mission";
case ApplicationState.RunningMission: return "Running Mission";
case ApplicationState.EndingMission: return "Ending Mission";
case ApplicationState.StoppingMission: return "Stopping Mission";
case ApplicationState.SuspendingMission: return "Suspended";
case ApplicationState.ResumingMission: return "Resuming Mission";
case ApplicationState.AbortingMission: return "Aborting Mission";
case ApplicationState.CreatingMission: return "Creating Mission";
default: return state.ToString();
}
}
private static Color ColorFor(ApplicationState state)
{
switch (state)
{
case ApplicationState.WaitingForEgg: return Color.ForestGreen;
case ApplicationState.WaitingForLaunch: return Color.DarkGoldenrod;
case ApplicationState.RunningMission: return Color.RoyalBlue;
case ApplicationState.InitializingState: return Color.Gray;
default: return Color.DarkOrange;
}
}
private static string SummarizeEgg(string eggText)
{
string adventure = null, map = null, scenario = null;
int pilots = 0;
foreach (string raw in eggText.Split('\n'))
{
string line = raw.Trim();
if (line.StartsWith("adventure=")) adventure = line.Substring(10);
else if (line.StartsWith("map=")) map = line.Substring(4);
else if (line.StartsWith("scenario=")) scenario = line.Substring(9);
else if (line.StartsWith("pilot=")) pilots++;
}
StringBuilder sb = new StringBuilder();
sb.Append(adventure ?? "(egg)");
if (map != null) sb.Append(" • map=").Append(map);
if (scenario != null) sb.Append(" • ").Append(scenario);
sb.Append(" • ").Append(pilots).Append(pilots == 1 ? " pilot" : " pilots");
return sb.ToString();
}
}