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); + } + } + } +}