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 <noreply@anthropic.com>
This commit is contained in:
+14
-2
@@ -12,7 +12,7 @@ namespace MSCO.Server;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GameServer
|
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, ...
|
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" };
|
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) =>
|
static bool IsGameMessage(MsgType t) =>
|
||||||
t is MsgType.Player or MsgType.ObjStates or MsgType.ObjOwn or MsgType.ObjRelease or MsgType.FsmEvent
|
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.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;
|
||||||
|
|
||||||
/// <summary>Uhrzeit/Wetter gibt der Spieler mit der kleinsten ID vor (so rechnen es auch die Clients).</summary>
|
/// <summary>Uhrzeit/Wetter gibt der Spieler mit der kleinsten ID vor (so rechnen es auch die Clients).</summary>
|
||||||
int WorldAuthorityId => clients.Values.Where(c => c.State == ClientState.Joined).Select(c => c.Id).DefaultIfEmpty(-1).Min();
|
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);
|
world.AddSpawn(m.Payload);
|
||||||
Log($"{c.Name} spawned {SpawnName(m.Payload)}");
|
Log($"{c.Name} spawned {SpawnName(m.Payload)}");
|
||||||
break;
|
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:
|
case MsgType.Chat:
|
||||||
if (m.Payload.Length > 1024) return; // Spam/Unsinn nicht weiterleiten
|
if (m.Payload.Length > 1024) return; // Spam/Unsinn nicht weiterleiten
|
||||||
Log($"[chat] {c.Name}: {m.Reader().ReadString()}");
|
Log($"[chat] {c.Name}: {m.Reader().ReadString()}");
|
||||||
break;
|
break;
|
||||||
|
case MsgType.TrafficStates:
|
||||||
|
if (c.Id != WorldAuthorityId) return; // nur einer simuliert den Verkehr
|
||||||
|
break;
|
||||||
case MsgType.WorldTime:
|
case MsgType.WorldTime:
|
||||||
if (c.Id != WorldAuthorityId) return; // nur einer gibt Uhrzeit/Wetter vor
|
if (c.Id != WorldAuthorityId) return; // nur einer gibt Uhrzeit/Wetter vor
|
||||||
lastWorldTime = m.Payload;
|
lastWorldTime = m.Payload;
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>MSCO.Server</RootNamespace>
|
<RootNamespace>MSCO.Server</RootNamespace>
|
||||||
<AssemblyName>MSCO.Server</AssemblyName>
|
<AssemblyName>MSCO.Server</AssemblyName>
|
||||||
<Version>2.7.0</Version>
|
<Version>2.8.0</Version>
|
||||||
<Company>MBM</Company>
|
<Company>MBM</Company>
|
||||||
<Product>MSCO Dedicated Server</Product>
|
<Product>MSCO Dedicated Server</Product>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
|||||||
+6
-1
@@ -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)
|
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
|
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
|
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)
|
public readonly record struct Message(MsgType Type, byte[] Payload)
|
||||||
|
|||||||
@@ -117,6 +117,28 @@ try
|
|||||||
var longChat = await bob.WaitFor(MsgType.Chat, 800);
|
var longChat = await bob.WaitFor(MsgType.Chat, 800);
|
||||||
Check(longChat == null, "Huge chat messages are not relayed");
|
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<byte>());
|
||||||
|
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)");
|
Console.WriteLine("== Last position (spawn)");
|
||||||
var pos1 = await TestClient.Join("Walker", 0, null);
|
var pos1 = await TestClient.Join("Walker", 0, null);
|
||||||
pos1.Send(MsgType.Player, PlayerPos(-15.6f, -0.3f, 7f)); // Startpunkt des Spielstands (Toilette) - vor Ready
|
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<int> ObjectIds = new();
|
public HashSet<int> ObjectIds = new();
|
||||||
public float[] Spawn;
|
public float[] Spawn;
|
||||||
public int Spawns = -1;
|
public int Spawns = -1;
|
||||||
|
public int Appeared = -1;
|
||||||
|
public List<int> Destroyed = new();
|
||||||
public int Vars = -1;
|
public int Vars = -1;
|
||||||
|
|
||||||
public static Snapshot Parse(byte[] data)
|
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); }
|
for (int i = 0; i < s.Vars; i++) { r.ReadInt32(); byte t = r.ReadByte(); r.ReadBytes(t == 3 ? 1 : 4); }
|
||||||
s.Spawns = r.ReadInt32();
|
s.Spawns = r.ReadInt32();
|
||||||
for (int i = 0; i < s.Spawns; i++) r.ReadBytes(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.Ok = r.BaseStream.Position == r.BaseStream.Length;
|
||||||
s.Info = $"{objects} objects, {events} events, money {money}, {doors} doors, {s.Vars} vars";
|
s.Info = $"{objects} objects, {events} events, money {money}, {doors} doors, {s.Vars} vars";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ public sealed class WorldState
|
|||||||
/// <summary>Von Admins gespawnte Items (SpawnItem-Nachrichten, Base64), damit später Beitretende sie auch haben.</summary>
|
/// <summary>Von Admins gespawnte Items (SpawnItem-Nachrichten, Base64), damit später Beitretende sie auch haben.</summary>
|
||||||
public List<string> Spawns { get; set; } = new();
|
public List<string> Spawns { get; set; } = new();
|
||||||
public const int MaxSpawns = 2000;
|
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; }
|
public bool Dirty { get; private set; }
|
||||||
|
|
||||||
@@ -61,12 +67,16 @@ public sealed class WorldState
|
|||||||
MoneyDelta = 0f;
|
MoneyDelta = 0f;
|
||||||
Doors.Clear();
|
Doors.Clear();
|
||||||
Vars.Clear();
|
Vars.Clear();
|
||||||
|
Appeared.Clear();
|
||||||
|
Destroyed.Clear();
|
||||||
Dirty = true;
|
Dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
Spawns.Clear();
|
Spawns.Clear();
|
||||||
|
Appeared.Clear();
|
||||||
|
Destroyed.Clear();
|
||||||
Objects.Clear();
|
Objects.Clear();
|
||||||
Events.Clear();
|
Events.Clear();
|
||||||
PlayerPositions.Clear();
|
PlayerPositions.Clear();
|
||||||
@@ -172,6 +182,21 @@ public sealed class WorldState
|
|||||||
Dirty = true;
|
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)
|
public void AddFsmEvent(byte[] payload)
|
||||||
{
|
{
|
||||||
if (Events.Count >= MaxEvents) return;
|
if (Events.Count >= MaxEvents) return;
|
||||||
@@ -244,6 +269,17 @@ public sealed class WorldState
|
|||||||
w.Write(bytes.Length);
|
w.Write(bytes.Length);
|
||||||
w.Write(bytes);
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user