using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using UnityEngine; namespace MSCO { public enum MsgType : byte { Hello = 1, // Client -> Host: Mod-Version + Name Welcome = 2, // Host -> Client: eigene ID + Spielerliste Reject = 3, // Host -> Client: Grund Player = 4, // Spielerposition SaveBegin = 5, // Host -> Client: Spielstand kommt SaveFile = 6, // Host -> Client: Dateiname + Inhalt StartGame = 7, // Host -> Client: jetzt laden ObjStates = 8, // Positionen der Objekte, die man gerade besitzt ObjOwn = 9, // "Ich übernehme dieses Objekt" ObjRelease = 10, // "Objekt liegt still, wieder frei" FsmEvent = 11, // Schraube angezogen/gelöst, Teil an-/abgebaut PlayerJoined = 12,// Host -> alle: neuer Spieler (ID + Name) PlayerLeft = 13, // Host -> alle: Spieler weg (ID) VehicleFx = 14, // Lichter und Motor-/Dauergeräusche eines Fahrzeugs Ping = 15, // Lebenszeichen (Steam hat keinen Verbindungsabbruch) 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 (FSM-Pfad + Zustand) 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; ins Menü und neu verbinden (z.B. Welt zurückgesetzt) SaveRequest = 34, // Server -> Client: int Anfrage; 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, Tüte ausgepackt ...): int Objekt-ID, string Objektname, 3 float Pos, 4 float Rot ObjDestroy = 38, // Item wurde verbraucht/zerstört (gegessen, getrunken ...): int Objekt-ID VehicleDamage = 39, // Unfallschaden: int Mesh-Schlüssel, int Eckpunkte gesamt, int n, je [int Index, 3 float] TrafficStates = 40, // Verkehr (nur 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 (2 int, 3 bool), Wert] HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel] VehicleControls = 43 // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand] } public struct Message { public MsgType Type; public byte[] Payload; public BinaryReader Reader() { return new BinaryReader(new MemoryStream(Payload)); } } public struct PlayerState { public Vector3 Position; public float RotationY; public Vector3 CamPos; // Kopf/Kamera in Weltkoordinaten public Quaternion CamRot; // Blickrichtung inkl. hoch/runter public int HeldId; // ID des gehaltenen Objekts, 0 = nichts public Vector3 HeldLocalPos; // relativ zur Kamera public Quaternion HeldLocalRot; // relativ zur Kamera public bool Seated; // fährt oder sitzt als Beifahrer public float FeetY; // Bodenhöhe unter dem Spieler (für Ducken/Lehnen der Figur) public byte[] Serialize() { PlayerState s = this; return NetSession.Build(w => { Write(w, s.Position); w.Write(s.RotationY); Write(w, s.CamPos); Write(w, s.CamRot); w.Write(s.Seated); w.Write(s.FeetY); w.Write(s.HeldId); if (s.HeldId == 0) return; Write(w, s.HeldLocalPos); Write(w, s.HeldLocalRot); }); } public static PlayerState Deserialize(BinaryReader r) { var s = new PlayerState { Position = ReadVector(r), RotationY = r.ReadSingle(), CamPos = ReadVector(r), CamRot = ReadQuaternion(r), Seated = r.ReadBoolean(), FeetY = r.ReadSingle(), HeldId = r.ReadInt32(), HeldLocalRot = Quaternion.identity }; if (s.HeldId != 0) { s.HeldLocalPos = ReadVector(r); s.HeldLocalRot = ReadQuaternion(r); } return s; } static void Write(BinaryWriter w, Vector3 v) { w.Write(v.x); w.Write(v.y); w.Write(v.z); } static void Write(BinaryWriter w, Quaternion q) { w.Write(q.x); w.Write(q.y); w.Write(q.z); w.Write(q.w); } static Vector3 ReadVector(BinaryReader r) { return new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); } static Quaternion ReadQuaternion(BinaryReader r) { return new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); } } public interface IConnection { bool IsClosed { get; } string CloseReason { get; } List Poll(); void Send(MsgType type, byte[] payload); void Close(string reason); } /// Setzt aus einem Byte-Strom Nachrichten zusammen: [int Länge][byte Typ][Payload]. public class FrameParser { const int MaxFrame = 32 * 1024 * 1024; byte[] buffer = new byte[64 * 1024]; int count; public void Append(byte[] data, int length) { EnsureCapacity(count + length); Buffer.BlockCopy(data, 0, buffer, count, length); count += length; } public 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 { Type = (MsgType)buffer[offset + 4], Payload = payload }); offset += 4 + length; } if (offset > 0) { Buffer.BlockCopy(buffer, offset, buffer, 0, count - offset); count -= offset; } } public static byte[] Frame(MsgType type, byte[] payload) { if (payload == null) payload = new byte[0]; 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; } void EnsureCapacity(int needed) { if (needed <= buffer.Length) return; var bigger = new byte[Math.Max(needed, buffer.Length * 2)]; Buffer.BlockCopy(buffer, 0, bigger, 0, count); buffer = bigger; } } /// TCP-Verbindung (LAN / IP). Lesen ist nicht-blockierend und wird über Poll() abgefragt. public class Connection : IConnection { public bool IsClosed { get; private set; } public string CloseReason { get; private set; } readonly TcpClient client; readonly NetworkStream stream; readonly FrameParser parser = new FrameParser(); readonly byte[] readBuffer = new byte[64 * 1024]; public Connection(TcpClient client) { this.client = client; client.NoDelay = true; client.SendBufferSize = 4 * 1024 * 1024; stream = client.GetStream(); } public List Poll() { var messages = new List(); if (IsClosed) return messages; try { // Verbindung von der Gegenseite geschlossen? if (client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0) { Close("Connection closed"); return messages; } while (stream.DataAvailable) { int read = stream.Read(readBuffer, 0, readBuffer.Length); if (read <= 0) break; parser.Append(readBuffer, read); } parser.Parse(messages); } catch (Exception e) { Close("Network error: " + e.Message); } return messages; } public void Send(MsgType type, byte[] payload) { if (IsClosed) return; try { byte[] frame = FrameParser.Frame(type, payload); stream.Write(frame, 0, frame.Length); // blockierend, damit große Dateien komplett rausgehen } catch (Exception e) { Close("Network error: " + e.Message); } } public void Close(string reason) { if (IsClosed) return; IsClosed = true; CloseReason = reason; try { client.Close(); } catch { } } } /// /// Verbindung über Steam P2P (Steam kümmert sich um NAT/Relay). Pakete sind auf 1 MB /// begrenzt, deshalb werden Nachrichten in Stücke geteilt und beim Empfänger über den /// FrameParser wieder zusammengesetzt (der Reliable-Kanal hält die Reihenfolge ein). /// public class SteamConnection : IConnection { const int ChunkSize = 512 * 1024; const float Timeout = 20f; // ohne Pakete (auch Pings) gilt die Verbindung als tot public readonly Steamworks.CSteamID Remote; public bool IsClosed { get; private set; } public string CloseReason { get; private set; } readonly FrameParser parser = new FrameParser(); float lastReceive; public SteamConnection(Steamworks.CSteamID remote) { Remote = remote; lastReceive = Time.realtimeSinceStartup; } public void OnPacket(byte[] data, int length) { parser.Append(data, length); lastReceive = Time.realtimeSinceStartup; } public List Poll() { var messages = new List(); if (IsClosed) return messages; try { parser.Parse(messages); } catch (Exception e) { Close("Network error: " + e.Message); } if (Time.realtimeSinceStartup - lastReceive > Timeout) Close("Timed out"); return messages; } public void Send(MsgType type, byte[] payload) { if (IsClosed) return; byte[] frame = FrameParser.Frame(type, payload); for (int offset = 0; offset < frame.Length; offset += ChunkSize) { int length = Math.Min(ChunkSize, frame.Length - offset); byte[] chunk = frame; if (offset != 0 || length != frame.Length) { chunk = new byte[length]; Buffer.BlockCopy(frame, offset, chunk, 0, length); } if (!Steamworks.SteamNetworking.SendP2PPacket(Remote, chunk, (uint)length, Steamworks.EP2PSend.k_EP2PSendReliable, 0)) { Close("Steam send failed"); return; } } } public void Close(string reason) { if (IsClosed) return; IsClosed = true; CloseReason = reason; try { Steamworks.SteamNetworking.CloseP2PSessionWithUser(Remote); } catch { } } } public enum NetEventKind { Connected, Disconnected, Message } public struct NetEvent { public NetEventKind Kind; public int PeerId; // beim Client immer NetSession.HostId public Message Message; public string Reason; } /// /// Stern-Topologie: Der Host hält eine Verbindung zu jedem Client, /// Clients sind nur mit dem Host verbunden. Der Host leitet Nachrichten weiter. /// Transport entweder TCP (LAN/IP) oder Steam P2P. /// public class NetSession { public const int HostId = 0; public bool IsHost { get; private set; } public bool IsSteam { get; private set; } public bool IsClosed { get; private set; } public string CloseReason { get; private set; } // Host TcpListener listener; readonly Dictionary peers = new Dictionary(); readonly Dictionary steamPeerIds = new Dictionary(); Func allowSteamPeer; int nextPeerId = 1; // Client TcpClient connectingClient; IAsyncResult connecting; IConnection server; bool pendingConnected; byte[] steamBuffer = new byte[64 * 1024]; public static NetSession StartHost(int port) { var s = new NetSession { IsHost = true }; s.listener = new TcpListener(IPAddress.Any, port); s.listener.Start(); return s; } public static NetSession StartClient(string ip, int port) { var s = new NetSession(); s.connectingClient = new TcpClient(); s.connecting = s.connectingClient.BeginConnect(ip, port, null, null); return s; } /// Steam-Host: nimmt Pakete von Spielern an, die allowPeer erlaubt (Lobby-Mitglieder). public static NetSession StartSteamHost(Func allowPeer) { return new NetSession { IsHost = true, IsSteam = true, allowSteamPeer = allowPeer }; } public static NetSession StartSteamClient(Steamworks.CSteamID host) { return new NetSession { IsSteam = true, server = new SteamConnection(host), pendingConnected = true }; } public List Poll() { var events = new List(); if (IsClosed) return events; try { if (IsSteam) PumpSteam(events); if (IsHost) PollHost(events); else PollClient(events); } catch (Exception e) { Close("Network error: " + e.Message); } return events; } void PumpSteam(List events) { uint size; while (Steamworks.SteamNetworking.IsP2PPacketAvailable(out size, 0)) { if (steamBuffer.Length < size) steamBuffer = new byte[size]; uint read; Steamworks.CSteamID from; if (!Steamworks.SteamNetworking.ReadP2PPacket(steamBuffer, size, out read, out from, 0)) break; if (IsHost) { int id; if (!steamPeerIds.TryGetValue(from.m_SteamID, out id)) { if (allowSteamPeer == null || !allowSteamPeer(from)) continue; // nicht in der Lobby id = nextPeerId++; steamPeerIds[from.m_SteamID] = id; peers[id] = new SteamConnection(from); events.Add(new NetEvent { Kind = NetEventKind.Connected, PeerId = id }); } IConnection c; if (peers.TryGetValue(id, out c)) ((SteamConnection)c).OnPacket(steamBuffer, (int)read); } else { var host = server as SteamConnection; if (host != null && from == host.Remote) host.OnPacket(steamBuffer, (int)read); } } } void PollHost(List events) { if (listener != null) { while (listener.Pending()) { int id = nextPeerId++; peers[id] = new Connection(listener.AcceptTcpClient()); events.Add(new NetEvent { Kind = NetEventKind.Connected, PeerId = id }); } } List closed = null; foreach (KeyValuePair p in peers) { foreach (Message m in p.Value.Poll()) events.Add(new NetEvent { Kind = NetEventKind.Message, PeerId = p.Key, Message = m }); if (p.Value.IsClosed) { if (closed == null) closed = new List(); closed.Add(p.Key); } } if (closed == null) return; foreach (int id in closed) { IConnection c = peers[id]; events.Add(new NetEvent { Kind = NetEventKind.Disconnected, PeerId = id, Reason = c.CloseReason }); var steamConn = c as SteamConnection; if (steamConn != null) steamPeerIds.Remove(steamConn.Remote.m_SteamID); peers.Remove(id); } } void PollClient(List events) { if (connecting != null) { if (!connecting.IsCompleted) return; IAsyncResult result = connecting; connecting = null; connectingClient.EndConnect(result); // wirft, wenn nicht erreichbar server = new Connection(connectingClient); pendingConnected = true; } if (server == null) return; if (pendingConnected) { pendingConnected = false; events.Add(new NetEvent { Kind = NetEventKind.Connected, PeerId = HostId }); } foreach (Message m in server.Poll()) events.Add(new NetEvent { Kind = NetEventKind.Message, PeerId = HostId, Message = m }); if (server.IsClosed) Close(server.CloseReason); } /// An einen bestimmten Spieler (Client: immer an den Host). public void SendTo(int peerId, MsgType type, byte[] payload = null) { if (!IsHost) { if (server != null) server.Send(type, payload); return; } IConnection c; if (peers.TryGetValue(peerId, out c)) c.Send(type, payload); } /// An alle (Client: an den Host). Optional einen Spieler auslassen. public void SendToAll(MsgType type, byte[] payload = null, int except = -1) { if (!IsHost) { if (server != null) server.Send(type, payload); return; } foreach (KeyValuePair p in peers) if (p.Key != except) p.Value.Send(type, payload); } /// Host: Verbindung zu einem Spieler trennen (wird beim nächsten Poll gemeldet). public void Kick(int peerId, string reason) { IConnection c; if (peers.TryGetValue(peerId, out c)) c.Close(reason); } /// Host: Steam-Spieler hat die Lobby verlassen. public void DropSteamPeer(ulong steamId, string reason) { int id; if (steamPeerIds.TryGetValue(steamId, out id)) Kick(id, reason); } public void Close(string reason = "Disconnected") { if (IsClosed) return; IsClosed = true; CloseReason = reason; foreach (IConnection c in peers.Values) c.Close(reason); peers.Clear(); steamPeerIds.Clear(); if (server != null) server.Close(reason); try { if (connectingClient != null && server == null) connectingClient.Close(); } catch { } try { if (listener != null) listener.Stop(); } catch { } } public static byte[] Build(Action write) { using (var ms = new MemoryStream()) using (var w = new BinaryWriter(ms)) { write(w); w.Flush(); return ms.ToArray(); } } /// Stellt der Nachricht die Absender-ID voran (für weitergeleitete Nachrichten). 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; } } }