MSCO 2.10.1: car doors and garage door synced, door lock fix, perf tools

- Car doors (FSM with bool "Open" and "Open door"/"Close door") are synced as open/closed:
  the receiver removes the hold joint and frees the hinge, closing runs the game's own logic
- Fix: every synced close of an already closed Satsuma door added another FixedJoint, so the
  door could not be opened anymore; extra joints are removed automatically
- Hinge sync also covers world doors/gates with hinges (garage door), interaction by bounds
- Scene name read once per frame (Application.loadedLevelName allocates a string per call)
- Console "perf" (FPS, ms per MSCO part incl. OnGUI, GC, hook calls), -msco-perf start option
- Console "fsmdump <text>" / -msco-dump: PlayMaker FSMs and hinges to MSCO\fsmdump.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 11:01:27 +02:00
co-authored by Claude Opus 5.5
parent 4c1366b5d9
commit adc6eff9e8
14 changed files with 429 additions and 11 deletions
+78
View File
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// Laufzeit-Statistik: wie viele ms pro Bild jeder MSCO-Teil braucht, FPS und Garbage-Collections. Konsole (F7):
/// "perf" bzw. "perf reset". Mit der Startoption -msco-perf wird alle 30 s eine Zeile in MSCO\MSCO_Loader.txt
/// geschrieben (zum Messen ohne Konsole).
/// </summary>
public static class PerfStats
{
static readonly Dictionary<string, double> totals = new Dictionary<string, double>();
/// <summary>Aufrufe der Harmony-Haken (Fsm.SwitchState, Fsm.Event, cInput) - zum Abschätzen ihrer Kosten.</summary>
public static int SwitchStateCalls, EventCalls, InputCalls;
static int frames, gcStart = -1;
static double frameSeconds, worstFrame;
static float started = -1f;
static readonly bool autoLog = Array.IndexOf(Environment.GetCommandLineArgs(), "-msco-perf") >= 0;
public static void Add(string part, double ms)
{
double t;
totals.TryGetValue(part, out t);
totals[part] = t + ms;
}
/// <summary>Einmal pro Bild (Update).</summary>
public static void Frame(float dt)
{
if (started < 0f) Reset();
frames++;
frameSeconds += dt;
if (dt > worstFrame) worstFrame = dt;
if (autoLog && Time.realtimeSinceStartup - started >= 30f)
{
Bootstrap.Log(Report());
Reset();
}
}
public static void Reset()
{
totals.Clear();
frames = 0;
frameSeconds = 0;
worstFrame = 0;
started = Time.realtimeSinceStartup;
gcStart = GC.CollectionCount(0);
SwitchStateCalls = EventCalls = InputCalls = 0;
}
public static string Report()
{
if (frames == 0) return "PERF: no frames measured yet";
double seconds = Math.Max(0.001, Time.realtimeSinceStartup - started);
double msco = 0;
foreach (double v in totals.Values) msco += v;
var parts = new List<KeyValuePair<string, double>>(totals);
parts.Sort((a, b) => b.Value.CompareTo(a.Value));
var sb = new StringBuilder();
sb.Append("PERF ").Append(seconds.ToString("0")).Append(" s (").Append(MenuHelper.Scene).Append("): ")
.Append((frames / frameSeconds).ToString("0")).Append(" fps, ")
.Append((frameSeconds * 1000.0 / frames).ToString("0.0")).Append(" ms/frame (worst ")
.Append((worstFrame * 1000.0).ToString("0")).Append(" ms), MSCO ")
.Append((msco / frames).ToString("0.00")).Append(" ms/frame, GC ")
.Append(((GC.CollectionCount(0) - gcStart) * 60.0 / seconds).ToString("0.0")).Append("/min")
.Append(", hooks/frame: state ").Append((SwitchStateCalls / (double)frames).ToString("0"))
.Append(" event ").Append((EventCalls / (double)frames).ToString("0"))
.Append(" input ").Append((InputCalls / (double)frames).ToString("0"));
for (int i = 0; i < parts.Count && i < 8; i++)
sb.Append(i == 0 ? " | " : ", ").Append(parts[i].Key).Append(' ').Append((parts[i].Value / frames).ToString("0.000"));
return sb.ToString();
}
}
}