vPOD has outgrown its home inside the console's folder: it now emulates both halves of a pod (Munga game client + TeslaLauncher service / provisioning), so it lives at the repo root beside Console/, Launcher/, Contract/ and SecureConfig/, like the peer it has become. Accompanying changes: project references rebased (Contract, SecureConfig, the console's vendored Munga Net.dll), solution + DiffTests reference paths, the console csproj's now-obsolete vPOD source exclusion removed, and the root README / Apps.xml / vPOD README path mentions updated. pack.ps1 is self-relative and now emits vPOD/dist/vPOD.zip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
255 lines
6.4 KiB
C#
255 lines
6.4 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Threading;
|
|
using Munga.Net;
|
|
|
|
namespace VPod;
|
|
|
|
/// <summary>
|
|
/// The server side of the Munga control protocol. A real game client is the
|
|
/// Munga server (the console <c>Connect()</c>s to pod:1501); the vendored
|
|
/// <see cref="MungaSocket" /> 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.
|
|
/// </summary>
|
|
internal sealed class MungaPodServer
|
|
{
|
|
/// <summary>A message received from the console, plus the header it arrived under.</summary>
|
|
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<Incoming> MessageReceived;
|
|
public event Action<string> ConnectionChanged; // remote endpoint string, or null when dropped
|
|
public event Action<string> 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 };
|
|
}
|
|
|
|
/// <summary>Maps a (ClientID, MessageID) to the vendored typed message. Mirrors MungaSocket.Receive.</summary>
|
|
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
|
|
}
|
|
|
|
/// <summary>Sends a pod->console message on the current connection (no-op if disconnected).</summary>
|
|
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;
|
|
}
|
|
}
|