New game from the panel instead of an uploaded save (2.9.3)
World & Backups -> Start new game (console: new-game, also when stopped): the save is backed up and replaced by a new-game marker (also as reset point). Joining players are told to start a new game; the server pulls the first player's save and from then on runs normally. Reset world returns to a new game when the reset point is one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -739,6 +739,7 @@ public sealed class AdminPanel
|
|||||||
GameServer.Log($"Save game backed up by {by}");
|
GameServer.Log($"Save game backed up by {by}");
|
||||||
return "Backup created";
|
return "Backup created";
|
||||||
case "reset": return SaveStore.ResetOffline(dataDir, by);
|
case "reset": return SaveStore.ResetOffline(dataDir, by);
|
||||||
|
case "newgame": return SaveStore.NewGameOffline(dataDir, by);
|
||||||
case "restore": return SaveStore.RestoreOffline(dataDir, name, by);
|
case "restore": return SaveStore.RestoreOffline(dataDir, name, by);
|
||||||
case "resetpoint": return SaveStore.SetResetPoint(dataDir) ?? "The current save game is now the reset point";
|
case "resetpoint": return SaveStore.SetResetPoint(dataDir) ?? "The current save game is now the reset point";
|
||||||
case "reloadsave": return "The server is stopped - it loads the save game when you start it";
|
case "reloadsave": return "The server is stopped - it loads the save game when you start it";
|
||||||
|
|||||||
+68
-3
@@ -136,6 +136,14 @@ public sealed class GameServer
|
|||||||
Log(PermissionHint());
|
Log(PermissionHint());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (loaded.All(f => f.Key != "defaultES2File.txt") && SaveStore.IsNewGame(DataDir))
|
||||||
|
{
|
||||||
|
// Neues Spiel: es gibt (noch) keinen Spielstand - der erste Spieler startet im Spiel ein neues Spiel
|
||||||
|
saveFiles = new List<KeyValuePair<string, byte[]>>();
|
||||||
|
NewGame = true;
|
||||||
|
Log("New game: the first player to join creates a fresh world, the server then takes over its save game");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (loaded.All(f => f.Key != "defaultES2File.txt"))
|
if (loaded.All(f => f.Key != "defaultES2File.txt"))
|
||||||
{
|
{
|
||||||
Log("No save game found in " + savePath);
|
Log("No save game found in " + savePath);
|
||||||
@@ -143,6 +151,7 @@ public sealed class GameServer
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
saveFiles = loaded;
|
saveFiles = loaded;
|
||||||
|
NewGame = false;
|
||||||
Log("Loaded save game (" + saveFiles.Count + " files, " + saveFiles.Sum(f => f.Value.Length) / 1024 + " KB)");
|
Log("Loaded save game (" + saveFiles.Count + " files, " + saveFiles.Sum(f => f.Value.Length) / 1024 + " KB)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -257,6 +266,7 @@ public sealed class GameServer
|
|||||||
|
|
||||||
void Timers()
|
void Timers()
|
||||||
{
|
{
|
||||||
|
PullNewGameSave();
|
||||||
double now = clock.Elapsed.TotalSeconds;
|
double now = clock.Elapsed.TotalSeconds;
|
||||||
if (now >= nextAutosave)
|
if (now >= nextAutosave)
|
||||||
{
|
{
|
||||||
@@ -583,7 +593,40 @@ public sealed class GameServer
|
|||||||
c.Conn.Send(MsgType.SaveBegin);
|
c.Conn.Send(MsgType.SaveBegin);
|
||||||
foreach (var f in saveFiles)
|
foreach (var f in saveFiles)
|
||||||
c.Conn.Send(MsgType.SaveFile, Wire.Build(w => { w.Write(f.Key); w.Write(f.Value.Length); w.Write(f.Value); }));
|
c.Conn.Send(MsgType.SaveFile, Wire.Build(w => { w.Write(f.Key); w.Write(f.Value.Length); w.Write(f.Value); }));
|
||||||
c.Conn.Send(MsgType.StartGame, Wire.Build(w => w.Write(false)));
|
// [bool neben den Host setzen, bool neues Spiel] - ältere Mods lesen nur den ersten Wert
|
||||||
|
bool newGame = NewGame && saveFiles.Count == 0;
|
||||||
|
c.Conn.Send(MsgType.StartGame, Wire.Build(w => { w.Write(false); w.Write(newGame); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Neues Spiel: noch kein Spielstand - der erste Spieler legt die Welt im Spiel an.</summary>
|
||||||
|
public bool NewGame { get; private set; }
|
||||||
|
DateTime nextNewGameSave = DateTime.MinValue;
|
||||||
|
|
||||||
|
/// <summary>Neues Spiel läuft: den Spielstand des ersten Spielers holen, sobald einer im Spiel ist.</summary>
|
||||||
|
void PullNewGameSave()
|
||||||
|
{
|
||||||
|
if (!NewGame || saveUpload != null || DateTime.UtcNow < nextNewGameSave) return;
|
||||||
|
nextNewGameSave = DateTime.UtcNow.AddSeconds(20);
|
||||||
|
RequestSave("new game - taking over the fresh world");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Panel/Konsole: aktuellen Spielstand sichern und ein neues Spiel starten. Alle verbinden neu.</summary>
|
||||||
|
string StartNewGame(string by)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SaveStore.MakeNewGame(DataDir, alsoResetPoint: true);
|
||||||
|
if (!LoadSave()) return "Could not start a new game";
|
||||||
|
}
|
||||||
|
catch (Exception e) { return "Could not start a new game: " + e.Message; }
|
||||||
|
saveUpload = null;
|
||||||
|
waitingForSave.Clear();
|
||||||
|
world.Reset();
|
||||||
|
SaveWorld();
|
||||||
|
nextNewGameSave = DateTime.UtcNow.AddSeconds(30); // erst laden lassen
|
||||||
|
Log($"New game started by {by} - the previous save game is in the backups");
|
||||||
|
ReconnectAll(by == "Console" ? "The server starts a new game - reconnecting..." : $"{by} started a new game - reconnecting...");
|
||||||
|
return "New game started - the first player to join creates the new world";
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendSaveToWaiting()
|
void SendSaveToWaiting()
|
||||||
@@ -656,6 +699,9 @@ public sealed class GameServer
|
|||||||
File.WriteAllBytes(tmp, f.Value);
|
File.WriteAllBytes(tmp, f.Value);
|
||||||
File.Move(tmp, target, true);
|
File.Move(tmp, target, true);
|
||||||
}
|
}
|
||||||
|
bool wasNewGame = NewGame;
|
||||||
|
SaveStore.ClearNewGameMarker(DataDir);
|
||||||
|
if (wasNewGame) Log($"The new game is running - took over the fresh world from {c.Name}");
|
||||||
if (LoadSave())
|
if (LoadSave())
|
||||||
{
|
{
|
||||||
world.ResetForNewSave();
|
world.ResetForNewSave();
|
||||||
@@ -700,7 +746,7 @@ public sealed class GameServer
|
|||||||
/// <summary>Beim Start: gibt es noch keinen Reset-Punkt, ist es der aktuelle Spielstand.</summary>
|
/// <summary>Beim Start: gibt es noch keinen Reset-Punkt, ist es der aktuelle Spielstand.</summary>
|
||||||
void EnsureResetPoint()
|
void EnsureResetPoint()
|
||||||
{
|
{
|
||||||
if (File.Exists(Path.Combine(originalPath, "defaultES2File.txt"))) return;
|
if (SaveStore.HasResetPoint(DataDir)) return;
|
||||||
if (SetResetPoint()) Log("Remembered the current save game as the reset point (reset-world goes back to it)");
|
if (SetResetPoint()) Log("Remembered the current save game as the reset point (reset-world goes back to it)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,6 +765,20 @@ public sealed class GameServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
string ResetWorld(string by)
|
string ResetWorld(string by)
|
||||||
{
|
{
|
||||||
|
if (SaveStore.ResetPointIsNewGame(DataDir))
|
||||||
|
{
|
||||||
|
// Reset-Punkt ist ein neues Spiel
|
||||||
|
try { SaveStore.MakeNewGame(DataDir, alsoResetPoint: false); LoadSave(); }
|
||||||
|
catch (Exception e) { Log("Could not reset to a new game: " + e.Message); }
|
||||||
|
saveUpload = null;
|
||||||
|
waitingForSave.Clear();
|
||||||
|
world.Reset();
|
||||||
|
SaveWorld();
|
||||||
|
nextNewGameSave = DateTime.UtcNow.AddSeconds(30);
|
||||||
|
Log($"World reset by {by} - back to a new game (previous state backed up).");
|
||||||
|
ReconnectAll(by == "Console" ? "The world is being reset - reconnecting..." : $"{by} reset the world - reconnecting...");
|
||||||
|
return "World reset to a new game";
|
||||||
|
}
|
||||||
bool restored = false;
|
bool restored = false;
|
||||||
if (File.Exists(Path.Combine(originalPath, "defaultES2File.txt")))
|
if (File.Exists(Path.Combine(originalPath, "defaultES2File.txt")))
|
||||||
{
|
{
|
||||||
@@ -859,7 +919,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> | set-reset-point | panel | restart | stop | exit | 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 | panel | restart | stop | exit | say <text> | op <id> | deop <id> | admins | save | reset-world | new-game | 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; }
|
||||||
@@ -943,6 +1003,9 @@ public sealed class GameServer
|
|||||||
case "reset-world":
|
case "reset-world":
|
||||||
ResetWorld("Console");
|
ResetWorld("Console");
|
||||||
break;
|
break;
|
||||||
|
case "new-game":
|
||||||
|
Log(StartNewGame("Console"));
|
||||||
|
break;
|
||||||
case "backups":
|
case "backups":
|
||||||
{
|
{
|
||||||
var list = SaveBackups();
|
var list = SaveBackups();
|
||||||
@@ -1060,6 +1123,7 @@ public sealed class GameServer
|
|||||||
? File.GetLastWriteTimeUtc(Path.Combine(originalPath, "defaultES2File.txt")).ToString("o") : null;
|
? File.GetLastWriteTimeUtc(Path.Combine(originalPath, "defaultES2File.txt")).ToString("o") : null;
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
|
newGame = NewGame,
|
||||||
name = config.Name,
|
name = config.Name,
|
||||||
description = config.Description,
|
description = config.Description,
|
||||||
version = ModVersion,
|
version = ModVersion,
|
||||||
@@ -1264,6 +1328,7 @@ public sealed class GameServer
|
|||||||
case "save": SaveWorld(); return "World state saved";
|
case "save": SaveWorld(); return "World state saved";
|
||||||
case "backup": return BackupNow(by);
|
case "backup": return BackupNow(by);
|
||||||
case "reset": return ResetWorld(by);
|
case "reset": return ResetWorld(by);
|
||||||
|
case "newgame": return StartNewGame(by);
|
||||||
case "restore": return RestoreSave(arg ?? "", by);
|
case "restore": return RestoreSave(arg ?? "", by);
|
||||||
case "resetpoint":
|
case "resetpoint":
|
||||||
if (!SetResetPoint()) return "No save game to use as reset point";
|
if (!SetResetPoint()) return "No save game to use as reset point";
|
||||||
|
|||||||
+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.9.2</Version>
|
<Version>2.9.3</Version>
|
||||||
<Company>MBM</Company>
|
<Company>MBM</Company>
|
||||||
<Product>MSCO Dedicated Server</Product>
|
<Product>MSCO Dedicated Server</Product>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
|||||||
@@ -708,6 +708,10 @@ function renderWorld(page) {
|
|||||||
h("div", { class: "stat-label" }, "Reset world"),
|
h("div", { class: "stat-label" }, "Reset world"),
|
||||||
h("p", { class: "muted" }, "Everything goes back to the reset point (the save game as it was uploaded to the server). The current state is backed up first. Everyone reconnects."),
|
h("p", { class: "muted" }, "Everything goes back to the reset point (the save game as it was uploaded to the server). The current state is backed up first. Everyone reconnects."),
|
||||||
h("button", { class: "btn danger", type: "button", onclick: () => worldAction("reset", { title: "Reset the whole world?", text: "Moved objects, bought items, doors, vehicle values and player positions are reset to the reset point. The current state is kept as a backup.", danger: true, button: "Reset world" }) }, "Reset world")),
|
h("button", { class: "btn danger", type: "button", onclick: () => worldAction("reset", { title: "Reset the whole world?", text: "Moved objects, bought items, doors, vehicle values and player positions are reset to the reset point. The current state is kept as a backup.", danger: true, button: "Reset world" }) }, "Reset world")),
|
||||||
|
h("div", { class: "card" },
|
||||||
|
h("div", { class: "stat-label" }, "New game"),
|
||||||
|
h("p", { class: "muted" }, "Start a completely fresh My Summer Car game instead of an uploaded save. The current save game is backed up first. The first player to join creates the new world."),
|
||||||
|
h("button", { class: "btn danger", type: "button", onclick: () => worldAction("newgame", { title: "Start a new game?", text: "The current save game and all changes in the world are replaced by a fresh new game (the current state is kept as a backup). “Reset world” then also goes back to a new game. Everyone reconnects.", danger: true, button: "Start new game" }) }, "Start new game")),
|
||||||
h("div", { class: "card" },
|
h("div", { class: "card" },
|
||||||
h("div", { class: "stat-label" }, "Backup"),
|
h("div", { class: "stat-label" }, "Backup"),
|
||||||
h("p", { class: "muted" }, "Save a copy of the current server save game. The last 5 backups are kept."),
|
h("p", { class: "muted" }, "Save a copy of the current server save game. The last 5 backups are kept."),
|
||||||
|
|||||||
+14
-2
@@ -13,6 +13,18 @@
|
|||||||
window.MSCO_LANG = lang;
|
window.MSCO_LANG = lang;
|
||||||
|
|
||||||
const DE = {
|
const DE = {
|
||||||
|
// Neues Spiel
|
||||||
|
"New game": "Neues Spiel",
|
||||||
|
"Start a completely fresh My Summer Car game instead of an uploaded save. The current save game is backed up first. The first player to join creates the new world.":
|
||||||
|
"Ein komplett neues My-Summer-Car-Spiel statt eines hochgeladenen Spielstands. Der aktuelle Spielstand wird vorher gesichert. Der erste Spieler, der beitritt, legt die neue Welt an.",
|
||||||
|
"Start new game": "Neues Spiel starten",
|
||||||
|
"Start a new game?": "Neues Spiel starten?",
|
||||||
|
"The current save game and all changes in the world are replaced by a fresh new game (the current state is kept as a backup). “Reset world” then also goes back to a new game. Everyone reconnects.":
|
||||||
|
"Der aktuelle Spielstand und alle Änderungen in der Welt werden durch ein frisches neues Spiel ersetzt (der jetzige Stand bleibt als Backup). „Welt zurücksetzen“ geht danach ebenfalls auf ein neues Spiel zurück. Alle verbinden sich neu.",
|
||||||
|
"New game started - the first player to join creates the new world": "Neues Spiel gestartet - der erste Spieler, der beitritt, legt die neue Welt an",
|
||||||
|
"New game set up - start the server; the first player to join creates the new world": "Neues Spiel eingerichtet - starte den Server; der erste Spieler, der beitritt, legt die neue Welt an",
|
||||||
|
"World reset to a new game": "Welt auf ein neues Spiel zurückgesetzt",
|
||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
"Tags": "Tags",
|
"Tags": "Tags",
|
||||||
"e.g. german, roleplay, vanilla": "z.B. deutsch, roleplay, vanilla",
|
"e.g. german, roleplay, vanilla": "z.B. deutsch, roleplay, vanilla",
|
||||||
@@ -387,8 +399,8 @@
|
|||||||
.replace(" - the new port only applies after the whole program restarts (docker compose restart); in Docker publish it in docker-compose.yml first",
|
.replace(" - the new port only applies after the whole program restarts (docker compose restart); in Docker publish it in docker-compose.yml first",
|
||||||
" - der neue Port gilt erst nach einem Neustart des ganzen Programms (docker compose restart); in Docker vorher in docker-compose.yml freigeben")],
|
" - der neue Port gilt erst nach einem Neustart des ganzen Programms (docker compose restart); in Docker vorher in docker-compose.yml freigeben")],
|
||||||
[/^The server is already (\w+)$/, (m, s) => "Der Server " + (s === "running" ? "läuft schon" : "startet schon")],
|
[/^The server is already (\w+)$/, (m, s) => "Der Server " + (s === "running" ? "läuft schon" : "startet schon")],
|
||||||
[/^No save game yet - upload one as ZIP in the panel \(World & Backups\) or copy it to data\/save$/,
|
[/^No save game yet - upload one as ZIP in the panel \(World & Backups\), start a new game there or copy it to data\/save$/,
|
||||||
"Noch kein Spielstand - lade einen als ZIP/RAR im Panel hoch (Welt & Backups) oder kopiere ihn nach data/save"],
|
"Noch kein Spielstand - lade einen als ZIP/RAR im Panel hoch (Welt & Backups), starte dort ein neues Spiel oder kopiere ihn nach data/save"],
|
||||||
[/^No license key set - (.+)$/, "Kein Lizenzschlüssel gesetzt - hol dir einen unter msco.mbm.wtf/keys und trag ihn in den Einstellungen ein"],
|
[/^No license key set - (.+)$/, "Kein Lizenzschlüssel gesetzt - hol dir einen unter msco.mbm.wtf/keys und trag ihn in den Einstellungen ein"],
|
||||||
[/^License key invalid: (.+)$/, "Lizenzschlüssel ungültig: $1"],
|
[/^License key invalid: (.+)$/, "Lizenzschlüssel ungültig: $1"],
|
||||||
[/^Update available: (.+)$/, "Update verfügbar: $1"],
|
[/^Update available: (.+)$/, "Update verfügbar: $1"],
|
||||||
|
|||||||
@@ -166,6 +166,9 @@ Mit diesem Code legst du im Browser das **Owner**-Konto an (erneut anzeigen: `ms
|
|||||||
- **Keymaster nicht erreichbar oder lehnt den Schlüssel ab?** Ein laufender Server läuft bis zum nächsten Neustart weiter
|
- **Keymaster nicht erreichbar oder lehnt den Schlüssel ab?** Ein laufender Server läuft bis zum nächsten Neustart weiter
|
||||||
und warnt im Log/Panel. Ein Neustart klappt noch bis 72 Stunden nach der letzten Bestätigung (gespeichert in
|
und warnt im Log/Panel. Ein Neustart klappt noch bis 72 Stunden nach der letzten Bestätigung (gespeichert in
|
||||||
`data/license.json`); danach startet der Server von selbst, sobald der Master den Schlüssel wieder bestätigt.
|
`data/license.json`); danach startet der Server von selbst, sobald der Master den Schlüssel wieder bestätigt.
|
||||||
|
- **Neues Spiel** (Welt & Backups → „Neues Spiel starten“, Konsole `new-game`): statt eines hochgeladenen Spielstands
|
||||||
|
legt der erste Spieler, der beitritt, eine frische Welt an; der Server übernimmt danach dessen Spielstand. Der
|
||||||
|
bisherige Spielstand landet in den Backups, „Welt zurücksetzen“ geht dann wieder auf ein neues Spiel zurück.
|
||||||
- **Updates im Panel** (Seite „Update“): Der Server prüft alle 6 Stunden, ob auf git.mbm.wtf eine neuere Version liegt
|
- **Updates im Panel** (Seite „Update“): Der Server prüft alle 6 Stunden, ob auf git.mbm.wtf eine neuere Version liegt
|
||||||
(Hinweis im Log und in der Seitenleiste). „Update installieren“ lädt den Quellcode, baut ihn mit dem .NET SDK und
|
(Hinweis im Log und in der Seitenleiste). „Update installieren“ lädt den Quellcode, baut ihn mit dem .NET SDK und
|
||||||
startet neu – mit Fortschrittsanzeige (Download, Bauen, Installieren, Neustart), die auch den Neustart des Panels
|
startet neu – mit Fortschrittsanzeige (Download, Bauen, Installieren, Neustart), die auch den Neustart des Panels
|
||||||
|
|||||||
+63
-2
@@ -19,7 +19,57 @@ public static class SaveStore
|
|||||||
public static string WorldPath(string dataDir) => Path.Combine(dataDir, "world.json");
|
public static string WorldPath(string dataDir) => Path.Combine(dataDir, "world.json");
|
||||||
|
|
||||||
public static bool HasSave(string dataDir) => File.Exists(Path.Combine(SavePath(dataDir), "defaultES2File.txt"));
|
public static bool HasSave(string dataDir) => File.Exists(Path.Combine(SavePath(dataDir), "defaultES2File.txt"));
|
||||||
public static bool HasResetPoint(string dataDir) => File.Exists(Path.Combine(OriginalPath(dataDir), "defaultES2File.txt"));
|
public static bool HasResetPoint(string dataDir) =>
|
||||||
|
File.Exists(Path.Combine(OriginalPath(dataDir), "defaultES2File.txt")) || File.Exists(Path.Combine(OriginalPath(dataDir), NewGameMarker));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Neues Spiel": statt Spielstand-Dateien liegt diese Markierung in data/save. Der erste Spieler startet im Spiel ein
|
||||||
|
/// neues Spiel, der Server holt sich danach dessen Spielstand. Liegt sie im Reset-Punkt, geht "Reset world" zurück
|
||||||
|
/// auf ein neues Spiel.
|
||||||
|
/// </summary>
|
||||||
|
public const string NewGameMarker = "NEWGAME.txt";
|
||||||
|
public static bool IsNewGame(string dataDir) => !HasSave(dataDir) && File.Exists(Path.Combine(SavePath(dataDir), NewGameMarker));
|
||||||
|
public static bool ResetPointIsNewGame(string dataDir) =>
|
||||||
|
!File.Exists(Path.Combine(OriginalPath(dataDir), "defaultES2File.txt")) && File.Exists(Path.Combine(OriginalPath(dataDir), NewGameMarker));
|
||||||
|
|
||||||
|
/// <summary>Aktuellen Spielstand sichern und durch "neues Spiel" ersetzen (auch als Reset-Punkt).</summary>
|
||||||
|
public static void MakeNewGame(string dataDir, bool alsoResetPoint)
|
||||||
|
{
|
||||||
|
Backup(dataDir);
|
||||||
|
string save = SavePath(dataDir);
|
||||||
|
Directory.CreateDirectory(save);
|
||||||
|
foreach (string name in SaveFiles)
|
||||||
|
{
|
||||||
|
string file = Path.Combine(save, name);
|
||||||
|
if (File.Exists(file)) File.Delete(file);
|
||||||
|
}
|
||||||
|
File.WriteAllText(Path.Combine(save, NewGameMarker), "MSCO: new game - the first player creates a fresh world");
|
||||||
|
if (!alsoResetPoint) return;
|
||||||
|
string original = OriginalPath(dataDir);
|
||||||
|
if (Directory.Exists(original)) Directory.Delete(original, true);
|
||||||
|
Directory.CreateDirectory(original);
|
||||||
|
File.WriteAllText(Path.Combine(original, NewGameMarker), "MSCO: the reset point is a new game");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Echter Spielstand ist da: die Neues-Spiel-Markierung entfernen.</summary>
|
||||||
|
public static void ClearNewGameMarker(string dataDir)
|
||||||
|
{
|
||||||
|
string marker = Path.Combine(SavePath(dataDir), NewGameMarker);
|
||||||
|
try { if (File.Exists(marker)) File.Delete(marker); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server aus: neues Spiel einrichten.</summary>
|
||||||
|
public static string NewGameOffline(string dataDir, string by)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MakeNewGame(dataDir, alsoResetPoint: true);
|
||||||
|
ResetWorldFile(dataDir, full: true);
|
||||||
|
}
|
||||||
|
catch (Exception e) { return "Could not start a new game: " + e.Message; }
|
||||||
|
GameServer.Log($"New game set up by {by} (server stopped) - the previous save game is in the backups");
|
||||||
|
return "New game set up - start the server; the first player to join creates the new world";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Backup-Ordner, neueste zuerst.</summary>
|
/// <summary>Backup-Ordner, neueste zuerst.</summary>
|
||||||
public static List<string> BackupDirs(string dataDir)
|
public static List<string> BackupDirs(string dataDir)
|
||||||
@@ -45,6 +95,15 @@ public static class SaveStore
|
|||||||
/// <summary>Aktuellen Spielstand als Reset-Punkt merken. Gibt einen Fehler zurück oder null.</summary>
|
/// <summary>Aktuellen Spielstand als Reset-Punkt merken. Gibt einen Fehler zurück oder null.</summary>
|
||||||
public static string SetResetPoint(string dataDir)
|
public static string SetResetPoint(string dataDir)
|
||||||
{
|
{
|
||||||
|
if (IsNewGame(dataDir))
|
||||||
|
{
|
||||||
|
// Neues Spiel als Reset-Punkt: "Reset world" startet wieder ein neues Spiel
|
||||||
|
string orig = OriginalPath(dataDir);
|
||||||
|
if (Directory.Exists(orig)) Directory.Delete(orig, true);
|
||||||
|
Directory.CreateDirectory(orig);
|
||||||
|
File.WriteAllText(Path.Combine(orig, NewGameMarker), "MSCO: the reset point is a new game");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!HasSave(dataDir)) return "No save game to use as reset point";
|
if (!HasSave(dataDir)) return "No save game to use as reset point";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -63,6 +122,7 @@ public static class SaveStore
|
|||||||
{
|
{
|
||||||
string save = SavePath(dataDir);
|
string save = SavePath(dataDir);
|
||||||
Directory.CreateDirectory(save);
|
Directory.CreateDirectory(save);
|
||||||
|
ClearNewGameMarker(dataDir);
|
||||||
foreach (string name in SaveFiles)
|
foreach (string name in SaveFiles)
|
||||||
{
|
{
|
||||||
string target = Path.Combine(save, name);
|
string target = Path.Combine(save, name);
|
||||||
@@ -212,7 +272,8 @@ public static class SaveStore
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (HasResetPoint(dataDir))
|
if (ResetPointIsNewGame(dataDir)) MakeNewGame(dataDir, alsoResetPoint: false);
|
||||||
|
else if (HasResetPoint(dataDir))
|
||||||
{
|
{
|
||||||
var files = ReadFolder(OriginalPath(dataDir));
|
var files = ReadFolder(OriginalPath(dataDir));
|
||||||
Backup(dataDir);
|
Backup(dataDir);
|
||||||
|
|||||||
+3
-2
@@ -306,7 +306,7 @@ public sealed class ServerHost
|
|||||||
if (!server.LoadSave())
|
if (!server.LoadSave())
|
||||||
return Fail(SaveStore.HasSave(dataDir)
|
return Fail(SaveStore.HasSave(dataDir)
|
||||||
? "The save game could not be loaded - see the console"
|
? "The save game could not be loaded - see the console"
|
||||||
: "No save game yet - upload one as ZIP in the panel (World & Backups) or copy it to data/save");
|
: "No save game yet - upload one as ZIP in the panel (World & Backups), start a new game there or copy it to data/save");
|
||||||
server.Panel = Panel;
|
server.Panel = Panel;
|
||||||
server.Host = this;
|
server.Host = this;
|
||||||
server.LicenseConfirmedUtc = confirmed;
|
server.LicenseConfirmedUtc = confirmed;
|
||||||
@@ -369,7 +369,8 @@ public sealed class ServerHost
|
|||||||
resetPoint = SaveStore.HasResetPoint(dataDir)
|
resetPoint = SaveStore.HasResetPoint(dataDir)
|
||||||
? File.GetLastWriteTimeUtc(Path.Combine(SaveStore.OriginalPath(dataDir), "defaultES2File.txt")).ToString("o") : null,
|
? File.GetLastWriteTimeUtc(Path.Combine(SaveStore.OriginalPath(dataDir), "defaultES2File.txt")).ToString("o") : null,
|
||||||
saveSyncMinutes = Config.SaveSyncMinutes,
|
saveSyncMinutes = Config.SaveSyncMinutes,
|
||||||
hasSave = SaveStore.HasSave(dataDir),
|
hasSave = SaveStore.HasSave(dataDir) || SaveStore.IsNewGame(dataDir),
|
||||||
|
newGame = SaveStore.IsNewGame(dataDir),
|
||||||
hasLicenseKey = !string.IsNullOrWhiteSpace(Config.LicenseKey)
|
hasLicenseKey = !string.IsNullOrWhiteSpace(Config.LicenseKey)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -554,6 +554,20 @@ async Task PanelTests()
|
|||||||
}
|
}
|
||||||
Check(code == 200 && host.Panel.SharesGamePort, "Panel is back on the game port");
|
Check(code == 200 && host.Panel.SharesGamePort, "Panel is back on the game port");
|
||||||
|
|
||||||
|
// Neues Spiel statt hochgeladenem Spielstand
|
||||||
|
var ng = await Call(owner, "POST", "api/world/newgame");
|
||||||
|
Check(ng.status == 200 && File.Exists(Path.Combine(dir, "save", SaveStore.NewGameMarker)) && !SaveStore.HasSave(dir),
|
||||||
|
"New game: the save is backed up and replaced by a new-game marker: " + ng.json);
|
||||||
|
await WaitForState(HostState.Running);
|
||||||
|
for (int i = 0; i < 40 && (Srv() == null || !Srv().NewGame); i++) await Task.Delay(100);
|
||||||
|
Check(Srv() != null && Srv().NewGame && SaveStore.ResetPointIsNewGame(dir), "The server runs a new game (also as reset point)");
|
||||||
|
var fresh = await TestClient.Join("Fresh", 0, null);
|
||||||
|
var start = fresh.StartGamePayload;
|
||||||
|
bool startsNew = false;
|
||||||
|
if (start != null && start.Length >= 2) { var sr = new BinaryReader(new MemoryStream(start)); sr.ReadBoolean(); startsNew = sr.ReadBoolean(); }
|
||||||
|
Check(startsNew, "Joining players are told to start a new game");
|
||||||
|
fresh.Close();
|
||||||
|
|
||||||
await Call(owner, "POST", "api/logout");
|
await Call(owner, "POST", "api/logout");
|
||||||
Check((await Call(owner, "GET", "api/status")).status == 401, "Logout ends the session");
|
Check((await Call(owner, "GET", "api/status")).status == 401, "Logout ends the session");
|
||||||
}
|
}
|
||||||
@@ -647,6 +661,7 @@ sealed class TestClient
|
|||||||
public bool? AdminStatus;
|
public bool? AdminStatus;
|
||||||
public int SaveFiles;
|
public int SaveFiles;
|
||||||
public bool GotStartGame;
|
public bool GotStartGame;
|
||||||
|
public byte[] StartGamePayload;
|
||||||
public string RejectReason;
|
public string RejectReason;
|
||||||
public Dictionary<string, byte[]> Files = new();
|
public Dictionary<string, byte[]> Files = new();
|
||||||
|
|
||||||
@@ -683,7 +698,8 @@ sealed class TestClient
|
|||||||
string fname = fr.ReadString();
|
string fname = fr.ReadString();
|
||||||
c.Files[fname] = fr.ReadBytes(fr.ReadInt32());
|
c.Files[fname] = fr.ReadBytes(fr.ReadInt32());
|
||||||
}
|
}
|
||||||
c.GotStartGame = await c.WaitFor(MsgType.StartGame, 1500) != null || c.SaveFiles > 0;
|
c.StartGamePayload = await c.WaitFor(MsgType.StartGame, 1500);
|
||||||
|
c.GotStartGame = c.StartGamePayload != null || c.SaveFiles > 0;
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user