using System.Net.Sockets; namespace MSCO.Server; /// Muss mit MsgType in der Mod (NetSession.cs) übereinstimmen. 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, 3 bring, 4 unban, 5 reset world, 6 backup, 7 restore backup), int Spieler-ID, string Grund/Name 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 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(); 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; } } /// TCP-Verbindung zu einem Spieler, gleiches Format wie in der Mod: [int Länge][byte Typ][Payload]. 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; } /// Wann zuletzt etwas ankam (Clients schicken alle 2 s einen Ping). 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 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() ?? "?"; } /// Socket zum Warten auf Daten (Socket.Select im Server-Loop). public Socket Socket => client.Client; /// /// Neue Nachrichten. Die Liste wird beim nächsten Aufruf wiederverwendet (keine Allokation pro Durchlauf). /// public List 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)); /// Fertig gerahmte Nachricht senden (Broadcast: einmal rahmen, an alle schicken). public void SendFrame(byte[] frame) { if (IsClosed || closing) return; try { stream.Write(frame, 0, frame.Length); } catch (Exception e) { Close("Network error: " + e.Message); } } /// /// 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. /// 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 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; } } }