using System; using System.Collections.Generic; using System.IO; using System.Text; using HutongGames.PlayMaker; using UnityEngine; namespace MSCO { /// /// Gesamtzustand der Fahrzeuge (vor allem Satsuma): Werte in den PlayMaker-FSMs unter dem Fahrzeug, /// z.B. Tankinhalt, Öl, Kühlwasser, Bremsflüssigkeit, Batterieladung, Schrauben-Festigkeit, /// Schäden/Verschleiß, Kilometerstand, Lack, Einstellungen (Vergaser, Zündung ...). /// Wer das Fahrzeug gerade besitzt (fährt/schiebt), gibt die Werte vor; ist es frei, /// schickt jeder seine Änderungen (z.B. Öl nachfüllen, Schraube festziehen). /// public class VehicleStateSync { // Nur Werte, die den Zustand beschreiben - nicht Drehzahl, Lenkung, Raycasts usw. static readonly string[] Keywords = { "fuel", "oil", "coolant", "water", "fluid", "brake", "clutchfluid", "battery", "charge", "damage", "wear", "condition", "odo", "km", "dirt", "paint", "color", "colour", "tune", "tuning", "ratio", "broken", "fixed", "flat", "pressure", "level", "mixture", "timing", "idle", "air", "jet", "spark", "gap", "rust", "health", "burn", "cool", "temp" }; // Montiert-Flags nicht direkt setzen: das echte Anbauen läuft über FsmSync (Events) // Schrauben/Festigkeit laufen über FsmSync (Events) - ein falscher Wert hier würde Teile lockern und abfallen lassen // Vom Spiel selbst berechnete Werte ebenfalls nicht: z.B. LeakOilpan/LeakOilfilter (leckt Öl?) ergibt sich aus // angebauten/festgezogenen Teilen - ein übertragener alter Wert ließ beim Beitreten Öl auslaufen. Raten (…Rate) // rechnet das Spiel laufend neu aus. static readonly string[] Excluded = { "install", "bolt", "tight", "detach", "attach", "rpm", "speed", "steer", "throttle", "gear", "raycast", "distance", "leak", "rate", // Spieler-Werte (Database/PlayerDatabase: Schmutz, Verbrennungen ...) sind pro Spieler; Fluch-Geräusche, // Ton-Verzerrung und Anzeige-Hilfswerte (GUI) rechnet jeder selbst "player", "swear", "distortion", "gui" }; // Wurzeln außer den Fahrzeugen: Satsuma-Teile (lose und angebaut) und ihre Datenbank (Verschleiß, beschädigt, // Lack, Füllstände ...). Maßgeblich ist dort wie beim Satsuma selbst, wer den Satsuma gerade fährt. static readonly string[] PartRoots = { "CARPARTS", "Database" }; const string PartOwnerPrefix = "SATSUMA"; const float ScanInterval = 10f; const float CheckInterval = 0.5f; const int MaxPerMessage = 200; class Var { public int Key; public string Name; // Pfad|FSM|Variable (für die Diagnose "vars") public string Root; public NamedVariable Variable; public byte Type; // 1 float, 2 int, 3 bool (übertragen); Farben als 4 einzelne Kanäle vom Typ 1 public int Channel = -1; // FsmColor: 0-3 = r/g/b/a public PlayMakerFSM Fsm; public float LastF; public int LastI; public bool LastB; } readonly Dictionary vars = new Dictionary(); readonly Dictionary pending = new Dictionary(); // noch nicht gefundene Werte readonly List outgoing = new List(); // Erfasste FSMs mit dem Elternobjekt beim Erfassen: wird ein Teil an-/abgebaut, ändert sich sein Pfad (und damit // die Schlüssel) - dann neu erfassen. FSMs außerhalb der Fahrzeuge/Teile werden bei jedem Suchlauf neu geprüft // (vorher einmal als "nicht Fahrzeug" gemerkt und nie wieder angesehen: später angebaute Teile fehlten) readonly Dictionary scanned = new Dictionary(); readonly Dictionary> keysByFsm = new Dictionary>(); string satsumaRoot; float nextScan, nextCheck; public int Count { get { return vars.Count; } } /// Diagnose (Konsole "vars "): synchronisierte Fahrzeugwerte mit aktuellem Wert. public List Describe(string filter, int max) { var lines = new List(); string f = (filter ?? "").ToLower(); foreach (Var v in vars.Values) { if (f.Length > 0 && (v.Name == null || !v.Name.ToLower().Contains(f))) continue; string value = v.Type == 1 ? GetFloat(v).ToString("0.###") : v.Type == 2 ? ((FsmInt)v.Variable).Value.ToString() : ((FsmBool)v.Variable).Value.ToString(); lines.Add(" " + v.Name + " = " + value); if (lines.Count >= max) { lines.Add(" ... (more - use a narrower filter)"); break; } } if (lines.Count == 0) lines.Add(" no synced vehicle value matches '" + filter + "'"); return lines; } public void Reset() { vars.Clear(); pending.Clear(); scanned.Clear(); keysByFsm.Clear(); satsumaRoot = null; nextScan = -1f; // erster Suchlauf verteilt, siehe Update } public void Update(ObjectSync objects, int localId, Action send) { if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen if (Time.time >= nextScan) { nextScan = Time.time + ScanInterval; Scan(); } if (Time.time < nextCheck) return; nextCheck = Time.time + CheckInterval; outgoing.Clear(); ResolveSatsuma(objects); foreach (Var v in vars.Values) { // Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden int owner = objects.VehicleOwner(OwnerRoot(v.Root)); if (owner != SyncedBody.NoOwner && owner != localId) { if (Changed(v, false)) Remember(v); continue; } // Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) - // langsames Abdriften (z.B. Abkühlen) rechnet jeder selbst, sonst überschreiben sich alle gegenseitig bool big = owner == SyncedBody.NoOwner; if (!Changed(v, big)) continue; Remember(v); outgoing.Add(v); } for (int start = 0; start < outgoing.Count; start += MaxPerMessage) { int from = start, count = Math.Min(MaxPerMessage, outgoing.Count - start); send(MsgType.VarSync, NetSession.Build(w => { w.Write(count); for (int i = from; i < from + count; i++) Write(w, outgoing[i]); })); } } public void Handle(int from, BinaryReader r, ObjectSync objects, int localId) { int count = r.ReadInt32(); for (int i = 0; i < count; i++) { int key = r.ReadInt32(); byte type = r.ReadByte(); byte[] value = r.ReadBytes(type == 3 ? 1 : 4); Var v; if (!vars.TryGetValue(key, out v)) { pending[key] = Combine(type, value); continue; } if (from >= 0 && objects.VehicleOwner(OwnerRoot(v.Root)) == localId) continue; // ich fahre gerade: meine Werte gelten Apply(v, type, value); } } /// Gespeicherte Werte vom Server (Snapshot) - auch für Teile, die erst später auftauchen. public void ApplySnapshot(BinaryReader r) { int count = r.ReadInt32(); for (int i = 0; i < count; i++) { int key = r.ReadInt32(); byte type = r.ReadByte(); byte[] value = r.ReadBytes(type == 3 ? 1 : 4); Var v; if (vars.TryGetValue(key, out v)) Apply(v, type, value); else pending[key] = Combine(type, value); } } // ---------------------------------------------------------------- intern void Scan() { int before = vars.Count; foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f)) { if (fsm == null) continue; // Schon erfasst und nicht umgehängt (an-/abgebaut): nichts zu tun (Pfad + Hash kosten) Transform parent; if (scanned.TryGetValue(fsm, out parent) && parent == fsm.transform.parent) continue; string root = fsm.transform.root.name; if (!IsTracked(root)) { Forget(fsm); continue; } scanned[fsm] = fsm.transform.parent; Forget(fsm); // FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert // kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed") if (TouchesTransform(fsm)) continue; if (fsm.transform.root.name == "Database" && FsmSync.PathOf(fsm.transform).Contains("PlayerDatabase")) continue; string prefix = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|"; FsmVariables fv = fsm.FsmVariables; foreach (FsmFloat f in fv.FloatVariables) Register(fsm, prefix, root, f, 1, -1); foreach (FsmInt n in fv.IntVariables) Register(fsm, prefix, root, n, 2, -1); foreach (FsmBool b in fv.BoolVariables) Register(fsm, prefix, root, b, 3, -1); // Lack-/Farbwerte (FsmColor) als vier Zahlen: alte Server/Mods kennen nur float/int/bool foreach (FsmColor c in fv.ColorVariables) { if (c == null || !IsColorName(c.Name)) continue; for (int ch = 0; ch < 4; ch++) Register(fsm, prefix, root, c, 1, ch); } } if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced"); } static readonly string[] TransformActionWords = { "Scale", "Position", "Rotate", "Rotation", "Translate", "Parent" }; static bool TouchesTransform(PlayMakerFSM fsm) { try { foreach (FsmState state in fsm.FsmStates) { if (!state.ActionsLoaded) state.LoadActions(); if (state.Actions == null) continue; foreach (FsmStateAction action in state.Actions) { if (action == null) continue; string type = action.GetType().Name; foreach (string word in TransformActionWords) if (type.Contains(word)) return true; } } } catch { return true; } // im Zweifel nicht anfassen return false; } void Register(PlayMakerFSM fsm, string prefix, string root, NamedVariable variable, byte type, int channel) { if (variable == null || string.IsNullOrEmpty(variable.Name)) return; if (channel < 0 && !Wanted(variable.Name)) return; string name = prefix + variable.Name + (channel >= 0 ? "#" + "rgba"[channel] : ""); int key = Hash(name); Var existing; if (vars.TryGetValue(key, out existing) && existing.Variable == variable) return; var v = new Var { Key = key, Name = name, Root = root, Variable = variable, Type = type, Channel = channel, Fsm = fsm }; Remember(v); vars[key] = v; List keys; if (!keysByFsm.TryGetValue(fsm, out keys)) keysByFsm[fsm] = keys = new List(); keys.Add(key); byte[] stored; if (pending.TryGetValue(key, out stored)) { pending.Remove(key); byte[] value = new byte[stored.Length - 1]; Array.Copy(stored, 1, value, 0, value.Length); Apply(v, stored[0], value); } } static bool Wanted(string name) { string n = name.ToLower(); foreach (string x in Excluded) if (n.Contains(x)) return false; foreach (string k in Keywords) if (n.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 bool IsTracked(string root) { return IsVehicle(root) || Array.IndexOf(PartRoots, root) >= 0; } static bool IsColorName(string name) { string n = name.ToLower(); return n.Contains("paint") || n.Contains("color") || n.Contains("colour"); } /// Werte einer umgehängten/verschwundenen FSM vergessen (Pfad und damit Schlüssel ändern sich). void Forget(PlayMakerFSM fsm) { List keys; if (!keysByFsm.TryGetValue(fsm, out keys)) return; foreach (int k in keys) { Var v; if (vars.TryGetValue(k, out v) && v.Fsm == fsm) vars.Remove(k); } keysByFsm.Remove(fsm); } /// Satsuma-Teile gehören dem, der den Satsuma fährt. string OwnerRoot(string root) { return Array.IndexOf(PartRoots, root) >= 0 ? satsumaRoot : root; } void ResolveSatsuma(ObjectSync objects) { if (satsumaRoot != null) return; foreach (SyncedBody b in objects.AllVehicles()) if (b.RootName != null && b.RootName.StartsWith(PartOwnerPrefix)) { satsumaRoot = b.RootName; return; } } static float GetFloat(Var v) { if (v.Channel < 0) return ((FsmFloat)v.Variable).Value; Color c = ((FsmColor)v.Variable).Value; return c[v.Channel]; } static void SetFloat(Var v, float f) { if (v.Channel < 0) { ((FsmFloat)v.Variable).Value = f; return; } var cv = (FsmColor)v.Variable; Color c = cv.Value; c[v.Channel] = Mathf.Clamp(f, 0f, 4f); cv.Value = c; ShowPaint(v, c); } /// /// Lack sichtbar machen: Die Lackier-FSM eines Teils setzt beim Lackieren die Materialfarbe ihres eigenen Meshes. /// Beim Mitspieler kommt nur der Farbwert an - also dieselbe Farbe aufs Mesh des Teils legen. /// static void ShowPaint(Var v, Color c) { if (v.Fsm == null || v.Fsm.FsmName.IndexOf("paint", StringComparison.OrdinalIgnoreCase) < 0) return; Renderer r = v.Fsm.GetComponent(); if (r == null || r.material == null) return; r.material.color = c; } static bool Changed(Var v, bool bigOnly) { switch (v.Type) { case 1: float f = GetFloat(v); float delta = Mathf.Abs(f - v.LastF); if (v.Channel >= 0) return delta > 0.002f; // Farben: jede Änderung (Lackieren) return bigOnly ? delta > Mathf.Max(0.5f, Mathf.Abs(v.LastF) * 0.05f) : delta > Mathf.Max(0.01f, Mathf.Abs(v.LastF) * 0.002f); case 2: return ((FsmInt)v.Variable).Value != v.LastI; default: return ((FsmBool)v.Variable).Value != v.LastB; } } static void Remember(Var v) { switch (v.Type) { case 1: v.LastF = GetFloat(v); break; case 2: v.LastI = ((FsmInt)v.Variable).Value; break; default: v.LastB = ((FsmBool)v.Variable).Value; break; } } static void Apply(Var v, byte type, byte[] value) { if (type != v.Type) return; switch (type) { case 1: float f = BitConverter.ToSingle(value, 0); if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen SetFloat(v, f); break; case 2: ((FsmInt)v.Variable).Value = BitConverter.ToInt32(value, 0); break; default: ((FsmBool)v.Variable).Value = value[0] != 0; break; } Remember(v); // nicht als eigene Änderung zurückschicken } static void Write(BinaryWriter w, Var v) { w.Write(v.Key); w.Write(v.Type); switch (v.Type) { case 1: w.Write(v.LastF); break; case 2: w.Write(v.LastI); break; default: w.Write(v.LastB); break; } } static byte[] Combine(byte type, byte[] value) { var all = new byte[value.Length + 1]; all[0] = type; Array.Copy(value, 0, all, 1, value.Length); return all; } static int Hash(string s) { unchecked { uint h = 2166136261; foreach (byte c in Encoding.UTF8.GetBytes(s)) { h ^= c; h *= 16777619; } return (int)h; } } } }