diff --git a/GameServer.cs b/GameServer.cs
index c108d84..30bb8c5 100644
--- a/GameServer.cs
+++ b/GameServer.cs
@@ -140,6 +140,8 @@ public sealed class GameServer
}
SaveUpload saveUpload;
int nextSaveRequestId = 1;
+ /// Wann der aktuelle Spielstand vom Spiel geschrieben wurde (Datei-Zeit) - bestimmt, ob ein Beitritt frisch speichern lässt.
+ DateTime lastGameSaveUtc = DateTime.MinValue;
double nextSaveSync;
readonly List waitingForSave = new(); // Beigetretene, die auf den frischen Spielstand warten
const long MaxUploadBytes = 32L * 1024 * 1024;
@@ -204,6 +206,12 @@ public sealed class GameServer
return false;
}
saveFiles = loaded;
+ try
+ {
+ string main = Path.Combine(savePath, "defaultES2File.txt");
+ if (File.Exists(main)) lastGameSaveUtc = File.GetLastWriteTimeUtc(main);
+ }
+ catch { }
NewGame = false;
Log("Loaded save game (" + saveFiles.Count + " files, " + saveFiles.Sum(f => f.Value.Length) / 1024 + " KB)");
return true;
@@ -369,7 +377,7 @@ public sealed class GameServer
{
nextSaveSync = now + config.SaveSyncMinutes * 60.0;
// Nur wenn sich seit dem letzten Spielstand etwas geändert hat - Speichern lässt das Spiel kurz hängen
- if (world.ChangesSinceSave > 0) RequestSave("regular save");
+ if (world.ChangesSinceSave > 0) RequestSave("regular save", urgent: false);
}
// Hochladen dauert zu lange (Spieler weg, alte Mod ...): Wartende bekommen den bisherigen Spielstand
if (saveUpload != null && DateTime.UtcNow > saveUpload.Deadline)
@@ -697,8 +705,12 @@ public sealed class GameServer
c.Conn.Send(MsgType.AdminStatus, Wire.Build(w => w.Write(isAdmin)));
// Spielstand schicken; nach dem Laden fragt der Client mit Ready nach dem Weltzustand.
- // Spielt schon jemand, erst dessen aktuellen Spielstand holen (Schrauben, Teile ... genau wie jetzt im Spiel).
- if (RequestSave("new player " + c.Name)) waitingForSave.Add(c);
+ // Spielt schon jemand und ist der Spielstand alt, erst dessen aktuellen Spielstand holen (Schrauben, Teile ...
+ // genau wie jetzt im Spiel). Speichern lässt das Spiel dieses Spielers einige Sekunden hängen - bei einem
+ // frischen Spielstand bekommt der Neue ihn plus die seitdem passierten Änderungen (Weltzustand).
+ bool saveIsFresh = world.ChangesSinceSave == 0
+ || (config.JoinSaveMaxAgeMinutes > 0 && DateTime.UtcNow - lastGameSaveUtc < TimeSpan.FromMinutes(config.JoinSaveMaxAgeMinutes));
+ if (!saveIsFresh && RequestSave("new player " + c.Name)) waitingForSave.Add(c);
else SendSave(c);
Log($"{c.Name} joined (SteamID {c.SteamId}, {c.Conn.RemoteAddress}) - {JoinedCount}/{config.MaxPlayers}");
@@ -782,16 +794,22 @@ public sealed class GameServer
/// Einen Spieler, der schon im Spiel ist (der am längsten verbundene), um seinen aktuellen Spielstand bitten.
/// Gibt false zurück, wenn niemand im Spiel ist - dann ist der gespeicherte Spielstand ohnehin aktuell.
///
- bool RequestSave(string why)
+ /// false = Routine: das Spiel darf auf einen ruhigen Moment warten (nicht während schneller Fahrt).
+ bool RequestSave(string why, bool urgent = true)
{
if (saveUpload != null) return true; // läuft schon
var source = clients.Values
.Where(x => x.State == ClientState.Joined && !x.Conn.IsClosing && x.TrackPositionFrom != DateTime.MaxValue)
.OrderBy(x => x.Id).FirstOrDefault();
if (source == null) return false;
- saveUpload = new SaveUpload { Id = nextSaveRequestId++, FromClient = source.Id, Deadline = DateTime.UtcNow.AddSeconds(SaveUploadTimeoutSeconds) };
+ saveUpload = new SaveUpload
+ {
+ Id = nextSaveRequestId++, FromClient = source.Id,
+ Deadline = DateTime.UtcNow.AddSeconds(urgent ? SaveUploadTimeoutSeconds : Math.Max(SaveUploadTimeoutSeconds, 60))
+ };
int id = saveUpload.Id;
- source.Conn.Send(MsgType.SaveRequest, Wire.Build(w => w.Write(id)));
+ // Ältere Mods lesen nur die ID - das zusätzliche Byte stört sie nicht
+ source.Conn.Send(MsgType.SaveRequest, Wire.Build(w => { w.Write(id); w.Write(urgent); }));
Log($"Asking {source.Name} for the current save game ({why})");
return true;
}
diff --git a/MSCO.Server.csproj b/MSCO.Server.csproj
index 1a3925d..3bafd2f 100644
--- a/MSCO.Server.csproj
+++ b/MSCO.Server.csproj
@@ -7,7 +7,7 @@
enable
MSCO.Server
MSCO.Server
- 2.10.4
+ 2.10.5
MBM
MSCO Dedicated Server
true
diff --git a/README.md b/README.md
index cf6eaf9..ae7145b 100644
--- a/README.md
+++ b/README.md
@@ -116,7 +116,8 @@ und neu starten.
| `PublicAddress` | öffentliche IP/Domain für die Liste (nur hinter NAT/Proxy nötig) |
| `RemoteManagement` | `true` = der Besitzer kann den Server im MSCO Launcher verwalten (Befehle über den Keymaster) |
| `AutosaveSeconds` | wie oft der Weltzustand gespeichert wird |
-| `SaveSyncMinutes` | wie oft (Minuten) sich der Server von einem Spieler im Spiel den **aktuellen Spielstand** hochladen lässt (Standard 10, zusätzlich bei jedem Beitritt; nur wenn sich etwas geändert hat; 0 = nur beim Beitritt) |
+| `SaveSyncMinutes` | wie oft (Minuten) sich der Server von einem Spieler im Spiel den **aktuellen Spielstand** hochladen lässt (Standard 30; nur wenn sich etwas geändert hat; 0 = nur beim Beitritt). Speichern lässt das Spiel dieses Spielers einige Sekunden hängen – das Spiel wartet dafür einen ruhigen Moment ab (nicht in schneller Fahrt) |
+| `JoinSaveMaxAgeMinutes` | beim Beitritt nur dann frisch speichern lassen, wenn der letzte Spielstand älter ist (Standard 15; 0 = bei jedem Beitritt). Sonst bekommt der Neue den gespeicherten Stand plus alle Änderungen seitdem |
| `SteamWebApiKey` | Steam-Anmeldetickets der Spieler bei Steam prüfen – **nötig für Admins** (kostenlos: https://steamcommunity.com/dev/apikey) |
| `BannedSteamIds` | gesperrte Spieler |
| `Admins` | SteamID64s, die im Spiel mit `/admin` kicken/bannen dürfen (nur mit `SteamWebApiKey`, siehe Sicherheit) |
@@ -267,7 +268,7 @@ Der Chat landet auch im Server-Log.
## Was der Server speichert
-**Aktueller Spielstand statt Nachspielen:** Tritt jemand bei (und alle `SaveSyncMinutes`), bittet der Server einen
+**Aktueller Spielstand statt Nachspielen:** Tritt jemand bei (wenn der letzte Spielstand älter als `JoinSaveMaxAgeMinutes` ist) und alle `SaveSyncMinutes`, bittet der Server einen
Spieler, der schon im Spiel ist, zu speichern und den Spielstand hochzuladen – genau wie das Spiel selbst speichert
(Schrauben, angebaute Teile, alles). Neue Spieler bekommen diesen Stand; alte Aktionen werden dann nicht mehr
nachgespielt. Der vorherige Spielstand liegt jeweils in `data/save_backups/` (die letzten 5).
diff --git a/ServerConfig.cs b/ServerConfig.cs
index b277ccf..021f984 100644
--- a/ServerConfig.cs
+++ b/ServerConfig.cs
@@ -28,9 +28,18 @@ public sealed class ServerConfig
public int AutosaveSeconds { get; set; } = 60;
///
/// So oft (Minuten) lässt sich der Server von einem Spieler im Spiel den aktuellen Spielstand hochladen
- /// (Schrauben, Teile, alles wie im Spiel gespeichert). Zusätzlich immer, wenn jemand beitritt. 0 = nur beim Beitreten.
+ /// (Schrauben, Teile, alles wie im Spiel gespeichert). Speichern lässt das Spiel dieses Spielers einige Sekunden
+ /// hängen - deshalb nicht zu oft. 0 = nur beim Beitreten.
///
- public int SaveSyncMinutes { get; set; } = 10;
+ public int SaveSyncMinutes { get; set; } = 30;
+ ///
+ /// Beim Beitreten nur dann frisch speichern lassen, wenn der letzte Spielstand älter ist (Minuten) - sonst bekommt der
+ /// Neue den gespeicherten Stand und die seitdem passierten Änderungen. 0 = bei jedem Beitritt frisch speichern.
+ ///
+ public int JoinSaveMaxAgeMinutes { get; set; } = 15;
+ /// Version der Einstellungen (für einmalige Anpassungen alter server.json).
+ public int ConfigVersion { get; set; } = 0;
+ public const int CurrentConfigVersion = 2;
/// Optional: Steam Web API Key, um Steam-Tickets der Spieler bei Steam zu prüfen.
public string SteamWebApiKey { get; set; } = "";
/// Gesperrte SteamID64s.
@@ -72,11 +81,18 @@ public sealed class ServerConfig
created = !File.Exists(path);
if (created)
{
- var fresh = new ServerConfig();
+ var fresh = new ServerConfig { ConfigVersion = CurrentConfigVersion };
fresh.Save(path);
return fresh;
}
var config = JsonSerializer.Deserialize(File.ReadAllText(path), Options) ?? new ServerConfig();
+ if (config.ConfigVersion < 2)
+ {
+ // Früherer Standard: alle 10 Minuten speichern lassen - das ließ einen Spieler alle 10 Minuten einige Sekunden
+ // hängen. Wer den Standard nie geändert hat, bekommt den neuen (30).
+ if (config.SaveSyncMinutes == 10) config.SaveSyncMinutes = 30;
+ config.ConfigVersion = CurrentConfigVersion;
+ }
// Neue Einstellungen (z.B. Admins) auch in ältere server.json schreiben, damit man sie sieht
try { config.Save(path); } catch (Exception) { } // z.B. schreibgeschützt - dann eben nicht
return config;
diff --git a/Tests/Program.cs b/Tests/Program.cs
index 141f731..bac3b27 100644
--- a/Tests/Program.cs
+++ b/Tests/Program.cs
@@ -12,7 +12,7 @@ Directory.CreateDirectory(Path.Combine(dir, "save"));
foreach (var name in new[] { "defaultES2File.txt", "items.txt", "meshsave.txt", "graveyard.txt", "notepad.txt" })
File.WriteAllText(Path.Combine(dir, "save", name), "test " + name);
string configPath = Path.Combine(dir, "server.json");
-var config = new ServerConfig { LicenseKey = "test", Name = "Test", Port = Port, MaxPlayers = 4, RequireSteam = false, Public = false, Admins = new() { AdminId } };
+var config = new ServerConfig { LicenseKey = "test", Name = "Test", Port = Port, MaxPlayers = 4, RequireSteam = false, Public = false, Admins = new() { AdminId }, JoinSaveMaxAgeMinutes = 0 };
config.Save(configPath);
// Host wie im echten Betrieb: besitzt Port + Web-Panel (gleicher Port wie das Spiel) und startet den Gameserver
@@ -32,6 +32,20 @@ void Check(bool ok, string what) { if (ok) passed++; else failed++; Console.Writ
try
{
+ Console.WriteLine("== Settings migration");
+ {
+ string oldCfg = Path.Combine(Path.GetTempPath(), "msco-oldcfg-" + Guid.NewGuid().ToString("N") + ".json");
+ File.WriteAllText(oldCfg, "{ \"Name\": \"Old\", \"SaveSyncMinutes\": 10 }");
+ var migrated = ServerConfig.LoadOrCreate(oldCfg, out _);
+ File.WriteAllText(oldCfg, "{ \"Name\": \"Custom\", \"SaveSyncMinutes\": 5 }");
+ var custom = ServerConfig.LoadOrCreate(oldCfg, out _);
+ var again = ServerConfig.LoadOrCreate(oldCfg, out _);
+ File.Delete(oldCfg);
+ Check(migrated.SaveSyncMinutes == 30 && migrated.ConfigVersion == ServerConfig.CurrentConfigVersion,
+ "Old default 'save every 10 minutes' becomes 30 (less freezing)");
+ Check(custom.SaveSyncMinutes == 5 && again.SaveSyncMinutes == 5, "A value the owner chose is kept");
+ }
+
Console.WriteLine("== Join");
var alice = await TestClient.Join("Alice", 0, null);
Check(alice.Welcome != null, "Alice gets Welcome");
@@ -338,6 +352,7 @@ try
var joinTask = TestClient.Join("Newcomer", 0, null);
var request = await inGame.WaitFor(MsgType.SaveRequest, 5000);
Check(request != null, "Server asks the player in the game for the current save");
+ Check(request != null && request.Length == 5 && request[4] == 1, "A save request for a joining player is marked urgent");
if (request != null)
{
int reqId = BitConverter.ToInt32(request, 0);