From 3b83644ce9b8976038c1e73721329027652a72b5 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Fri, 2 Oct 2026 14:10:42 +0200 Subject: [PATCH] Satsuma: sync part state from CARPARTS/Database, paint, late-installed parts, more controls - VehicleStateSync also tracks the CARPARTS and Database roots (wear, damaged parts, tire wear, oilpan oil, battery charge, carb/ignition settings, suspension, odometer); the Satsuma's driver is authoritative for them - Paint colors (FsmColor) as four float channels (compatible with old servers) and applied to the part's material - FSMs are re-examined when a part is installed/removed (path changes); before, anything not on the car at the first scan was never synced - ControlSync: handbrake, sun visors, seat recline, seat belt, CD player, interior light; pending states for controls not found yet - Late joiners: job/shop values and switch states from the server snapshot; P2P host resends all switch states every 15 s Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 ++++ ControlSync.cs | 63 +++++++++++++++---- MultiplayerManager.cs | 15 ++++- ObjectSync.cs | 1 + VehicleStateSync.cs | 139 ++++++++++++++++++++++++++++++++++++------ WorldVarSync.cs | 25 +++++++- 6 files changed, 221 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfcad10..44d4b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,18 @@ - Geht jetzt auch, **wenn noch niemand fährt** (Sitzposition kommt aus dem Fahrersitz des Spiels); fährt danach jemand los, fährt man mit. Setzt man sich selbst ans Steuer, endet der Beifahrer-Modus +**Satsuma vollständiger synchron** +- Zustand aller Satsuma-Teile wird jetzt übertragen - auch der Teile, die das Spiel außerhalb des Autos verwaltet + (CARPARTS und die Teile-Datenbank): Verschleiß (Kolben, Kupplung, Getriebe, Lichtmaschine ...), beschädigte Teile + (z.B. Kolben/Zylinderkopf nach Überhitzen), Reifen-Verschleiß, Öl im Ölwannen-Teil (Menge, Verschmutzung), Batterie- + Ladung, Vergaser-/Zündeinstellungen, Fahrwerk, Kilometerstand. Maßgeblich ist, wer den Satsuma gerade fährt +- **Lack**: Lackfarben werden übertragen und sofort auf das Teil gelegt +- Teile, die erst während des Spiels angebaut werden (Radio, Sitze, Armaturen ...), werden jetzt auch erfasst - vorher + wurden sie einmal als „nicht am Auto“ gemerkt und nie wieder angesehen +- Mehr Bedienelemente: Handbremse, Sonnenblenden, Sitzlehne, Gurt, CD-Spieler, Innenlicht +- **Später Beitretende** bekommen Schalter (Licht an, Motor läuft ...) und Jobs/Laden vom Server (ab Server 2.9.2); + im P2P gleicht der Host die Schalter alle 15 s ab + **Satsuma zerfällt nicht mehr beim gemeinsamen Fahren** - Hing das Spiel des Fahrers kurz (z.B. beim Speichern für den Server), gab der Mitfahrer das Auto nach 3 s frei: es blieb bei ihm schlagartig stehen, die Teile wollten mit Tempo weiter und rissen ab - und das „Teil abgefallen“ ging an diff --git a/ControlSync.cs b/ControlSync.cs index 40b947b..c8dc7c9 100644 --- a/ControlSync.cs +++ b/ControlSync.cs @@ -21,7 +21,9 @@ namespace MSCO static readonly string[] Keywords = { "ignition", "starter", "engine", "light", "choke", "wiper", "hazard", "indicator", "turnsignal", "blinker", - "horn", "heater", "fan", "radio", "key", "switch", "knob", "button" + "horn", "heater", "fan", "radio", "key", "switch", "knob", "button", + // Handbremse, Sonnenblenden, Sitzlehne, Gurt, CD-Spieler, Innenlicht, Tankdeckel + "handbrake", "sunvisor", "seattop", "seatbelt", "cdplayer", "interiorlight", "opencap" }; // Montage/Speichern/Schrauben - läuft über den Objekt- bzw. FSM-Sync, hier nicht anfassen static readonly string[] Excluded = @@ -51,6 +53,8 @@ namespace MSCO readonly Dictionary controls = new Dictionary(); readonly HashSet scanned = new HashSet(); readonly List outgoing = new List(); + // Zustände für Schalter, die wir noch nicht kennen (Teil noch nicht angebaut/aktiv, Server-Snapshot vor dem Suchlauf) + readonly Dictionary pending = new Dictionary(); float nextScan, nextCheck; public int Count { get { return controls.Count; } } @@ -60,6 +64,7 @@ namespace MSCO { controls.Clear(); scanned.Clear(); + pending.Clear(); nextScan = -1f; // erster Suchlauf verteilt, siehe Update } @@ -115,32 +120,68 @@ namespace MSCO int key = r.ReadInt32(); string state = r.ReadString(); Control c; - if (!controls.TryGetValue(key, out c) || c.Fsm == null) { nextScan = 0f; continue; } + if (!controls.TryGetValue(key, out c) || c.Fsm == null) { pending[key] = state; 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") + SwitchTo(c, state); } } + void SwitchTo(Control c, string state) + { + if (c.Fsm.ActiveStateName == state) { c.LastState = state; return; } + FsmState target = c.Fsm.Fsm.GetState(state); + if (target == null) return; + 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(20f)) { - if (fsm == null || !scanned.Add(fsm)) continue; + if (fsm == null || scanned.Contains(fsm)) continue; string root = fsm.transform.root.name; - if (!IsVehicle(root) || !Wanted(fsm.gameObject.name, fsm.FsmName)) continue; + // Noch nicht am Fahrzeug (z.B. Radio liegt in CARPARTS): beim nächsten Suchlauf nochmal ansehen + if (!IsVehicle(root)) continue; + scanned.Add(fsm); + if (!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 }; + var control = new Control { Key = key, Root = root, Fsm = fsm, LastState = fsm.ActiveStateName, WindowStart = Time.time }; + controls[key] = control; + string waiting; + if (pending.TryGetValue(key, out waiting)) { pending.Remove(key); SwitchTo(control, waiting); } } if (controls.Count != before) MscoLog.Info("[MSCO] Vehicle controls (ignition, lights, engine ...): " + controls.Count + " synced"); } + /// + /// P2P-Host: alle Schalterzustände nochmal schicken (Licht an, Motor läuft ...) - für später Beigetretene und + /// falls eine Nachricht verloren ging. Fahrzeuge, die ein anderer fährt, gibt dieser vor. + /// + public void ResendAll(ObjectSync objects, int localId, Action send) + { + outgoing.Clear(); + foreach (Control c in controls.Values) + { + if (c.Fsm == null || c.Muted || string.IsNullOrEmpty(c.Fsm.ActiveStateName)) continue; + int owner = objects.VehicleOwner(c.Root); + if (owner != SyncedBody.NoOwner && owner != localId) 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].Fsm.ActiveStateName); } + })); + } + } + static bool Wanted(string objectName, string fsmName) { string o = objectName.ToLower(), f = fsmName.ToLower(); diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index 40a0a2a..e2c2908 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -65,6 +65,7 @@ namespace MSCO readonly TrafficSync traffic = new TrafficSync(); readonly HingeSync hinges = new HingeSync(); readonly ControlSync controls = new ControlSync(); + float nextControlResync; readonly Toasts toasts = new Toasts(); float nextNeedsSend; const float NeedsInterval = 10f; @@ -1940,7 +1941,16 @@ namespace MSCO PartEnd("crash damage sync"); // Zündung, Motor, Licht, Choke ... der Fahrzeuge PartStart(); - try { if (objects.ScanComplete && localPlayer != null) controls.Update(objects, localId, localPlayer.position, SendGame); } + try + { + if (objects.ScanComplete && localPlayer != null) controls.Update(objects, localId, localPlayer.position, SendGame); + // P2P: der Host gleicht regelmäßig alle Schalter ab (später Beigetretene, verlorene Nachrichten) + if (objects.ScanComplete && IsHost && !dedicated && Time.time >= nextControlResync) + { + nextControlResync = Time.time + 15f; + controls.ResendAll(objects, localId, SendGame); + } + } catch (System.Exception e) { ReportError("vehicle controls sync", e); } PartEnd("vehicle controls sync"); // Türen, Motorhaube, Kofferraum der Fahrzeuge @@ -2188,6 +2198,9 @@ namespace MSCO int needs = PlayerNeeds.Apply(r); if (needs > 0) MscoLog.Info("[MSCO] Your own hunger/thirst/... restored (" + needs + " values)"); } + // Jobs/Laden und Schalter (Licht, Zündung, Motor ...) - Server ab 2.9.2 + if (r.BaseStream.Length - r.BaseStream.Position >= 4) worldVars.Handle(r); + if (r.BaseStream.Length - r.BaseStream.Position >= 4) controls.Handle(r, objects, localId); // Abgebaute Teile und gespawnte Items sind jetzt eigene Objekte: erfassen, dann alle Positionen setzen yield return null; diff --git a/ObjectSync.cs b/ObjectSync.cs index bdde037..5e1a08a 100644 --- a/ObjectSync.cs +++ b/ObjectSync.cs @@ -305,6 +305,7 @@ namespace MSCO /// Besitzer eines Fahrzeugs (nach Wurzelname), NoOwner wenn frei oder unbekannt. public int VehicleOwner(string rootName) { + if (rootName == null) return SyncedBody.NoOwner; foreach (SyncedBody b in byId.Values) if (b.IsVehicle && b.RootName == rootName) return b.OwnerId; return SyncedBody.NoOwner; diff --git a/VehicleStateSync.cs b/VehicleStateSync.cs index 720c175..cbbdaef 100644 --- a/VehicleStateSync.cs +++ b/VehicleStateSync.cs @@ -34,9 +34,17 @@ namespace MSCO static readonly string[] Excluded = { "install", "bolt", "tight", "detach", "attach", "rpm", "speed", "steer", "throttle", "gear", "raycast", "distance", - "leak", "rate" + "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; @@ -47,7 +55,9 @@ namespace MSCO 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 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; @@ -56,7 +66,12 @@ namespace MSCO readonly Dictionary vars = new Dictionary(); readonly Dictionary pending = new Dictionary(); // noch nicht gefundene Werte readonly List outgoing = new List(); - readonly HashSet scanned = new HashSet(); + // 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; } } @@ -69,7 +84,7 @@ namespace MSCO 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.###") + 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); @@ -84,6 +99,8 @@ namespace MSCO vars.Clear(); pending.Clear(); scanned.Clear(); + keysByFsm.Clear(); + satsumaRoot = null; nextScan = -1f; // erster Suchlauf verteilt, siehe Update } @@ -99,10 +116,11 @@ namespace MSCO 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(v.Root); + 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 ...) - @@ -134,7 +152,7 @@ namespace MSCO 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 + if (from >= 0 && objects.VehicleOwner(OwnerRoot(v.Root)) == localId) continue; // ich fahre gerade: meine Werte gelten Apply(v, type, value); } } @@ -161,18 +179,29 @@ namespace MSCO int before = vars.Count; foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f)) { - // Schon erfasste FSMs nicht jedes Mal neu durchgehen (Pfad + Hash kosten) - if (fsm == null || !scanned.Add(fsm)) continue; + 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 (!IsVehicle(root)) continue; + 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(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); + 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"); } @@ -200,15 +229,20 @@ namespace MSCO return false; } - void Register(string prefix, string root, NamedVariable variable, byte type) + void Register(PlayMakerFSM fsm, string prefix, string root, NamedVariable variable, byte type, int channel) { - if (variable == null || string.IsNullOrEmpty(variable.Name) || !Wanted(variable.Name)) return; - int key = Hash(prefix + variable.Name); + 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 = prefix + variable.Name, Root = root, Variable = variable, Type = type }; + 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)) @@ -234,13 +268,80 @@ namespace MSCO 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 = ((FsmFloat)v.Variable).Value; + 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); @@ -253,7 +354,7 @@ namespace MSCO { switch (v.Type) { - case 1: v.LastF = ((FsmFloat)v.Variable).Value; break; + case 1: v.LastF = GetFloat(v); break; case 2: v.LastI = ((FsmInt)v.Variable).Value; break; default: v.LastB = ((FsmBool)v.Variable).Value; break; } @@ -267,7 +368,7 @@ namespace MSCO 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; + SetFloat(v, f); break; case 2: ((FsmInt)v.Variable).Value = BitConverter.ToInt32(value, 0); break; default: ((FsmBool)v.Variable).Value = value[0] != 0; break; diff --git a/WorldVarSync.cs b/WorldVarSync.cs index fe142ac..3a0aaf7 100644 --- a/WorldVarSync.cs +++ b/WorldVarSync.cs @@ -41,6 +41,8 @@ namespace MSCO readonly Dictionary vars = new Dictionary(); readonly HashSet scanned = new HashSet(); readonly List outgoing = new List(); + // Werte für noch nicht gefundene FSMs (Server-Snapshot vor dem ersten Suchlauf): [typ, wert] + readonly Dictionary> pending = new Dictionary>(); float nextScan, nextCheck; public int Count { get { return vars.Count; } } @@ -49,6 +51,7 @@ namespace MSCO { vars.Clear(); scanned.Clear(); + pending.Clear(); nextScan = -1f; // erster Suchlauf verteilt, siehe Update } @@ -102,9 +105,13 @@ namespace MSCO int iv = 0; bool bv = false; if (type == 2) iv = r.ReadInt32(); else if (type == 3) bv = r.ReadBoolean(); else return; Var v; - if (!vars.TryGetValue(key, out v) || v.Type != type) continue; - if (type == 2) ((FsmInt)v.Variable).Value = iv; else ((FsmBool)v.Variable).Value = bv; - Remember(v); // nicht zurückschicken + if (!vars.TryGetValue(key, out v)) + { + if (pending.Count < 20000) pending[key] = new KeyValuePair(type, type == 2 ? iv : (bv ? 1 : 0)); + continue; + } + if (v.Type != type) continue; + Set(v, type == 2 ? iv : (bv ? 1 : 0)); } } @@ -131,6 +138,18 @@ namespace MSCO var v = new Var { Key = key, Variable = variable, Type = type }; Remember(v); vars[key] = v; + KeyValuePair waiting; + if (pending.TryGetValue(key, out waiting)) + { + pending.Remove(key); + if (waiting.Key == type) Set(v, waiting.Value); + } + } + + static void Set(Var v, int value) + { + if (v.Type == 2) ((FsmInt)v.Variable).Value = value; else ((FsmBool)v.Variable).Value = value != 0; + Remember(v); // nicht zurückschicken } static bool Changed(Var v)