diff --git a/Bootstrap.cs b/Bootstrap.cs index a6e49fb..132ff8f 100644 --- a/Bootstrap.cs +++ b/Bootstrap.cs @@ -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); diff --git a/CHANGELOG.md b/CHANGELOG.md index 834b130..3cbd97b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `: Diagnose für Spiel-Logik (Zustände, Aktionen, Scharniere) in `MSCO\fsmdump.txt` + ## 2.10.0 **Mod-Support** diff --git a/Commands.cs b/Commands.cs index 752ef4a..0cd5f24 100644 --- a/Commands.cs +++ b/Commands.cs @@ -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) diff --git a/DoorSync.cs b/DoorSync.cs index 3da3b1f..4e5b49a 100644 --- a/DoorSync.cs +++ b/DoorSync.cs @@ -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 byKey = new Dictionary(); 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(); + 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); + } + } + + /// Tür auf- bzw. zumachen wie beim Spieler, der sie bedient hat. + 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() == null) return; // schon offen + foreach (FixedJoint j in t.Door.GetComponents()) 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(); + 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()) 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")); + } + + /// Mehrere FixedJoints an einer geschlossenen Tür (alter Fehler): bis auf einen entfernen. + static void RemoveExtraFixedJoints(Tracked t) + { + if (t.Door == null) return; + FixedJoint[] joints = t.Door.GetComponents(); + 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(); diff --git a/FsmDump.cs b/FsmDump.cs new file mode 100644 index 0000000..c9a82bd --- /dev/null +++ b/FsmDump.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using HutongGames.PlayMaker; +using UnityEngine; + +namespace MSCO +{ + /// + /// 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 <text>". Mit der Startoption + /// "-msco-dump text1,text2" automatisch 25 s nach dem Laden des Spiels. + /// + public static class FsmDump + { + public static string Write(string filter) + { + string[] filters = (filter ?? "").ToLowerInvariant().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (filters.Length == 0) return "fsmdump [,text2] - e.g. fsmdump garage"; + var sb = new StringBuilder(); + int count = 0; + foreach (PlayMakerFSM fsm in Resources.FindObjectsOfTypeAll()) + { + 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()) + { + 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(); + 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; + } + } +} diff --git a/FsmSync.cs b/FsmSync.cs index 01c4697..7e630f7 100644 --- a/FsmSync.cs +++ b/FsmSync.cs @@ -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; diff --git a/HingeSync.cs b/HingeSync.cs index f89c4d6..e9de600 100644 --- a/HingeSync.cs +++ b/HingeSync.cs @@ -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 /// Jedes Frame: Klick (linke Maustaste / Benutzen) in der Nähe einer Tür = wir bewegen sie gerade. 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 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() + }; } // Abgebaute Teile (Scharnier weg) vergessen var gone = new List(); @@ -197,6 +208,15 @@ namespace MSCO if (hinges.Count != before) MscoLog.Info("[MSCO] Vehicle doors/hood/trunk: " + hinges.Count + " synced"); } + /// Tür/Tor der Welt (nicht Verkehr/NPC-Autos, nicht der Spieler): Name enthält door/gate. + 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; diff --git a/InputLock.cs b/InputLock.cs index 5e7383c..f3b99eb 100644 --- a/InputLock.cs +++ b/InputLock.cs @@ -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; diff --git a/MSCO.cs b/MSCO.cs index 14384ea..c1f89f7 100644 --- a/MSCO.cs +++ b/MSCO.cs @@ -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. /// - public const string ModVersion = "2.10.0"; + public const string ModVersion = "2.10.1"; /// /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// auch GameServer.ModVersion im Server. diff --git a/MSCO.csproj b/MSCO.csproj index 407be74..80d6d5d 100644 --- a/MSCO.csproj +++ b/MSCO.csproj @@ -94,6 +94,8 @@ + + diff --git a/MenuHelper.cs b/MenuHelper.cs index ac2fbe4..9c15a16 100644 --- a/MenuHelper.cs +++ b/MenuHelper.cs @@ -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; + + /// + /// 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. + /// + public static string Scene + { + get + { + int frame = Time.frameCount; + if (frame != sceneFrame || scene == null) + { + sceneFrame = frame; + scene = Application.loadedLevelName; + } + return scene; + } + } + + /// Nach einem Szenenwechsel (OnLevelWasLoaded) neu lesen. + public static void SceneChanged() { scene = null; } static readonly MethodInfo switchState = typeof(Fsm).GetMethod("SwitchState", BindingFlags.NonPublic | BindingFlags.Instance); diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index d85a4d4..3d68a63 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -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) diff --git a/PerfStats.cs b/PerfStats.cs new file mode 100644 index 0000000..e16cba1 --- /dev/null +++ b/PerfStats.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; + +namespace MSCO +{ + /// + /// 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). + /// + public static class PerfStats + { + static readonly Dictionary totals = new Dictionary(); + /// Aufrufe der Harmony-Haken (Fsm.SwitchState, Fsm.Event, cInput) - zum Abschätzen ihrer Kosten. + 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; + } + + /// Einmal pro Bild (Update). + 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>(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(); + } + } +} diff --git a/README.md b/README.md index c0881ce..582c9e4 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ In der F7-Konsole: | `model [game\|classic]` | Mitspieler als MSC-Menschen oder als einfache Figur | | `vars ` | Diagnose: synchronisierte Fahrzeugwerte | | `mods` | geladene MSCO-Mods | +| `perf [reset]` | FPS und wie lange jeder MSCO-Teil pro Bild braucht | +| `fsmdump ` | 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 |