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();
if (!Objects.TryGetValue(id, out var pose))
{
pose = new ObjectPose(); // bei voller Liste nur mitlesen, nicht speichern
if (Objects.Count < MaxObjects) Objects[id] = pose;
}
for (int k = 0; k < 3; k++) pose.P[k] = r.ReadSingle();
for (int k = 0; k < 4; k++) pose.R[k] = r.ReadSingle();
r.ReadSingle(); r.ReadSingle(); r.ReadSingle(); // Geschwindigkeit wird nicht gespeichert
}
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));
}
});
}
}