diff --git a/CHANGELOG.md b/CHANGELOG.md index fff0003..19f787c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,16 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d - **Verkehr**: NPC-Autos fahren bei allen an derselben Stelle (der erste Spieler simuliert, die anderen folgen) - `vehicles` in der F7-Konsole zeigt, wie viele Karosserie-Meshes, Job-Werte und Verkehrsautos synchronisiert werden +**Trennen & Neu-Verbinden** +- Gekickt, gebannt, Server beendet, Verbindung verloren: eigenes Fenster in der Bildmitte mit **Grund** und OK + (im Spiel wird der Grund vorher 2,5 s groß angezeigt) +- `reset-world` / `reload-save`: Fenster „Reconnecting“ – verbindet automatisch neu, bis zu 5 Versuche (falls der + Server gerade neu startet), mit CANCEL-Knopf + +**Behoben** +- Beim Neuladen der Welt wurden herumliegende Objekte als „verbraucht“ gemeldet und bei allen gelöscht. Jetzt werden + nur einzelne echte Items gemeldet, Fahrzeuge/Teile nie gelöscht, und der Server nimmt höchstens 20 pro Minute an + **Leistung** - Mod: herumliegende Objekte werden nur noch jedes 4. Frame geprüft (eigene/gehaltene/gefahrene weiterhin jedes Frame), weniger Speicher-Allokationen pro Frame, Gelenke fremd gefahrener Autos seltener geprüft diff --git a/LobbyUI.cs b/LobbyUI.cs index 4c1beb8..c9fd035 100644 --- a/LobbyUI.cs +++ b/LobbyUI.cs @@ -73,7 +73,10 @@ namespace MSCO bool pendingJoin = mp.PendingJoinHost != null && !mp.IsInSession; bool versionWarning = mp.PendingVersionWarning != null && !mp.IsInSession; bool disconnected = mp.DisconnectNotice != null && !mp.IsInSession; - if (pendingJoin || versionWarning || disconnected) minimized = false; // JOIN-Link / Warnung / Trennung: Fenster zeigen + // Getrennt / Neu-Verbinden: eigenes Fenster in der Bildmitte, alles andere gesperrt + if (disconnected) { DrawPrompt(mp, false); return; } + if (mp.IsReconnecting) { DrawPrompt(mp, true); return; } + if (pendingJoin || versionWarning) minimized = false; // JOIN-Link / Warnung: Fenster zeigen if (minimized) { DrawMinimized(mp); return; } var window = new Rect((Screen.width - Width) / 2f, (Screen.height - Height) / 2f, Width, Height); @@ -85,10 +88,8 @@ namespace MSCO var content = new Rect(window.x + SidebarWidth + 32, window.y + 28, Width - SidebarWidth - 64, Height - StatusHeight - 52); GUILayout.BeginArea(content); - if (disconnected) DrawDisconnectNotice(mp); - else if (versionWarning) DrawVersionWarning(mp); + if (versionWarning) DrawVersionWarning(mp); else if (pendingJoin) DrawJoinConfirm(mp); - else if (mp.IsReconnecting) DrawReconnecting(mp); else if (mp.IsInSession) DrawSession(mp); else if (page == Page.P2P) DrawP2P(mp); else DrawServerBrowser(mp); @@ -486,29 +487,54 @@ namespace MSCO GUILayout.EndHorizontal(); } - /// Nach dem Trennen (Kick, Ban, Shutdown, Timeout ...): Grund anzeigen. - void DrawDisconnectNotice(MultiplayerManager mp) - { - GUILayout.Label("Disconnected", titleStyle); - GUILayout.Space(8); - GUILayout.BeginVertical(rowBox); - GUILayout.Space(6); - GUILayout.Label("REASON", captionStyle); - GUILayout.Label(mp.DisconnectNotice, rowNameStyle); - GUILayout.Space(6); - GUILayout.EndVertical(); - GUILayout.Space(8); - GUILayout.Label("Your own save game has been restored.", textStyle); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("OK", primaryStyle, GUILayout.Height(42), GUILayout.Width(170))) mp.ClearDisconnectNotice(); - } + const float PromptWidth = 540f; + const float PromptHeight = 270f; + Texture2D texDim, texDanger; + GUIStyle promptTextStyle; - void DrawReconnecting(MultiplayerManager mp) + /// + /// Fenster in der Bildmitte über abgedunkeltem Hintergrund: nach dem Trennen (Kick, Ban, Shutdown, Timeout ...) + /// mit Grund und OK, bzw. beim automatischen Neu-Verbinden nach reset-world mit Versuch-Anzeige und CANCEL. + /// + void DrawPrompt(MultiplayerManager mp, bool reconnecting) { - GUILayout.Label("Reconnecting", titleStyle); - GUILayout.Space(8); - GUILayout.Label(mp.ReconnectMessage, rowNameStyle); - GUILayout.Label("You will be connected to the server again in a moment.", textStyle); + if (texDim == null) texDim = Solid(new Color(0f, 0f, 0f, 0.6f)); + if (texDanger == null) texDanger = Solid(Hex(0xe5, 0x48, 0x4d)); + if (promptTextStyle == null) + { + promptTextStyle = new GUIStyle(rowNameStyle); + promptTextStyle.wordWrap = true; + promptTextStyle.fontSize = 15; + } + GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), texDim); + + var r = new Rect((Screen.width - PromptWidth) / 2f, (Screen.height - PromptHeight) / 2f, PromptWidth, PromptHeight); + ScreenRect = new Rect(0, 0, Screen.width, Screen.height); // Spielmenü dahinter komplett sperren + GUI.Box(r, GUIContent.none, windowBox); + string title = reconnecting ? "RECONNECTING" : mp.DisconnectTitle; + bool bad = !reconnecting && title != "DISCONNECTED"; + GUI.DrawTexture(new Rect(r.x, r.y, r.width, 4), bad ? texDanger : texAccent); + + float x = r.x + 32, w = r.width - 64, y = r.y + 28; + GUI.Label(new Rect(x, y, w, 34), title, titleStyle); + y += 50; + GUI.Label(new Rect(x, y, w, 20), "REASON", captionStyle); + + if (reconnecting) + { + GUI.Label(new Rect(x, y + 22, w, 44), mp.ReconnectMessage ?? "", promptTextStyle); + string attempt = mp.ReconnectAttempt == 0 + ? "Connecting again in a moment..." + : "Attempt " + mp.ReconnectAttempt + " of " + mp.ReconnectMaxAttempts + + (string.IsNullOrEmpty(mp.Status) ? "" : " - " + mp.Status); + GUI.Label(new Rect(x, y + 74, w, 40), attempt, textStyle); + if (GUI.Button(new Rect(r.xMax - 32 - 170, r.yMax - 28 - 42, 170, 42), "CANCEL", secondaryStyle)) mp.CancelReconnect(); + return; + } + + GUI.Label(new Rect(x, y + 22, w, 60), mp.DisconnectNotice ?? "", promptTextStyle); + GUI.Label(new Rect(x, y + 88, w, 20), "Your own save game has been restored.", textStyle); + if (GUI.Button(new Rect(r.xMax - 32 - 170, r.yMax - 28 - 42, 170, 42), "OK", primaryStyle)) mp.ClearDisconnectNotice(); } void DrawSession(MultiplayerManager mp) diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index 87029c0..d9d3ed2 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -594,6 +594,9 @@ namespace MSCO string centerMessage; // große Meldung in der Bildmitte float centerMessageUntil; float reconnectAt; + int reconnectAttempts; + const int MaxReconnectAttempts = 5; + const float ReconnectRetrySeconds = 6f; void ConnectToServer(string host, int port, string displayName, int maxPlayers) { @@ -817,14 +820,22 @@ namespace MSCO steam.Tick(); - // Nach reset-world o.ä.: im Hauptmenü automatisch wieder mit demselben Server verbinden - if (reconnectMessage != null && session == null && MenuHelper.InMenu && Time.realtimeSinceStartup >= reconnectAt) + // Nach reset-world o.ä.: im Hauptmenü automatisch wieder mit demselben Server verbinden. Mehrere Versuche - + // der Server kann nach einem Neustart noch ein paar Sekunden brauchen. + if (reconnectMessage != null && session == null && MenuHelper.InMenu && !checkingServer + && Time.realtimeSinceStartup >= reconnectAt) { - string message = reconnectMessage; - reconnectMessage = null; - if (lastServerHost != null) + if (lastServerHost == null || reconnectAttempts >= MaxReconnectAttempts) { - SetStatus(message); + DisconnectNotice = "Could not reconnect to the server after " + reconnectAttempts + " attempts. " + + "Try again from the server browser."; + reconnectMessage = null; + reconnectAttempts = 0; + } + else + { + reconnectAttempts++; + reconnectAt = Time.realtimeSinceStartup + ReconnectRetrySeconds; JoinServer(lastServerHost, lastServerPort, lastServerName, lastServerMax); } } @@ -909,6 +920,30 @@ namespace MSCO public string DisconnectNotice { get; private set; } public void ClearDisconnectNotice() { DisconnectNotice = null; } public bool IsReconnecting { get { return reconnectMessage != null && session == null; } } + public int ReconnectAttempt { get { return reconnectAttempts; } } + public int ReconnectMaxAttempts { get { return MaxReconnectAttempts; } } + + /// Automatisches Neu-Verbinden abbrechen (Knopf im Reconnect-Fenster). + public void CancelReconnect() + { + reconnectMessage = null; + reconnectAttempts = 0; + SetStatus("Reconnect cancelled"); + } + + /// Überschrift für das Trennungs-Fenster. + public string DisconnectTitle + { + get + { + string r = (DisconnectNotice ?? "").ToLower(); + if (r.Contains("banned")) return "BANNED"; + if (r.Contains("kick")) return "KICKED"; + if (r.Contains("shutting down") || r.Contains("server stopped") || r.Contains("shut down")) return "SERVER CLOSED"; + if (r.Contains("could not reconnect")) return "RECONNECT FAILED"; + return "DISCONNECTED"; + } + } bool worldSynced; // Server: Weltzustand angewendet @@ -1120,6 +1155,8 @@ namespace MSCO AddPlayer(id, r.ReadString(), id == localId); } handshakeDone = true; + reconnectMessage = null; // (wieder) verbunden + reconnectAttempts = 0; SetStatus(dedicated ? "Connected to server - loading..." : "Connected - waiting for host to start..."); break; } @@ -1127,8 +1164,17 @@ namespace MSCO case MsgType.Reject: { string why = r.ReadString(); - serverReason = why; // für den Hinweis im Hauptmenü (Kick, Ban, Server voll, Shutdown ...) + serverReason = why; // für das Fenster im Hauptmenü (Kick, Ban, Server voll, Shutdown ...) + if (why.ToLower().Contains("banned")) reconnectMessage = null; // gebannt: nicht weiter versuchen session.Close((dedicated || connectingToServer ? "Rejected by server: " : "Rejected by host: ") + why); + if (MenuHelper.InGame) + { + // Im Spiel: Grund erst groß anzeigen, dann ins Hauptmenü (dort kommt das Fenster mit OK) + centerMessage = why; + centerMessageUntil = Time.realtimeSinceStartup + 3f; + reconnectHoldUntil = Time.realtimeSinceStartup + 2.5f; + return; + } OnSessionEnded(session.CloseReason); break; } diff --git a/ObjectSync.cs b/ObjectSync.cs index 5052a90..2c86afc 100644 --- a/ObjectSync.cs +++ b/ObjectSync.cs @@ -16,6 +16,7 @@ namespace MSCO public int Id; public string Path; public Rigidbody Rb; + public string Name; public GameObject Go; // zum Unterscheiden: nur Rigidbody weg (angebaut) oder ganzes Objekt weg (verbraucht) public Vector3 LastPos; public bool IsVehicle; @@ -101,6 +102,23 @@ namespace MSCO public void MarkNetworkCreated(GameObject go) { if (go != null) networkCreated.Add(go); } + /// + /// Verschwundene Items melden (gegessen, getrunken, verbraucht) - nur wenn das ganze Objekt weg ist (nicht nur der + /// Rigidbody wie beim Anbauen), es ein Item ist und es bei uns passiert ist. Verschwinden viele auf einmal, wird + /// gerade die Szene entladen (Neu laden, Hauptmenü) - dann NICHTS melden, sonst löscht man bei allen die Welt. + /// + void ReportConsumed(Transform player) + { + if (ItemDestroyed == null || removeList.Count == 0) return; + if (player == null || removeList.Count > 2 || Time.time - loadTime < 30f) return; + foreach (SyncedBody b in removeList) + { + if (b.Go != null || b.IsVehicle || b.Name == null || !b.Name.EndsWith("(itemx)")) continue; + if (IsLocal(b) || (b.LastPos - lastPlayerPos).sqrMagnitude < LocalEventRange * LocalEventRange) + ItemDestroyed(b); + } + } + /// Gibt es dieses Objekt bei uns schon an dieser Stelle? (dann nicht nochmal erzeugen) public bool HasNear(int id, Vector3 pos, float range) { @@ -113,6 +131,8 @@ namespace MSCO { SyncedBody b; if (!byId.TryGetValue(id, out b)) return false; + // Nur echte Items (Essen, Getränke ...) - nie Fahrzeuge oder Teile, egal was ein anderer meldet + if (b.IsVehicle || b.Name == null || !b.Name.EndsWith("(itemx)")) return false; if (b.Go != null) UnityEngine.Object.Destroy(b.Go); Unregister(b); return true; @@ -307,11 +327,6 @@ namespace MSCO if (b.OwnerId == SyncedBody.NoOwner && ((b.Id ^ frame) & 3) != 0 && b != held && b != driven) continue; if (b.Rb == null) { - // Ganzes Objekt weg (gegessen, getrunken, verbraucht) - nicht nur der Rigidbody (Teil angebaut). - // Nur melden, wenn es bei uns passiert ist: wir besaßen es oder standen daneben. - if (b.Go == null && !b.IsVehicle && ItemDestroyed != null - && (IsLocal(b) || (b.LastPos - lastPlayerPos).sqrMagnitude < LocalEventRange * LocalEventRange)) - ItemDestroyed(b); removeList.Add(b); continue; } @@ -350,6 +365,7 @@ namespace MSCO else if (Time.time - b.LastRecvTime > RemoteTimeout) SetFree(b); } } + ReportConsumed(player); foreach (SyncedBody b in removeList) Unregister(b); Held = held != null && held.Rb != null && IsLocal(held) ? held : null; @@ -895,6 +911,7 @@ namespace MSCO Path = path, Rb = c.Key, Go = c.Key.gameObject, + Name = c.Key.gameObject.name, LastPos = c.Key.position, IsVehicle = c.Value, RootName = c.Key.transform.root.name @@ -906,7 +923,7 @@ namespace MSCO // Neues Item nach dem ersten Suchlauf, bei uns in der Nähe und nicht übers Netz entstanden: // wahrscheinlich gekauft / ausgepackt / geliefert - den anderen melden bool isItem = !b.IsVehicle && b.Go.name.EndsWith("(itemx)"); - if (ScanComplete && isItem && ItemAppeared != null && !networkCreated.Contains(b.Go) + if (ScanComplete && isItem && ItemAppeared != null && !networkCreated.Contains(b.Go) && Time.time - loadTime > 30f && (b.LastPos - lastPlayerPos).sqrMagnitude < LocalEventRange * LocalEventRange) ItemAppeared(b); }