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:
+18
-1
@@ -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);
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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
@@ -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,9 +143,106 @@ 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)
|
||||
|
||||
+110
@@ -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 <text>". 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user