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>
This commit is contained in:
2026-10-03 13:04:27 +02:00
co-authored by Claude Opus 5.5
parent 06df8fecc5
commit 4be6b68f25
7 changed files with 384 additions and 4 deletions
+106
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using UnityEngine;
@@ -30,6 +31,16 @@ namespace MSCO
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>();
@@ -78,6 +89,8 @@ namespace MSCO
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);
@@ -229,6 +242,99 @@ namespace MSCO
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);