286 lines
10 KiB
C#
286 lines
10 KiB
C#
using System.Text.Json;
|
|
|
|
namespace MSCO.Server;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<int, ObjectPose> Objects { get; set; } = new();
|
|
/// <summary>FsmEvent-Payloads (Base64) in der Reihenfolge, in der sie passiert sind.</summary>
|
|
public List<string> Events { get; set; } = new();
|
|
/// <summary>SteamID64 -> letzte Position.</summary>
|
|
public Dictionary<string, float[]> PlayerPositions { get; set; } = new();
|
|
/// <summary>Summe aller Geldänderungen seit dem Ausgangs-Spielstand (gemeinsame Kasse).</summary>
|
|
public float MoneyDelta { get; set; }
|
|
/// <summary>Letzter Zustand jeder Tür/jedes Tors (FSM-Schlüssel -> Zustand).</summary>
|
|
public Dictionary<string, string> Doors { get; set; } = new();
|
|
/// <summary>Fahrzeugzustand (Tank, Öl, Schrauben ...): Schlüssel -> Base64 [byte typ, wert].</summary>
|
|
public Dictionary<int, string> Vars { get; set; } = new();
|
|
/// <summary>Von Admins gespawnte Items (SpawnItem-Nachrichten, Base64), damit später Beitretende sie auch haben.</summary>
|
|
public List<string> Spawns { get; set; } = new();
|
|
public const int MaxSpawns = 2000;
|
|
/// <summary>Seit dem letzten Spielstand gekaufte/ausgepackte Items (ItemAppeared, Base64) - stecken danach im Spielstand.</summary>
|
|
public List<string> Appeared { get; set; } = new();
|
|
/// <summary>Seit dem letzten Spielstand verbrauchte Items (Objekt-IDs).</summary>
|
|
public List<int> Destroyed { get; set; } = new();
|
|
public const int MaxAppeared = 2000;
|
|
public const int MaxDestroyed = 5000;
|
|
|
|
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<WorldState>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void ResetForNewSave()
|
|
{
|
|
Objects.Clear();
|
|
Events.Clear();
|
|
MoneyDelta = 0f;
|
|
Doors.Clear();
|
|
Vars.Clear();
|
|
Appeared.Clear();
|
|
Destroyed.Clear();
|
|
Dirty = true;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
Spawns.Clear();
|
|
Appeared.Clear();
|
|
Destroyed.Clear();
|
|
Objects.Clear();
|
|
Events.Clear();
|
|
PlayerPositions.Clear();
|
|
MoneyDelta = 0f;
|
|
Doors.Clear();
|
|
Vars.Clear();
|
|
Dirty = true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Aus Spiel-Nachrichten lernen
|
|
|
|
/// <summary>ObjStates: int Anzahl, dann je [int id, 3 float pos, 4 float rot, 3 float vel].</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>ObjRelease: ein Eintrag wie bei ObjStates (Endposition).</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Position innerhalb der Karte, Drehung ein gültiges Quaternion.</summary>
|
|
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 AddSpawn(byte[] payload)
|
|
{
|
|
if (Spawns.Count >= MaxSpawns || payload.Length > 512) return;
|
|
Spawns.Add(Convert.ToBase64String(payload));
|
|
Dirty = true;
|
|
}
|
|
|
|
public void AddAppeared(byte[] payload)
|
|
{
|
|
if (Appeared.Count >= MaxAppeared) return;
|
|
Appeared.Add(Convert.ToBase64String(payload));
|
|
Dirty = true;
|
|
}
|
|
|
|
public void AddDestroyed(int id)
|
|
{
|
|
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;
|
|
Events.Add(Convert.ToBase64String(payload));
|
|
Dirty = true;
|
|
}
|
|
|
|
/// <summary>Player: beginnt mit der Position (3 float).</summary>
|
|
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;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Snapshot für neue Spieler
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
});
|
|
}
|
|
}
|