MSCO dedicated server 2.6: code, Docker, systemd, README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+185
@@ -0,0 +1,185 @@
|
||||
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();
|
||||
if (!Objects.TryGetValue(id, out var pose)) Objects[id] = pose = new ObjectPose();
|
||||
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);
|
||||
Vars[key] = Convert.ToBase64String(all);
|
||||
}
|
||||
Dirty = true;
|
||||
}
|
||||
|
||||
public void AddFsmEvent(byte[] payload)
|
||||
{
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user