P2P host lobby: CONTINUE own save or NEW GAME (previous save backed up to MSCO_Saves, new world saved and sent to waiting players)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,11 @@
|
|||||||
|
|
||||||
Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d --build`).
|
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/<Datum>` gesichert (die letzten 10 bleiben), mit
|
||||||
|
Sicherheitsabfrage. Mitspieler bekommen die neue Welt automatisch, sobald sie einmal gespeichert ist
|
||||||
|
|
||||||
**Mehr Sync**
|
**Mehr Sync**
|
||||||
- **Einkäufe**: gekaufte/ausgepackte Items tauchen bei allen auf (auch bei später Beitretenden)
|
- **Einkäufe**: gekaufte/ausgepackte Items tauchen bei allen auf (auch bei später Beitretenden)
|
||||||
- **Verbrauchte Items**: Gegessenes, Getrunkenes und Verbrauchtes verschwindet bei allen
|
- **Verbrauchte Items**: Gegessenes, Getrunkenes und Verbrauchtes verschwindet bei allen
|
||||||
|
|||||||
+60
-6
@@ -153,8 +153,8 @@ namespace MSCO
|
|||||||
void DrawP2P(MultiplayerManager mp)
|
void DrawP2P(MultiplayerManager mp)
|
||||||
{
|
{
|
||||||
GUILayout.Label("P2P", titleStyle);
|
GUILayout.Label("P2P", titleStyle);
|
||||||
GUILayout.Label("Host a game from your own save and invite up to " + (MultiplayerManager.MaxPlayers - 1) +
|
GUILayout.Label("Host a game and invite up to " + (MultiplayerManager.MaxPlayers - 1) +
|
||||||
" friends through Steam.", textStyle);
|
" friends through Steam - continue your own save or start a new game.", textStyle);
|
||||||
GUILayout.Space(18);
|
GUILayout.Space(18);
|
||||||
|
|
||||||
if (mp.SteamAvailable)
|
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();
|
if (GUI.Button(new Rect(r.xMax - 32 - 170, r.yMax - 28 - 42, 170, 42), "OK", primaryStyle)) mp.ClearDisconnectNotice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool newGameConfirm;
|
||||||
|
|
||||||
|
/// <summary>Host: eigenen Spielstand weiterspielen oder ein neues Spiel anlegen (mit Sicherheitsabfrage).</summary>
|
||||||
|
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)
|
void DrawSession(MultiplayerManager mp)
|
||||||
{
|
{
|
||||||
GUILayout.Label(mp.IsHost ? "Hosting" : "Connected", titleStyle);
|
GUILayout.Label(mp.IsHost ? "Hosting" : "Connected", titleStyle);
|
||||||
GUILayout.Label(mp.IsHost
|
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
|
: mp.IsDedicated || mp.IsConnectingToServer
|
||||||
? "Connected to the server - the world is loading" + (string.IsNullOrEmpty(mp.Status) ? "." : ": " + mp.Status)
|
? "Connected to the server - the world is loading" + (string.IsNullOrEmpty(mp.Status) ? "." : ": " + mp.Status)
|
||||||
: "Waiting for the host to start the game.", textStyle);
|
: "Waiting for the host to start the game.", textStyle);
|
||||||
@@ -773,12 +824,15 @@ namespace MSCO
|
|||||||
GUILayout.EndScrollView();
|
GUILayout.EndScrollView();
|
||||||
GUILayout.Space(14);
|
GUILayout.Space(14);
|
||||||
|
|
||||||
|
if (mp.IsHost)
|
||||||
|
{
|
||||||
|
DrawHostStart(mp);
|
||||||
|
GUILayout.Space(10);
|
||||||
|
}
|
||||||
|
|
||||||
GUILayout.BeginHorizontal();
|
GUILayout.BeginHorizontal();
|
||||||
if (mp.IsHost)
|
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 &&
|
if (mp.IsSteamSession && mp.Players.Count < MultiplayerManager.MaxPlayers &&
|
||||||
GUILayout.Button("INVITE FRIENDS", secondaryStyle, GUILayout.Height(42), GUILayout.Width(150)))
|
GUILayout.Button("INVITE FRIENDS", secondaryStyle, GUILayout.Height(42), GUILayout.Width(150)))
|
||||||
mp.InviteFriends();
|
mp.InviteFriends();
|
||||||
|
|||||||
@@ -88,6 +88,53 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>FSM unter einem Objekt, dessen Name "hint" enthält, mit einem Zustand, der eine Szene lädt.</summary>
|
||||||
|
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)
|
static bool HierarchyContains(Transform t, string text)
|
||||||
{
|
{
|
||||||
for (; t != null; t = t.parent)
|
for (; t != null; t = t.parent)
|
||||||
|
|||||||
+54
-2
@@ -790,11 +790,57 @@ namespace MSCO
|
|||||||
public void StartHostGame()
|
public void StartHostGame()
|
||||||
{
|
{
|
||||||
if (!IsHost || !MenuHelper.InMenu) return;
|
if (!IsHost || !MenuHelper.InMenu) return;
|
||||||
|
newGamePending = false;
|
||||||
SendSaveToWaitingPlayers();
|
SendSaveToWaitingPlayers();
|
||||||
SetStatus("Starting game...");
|
SetStatus("Starting game...");
|
||||||
MenuHelper.StartGame();
|
MenuHelper.StartGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host hat ein neues Spiel gestartet: es gibt noch keinen Spielstand zum Verschicken - erst im Spiel speichern
|
||||||
|
bool newGamePending;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Neues Spiel geladen: warten, bis die Welt steht, speichern und an die wartenden Mitspieler schicken.</summary>
|
||||||
|
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<PlayerInfo>();
|
||||||
|
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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Speichert wie die Toilette: das Spiel schickt allen FSMs das globale Event "SAVEGAME".
|
/// Speichert wie die Toilette: das Spiel schickt allen FSMs das globale Event "SAVEGAME".
|
||||||
/// Nur der Host speichert, Clients haben nur eine Kopie seines Spielstands.
|
/// Nur der Host speichert, Clients haben nur eine Kopie seines Spielstands.
|
||||||
@@ -1582,6 +1628,7 @@ namespace MSCO
|
|||||||
|
|
||||||
void SendSaveToWaitingPlayers()
|
void SendSaveToWaitingPlayers()
|
||||||
{
|
{
|
||||||
|
if (newGamePending) return; // neues Spiel: es gibt noch keinen Spielstand - kommt nach dem ersten Speichern
|
||||||
var targets = new List<PlayerInfo>();
|
var targets = new List<PlayerInfo>();
|
||||||
foreach (PlayerInfo p in players.Values)
|
foreach (PlayerInfo p in players.Values)
|
||||||
if (!p.IsLocal && !p.SaveSent) targets.Add(p);
|
if (!p.IsLocal && !p.SaveSent) targets.Add(p);
|
||||||
@@ -1630,8 +1677,12 @@ namespace MSCO
|
|||||||
nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren
|
nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren
|
||||||
if (MenuHelper.InGame)
|
if (MenuHelper.InGame)
|
||||||
{
|
{
|
||||||
// Falls der CONTINUE-Klick nicht erkannt wurde: jetzt nachholen
|
// Falls der CONTINUE-Klick nicht erkannt wurde: jetzt nachholen. Neues Spiel: erst speichern, dann senden
|
||||||
if (session != null && session.IsHost) SendSaveToWaitingPlayers();
|
if (session != null && session.IsHost)
|
||||||
|
{
|
||||||
|
if (newGamePending) StartCoroutine(NewGameSaveAndSend());
|
||||||
|
else SendSaveToWaitingPlayers();
|
||||||
|
}
|
||||||
passenger.Reset();
|
passenger.Reset();
|
||||||
localPlayer = null;
|
localPlayer = null;
|
||||||
objects.OnGameLoaded();
|
objects.OnGameLoaded();
|
||||||
@@ -1653,6 +1704,7 @@ namespace MSCO
|
|||||||
DestroyAvatar(p);
|
DestroyAvatar(p);
|
||||||
}
|
}
|
||||||
continueFsm = null;
|
continueFsm = null;
|
||||||
|
newGamePending = false;
|
||||||
passenger.Reset();
|
passenger.Reset();
|
||||||
localPlayer = null;
|
localPlayer = null;
|
||||||
objects.Reset();
|
objects.Reset();
|
||||||
|
|||||||
+48
@@ -24,6 +24,54 @@ namespace MSCO
|
|||||||
|
|
||||||
public static bool HasBackup { get { return File.Exists(Marker); } }
|
public static bool HasBackup { get { return File.Exists(Marker); } }
|
||||||
|
|
||||||
|
static string ArchiveDir { get { return Path.Combine(SaveDir, "MSCO_Saves"); } }
|
||||||
|
const int KeepArchives = 10;
|
||||||
|
|
||||||
|
/// <summary>Gibt es einen eigenen Spielstand zum Weiterspielen?</summary>
|
||||||
|
public static bool HasLocalSave { get { return File.Exists(Path.Combine(SaveDir, "defaultES2File.txt")); } }
|
||||||
|
|
||||||
|
/// <summary>Wann der eigene Spielstand zuletzt gespeichert wurde (null = keiner).</summary>
|
||||||
|
public static System.DateTime? LocalSaveTime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string file = Path.Combine(SaveDir, "defaultES2File.txt");
|
||||||
|
return File.Exists(file) ? File.GetLastWriteTime(file) : (System.DateTime?)null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ordner mit den Spielständen, die vor einem neuen Spiel gesichert wurden.</summary>
|
||||||
|
public static string ArchiveFolder { get { return ArchiveDir; } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
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<KeyValuePair<string, byte[]>> ReadLocalSave()
|
public static List<KeyValuePair<string, byte[]>> ReadLocalSave()
|
||||||
{
|
{
|
||||||
var result = new List<KeyValuePair<string, byte[]>>();
|
var result = new List<KeyValuePair<string, byte[]>>();
|
||||||
|
|||||||
Reference in New Issue
Block a user