Beta Release
This commit is contained in:
+396
@@ -0,0 +1,396 @@
|
||||
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())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ü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.
|
||||
/// </summary>
|
||||
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<FsmEventMsg> Outbox = new List<FsmEventMsg>();
|
||||
|
||||
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<string, List<PlayMakerFSM>> 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);
|
||||
ModConsole.Print("[MSCO] FSM sync ready");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModConsole.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<FsmState> States = new List<FsmState>();
|
||||
}
|
||||
|
||||
static readonly Dictionary<Fsm, ClickCandidate> clickCandidates = new Dictionary<Fsm, ClickCandidate>();
|
||||
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)
|
||||
{
|
||||
ModConsole.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;
|
||||
}
|
||||
|
||||
/// <summary>Jedes Frame vom Manager aufrufen: abgeschlossene Klicks auswerten.</summary>
|
||||
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) ModConsole.Print("[MSCO] >> CLICK " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]");
|
||||
}
|
||||
|
||||
/// <summary>Prüft der Zustand nur Maus/Tasten/Raycast (z.B. "Maus zeigt auf den Griff")?</summary>
|
||||
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) ModConsole.Print("[MSCO] >> " + msg.EventName + " " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModConsole.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) ModConsole.Warning("[MSCO] << not found: " + msg.Path + " [" + msg.FsmName + "]");
|
||||
return;
|
||||
}
|
||||
FsmState state = fsm.Fsm.GetState(msg.State);
|
||||
if (state == null || switchState == null) return;
|
||||
|
||||
if (Debug) ModConsole.Print("[MSCO] << " + msg.EventName + " " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]");
|
||||
applying++;
|
||||
lastApplyTime = Time.realtimeSinceStartup;
|
||||
try { switchState.Invoke(fsm.Fsm, new object[] { state }); }
|
||||
catch (Exception e) { ModConsole.Error("[MSCO] FSM apply error: " + e.Message); }
|
||||
finally { applying--; }
|
||||
}
|
||||
|
||||
/// <summary>FSM in einen Zustand schalten, ohne dass das als eigene Aktion erkannt und gesendet wird.</summary>
|
||||
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) { ModConsole.Error("[MSCO] FSM switch error: " + e.Message); }
|
||||
finally { applying--; }
|
||||
}
|
||||
|
||||
/// <summary>Pfad eines Objekts, auf allen Rechnern gleich (siehe BuildPath).</summary>
|
||||
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<PlayMakerFSM> 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<string, List<PlayMakerFSM>>();
|
||||
foreach (PlayMakerFSM fsm in SceneCache.Fsms(1f))
|
||||
{
|
||||
if (fsm == null) continue;
|
||||
string key = fsm.gameObject.name + "|" + fsm.FsmName;
|
||||
List<PlayMakerFSM> list;
|
||||
if (!fsmCache.TryGetValue(key, out list)) fsmCache[key] = list = new List<PlayMakerFSM>();
|
||||
list.Add(fsm);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Pfade
|
||||
|
||||
/// <summary>
|
||||
/// "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.
|
||||
/// </summary>
|
||||
static string BuildPath(Transform t)
|
||||
{
|
||||
var segments = new List<string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user