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);
}