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
+18 -1
View File
@@ -124,6 +124,7 @@ namespace MSCO
{
void Start()
{
host = this;
MscoLog.Info("Welcome to MSCO by mbm.wtf");
MenuBranding.ApplyWindow();
// Den Nachrichten-Bildschirm des Spiels ("Latest news ... press any key") überspringen - direkt ins Hauptmenü
@@ -153,7 +154,22 @@ namespace MSCO
ModLoader.LoadLocal();
ModLoader.MenuLoad();
}
else if (scene == "GAME") ModLoader.GameLoad();
else if (scene == "GAME")
{
ModLoader.GameLoad();
string[] cmd = Environment.GetCommandLineArgs();
int i = Array.IndexOf(cmd, "-msco-dump");
if (i >= 0 && i + 1 < cmd.Length && host != null) host.StartCoroutine(DumpLater(cmd[i + 1]));
}
}
static MscoHost host;
static System.Collections.IEnumerator DumpLater(string filter)
{
yield return new WaitForSeconds(25f);
try { Bootstrap.Log("FSM dump: " + FsmDump.Write(filter)); }
catch (Exception e) { Bootstrap.Log("FSM dump failed: " + e); }
}
void FixedUpdate()
@@ -200,6 +216,7 @@ namespace MSCO
void OnLevelWasLoaded(int level)
{
MenuHelper.SceneChanged();
loadingScreen = false;
if (previousVolume >= 0f) { AudioListener.volume = previousVolume; previousVolume = -1f; }
Bootstrap.Log("Level loaded: " + Application.loadedLevelName);
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## 2.10.1
**Türen**
- Autotüren (Satsuma, Hayosiko, Kekmet, Gifu, Ferndale, Ruscko ...) werden jetzt als offen/zu übertragen: beim
Mitspieler geht die Tür wirklich auf (Halte-Joint weg, Scharnier frei) und schwingt mit, Zumachen läuft über die
Schließ-Logik des Spiels (mit Geräusch)
- Behoben: Satsuma-Türen ließen sich manchmal gar nicht mehr öffnen – jedes übertragene Zumachen hängte einen weiteren
Halte-Joint an. Solche „verklemmten“ Türen werden automatisch repariert
- Garagentor (und andere Tore/Türen mit Scharnier) synchron
**Leistung**
- Szenenname wird nur noch einmal pro Bild abgefragt (vorher hundertfach – jedes Mal ein neuer String)
- Konsole `perf`: FPS und Zeit pro MSCO-Teil (inkl. Oberfläche); Messung auf einem Server: MSCO braucht ~0,2 ms pro
Bild – die FPS begrenzt das Spiel selbst
- Konsole `fsmdump <text>`: Diagnose für Spiel-Logik (Zustände, Aktionen, Scharniere) in `MSCO\fsmdump.txt`
## 2.10.0
**Mod-Support**
+8
View File
@@ -20,6 +20,7 @@ namespace MSCO
"save - save the game (host / singleplayer)\n" +
"bind [action key] - show / change keys (passenger, console, chat)\n" +
"mods - loaded MSCO mods (folder MSCO\\Mods)\n" +
"perf [reset] - FPS and how long each MSCO part takes per frame\n" +
"ping [on|off] - show your ping to the server (toggle)\n" +
"fps [on|off] - show the frames per second (toggle)\n" +
"/admin - admin menu (kick / ban / teleport / cheats)\n" +
@@ -118,6 +119,13 @@ namespace MSCO
else mp.ShowPing = !mp.ShowPing;
print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")");
break;
case "fsmdump":
print(FsmDump.Write(args.Length > 1 ? string.Join(" ", args, 1, args.Length - 1) : ""));
break;
case "perf":
if (args.Length > 1 && args[1].ToLower() == "reset") { PerfStats.Reset(); print("Performance statistics reset"); break; }
print(PerfStats.Report());
break;
case "mods":
if (ModLoader.All.Count == 0) { print("No mods. Put MSCO mods (.dll) into " + ModLoader.Folder + " and restart the game."); break; }
foreach (ModLoader.LoadedMod m in ModLoader.All)
+130 -2
View File
@@ -32,8 +32,19 @@ namespace MSCO
public PlayMakerFSM Fsm;
public string LastState;
public float QuietUntil;
// Autotür (FSM mit Bool "Open" und Zuständen "Open door"/"Close door"): übertragen wird nur offen/zu
public bool IsCarDoor;
public FsmBool OpenVar;
public bool LastOpen;
public GameObject Door;
public HingeJoint Hinge;
public bool HasOpenLimits;
public float OpenMin, OpenMax;
}
// Statt Zustandsnamen: offen/zu einer Autotür (ältere Mods kennen diese "Zustände" nicht und ignorieren sie)
const string OpenToken = "__open", ClosedToken = "__closed";
readonly Dictionary<string, Tracked> byKey = new Dictionary<string, Tracked>();
float nextScan;
@@ -57,6 +68,18 @@ namespace MSCO
foreach (Tracked t in byKey.Values)
{
if (t.Fsm == null) continue;
if (t.IsCarDoor)
{
bool open = t.OpenVar.Value;
if (open == t.LastOpen) continue;
t.LastOpen = open;
if (now < t.QuietUntil) continue; // Folge einer übernommenen Änderung
t.QuietUntil = now + Cooldown;
string doorKey = t.Key, token = open ? OpenToken : ClosedToken;
send(MsgType.DoorState, NetSession.Build(w => { w.Write(doorKey); w.Write(token); }));
if (FsmSync.Debug) MscoLog.Info("[MSCO] >> CAR DOOR " + doorKey + " -> " + token);
continue;
}
string state = t.Fsm.ActiveStateName;
if (state == t.LastState) continue;
t.LastState = state;
@@ -91,6 +114,14 @@ namespace MSCO
}
}
if (t.IsCarDoor)
{
// Ältere Mods schicken Zustandsnamen: die des Zumachens als "zu" verstehen, den Rest ignorieren
bool closing = state == ClosedToken || state == "Sound" || state == "Close door" || state == "State 1";
if (state == OpenToken || closing) ApplyCarDoor(t, state == OpenToken);
return;
}
if (t.Fsm.ActiveStateName == state) return;
FsmState target = t.Fsm.Fsm.GetState(state);
if (target == null) return;
@@ -112,11 +143,108 @@ namespace MSCO
string key = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName;
Tracked t;
if (byKey.TryGetValue(key, out t) && t.Fsm == fsm) continue;
byKey[key] = new Tracked { Key = key, Fsm = fsm, LastState = fsm.ActiveStateName };
if (byKey.TryGetValue(key, out t) && t.Fsm == fsm)
{
if (t.IsCarDoor && !t.OpenVar.Value) RemoveExtraFixedJoints(t);
continue;
}
t = new Tracked { Key = key, Fsm = fsm, LastState = fsm.ActiveStateName };
DetectCarDoor(t);
byKey[key] = t;
}
}
// ---------------------------------------------------------------- Autotüren
// Alle Autotüren im Spiel: Bool "Open", "Open door" (Halte-Joint weg, Scharnier-Grenzen auf, Drehmoment solange
// die Maus gedrückt ist) und "Close door" (zu, Grenzen 0..0 bzw. beim Satsuma ein neuer FixedJoint). "Open door"
// prüft die Maustaste und wurde deshalb nie übertragen - beim Mitspieler blieb die Tür fest. Und jedes übertragene
// Zumachen einer schon geschlossenen Satsuma-Tür hängte einen weiteren FixedJoint an - beim nächsten Öffnen wurde
// nur einer entfernt und die Tür ging gar nicht mehr auf.
static void DetectCarDoor(Tracked t)
{
try
{
Fsm f = t.Fsm.Fsm;
FsmState openState = f.GetState("Open door");
if (openState == null || f.GetState("Close door") == null) return;
FsmBool open = t.Fsm.FsmVariables.FindFsmBool("Open");
if (open == null) return;
FsmGameObject door = t.Fsm.FsmVariables.FindFsmGameObject("Door");
t.Door = door != null && door.Value != null ? door.Value : t.Fsm.gameObject;
t.Hinge = t.Door.GetComponent<HingeJoint>();
t.OpenVar = open;
t.LastOpen = open.Value;
t.IsCarDoor = true;
// Grenzen beim Öffnen: aus der Aktion "SetHingeJointLimits" des Zustands "Open door"
if (!openState.ActionsLoaded) openState.LoadActions();
if (openState.Actions == null) return;
foreach (FsmStateAction a in openState.Actions)
{
if (a == null || a.GetType().Name != "SetHingeJointLimits") continue;
var min = a.GetType().GetField("min") != null ? a.GetType().GetField("min").GetValue(a) as FsmFloat : null;
var max = a.GetType().GetField("max") != null ? a.GetType().GetField("max").GetValue(a) as FsmFloat : null;
if (min == null || max == null) continue;
t.OpenMin = min.Value;
t.OpenMax = max.Value;
t.HasOpenLimits = true;
}
}
catch (Exception e)
{
t.IsCarDoor = false;
MscoLog.Warning("[MSCO] Car door " + t.Key + ": " + e.Message);
}
}
/// <summary>Tür auf- bzw. zumachen wie beim Spieler, der sie bedient hat.</summary>
void ApplyCarDoor(Tracked t, bool open)
{
if (t.Door == null) return;
t.QuietUntil = Time.realtimeSinceStartup + Cooldown;
t.LastOpen = open;
if (open)
{
if (t.OpenVar.Value && t.Door.GetComponent<FixedJoint>() == null) return; // schon offen
foreach (FixedJoint j in t.Door.GetComponents<FixedJoint>()) UnityEngine.Object.Destroy(j);
if (t.Hinge != null && t.HasOpenLimits)
{
JointLimits l = t.Hinge.limits;
l.min = t.OpenMin;
l.max = t.OpenMax;
t.Hinge.limits = l;
t.Hinge.useLimits = true;
}
t.OpenVar.Value = true;
Rigidbody rb = t.Door.GetComponent<Rigidbody>();
if (rb != null) rb.WakeUp();
// Ruht die FSM (nicht gerade selbst an der Tür): neu prüfen lassen, damit sie "offen" kennt (Innenlicht ...)
string active = t.Fsm.ActiveStateName ?? "";
FsmState idle = t.Fsm.Fsm.GetState("Mouse off");
if (idle != null && (active.StartsWith("Mouse") || active == "Check position")) FsmSync.SwitchStateSilently(t.Fsm, idle);
}
else
{
if (!t.OpenVar.Value) { RemoveExtraFixedJoints(t); return; } // schon zu
// Halte-Joints weg, dann die Schließ-Logik des Spiels (Geräusch, zu, beim Satsuma genau EIN neuer FixedJoint)
foreach (FixedJoint j in t.Door.GetComponents<FixedJoint>()) UnityEngine.Object.Destroy(j);
FsmState close = t.Fsm.Fsm.GetState("Sound") ?? t.Fsm.Fsm.GetState("Close door");
if (close != null) FsmSync.SwitchStateSilently(t.Fsm, close);
}
if (FsmSync.Debug) MscoLog.Info("[MSCO] << CAR DOOR " + t.Key + " -> " + (open ? "open" : "closed"));
}
/// <summary>Mehrere FixedJoints an einer geschlossenen Tür (alter Fehler): bis auf einen entfernen.</summary>
static void RemoveExtraFixedJoints(Tracked t)
{
if (t.Door == null) return;
FixedJoint[] joints = t.Door.GetComponents<FixedJoint>();
if (joints.Length <= 1) return;
for (int i = 1; i < joints.Length; i++) UnityEngine.Object.Destroy(joints[i]);
MscoLog.Info("[MSCO] Fixed a stuck door (" + (joints.Length - 1) + " extra lock(s) removed): " + t.Door.name);
}
static bool IsPartLogic(PlayMakerFSM fsm)
{
string f = fsm.FsmName.ToLower(), o = fsm.gameObject.name.ToLower();
+110
View File
@@ -0,0 +1,110 @@
using System;
using System.IO;
using System.Reflection;
using System.Text;
using HutongGames.PlayMaker;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// Diagnose: PlayMaker-FSMs (Zustände, Aktionen mit Werten, Übergänge) und Scharniere von Objekten, deren Pfad einen
/// Suchtext enthält, in MSCO\fsmdump.txt schreiben. Konsole (F7): "fsmdump &lt;text&gt;". Mit der Startoption
/// "-msco-dump text1,text2" automatisch 25 s nach dem Laden des Spiels.
/// </summary>
public static class FsmDump
{
public static string Write(string filter)
{
string[] filters = (filter ?? "").ToLowerInvariant().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (filters.Length == 0) return "fsmdump <text>[,text2] - e.g. fsmdump garage";
var sb = new StringBuilder();
int count = 0;
foreach (PlayMakerFSM fsm in Resources.FindObjectsOfTypeAll<PlayMakerFSM>())
{
if (fsm == null || fsm.gameObject == null || fsm.gameObject.hideFlags != HideFlags.None) continue;
string path, lower;
try { path = FsmSync.PathOf(fsm.transform); lower = (path + "|" + fsm.FsmName).ToLowerInvariant(); }
catch { continue; }
if (Array.FindIndex(filters, f => lower.Contains(f.Trim())) < 0) continue;
if (++count > 400) break;
try { Dump(sb, fsm, path); }
catch (Exception e) { sb.Append(" (error: ").Append(e.Message).Append(")").AppendLine(); }
}
foreach (HingeJoint j in Resources.FindObjectsOfTypeAll<HingeJoint>())
{
if (j == null || j.gameObject.hideFlags != HideFlags.None) continue;
string path = FsmSync.PathOf(j.transform);
if (Array.FindIndex(filters, f => path.ToLowerInvariant().Contains(f.Trim())) < 0) continue;
Rigidbody rb = j.GetComponent<Rigidbody>();
try {
sb.Append("HINGE ").Append(path).Append(" -> ").Append(j.connectedBody != null ? FsmSync.PathOf(j.connectedBody.transform) : "-")
.Append(" | active ").Append(j.gameObject.activeInHierarchy).Append(" angle ").Append(j.angle.ToString("0.0"))
.Append(" limits ").Append(j.useLimits).Append(' ').Append(j.limits.min.ToString("0")).Append("..").Append(j.limits.max.ToString("0"))
.Append(" spring ").Append(j.useSpring).Append(' ').Append(j.spring.spring.ToString("0")).Append('/').Append(j.spring.targetPosition.ToString("0"))
.Append(" motor ").Append(j.useMotor)
.Append(" kinematic ").Append(rb != null && rb.isKinematic).Append(" break ").Append(j.breakForce).AppendLine();
} catch (Exception e) { sb.Append(" (error: ").Append(e.Message).Append(")").AppendLine(); }
}
string file = Path.Combine(Bootstrap.Folder ?? ".", "fsmdump.txt");
File.WriteAllText(file, sb.ToString());
return count + " FSM(s) written to " + file;
}
static void Dump(StringBuilder sb, PlayMakerFSM fsm, string path)
{
sb.Append("==== ").Append(path).Append(" | ").Append(fsm.FsmName).Append(" | active ").Append(fsm.gameObject.activeInHierarchy)
.Append(" | state ").Append(fsm.ActiveStateName).AppendLine();
if (fsm.FsmGlobalTransitions != null)
foreach (FsmTransition t in fsm.FsmGlobalTransitions)
sb.Append(" GLOBAL ").Append(t.EventName).Append(" -> ").Append(t.ToState).AppendLine();
if (fsm.FsmStates == null) return;
foreach (FsmState s in fsm.FsmStates)
{
if (s == null) continue;
if (!s.ActionsLoaded) s.LoadActions();
sb.Append(" [").Append(s.Name).Append(']');
if (s.Transitions != null) foreach (FsmTransition t in s.Transitions) if (t != null) sb.Append(" ").Append(t.EventName).Append("->").Append(t.ToState);
sb.AppendLine();
if (s.Actions == null) continue;
foreach (FsmStateAction a in s.Actions)
{
if (a == null) continue;
sb.Append(" ").Append(a.GetType().Name).Append(Fields(a)).AppendLine();
}
}
}
static string Fields(FsmStateAction a)
{
var sb = new StringBuilder();
foreach (FieldInfo f in a.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
{
object v;
try { v = f.GetValue(a); } catch { continue; }
string text = Describe(v);
if (text == null) continue;
sb.Append(' ').Append(f.Name).Append('=').Append(text);
}
return sb.ToString();
}
static string Describe(object v)
{
if (v == null) return null;
var named = v as NamedVariable;
if (named != null)
{
string value;
try { value = Convert.ToString(v.GetType().GetProperty("Value").GetValue(v, null)); } catch { value = "?"; }
return (string.IsNullOrEmpty(named.Name) ? "" : "{" + named.Name + "}") + value;
}
var ev = v as FsmEvent;
if (ev != null) return "event:" + ev.Name;
var owner = v as FsmOwnerDefault;
if (owner != null) return owner.OwnerOption == OwnerDefaultOption.UseOwner ? "owner" : Describe(owner.GameObject);
if (v is string || v is bool || v is int || v is float || v.GetType().IsEnum) return Convert.ToString(v);
return null;
}
}
}
+2
View File
@@ -132,6 +132,7 @@ namespace MSCO
static void SwitchStatePrefix(Fsm __instance, FsmState toState)
{
PerfStats.SwitchStateCalls++;
if (!Enabled || applying > 0 || toState == null) return;
try
{
@@ -235,6 +236,7 @@ namespace MSCO
static void EventPrefix(Fsm __instance, FsmEvent fsmEvent)
{
PerfStats.EventCalls++;
if (!Enabled || applying > 0 || fsmEvent == null) return;
if (Array.IndexOf(SyncedEvents, fsmEvent.Name) < 0) return;
+25 -5
View File
@@ -40,6 +40,7 @@ namespace MSCO
public float TargetTime = -100f;
public bool Driving; // Feder gerade von uns gesetzt
public float LocalAuthorityUntil = -100f;
public Collider Collider; // für den Abstand zum Spieler (große Türen wie das Garagentor)
public bool OrigUseSpring;
public JointSpring OrigSpring;
}
@@ -102,10 +103,13 @@ namespace MSCO
/// <summary>Jedes Frame: Klick (linke Maustaste / Benutzen) in der Nähe einer Tür = wir bewegen sie gerade.</summary>
public void NoteInteraction(Vector3 playerPos)
{
if (!Input.GetMouseButtonDown(0) && !Input.GetKeyDown(KeyCode.F)) return;
// Links öffnet, rechts schließt (Garagentor), F = Benutzen
if (!Input.GetMouseButtonDown(0) && !Input.GetMouseButtonDown(1) && !Input.GetKeyDown(KeyCode.F)) return;
foreach (Hinge h in hinges.Values)
{
if (h.Rb == null || (h.Rb.worldCenterOfMass - playerPos).sqrMagnitude > InteractRange * InteractRange) continue;
if (h.Rb == null) continue;
Vector3 near = h.Collider != null ? h.Collider.ClosestPointOnBounds(playerPos) : h.Rb.worldCenterOfMass;
if ((near - playerPos).sqrMagnitude > InteractRange * InteractRange) continue;
h.LocalAuthorityUntil = Time.time + InteractAuthority;
h.TargetTime = -100f; // eigene Bewegung: keine Feder mehr auf den alten Winkel
}
@@ -183,10 +187,17 @@ namespace MSCO
if (j == null || !seen.Add(j)) continue;
Rigidbody rb = j.GetComponent<Rigidbody>();
Rigidbody body = j.connectedBody;
if (rb == null || body == null || !IsVehicle(body.transform.root.name)) continue;
int key = Hash(FsmSync.PathOf(j.transform) + "|" + FsmSync.PathOf(body.transform));
if (rb == null) continue;
// Fahrzeugtüren/-hauben - und Türen/Tore der Welt mit Scharnier (z.B. das Garagentor)
bool vehicle = body != null && IsVehicle(body.transform.root.name);
if (!vehicle && !IsWorldDoor(j)) continue;
int key = Hash(FsmSync.PathOf(j.transform) + "|" + (body != null ? FsmSync.PathOf(body.transform) : "world"));
if (hinges.ContainsKey(key)) continue;
hinges[key] = new Hinge { Key = key, Root = body.transform.root.name, Joint = j, Rb = rb };
hinges[key] = new Hinge
{
Key = key, Root = (body != null ? body.transform : j.transform).root.name, Joint = j, Rb = rb,
Collider = rb.GetComponentInChildren<Collider>()
};
}
// Abgebaute Teile (Scharnier weg) vergessen
var gone = new List<int>();
@@ -197,6 +208,15 @@ namespace MSCO
if (hinges.Count != before) MscoLog.Info("[MSCO] Vehicle doors/hood/trunk: " + hinges.Count + " synced");
}
/// <summary>Tür/Tor der Welt (nicht Verkehr/NPC-Autos, nicht der Spieler): Name enthält door/gate.</summary>
static bool IsWorldDoor(HingeJoint j)
{
string root = j.transform.root.name;
if (root == "PLAYER" || root == "NPC_CARS" || root == "TRAFFIC" || root.StartsWith("MSCO_")) return false;
string path = FsmSync.PathOf(j.transform).ToLowerInvariant();
return path.Contains("door") || path.Contains("gate");
}
static bool IsVehicle(string root)
{
foreach (string v in ObjectSync.VehicleRoots) if (root.StartsWith(v)) return true;
+1
View File
@@ -151,6 +151,7 @@ namespace MSCO
// Harmony-Prefixe: solange ein Eingabefenster offen ist, meldet cInput "nichts gedrückt"
static bool NoKeyWhileLocked(ref bool __result)
{
PerfStats.InputCalls++;
if (!locked) return true;
__result = false;
return false;
+1 -1
View File
@@ -11,7 +11,7 @@ namespace MSCO
/// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
/// </summary>
public const string ModVersion = "2.10.0";
public const string ModVersion = "2.10.1";
/// <summary>
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server.
+2
View File
@@ -94,6 +94,8 @@
<Compile Include="MenuHelper.cs" />
<Compile Include="MenuInputBlocker.cs" />
<Compile Include="MiniJson.cs" />
<Compile Include="PerfStats.cs" />
<Compile Include="FsmDump.cs" />
<Compile Include="ModApi.cs" />
<Compile Include="ModLoader.cs" />
<Compile Include="MenuBranding.cs" />
+26 -2
View File
@@ -11,8 +11,32 @@ namespace MSCO
public const string MenuScene = "MainMenu";
public const string GameScene = "GAME";
public static bool InMenu { get { return Application.loadedLevelName == MenuScene; } }
public static bool InGame { get { return Application.loadedLevelName == GameScene; } }
public static bool InMenu { get { return Scene == MenuScene; } }
public static bool InGame { get { return Scene == GameScene; } }
static string scene;
static int sceneFrame = -1;
/// <summary>
/// Aktuelle Szene, einmal pro Bild gelesen: Application.loadedLevelName erzeugt bei jedem Aufruf einen neuen String
/// (Müll für den Garbage Collector), und InGame/InMenu werden pro Bild sehr oft abgefragt.
/// </summary>
public static string Scene
{
get
{
int frame = Time.frameCount;
if (frame != sceneFrame || scene == null)
{
sceneFrame = frame;
scene = Application.loadedLevelName;
}
return scene;
}
}
/// <summary>Nach einem Szenenwechsel (OnLevelWasLoaded) neu lesen.</summary>
public static void SceneChanged() { scene = null; }
static readonly MethodInfo switchState =
typeof(Fsm).GetMethod("SwitchState", BindingFlags.NonPublic | BindingFlags.Instance);
+10
View File
@@ -339,6 +339,7 @@ namespace MSCO
void MeasureFps()
{
float dt = Time.unscaledDeltaTime;
PerfStats.Frame(dt);
fpsFrames++;
fpsTimer += dt;
frameMsWorstWindow = Mathf.Max(frameMsWorstWindow, dt * 1000f);
@@ -1992,6 +1993,7 @@ namespace MSCO
void OnLevelWasLoaded(int level)
{
MenuHelper.SceneChanged();
menuButtonsHidden = false; // neues Hauptmenü: Knöpfe wieder ausblenden
nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren
if (MenuHelper.InGame)
@@ -2252,6 +2254,7 @@ namespace MSCO
{
double ms = (partWatch.ElapsedTicks - partStart) * 1000.0 / System.Diagnostics.Stopwatch.Frequency;
frameMsco += ms;
PerfStats.Add(name, ms);
if (ms > slowestMs) { slowestMs = ms; slowestPart = name; }
}
@@ -2594,6 +2597,13 @@ namespace MSCO
// ---------------------------------------------------------------- GUI
void OnGUI()
{
PartStart();
try { DrawGui(); }
finally { PartEnd("interface (OnGUI)"); }
}
void DrawGui()
{
InputLock.KeepCursorFree();
if (MenuHelper.InMenu)
+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();
}
}
}
+2
View File
@@ -115,6 +115,8 @@ In der F7-Konsole:
| `model [game\|classic]` | Mitspieler als MSC-Menschen oder als einfache Figur |
| `vars <filter>` | Diagnose: synchronisierte Fahrzeugwerte |
| `mods` | geladene MSCO-Mods |
| `perf [reset]` | FPS und wie lange jeder MSCO-Teil pro Bild braucht |
| `fsmdump <text>` | Diagnose: Spiel-Logik (PlayMaker) und Scharniere von Objekten mit diesem Namen nach `MSCO\fsmdump.txt` |
| `admin` bzw. `/admin` im Chat | Admin-Menü |
| `vehicles`, `rig` | Diagnose: Fahrzeuge / Spielermodelle |
| `debug` | Sync-Debugausgaben an/aus |