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
+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;
}
}
}