using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; using Munga.Net; namespace VPod; /// /// The server side of the Munga control protocol. A real game client is the /// Munga server (the console Connect()s to pod:1501); the vendored /// only implements the client half, so this /// reimplements the identical framing for the listening side. /// /// Wire format (mirrors MungaSocket.Send/Receive, little-endian): /// [16-byte NetworkPacketHeader][MungaMessage: 4=len 4=id 4=flags + body] /// where the message length field counts the 12-byte base but not the header. /// Messages are dispatched to the vendored typed classes by ClientID+MessageID. /// internal sealed class MungaPodServer { /// A message received from the console, plus the header it arrived under. public sealed class Incoming { public NetworkPacketHeader Header; public MungaMessage Message; } private readonly int mPort; private TcpListener mListener; private Thread mAcceptThread; private volatile bool mRunning; private readonly object mClientLock = new object(); private TcpClient mClient; private NetworkStream mStream; public event Action MessageReceived; public event Action ConnectionChanged; // remote endpoint string, or null when dropped public event Action Log; public bool IsListening => mRunning; public bool IsConnected { get { lock (mClientLock) { return mClient != null && mClient.Connected; } } } public MungaPodServer(int port) { mPort = port; } public void Start() { if (mRunning) { return; } mListener = new TcpListener(IPAddress.Any, mPort); mListener.Start(); mRunning = true; mAcceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "vPOD-accept" }; mAcceptThread.Start(); Log?.Invoke($"Listening on TCP {mPort} (all interfaces)."); } public void Stop() { mRunning = false; try { mListener?.Stop(); } catch { } DropClient(); Log?.Invoke("Stopped listening."); } private void AcceptLoop() { while (mRunning) { TcpClient client; try { client = mListener.AcceptTcpClient(); } catch { break; // listener stopped } // One console at a time: replace any prior connection. DropClient(); lock (mClientLock) { mClient = client; mClient.NoDelay = true; mStream = mClient.GetStream(); } string remote = client.Client.RemoteEndPoint?.ToString() ?? "?"; Log?.Invoke($"Console connected from {remote}."); ConnectionChanged?.Invoke(remote); ReceiveLoop(client); Log?.Invoke("Console disconnected."); ConnectionChanged?.Invoke(null); } } private void ReceiveLoop(TcpClient client) { try { NetworkStream stream = client.GetStream(); while (mRunning && client.Connected) { Incoming incoming = ReadMessage(stream); if (incoming == null) { break; } MessageReceived?.Invoke(incoming); } } catch (Exception ex) { Log?.Invoke("Receive error: " + ex.Message); } finally { DropClient(); } } private static Incoming ReadMessage(NetworkStream stream) { byte[] headerBytes = ReadExact(stream, NetworkPacketHeader.PacketHeaderSize); // 16 if (headerBytes == null) { return null; } NetworkPacketHeader header = new NetworkPacketHeader(headerBytes); byte[] baseBytes = ReadExact(stream, MungaMessage.BaseMessageSize); // 12 if (baseBytes == null) { return null; } MungaMessage probe = new MungaMessage(new BinaryReader(new MemoryStream(baseBytes)), header.ClientID); int total = probe.MessageLength; // includes the 12-byte base byte[] full = new byte[total]; Buffer.BlockCopy(baseBytes, 0, full, 0, MungaMessage.BaseMessageSize); if (total > MungaMessage.BaseMessageSize) { byte[] body = ReadExact(stream, total - MungaMessage.BaseMessageSize); if (body == null) { return null; } Buffer.BlockCopy(body, 0, full, MungaMessage.BaseMessageSize, body.Length); } BinaryReader reader = new BinaryReader(new MemoryStream(full)); MungaMessage message = Dispatch(header.ClientID, probe.MessageID, reader); return new Incoming { Header = header, Message = message }; } /// Maps a (ClientID, MessageID) to the vendored typed message. Mirrors MungaSocket.Receive. private static MungaMessage Dispatch(ClientID clientId, int messageId, BinaryReader reader) { switch (clientId) { case ClientID.NetworkManagerClientID: switch (messageId) { case 3: return new EggFileMessage(reader); case 4: return new AcknowledgeEggFileMessage(reader); } break; case ClientID.ApplicationClientID: switch (messageId) { case 3: return new StateQueryMessage(reader); case 4: return new CheckLoadMessage(reader); case 5: return new RunMissionMessage(reader); case 6: return new StopMissionMessage(reader); case 8: return new SuspendMissionMessage(reader); case 9: return new ResumeMissionMessage(reader); case 10: return new LoadMissionMessage(reader); case 11: return new AbortMissionMessage(reader); case 12: return new LightsOutMissionMessage(reader); } break; } return null; // unknown/ignored message id } /// Sends a pod->console message on the current connection (no-op if disconnected). public void Send(MungaMessage message, int gameId, int fromHost) { lock (mClientLock) { if (mStream == null || mClient == null || !mClient.Connected) { return; } try { NetworkPacketHeader header = new NetworkPacketHeader(message.ClientID, gameId, fromHost, Environment.TickCount); MemoryStream ms = new MemoryStream(); BinaryWriter writer = new BinaryWriter(ms); header.WriteTo(writer); message.WriteTo(writer); byte[] bytes = ms.ToArray(); mStream.Write(bytes, 0, bytes.Length); mStream.Flush(); } catch (Exception ex) { Log?.Invoke("Send error: " + ex.Message); } } } private void DropClient() { lock (mClientLock) { try { mStream?.Dispose(); } catch { } try { mClient?.Close(); } catch { } mStream = null; mClient = null; } } private static byte[] ReadExact(NetworkStream stream, int count) { byte[] buffer = new byte[count]; int offset = 0; while (offset < count) { int read = stream.Read(buffer, offset, count - offset); if (read == 0) { return null; // connection closed } offset += read; } return buffer; } }