using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using HutongGames.PlayMaker;
using MSCLoader;
using UnityEngine;
namespace MSCO
{
///
/// Gesamtzustand der Fahrzeuge (vor allem Satsuma): Werte in den PlayMaker-FSMs unter dem Fahrzeug,
/// z.B. Tankinhalt, Öl, Kühlwasser, Bremsflüssigkeit, Batterieladung, Schrauben-Festigkeit,
/// Schäden/Verschleiß, Kilometerstand, Lack, Einstellungen (Vergaser, Zündung ...).
/// Wer das Fahrzeug gerade besitzt (fährt/schiebt), gibt die Werte vor; ist es frei,
/// schickt jeder seine Änderungen (z.B. Öl nachfüllen, Schraube festziehen).
///
public class VehicleStateSync
{
// Nur Werte, die den Zustand beschreiben - nicht Drehzahl, Lenkung, Raycasts usw.
static readonly string[] Keywords =
{
"fuel", "oil", "coolant", "water", "fluid", "brake", "clutchfluid", "battery", "charge",
"damage", "wear", "condition", "odo", "km", "dirt", "paint", "color", "colour",
"tune", "tuning", "ratio", "broken", "fixed", "flat", "pressure", "level", "mixture", "timing",
"idle", "air", "jet", "spark", "gap", "rust", "health", "burn", "cool", "temp"
};
// Montiert-Flags nicht direkt setzen: das echte Anbauen läuft über FsmSync (Events)
// Schrauben/Festigkeit laufen über FsmSync (Events) - ein falscher Wert hier würde Teile lockern und abfallen lassen
// Vom Spiel selbst berechnete Werte ebenfalls nicht: z.B. LeakOilpan/LeakOilfilter (leckt Öl?) ergibt sich aus
// angebauten/festgezogenen Teilen - ein übertragener alter Wert ließ beim Beitreten Öl auslaufen. Raten (…Rate)
// rechnet das Spiel laufend neu aus.
static readonly string[] Excluded =
{
"install", "bolt", "tight", "detach", "attach", "rpm", "speed", "steer", "throttle", "gear", "raycast", "distance",
"leak", "rate"
};
const float ScanInterval = 10f;
const float CheckInterval = 0.5f;
const int MaxPerMessage = 200;
class Var
{
public int Key;
public string Name; // Pfad|FSM|Variable (für die Diagnose "vars")
public string Root;
public NamedVariable Variable;
public byte Type; // 1 float, 2 int, 3 bool
public float LastF;
public int LastI;
public bool LastB;
}
readonly Dictionary vars = new Dictionary();
readonly Dictionary pending = new Dictionary(); // noch nicht gefundene Werte
readonly List outgoing = new List();
readonly HashSet scanned = new HashSet();
float nextScan, nextCheck;
public int Count { get { return vars.Count; } }
/// Diagnose (Konsole "vars "): synchronisierte Fahrzeugwerte mit aktuellem Wert.
public List Describe(string filter, int max)
{
var lines = new List();
string f = (filter ?? "").ToLower();
foreach (Var v in vars.Values)
{
if (f.Length > 0 && (v.Name == null || !v.Name.ToLower().Contains(f))) continue;
string value = v.Type == 1 ? ((FsmFloat)v.Variable).Value.ToString("0.###")
: v.Type == 2 ? ((FsmInt)v.Variable).Value.ToString()
: ((FsmBool)v.Variable).Value.ToString();
lines.Add(" " + v.Name + " = " + value);
if (lines.Count >= max) { lines.Add(" ... (more - use a narrower filter)"); break; }
}
if (lines.Count == 0) lines.Add(" no synced vehicle value matches '" + filter + "'");
return lines;
}
public void Reset()
{
vars.Clear();
pending.Clear();
scanned.Clear();
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
}
public void Update(ObjectSync objects, int localId, Action send)
{
if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan)
{
nextScan = Time.time + ScanInterval;
Scan();
}
if (Time.time < nextCheck) return;
nextCheck = Time.time + CheckInterval;
outgoing.Clear();
foreach (Var v in vars.Values)
{
// Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden
int owner = objects.VehicleOwner(v.Root);
if (owner != SyncedBody.NoOwner && owner != localId) { if (Changed(v, false)) Remember(v); continue; }
// Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) -
// langsames Abdriften (z.B. Abkühlen) rechnet jeder selbst, sonst überschreiben sich alle gegenseitig
bool big = owner == SyncedBody.NoOwner;
if (!Changed(v, big)) continue;
Remember(v);
outgoing.Add(v);
}
for (int start = 0; start < outgoing.Count; start += MaxPerMessage)
{
int from = start, count = Math.Min(MaxPerMessage, outgoing.Count - start);
send(MsgType.VarSync, NetSession.Build(w =>
{
w.Write(count);
for (int i = from; i < from + count; i++) Write(w, outgoing[i]);
}));
}
}
public void Handle(int from, BinaryReader r, ObjectSync objects, int localId)
{
int count = r.ReadInt32();
for (int i = 0; i < count; i++)
{
int key = r.ReadInt32();
byte type = r.ReadByte();
byte[] value = r.ReadBytes(type == 3 ? 1 : 4);
Var v;
if (!vars.TryGetValue(key, out v)) { pending[key] = Combine(type, value); continue; }
if (from >= 0 && objects.VehicleOwner(v.Root) == localId) continue; // ich fahre gerade: meine Werte gelten
Apply(v, type, value);
}
}
/// Gespeicherte Werte vom Server (Snapshot) - auch für Teile, die erst später auftauchen.
public void ApplySnapshot(BinaryReader r)
{
int count = r.ReadInt32();
for (int i = 0; i < count; i++)
{
int key = r.ReadInt32();
byte type = r.ReadByte();
byte[] value = r.ReadBytes(type == 3 ? 1 : 4);
Var v;
if (vars.TryGetValue(key, out v)) Apply(v, type, value);
else pending[key] = Combine(type, value);
}
}
// ---------------------------------------------------------------- intern
void Scan()
{
int before = vars.Count;
foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f))
{
// Schon erfasste FSMs nicht jedes Mal neu durchgehen (Pfad + Hash kosten)
if (fsm == null || !scanned.Add(fsm)) continue;
string root = fsm.transform.root.name;
if (!IsVehicle(root)) continue;
// FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert
// kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed")
if (TouchesTransform(fsm)) continue;
string prefix = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|";
FsmVariables fv = fsm.FsmVariables;
foreach (FsmFloat f in fv.FloatVariables) Register(prefix, root, f, 1);
foreach (FsmInt n in fv.IntVariables) Register(prefix, root, n, 2);
foreach (FsmBool b in fv.BoolVariables) Register(prefix, root, b, 3);
}
if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced");
}
static readonly string[] TransformActionWords = { "Scale", "Position", "Rotate", "Rotation", "Translate", "Parent" };
static bool TouchesTransform(PlayMakerFSM fsm)
{
try
{
foreach (FsmState state in fsm.FsmStates)
{
if (!state.ActionsLoaded) state.LoadActions();
if (state.Actions == null) continue;
foreach (FsmStateAction action in state.Actions)
{
if (action == null) continue;
string type = action.GetType().Name;
foreach (string word in TransformActionWords)
if (type.Contains(word)) return true;
}
}
}
catch { return true; } // im Zweifel nicht anfassen
return false;
}
void Register(string prefix, string root, NamedVariable variable, byte type)
{
if (variable == null || string.IsNullOrEmpty(variable.Name) || !Wanted(variable.Name)) return;
int key = Hash(prefix + variable.Name);
Var existing;
if (vars.TryGetValue(key, out existing) && existing.Variable == variable) return;
var v = new Var { Key = key, Name = prefix + variable.Name, Root = root, Variable = variable, Type = type };
Remember(v);
vars[key] = v;
byte[] stored;
if (pending.TryGetValue(key, out stored))
{
pending.Remove(key);
byte[] value = new byte[stored.Length - 1];
Array.Copy(stored, 1, value, 0, value.Length);
Apply(v, stored[0], value);
}
}
static bool Wanted(string name)
{
string n = name.ToLower();
foreach (string x in Excluded) if (n.Contains(x)) return false;
foreach (string k in Keywords) if (n.Contains(k)) return true;
return false;
}
static bool IsVehicle(string root)
{
foreach (string v in ObjectSync.VehicleRoots) if (root.StartsWith(v)) return true;
return false;
}
static bool Changed(Var v, bool bigOnly)
{
switch (v.Type)
{
case 1:
float f = ((FsmFloat)v.Variable).Value;
float delta = Mathf.Abs(f - v.LastF);
return bigOnly
? delta > Mathf.Max(0.5f, Mathf.Abs(v.LastF) * 0.05f)
: delta > Mathf.Max(0.01f, Mathf.Abs(v.LastF) * 0.002f);
case 2: return ((FsmInt)v.Variable).Value != v.LastI;
default: return ((FsmBool)v.Variable).Value != v.LastB;
}
}
static void Remember(Var v)
{
switch (v.Type)
{
case 1: v.LastF = ((FsmFloat)v.Variable).Value; break;
case 2: v.LastI = ((FsmInt)v.Variable).Value; break;
default: v.LastB = ((FsmBool)v.Variable).Value; break;
}
}
static void Apply(Var v, byte type, byte[] value)
{
if (type != v.Type) return;
switch (type)
{
case 1:
float f = BitConverter.ToSingle(value, 0);
if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen
((FsmFloat)v.Variable).Value = f;
break;
case 2: ((FsmInt)v.Variable).Value = BitConverter.ToInt32(value, 0); break;
default: ((FsmBool)v.Variable).Value = value[0] != 0; break;
}
Remember(v); // nicht als eigene Änderung zurückschicken
}
static void Write(BinaryWriter w, Var v)
{
w.Write(v.Key);
w.Write(v.Type);
switch (v.Type)
{
case 1: w.Write(v.LastF); break;
case 2: w.Write(v.LastI); break;
default: w.Write(v.LastB); break;
}
}
static byte[] Combine(byte type, byte[] value)
{
var all = new byte[value.Length + 1];
all[0] = type;
Array.Copy(value, 0, all, 1, value.Length);
return all;
}
static int Hash(string s)
{
unchecked
{
uint h = 2166136261;
foreach (byte c in Encoding.UTF8.GetBytes(s)) { h ^= c; h *= 16777619; }
return (int)h;
}
}
}
}