diff --git a/CHANGELOG.md b/CHANGELOG.md index c48e1e6..1f70983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d --build`). +**Weniger Hänger** +- Der Server holt sich den Spielstand nur noch, wenn sich seit dem letzten etwas geändert hat (Speichern lässt das Spiel + kurz hängen), Standard für neue Server alle 10 statt 5 Minuten +- Die teure Suche über alle PlayMaker-FSMs läuft seltener; die Schadens-Prüfung liest ab und zu nur noch ein Mesh +- Hänger-Erkennung: dauert ein Bild länger als 0,2 s, steht in der F7-Konsole, ob MSCO schuld war (und welcher Teil) + oder das Spiel selbst (z.B. beim Speichern) + **Behoben: Öl lief aus dem Satsuma** - Beim Beitreten lief Öl aus, obwohl der Spielstand lokal dicht war: die vom Spiel selbst berechneten Werte `LeakOilpan`, `LeakOilfilter`, `LeakRockerCover`, `LeakHeadgasket` (und Raten wie `…LeakRate`) wurden wie echte diff --git a/ControlSync.cs b/ControlSync.cs index 06a868c..40b947b 100644 --- a/ControlSync.cs +++ b/ControlSync.cs @@ -129,7 +129,7 @@ namespace MSCO void Scan() { int before = controls.Count; - foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f)) + foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f)) { if (fsm == null || !scanned.Add(fsm)) continue; string root = fsm.transform.root.name; diff --git a/DamageSync.cs b/DamageSync.cs index b15dde4..a0d6607 100644 --- a/DamageSync.cs +++ b/DamageSync.cs @@ -17,7 +17,7 @@ namespace MSCO const float CheckInterval = 0.5f; // so oft nach einem Aufprall prüfen const float ImpactWindow = 4f; // so lange nach einem Aufprall prüfen (Verformung läuft ein paar Frames) const float ImpactDeltaV = 2.5f; // plötzliche Geschwindigkeitsänderung (m/s pro Frame) = Aufprall - const float SafetyCheckInterval = 30f; // sonst nur selten (z.B. Reparatur in der Werkstatt) + const float SafetyCheckInterval = 2f; // sonst nur ab und zu EIN Mesh (z.B. Reparatur in der Werkstatt) - verteilt statt alle auf einmal const float MinMove = 0.002f; const int MaxPerMessage = 4000; @@ -36,6 +36,7 @@ namespace MSCO readonly HashSet seen = new HashSet(); readonly List changed = new List(); float nextScan, nextCheck, nextSafetyCheck; + int safetyIndex; // Mesh-Daten lesen ist teuer (große Arrays, Speicher-Müll) - nur nach einem Aufprall readonly Dictionary lastVelocity = new Dictionary(); readonly Dictionary lastImpact = new Dictionary(); @@ -76,16 +77,19 @@ namespace MSCO if (Time.time < nextCheck) return; nextCheck = Time.time + CheckInterval; bool safety = Time.time >= nextSafetyCheck; - if (safety) nextSafetyCheck = Time.time + SafetyCheckInterval; + if (safety) { nextSafetyCheck = Time.time + SafetyCheckInterval; safetyIndex++; } + int index = -1; foreach (DamageMesh m in meshes.Values) { + index++; if (m.Filter == null) continue; + bool safetyThis = safety && meshes.Count > 0 && index == safetyIndex % meshes.Count; int owner = objects.VehicleOwner(m.Root); if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt die Schäden float impact; bool recentImpact = m.Body != null && lastImpact.TryGetValue(m.Body, out impact) && Time.time - impact < ImpactWindow; - if (!recentImpact && !safety) continue; + if (!recentImpact && !safetyThis) continue; Mesh mesh = m.Filter.sharedMesh; if (mesh == null) continue; diff --git a/DoorSync.cs b/DoorSync.cs index b331a18..b3c5d0a 100644 --- a/DoorSync.cs +++ b/DoorSync.cs @@ -96,7 +96,7 @@ namespace MSCO void Scan() { - foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f)) + foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f)) { if (fsm == null) continue; string root = fsm.transform.root.name; diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index dcfb7e6..3ab8cfc 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -183,6 +183,7 @@ namespace MSCO public void Host(int port) { Disconnect(); + PartStart(); try { session = NetSession.StartHost(port); @@ -944,6 +945,7 @@ namespace MSCO return; } savingMessageUntil = Time.realtimeSinceStartup + 2.5f; // "SAVING GAME..." in der Bildschirmmitte + lastSaveTriggered = Time.realtimeSinceStartup; PlayMakerFSM.BroadcastEvent("SAVEGAME"); MscoLog.Info("[MSCO] Game saved."); } @@ -952,6 +954,7 @@ namespace MSCO void Update() { + CheckHitch(); MeasureFps(); // Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog) // Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht; @@ -1866,62 +1869,89 @@ namespace MSCO if (sendTimer >= SendInterval) { sendTimer = 0f; + PartStart(); try { SendGame(MsgType.Player, BuildLocalState().Serialize()); } catch (System.Exception e) { ReportError("player state", e); } + PartEnd("player state"); } // Eigene Bedürfnisse (Hunger, Durst ...) pro Spieler auf dem Server sichern if (dedicated && worldSynced && Time.realtimeSinceStartup >= nextNeedsSend) { nextNeedsSend = Time.realtimeSinceStartup + NeedsInterval; + PartStart(); try { byte[] needs = PlayerNeeds.Build(); if (needs != null) session.SendTo(NetSession.HostId, MsgType.PlayerNeeds, needs); } catch (System.Exception e) { ReportError("player needs", e); } + PartEnd("player needs"); } + PartStart(); try { objects.Update(localPlayer, Time.deltaTime); } catch (System.Exception e) { ReportError("object sync", e); } + PartStart(); + PartEnd("object sync"); try { vehicleFx.Update(objects, SendGame, Time.deltaTime, IsWorldAuthority); } catch (System.Exception e) { ReportError("vehicle lights/sound", e); } + PartStart(); + PartEnd("vehicle lights/sound"); try { worldSync.Update(IsWorldAuthority, SendGame, Time.unscaledDeltaTime); } catch (System.Exception e) { ReportError("time/weather sync", e); } + PartEnd("time/weather sync"); // Geld erst beobachten, wenn das Spiel fertig geladen ist + PartStart(); try { if (objects.ScanComplete) moneySync.Begin(); moneySync.Update(SendGame); } catch (System.Exception e) { ReportError("money sync", e); } + PartEnd("money sync"); // Türen, Tore, Hauben (z.B. Garagentor) + PartStart(); try { if (objects.ScanComplete) doorSync.Update(SendGame); } catch (System.Exception e) { ReportError("door sync", e); } + PartEnd("door sync"); // Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...) + PartStart(); try { if (objects.ScanComplete) vehicleState.Update(objects, localId, SendGame); } catch (System.Exception e) { ReportError("vehicle state sync", e); } + PartEnd("vehicle state sync"); // Unfallschäden (verbeulte Karosserie) + PartStart(); try { if (objects.ScanComplete) damageSync.Update(objects, localId, SendGame); } catch (System.Exception e) { ReportError("crash damage sync", e); } + PartEnd("crash damage sync"); // Zündung, Motor, Licht, Choke ... der Fahrzeuge + PartStart(); try { if (objects.ScanComplete && localPlayer != null) controls.Update(objects, localId, localPlayer.position, SendGame); } catch (System.Exception e) { ReportError("vehicle controls sync", e); } + PartEnd("vehicle controls sync"); // Türen, Motorhaube, Kofferraum der Fahrzeuge + PartStart(); try { if (objects.ScanComplete && localPlayer != null) hinges.Update(objects, localId, localPlayer.position, Time.deltaTime, SendGame); } catch (System.Exception e) { ReportError("car door sync", e); } + PartEnd("car door sync"); // Jobs, Aufträge, Laden + PartStart(); try { if (objects.ScanComplete) worldVars.Update(SendGame); } catch (System.Exception e) { ReportError("jobs/shop sync", e); } + PartEnd("jobs/shop sync"); // Verkehr: die Welt-Autorität simuliert, alle anderen fahren nach + PartStart(); try { if (objects.ScanComplete) traffic.Update(IsWorldAuthority, Time.deltaTime, SendGame); } catch (System.Exception e) { ReportError("traffic sync", e); } + PartEnd("traffic sync"); } foreach (PlayerInfo p in players.Values) { if (p.Avatar == null) continue; + PartStart(); try { // Sitzt der Mitspieler in einem Fahrzeug, die Figur an unsere Kopie dieses Fahrzeugs hängen @@ -1937,6 +1967,7 @@ namespace MSCO objects.ApplyAvatarHold(p.Id, p.Avatar.HeldId, pos, rot); } catch (System.Exception e) { ReportError("player figure", e); } + PartEnd("player figure"); } // Abgefangene Schraub-/Montage-Aktionen verschicken @@ -1947,12 +1978,52 @@ namespace MSCO foreach (FsmEventMsg msg in FsmSync.Outbox) SendGame(MsgType.FsmEvent, msg.Serialize()); } catch (System.Exception e) { ReportError("part/bolt sync", e); } + PartEnd("part/bolt sync"); FsmSync.Outbox.Clear(); } readonly Dictionary lastErrorReport = new Dictionary(); /// Fehler eines Teilsystems melden (höchstens alle 30 s pro Teilsystem, damit das Log nicht vollläuft). + // ---------------------------------------------------------------- Hänger erkennen + // Jeder Sync-Teil wird gemessen. Dauert ein Bild sehr lange, steht in der F7-Konsole, ob MSCO schuld war + // (und welcher Teil) oder das Spiel selbst (z.B. beim Speichern). + readonly System.Diagnostics.Stopwatch partWatch = System.Diagnostics.Stopwatch.StartNew(); + long partStart; + double frameMsco, slowestMs; + string slowestPart; + float nextHitchReport, lastSaveTriggered = -100f; + const float HitchSeconds = 0.2f; + + void PartStart() { partStart = partWatch.ElapsedTicks; } + + void PartEnd(string name) + { + double ms = (partWatch.ElapsedTicks - partStart) * 1000.0 / System.Diagnostics.Stopwatch.Frequency; + frameMsco += ms; + if (ms > slowestMs) { slowestMs = ms; slowestPart = name; } + } + + /// Zu Beginn jedes Bilds: war das letzte Bild ein Hänger? Dann Ursache melden (höchstens alle 20 s). + void CheckHitch() + { + float dt = Time.unscaledDeltaTime; + if (dt > HitchSeconds && MenuHelper.InGame && Time.realtimeSinceStartup >= nextHitchReport) + { + nextHitchReport = Time.realtimeSinceStartup + 20f; + int total = Mathf.RoundToInt(dt * 1000f); + if (frameMsco > dt * 1000f * 0.4f) + MscoLog.Warning("[MSCO] Hitch " + total + " ms - caused by MSCO: " + slowestPart + " " + slowestMs.ToString("0") + + " ms (MSCO total " + frameMsco.ToString("0") + " ms)"); + else + MscoLog.Info("[MSCO] Hitch " + total + " ms - not MSCO (MSCO used " + frameMsco.ToString("0") + " ms)" + + (Time.realtimeSinceStartup - lastSaveTriggered < 5f ? " - the game was saving" : " - the game itself (loading, saving, physics)")); + } + frameMsco = 0; + slowestMs = 0; + slowestPart = null; + } + void ReportError(string what, System.Exception e) { float now = Time.realtimeSinceStartup, last; @@ -1965,25 +2036,37 @@ namespace MSCO void FixedUpdate() { if (!IsPeerReady || !MenuHelper.InGame) return; + PartStart(); try { objects.FixedUpdate(); } catch (System.Exception e) { ReportError("object movement", e); } + PartEnd("object movement"); + PartStart(); try { traffic.FixedUpdate(); } catch (System.Exception e) { ReportError("traffic movement", e); } + PartEnd("traffic movement"); + PartStart(); try { hinges.FixedUpdate(); } catch (System.Exception e) { ReportError("car door movement", e); } + PartEnd("car door movement"); } void LateUpdate() { InputLock.KeepCursorFree(); // das Spiel sperrt den Zeiger im selben Frame evtl. wieder // Nach den Spiel-FSMs, damit Lichter/Motorgeräusch der Fahrzeuge anderer gewinnen + PartStart(); try { if (IsPeerReady && MenuHelper.InGame) vehicleFx.Apply(objects); } catch (System.Exception e) { ReportError("vehicle lights/sound", e); } + PartEnd("vehicle lights/sound"); + PartStart(); try { if (IsPeerReady && MenuHelper.InGame) wheelSpin.Apply(objects, Time.deltaTime); } catch (System.Exception e) { ReportError("wheels", e); } + PartEnd("wheels"); // Beifahrer nach der Fahrzeugbewegung auf den Sitz setzen + PartStart(); try { if (localPlayer != null && MenuHelper.InGame) passenger.LateUpdate(localPlayer, playerCamera, objects); } catch (System.Exception e) { ReportError("passenger", e); } + PartEnd("passenger"); } /// @@ -2122,6 +2205,7 @@ namespace MSCO System.Collections.IEnumerator UploadSave(int requestId) { if (!MenuHelper.InGame || !dedicated || session == null) yield break; + lastSaveTriggered = Time.realtimeSinceStartup; PlayMakerFSM.BroadcastEvent("SAVEGAME"); float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames while (Time.realtimeSinceStartup < until) yield return null; diff --git a/VehicleStateSync.cs b/VehicleStateSync.cs index a77e72e..720c175 100644 --- a/VehicleStateSync.cs +++ b/VehicleStateSync.cs @@ -159,7 +159,7 @@ namespace MSCO void Scan() { int before = vars.Count; - foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f)) + foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f)) { // Schon erfasste FSMs nicht jedes Mal neu durchgehen (Pfad + Hash kosten) if (fsm == null || !scanned.Add(fsm)) continue; diff --git a/WorldSync.cs b/WorldSync.cs index 44731f9..a51c9d4 100644 --- a/WorldSync.cs +++ b/WorldSync.cs @@ -163,7 +163,7 @@ namespace MSCO { nextDiscover = Time.time + DiscoverRetry; tracked.Clear(); - foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f)) + foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f)) { if (fsm == null) continue; if (Array.IndexOf(TrackedFsmNames, fsm.FsmName) < 0) continue; diff --git a/WorldVarSync.cs b/WorldVarSync.cs index 9b8534f..fe142ac 100644 --- a/WorldVarSync.cs +++ b/WorldVarSync.cs @@ -111,7 +111,7 @@ namespace MSCO void Scan() { int before = vars.Count; - foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f)) + foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f)) { if (fsm == null || !scanned.Add(fsm)) continue; if (Array.IndexOf(Roots, fsm.transform.root.name) < 0) continue;