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 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 12:16:29 +02:00
co-authored by Claude Opus 5.5
parent 7b4a89d50f
commit 29a3697cc8
6 changed files with 235 additions and 15 deletions
+114 -4
View File
@@ -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<PlayMakerFSM> Bolts = new List<PlayMakerFSM>();
public bool HasBolts { get { return Bolts.Count > 0 || Tightness != null; } }
}
static readonly List<Fluid> fluids = new List<Fluid>();
@@ -53,6 +59,7 @@ namespace MSCO
parts.Clear();
if (!MenuHelper.InGame) return;
var byName = new Dictionary<string, PlayMakerFSM>();
var partsByDb = new Dictionary<GameObject, Part>();
foreach (PlayMakerFSM fsm in Resources.FindObjectsOfTypeAll<PlayMakerFSM>())
{
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
}
}
/// <summary>
/// Zu jedem Database-Teil das eingebaute Teil: dessen BoltCheck zeigt mit "db_ThisPart" auf den Database-Eintrag,
/// die Schrauben (FSM "Screw") liegen darunter.
/// </summary>
static void FindBolts(Dictionary<GameObject, Part> partsByDb)
{
foreach (PlayMakerFSM fsm in UnityEngine.Object.FindObjectsOfType<PlayMakerFSM>())
{
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<PlayMakerFSM>())
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;
}
}
/// <summary>Höchste Festigkeit eines Teils: das FloatClamp im BoltCheck ("Bolts OFF").</summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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<HingeJoint>() != null) return false;
foreach (PlayMakerFSM fsm in part.GetComponents<PlayMakerFSM>()) 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<Rigidbody>();
if (rb != null && !rb.isKinematic)
{
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
}
return true;
}
/// <summary>Alle Schrauben eines Teils ganz fest (true) oder ganz lose (false) - wie mit dem Werkzeug.</summary>
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;
}
/// <summary>"RadiatorHose1" -> "Radiator Hose 1", "Drumbrake_RR" -> "Drumbrake RR".</summary>
static string Pretty(string name)
{