Files
MSCO/VehicleAdmin.cs
T
MrMahoandClaude Opus 5.5 4be6b68f25 Admin: install parts, set time and weather; join without prompt
- VEHICLE: INSTALL for parts that are not installed - runs the game's own
  assembly (trigger "Assembly" like on load), so the part sits exactly on its
  pivot; synced to other players via FsmSync (MSCO_INSTALL)
- WORLD: time buttons (2-hour steps via the sun FSM's load path) and
  CLEAR / RAIN / THUNDERSTORM (weather front moved above the admin);
  sent to everyone as mod message "msco.world" (no server update needed)
- JOIN from the server browser/website connects right away in the main menu;
  server mods still ask before downloading

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:04:27 +02:00

354 lines
17 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// Admin-Menü, Reiter VEHICLE: Füllstände und Teile des Satsuma aus der "Database" des Spiels anzeigen und zum Testen
/// reparieren/zerstören. Jedes Teil hat dort ein Objekt mit der FSM "Data" (Installed, Bolted, Damaged, teils Wear);
/// die Füllstände stehen in Tank, Ölwanne, Brems- und Kupplungszylinder. Die Werte überträgt VehicleStateSync an alle.
/// </summary>
public static class VehicleAdmin
{
public sealed class Fluid
{
public string Label;
public FsmFloat Value;
public float Max;
}
public sealed class Part
{
public string Name, Category;
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; } }
// Einbau: das lose Teil (ThisPart) und der Trigger mit der FSM "Assembly" am Fahrzeug
public FsmGameObject ThisPart, Trigger;
public bool CanInstall
{
get
{
return Installed != null && !Installed.Value && ThisPart != null && ThisPart.Value != null
&& Trigger != null && Trigger.Value != null;
}
}
}
static readonly List<Fluid> fluids = new List<Fluid>();
static readonly List<Part> parts = new List<Part>();
static float scannedAt = -100f;
public static List<Fluid> Fluids { get { Refresh(false); return fluids; } }
public static List<Part> Parts { get { Refresh(false); return parts; } }
// Füllstand: Teil in der Database, Variable, Anzeige, Maximum (Tank: aus MaxCapacity)
static readonly string[][] FluidDefs =
{
new[] { "FuelTank", "FuelLevel", "Fuel", "36" },
new[] { "Oilpan", "Oil", "Engine oil", "3" },
new[] { "BrakeMasterCylinder", "BrakeFluidF", "Brake fluid (front)", "1" },
new[] { "BrakeMasterCylinder", "BrakeFluidR", "Brake fluid (rear)", "1" },
new[] { "ClutchMasterCylinder", "ClutchFluid", "Clutch fluid", "0.5" },
new[] { "Radiator", "Water", "Coolant", "6" },
};
/// <summary>Database neu einlesen (höchstens alle 5 s, außer erzwungen).</summary>
public static void Refresh(bool force)
{
if (!force && Time.realtimeSinceStartup - scannedAt < 5f) return;
scannedAt = Time.realtimeSinceStartup;
fluids.Clear();
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;
Transform t = fsm.transform;
if (t.root.name != "Database" || t.parent == null || t.parent.parent != t.root) continue;
string category = t.parent.name;
if (category == "DatabaseOrders" || category == "PlayerDatabase") continue; // Bestellungen/Spieler: keine Fahrzeugteile
byName[t.name] = fsm;
FsmBool installed = fsm.FsmVariables.FindFsmBool("Installed");
FsmBool damaged = fsm.FsmVariables.FindFsmBool("Damaged");
FsmFloat wear = fsm.FsmVariables.FindFsmFloat("Wear");
var part = new Part
{
Name = Pretty(t.name),
Category = category.Replace("Database", "").Replace("Parts", ""),
Damaged = damaged, Installed = installed, Wear = wear,
Bolted = fsm.FsmVariables.FindFsmBool("Bolted"),
ThisPart = fsm.FsmVariables.FindFsmGameObject("ThisPart"),
Trigger = fsm.FsmVariables.FindFsmGameObject("Trigger"),
};
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)
{
PlayMakerFSM fsm;
if (!byName.TryGetValue(def[0], out fsm)) continue;
FsmFloat value = fsm.FsmVariables.FindFsmFloat(def[1]);
if (value == null) continue;
float max = float.Parse(def[3], System.Globalization.CultureInfo.InvariantCulture);
FsmFloat capacity = fsm.FsmVariables.FindFsmFloat("MaxCapacity");
if (def[0] == "FuelTank" && capacity != null && capacity.Value > 0f) max = capacity.Value;
fluids.Add(new Fluid { Label = def[2], Value = value, Max = max });
}
}
/// <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)
{
var sb = new System.Text.StringBuilder();
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if (c == '_') { sb.Append(' '); continue; }
if (i > 0 && ((char.IsUpper(c) && char.IsLower(name[i - 1])) || (char.IsDigit(c) && !char.IsDigit(name[i - 1])))) sb.Append(' ');
sb.Append(c);
}
return sb.ToString();
}
public static void SetFluid(Fluid f, bool full)
{
if (f.Value != null) f.Value.Value = full ? f.Max : 0f;
}
public static void Repair(Part p)
{
if (p.Damaged != null) p.Damaged.Value = false;
if (p.Wear != null) p.Wear.Value = 100f;
}
public static void Break(Part p)
{
if (p.Damaged != null) p.Damaged.Value = true;
if (p.Wear != null) p.Wear.Value = 0f;
}
// ---------------------------------------------------------------- Einbauen
/// <summary>
/// Teil einbauen, wie es das Spiel beim Laden eines Spielstands macht: der Trigger am Fahrzeug bekommt das Teil
/// (FSM "Assembly", Variable Part) und läuft durch seinen Einbau-Zustand - das setzt das Teil exakt auf seinen
/// Halter (pivot_...), aktiviert die Schrauben und trägt es in der Database als eingebaut ein.
/// Gibt null zurück oder den Grund, warum es nicht geht. Die anderen Spieler bauen es genauso ein.
/// </summary>
public static string Install(Part p)
{
if (!p.CanInstall) return "can't be installed";
GameObject trigger = p.Trigger.Value, part = p.ThisPart.Value;
if (trigger.transform.parent != null && !trigger.transform.parent.gameObject.activeInHierarchy)
return "install the part it mounts on first";
if (part.transform.parent != null && part.transform.parent.name == "ItemPivot") return "drop it first (it is held in a hand)";
PlayMakerFSM assembly = Assembly(trigger);
if (assembly == null || assembly.FsmVariables.FindFsmGameObject("Part") == null) return "no mount point found";
if (assembly.Fsm.GetState("Check Assembly") == null && assembly.Fsm.GetState("Assemble") == null) return "no mount point found";
bool wasActive = trigger.activeInHierarchy;
Run(InstallRoutine(assembly, part));
if (wasActive) // bei den anderen Spielern genauso (der Trigger ist dort auch aktiv, solange das Teil fehlt)
FsmSync.Outbox.Add(new FsmEventMsg
{
Path = FsmSync.PathOf(trigger.transform), FsmName = "Assembly", EventName = InstallEvent, State = "",
Position = trigger.transform.position
});
return null;
}
/// <summary>Event-Name der Einbau-Nachricht an die anderen Spieler (läuft über FsmSync).</summary>
public const string InstallEvent = "MSCO_INSTALL";
/// <summary>Einbau-Nachricht eines Mitspielers: zum Trigger das Teil aus der Database holen und einbauen.</summary>
public static void InstallRemote(PlayMakerFSM assembly)
{
FsmGameObject db = assembly.FsmVariables.FindFsmGameObject("db_ThisPart");
if (db == null || db.Value == null) return;
PlayMakerFSM data = null;
foreach (PlayMakerFSM f in db.Value.GetComponents<PlayMakerFSM>()) if (f.FsmName == "Data") data = f;
if (data == null) return;
FsmBool installed = data.FsmVariables.FindFsmBool("Installed");
FsmGameObject part = data.FsmVariables.FindFsmGameObject("ThisPart");
if (installed == null || installed.Value || part == null || part.Value == null) return;
Run(InstallRoutine(assembly, part.Value));
}
static PlayMakerFSM Assembly(GameObject trigger)
{
foreach (PlayMakerFSM f in trigger.GetComponents<PlayMakerFSM>()) if (f.FsmName == "Assembly") return f;
return null;
}
static IEnumerator InstallRoutine(PlayMakerFSM assembly, GameObject part)
{
GameObject trigger = assembly.gameObject;
bool started = trigger.activeSelf && assembly.enabled;
part.SetActive(true);
trigger.SetActive(true);
assembly.enabled = true;
if (!started) yield return null; // FSM erst starten lassen, sonst setzt ihr Start den Zustand zurück
if (assembly == null || part == null) yield break;
assembly.FsmVariables.FindFsmGameObject("Part").Value = part;
FsmBool setup = assembly.FsmVariables.FindFsmBool("Setup");
FsmState check = assembly.Fsm.GetState("Check Assembly");
if (setup != null && check != null)
{
setup.Value = true; // wie beim Laden: "Check Assembly" sieht Setup und baut über den passenden Weg ein
FsmSync.SwitchStateSilently(assembly, check);
}
else FsmSync.SwitchStateSilently(assembly, assembly.Fsm.GetState("Assemble"));
yield return new WaitForSeconds(0.6f);
if (part != null) SnapToPivot(part);
Refresh(true); // Schrauben des Teils sind jetzt aktiv
}
// Coroutinen ohne eigenes MonoBehaviour im Aufrufer
sealed class Runner : MonoBehaviour { }
static Runner runner;
static void Run(IEnumerator routine)
{
if (runner == null)
{
var go = new GameObject("MSCO_AdminRunner");
UnityEngine.Object.DontDestroyOnLoad(go);
runner = go.AddComponent<Runner>();
}
runner.StartCoroutine(routine);
}
public static int RepairAll()
{
Refresh(true);
int n = 0;
foreach (Part p in parts)
{
if (p.Installed != null && !p.Installed.Value) continue;
bool needs = (p.Damaged != null && p.Damaged.Value) || (p.Wear != null && p.Wear.Value < 100f);
if (!needs) continue;
Repair(p);
n++;
}
return n;
}
}
}