using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using HutongGames.PlayMaker;
using UnityEngine;
namespace MSCO
{
///
/// Bedienelemente der Fahrzeuge: Zündschloss, Anlasser, Motor, Lichtschalter, Choke, Blinker, Scheibenwischer ...
/// Das Spiel schaltet damit Motor und Licht über PlayMaker-FSMs (Zustände wie "Off" / "Lights" / "Running").
/// Übertragen wird der aktive Zustand; beim anderen Spieler wird dieselbe FSM in denselben Zustand geschaltet,
/// sodass sein Spiel Motor, Geräusch, Scheinwerfer und Armaturen selbst richtig anschaltet.
/// Wer sendet: der Fahrer; ist das Fahrzeug frei, wer daneben steht bzw. drin sitzt (Schalter betätigt).
/// FSMs, die ständig den Zustand wechseln (interne Schleifen), werden nach kurzer Zeit nicht mehr übertragen.
///
public class ControlSync
{
// Objekt- oder FSM-Name enthält eines davon
static readonly string[] Keywords =
{
"ignition", "starter", "engine", "light", "choke", "wiper", "hazard", "indicator", "turnsignal", "blinker",
"horn", "heater", "fan", "radio", "key", "switch", "knob", "button",
// Handbremse, Sonnenblenden, Sitzlehne, Gurt, CD-Spieler, Innenlicht, Tankdeckel
"handbrake", "sunvisor", "seattop", "seatbelt", "cdplayer", "interiorlight", "opencap"
};
// Montage/Speichern/Schrauben - läuft über den Objekt- bzw. FSM-Sync, hier nicht anfassen
static readonly string[] Excluded =
{
"removal", "assembly", "assemble", "bolt", "screw", "data", "save", "load", "paint", "detach", "attach",
"install", "trigger", "tighten", "wear", "damage", "oil", "coolant", "fuel", "inspection"
};
const float ScanInterval = 10f;
const float CheckInterval = 0.1f;
const float NearRange = 5f;
const int FlipLimit = 12; // so viele Zustandswechsel ...
const float FlipWindow = 10f; // ... in dieser Zeit = interne Schleife, nicht übertragen
const int MaxPerMessage = 100;
class Control
{
public int Key;
public string Root;
public PlayMakerFSM Fsm;
public string LastState;
public float WindowStart;
public int Flips;
public bool Muted;
}
readonly Dictionary controls = new Dictionary();
readonly HashSet scanned = new HashSet();
readonly List outgoing = new List();
// Zustände für Schalter, die wir noch nicht kennen (Teil noch nicht angebaut/aktiv, Server-Snapshot vor dem Suchlauf)
readonly Dictionary pending = new Dictionary();
float nextScan, nextCheck;
public int Count { get { return controls.Count; } }
public int MutedCount { get { int n = 0; foreach (Control c in controls.Values) if (c.Muted) n++; return n; } }
public void Reset()
{
controls.Clear();
scanned.Clear();
pending.Clear();
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
}
public void Update(ObjectSync objects, int localId, Vector3 playerPos, Action send)
{
if (nextScan < 0f) nextScan = Time.time + 2.4f; // 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 (Control c in controls.Values)
{
if (c.Fsm == null || c.Muted) continue;
string state = c.Fsm.ActiveStateName;
if (state == c.LastState || string.IsNullOrEmpty(state)) continue;
c.LastState = state;
if (Time.time - c.WindowStart > FlipWindow) { c.WindowStart = Time.time; c.Flips = 0; }
if (++c.Flips > FlipLimit)
{
c.Muted = true;
MscoLog.Info("[MSCO] Control not synced (changes all the time): " + c.Fsm.gameObject.name + " [" + c.Fsm.FsmName + "]");
continue;
}
int owner = objects.VehicleOwner(c.Root);
if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer gibt vor
if (owner == SyncedBody.NoOwner && (c.Fsm.transform.position - playerPos).sqrMagnitude > NearRange * NearRange) continue;
outgoing.Add(c);
}
for (int start = 0; start < outgoing.Count; start += MaxPerMessage)
{
int from = start, count = Math.Min(MaxPerMessage, outgoing.Count - start);
send(MsgType.VehicleControls, NetSession.Build(w =>
{
w.Write(count);
for (int i = from; i < from + count; i++) { w.Write(outgoing[i].Key); w.Write(outgoing[i].LastState); }
}));
}
}
public void Handle(BinaryReader r, ObjectSync objects, int localId)
{
int count = r.ReadInt32();
for (int i = 0; i < count; i++)
{
int key = r.ReadInt32();
string state = r.ReadString();
Control c;
if (!controls.TryGetValue(key, out c) || c.Fsm == null) { pending[key] = state; nextScan = 0f; continue; }
if (objects.VehicleOwner(c.Root) == localId) continue; // ich fahre: meine Schalter gelten
SwitchTo(c, state);
}
}
void SwitchTo(Control c, string state)
{
if (c.Fsm.ActiveStateName == state) { c.LastState = state; return; }
FsmState target = c.Fsm.Fsm.GetState(state);
if (target == null) return;
c.LastState = state; // nicht als eigene Änderung zurückschicken
FsmSync.SwitchStateSilently(c.Fsm, target);
c.LastState = c.Fsm.ActiveStateName; // FSM kann sofort weiterschalten (z.B. "Start" -> "Running")
}
void Scan()
{
int before = controls.Count;
foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f))
{
if (fsm == null || scanned.Contains(fsm)) continue;
string root = fsm.transform.root.name;
// Noch nicht am Fahrzeug (z.B. Radio liegt in CARPARTS): beim nächsten Suchlauf nochmal ansehen
if (!IsVehicle(root)) continue;
scanned.Add(fsm);
if (!Wanted(fsm.gameObject.name, fsm.FsmName)) continue;
int key = Hash(FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName);
if (controls.ContainsKey(key)) continue;
var control = new Control { Key = key, Root = root, Fsm = fsm, LastState = fsm.ActiveStateName, WindowStart = Time.time };
controls[key] = control;
string waiting;
if (pending.TryGetValue(key, out waiting)) { pending.Remove(key); SwitchTo(control, waiting); }
}
if (controls.Count != before) MscoLog.Info("[MSCO] Vehicle controls (ignition, lights, engine ...): " + controls.Count + " synced");
}
///
/// P2P-Host: alle Schalterzustände nochmal schicken (Licht an, Motor läuft ...) - für später Beigetretene und
/// falls eine Nachricht verloren ging. Fahrzeuge, die ein anderer fährt, gibt dieser vor.
///
public void ResendAll(ObjectSync objects, int localId, Action send)
{
outgoing.Clear();
foreach (Control c in controls.Values)
{
if (c.Fsm == null || c.Muted || string.IsNullOrEmpty(c.Fsm.ActiveStateName)) continue;
int owner = objects.VehicleOwner(c.Root);
if (owner != SyncedBody.NoOwner && owner != localId) continue;
outgoing.Add(c);
}
for (int start = 0; start < outgoing.Count; start += MaxPerMessage)
{
int from = start, count = Math.Min(MaxPerMessage, outgoing.Count - start);
send(MsgType.VehicleControls, NetSession.Build(w =>
{
w.Write(count);
for (int i = from; i < from + count; i++) { w.Write(outgoing[i].Key); w.Write(outgoing[i].Fsm.ActiveStateName); }
}));
}
}
static bool Wanted(string objectName, string fsmName)
{
string o = objectName.ToLower(), f = fsmName.ToLower();
foreach (string x in Excluded) if (o.Contains(x) || f.Contains(x)) return false;
foreach (string k in Keywords) if (o.Contains(k) || f.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 int Hash(string s)
{
unchecked
{
uint h = 2166136261;
foreach (byte c in Encoding.UTF8.GetBytes(s)) { h ^= c; h *= 16777619; }
return (int)h;
}
}
}
}