2.10.8: fewer hitches

- Sending on a background thread (the game thread blocked whenever the TCP
  buffer was full, e.g. while uploading the save); small frames are batched
- Server save requests: routine saves wait for a calm moment (not while driving
  fast) and are announced; urgent flag from server 2.10.5
- Dynamic built-in font for all MSCO GUI (Unity logged hundreds of
  "Font size and style overrides" warnings with stutters)
- Admin VEHICLE tab rescans every 30 s / after actions instead of every 5 s;
  time/weather FSM search less often
- Reused buffers for Build/Frame/Poll (less garbage, fewer GC pauses)
- Hitch log: GC and save detection, count of hitches between reports
- Show Bolt Size 1.0.1: finds the FPSCamera (Camera.main may be missing), uses
  MSCO's shared FSM list instead of FindObjectsOfType every 2 s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 00:08:36 +02:00
co-authored by Claude Opus 5.5
parent e32ddadbc8
commit d195a484d8
9 changed files with 277 additions and 57 deletions
+66 -13
View File
@@ -1635,7 +1635,12 @@ namespace MSCO
case MsgType.SaveRequest:
// Server möchte den aktuellen Spielstand (für neue Spieler / zum Sichern)
StartCoroutine(UploadSave(r.ReadInt32()));
{
int requestId = r.ReadInt32();
// Neuere Server sagen, ob es eilt (jemand tritt bei) - sonst darf auf einen ruhigen Moment gewartet werden
bool urgent = r.BaseStream.Position >= r.BaseStream.Length || r.ReadBoolean();
StartCoroutine(UploadSave(requestId, urgent));
}
break;
case MsgType.Reconnect:
@@ -2290,19 +2295,34 @@ namespace MSCO
}
/// <summary>Zu Beginn jedes Bilds: war das letzte Bild ein Hänger? Dann Ursache melden (höchstens alle 20 s).</summary>
int lastGcCount = -1;
int hitchesSinceReport;
void CheckHitch()
{
float dt = Time.unscaledDeltaTime;
if (dt > HitchSeconds && MenuHelper.InGame && Time.realtimeSinceStartup >= nextHitchReport)
// Lief in diesem Frame der Garbage Collector? (hält bei großem Speicher das ganze Spiel an)
int gc = System.GC.CollectionCount(0);
bool gcRan = lastGcCount >= 0 && gc != lastGcCount;
lastGcCount = gc;
if (dt > HitchSeconds && MenuHelper.InGame)
{
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)"));
hitchesSinceReport++;
if (Time.realtimeSinceStartup >= nextHitchReport)
{
nextHitchReport = Time.realtimeSinceStartup + 20f;
int total = Mathf.RoundToInt(dt * 1000f);
string more = hitchesSinceReport > 1 ? " [" + hitchesSinceReport + " hitches since the last report]" : "";
hitchesSinceReport = 0;
// Speichern friert genau den Frame ein, in dem es ausgelöst wurde - also auch so weit zurück schauen
bool saving = Time.realtimeSinceStartup - lastSaveTriggered < dt + 5f;
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)" + (gcRan ? ", garbage collection ran" : "") + more);
else
MscoLog.Info("[MSCO] Hitch " + total + " ms - not MSCO (MSCO used " + frameMsco.ToString("0") + " ms)" +
(saving ? " - the game was saving" : gcRan ? " - garbage collection (memory cleanup)" : " - the game itself (loading, physics)") + more);
}
}
frameMsco = 0;
slowestMs = 0;
@@ -2490,9 +2510,30 @@ namespace MSCO
/// genau die aktuelle Welt - Schrauben, Teile, alles - statt eines alten Stands mit nachgespielten Aktionen.
/// Der eigene Spielstand ist davon nicht betroffen: er liegt gesichert im Backup und kommt im Hauptmenü zurück.
/// </summary>
System.Collections.IEnumerator UploadSave(int requestId)
System.Collections.IEnumerator UploadSave(int requestId, bool urgent)
{
if (!MenuHelper.InGame || !dedicated || session == null) yield break;
// Speichern lässt das Spiel einige Sekunden hängen: bei einer Routine-Anfrage nicht mitten in schneller Fahrt,
// sondern einen ruhigen Moment abwarten (höchstens 20 s - der Server wartet länger)
if (!urgent)
{
float giveUp = Time.realtimeSinceStartup + 20f;
while (Time.realtimeSinceStartup < giveUp && localPlayer != null && session != null)
{
Vector3 from = localPlayer.position;
float t0 = Time.realtimeSinceStartup;
while (Time.realtimeSinceStartup < t0 + 0.5f) yield return null;
if (localPlayer == null) break;
float speed = (localPlayer.position - from).magnitude / Mathf.Max(0.05f, Time.realtimeSinceStartup - t0);
if (speed < 3f) break;
}
if (session == null || !MenuHelper.InGame) yield break;
}
// Erst den Hinweis zeigen, dann speichern (sonst friert das Bild ein, bevor man ihn sieht)
centerMessage = "Saving the world for the server ...";
centerMessageUntil = Time.realtimeSinceStartup + 4f;
yield return null;
yield return null;
lastSaveTriggered = Time.realtimeSinceStartup;
PlayMakerFSM.BroadcastEvent("SAVEGAME");
float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames
@@ -2630,10 +2671,21 @@ namespace MSCO
void OnGUI()
{
PartStart();
// Eigene dynamische Schrift (siehe UiTheme.DynamicFont); danach wieder die bisherige
GUISkin skin = GUI.skin;
Font previousFont = skin.font;
Font font = UiTheme.DynamicFont;
if (font != null) skin.font = font;
try { DrawGui(); }
finally { PartEnd("interface (OnGUI)"); }
finally
{
skin.font = previousFont;
PartEnd("interface (OnGUI)");
}
}
string centerMessageSource, centerMessageUpper; // Großschreibung nur einmal pro Meldung (nicht jedes Frame)
void DrawGui()
{
InputLock.KeepCursorFree();
@@ -2652,7 +2704,8 @@ namespace MSCO
{
int oldSize = savingStyle.fontSize;
savingStyle.fontSize = 30;
GUI.Label(new Rect(0, Screen.height / 2f - 40, Screen.width, 80), centerMessage.ToUpper(), savingStyle);
if (!ReferenceEquals(centerMessage, centerMessageSource)) { centerMessageSource = centerMessage; centerMessageUpper = centerMessage.ToUpper(); }
GUI.Label(new Rect(0, Screen.height / 2f - 40, Screen.width, 80), centerMessageUpper, savingStyle);
savingStyle.fontSize = oldSize;
}