- 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>
266 lines
12 KiB
C#
266 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using HutongGames.PlayMaker;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>
|
|
/// Türen, Tore, Hauben, Klappen (Garagentor, Haustüren, Autotüren ...). Die Mod beobachtet die
|
|
/// PlayMaker-FSMs dieser Objekte: Wechselt eine bei uns in einen neuen Zustand (z.B. "Open door"),
|
|
/// wird dieser Zustand an die anderen geschickt und dort ebenfalls ausgelöst. Unabhängig davon,
|
|
/// wie der Spieler das Objekt bedient hat (Klick, Taste, Hand-FSM ...).
|
|
/// </summary>
|
|
public class DoorSync
|
|
{
|
|
static readonly string[] Keywords = { "door", "gate", "hood", "bonnet", "trunk", "lid", "hatch" };
|
|
// Nicht die FSMs zum An-/Abbauen, Festschrauben, Lackieren, Speichern ... der Tür-TEILE: deren Zustände wurden
|
|
// beim Mitspieler erzwungen - Türen hingen schief, lösten sich oder sprangen zurück. Montage läuft über FsmSync.
|
|
static readonly string[] ExcludedFsms =
|
|
{
|
|
"removal", "assembly", "assemble", "boltcheck", "bolt", "screw", "paint", "data", "save", "load", "wiring",
|
|
"setup", "damage", "tightness", "detach", "attach", "install"
|
|
};
|
|
|
|
const float Cooldown = 1.5f; // Folge-Zustände der Animation nicht nochmal senden
|
|
const float RescanInterval = 10f; // neue Objekte (z.B. angebaute Autotüren) aufnehmen
|
|
|
|
class Tracked
|
|
{
|
|
public string Key;
|
|
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;
|
|
|
|
public int Count { get { return byKey.Count; } }
|
|
|
|
public void Reset()
|
|
{
|
|
byKey.Clear();
|
|
nextScan = 0f;
|
|
}
|
|
|
|
public void Update(Action<MsgType, byte[]> send)
|
|
{
|
|
float now = Time.realtimeSinceStartup;
|
|
if (now >= nextScan)
|
|
{
|
|
nextScan = now + RescanInterval;
|
|
Scan();
|
|
}
|
|
|
|
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;
|
|
|
|
if (now < t.QuietUntil) continue; // Folge einer eigenen oder fremden Aktion
|
|
FsmState s = t.Fsm.Fsm.GetState(state);
|
|
if (s == null || FsmSync.ChecksInputPublic(s)) continue; // nur "Maus zeigt drauf" o.ä.
|
|
|
|
t.QuietUntil = now + Cooldown;
|
|
string key = t.Key;
|
|
send(MsgType.DoorState, NetSession.Build(w => { w.Write(key); w.Write(state); }));
|
|
if (FsmSync.Debug) MscoLog.Info("[MSCO] >> DOOR " + key + " -> " + state);
|
|
}
|
|
}
|
|
|
|
public void Apply(BinaryReader r)
|
|
{
|
|
ApplyState(r.ReadString(), r.ReadString());
|
|
}
|
|
|
|
/// <summary>Zustand übernehmen (von einem Mitspieler oder aus dem Server-Snapshot).</summary>
|
|
public void ApplyState(string key, string state)
|
|
{
|
|
Tracked t;
|
|
if (!byKey.TryGetValue(key, out t) || t.Fsm == null)
|
|
{
|
|
Scan(); // Objekt vielleicht erst seit kurzem aktiv
|
|
if (!byKey.TryGetValue(key, out t) || t.Fsm == null)
|
|
{
|
|
if (FsmSync.Debug) MscoLog.Warning("[MSCO] << DOOR not found: " + key);
|
|
return;
|
|
}
|
|
}
|
|
|
|
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;
|
|
t.QuietUntil = Time.realtimeSinceStartup + Cooldown; // Folge-Zustände nicht zurückschicken
|
|
t.LastState = state;
|
|
FsmSync.SwitchStateSilently(t.Fsm, target);
|
|
if (FsmSync.Debug) MscoLog.Info("[MSCO] << DOOR " + key + " -> " + state);
|
|
}
|
|
|
|
void Scan()
|
|
{
|
|
foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f))
|
|
{
|
|
if (fsm == null) continue;
|
|
string root = fsm.transform.root.name;
|
|
if (root == "PLAYER" || root.StartsWith("MSCO_")) continue;
|
|
if (!Matches(fsm.gameObject.name) && !Matches(fsm.FsmName) && !Matches(fsm.transform.parent != null ? fsm.transform.parent.name : "")) continue;
|
|
if (IsPartLogic(fsm)) continue;
|
|
|
|
string key = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName;
|
|
Tracked t;
|
|
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();
|
|
if (o.StartsWith("trigger_") || o.StartsWith("bolt")) return true;
|
|
foreach (string x in ExcludedFsms) if (f.Contains(x)) return true;
|
|
return false;
|
|
}
|
|
|
|
static bool Matches(string name)
|
|
{
|
|
if (string.IsNullOrEmpty(name)) return false;
|
|
string lower = name.ToLower();
|
|
foreach (string k in Keywords)
|
|
if (lower.Contains(k)) return true;
|
|
return false;
|
|
}
|
|
}
|
|
}
|