From 29a3697cc82242306502d2acab36c135438b5778 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Sat, 3 Oct 2026 12:16:29 +0200 Subject: [PATCH] MSCO 2.10.3: bolt stages and part tightness synced, snap to pivot, admin tighten/loosen - Every bolt (Screw FSM, stage 0-8) and every part's tightness (BoltCheck/Use "Tightness", counted by the tool) is synced as state - before only the bolt turned on other clients, the part stayed loose or tight there - Tightening a part (tool, admin, other player) snaps it exactly onto its pivot (installed parts sit at local 0), except screw-in parts and hinged doors/hoods - Admin VEHICLE tab: TIGHTEN/LOOSEN per part, TIGHTEN ALL/LOOSEN ALL, bolted/loose state - fsmdump "pivots": installed part offsets Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 +++++ FsmDump.cs | 21 ++++++++ MSCO.cs | 2 +- Overlays.cs | 37 ++++++++++++-- VehicleAdmin.cs | 118 ++++++++++++++++++++++++++++++++++++++++++-- VehicleStateSync.cs | 59 +++++++++++++++++++--- 6 files changed, 235 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf5694..2697db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 2.10.3 + +**Schrauben synchron** +- Jede Schraube wird jetzt als Stufe (0–8) übertragen und die Festigkeit jedes Teils dazu: vorher drehte sich beim + Mitspieler nur die Schraube, sein Teil blieb „lose“ bzw. „fest“ (die Festigkeit zählt im Spiel das Werkzeug hoch) – + Teile ließen sich dann nicht abbauen oder fielen ab +- Wird ein Teil festgezogen (Werkzeug, Admin oder vom Mitspieler), springt es exakt auf seinen Halter – verrutschte + Teile sitzen wieder richtig (nicht bei eingedrehten Teilen wie Zündkerzen/Ölfilter und nicht bei Türen/Hauben) + +**Admin-Menü, Reiter VEHICLE** +- Je Teil **TIGHTEN** / **LOOSEN** (alle Schrauben ganz fest bzw. ganz lose, wie mit dem Werkzeug) und oben + **TIGHTEN ALL** / **LOOSEN ALL**; Anzeige „bolted“/„loose“; das Fenster ist auf diesem Reiter breiter + ## 2.10.2 - **Beifahrer**: automatisch festgehalten wird man nur noch, wenn das Fahrzeug fährt (egal wer fährt) – am stehenden diff --git a/FsmDump.cs b/FsmDump.cs index 1531a2e..fdb2beb 100644 --- a/FsmDump.cs +++ b/FsmDump.cs @@ -17,6 +17,7 @@ namespace MSCO public static string Write(string filter) { string[] filters = (filter ?? "").ToLowerInvariant().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (filter != null && filter.Trim().ToLowerInvariant() == "pivots") return WritePivots(); if (filters.Length == 0) return "fsmdump [,text2] - e.g. fsmdump garage"; var sb = new StringBuilder(); int count = 0; @@ -51,6 +52,26 @@ namespace MSCO return count + " FSM(s) written to " + file; } + /// Eingebaute Teile: Lage relativ zu ihrem Halter (pivot_...) - ist sie immer 0? + static string WritePivots() + { + var sb = new StringBuilder(); + int count = 0; + foreach (Transform t in Resources.FindObjectsOfTypeAll()) + { + if (t == null || t.gameObject.hideFlags != HideFlags.None || !t.name.StartsWith("pivot_")) continue; + foreach (Transform child in t) + { + count++; + sb.Append(FsmSync.PathOf(child)).Append(" | pos ").Append(child.localPosition.ToString("F4")) + .Append(" rot ").Append(child.localEulerAngles.ToString("F1")).Append(" active ").Append(child.gameObject.activeInHierarchy).AppendLine(); + } + } + string file = Path.Combine(Bootstrap.Folder ?? ".", "fsmdump.txt"); + File.WriteAllText(file, sb.ToString()); + return count + " installed part(s) written to " + file; + } + static void Dump(StringBuilder sb, PlayMakerFSM fsm, string path) { sb.Append("==== ").Append(path).Append(" | ").Append(fsm.FsmName).Append(" | active ").Append(fsm.gameObject.activeInHierarchy) diff --git a/MSCO.cs b/MSCO.cs index 319e908..def62fb 100644 --- a/MSCO.cs +++ b/MSCO.cs @@ -11,7 +11,7 @@ namespace MSCO /// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit /// Servern und Mitspielern derselben ersten zwei Stellen kompatibel. /// - public const string ModVersion = "2.10.2"; + public const string ModVersion = "2.10.3"; /// /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// auch GameServer.ModVersion im Server. diff --git a/Overlays.cs b/Overlays.cs index 8095b05..0cc01ff 100644 --- a/Overlays.cs +++ b/Overlays.cs @@ -270,7 +270,7 @@ namespace MSCO void DrawAdmin(MultiplayerManager mp) { - float width = Mathf.Min(720, Screen.width - 40), height = Mathf.Min(580, Screen.height - 40); + float width = Mathf.Min(adminTab == 2 ? 940 : 720, Screen.width - 40), height = Mathf.Min(adminTab == 2 ? 680 : 580, Screen.height - 40); var rect = new Rect((Screen.width - width) / 2f, (Screen.height - height) / 2f, width, height); UiTheme.DrawShadow(rect); GUI.Box(rect, GUIContent.none, panel); @@ -338,7 +338,21 @@ namespace MSCO if (n > 0) mp.VehicleCheat("repaired the Satsuma (" + n + " parts)"); } GUILayout.Space(6); - if (GUILayout.Button("REFRESH", secondary, GUILayout.Height(30), GUILayout.Width(100))) VehicleAdmin.Refresh(true); + if (GUILayout.Button("TIGHTEN ALL", secondary, GUILayout.Height(30), GUILayout.Width(110))) + { + int n = VehicleAdmin.SetAllBolts(true); + Log(n + " part(s) tightened"); + if (n > 0) mp.VehicleCheat("tightened all bolts (" + n + " parts)"); + } + GUILayout.Space(6); + if (GUILayout.Button("LOOSEN ALL", danger, GUILayout.Height(30), GUILayout.Width(106))) + { + int n = VehicleAdmin.SetAllBolts(false); + Log(n + " part(s) loosened"); + if (n > 0) mp.VehicleCheat("loosened all bolts (" + n + " parts, testing)"); + } + GUILayout.Space(6); + if (GUILayout.Button("REFRESH", secondary, GUILayout.Height(30), GUILayout.Width(90))) VehicleAdmin.Refresh(true); GUILayout.Space(12); GUILayout.Label("Filter", caption, GUILayout.Width(44)); partFilter = GUILayout.TextField(partFilter ?? "", 30, field, GUILayout.Height(30)); @@ -397,7 +411,24 @@ namespace MSCO GUI.DrawTexture(back, texBarBack); GUI.DrawTexture(new Rect(back.x, back.y, Mathf.Max(2f, back.width * frac), back.height), frac < 0.3f ? texBarLow : texBarFill); } - GUILayout.Label(damaged ? "DAMAGED" : p.Wear != null ? p.Wear.Value.ToString("0") + " %" : "OK", damaged ? danger : small, GUILayout.Width(84)); + string boltText = p.Tightness != null ? (p.Bolted != null && p.Bolted.Value ? " bolted" : " loose") : ""; + GUILayout.Label((damaged ? "DAMAGED" : p.Wear != null ? p.Wear.Value.ToString("0") + " %" : "OK") + boltText, damaged ? danger : small, GUILayout.Width(120)); + if (p.HasBolts && installed) + { + if (GUILayout.Button("TIGHTEN", secondary, GUILayout.Height(26), GUILayout.Width(80))) + { + VehicleAdmin.SetBolts(p, true); + mp.VehicleCheat("tightened " + p.Name); + } + GUILayout.Space(4); + if (GUILayout.Button("LOOSEN", secondary, GUILayout.Height(26), GUILayout.Width(76))) + { + VehicleAdmin.SetBolts(p, false); + mp.VehicleCheat("loosened " + p.Name); + } + GUILayout.Space(4); + } + else GUILayout.Space(164); if (GUILayout.Button("REPAIR", secondary, GUILayout.Height(26), GUILayout.Width(76))) { VehicleAdmin.Repair(p); diff --git a/VehicleAdmin.cs b/VehicleAdmin.cs index 302b228..8e60f79 100644 --- a/VehicleAdmin.cs +++ b/VehicleAdmin.cs @@ -22,8 +22,14 @@ namespace MSCO public sealed class Part { public string Name, Category; - public FsmBool Damaged, Installed; + public FsmBool Damaged, Installed, Bolted; public FsmFloat Wear; + // Schrauben des eingebauten Teils (BoltCheck zählt die Festigkeit, die Schrauben haben je eine Stufe 0-8) + public PlayMakerFSM BoltCheck; + public FsmFloat Tightness; + public float MaxTightness; + public readonly List Bolts = new List(); + public bool HasBolts { get { return Bolts.Count > 0 || Tightness != null; } } } static readonly List fluids = new List(); @@ -53,6 +59,7 @@ namespace MSCO parts.Clear(); if (!MenuHelper.InGame) return; var byName = new Dictionary(); + var partsByDb = new Dictionary(); foreach (PlayMakerFSM fsm in Resources.FindObjectsOfTypeAll()) { if (fsm == null || fsm.FsmName != "Data" || fsm.gameObject.hideFlags != HideFlags.None) continue; @@ -65,14 +72,17 @@ namespace MSCO FsmBool installed = fsm.FsmVariables.FindFsmBool("Installed"); FsmBool damaged = fsm.FsmVariables.FindFsmBool("Damaged"); FsmFloat wear = fsm.FsmVariables.FindFsmFloat("Wear"); - if (damaged == null && wear == null) continue; - parts.Add(new Part + var part = new Part { Name = Pretty(t.name), Category = category.Replace("Database", ""), Damaged = damaged, Installed = installed, Wear = wear, - }); + Bolted = fsm.FsmVariables.FindFsmBool("Bolted"), + }; + partsByDb[fsm.gameObject] = part; + if (damaged != null || wear != null || part.Bolted != null) parts.Add(part); } + FindBolts(partsByDb); parts.Sort((a, b) => string.Compare(a.Category + a.Name, b.Category + b.Name, StringComparison.Ordinal)); foreach (string[] def in FluidDefs) @@ -88,6 +98,106 @@ namespace MSCO } } + /// + /// Zu jedem Database-Teil das eingebaute Teil: dessen BoltCheck zeigt mit "db_ThisPart" auf den Database-Eintrag, + /// die Schrauben (FSM "Screw") liegen darunter. + /// + static void FindBolts(Dictionary partsByDb) + { + foreach (PlayMakerFSM fsm in UnityEngine.Object.FindObjectsOfType()) + { + if (fsm == null || fsm.FsmName != "BoltCheck") continue; + FsmGameObject db = fsm.FsmVariables.FindFsmGameObject("db_ThisPart"); + Part part; + if (db == null || db.Value == null || !partsByDb.TryGetValue(db.Value, out part)) continue; + part.BoltCheck = fsm; + part.Tightness = fsm.FsmVariables.FindFsmFloat("Tightness"); + part.MaxTightness = ClampMax(fsm); + foreach (PlayMakerFSM bolt in fsm.GetComponentsInChildren()) + if (bolt != null && bolt.FsmName == "Screw" && bolt.FsmVariables.FindFsmInt("Stage") != null) part.Bolts.Add(bolt); + if (part.MaxTightness <= 0f) part.MaxTightness = Mathf.Max(1, part.Bolts.Count) * 8f; + } + } + + /// Höchste Festigkeit eines Teils: das FloatClamp im BoltCheck ("Bolts OFF"). + static float ClampMax(PlayMakerFSM boltCheck) + { + try + { + foreach (FsmState state in boltCheck.FsmStates) + { + if (!state.ActionsLoaded) state.LoadActions(); + if (state.Actions == null) continue; + foreach (FsmStateAction a in state.Actions) + { + if (a == null || a.GetType().Name != "FloatClamp") continue; + var max = a.GetType().GetField("maxValue") != null ? a.GetType().GetField("maxValue").GetValue(a) as FsmFloat : null; + if (max != null && max.Value > 0f) return max.Value; + } + } + } + catch { } + return 0f; + } + + /// + /// Eingebautes Teil exakt auf seinen Halter (pivot_...) setzen - dort sitzt es im Spiel immer bei Position/Drehung 0. + /// Nicht bei Teilen, die selbst eingedreht werden (Zündkerze, Ölfilter: Tiefe = Stufe), und nicht bei Türen/Hauben + /// (Scharnier). Gibt true zurück, wenn es verrutscht war. + /// + public static bool SnapToPivot(GameObject part) + { + if (part == null) return false; + Transform t = part.transform; + if (t.parent == null || !t.parent.name.StartsWith("pivot_")) return false; + if (part.GetComponent() != null) return false; + foreach (PlayMakerFSM fsm in part.GetComponents()) if (fsm.FsmName == "Screw") return false; + if (t.localPosition.sqrMagnitude < 0.0004f * 0.0004f && Quaternion.Angle(t.localRotation, Quaternion.identity) < 0.05f) return false; + t.localPosition = Vector3.zero; + t.localRotation = Quaternion.identity; + Rigidbody rb = part.GetComponent(); + if (rb != null && !rb.isKinematic) + { + rb.velocity = Vector3.zero; + rb.angularVelocity = Vector3.zero; + } + return true; + } + + /// Alle Schrauben eines Teils ganz fest (true) oder ganz lose (false) - wie mit dem Werkzeug. + public static int SetBolts(Part p, bool tight) + { + int changed = 0; + foreach (PlayMakerFSM bolt in p.Bolts) + { + if (bolt == null || !bolt.gameObject.activeInHierarchy) continue; + FsmInt stage = bolt.FsmVariables.FindFsmInt("Stage"); + FsmState dispatch = bolt.Fsm.GetState("Stages 2") ?? bolt.Fsm.GetState("Stages"); + if (stage == null || dispatch == null) continue; + int target = tight ? 8 : 0; + if (stage.Value == target) continue; + stage.Value = target; + FsmSync.SwitchStateSilently(bolt, dispatch); // Schraube an ihre Stelle, Zähler des Teils (Array) aktualisieren + changed++; + } + if (p.Tightness != null) + { + float target = tight ? p.MaxTightness : 0f; + if (Mathf.Abs(p.Tightness.Value - target) > 0.01f) { p.Tightness.Value = target; changed++; } + } + if (tight && p.BoltCheck != null && SnapToPivot(p.BoltCheck.gameObject)) changed++; + return changed; + } + + public static int SetAllBolts(bool tight) + { + Refresh(true); + int n = 0; + foreach (Part p in parts) + if (p.HasBolts && (p.Installed == null || p.Installed.Value) && SetBolts(p, tight) > 0) n++; + return n; + } + /// "RadiatorHose1" -> "Radiator Hose 1", "Drumbrake_RR" -> "Drumbrake RR". static string Pretty(string name) { diff --git a/VehicleStateSync.cs b/VehicleStateSync.cs index fb33e72..ea9eb65 100644 --- a/VehicleStateSync.cs +++ b/VehicleStateSync.cs @@ -64,6 +64,8 @@ namespace MSCO // übertragen und über die Stufen-Logik der Schraube angewendet (Position, Dichtung, Zähler des Teils) public FsmState BoltDispatch; public bool InitialSent; + public bool Functional; // Schraube schaltet etwas (Ablassschraube): Welt-Autorität gibt den Stand einmal vor + public bool Exact; // jede Änderung senden (Schrauben-Stufen, Festigkeit der Teile) } readonly Dictionary vars = new Dictionary(); @@ -122,14 +124,17 @@ namespace MSCO ResolveSatsuma(objects); foreach (Var v in vars.Values) { + // Teil wird fester (eigenes Werkzeug): genau auf seinen Halter setzen (verrutschte Teile, z.B. nach der Sync) + if (v.Exact && v.Type == 1 && v.Fsm != null && v.Fsm.FsmName == "BoltCheck" && GetFloat(v) > v.LastF + 0.01f) + VehicleAdmin.SnapToPivot(v.Fsm.gameObject); // 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 && v.BoltDispatch == null; - if (v.BoltDispatch != null && worldAuthority && !v.InitialSent) + bool big = owner == SyncedBody.NoOwner && !v.Exact; + if (v.Functional && worldAuthority && !v.InitialSent) { // Einmal den eigenen Stand vorgeben, damit alle gleich sind (vorher auseinandergelaufene Schrauben) v.InitialSent = true; @@ -198,7 +203,8 @@ namespace MSCO if (!IsTracked(root)) { Forget(fsm); continue; } scanned[fsm] = fsm.transform.parent; Forget(fsm); - if (RegisterFunctionalBolt(fsm, root)) continue; + RegisterTightness(fsm, root); + if (RegisterBolt(fsm, 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; @@ -223,7 +229,46 @@ namespace MSCO /// Leck "Oil" ist an). Sonst laufen Schrauben über FsmSync (Ereignisse): ging eins verloren, war die Ablassschraube /// bei einem offen und beim anderen zu - bei ihm lief dann endlos Öl aus (der Ölstand kam ja ständig nach). /// - bool RegisterFunctionalBolt(PlayMakerFSM fsm, string root) + /// + /// Festigkeit eines Teils (BoltCheck bzw. bei aufgeschraubten Teilen wie dem Ölfilter "Use"): zählt das Werkzeug + /// beim Schrauben hoch - beim Mitspieler drehte sich bisher nur die Schraube, das Teil blieb "lose" bzw. "fest". + /// + void RegisterTightness(PlayMakerFSM fsm, string root) + { + if (fsm.FsmName != "BoltCheck" && fsm.FsmName != "Use") return; + FsmFloat tightness = fsm.FsmVariables.FindFsmFloat("Tightness"); + if (tightness == null) return; + if (fsm.FsmName == "Use" && fsm.gameObject.GetComponents().Length > 0) + { + bool screwed = false; + foreach (PlayMakerFSM other in fsm.gameObject.GetComponents()) if (other.FsmName == "Screw") screwed = true; + if (!screwed) return; + } + string name = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|Tightness"; + int key = Hash(name); + Var existing; + if (vars.TryGetValue(key, out existing) && existing.Variable == tightness) return; + var v = new Var { Key = key, Name = name, Root = root, Variable = tightness, Type = 1, Fsm = fsm, Exact = true }; + 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); + } + } + + /// + /// Jede Schraube (FSM "Screw" mit Stufe 0-8): Stufe als Zustand übertragen, angewendet über die Stufen-Logik der + /// Schraube. Schrauben, die etwas schalten (Ablassschraube), gibt die Welt-Autorität nach dem Beitreten einmal vor. + /// + bool RegisterBolt(PlayMakerFSM fsm, string root) { if (fsm.FsmName != "Screw") return false; FsmInt stage = fsm.FsmVariables.FindFsmInt("Stage"); @@ -243,13 +288,11 @@ namespace MSCO } } catch { return false; } - if (!switches) return false; - string name = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|Stage"; int key = Hash(name); Var existing; if (vars.TryGetValue(key, out existing) && existing.Variable == stage) return true; - var v = new Var { Key = key, Name = name, Root = root, Variable = stage, Type = 2, Fsm = fsm, BoltDispatch = dispatch }; + var v = new Var { Key = key, Name = name, Root = root, Variable = stage, Type = 2, Fsm = fsm, BoltDispatch = dispatch, Functional = switches, Exact = true }; Remember(v); vars[key] = v; List keys; @@ -428,7 +471,9 @@ namespace MSCO case 1: float f = BitConverter.ToSingle(value, 0); if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen + bool tighter = v.Exact && v.Fsm != null && v.Fsm.FsmName == "BoltCheck" && f > GetFloat(v) + 0.01f; SetFloat(v, f); + if (tighter) VehicleAdmin.SnapToPivot(v.Fsm.gameObject); // vom Mitspieler festgezogen: Teil auf den Halter break; case 2: int n = BitConverter.ToInt32(value, 0);