diff --git a/CHANGELOG.md b/CHANGELOG.md index 2411f4c..133caf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d --build`). +**P2P: Weiterspielen oder neues Spiel** +- In der Host-Lobby: **CONTINUE** (eigenen Spielstand weiterspielen, mit Datum) oder **NEW GAME** (frische Welt für + alle). Der bisherige Spielstand wird vorher nach `MSCO_Saves/` gesichert (die letzten 10 bleiben), mit + Sicherheitsabfrage. Mitspieler bekommen die neue Welt automatisch, sobald sie einmal gespeichert ist + **Mehr Sync** - **Einkäufe**: gekaufte/ausgepackte Items tauchen bei allen auf (auch bei später Beitretenden) - **Verbrauchte Items**: Gegessenes, Getrunkenes und Verbrauchtes verschwindet bei allen diff --git a/LobbyUI.cs b/LobbyUI.cs index 7c4352d..a813b4e 100644 --- a/LobbyUI.cs +++ b/LobbyUI.cs @@ -153,8 +153,8 @@ namespace MSCO void DrawP2P(MultiplayerManager mp) { GUILayout.Label("P2P", titleStyle); - GUILayout.Label("Host a game from your own save and invite up to " + (MultiplayerManager.MaxPlayers - 1) + - " friends through Steam.", textStyle); + GUILayout.Label("Host a game and invite up to " + (MultiplayerManager.MaxPlayers - 1) + + " friends through Steam - continue your own save or start a new game.", textStyle); GUILayout.Space(18); if (mp.SteamAvailable) @@ -747,11 +747,62 @@ namespace MSCO if (GUI.Button(new Rect(r.xMax - 32 - 170, r.yMax - 28 - 42, 170, 42), "OK", primaryStyle)) mp.ClearDisconnectNotice(); } + bool newGameConfirm; + + /// Host: eigenen Spielstand weiterspielen oder ein neues Spiel anlegen (mit Sicherheitsabfrage). + void DrawHostStart(MultiplayerManager mp) + { + System.DateTime? saved = SaveSync.LocalSaveTime; + if (newGameConfirm) + { + GUILayout.BeginVertical(rowBox); + GUILayout.Space(6); + GUILayout.Label("START A NEW GAME?", captionStyle); + GUILayout.Label(saved != null + ? "Your current save game is kept as a backup in MSCO_Saves - everyone starts in a fresh world." + : "Everyone starts in a fresh world.", textStyle); + GUILayout.Space(6); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("YES, NEW GAME", dangerStyle, GUILayout.Height(38), GUILayout.Width(170))) + { + newGameConfirm = false; + mp.StartHostNewGame(); + } + GUILayout.Space(8); + if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(38), GUILayout.Width(110))) newGameConfirm = false; + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(6); + GUILayout.EndVertical(); + return; + } + + GUILayout.BeginHorizontal(); + GUI.enabled = saved != null; + GUILayout.BeginVertical(GUILayout.Width(190)); + if (GUILayout.Button("CONTINUE", primaryStyle, GUILayout.Height(42), GUILayout.Width(190))) + mp.StartHostGame(); + GUILayout.Label(saved != null ? "saved " + saved.Value.ToString("dd.MM.yyyy HH:mm") : "no save game yet", captionStyle); + GUILayout.EndVertical(); + GUI.enabled = true; + GUILayout.Space(10); + GUILayout.BeginVertical(GUILayout.Width(190)); + if (GUILayout.Button("NEW GAME", secondaryStyle, GUILayout.Height(42), GUILayout.Width(190))) + { + if (saved != null) newGameConfirm = true; + else mp.StartHostNewGame(); + } + GUILayout.Label("fresh world for everyone", captionStyle); + GUILayout.EndVertical(); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } + void DrawSession(MultiplayerManager mp) { GUILayout.Label(mp.IsHost ? "Hosting" : "Connected", titleStyle); GUILayout.Label(mp.IsHost - ? "Your game is open. Start when everyone has joined." + ? "Your game is open. Continue your save or start a new game - friends can also join later." : mp.IsDedicated || mp.IsConnectingToServer ? "Connected to the server - the world is loading" + (string.IsNullOrEmpty(mp.Status) ? "." : ": " + mp.Status) : "Waiting for the host to start the game.", textStyle); @@ -773,12 +824,15 @@ namespace MSCO GUILayout.EndScrollView(); GUILayout.Space(14); + if (mp.IsHost) + { + DrawHostStart(mp); + GUILayout.Space(10); + } + GUILayout.BeginHorizontal(); if (mp.IsHost) { - if (GUILayout.Button("START GAME", primaryStyle, GUILayout.Height(42), GUILayout.Width(150))) - mp.StartHostGame(); - GUILayout.Space(10); if (mp.IsSteamSession && mp.Players.Count < MultiplayerManager.MaxPlayers && GUILayout.Button("INVITE FRIENDS", secondaryStyle, GUILayout.Height(42), GUILayout.Width(150))) mp.InviteFriends(); diff --git a/MenuHelper.cs b/MenuHelper.cs index 7e7fe86..ea306e0 100644 --- a/MenuHelper.cs +++ b/MenuHelper.cs @@ -88,6 +88,53 @@ namespace MSCO } } + /// + /// Neues Spiel starten (der Spielstand wurde vorher geleert): über die Logik des NEW-GAME-Knopfs des Spiels, + /// sonst wie CONTINUE - ohne Spielstand legt das Spiel dann eine frische Welt an. + /// + public static void StartNewGame() + { + PlayMakerFSM fsm; + FsmState state; + if (switchState != null && FindLoadState("newgame", out fsm, out state)) + { + MscoLog.Info("[MSCO] Starting a new game via NEW GAME (" + fsm.name + " / " + state.Name + ")"); + switchState.Invoke(fsm.Fsm, new object[] { state }); + return; + } + MscoLog.Info("[MSCO] NEW GAME button not found - starting with an empty save"); + StartGame(); + } + + /// FSM unter einem Objekt, dessen Name "hint" enthält, mit einem Zustand, der eine Szene lädt. + static bool FindLoadState(string hint, out PlayMakerFSM fsm, out FsmState state) + { + fsm = null; + state = null; + // Nur aktive Objekte der Menü-Szene (die Spiel-Knöpfe sind nur unsichtbar, nicht abgeschaltet) + foreach (Object o in Object.FindObjectsOfType(typeof(PlayMakerFSM))) + { + var candidate = (PlayMakerFSM)o; + if (!HierarchyContains(candidate.transform, hint)) continue; + foreach (FsmState s in candidate.FsmStates) + { + if (!s.ActionsLoaded) s.LoadActions(); + if (s.Actions == null) continue; + foreach (FsmStateAction action in s.Actions) + { + var load = action as LoadLevel; + if (load != null && load.levelName != null && !string.IsNullOrEmpty(load.levelName.Value)) + { + fsm = candidate; + state = s; + return true; + } + } + } + } + return false; + } + static bool HierarchyContains(Transform t, string text) { for (; t != null; t = t.parent) diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index 15a1c09..618f3b6 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -790,11 +790,57 @@ namespace MSCO public void StartHostGame() { if (!IsHost || !MenuHelper.InMenu) return; + newGamePending = false; SendSaveToWaitingPlayers(); SetStatus("Starting game..."); MenuHelper.StartGame(); } + // Host hat ein neues Spiel gestartet: es gibt noch keinen Spielstand zum Verschicken - erst im Spiel speichern + bool newGamePending; + + /// + /// Host: neues Spiel statt des eigenen Spielstands. Der bisherige Spielstand wird gesichert (MSCO_Saves), die + /// Welt frisch angelegt; die Mitspieler bekommen sie, sobald sie einmal gespeichert ist. + /// + public void StartHostNewGame() + { + if (!IsHost || !MenuHelper.InMenu) return; + string archived; + try { archived = SaveSync.ArchiveForNewGame(); } + catch (System.Exception e) + { + // Lieber gar nicht starten, als den Spielstand ungesichert zu löschen + MscoLog.Error("[MSCO] Could not back up your save game - new game cancelled: " + e.Message); + SetStatus("Could not back up your save game - new game cancelled"); + return; + } + newGamePending = true; + if (archived != null) overlays.Log("Your previous save game was backed up to " + archived); + SetStatus("Starting a new game..."); + MenuHelper.StartNewGame(); + } + + /// Neues Spiel geladen: warten, bis die Welt steht, speichern und an die wartenden Mitspieler schicken. + System.Collections.IEnumerator NewGameSaveAndSend() + { + float give = Time.realtimeSinceStartup + 60f; + while (localPlayer == null && Time.realtimeSinceStartup < give) yield return new WaitForSeconds(0.5f); + yield return new WaitForSeconds(3f); // Spielwelt erst fertig aufbauen lassen + if (!newGamePending || session == null || !session.IsHost || !MenuHelper.InGame) yield break; + SaveGame(); + yield return new WaitForSeconds(LateJoinSaveDelay); + newGamePending = false; + var targets = new List(); + foreach (PlayerInfo p in players.Values) + if (!p.IsLocal && !p.SaveSent) targets.Add(p); + if (targets.Count > 0) + { + SendSave(targets, true); + SetStatus("New game sent to " + targets.Count + " player(s)"); + } + } + /// /// Speichert wie die Toilette: das Spiel schickt allen FSMs das globale Event "SAVEGAME". /// Nur der Host speichert, Clients haben nur eine Kopie seines Spielstands. @@ -1582,6 +1628,7 @@ namespace MSCO void SendSaveToWaitingPlayers() { + if (newGamePending) return; // neues Spiel: es gibt noch keinen Spielstand - kommt nach dem ersten Speichern var targets = new List(); foreach (PlayerInfo p in players.Values) if (!p.IsLocal && !p.SaveSent) targets.Add(p); @@ -1630,8 +1677,12 @@ namespace MSCO nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren if (MenuHelper.InGame) { - // Falls der CONTINUE-Klick nicht erkannt wurde: jetzt nachholen - if (session != null && session.IsHost) SendSaveToWaitingPlayers(); + // Falls der CONTINUE-Klick nicht erkannt wurde: jetzt nachholen. Neues Spiel: erst speichern, dann senden + if (session != null && session.IsHost) + { + if (newGamePending) StartCoroutine(NewGameSaveAndSend()); + else SendSaveToWaitingPlayers(); + } passenger.Reset(); localPlayer = null; objects.OnGameLoaded(); @@ -1653,6 +1704,7 @@ namespace MSCO DestroyAvatar(p); } continueFsm = null; + newGamePending = false; passenger.Reset(); localPlayer = null; objects.Reset(); diff --git a/SaveSync.cs b/SaveSync.cs index 135f644..29192ea 100644 --- a/SaveSync.cs +++ b/SaveSync.cs @@ -24,6 +24,54 @@ namespace MSCO public static bool HasBackup { get { return File.Exists(Marker); } } + static string ArchiveDir { get { return Path.Combine(SaveDir, "MSCO_Saves"); } } + const int KeepArchives = 10; + + /// Gibt es einen eigenen Spielstand zum Weiterspielen? + public static bool HasLocalSave { get { return File.Exists(Path.Combine(SaveDir, "defaultES2File.txt")); } } + + /// Wann der eigene Spielstand zuletzt gespeichert wurde (null = keiner). + public static System.DateTime? LocalSaveTime + { + get + { + string file = Path.Combine(SaveDir, "defaultES2File.txt"); + return File.Exists(file) ? File.GetLastWriteTime(file) : (System.DateTime?)null; + } + } + + /// Ordner mit den Spielständen, die vor einem neuen Spiel gesichert wurden. + public static string ArchiveFolder { get { return ArchiveDir; } } + + /// + /// Neues Spiel: den bisherigen Spielstand nach MSCO_Saves/Datum kopieren (nichts geht verloren) und die + /// Spielstand-Dateien leeren, damit das Spiel eine frische Welt anlegt. Gibt den Sicherungsordner zurück (oder null). + /// + public static string ArchiveForNewGame() + { + // War noch der Spielstand eines anderen Hosts aktiv, erst den eigenen zurückholen + if (HasBackup) RestoreBackup(); + string target = null; + if (HasLocalSave) + { + target = Path.Combine(ArchiveDir, System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")); + Directory.CreateDirectory(target); + foreach (string name in Files) + { + string path = Path.Combine(SaveDir, name); + if (File.Exists(path)) File.Copy(path, Path.Combine(target, name), true); + } + MscoLog.Info("[MSCO] Your previous save game was backed up to " + target); + // Nur die letzten Sicherungen behalten + string[] all = Directory.GetDirectories(ArchiveDir); + System.Array.Sort(all); + for (int i = 0; i < all.Length - KeepArchives; i++) + try { Directory.Delete(all[i], true); } catch { } + } + DeleteSaveFiles(); + return target; + } + public static List> ReadLocalSave() { var result = new List>();