Server 2.10.5: far fewer save freezes

Saving makes the game of the asked player freeze for seconds. Joining now asks
for a fresh save only if the stored one is older than JoinSaveMaxAgeMinutes (15)
and something changed - otherwise the stored save plus the world state; regular
saves every 30 minutes (old untouched default 10 is migrated once) and marked
not urgent, so the game can wait for a calm moment (60 s deadline). 3 new tests (168).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 00:08:56 +02:00
co-authored by Claude Opus 5.5
parent 4cc71aa079
commit 6e2ef3b570
5 changed files with 63 additions and 13 deletions
+24 -6
View File
@@ -140,6 +140,8 @@ public sealed class GameServer
}
SaveUpload saveUpload;
int nextSaveRequestId = 1;
/// <summary>Wann der aktuelle Spielstand vom Spiel geschrieben wurde (Datei-Zeit) - bestimmt, ob ein Beitritt frisch speichern lässt.</summary>
DateTime lastGameSaveUtc = DateTime.MinValue;
double nextSaveSync;
readonly List<Client> 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.
/// </summary>
bool RequestSave(string why)
/// <param name="urgent">false = Routine: das Spiel darf auf einen ruhigen Moment warten (nicht während schneller Fahrt).</param>
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;
}