Store job/shop values and vehicle switch states for late joiners (2.9.2)

WorldVars and VehicleControls were only relayed; players joining later only
got them with the next save pull. They are now stored in world.json (cleared
with a new save) and appended to the world snapshot (old clients ignore it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 14:11:05 +02:00
co-authored by Claude Opus 5.5
parent 48446c46db
commit 06a58e08d5
4 changed files with 77 additions and 1 deletions
+2
View File
@@ -448,6 +448,8 @@ public sealed class GameServer
case MsgType.MoneyDelta: world.ApplyMoney(m.Payload); break; case MsgType.MoneyDelta: world.ApplyMoney(m.Payload); break;
case MsgType.DoorState: world.ApplyDoor(m.Payload); break; case MsgType.DoorState: world.ApplyDoor(m.Payload); break;
case MsgType.VarSync: world.ApplyVars(m.Payload); break; case MsgType.VarSync: world.ApplyVars(m.Payload); break;
case MsgType.WorldVars: world.ApplyJobVars(m.Payload); break;
case MsgType.VehicleControls: world.ApplyControls(m.Payload); break;
case MsgType.SpawnItem: case MsgType.SpawnItem:
if (!c.IsAdmin) return; // nur Admins dürfen Items spawnen if (!c.IsAdmin) return; // nur Admins dürfen Items spawnen
world.AddSpawn(m.Payload); world.AddSpawn(m.Payload);
+1 -1
View File
@@ -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.9.1</Version> <Version>2.9.2</Version>
<Company>MBM</Company> <Company>MBM</Company>
<Product>MSCO Dedicated Server</Product> <Product>MSCO Dedicated Server</Product>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
+13
View File
@@ -68,6 +68,11 @@ try
Check(relay != null, "Object states are relayed to Bob"); Check(relay != null, "Object states are relayed to Bob");
alice.Send(MsgType.VarSync, Wire.Build(w => { w.Write(2); w.Write(42); w.Write((byte)1); w.Write(3.5f); w.Write(43); w.Write((byte)3); w.Write(true); })); alice.Send(MsgType.VarSync, Wire.Build(w => { w.Write(2); w.Write(42); w.Write((byte)1); w.Write(3.5f); w.Write(43); w.Write((byte)3); w.Write(true); }));
await bob.WaitFor(MsgType.VarSync); await bob.WaitFor(MsgType.VarSync);
// Jobs/Laden und Schalter (Licht an, Motor läuft) werden gespeichert, damit später Beitretende sie haben
alice.Send(MsgType.WorldVars, Wire.Build(w => { w.Write(2); w.Write(7); w.Write((byte)2); w.Write(5); w.Write(8); w.Write((byte)3); w.Write(true); }));
await bob.WaitFor(MsgType.WorldVars);
alice.Send(MsgType.VehicleControls, Wire.Build(w => { w.Write(1); w.Write(99); w.Write("Lights"); }));
await bob.WaitFor(MsgType.VehicleControls);
var carol = await TestClient.Join("Carol", 0, null); var carol = await TestClient.Join("Carol", 0, null);
carol.Send(MsgType.Ready, Array.Empty<byte>()); carol.Send(MsgType.Ready, Array.Empty<byte>());
var snap2 = Snapshot.Parse(await carol.WaitFor(MsgType.WorldSnapshot)); var snap2 = Snapshot.Parse(await carol.WaitFor(MsgType.WorldSnapshot));
@@ -76,6 +81,8 @@ try
Check(!snap2.ObjectIds.Contains(333), "Pose far below the map is rejected"); Check(!snap2.ObjectIds.Contains(333), "Pose far below the map is rejected");
Check(!snap2.ObjectIds.Contains(444), "Invalid quaternion is rejected"); Check(!snap2.ObjectIds.Contains(444), "Invalid quaternion is rejected");
Check(snap2.Vars == 2, "Vehicle values are stored and sent to new players (" + snap2.Vars + ")"); Check(snap2.Vars == 2, "Vehicle values are stored and sent to new players (" + snap2.Vars + ")");
Check(snap2.JobVars == 2 && snap2.Controls.TryGetValue(99, out string lightState) && lightState == "Lights",
"Job/shop values and switch states (lights, engine) are stored for players joining later");
Console.WriteLine("== Server full"); Console.WriteLine("== Server full");
var eve = await TestClient.Join("Eve", 0, null); var eve = await TestClient.Join("Eve", 0, null);
@@ -583,6 +590,8 @@ sealed class Snapshot
public List<int> Destroyed = new(); public List<int> Destroyed = new();
public Dictionary<string, float> Needs = new(); public Dictionary<string, float> Needs = new();
public int Vars = -1; public int Vars = -1;
public int JobVars = -1;
public Dictionary<int, string> Controls = new();
public static Snapshot Parse(byte[] data) public static Snapshot Parse(byte[] data)
{ {
@@ -612,6 +621,10 @@ sealed class Snapshot
int needs = r.ReadInt32(); int needs = r.ReadInt32();
for (int i = 0; i < needs; i++) s.Needs[r.ReadString()] = r.ReadSingle(); for (int i = 0; i < needs; i++) s.Needs[r.ReadString()] = r.ReadSingle();
} }
s.JobVars = r.ReadInt32();
for (int i = 0; i < s.JobVars; i++) { r.ReadInt32(); byte t = r.ReadByte(); r.ReadBytes(t == 3 ? 1 : 4); }
int controls = r.ReadInt32();
for (int i = 0; i < controls; i++) { int key = r.ReadInt32(); s.Controls[key] = r.ReadString(); }
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";
} }
+61
View File
@@ -29,6 +29,10 @@ public sealed class WorldState
public Dictionary<string, string> Doors { get; set; } = new(); public Dictionary<string, string> Doors { get; set; } = new();
/// <summary>Fahrzeugzustand (Tank, Öl, Schrauben ...): Schlüssel -> Base64 [byte typ, wert].</summary> /// <summary>Fahrzeugzustand (Tank, Öl, Schrauben ...): Schlüssel -> Base64 [byte typ, wert].</summary>
public Dictionary<int, string> Vars { get; set; } = new(); public Dictionary<int, string> Vars { get; set; } = new();
/// <summary>Jobs/Laden (WorldVars): Schlüssel -> Base64 [byte typ, wert] - nur int/bool.</summary>
public Dictionary<int, string> JobVars { get; set; } = new();
/// <summary>Schalter an Fahrzeugen (Licht, Zündung, Motor ...): FSM-Schlüssel -> aktiver Zustand.</summary>
public Dictionary<int, string> Controls { get; set; } = new();
/// <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;
@@ -76,6 +80,8 @@ public sealed class WorldState
MoneyDelta = 0f; MoneyDelta = 0f;
Doors.Clear(); Doors.Clear();
Vars.Clear(); Vars.Clear();
JobVars.Clear();
Controls.Clear();
Appeared.Clear(); Appeared.Clear();
Destroyed.Clear(); Destroyed.Clear();
Dirty = true; Dirty = true;
@@ -93,6 +99,8 @@ public sealed class WorldState
MoneyDelta = 0f; MoneyDelta = 0f;
Doors.Clear(); Doors.Clear();
Vars.Clear(); Vars.Clear();
JobVars.Clear();
Controls.Clear();
Dirty = true; Dirty = true;
} }
@@ -183,6 +191,47 @@ public sealed class WorldState
Dirty = true; Dirty = true;
} }
/// <summary>WorldVars (Jobs/Laden): int n, je [int key, byte typ (2 int / 3 bool), wert].</summary>
public void ApplyJobVars(byte[] payload)
{
ChangesSinceSave++;
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();
if (type != 2 && type != 3) return;
byte[] value = r.ReadBytes(type == 3 ? 1 : 4);
var all = new byte[value.Length + 1];
all[0] = type;
value.CopyTo(all, 1);
if (JobVars.Count >= MaxVars && !JobVars.ContainsKey(key)) continue;
JobVars[key] = Convert.ToBase64String(all);
}
Dirty = true;
}
/// <summary>VehicleControls: int n, je [int key, string zustand].</summary>
public void ApplyControls(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();
string state = r.ReadString();
if (state.Length > 100) continue;
if (Controls.Count >= MaxControls && !Controls.ContainsKey(key)) continue;
Controls[key] = state;
}
Dirty = true;
}
public const int MaxControls = 5000;
// Obergrenzen, damit ein fehlerhafter oder manipulierter Client world.json nicht endlos aufbläht // Obergrenzen, damit ein fehlerhafter oder manipulierter Client world.json nicht endlos aufbläht
public const int MaxEvents = 50_000; public const int MaxEvents = 50_000;
public const int MaxVars = 50_000; public const int MaxVars = 50_000;
@@ -330,6 +379,18 @@ public sealed class WorldState
w.Write(needs.Count); w.Write(needs.Count);
foreach (var kv in needs) { w.Write(kv.Key); w.Write(kv.Value); } foreach (var kv in needs) { w.Write(kv.Key); w.Write(kv.Value); }
} }
// Jobs/Laden: int n, je [int key, byte typ, wert]
w.Write(JobVars.Count);
foreach (var kv in JobVars)
{
w.Write(kv.Key);
w.Write(Convert.FromBase64String(kv.Value));
}
// Schalter (Licht, Zündung, Motor ...): int n, je [int key, string zustand]
w.Write(Controls.Count);
foreach (var kv in Controls) { w.Write(kv.Key); w.Write(kv.Value); }
}); });
} }
} }