using System.Text.Json; namespace MSCO.Server; /// /// Was sich seit dem Ausgangs-Spielstand (Ordner "save") geändert hat: /// Positionen aller bewegten Objekte, alle Schraub-/Montage-Aktionen (in Reihenfolge) /// und wo jeder Spieler zuletzt war. Neue Spieler laden den Spielstand und bekommen /// danach diesen Zustand als Snapshot. /// public sealed class WorldState { public sealed class ObjectPose { public float[] P { get; set; } = new float[3]; public float[] R { get; set; } = new float[4]; } public Dictionary Objects { get; set; } = new(); /// FsmEvent-Payloads (Base64) in der Reihenfolge, in der sie passiert sind. public List Events { get; set; } = new(); /// SteamID64 -> letzte Position. public Dictionary PlayerPositions { get; set; } = new(); /// Summe aller Geldänderungen seit dem Ausgangs-Spielstand (gemeinsame Kasse). public float MoneyDelta { get; set; } /// Letzter Zustand jeder Tür/jedes Tors (FSM-Schlüssel -> Zustand). public Dictionary Doors { get; set; } = new(); /// Fahrzeugzustand (Tank, Öl, Schrauben ...): Schlüssel -> Base64 [byte typ, wert]. public Dictionary Vars { get; set; } = new(); public bool Dirty { get; private set; } static readonly JsonSerializerOptions Options = new() { WriteIndented = false }; public static WorldState Load(string path) { if (!File.Exists(path)) return new WorldState(); return JsonSerializer.Deserialize(File.ReadAllText(path), Options) ?? new WorldState(); } public void Save(string path) { string tmp = path + ".tmp"; File.WriteAllText(tmp, JsonSerializer.Serialize(this, Options)); File.Move(tmp, path, true); Dirty = false; } public void Reset() { Objects.Clear(); Events.Clear(); PlayerPositions.Clear(); MoneyDelta = 0f; Doors.Clear(); Vars.Clear(); Dirty = true; } // ---------------------------------------------------------------- Aus Spiel-Nachrichten lernen /// ObjStates: int Anzahl, dann je [int id, 3 float pos, 4 float rot, 3 float vel]. public void ApplyObjStates(byte[] payload) { using var r = new BinaryReader(new MemoryStream(payload)); int count = r.ReadInt32(); for (int i = 0; i < count; i++) ReadPose(r); Dirty = true; } /// ObjRelease: ein Eintrag wie bei ObjStates (Endposition). public void ApplyObjRelease(byte[] payload) { using var r = new BinaryReader(new MemoryStream(payload)); ReadPose(r); Dirty = true; } void ReadPose(BinaryReader r) { int id = r.ReadInt32(); var p = new float[3]; var q = new float[4]; for (int k = 0; k < 3; k++) p[k] = r.ReadSingle(); for (int k = 0; k < 4; k++) q[k] = r.ReadSingle(); r.ReadSingle(); r.ReadSingle(); r.ReadSingle(); // Geschwindigkeit wird nicht gespeichert // Unsinnige Werte (Fehler, manipulierter Client) nicht speichern - sonst landet z.B. das Auto // beim nächsten Beitreten im Boden oder außerhalb der Karte if (!IsValidPose(p, q)) return; if (!Objects.TryGetValue(id, out var pose)) { if (Objects.Count >= MaxObjects) return; Objects[id] = pose = new ObjectPose(); } pose.P = p; pose.R = q; } /// Position innerhalb der Karte, Drehung ein gültiges Quaternion. public static bool IsValidPose(float[] p, float[] q) { foreach (float f in p) if (!float.IsFinite(f)) return false; foreach (float f in q) if (!float.IsFinite(f)) return false; if (Math.Abs(p[0]) > 6000 || Math.Abs(p[2]) > 6000 || p[1] < -200 || p[1] > 3000) return false; float len = MathF.Sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); return len > 0.9f && len < 1.1f; } public void ApplyMoney(byte[] payload) { if (payload.Length < 4) return; MoneyDelta += BitConverter.ToSingle(payload, 0); Dirty = true; } public void ApplyDoor(byte[] payload) { using var r = new BinaryReader(new MemoryStream(payload)); string key = r.ReadString(); Doors[key] = r.ReadString(); Dirty = true; } public void ApplyVars(byte[] payload) { using var r = new BinaryReader(new MemoryStream(payload)); int count = r.ReadInt32(); if (count < 0 || count > 10000) return; for (int i = 0; i < count; i++) { int key = r.ReadInt32(); byte type = r.ReadByte(); byte[] value = r.ReadBytes(type == 3 ? 1 : 4); var all = new byte[value.Length + 1]; all[0] = type; value.CopyTo(all, 1); if (Vars.Count >= MaxVars && !Vars.ContainsKey(key)) continue; Vars[key] = Convert.ToBase64String(all); } Dirty = true; } // Obergrenzen, damit ein fehlerhafter oder manipulierter Client world.json nicht endlos aufbläht public const int MaxEvents = 50_000; public const int MaxVars = 50_000; public const int MaxObjects = 20_000; public void AddFsmEvent(byte[] payload) { if (Events.Count >= MaxEvents) return; Events.Add(Convert.ToBase64String(payload)); Dirty = true; } /// Player: beginnt mit der Position (3 float). public void ApplyPlayer(string steamId, byte[] payload) { if (string.IsNullOrEmpty(steamId) || payload.Length < 12) return; PlayerPositions[steamId] = new[] { BitConverter.ToSingle(payload, 0), BitConverter.ToSingle(payload, 4), BitConverter.ToSingle(payload, 8) }; Dirty = true; } // ---------------------------------------------------------------- Snapshot für neue Spieler /// /// WorldSnapshot: bool hatSpawn [3 float], int Objekte, je [int id, 3 float pos, 4 float rot], /// int Events, je [int Länge, bytes FsmEvent-Payload], float Geldänderung seit dem Spielstand. /// public byte[] BuildSnapshot(string steamId) { return Wire.Build(w => { bool hasSpawn = steamId != null && PlayerPositions.TryGetValue(steamId, out _); w.Write(hasSpawn); if (hasSpawn) foreach (float f in PlayerPositions[steamId]) w.Write(f); w.Write(Objects.Count); foreach (var kv in Objects) { w.Write(kv.Key); foreach (float f in kv.Value.P) w.Write(f); foreach (float f in kv.Value.R) w.Write(f); } w.Write(Events.Count); foreach (string e in Events) { byte[] bytes = Convert.FromBase64String(e); w.Write(bytes.Length); w.Write(bytes); } // Geld: Summe der Änderungen seit dem Spielstand w.Write(MoneyDelta); // Türen/Tore: letzter Zustand w.Write(Doors.Count); foreach (var kv in Doors) { w.Write(kv.Key); w.Write(kv.Value); } // Fahrzeugzustand: int n, je [int key, byte typ, wert] w.Write(Vars.Count); foreach (var kv in Vars) { w.Write(kv.Key); w.Write(Convert.FromBase64String(kv.Value)); } }); } }