Files
MSCO-Dedicated-Server/Protocol.cs
T
2026-09-27 15:13:13 +02:00

241 lines
9.1 KiB
C#

using System.Net.Sockets;
namespace MSCO.Server;
/// <summary>Muss mit MsgType in der Mod (NetSession.cs) übereinstimmen.</summary>
public enum MsgType : byte
{
Hello = 1,
Welcome = 2,
Reject = 3,
Player = 4,
SaveBegin = 5,
SaveFile = 6,
StartGame = 7,
ObjStates = 8,
ObjOwn = 9,
ObjRelease = 10,
FsmEvent = 11,
PlayerJoined = 12,
PlayerLeft = 13,
VehicleFx = 14,
Ping = 15,
Ready = 16, // Client -> Server: Spiel geladen, Objekte erfasst
WorldSnapshot = 17,// Server -> Client: aktueller Weltzustand
Info = 18, // Server-Browser fragt Infos ab (ohne Hello)
InfoReply = 19,
WorldTime = 20, // Uhrzeit + Wetter vom "Weltuhr"-Spieler
MoneyDelta = 21, // Geld hat sich geändert (+/- Betrag, gemeinsame Kasse)
DoorState = 22, // Tür/Tor/Haube hat den Zustand gewechselt
AdminStatus = 23, // Server -> Client: bool, ist Admin
AdminListRequest = 24, // Client -> Server: Spielerliste fürs Admin-Menü
AdminList = 25, // Server -> Client: int Anzahl, je [int id, string name, string steamId, bool admin]
AdminAction = 26, // Client -> Server: byte (1 = kick, 2 = ban), int Spieler-ID, string Grund
AdminResult = 27, // Server -> Client: string Meldung
Chat = 28, // Chatnachricht (string)
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
Teleport = 30, // Admin holt einen Spieler zu sich: 3 float Position
Pong = 31, // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
SpawnItem = 32, // Admin spawnt ein Item: string Item-Name, 3 float Position, 4 float Drehung
Reconnect = 33, // Server -> Client: string Meldung; Client geht ins Menü und verbindet sich neu (z.B. Welt zurückgesetzt)
SaveRequest = 34, // Server -> Client: int Anfrage; bitte im Spiel speichern und den Spielstand hochladen
SaveUploadPart = 35, // Client -> Server: int Anfrage, string Datei, int Gesamtgröße, int Offset, int Länge, bytes
SaveUploadDone = 36, // Client -> Server: int Anfrage, int Anzahl Dateien
ItemAppeared = 37, // neues Item bei einem Spieler (Einkauf ...): int Objekt-ID, string Objektname, 3 float Pos, 4 float Rot
ObjDestroy = 38, // Item verbraucht/zerstört (gegessen, getrunken ...): int Objekt-ID
VehicleDamage = 39, // Unfallschaden (verformte Karosserie): int Mesh-Schlüssel, int Eckpunkte, int n, je [int Index, 3 float]
TrafficStates = 40, // Verkehr, nur von der Welt-Autorität: int n, je [int Schlüssel, 3 float Pos, 4 float Rot, 3 float Geschw.]
WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ, Wert]
HingeStates = 42 // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, 3 float Pos, 4 float Rot relativ zum Auto]
}
public readonly record struct Message(MsgType Type, byte[] Payload)
{
public BinaryReader Reader() => new(new MemoryStream(Payload));
}
public static class Wire
{
public static byte[] Build(Action<BinaryWriter> write)
{
using var ms = new MemoryStream();
using var w = new BinaryWriter(ms);
write(w);
w.Flush();
return ms.ToArray();
}
public static byte[] WithSender(int senderId, byte[] payload)
{
var result = new byte[4 + payload.Length];
BitConverter.GetBytes(senderId).CopyTo(result, 0);
Buffer.BlockCopy(payload, 0, result, 4, payload.Length);
return result;
}
public static byte[] Frame(MsgType type, byte[] payload)
{
payload ??= Array.Empty<byte>();
var frame = new byte[5 + payload.Length];
BitConverter.GetBytes(payload.Length + 1).CopyTo(frame, 0);
frame[4] = (byte)type;
Buffer.BlockCopy(payload, 0, frame, 5, payload.Length);
return frame;
}
}
/// <summary>TCP-Verbindung zu einem Spieler, gleiches Format wie in der Mod: [int Länge][byte Typ][Payload].</summary>
public sealed class Connection
{
// Spieler schicken nur kleine Pakete (Positionen, Aktionen); der Spielstand geht nur vom Server zum Spieler
const int MaxFrame = 1024 * 1024;
public bool IsClosed { get; private set; }
public string CloseReason { get; private set; }
public string RemoteAddress { get; }
/// <summary>Wann zuletzt etwas ankam (Clients schicken alle 2 s einen Ping).</summary>
public DateTime LastReceiveUtc { get; private set; } = DateTime.UtcNow;
readonly TcpClient client;
readonly NetworkStream stream;
bool closing; // Meldung gesendet, warten bis der Spieler sie gelesen hat (siehe CloseAfterSend)
DateTime closeDeadline;
string closingReason;
readonly byte[] readBuffer = new byte[64 * 1024];
readonly List<Message> received = new();
byte[] buffer = new byte[64 * 1024];
int count;
public Connection(TcpClient client)
{
this.client = client;
client.NoDelay = true;
client.SendBufferSize = 4 * 1024 * 1024;
stream = client.GetStream();
RemoteAddress = client.Client.RemoteEndPoint?.ToString() ?? "?";
}
/// <summary>Socket zum Warten auf Daten (Socket.Select im Server-Loop).</summary>
public Socket Socket => client.Client;
/// <summary>
/// Neue Nachrichten. Die Liste wird beim nächsten Aufruf wiederverwendet (keine Allokation pro Durchlauf).
/// </summary>
public List<Message> Poll()
{
var messages = received;
messages.Clear();
if (IsClosed) return messages;
if (closing)
{
DrainWhileClosing();
return messages;
}
try
{
if (client.Available == 0)
{
// Getrennt? "lesbar, aber keine Daten" = Gegenseite hat geschlossen
if (client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0) Close("Connection closed");
return messages;
}
while (client.Available > 0)
{
int read = stream.Read(readBuffer, 0, readBuffer.Length);
if (read <= 0) break;
Append(readBuffer, read);
LastReceiveUtc = DateTime.UtcNow;
}
Parse(messages);
}
catch (Exception e)
{
Close("Network error: " + e.Message);
}
return messages;
}
public void Send(MsgType type, byte[] payload = null) => SendFrame(Wire.Frame(type, payload));
/// <summary>Fertig gerahmte Nachricht senden (Broadcast: einmal rahmen, an alle schicken).</summary>
public void SendFrame(byte[] frame)
{
if (IsClosed || closing) return;
try
{
stream.Write(frame, 0, frame.Length);
}
catch (Exception e)
{
Close("Network error: " + e.Message);
}
}
/// <summary>
/// Sauber trennen, nachdem eine letzte Meldung gesendet wurde (Kick/Ban-Grund, Neu-Verbinden, Shutdown).
/// Sofortiges Close() mit noch ungelesenen Daten des Spielers (er schickt ständig Positionen) führt zu einem
/// TCP-Reset - dann geht die gerade gesendete Meldung verloren. Deshalb: nur die Sende-Richtung schließen,
/// eingehende Daten noch kurz lesen und verwerfen, dann erst schließen.
/// </summary>
public void CloseAfterSend(string reason)
{
if (IsClosed || closing) return;
closing = true;
closingReason = reason;
closeDeadline = DateTime.UtcNow.AddSeconds(3);
try { client.Client.Shutdown(SocketShutdown.Send); } catch { Close(reason); }
}
public bool IsClosing => closing && !IsClosed;
void DrainWhileClosing()
{
try
{
while (stream.DataAvailable)
if (stream.Read(readBuffer, 0, readBuffer.Length) <= 0) break;
bool remoteClosed = client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0;
if (remoteClosed || DateTime.UtcNow >= closeDeadline) Close(closingReason);
}
catch
{
Close(closingReason);
}
}
public void Close(string reason)
{
if (IsClosed) return;
IsClosed = true;
CloseReason = reason;
try { client.Close(); } catch { }
}
void Append(byte[] data, int length)
{
if (count + length > buffer.Length) Array.Resize(ref buffer, Math.Max(count + length, buffer.Length * 2));
Buffer.BlockCopy(data, 0, buffer, count, length);
count += length;
}
void Parse(List<Message> messages)
{
int offset = 0;
while (count - offset >= 5)
{
int length = BitConverter.ToInt32(buffer, offset);
if (length < 1 || length > MaxFrame) throw new IOException("Invalid packet");
if (count - offset - 4 < length) break;
var payload = new byte[length - 1];
Buffer.BlockCopy(buffer, offset + 5, payload, 0, payload.Length);
messages.Add(new Message((MsgType)buffer[offset + 4], payload));
offset += 4 + length;
}
if (offset > 0)
{
Buffer.BlockCopy(buffer, offset, buffer, 0, count - offset);
count -= offset;
}
}
}