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;
}
}