using System; using System.Collections.Generic; using System.IO; using System.Text; using HutongGames.PlayMaker; using MSCLoader; 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" }; 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 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(); readonly HashSet scanned = new HashSet(); 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 ? ((FsmFloat)v.Variable).Value.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(); 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(); foreach (Var v in vars.Values) { // Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden int owner = objects.VehicleOwner(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(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(5f)) { // Schon erfasste FSMs nicht jedes Mal neu durchgehen (Pfad + Hash kosten) if (fsm == null || !scanned.Add(fsm)) continue; string root = fsm.transform.root.name; if (!IsVehicle(root)) continue; // 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; string prefix = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|"; FsmVariables fv = fsm.FsmVariables; foreach (FsmFloat f in fv.FloatVariables) Register(prefix, root, f, 1); foreach (FsmInt n in fv.IntVariables) Register(prefix, root, n, 2); foreach (FsmBool b in fv.BoolVariables) Register(prefix, root, b, 3); } 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(string prefix, string root, NamedVariable variable, byte type) { if (variable == null || string.IsNullOrEmpty(variable.Name) || !Wanted(variable.Name)) return; int key = Hash(prefix + variable.Name); Var existing; if (vars.TryGetValue(key, out existing) && existing.Variable == variable) return; var v = new Var { Key = key, Name = prefix + variable.Name, Root = root, Variable = variable, Type = type }; Remember(v); vars[key] = v; 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 Changed(Var v, bool bigOnly) { switch (v.Type) { case 1: float f = ((FsmFloat)v.Variable).Value; float delta = Mathf.Abs(f - v.LastF); 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 = ((FsmFloat)v.Variable).Value; 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 ((FsmFloat)v.Variable).Value = 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; } } } }