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 -> eigene Bedürfnisse (Hunger, Durst, Stress ...), Name -> Wert.
public Dictionary> PlayerNeeds { 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();
/// Jobs/Laden (WorldVars): Schlüssel -> Base64 [byte typ, wert] - nur int/bool.
public Dictionary JobVars { get; set; } = new();
/// Schalter an Fahrzeugen (Licht, Zündung, Motor ...): FSM-Schlüssel -> aktiver Zustand.
public Dictionary Controls { get; set; } = new();
/// Von Admins gespawnte Items (SpawnItem-Nachrichten, Base64), damit später Beitretende sie auch haben.
public List Spawns { get; set; } = new();
public const int MaxSpawns = 2000;
/// Seit dem letzten Spielstand gekaufte/ausgepackte Items (ItemAppeared, Base64) - stecken danach im Spielstand.
public List Appeared { get; set; } = new();
/// Seit dem letzten Spielstand verbrauchte Items (Objekt-IDs).
public List Destroyed { get; set; } = new();
public const int MaxAppeared = 2000;
public const int MaxDestroyed = 5000;
public bool Dirty { get; private set; }
///
/// Änderungen, die im Spielstand landen (Aktionen, Fahrzeugwerte, Geld, Türen, Käufe ...) seit dem letzten hochgeladenen
/// Spielstand. 0 = der regelmäßige Spielstand-Abruf kann ausfallen (Speichern lässt das Spiel kurz hängen).
///
[System.Text.Json.Serialization.JsonIgnore]
public int ChangesSinceSave { get; set; } = 1;
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;
}
///
/// Ein neuer Spielstand vom Spiel selbst ist da: alles, was bisher nachgespielt werden musste (Aktionen, Positionen,
/// Fahrzeugwerte, Türen, Geld), steckt jetzt darin. Behalten werden nur die letzten Spielerpositionen und die von
/// Admins gespawnten Items.
///
public void ResetForNewSave()
{
ChangesSinceSave = 0;
Objects.Clear();
Events.Clear();
MoneyDelta = 0f;
Doors.Clear();
Vars.Clear();
JobVars.Clear();
Controls.Clear();
Appeared.Clear();
Destroyed.Clear();
Dirty = true;
}
public void Reset()
{
PlayerNeeds.Clear();
Spawns.Clear();
Appeared.Clear();
Destroyed.Clear();
Objects.Clear();
Events.Clear();
PlayerPositions.Clear();
MoneyDelta = 0f;
Doors.Clear();
Vars.Clear();
JobVars.Clear();
Controls.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)
{
ChangesSinceSave++;
if (payload.Length < 4) return;
float delta = BitConverter.ToSingle(payload, 0);
if (!float.IsFinite(delta)) return;
MoneyDelta += delta;
Dirty = true;
}
public void ApplyDoor(byte[] payload)
{
ChangesSinceSave++;
using var r = new BinaryReader(new MemoryStream(payload));
string key = r.ReadString();
Doors[key] = r.ReadString();
Dirty = true;
}
public void ApplyVars(byte[] payload)
{
ChangesSinceSave++;
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;
}
/// WorldVars (Jobs/Laden): int n, je [int key, byte typ (2 int / 3 bool), wert].
public void ApplyJobVars(byte[] payload)
{
ChangesSinceSave++;
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();
if (type != 2 && type != 3) return;
byte[] value = r.ReadBytes(type == 3 ? 1 : 4);
var all = new byte[value.Length + 1];
all[0] = type;
value.CopyTo(all, 1);
if (JobVars.Count >= MaxVars && !JobVars.ContainsKey(key)) continue;
JobVars[key] = Convert.ToBase64String(all);
}
Dirty = true;
}
/// VehicleControls: int n, je [int key, string zustand].
public void ApplyControls(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();
string state = r.ReadString();
if (state.Length > 100) continue;
if (Controls.Count >= MaxControls && !Controls.ContainsKey(key)) continue;
Controls[key] = state;
}
Dirty = true;
}
public const int MaxControls = 5000;
// 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 AddSpawn(byte[] payload)
{
if (Spawns.Count >= MaxSpawns || payload.Length > 512) return;
Spawns.Add(Convert.ToBase64String(payload));
Dirty = true;
}
public void AddAppeared(byte[] payload)
{
ChangesSinceSave++;
if (Appeared.Count >= MaxAppeared) return;
Appeared.Add(Convert.ToBase64String(payload));
Dirty = true;
}
public void AddDestroyed(int id)
{
ChangesSinceSave++;
if (Destroyed.Count >= MaxDestroyed || Destroyed.Contains(id)) return;
Destroyed.Add(id);
Objects.Remove(id);
Dirty = true;
}
public void AddFsmEvent(byte[] payload)
{
if (Events.Count >= MaxEvents) return;
ChangesSinceSave++;
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;
var pos = new[] { BitConverter.ToSingle(payload, 0), BitConverter.ToSingle(payload, 4), BitConverter.ToSingle(payload, 8) };
// Unsinnige Positionen (z.B. mit einem explodierten Auto weggeschleudert) nicht speichern
if (!IsValidPose(pos, new float[] { 0, 0, 0, 1 })) return;
PlayerPositions[steamId] = pos;
Dirty = true;
}
/// PlayerNeeds: int n, je [string Name, float Wert]. Werte werden begrenzt, Unsinn verworfen.
public void ApplyNeeds(string steamId, byte[] payload)
{
if (string.IsNullOrEmpty(steamId) || payload.Length > 1024) return;
try
{
using var r = new BinaryReader(new MemoryStream(payload));
int count = r.ReadInt32();
if (count < 0 || count > 16) return;
var needs = new Dictionary();
for (int i = 0; i < count; i++)
{
string name = r.ReadString();
float value = r.ReadSingle();
if (name.Length == 0 || name.Length > 40 || !float.IsFinite(value)) continue;
needs[name] = Math.Clamp(value, -1000f, 1000f);
}
if (needs.Count == 0) return;
PlayerNeeds[steamId] = needs;
Dirty = true;
}
catch (EndOfStreamException) { }
}
// ---------------------------------------------------------------- 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));
}
// Von Admins gespawnte Items: int n, je [int Länge, SpawnItem-Payload]
w.Write(Spawns.Count);
foreach (string sp in Spawns)
{
byte[] bytes = Convert.FromBase64String(sp);
w.Write(bytes.Length);
w.Write(bytes);
}
// Gekaufte Items: int n, je [int Länge, ItemAppeared-Payload]; verbrauchte: int n, je int ID
w.Write(Appeared.Count);
foreach (string a in Appeared)
{
byte[] bytes = Convert.FromBase64String(a);
w.Write(bytes.Length);
w.Write(bytes);
}
w.Write(Destroyed.Count);
foreach (int id in Destroyed) w.Write(id);
// Eigene Bedürfnisse des Beitretenden: bool vorhanden [int n, je string Name, float Wert]
bool hasNeeds = steamId != null && PlayerNeeds.TryGetValue(steamId, out var own) && own.Count > 0;
w.Write(hasNeeds);
if (hasNeeds)
{
var needs = PlayerNeeds[steamId];
w.Write(needs.Count);
foreach (var kv in needs) { w.Write(kv.Key); w.Write(kv.Value); }
}
// Jobs/Laden: int n, je [int key, byte typ, wert]
w.Write(JobVars.Count);
foreach (var kv in JobVars)
{
w.Write(kv.Key);
w.Write(Convert.FromBase64String(kv.Value));
}
// Schalter (Licht, Zündung, Motor ...): int n, je [int key, string zustand]
w.Write(Controls.Count);
foreach (var kv in Controls) { w.Write(kv.Key); w.Write(kv.Value); }
});
}
}