using System; using System.Collections.Generic; using System.IO; using HutongGames.PlayMaker; using UnityEngine; namespace MSCO { /// /// 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 ...). /// 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 byKey = new Dictionary(); float nextScan; public int Count { get { return byKey.Count; } } public void Reset() { byKey.Clear(); nextScan = 0f; } public void Update(Action 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()); } /// Zustand übernehmen (von einem Mitspieler oder aus dem Server-Snapshot). 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(); 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(); 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; } } }