diff --git a/FsmSync.cs b/FsmSync.cs
index 7e630f7..d862b1a 100644
--- a/FsmSync.cs
+++ b/FsmSync.cs
@@ -292,6 +292,7 @@ namespace MSCO
if (Debug) MscoLog.Warning("[MSCO] << not found: " + msg.Path + " [" + msg.FsmName + "]");
return;
}
+ if (msg.EventName == VehicleAdmin.InstallEvent) { VehicleAdmin.InstallRemote(fsm); return; } // Admin hat ein Teil eingebaut
FsmState state = fsm.Fsm.GetState(msg.State);
if (state == null || switchState == null) return;
diff --git a/JoinLinks.cs b/JoinLinks.cs
index 2144b5e..4aad127 100644
--- a/JoinLinks.cs
+++ b/JoinLinks.cs
@@ -8,7 +8,7 @@ namespace MSCO
/// Die Mod registriert den Link-Typ in Windows so, dass ein Klick Steam direkt mit
/// "-applaunch 516750 +msco_connect <link>" startet - ohne cmd/Skript dazwischen, damit
/// präparierte Links keine Befehle ausführen können. Die Mod liest die Adresse beim Start
- /// aus der Kommandozeile und fragt im Hauptmenü, ob verbunden werden soll.
+ /// aus der Kommandozeile und verbindet im Hauptmenü sofort (Server-Mods fragen weiterhin nach).
/// (Läuft das Spiel schon, gibt Steam den Link nicht weiter - dann den Server-Browser nutzen.)
///
public static class JoinLinks
diff --git a/MSCO.csproj b/MSCO.csproj
index 24c34cb..32c57b3 100644
--- a/MSCO.csproj
+++ b/MSCO.csproj
@@ -97,6 +97,7 @@
+
diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs
index 4399ba9..f0d1d5b 100644
--- a/MultiplayerManager.cs
+++ b/MultiplayerManager.cs
@@ -575,6 +575,19 @@ namespace MSCO
if (CanCheat) AnnounceCheat(what);
}
+ /// Admin: Uhrzeit (kind 1, Stunde) oder Wetter (kind 2: 0 klar, 1 Regen, 2 Gewitter) für alle umstellen.
+ public void WorldCheat(byte kind, int value)
+ {
+ if (!CanCheat) return;
+ Vector3 pos = localPlayer != null ? localPlayer.position : Vector3.zero;
+ string error = WorldAdmin.Apply(kind, value, pos);
+ if (error != null) { overlays.Log(error); return; }
+ SendModMessage(WorldAdmin.MessageId, WorldAdmin.Build(kind, value, pos));
+ AnnounceCheat(kind == WorldAdmin.KindHour
+ ? "set the time to " + value.ToString("00") + ":00"
+ : "set the weather to " + WorldAdmin.WeatherNames[value]);
+ }
+
void AnnounceCheat(string what)
{
string text = OwnName() + " " + what + " (admin)";
@@ -642,7 +655,7 @@ namespace MSCO
PendingJoinHost = host;
PendingJoinPort = port;
if (IsInSession || !MenuHelper.InMenu)
- toasts.Add("Join link: " + host + ":" + port, "leave this game - then confirm JOIN in the main menu", UiTheme.Accent);
+ toasts.Add("Join link: " + host + ":" + port, "leave this game - you join automatically in the main menu", UiTheme.Accent);
}
public void CancelPendingJoin()
@@ -1010,6 +1023,10 @@ namespace MSCO
{
CheckHitch();
CheckJoinRequest();
+ // JOIN im Server-Browser (Launcher/Website): ohne Rückfrage verbinden, sobald das Hauptmenü steht.
+ // Server-Mods fragen weiterhin extra nach (das sind Programme).
+ if (PendingJoinHost != null && !IsInSession && !checkingServer && MenuHelper.InMenu && Time.timeSinceLevelLoad > 1f)
+ ConfirmPendingJoin();
MeasureFps();
// Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog)
// Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht;
@@ -1714,6 +1731,7 @@ namespace MSCO
string modId = r.ReadString();
int len = r.ReadInt32();
if (len < 0 || len > MaxModMessageBytes) return;
+ if (modId == WorldAdmin.MessageId) { WorldAdmin.Receive(r.ReadBytes(len)); return; } // Admin: Uhrzeit/Wetter
ModLoader.Message(modId, from, r.ReadBytes(len));
return;
}
diff --git a/Overlays.cs b/Overlays.cs
index d1d7d29..29e6705 100644
--- a/Overlays.cs
+++ b/Overlays.cs
@@ -578,9 +578,22 @@ namespace MSCO
float right = row.xMax;
if (RightButton(ref right, row, 66, "BREAK", danger)) { VehicleAdmin.Break(p); mp.VehicleCheat("broke " + p.Name + " (testing)"); }
if (RightButton(ref right, row, 70, "REPAIR", secondary)) { VehicleAdmin.Repair(p); mp.VehicleCheat("repaired " + p.Name); }
+ if (!installed)
+ {
+ // nicht eingebaut: statt der Schrauben-Knöpfe INSTALL (setzt das Teil auf seinen Halter)
+ if (RightButton(ref right, row, 154, "INSTALL", primary, p.CanInstall))
+ {
+ string error = VehicleAdmin.Install(p);
+ if (error == null) mp.VehicleCheat("installed " + p.Name);
+ else Log(p.Name + ": " + error);
+ }
+ }
bool bolts = p.HasBolts && installed;
- if (RightButton(ref right, row, 72, "LOOSEN", secondary, bolts)) { VehicleAdmin.SetBolts(p, false); mp.VehicleCheat("loosened " + p.Name); }
- if (RightButton(ref right, row, 76, "TIGHTEN", secondary, bolts)) { VehicleAdmin.SetBolts(p, true); mp.VehicleCheat("tightened " + p.Name); }
+ if (installed)
+ {
+ if (RightButton(ref right, row, 72, "LOOSEN", secondary, bolts)) { VehicleAdmin.SetBolts(p, false); mp.VehicleCheat("loosened " + p.Name); }
+ if (RightButton(ref right, row, 76, "TIGHTEN", secondary, bolts)) { VehicleAdmin.SetBolts(p, true); mp.VehicleCheat("tightened " + p.Name); }
+ }
// Verschleiß
float wearX = row.x + 410;
@@ -632,6 +645,30 @@ namespace MSCO
/// Welt zurücksetzen, Backups anlegen und wiederherstellen (dedizierter Server).
void DrawAdminWorld(MultiplayerManager mp)
{
+ // Uhrzeit: je 2 Stunden ein Knopf (so schaltet das Spiel die Tageszeit)
+ int hour = WorldAdmin.CurrentHour;
+ GUILayout.Label("TIME" + (hour >= 0 ? " (now " + hour.ToString("00") + ":00 - " + ((hour + 2) % 24).ToString("00") + ":00)" : ""), caption);
+ Rect hours = GUILayoutUtility.GetRect(10, 30, GUILayout.ExpandWidth(true));
+ float hw = (hours.width - AdminGap * 11) / 12f;
+ for (int i = 0; i < 12; i++)
+ {
+ int h = i * 2;
+ if (GUI.Button(new Rect(hours.x + i * (hw + AdminGap), hours.y, hw, 28), h.ToString("00") + ":00", hour == h ? chipOn : chipOff))
+ mp.WorldCheat(WorldAdmin.KindHour, h);
+ }
+ GUILayout.Space(8);
+
+ int weatherNow = WorldAdmin.CurrentWeather;
+ GUILayout.Label("WEATHER" + (weatherNow >= 0 ? " (now " + WorldAdmin.WeatherNames[weatherNow] + ")" : ""), caption);
+ Rect wr = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
+ float ww = (wr.width - AdminGap * 2) / 3f;
+ string[] labels = { "CLEAR", "RAIN", "THUNDERSTORM" };
+ for (int i = 0; i < 3; i++)
+ if (GUI.Button(new Rect(wr.x + i * (ww + AdminGap), wr.y, ww, 32), labels[i], weatherNow == i ? primary : secondary))
+ mp.WorldCheat(WorldAdmin.KindWeather, i);
+ GUILayout.Label("Rain and thunder start right above you. Everyone gets the same time and weather.", small);
+ GUILayout.Space(14);
+
if (!mp.IsDedicated)
{
GUILayout.Label("World reset and save backups are only available on a dedicated server.", small);
diff --git a/VehicleAdmin.cs b/VehicleAdmin.cs
index ecd1a3a..206a404 100644
--- a/VehicleAdmin.cs
+++ b/VehicleAdmin.cs
@@ -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 Bolts = new List();
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 fluids = new List();
@@ -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
+
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+
+ /// Event-Name der Einbau-Nachricht an die anderen Spieler (läuft über FsmSync).
+ public const string InstallEvent = "MSCO_INSTALL";
+
+ /// Einbau-Nachricht eines Mitspielers: zum Trigger das Teil aus der Database holen und einbauen.
+ 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()) 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()) 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.StartCoroutine(routine);
+ }
+
public static int RepairAll()
{
Refresh(true);
diff --git a/WorldAdmin.cs b/WorldAdmin.cs
new file mode 100644
index 0000000..cdba227
--- /dev/null
+++ b/WorldAdmin.cs
@@ -0,0 +1,217 @@
+using System;
+using System.IO;
+using HutongGames.PlayMaker;
+using UnityEngine;
+
+namespace MSCO
+{
+ ///
+ /// Admin-Menü, Reiter WORLD: Uhrzeit und Wetter umstellen. Beides läuft im Spiel über PlayMaker:
+ /// - Uhr: die Sonnen-FSM hat je 2 Stunden einen Zustand ("00-02" ... "22-00") und die Zahl "Time"; beim Laden
+ /// setzt das Spiel "Time" und wählt in "State 1" den passenden Zustand - genau so stellen wir die Zeit.
+ /// - Wetter: die FSM "Weather" (Wolkenfront) mit den Zuständen "No weather", "Rain" und "Thunder"; bei Regen
+ /// wird die Front über den Admin gesetzt, damit es dort auch regnet.
+ /// Die anderen Spieler bekommen den Befehl über eine MSCO-Nachricht und führen ihn genauso aus.
+ ///
+ public static class WorldAdmin
+ {
+ public const string MessageId = "msco.world";
+ public const byte KindHour = 1, KindWeather = 2;
+
+ public static readonly string[] WeatherStates = { "No weather", "Rain", "Thunder" };
+ public static readonly string[] WeatherEvents = { "NORAIN", "RAIN", "THUNDER" };
+ public static readonly string[] WeatherNames = { "clear", "rain", "thunder" };
+
+ static PlayMakerFSM sun, weather;
+ static float searchedAt = -100f;
+
+ static void Find()
+ {
+ if (sun != null && weather != null) return;
+ if (Time.realtimeSinceStartup - searchedAt < 3f) return;
+ searchedAt = Time.realtimeSinceStartup;
+ foreach (PlayMakerFSM f in SceneCache.Fsms(5f))
+ {
+ if (f == null) continue;
+ if (sun == null && f.FsmVariables.FindFsmInt("Time") != null && f.Fsm.GetState("12-14") != null
+ && f.Fsm.GetState("22-00") != null && f.Fsm.GetState("State 1") != null && f.Fsm.GetState("Next day") != null)
+ sun = f;
+ if (weather == null && f.FsmName == "Weather" && f.Fsm.GetState("No weather") != null && f.Fsm.GetState("Rain") != null)
+ weather = f;
+ }
+ }
+
+ // ---------------------------------------------------------------- Anzeige
+
+ /// Aktueller 2-Stunden-Block der Uhr (Startstunde) oder -1.
+ public static int CurrentHour
+ {
+ get
+ {
+ Find();
+ string s = sun != null ? sun.ActiveStateName : null;
+ int h;
+ if (s != null && s.Length == 5 && s[2] == '-' && int.TryParse(s.Substring(0, 2), out h)) return h;
+ return -1;
+ }
+ }
+
+ /// Aktuelles Wetter (0 klar, 1 Regen, 2 Gewitter) oder -1.
+ public static int CurrentWeather
+ {
+ get
+ {
+ Find();
+ if (weather == null) return -1;
+ FsmInt type = weather.FsmVariables.FindFsmInt("WeatherType");
+ if (type == null) return -1;
+ FsmState check = weather.Fsm.GetState("State 2");
+ for (int i = 0; i < WeatherEvents.Length; i++)
+ {
+ int value;
+ if (check != null && IntCompareValue(check, WeatherEvents[i], out value) && value == type.Value) return i;
+ }
+ return -1;
+ }
+ }
+
+ // ---------------------------------------------------------------- Ausführen
+
+ /// Befehl lokal ausführen. Gibt null zurück oder einen Fehlertext.
+ public static string Apply(byte kind, int value, Vector3 position)
+ {
+ Find();
+ try
+ {
+ if (kind == KindHour) return SetHour(value);
+ if (kind == KindWeather) return SetWeather(value, position);
+ }
+ catch (Exception e)
+ {
+ MscoLog.Error("[MSCO] world admin: " + e);
+ return "failed: " + e.Message;
+ }
+ return "unknown command";
+ }
+
+ static string SetHour(int hour)
+ {
+ if (sun == null) return "Clock not found.";
+ int start = ((hour % 24 + 24) % 24) / 2 * 2;
+ string block = start.ToString("00") + "-" + ((start + 2) % 24).ToString("00");
+ FsmState target = sun.Fsm.GetState(block);
+ if (target == null) return "Clock state " + block + " not found.";
+
+ // wie beim Laden: "Time" setzen, "State 1" wählt daraus den Zustand (und stellt Licht/Farben ein)
+ FsmState dispatch = sun.Fsm.GetState("State 1");
+ int timeValue;
+ if (dispatch != null && IntCompareValue(dispatch, block, out timeValue))
+ {
+ sun.FsmVariables.FindFsmInt("Time").Value = timeValue;
+ FsmSync.SwitchStateSilently(sun, dispatch);
+ }
+ else FsmSync.SwitchStateSilently(sun, target);
+ return null;
+ }
+
+ static string SetWeather(int kind, Vector3 position)
+ {
+ if (weather == null) return "Weather not found.";
+ if (kind < 0 || kind >= WeatherStates.Length) return "Unknown weather.";
+ FsmState state = weather.Fsm.GetState(WeatherStates[kind]);
+ if (state == null) return "Weather state not found.";
+ FsmSync.SwitchStateSilently(weather, state);
+ if (kind > 0) MoveFrontOver(position);
+ return null;
+ }
+
+ ///
+ /// Wolkenfront über eine Stelle setzen: "Move clouds" setzt jedes Bild die Position aus PosX/PosZ - also diese
+ /// beiden so wählen, dass die Front über der Stelle steht (Koordinatenraum aus der SetPosition-Aktion).
+ ///
+ static void MoveFrontOver(Vector3 position)
+ {
+ FsmState move = weather.Fsm.GetState("Move clouds");
+ if (move == null) return;
+ if (!move.ActionsLoaded) move.LoadActions();
+ if (move.Actions == null) return;
+ foreach (FsmStateAction a in move.Actions)
+ {
+ if (a == null || a.GetType().Name != "SetPosition") continue;
+ Type t = a.GetType();
+ var owner = Field(t, a, "gameObject") as FsmOwnerDefault;
+ var x = Field(t, a, "x") as FsmFloat;
+ var z = Field(t, a, "z") as FsmFloat;
+ object space = Field(t, a, "space");
+ GameObject go = owner != null ? weather.Fsm.GetOwnerDefaultTarget(owner) : null;
+ if (go == null) continue;
+
+ bool local = space is Space && (Space)space == Space.Self && go.transform.parent != null;
+ Vector3 p = local ? go.transform.parent.InverseTransformPoint(position) : position;
+ SetVar(x, p.x);
+ SetVar(z, p.z);
+ go.transform.position = new Vector3(position.x, go.transform.position.y, position.z);
+ return;
+ }
+ }
+
+ static void SetVar(FsmFloat field, float value)
+ {
+ if (field == null || field.IsNone || string.IsNullOrEmpty(field.Name)) return;
+ FsmFloat v = weather.FsmVariables.FindFsmFloat(field.Name);
+ if (v != null) v.Value = value;
+ }
+
+ static object Field(Type t, object o, string name)
+ {
+ var f = t.GetField(name);
+ return f != null ? f.GetValue(o) : null;
+ }
+
+ /// In einem Verteiler-Zustand die Zahl finden, bei der IntCompare das Event auslöst ("gleich").
+ static bool IntCompareValue(FsmState state, string eventName, out int value)
+ {
+ value = 0;
+ if (!state.ActionsLoaded) state.LoadActions();
+ if (state.Actions == null) return false;
+ foreach (FsmStateAction a in state.Actions)
+ {
+ if (a == null || a.GetType().Name != "IntCompare") continue;
+ Type t = a.GetType();
+ var equal = Field(t, a, "equal") as FsmEvent;
+ var number = Field(t, a, "integer2") as FsmInt;
+ if (equal == null || number == null || equal.Name != eventName) continue;
+ value = number.Value;
+ return true;
+ }
+ return false;
+ }
+
+ // ---------------------------------------------------------------- Netz
+
+ public static byte[] Build(byte kind, int value, Vector3 position)
+ {
+ using (var ms = new MemoryStream())
+ using (var w = new BinaryWriter(ms))
+ {
+ w.Write(kind); w.Write(value);
+ w.Write(position.x); w.Write(position.y); w.Write(position.z);
+ w.Flush();
+ return ms.ToArray();
+ }
+ }
+
+ public static void Receive(byte[] data)
+ {
+ if (!MenuHelper.InGame || data == null || data.Length < 17) return;
+ using (var r = new BinaryReader(new MemoryStream(data)))
+ {
+ byte kind = r.ReadByte();
+ int value = r.ReadInt32();
+ var pos = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
+ string error = Apply(kind, value, pos);
+ if (error != null) MscoLog.Warning("[MSCO] world admin from other player: " + error);
+ }
+ }
+ }
+}