using System; using System.Collections.Generic; using System.IO; using System.Text; using HutongGames.PlayMaker; using UnityEngine; namespace MSCO { /// /// Bedienelemente der Fahrzeuge: Zündschloss, Anlasser, Motor, Lichtschalter, Choke, Blinker, Scheibenwischer ... /// Das Spiel schaltet damit Motor und Licht über PlayMaker-FSMs (Zustände wie "Off" / "Lights" / "Running"). /// Übertragen wird der aktive Zustand; beim anderen Spieler wird dieselbe FSM in denselben Zustand geschaltet, /// sodass sein Spiel Motor, Geräusch, Scheinwerfer und Armaturen selbst richtig anschaltet. /// Wer sendet: der Fahrer; ist das Fahrzeug frei, wer daneben steht bzw. drin sitzt (Schalter betätigt). /// FSMs, die ständig den Zustand wechseln (interne Schleifen), werden nach kurzer Zeit nicht mehr übertragen. /// public class ControlSync { // Objekt- oder FSM-Name enthält eines davon static readonly string[] Keywords = { "ignition", "starter", "engine", "light", "choke", "wiper", "hazard", "indicator", "turnsignal", "blinker", "horn", "heater", "fan", "radio", "key", "switch", "knob", "button" }; // Montage/Speichern/Schrauben - läuft über den Objekt- bzw. FSM-Sync, hier nicht anfassen static readonly string[] Excluded = { "removal", "assembly", "assemble", "bolt", "screw", "data", "save", "load", "paint", "detach", "attach", "install", "trigger", "tighten", "wear", "damage", "oil", "coolant", "fuel", "inspection" }; const float ScanInterval = 10f; const float CheckInterval = 0.1f; const float NearRange = 5f; const int FlipLimit = 12; // so viele Zustandswechsel ... const float FlipWindow = 10f; // ... in dieser Zeit = interne Schleife, nicht übertragen const int MaxPerMessage = 100; class Control { public int Key; public string Root; public PlayMakerFSM Fsm; public string LastState; public float WindowStart; public int Flips; public bool Muted; } readonly Dictionary controls = new Dictionary(); readonly HashSet scanned = new HashSet(); readonly List outgoing = new List(); float nextScan, nextCheck; public int Count { get { return controls.Count; } } public int MutedCount { get { int n = 0; foreach (Control c in controls.Values) if (c.Muted) n++; return n; } } public void Reset() { controls.Clear(); scanned.Clear(); nextScan = 0f; } public void Update(ObjectSync objects, int localId, Vector3 playerPos, Action send) { if (Time.time >= nextScan) { nextScan = Time.time + ScanInterval; Scan(); } if (Time.time < nextCheck) return; nextCheck = Time.time + CheckInterval; outgoing.Clear(); foreach (Control c in controls.Values) { if (c.Fsm == null || c.Muted) continue; string state = c.Fsm.ActiveStateName; if (state == c.LastState || string.IsNullOrEmpty(state)) continue; c.LastState = state; if (Time.time - c.WindowStart > FlipWindow) { c.WindowStart = Time.time; c.Flips = 0; } if (++c.Flips > FlipLimit) { c.Muted = true; MscoLog.Info("[MSCO] Control not synced (changes all the time): " + c.Fsm.gameObject.name + " [" + c.Fsm.FsmName + "]"); continue; } int owner = objects.VehicleOwner(c.Root); if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer gibt vor if (owner == SyncedBody.NoOwner && (c.Fsm.transform.position - playerPos).sqrMagnitude > NearRange * NearRange) continue; outgoing.Add(c); } for (int start = 0; start < outgoing.Count; start += MaxPerMessage) { int from = start, count = Math.Min(MaxPerMessage, outgoing.Count - start); send(MsgType.VehicleControls, NetSession.Build(w => { w.Write(count); for (int i = from; i < from + count; i++) { w.Write(outgoing[i].Key); w.Write(outgoing[i].LastState); } })); } } public void Handle(BinaryReader r, ObjectSync objects, int localId) { int count = r.ReadInt32(); for (int i = 0; i < count; i++) { int key = r.ReadInt32(); string state = r.ReadString(); Control c; if (!controls.TryGetValue(key, out c) || c.Fsm == null) { nextScan = 0f; continue; } if (objects.VehicleOwner(c.Root) == localId) continue; // ich fahre: meine Schalter gelten if (c.Fsm.ActiveStateName == state) { c.LastState = state; continue; } FsmState target = c.Fsm.Fsm.GetState(state); if (target == null) continue; c.LastState = state; // nicht als eigene Änderung zurückschicken FsmSync.SwitchStateSilently(c.Fsm, target); c.LastState = c.Fsm.ActiveStateName; // FSM kann sofort weiterschalten (z.B. "Start" -> "Running") } } void Scan() { int before = controls.Count; foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f)) { if (fsm == null || !scanned.Add(fsm)) continue; string root = fsm.transform.root.name; if (!IsVehicle(root) || !Wanted(fsm.gameObject.name, fsm.FsmName)) continue; int key = Hash(FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName); if (controls.ContainsKey(key)) continue; controls[key] = new Control { Key = key, Root = root, Fsm = fsm, LastState = fsm.ActiveStateName, WindowStart = Time.time }; } if (controls.Count != before) MscoLog.Info("[MSCO] Vehicle controls (ignition, lights, engine ...): " + controls.Count + " synced"); } static bool Wanted(string objectName, string fsmName) { string o = objectName.ToLower(), f = fsmName.ToLower(); foreach (string x in Excluded) if (o.Contains(x) || f.Contains(x)) return false; foreach (string k in Keywords) if (o.Contains(k) || f.Contains(k)) return true; return false; } static bool IsVehicle(string root) { foreach (string v in ObjectSync.VehicleRoots) if (root.StartsWith(v)) return true; return false; } static int Hash(string s) { unchecked { uint h = 2166136261; foreach (byte c in Encoding.UTF8.GetBytes(s)) { h ^= c; h *= 16777619; } return (int)h; } } } }