using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Harmony;
using HutongGames.PlayMaker;
using MSCLoader;
using UnityEngine;
namespace MSCO
{
public struct FsmEventMsg
{
public string Path;
public string FsmName;
public string EventName;
public string State;
public Vector3 Position;
public byte[] Serialize()
{
FsmEventMsg m = this;
return NetSession.Build(w =>
{
w.Write(m.Path); w.Write(m.FsmName); w.Write(m.EventName); w.Write(m.State);
w.Write(m.Position.x); w.Write(m.Position.y); w.Write(m.Position.z);
});
}
public static FsmEventMsg Deserialize(BinaryReader r)
{
return new FsmEventMsg
{
Path = r.ReadString(), FsmName = r.ReadString(), EventName = r.ReadString(), State = r.ReadString(),
Position = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle())
};
}
}
///
/// Überträgt Spieler-Aktionen, die in PlayMaker-FSMs ablaufen: Schrauben anziehen/lösen,
/// Teile anbauen/abbauen. Dazu wird Fsm.Event per Harmony abgefangen; beim anderen
/// Spieler wird dieselbe FSM in denselben Zielzustand geschaltet.
///
public static class FsmSync
{
// Events, die direkt durch den Spieler ausgelöst werden
static readonly string[] SyncedEvents =
{
"TIGHTEN", "UNTIGHTEN", "ASSEMBLE", "DISASSEMBLE", "REMOVE", "INSTALL", "UNINSTALL"
};
// Folge-Events einer Aktion laufen auf beiden Seiten von selbst ab und dürfen nicht nochmal gesendet werden
const float ChainWindow = 0.3f;
public static bool Enabled;
public static bool Debug;
public static readonly List Outbox = new List();
static bool patched;
static int applying;
static float lastApplyTime = -100f;
static float lastSendTime = -100f;
static Fsm lastSendFsm;
static readonly MethodInfo switchState =
typeof(Fsm).GetMethod("SwitchState", BindingFlags.NonPublic | BindingFlags.Instance);
// "GameObject-Name|FSM-Name" -> FSMs, zum Wiederfinden beim Empfänger
static Dictionary> fsmCache;
static float lastCacheBuild = -100f;
public static void Init()
{
if (patched) return;
patched = true;
try
{
MethodInfo original = typeof(Fsm).GetMethod("Event", new[] { typeof(FsmEvent) });
MethodInfo prefix = typeof(FsmSync).GetMethod("EventPrefix", BindingFlags.NonPublic | BindingFlags.Static);
HarmonyInstance harmony = HarmonyInstance.Create("mbm.msco.fsmsync");
harmony.Patch(original, new HarmonyMethod(prefix), null);
// Klick-Interaktionen (Lichtschalter, Knöpfe, Radio ...) über Zustandswechsel erkennen
MethodInfo switchPrefix = typeof(FsmSync).GetMethod("SwitchStatePrefix", BindingFlags.NonPublic | BindingFlags.Static);
harmony.Patch(switchState, new HarmonyMethod(switchPrefix), null);
MscoLog.Info("[MSCO] FSM sync ready");
}
catch (Exception e)
{
MscoLog.Error("[MSCO] FSM sync patch failed: " + e);
}
}
public static void Reset()
{
Outbox.Clear();
fsmCache = null;
lastSendFsm = null;
clickCandidates.Clear();
clickActive = false;
}
// ---------------------------------------------------------------- Klick-Interaktionen
// Nach einem Klick wird gesammelt, welche FSMs in der Nähe den Zustand wechseln.
// Das Objekt, auf das der Spieler zielt, gewinnt; gesendet wird der erste Zustand,
// der nicht selbst auf Maus/Tasten/Raycast prüft (das würde beim Empfänger scheitern).
const float ClickWindow = 0.4f;
const float ClickRange = 4f;
static readonly string[] InputActionWords = { "Mouse", "Raycast", "GetKey", "GetButton", "Input", "Pick", "Axis" };
class ClickCandidate
{
public Fsm Fsm;
public float AimDistance;
public readonly List States = new List();
}
static readonly Dictionary clickCandidates = new Dictionary();
static bool clickActive;
static float clickTime;
static int lastClickCheckFrame = -1;
static bool useKeyAvailable = true;
static bool ClickThisFrame()
{
if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) return true;
if (!useKeyAvailable) return false;
try { return cInput.GetKeyDown("Use"); }
catch { useKeyAvailable = false; return false; }
}
static void SwitchStatePrefix(Fsm __instance, FsmState toState)
{
if (!Enabled || applying > 0 || toState == null) return;
try
{
float now = Time.realtimeSinceStartup;
if (now - lastApplyTime < ChainWindow) return;
if (!clickActive && lastClickCheckFrame != Time.frameCount)
{
lastClickCheckFrame = Time.frameCount;
if (ClickThisFrame())
{
clickActive = true;
clickTime = now;
clickCandidates.Clear();
}
}
if (!clickActive) return;
ClickCandidate c;
if (!clickCandidates.TryGetValue(__instance, out c))
{
float aim;
if (!IsAimedAt(__instance, out aim)) return;
c = new ClickCandidate { Fsm = __instance, AimDistance = aim };
clickCandidates[__instance] = c;
}
c.States.Add(toState);
}
catch (Exception e)
{
MscoLog.Error("[MSCO] Click capture error: " + e.Message);
}
}
static bool IsAimedAt(Fsm fsm, out float aimDistance)
{
aimDistance = float.MaxValue;
GameObject go = fsm.GameObject;
Camera cam = Camera.main;
if (go == null || cam == null) return false;
string root = go.transform.root.name;
if (root == "PLAYER" || root.StartsWith("MSCO_")) return false;
Vector3 toObject = go.transform.position - cam.transform.position;
if (toObject.magnitude > ClickRange || Vector3.Dot(toObject, cam.transform.forward) < 0f) return false;
aimDistance = Vector3.Cross(cam.transform.forward, toObject).magnitude; // Abstand zur Blickachse
return true;
}
/// Jedes Frame vom Manager aufrufen: abgeschlossene Klicks auswerten.
public static void Tick()
{
if (!clickActive || Time.realtimeSinceStartup - clickTime < ClickWindow) return;
clickActive = false;
// Schrauben/Montage wurden schon über die Events gesendet
if (Time.realtimeSinceStartup - lastSendTime < ClickWindow + ChainWindow) { clickCandidates.Clear(); return; }
ClickCandidate best = null;
foreach (ClickCandidate c in clickCandidates.Values)
if (best == null || c.AimDistance < best.AimDistance) best = c;
clickCandidates.Clear();
if (best == null || best.Fsm.GameObject == null) return;
FsmState target = null;
foreach (FsmState s in best.States)
if (!ChecksInput(s)) { target = s; break; }
if (target == null) return;
GameObject go = best.Fsm.GameObject;
var msg = new FsmEventMsg
{
Path = BuildPath(go.transform),
FsmName = best.Fsm.Name,
EventName = "CLICK",
State = target.Name,
Position = go.transform.position
};
Outbox.Add(msg);
if (Debug) MscoLog.Info("[MSCO] >> CLICK " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]");
}
/// Prüft der Zustand nur Maus/Tasten/Raycast (z.B. "Maus zeigt auf den Griff")?
public static bool ChecksInputPublic(FsmState state) { return ChecksInput(state); }
static bool ChecksInput(FsmState state)
{
if (!state.ActionsLoaded) state.LoadActions();
if (state.Actions == null) return false;
foreach (FsmStateAction action in state.Actions)
{
if (action == null) continue;
string type = action.GetType().Name;
foreach (string word in InputActionWords)
if (type.Contains(word)) return true;
}
return false;
}
// ---------------------------------------------------------------- Senden
static void EventPrefix(Fsm __instance, FsmEvent fsmEvent)
{
if (!Enabled || applying > 0 || fsmEvent == null) return;
if (Array.IndexOf(SyncedEvents, fsmEvent.Name) < 0) return;
try
{
float now = Time.realtimeSinceStartup;
if (now - lastApplyTime < ChainWindow) return; // Folge einer Remote-Aktion
if (__instance != lastSendFsm && now - lastSendTime < ChainWindow) return; // Folge einer eigenen Aktion
GameObject go = __instance.GameObject;
if (go == null || go.transform.root.name == "PLAYER") return;
string target = FindTargetState(__instance, fsmEvent.Name);
if (target == null) return; // Event bewirkt im aktuellen Zustand nichts
lastSendTime = now;
lastSendFsm = __instance;
var msg = new FsmEventMsg
{
Path = BuildPath(go.transform),
FsmName = __instance.Name,
EventName = fsmEvent.Name,
State = target,
Position = go.transform.position
};
Outbox.Add(msg);
if (Debug) MscoLog.Info("[MSCO] >> " + msg.EventName + " " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]");
}
catch (Exception e)
{
MscoLog.Error("[MSCO] FSM capture error: " + e.Message);
}
}
static string FindTargetState(Fsm fsm, string eventName)
{
FsmState active = fsm.ActiveState;
if (active != null)
foreach (FsmTransition t in active.Transitions)
if (t.EventName == eventName) return t.ToState;
foreach (FsmTransition t in fsm.GlobalTransitions)
if (t.EventName == eventName) return t.ToState;
return null;
}
// ---------------------------------------------------------------- Empfangen
public static void Apply(FsmEventMsg msg)
{
PlayMakerFSM fsm = Resolve(msg);
if (fsm == null)
{
if (Debug) MscoLog.Warning("[MSCO] << not found: " + msg.Path + " [" + msg.FsmName + "]");
return;
}
FsmState state = fsm.Fsm.GetState(msg.State);
if (state == null || switchState == null) return;
if (Debug) MscoLog.Info("[MSCO] << " + msg.EventName + " " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]");
applying++;
lastApplyTime = Time.realtimeSinceStartup;
try { switchState.Invoke(fsm.Fsm, new object[] { state }); }
catch (Exception e) { MscoLog.Error("[MSCO] FSM apply error: " + e.Message); }
finally { applying--; }
}
/// FSM in einen Zustand schalten, ohne dass das als eigene Aktion erkannt und gesendet wird.
public static void SwitchStateSilently(PlayMakerFSM fsm, FsmState state)
{
if (switchState == null) return;
applying++;
lastApplyTime = Time.realtimeSinceStartup;
try { switchState.Invoke(fsm.Fsm, new object[] { state }); }
catch (Exception e) { MscoLog.Error("[MSCO] FSM switch error: " + e.Message); }
finally { applying--; }
}
/// Pfad eines Objekts, auf allen Rechnern gleich (siehe BuildPath).
public static string PathOf(Transform t) { return BuildPath(t); }
static PlayMakerFSM Resolve(FsmEventMsg msg)
{
string goName = LastSegmentName(msg.Path);
PlayMakerFSM found = FindInCache(goName, msg);
if (found == null && Time.realtimeSinceStartup - lastCacheBuild > 1f)
{
BuildCache(); // neu aktivierte Objekte (z.B. Schrauben eines gerade montierten Teils)
found = FindInCache(goName, msg);
}
return found;
}
static PlayMakerFSM FindInCache(string goName, FsmEventMsg msg)
{
if (fsmCache == null) return null;
List list;
if (!fsmCache.TryGetValue(goName + "|" + msg.FsmName, out list)) return null;
// Gleicher Pfad; bei mehreren Treffern das nächstgelegene Objekt
PlayMakerFSM best = null;
float bestDist = float.MaxValue;
foreach (PlayMakerFSM fsm in list)
{
if (fsm == null || BuildPath(fsm.transform) != msg.Path) continue;
float d = (fsm.transform.position - msg.Position).sqrMagnitude;
if (d < bestDist) { bestDist = d; best = fsm; }
}
return best;
}
static void BuildCache()
{
lastCacheBuild = Time.realtimeSinceStartup;
fsmCache = new Dictionary>();
foreach (PlayMakerFSM fsm in SceneCache.Fsms(1f))
{
if (fsm == null) continue;
string key = fsm.gameObject.name + "|" + fsm.FsmName;
List list;
if (!fsmCache.TryGetValue(key, out list)) fsmCache[key] = list = new List();
list.Add(fsm);
}
}
// ---------------------------------------------------------------- Pfade
///
/// "SATSUMA(557kg, 248)|Body#0|alternator(Clone)#0|Bolts#0|bolt#2" - Name plus Nummer unter
/// gleichnamigen Geschwistern. Beim Wurzelobjekt nur der Name, Mehrdeutigkeit löst die Position.
///
static string BuildPath(Transform t)
{
var segments = new List();
for (; t != null; t = t.parent)
{
if (t.parent == null) { segments.Add(t.name); break; }
int ordinal = 0;
Transform parent = t.parent;
for (int i = 0; i < parent.childCount; i++)
{
Transform sibling = parent.GetChild(i);
if (sibling == t) break;
if (sibling.name == t.name) ordinal++;
}
segments.Add(t.name + "#" + ordinal);
}
segments.Reverse();
return string.Join("|", segments.ToArray());
}
static string LastSegmentName(string path)
{
string last = path.Substring(path.LastIndexOf('|') + 1);
int hash = last.LastIndexOf('#');
return hash >= 0 && path.Contains("|") ? last.Substring(0, hash) : last;
}
}
}