using System; using System.Drawing; using System.Text; using System.Windows.Forms; using Munga.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. /// internal sealed class VPodForm : Form { private readonly PodArguments mOptions; private readonly MungaPodServer mServer; private readonly PodSimulator mSimulator; private Label mListeningLabel; private Label mConnectionLabel; private Label mStateLabel; private Label mEggSummaryLabel; private RadioButton mRedPlanetRadio; private RadioButton mBattleTechRadio; private Button mPowerButton; private Button mResetButton; private TextBox mEggBox; private TextBox mLogBox; public VPodForm(PodArguments options) { mOptions = options; mServer = new MungaPodServer(options.Port); mSimulator = new PodSimulator(mServer, options.Application, options.HostId); BuildUi(); mServer.Log += OnLog; mServer.ConnectionChanged += OnConnectionChanged; mServer.MessageReceived += OnMessageReceived; mSimulator.Log += OnLog; mSimulator.StateChanged += OnStateChanged; mSimulator.EggReceived += OnEggReceived; mSimulator.EggProgress += OnEggProgress; Load += OnFormLoad; FormClosing += OnFormClosing; } private void BuildUi() { Text = $"vPOD - virtual pod (port {mOptions.Port}, host {mOptions.HostId})"; ClientSize = new Size(920, 560); MinimumSize = new Size(760, 460); Font = new Font("Segoe UI", 9f); // ---- status panel ---- GroupBox statusGroup = new GroupBox { Text = "Pod Status", Dock = DockStyle.Top, Height = 168, 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 = "—" }; // game toggle GroupBox gameGroup = new GroupBox { Text = "Mimicking Game", Location = new Point(560, 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; mPowerButton = new Button { Text = "Power On", Location = new Point(18, 88), Size = new Size(90, 28) }; mResetButton = new Button { Text = "Reset", Location = new Point(116, 88), Size = new Size(90, 28) }; mPowerButton.Click += (s, e) => mSimulator.PowerOn(); mResetButton.Click += (s, e) => mSimulator.Reset(); gameGroup.Controls.Add(mRedPlanetRadio); gameGroup.Controls.Add(mBattleTechRadio); gameGroup.Controls.Add(mPowerButton); gameGroup.Controls.Add(mResetButton); statusGroup.Controls.Add(mListeningLabel); statusGroup.Controls.Add(mConnectionLabel); statusGroup.Controls.Add(roleLabel); statusGroup.Controls.Add(stateCaption); statusGroup.Controls.Add(mStateLabel); statusGroup.Controls.Add(gameGroup); // ---- egg viewer + log ---- SplitContainer split = new SplitContainer { Dock = DockStyle.Fill, SplitterDistance = 480, Orientation = Orientation.Vertical }; GroupBox eggGroup = new GroupBox { Text = "Current Egg", Dock = DockStyle.Fill, Padding = new Padding(8) }; mEggSummaryLabel = new Label { Dock = DockStyle.Top, Height = 40, Text = "No egg loaded." }; 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(mEggSummaryLabel); 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); Controls.Add(split); Controls.Add(statusGroup); } private void OnFormLoad(object sender, EventArgs e) { UpdateListeningLabel(false); UpdateConnectionLabel(null); UpdateStateLabel(mSimulator.State); try { mServer.Start(); UpdateListeningLabel(true); mSimulator.PowerOn(); } 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); } } private void OnFormClosing(object sender, FormClosingEventArgs e) { mServer.Stop(); } 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; 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}%"; } })); } // ---- 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(); } }