Fewer hitches: FSM scene scans less often, damage safety check one mesh at a time; hitch detector (per-subsystem timing, reports MSCO part or game) in the F7 console

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:36:33 +02:00
co-authored by Claude Opus 5.5
parent 4801539b9d
commit 2faf03035a
8 changed files with 103 additions and 8 deletions
+7
View File
@@ -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
+1 -1
View File
@@ -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;
+7 -3
View File
@@ -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<MeshFilter> seen = new HashSet<MeshFilter>();
readonly List<int> changed = new List<int>();
float nextScan, nextCheck, nextSafetyCheck;
int safetyIndex;
// Mesh-Daten lesen ist teuer (große Arrays, Speicher-Müll) - nur nach einem Aufprall
readonly Dictionary<Rigidbody, Vector3> lastVelocity = new Dictionary<Rigidbody, Vector3>();
readonly Dictionary<Rigidbody, float> lastImpact = new Dictionary<Rigidbody, float>();
@@ -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;
+1 -1
View File
@@ -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;
+84
View File
@@ -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<string, float> lastErrorReport = new Dictionary<string, float>();
/// <summary>Fehler eines Teilsystems melden (höchstens alle 30 s pro Teilsystem, damit das Log nicht vollläuft).</summary>
// ---------------------------------------------------------------- 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; }
}
/// <summary>Zu Beginn jedes Bilds: war das letzte Bild ein Hänger? Dann Ursache melden (höchstens alle 20 s).</summary>
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");
}
/// <summary>
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;