- Sending on a background thread (the game thread blocked whenever the TCP buffer was full, e.g. while uploading the save); small frames are batched - Server save requests: routine saves wait for a calm moment (not while driving fast) and are announced; urgent flag from server 2.10.5 - Dynamic built-in font for all MSCO GUI (Unity logged hundreds of "Font size and style overrides" warnings with stutters) - Admin VEHICLE tab rescans every 30 s / after actions instead of every 5 s; time/weather FSM search less often - Reused buffers for Build/Frame/Poll (less garbage, fewer GC pauses) - Hitch log: GC and save detection, count of hitches between reports - Show Bolt Size 1.0.1: finds the FPSCamera (Camera.main may be missing), uses MSCO's shared FSM list instead of FindObjectsOfType every 2 s Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
357 lines
17 KiB
C#
357 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 30 s, außer erzwungen - nach jeder Aktion). Das Durchsuchen aller FSMs
|
|
/// dauert spürbar (Ruckler bei offenem Admin-Menü); die angezeigten Werte selbst sind ohnehin live.
|
|
/// </summary>
|
|
public static void Refresh(bool force)
|
|
{
|
|
if (!force && Time.realtimeSinceStartup - scannedAt < 30f) 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;
|
|
}
|
|
}
|
|
}
|