reset-world restores the original save (reset point in data/save_original), set-reset-point; per-player needs stored and returned; tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+77
-5
@@ -93,6 +93,7 @@ public sealed class GameServer
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
this.configPath = configPath;
|
this.configPath = configPath;
|
||||||
savePath = Path.Combine(dataDir, "save");
|
savePath = Path.Combine(dataDir, "save");
|
||||||
|
originalPath = Path.Combine(dataDir, "save_original");
|
||||||
worldPath = Path.Combine(dataDir, "world.json");
|
worldPath = Path.Combine(dataDir, "world.json");
|
||||||
master = new MasterClient(MasterServer.Url);
|
master = new MasterClient(MasterServer.Url);
|
||||||
steam = new SteamVerifier(config.SteamWebApiKey);
|
steam = new SteamVerifier(config.SteamWebApiKey);
|
||||||
@@ -137,6 +138,7 @@ public sealed class GameServer
|
|||||||
|
|
||||||
public void Run()
|
public void Run()
|
||||||
{
|
{
|
||||||
|
EnsureResetPoint();
|
||||||
listener = new TcpListener(IPAddress.Any, config.Port);
|
listener = new TcpListener(IPAddress.Any, config.Port);
|
||||||
listener.Start();
|
listener.Start();
|
||||||
Log($"Listening on port {config.Port} - {config.Name} (max {config.MaxPlayers} players)");
|
Log($"Listening on port {config.Port} - {config.Name} (max {config.MaxPlayers} players)");
|
||||||
@@ -347,6 +349,11 @@ public sealed class GameServer
|
|||||||
FinishSaveUpload(c, m.Reader());
|
FinishSaveUpload(c, m.Reader());
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case MsgType.PlayerNeeds:
|
||||||
|
// Nur speichern, nicht weiterleiten - die Bedürfnisse gehören jedem selbst
|
||||||
|
if (c.State == ClientState.Joined && DateTime.UtcNow >= c.TrackPositionFrom) world.ApplyNeeds(c.SteamId, m.Payload);
|
||||||
|
return;
|
||||||
|
|
||||||
case MsgType.AdminListRequest:
|
case MsgType.AdminListRequest:
|
||||||
if (c.State != ClientState.Joined) return;
|
if (c.State != ClientState.Joined) return;
|
||||||
if (!c.IsAdmin) { c.Conn.Send(MsgType.AdminResult, Wire.Build(w => w.Write("You are not an admin on this server"))); return; }
|
if (!c.IsAdmin) { c.Conn.Send(MsgType.AdminResult, Wire.Build(w => w.Write("You are not an admin on this server"))); return; }
|
||||||
@@ -384,6 +391,11 @@ public sealed class GameServer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (action == 6) result = BackupNow(c.Name);
|
else if (action == 6) result = BackupNow(c.Name);
|
||||||
|
else if (action == 8)
|
||||||
|
{
|
||||||
|
result = SetResetPoint() ? "The current save game is now the reset point" : "No save game to use as reset point";
|
||||||
|
if (result.StartsWith("The")) Log($"{c.Name} set the current save game as the reset point");
|
||||||
|
}
|
||||||
else if (!clients.TryGetValue(targetId, out var target) || target.State != ClientState.Joined) result = "Player not found";
|
else if (!clients.TryGetValue(targetId, out var target) || target.State != ClientState.Joined) result = "Player not found";
|
||||||
else if (action == 3)
|
else if (action == 3)
|
||||||
{
|
{
|
||||||
@@ -670,14 +682,66 @@ public sealed class GameServer
|
|||||||
if (count > 0) Log($"{count} player(s) will reconnect");
|
if (count > 0) Log($"{count} player(s) will reconnect");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Weltzustand zurücksetzen (Konsole oder Admin im Spiel): alle verbinden neu und laden den reinen Spielstand.</summary>
|
// ---------------------------------------------------------------- Reset-Punkt
|
||||||
|
// Der Spielstand, so wie er dem Server gegeben wurde (Start, --import-save, reload-save). Das automatische
|
||||||
|
// Hochladen alle paar Minuten überschreibt nur data/save - "reset-world" holt diesen Stand zurück.
|
||||||
|
string originalPath;
|
||||||
|
|
||||||
|
/// <summary>Beim Start: gibt es noch keinen Reset-Punkt, ist es der aktuelle Spielstand.</summary>
|
||||||
|
void EnsureResetPoint()
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(originalPath, "defaultES2File.txt"))) return;
|
||||||
|
if (SetResetPoint()) Log("Remembered the current save game as the reset point (reset-world goes back to it)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Aktuellen Spielstand als Reset-Punkt merken.</summary>
|
||||||
|
bool SetResetPoint()
|
||||||
|
{
|
||||||
|
if (!File.Exists(Path.Combine(savePath, "defaultES2File.txt"))) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(originalPath)) Directory.Delete(originalPath, true);
|
||||||
|
Directory.CreateDirectory(originalPath);
|
||||||
|
foreach (string file in Directory.GetFiles(savePath))
|
||||||
|
if (!file.EndsWith(".tmp")) File.Copy(file, Path.Combine(originalPath, Path.GetFileName(file)), true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e) { Log("Could not remember the reset point: " + e.Message); return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Welt komplett zurücksetzen (Konsole oder Admin im Spiel): Spielstand zurück auf den Reset-Punkt, alle
|
||||||
|
/// gespeicherten Änderungen (Positionen, gekaufte Items, Türen, Fahrzeugwerte ...) weg, alle verbinden neu.
|
||||||
|
/// Der Stand davor landet als Backup in save_backups.
|
||||||
|
/// </summary>
|
||||||
string ResetWorld(string by)
|
string ResetWorld(string by)
|
||||||
{
|
{
|
||||||
|
bool restored = false;
|
||||||
|
if (File.Exists(Path.Combine(originalPath, "defaultES2File.txt")))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BackupSave();
|
||||||
|
Directory.CreateDirectory(savePath);
|
||||||
|
foreach (string name in SaveFiles)
|
||||||
|
{
|
||||||
|
string from = Path.Combine(originalPath, name), to = Path.Combine(savePath, name);
|
||||||
|
if (File.Exists(from)) File.Copy(from, to, true);
|
||||||
|
else if (File.Exists(to)) File.Delete(to); // gab es im Original nicht
|
||||||
|
}
|
||||||
|
restored = LoadSave();
|
||||||
|
}
|
||||||
|
catch (Exception e) { Log("Could not restore the reset point: " + e.Message); }
|
||||||
|
}
|
||||||
|
// Laufendes Hochladen eines Spielstands (alter Stand) verwerfen
|
||||||
|
saveUpload = null;
|
||||||
|
waitingForSave.Clear();
|
||||||
world.Reset();
|
world.Reset();
|
||||||
SaveWorld();
|
SaveWorld();
|
||||||
Log($"World state reset by {by} - new players get the plain save game again.");
|
Log(restored ? $"World reset by {by} - back to the original save game (previous state backed up)."
|
||||||
|
: $"World state reset by {by} - new players get the plain save game again.");
|
||||||
ReconnectAll(by == "Console" ? "The world is being reset - reconnecting..." : $"{by} reset the world - reconnecting...");
|
ReconnectAll(by == "Console" ? "The world is being reset - reconnecting..." : $"{by} reset the world - reconnecting...");
|
||||||
return "World reset";
|
return restored ? "World reset to the original save game" : "World reset";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Aktuellen Spielstand jetzt als Backup sichern.</summary>
|
/// <summary>Aktuellen Spielstand jetzt als Backup sichern.</summary>
|
||||||
@@ -780,7 +844,7 @@ public sealed class GameServer
|
|||||||
switch (args[0].ToLowerInvariant())
|
switch (args[0].ToLowerInvariant())
|
||||||
{
|
{
|
||||||
case "help":
|
case "help":
|
||||||
Log("status | players | kick <id> [reason] | ban <id> [reason] | bans | unban <SteamID|name:x> | backups | backup | restore-save <n> | say <text> | op <id> | deop <id> | admins | save | reset-world | reload-save | stop");
|
Log("status | players | kick <id> [reason] | ban <id> [reason] | bans | unban <SteamID|name:x> | backups | backup | restore-save <n> | set-reset-point | say <text> | op <id> | deop <id> | admins | save | reset-world | reload-save | stop");
|
||||||
break;
|
break;
|
||||||
case "say":
|
case "say":
|
||||||
if (args.Length < 2) { Log("say <text>"); break; }
|
if (args.Length < 2) { Log("say <text>"); break; }
|
||||||
@@ -875,7 +939,15 @@ public sealed class GameServer
|
|||||||
Log(RestoreSave(args.Length > 1 ? args[1].Trim() : "", "Console"));
|
Log(RestoreSave(args.Length > 1 ? args[1].Trim() : "", "Console"));
|
||||||
break;
|
break;
|
||||||
case "reload-save":
|
case "reload-save":
|
||||||
if (LoadSave()) ReconnectAll("The server loaded a new save game - reconnecting...");
|
if (LoadSave())
|
||||||
|
{
|
||||||
|
// Neuer Spielstand von Hand hineinkopiert: ab jetzt ist das der Reset-Punkt
|
||||||
|
SetResetPoint();
|
||||||
|
ReconnectAll("The server loaded a new save game - reconnecting...");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "set-reset-point":
|
||||||
|
Log(SetResetPoint() ? "The current save game is now the reset point" : "No save game to use as reset point");
|
||||||
break;
|
break;
|
||||||
case "stop":
|
case "stop":
|
||||||
case "quit":
|
case "quit":
|
||||||
|
|||||||
+3
-1
@@ -87,9 +87,11 @@ if (Array.IndexOf(args, "--import-save") >= 0)
|
|||||||
copied++;
|
copied++;
|
||||||
}
|
}
|
||||||
GameServer.Log($"Imported {copied} save files from {source}");
|
GameServer.Log($"Imported {copied} save files from {source}");
|
||||||
// Neuer Spielstand = alte Änderungen passen nicht mehr dazu
|
// Neuer Spielstand = alte Änderungen passen nicht mehr dazu, und er ist der neue Reset-Punkt
|
||||||
string world = Path.Combine(dataDir, "world.json");
|
string world = Path.Combine(dataDir, "world.json");
|
||||||
if (File.Exists(world)) File.Delete(world);
|
if (File.Exists(world)) File.Delete(world);
|
||||||
|
string original = Path.Combine(dataDir, "save_original");
|
||||||
|
if (Directory.Exists(original)) Directory.Delete(original, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lizenzschlüssel beim Keymaster prüfen
|
// Lizenzschlüssel beim Keymaster prüfen
|
||||||
|
|||||||
+3
-2
@@ -30,7 +30,7 @@ public enum MsgType : byte
|
|||||||
AdminStatus = 23, // Server -> Client: bool, ist Admin
|
AdminStatus = 23, // Server -> Client: bool, ist Admin
|
||||||
AdminListRequest = 24, // Client -> Server: Spielerliste fürs Admin-Menü
|
AdminListRequest = 24, // Client -> Server: Spielerliste fürs Admin-Menü
|
||||||
AdminList = 25, // Server -> Client: int Anzahl, je [int id, string name, string steamId, bool admin]
|
AdminList = 25, // Server -> Client: int Anzahl, je [int id, string name, string steamId, bool admin]
|
||||||
AdminAction = 26, // Client -> Server: byte (1 kick, 2 ban, 3 bring, 4 unban, 5 reset world, 6 backup, 7 restore backup), int Spieler-ID, string Grund/Name
|
AdminAction = 26, // Client -> Server: byte (1 kick, 2 ban, 3 bring, 4 unban, 5 reset world, 6 backup, 7 restore backup, 8 set reset point), int Spieler-ID, string Grund/Name
|
||||||
AdminResult = 27, // Server -> Client: string Meldung
|
AdminResult = 27, // Server -> Client: string Meldung
|
||||||
Chat = 28, // Chatnachricht (string)
|
Chat = 28, // Chatnachricht (string)
|
||||||
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
|
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
|
||||||
@@ -47,7 +47,8 @@ public enum MsgType : byte
|
|||||||
TrafficStates = 40, // Verkehr, nur von der Welt-Autorität: int n, je [int Schlüssel, 3 float Pos, 4 float Rot, 3 float Geschw.]
|
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]
|
WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ, Wert]
|
||||||
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel]
|
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel]
|
||||||
VehicleControls = 43 // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
|
VehicleControls = 43, // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
|
||||||
|
PlayerNeeds = 44 // Client -> Server: eigene Bedürfnisse (Hunger, Durst ...): int n, je [string Name, float Wert]
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct Message(MsgType Type, byte[] Payload)
|
public readonly record struct Message(MsgType Type, byte[] Payload)
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ Fehlende Einstellungen ergänzt der Server beim Start automatisch. Umgebungsvari
|
|||||||
| `reload-save` | Spielstand-Dateien neu einlesen – verbundene Spieler verbinden sich automatisch neu |
|
| `reload-save` | Spielstand-Dateien neu einlesen – verbundene Spieler verbinden sich automatisch neu |
|
||||||
| `backups`, `restore-save <nr>` | gesicherte Spielstände anzeigen / einen davon zurückholen (z.B. wenn der aktuelle kaputt ist) – alle verbinden sich neu |
|
| `backups`, `restore-save <nr>` | gesicherte Spielstände anzeigen / einen davon zurückholen (z.B. wenn der aktuelle kaputt ist) – alle verbinden sich neu |
|
||||||
| `backup` | aktuellen Spielstand jetzt sichern |
|
| `backup` | aktuellen Spielstand jetzt sichern |
|
||||||
|
| `reset-world` | ganze Welt zurück auf den Reset-Punkt (den Spielstand, wie er dem Server gegeben wurde: Start, `--import-save`, `reload-save`) – alle verbinden neu |
|
||||||
|
| `set-reset-point` | aktuellen Spielstand als neuen Reset-Punkt nehmen |
|
||||||
| `stop` | speichern und beenden |
|
| `stop` | speichern und beenden |
|
||||||
|
|
||||||
Im Spiel öffnen Admins mit `/admin` (Chat, Taste T) oder `admin` (F7-Konsole) das Admin-Menü: Kick/Ban,
|
Im Spiel öffnen Admins mit `/admin` (Chat, Taste T) oder `admin` (F7-Konsole) das Admin-Menü: Kick/Ban,
|
||||||
|
|||||||
@@ -145,6 +145,29 @@ try
|
|||||||
Check(lateSnap.Destroyed.Count <= 21, "Mass 'consumed' reports (e.g. scene unload) are capped: " + lateSnap.Destroyed.Count + " stored");
|
Check(lateSnap.Destroyed.Count <= 21, "Mass 'consumed' reports (e.g. scene unload) are capped: " + lateSnap.Destroyed.Count + " stored");
|
||||||
late.Close();
|
late.Close();
|
||||||
|
|
||||||
|
Console.WriteLine("== Own needs (hunger, thirst ...) are stored per player");
|
||||||
|
var hungry = await TestClient.Join("Hungry", 0, null);
|
||||||
|
hungry.Send(MsgType.Ready, Array.Empty<byte>());
|
||||||
|
await hungry.WaitFor(MsgType.WorldSnapshot);
|
||||||
|
server.EnqueueCommand("save"); // (Zeitfenster TrackPositionFrom abwarten)
|
||||||
|
await Task.Delay(8500);
|
||||||
|
hungry.Send(MsgType.PlayerNeeds, Wire.Build(w => { w.Write(2); w.Write("PlayerHunger"); w.Write(77f); w.Write("PlayerThirst"); w.Write(12.5f); }));
|
||||||
|
await Task.Delay(300);
|
||||||
|
hungry.Close();
|
||||||
|
await Task.Delay(500);
|
||||||
|
var hungry2 = await TestClient.Join("Hungry", 0, null);
|
||||||
|
hungry2.Send(MsgType.Ready, Array.Empty<byte>());
|
||||||
|
var needsSnap = Snapshot.Parse(await hungry2.WaitFor(MsgType.WorldSnapshot));
|
||||||
|
Check(needsSnap.Ok && needsSnap.Needs.TryGetValue("PlayerHunger", out float hunger) && Math.Abs(hunger - 77f) < 0.01f,
|
||||||
|
"Rejoining player gets their own needs back: " + string.Join(", ", needsSnap.Needs.Select(kv => kv.Key + "=" + kv.Value)));
|
||||||
|
var otherSnapClient = await TestClient.Join("NotHungry", 0, null);
|
||||||
|
otherSnapClient.Send(MsgType.Ready, Array.Empty<byte>());
|
||||||
|
var otherSnap = Snapshot.Parse(await otherSnapClient.WaitFor(MsgType.WorldSnapshot));
|
||||||
|
Check(otherSnap.Ok && otherSnap.Needs.Count == 0, "Other players do not get someone else's needs");
|
||||||
|
hungry2.Close();
|
||||||
|
otherSnapClient.Close();
|
||||||
|
await Task.Delay(300);
|
||||||
|
|
||||||
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
|
||||||
@@ -253,6 +276,17 @@ try
|
|||||||
var restoreMsg = await newcomer.WaitFor(MsgType.Reconnect);
|
var restoreMsg = await newcomer.WaitFor(MsgType.Reconnect);
|
||||||
Check(restoreMsg != null, "Restoring a backup makes everyone reconnect");
|
Check(restoreMsg != null, "Restoring a backup makes everyone reconnect");
|
||||||
Check(File.ReadAllText(Path.Combine(dir, "save", "defaultES2File.txt")).EndsWith("CURRENT-SAVE"), "The backup's files are back in the active save");
|
Check(File.ReadAllText(Path.Combine(dir, "save", "defaultES2File.txt")).EndsWith("CURRENT-SAVE"), "The backup's files are back in the active save");
|
||||||
|
|
||||||
|
Console.WriteLine("== reset-world goes back to the ORIGINAL save (not the auto-uploaded one)");
|
||||||
|
var r2 = await TestClient.Join("Resetter2", 0, null);
|
||||||
|
server.EnqueueCommand("reset-world");
|
||||||
|
Check(await r2.WaitFor(MsgType.Reconnect) != null, "Players reconnect after the reset");
|
||||||
|
Check(File.ReadAllText(Path.Combine(dir, "save", "defaultES2File.txt")) == "test defaultES2File.txt",
|
||||||
|
"The active save is the original one again: " + File.ReadAllText(Path.Combine(dir, "save", "defaultES2File.txt")).Length + " chars");
|
||||||
|
var afterReset = await TestClient.Join("AfterReset", 0, null);
|
||||||
|
Check(afterReset.Files.TryGetValue("defaultES2File.txt", out var orig) && System.Text.Encoding.UTF8.GetString(orig) == "test defaultES2File.txt",
|
||||||
|
"Joining players download the original save");
|
||||||
|
afterReset.Close();
|
||||||
inGame.Close();
|
inGame.Close();
|
||||||
newcomer.Close();
|
newcomer.Close();
|
||||||
}
|
}
|
||||||
@@ -292,6 +326,7 @@ sealed class Snapshot
|
|||||||
public int Spawns = -1;
|
public int Spawns = -1;
|
||||||
public int Appeared = -1;
|
public int Appeared = -1;
|
||||||
public List<int> Destroyed = new();
|
public List<int> Destroyed = new();
|
||||||
|
public Dictionary<string, float> Needs = new();
|
||||||
public int Vars = -1;
|
public int Vars = -1;
|
||||||
|
|
||||||
public static Snapshot Parse(byte[] data)
|
public static Snapshot Parse(byte[] data)
|
||||||
@@ -317,6 +352,11 @@ sealed class Snapshot
|
|||||||
for (int i = 0; i < s.Appeared; i++) r.ReadBytes(r.ReadInt32());
|
for (int i = 0; i < s.Appeared; i++) r.ReadBytes(r.ReadInt32());
|
||||||
int destroyed = r.ReadInt32();
|
int destroyed = r.ReadInt32();
|
||||||
for (int i = 0; i < destroyed; i++) s.Destroyed.Add(r.ReadInt32());
|
for (int i = 0; i < destroyed; i++) s.Destroyed.Add(r.ReadInt32());
|
||||||
|
if (r.ReadBoolean())
|
||||||
|
{
|
||||||
|
int needs = r.ReadInt32();
|
||||||
|
for (int i = 0; i < needs; i++) s.Needs[r.ReadString()] = r.ReadSingle();
|
||||||
|
}
|
||||||
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public sealed class WorldState
|
|||||||
public Dictionary<int, ObjectPose> Objects { get; set; } = new();
|
public Dictionary<int, ObjectPose> Objects { get; set; } = new();
|
||||||
/// <summary>FsmEvent-Payloads (Base64) in der Reihenfolge, in der sie passiert sind.</summary>
|
/// <summary>FsmEvent-Payloads (Base64) in der Reihenfolge, in der sie passiert sind.</summary>
|
||||||
public List<string> Events { get; set; } = new();
|
public List<string> Events { get; set; } = new();
|
||||||
|
/// <summary>SteamID64 -> eigene Bedürfnisse (Hunger, Durst, Stress ...), Name -> Wert.</summary>
|
||||||
|
public Dictionary<string, Dictionary<string, float>> PlayerNeeds { get; set; } = new();
|
||||||
/// <summary>SteamID64 -> letzte Position.</summary>
|
/// <summary>SteamID64 -> letzte Position.</summary>
|
||||||
public Dictionary<string, float[]> PlayerPositions { get; set; } = new();
|
public Dictionary<string, float[]> PlayerPositions { get; set; } = new();
|
||||||
/// <summary>Summe aller Geldänderungen seit dem Ausgangs-Spielstand (gemeinsame Kasse).</summary>
|
/// <summary>Summe aller Geldänderungen seit dem Ausgangs-Spielstand (gemeinsame Kasse).</summary>
|
||||||
@@ -74,6 +76,7 @@ public sealed class WorldState
|
|||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
PlayerNeeds.Clear();
|
||||||
Spawns.Clear();
|
Spawns.Clear();
|
||||||
Appeared.Clear();
|
Appeared.Clear();
|
||||||
Destroyed.Clear();
|
Destroyed.Clear();
|
||||||
@@ -215,6 +218,30 @@ public sealed class WorldState
|
|||||||
Dirty = true;
|
Dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>PlayerNeeds: int n, je [string Name, float Wert]. Werte werden begrenzt, Unsinn verworfen.</summary>
|
||||||
|
public void ApplyNeeds(string steamId, byte[] payload)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(steamId) || payload.Length > 1024) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var r = new BinaryReader(new MemoryStream(payload));
|
||||||
|
int count = r.ReadInt32();
|
||||||
|
if (count < 0 || count > 16) return;
|
||||||
|
var needs = new Dictionary<string, float>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
string name = r.ReadString();
|
||||||
|
float value = r.ReadSingle();
|
||||||
|
if (name.Length == 0 || name.Length > 40 || !float.IsFinite(value)) continue;
|
||||||
|
needs[name] = Math.Clamp(value, -1000f, 1000f);
|
||||||
|
}
|
||||||
|
if (needs.Count == 0) return;
|
||||||
|
PlayerNeeds[steamId] = needs;
|
||||||
|
Dirty = true;
|
||||||
|
}
|
||||||
|
catch (EndOfStreamException) { }
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Snapshot für neue Spieler
|
// ---------------------------------------------------------------- Snapshot für neue Spieler
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -280,6 +307,16 @@ public sealed class WorldState
|
|||||||
}
|
}
|
||||||
w.Write(Destroyed.Count);
|
w.Write(Destroyed.Count);
|
||||||
foreach (int id in Destroyed) w.Write(id);
|
foreach (int id in Destroyed) w.Write(id);
|
||||||
|
|
||||||
|
// Eigene Bedürfnisse des Beitretenden: bool vorhanden [int n, je string Name, float Wert]
|
||||||
|
bool hasNeeds = steamId != null && PlayerNeeds.TryGetValue(steamId, out var own) && own.Count > 0;
|
||||||
|
w.Write(hasNeeds);
|
||||||
|
if (hasNeeds)
|
||||||
|
{
|
||||||
|
var needs = PlayerNeeds[steamId];
|
||||||
|
w.Write(needs.Count);
|
||||||
|
foreach (var kv in needs) { w.Write(kv.Key); w.Write(kv.Value); }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user