Files
MSCO/DoorSync.cs
T
MrMahoandClaude Opus 5.5 f707e67e5c Doors on all vehicles: no more part-logic FSMs in DoorSync, single sender for hinges
- DoorSync matched every FSM with door/hood/lid/trunk in its name, including
  the door parts' removal/assembly/bolt/paint FSMs and trigger_ objects, and
  forced their states at the other players: doors hung crooked, snapped back
  or came off. Those are excluded now (assembly runs via FsmSync).
- HingeSync: on a parked vehicle everyone nearby sent the angle, so doors
  ping-ponged. Now only the player who clicked near the door (4 s) sends;
  on a driven vehicle the driver sends; received angles are held 3 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:44:03 +02:00

139 lines
5.4 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using HutongGames.PlayMaker;
using MSCLoader;
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;
}
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;
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.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) continue;
byKey[key] = new Tracked { Key = key, Fsm = fsm, LastState = fsm.ActiveStateName };
}
}
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;
}
}
}