using System; using System.Collections.Generic; using System.Text; using System.Threading; using Munga.Net; namespace VPod; /// /// Emulates a game client's ApplicationState machine over the Munga control /// channel, driving it from the messages the console sends. This is the brain of /// vPOD: it answers state queries, reassembles the streamed egg, and walks the /// pod through the load/run/stop lifecycle the console's game window expects. /// /// The console (RPGame/BTGame NetworkScan) gates purely on the reported /// ApplicationState: /// WaitingForEgg -- ready; console streams the egg here /// (egg complete) -> LoadingMission -> WaitingForLaunch (console's "Loaded") /// RunMission -> LaunchingMission -> RunningMission (console's "Running") /// StopMission -> EndingMission -> WaitingForEgg (mission over/reset) /// AbortMission -> AbortingMission -> WaitingForEgg /// Intermediate states are shown for realism; the console only waits on the gate /// states, so a short delay between them is harmless. /// internal sealed class PodSimulator { private const int NetworkManagerFromHost = 0; private readonly MungaPodServer mServer; private readonly object mLock = new object(); private ApplicationState mState = ApplicationState.InitializingState; private ApplicationID mApplicationId; private int mHostId; private int mTransitionDelayMs = 400; // Egg reassembly private byte[] mEggBuffer; private int mEggTotal; private int mEggReceived; private readonly HashSet mEggChunks = new HashSet(); private string mEggText; // Cancels a pending delayed transition when a newer one supersedes it. private int mTransitionToken; private Timer mTransitionTimer; public event Action StateChanged; public event Action EggReceived; // full egg text (NUL-normalized) public event Action EggProgress; // a chunk arrived (mid-transfer) public event Action Log; public PodSimulator(MungaPodServer server, ApplicationID applicationId, int hostId) { mServer = server; mApplicationId = applicationId; mHostId = hostId; } public ApplicationState State { get { lock (mLock) { return mState; } } } public ApplicationID ApplicationId { get { lock (mLock) { return mApplicationId; } } set { lock (mLock) { mApplicationId = value; } Log?.Invoke("Reported application changed to " + value + "."); } } public int HostId { get { lock (mLock) { return mHostId; } } } public int TransitionDelayMs { get { lock (mLock) { return mTransitionDelayMs; } } set { lock (mLock) { mTransitionDelayMs = Math.Max(0, value); } } } public string EggText { get { lock (mLock) { return mEggText; } } } public int EggPercent { get { lock (mLock) { if (mEggTotal <= 0) return 0; return (int)(100L * mEggReceived / mEggTotal); } } } /// Bring the pod online: Initializing -> WaitingForEgg. public void PowerOn() { SetState(ApplicationState.InitializingState); ScheduleTransition(ApplicationState.WaitingForEgg, ResetEggOnArrive: true); } /// Operator reset: drop any mission and return to WaitingForEgg immediately. public void Reset() { lock (mLock) { ClearEggLocked(); CancelPendingLocked(); } SetState(ApplicationState.WaitingForEgg); Log?.Invoke("Reset to WaitingForEgg."); } public void HandleMessage(MungaPodServer.Incoming incoming) { MungaMessage msg = incoming?.Message; if (msg == null) { return; } switch (msg) { case StateQueryMessage _: RespondState(); break; case EggFileMessage egg: HandleEggChunk(egg); break; case RunMissionMessage _: Log?.Invoke("<- RunMissionMessage"); if (State == ApplicationState.WaitingForLaunch) { SetState(ApplicationState.LaunchingMission); mServer.Send(new ReadyToRunMessage(HostId), 0, HostId); ScheduleTransition(ApplicationState.RunningMission); } break; case StopMissionMessage _: Log?.Invoke("<- StopMissionMessage"); if (State == ApplicationState.RunningMission || State == ApplicationState.SuspendingMission) { SetState(ApplicationState.EndingMission); ScheduleTransition(ApplicationState.WaitingForEgg, ResetEggOnArrive: true); } break; case AbortMissionMessage _: Log?.Invoke("<- AbortMissionMessage"); SetState(ApplicationState.AbortingMission); mServer.Send(new AbortMissionResponseMessage(HostId), 0, HostId); ScheduleTransition(ApplicationState.WaitingForEgg, ResetEggOnArrive: true); break; case SuspendMissionMessage _: Log?.Invoke("<- SuspendMissionMessage"); if (State == ApplicationState.RunningMission) { SetState(ApplicationState.SuspendingMission); } break; case ResumeMissionMessage _: Log?.Invoke("<- ResumeMissionMessage"); if (State == ApplicationState.SuspendingMission) { SetState(ApplicationState.ResumingMission); ScheduleTransition(ApplicationState.RunningMission); } break; case LoadMissionMessage _: Log?.Invoke("<- LoadMissionMessage"); if (State == ApplicationState.WaitingForEgg) { SetState(ApplicationState.LoadingMission); ScheduleTransition(ApplicationState.WaitingForLaunch); } break; case CheckLoadMessage _: Log?.Invoke("<- CheckLoadMessage"); break; case LightsOutMissionMessage _: Log?.Invoke("<- LightsOutMissionMessage"); break; } } private void RespondState() { ApplicationState state; ApplicationID app; int host; lock (mLock) { state = mState; app = mApplicationId; host = mHostId; } mServer.Send(new StateResponseMessage(host, state, app), 0, host); } private void HandleEggChunk(EggFileMessage egg) { bool complete = false; string text = null; lock (mLock) { if (mEggBuffer == null || mEggTotal != egg.NotationFileLength) { mEggTotal = egg.NotationFileLength; mEggBuffer = new byte[mEggTotal]; mEggReceived = 0; mEggChunks.Clear(); } int offset = egg.SequenceNumber * 1000; int len = egg.ThisMessageLength; if (offset >= 0 && offset + len <= mEggBuffer.Length && mEggChunks.Add(egg.SequenceNumber)) { Buffer.BlockCopy(egg.NotationData, 0, mEggBuffer, offset, len); mEggReceived += len; } if (mEggReceived >= mEggTotal && mEggTotal > 0) { // On the wire the egg is NUL-delimited ASCII; show it line-per-field. text = Encoding.ASCII.GetString(mEggBuffer).Replace('\0', '\n'); mEggText = text; complete = true; } } EggProgress?.Invoke(); if (complete) { Log?.Invoke($"Egg received ({mEggTotal} bytes, {mEggChunks.Count} chunks). Acknowledging."); mServer.Send(new AcknowledgeEggFileMessage(), NetworkManagerFromHost, HostId); EggReceived?.Invoke(text); // WaitingForEgg -> LoadingMission -> WaitingForLaunch SetState(ApplicationState.LoadingMission); ScheduleTransition(ApplicationState.WaitingForLaunch); } } private void SetState(ApplicationState state) { bool changed; lock (mLock) { changed = mState != state; mState = state; } if (changed) { Log?.Invoke("State -> " + state); StateChanged?.Invoke(state); } } private void ScheduleTransition(ApplicationState target, bool ResetEggOnArrive = false) { int token; int delay; lock (mLock) { token = ++mTransitionToken; delay = mTransitionDelayMs; mTransitionTimer?.Dispose(); mTransitionTimer = new Timer(delegate { bool proceed; lock (mLock) { proceed = token == mTransitionToken; if (proceed && ResetEggOnArrive) { ClearEggLocked(); } } if (proceed) { SetState(target); } }, null, delay, Timeout.Infinite); } } private void CancelPendingLocked() { mTransitionToken++; mTransitionTimer?.Dispose(); mTransitionTimer = null; } private void ClearEggLocked() { mEggBuffer = null; mEggTotal = 0; mEggReceived = 0; mEggChunks.Clear(); mEggText = null; } }