- PacketReader for all network data: lengths are checked against the real data (Mono allocated the claimed size first - a fake 2 GB length crashed the game) - FsmSync: remote players can no longer switch arbitrary FSMs - never PLAYER / Systems / MSCO objects, CLICK only on FSMs that wait for a click, bolt/assembly events only along a transition the FSM really has for that event - Time/weather (msco.world) and '* ' system-style chat only from the host in P2P (the dedicated server filters them itself); other msco.* ids are reserved Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
218 lines
9.1 KiB
C#
218 lines
9.1 KiB
C#
using System;
|
|
using System.IO;
|
|
using HutongGames.PlayMaker;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>Aktueller 2-Stunden-Block der Uhr (Startstunde) oder -1.</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Aktuelles Wetter (0 klar, 1 Regen, 2 Gewitter) oder -1.</summary>
|
|
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
|
|
|
|
/// <summary>Befehl lokal ausführen. Gibt null zurück oder einen Fehlertext.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>In einem Verteiler-Zustand die Zahl finden, bei der IntCompare das Event auslöst ("gleich").</summary>
|
|
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 PacketReader(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);
|
|
}
|
|
}
|
|
}
|
|
}
|