Files
MSCO-Dedicated-Server/WorldState.cs
T

215 lines
7.6 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();
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;
}
public void Reset()
{
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 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;
PlayerPositions[steamId] = new[]
{
BitConverter.ToSingle(payload, 0), BitConverter.ToSingle(payload, 4), BitConverter.ToSingle(payload, 8)
};
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));
}
});
}
}