From 6f8474d66b51356a38d97de98a6bca2643e3c020 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Sun, 27 Sep 2026 14:58:47 +0200 Subject: [PATCH] 2.8: relay bought/consumed items (stored for joiners), crash damage, job values, traffic (world authority only); tests Co-Authored-By: Claude Opus 5.5 --- GameServer.cs | 16 ++++++++++++++-- MSCO.Server.csproj | 2 +- Protocol.cs | 7 ++++++- Tests/Program.cs | 28 ++++++++++++++++++++++++++++ WorldState.cs | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/GameServer.cs b/GameServer.cs index c8a6e74..2763ad1 100644 --- a/GameServer.cs +++ b/GameServer.cs @@ -12,7 +12,7 @@ namespace MSCO.Server; /// public sealed class GameServer { - public const string ModVersion = "2.7"; // muss mit MSCO.ModVersion übereinstimmen + public const string ModVersion = "2.8"; // muss mit MSCO.ModVersion übereinstimmen public const int ServerId = 0; // Absender-ID des Servers; Spieler bekommen 1, 2, ... static readonly string[] SaveFiles = { "defaultES2File.txt", "items.txt", "meshsave.txt", "graveyard.txt", "notepad.txt" }; @@ -287,7 +287,8 @@ public sealed class GameServer static bool IsGameMessage(MsgType t) => t is MsgType.Player or MsgType.ObjStates or MsgType.ObjOwn or MsgType.ObjRelease or MsgType.FsmEvent or MsgType.VehicleFx or MsgType.WorldTime or MsgType.MoneyDelta or MsgType.DoorState or MsgType.Chat or MsgType.VarSync - or MsgType.SpawnItem; + or MsgType.SpawnItem or MsgType.ItemAppeared or MsgType.ObjDestroy or MsgType.VehicleDamage + or MsgType.TrafficStates or MsgType.WorldVars; /// Uhrzeit/Wetter gibt der Spieler mit der kleinsten ID vor (so rechnen es auch die Clients). int WorldAuthorityId => clients.Values.Where(c => c.State == ClientState.Joined).Select(c => c.Id).DefaultIfEmpty(-1).Min(); @@ -403,10 +404,21 @@ public sealed class GameServer world.AddSpawn(m.Payload); Log($"{c.Name} spawned {SpawnName(m.Payload)}"); break; + case MsgType.ItemAppeared: + if (m.Payload.Length > 256) return; + world.AddAppeared(m.Payload); + break; + case MsgType.ObjDestroy: + if (m.Payload.Length != 4) return; + world.AddDestroyed(BitConverter.ToInt32(m.Payload, 0)); + break; case MsgType.Chat: if (m.Payload.Length > 1024) return; // Spam/Unsinn nicht weiterleiten Log($"[chat] {c.Name}: {m.Reader().ReadString()}"); break; + case MsgType.TrafficStates: + if (c.Id != WorldAuthorityId) return; // nur einer simuliert den Verkehr + break; case MsgType.WorldTime: if (c.Id != WorldAuthorityId) return; // nur einer gibt Uhrzeit/Wetter vor lastWorldTime = m.Payload; diff --git a/MSCO.Server.csproj b/MSCO.Server.csproj index 2f58cb1..1f805c3 100644 --- a/MSCO.Server.csproj +++ b/MSCO.Server.csproj @@ -7,7 +7,7 @@ enable MSCO.Server MSCO.Server - 2.7.0 + 2.8.0 MBM MSCO Dedicated Server true diff --git a/Protocol.cs b/Protocol.cs index 3570e05..0de748a 100644 --- a/Protocol.cs +++ b/Protocol.cs @@ -40,7 +40,12 @@ public enum MsgType : byte Reconnect = 33, // Server -> Client: string Meldung; Client geht ins Menü und verbindet sich neu (z.B. Welt zurückgesetzt) SaveRequest = 34, // Server -> Client: int Anfrage; bitte im Spiel speichern und den Spielstand hochladen SaveUploadPart = 35, // Client -> Server: int Anfrage, string Datei, int Gesamtgröße, int Offset, int Länge, bytes - SaveUploadDone = 36 // Client -> Server: int Anfrage, int Anzahl Dateien + SaveUploadDone = 36, // Client -> Server: int Anfrage, int Anzahl Dateien + ItemAppeared = 37, // neues Item bei einem Spieler (Einkauf ...): int Objekt-ID, string Objektname, 3 float Pos, 4 float Rot + ObjDestroy = 38, // Item verbraucht/zerstört (gegessen, getrunken ...): int Objekt-ID + VehicleDamage = 39, // Unfallschaden (verformte Karosserie): int Mesh-Schlüssel, int Eckpunkte, int n, je [int Index, 3 float] + TrafficStates = 40, // Verkehr, nur von der Welt-Autorität: int n, je [int Schlüssel, 3 float Pos, 4 float Rot, 3 float Geschw.] + WorldVars = 41 // Jobs/Laden: int n, je [int Schlüssel, byte Typ, Wert] } public readonly record struct Message(MsgType Type, byte[] Payload) diff --git a/Tests/Program.cs b/Tests/Program.cs index 81077f3..b94f1cd 100644 --- a/Tests/Program.cs +++ b/Tests/Program.cs @@ -117,6 +117,28 @@ try var longChat = await bob.WaitFor(MsgType.Chat, 800); Check(longChat == null, "Huge chat messages are not relayed"); + Console.WriteLine("== Bought / consumed items"); + alice.Send(MsgType.ItemAppeared, Wire.Build(w => { w.Write(4242); w.Write("beer case(itemx)"); w.Write(1f); w.Write(2f); w.Write(3f); w.Write(0f); w.Write(0f); w.Write(0f); w.Write(1f); })); + Check(await bob.WaitFor(MsgType.ItemAppeared) != null, "Bought item is relayed to the other players"); + alice.Send(MsgType.ObjDestroy, Wire.Build(w => w.Write(777))); + Check(await bob.WaitFor(MsgType.ObjDestroy) != null, "Consumed item is relayed to the other players"); + alice.Send(MsgType.VehicleDamage, Wire.Build(w => { w.Write(1); w.Write(10); w.Write(1); w.Write(3); w.Write(0.1f); w.Write(0.2f); w.Write(0.3f); })); + Check(await bob.WaitFor(MsgType.VehicleDamage) != null, "Crash damage is relayed"); + alice.Send(MsgType.WorldVars, Wire.Build(w => { w.Write(1); w.Write(5); w.Write((byte)3); w.Write(true); })); + Check(await bob.WaitFor(MsgType.WorldVars) != null, "Job/shop values are relayed"); + byte[] trafficPayload = Wire.Build(w => { w.Write(0); }); + bob.Send(MsgType.TrafficStates, trafficPayload); + Check(await alice.WaitFor(MsgType.TrafficStates, 800) == null, "Traffic from a player who is not the world authority is ignored"); + alice.Send(MsgType.TrafficStates, trafficPayload); + Check(await bob.WaitFor(MsgType.TrafficStates) != null, "Traffic from the world authority is relayed"); + await Task.Delay(200); + var late = await TestClient.Join("Late", 0, null); + late.Send(MsgType.Ready, Array.Empty()); + var lateSnap = Snapshot.Parse(await late.WaitFor(MsgType.WorldSnapshot)); + Check(lateSnap.Ok && lateSnap.Appeared == 1 && lateSnap.Destroyed.Contains(777), + "Players joining later get bought and consumed items: " + lateSnap.Info); + late.Close(); + Console.WriteLine("== Last position (spawn)"); var pos1 = await TestClient.Join("Walker", 0, null); pos1.Send(MsgType.Player, PlayerPos(-15.6f, -0.3f, 7f)); // Startpunkt des Spielstands (Toilette) - vor Ready @@ -243,6 +265,8 @@ sealed class Snapshot public HashSet ObjectIds = new(); public float[] Spawn; public int Spawns = -1; + public int Appeared = -1; + public List Destroyed = new(); public int Vars = -1; public static Snapshot Parse(byte[] data) @@ -264,6 +288,10 @@ sealed class Snapshot for (int i = 0; i < s.Vars; i++) { r.ReadInt32(); byte t = r.ReadByte(); r.ReadBytes(t == 3 ? 1 : 4); } s.Spawns = r.ReadInt32(); for (int i = 0; i < s.Spawns; i++) r.ReadBytes(r.ReadInt32()); + s.Appeared = r.ReadInt32(); + for (int i = 0; i < s.Appeared; i++) r.ReadBytes(r.ReadInt32()); + int destroyed = r.ReadInt32(); + for (int i = 0; i < destroyed; i++) s.Destroyed.Add(r.ReadInt32()); s.Ok = r.BaseStream.Position == r.BaseStream.Length; s.Info = $"{objects} objects, {events} events, money {money}, {doors} doors, {s.Vars} vars"; } diff --git a/WorldState.cs b/WorldState.cs index 88f8c95..9771207 100644 --- a/WorldState.cs +++ b/WorldState.cs @@ -30,6 +30,12 @@ public sealed class WorldState /// 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; } @@ -61,12 +67,16 @@ public sealed class WorldState 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(); @@ -172,6 +182,21 @@ public sealed class WorldState 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; @@ -244,6 +269,17 @@ public sealed class WorldState 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); }); } }