using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
namespace MSCO
{
///
/// Lichter, Dauergeräusche (Motor, Blinker, Hupe, Radio ...) und Anzeigen (Tacho, Drehzahlmesser, Tank-/Temperatur-
/// zeiger, Lenkrad) von Fahrzeugen. Wer ein Fahrzeug besitzt (fährt/schiebt), schickt regelmäßig deren Zustand;
/// bei allen anderen wird dieser Zustand jedes Frame übernommen. Die Zeiger bewegen sich beim Beifahrer sonst nicht,
/// weil das Auto dort nicht selbst fährt (die Spiel-Logik sieht Geschwindigkeit/Drehzahl 0).
///
public class VehicleFx
{
const float SendInterval = 0.1f;
const float DialSmoothing = 14f; // Zeiger gleiten zwischen den Paketen weich zur neuen Stellung
/// Objekte, deren Drehung eine Anzeige ist: Tacho-/Drehzahl-/Tank-/Temperaturzeiger, Lenkrad.
static bool IsDial(string name)
{
string n = name.ToLower();
return n.Contains("needle") || n.Contains("steeringwheel");
}
const float CacheRefresh = 5f; // Teile können an-/abgebaut werden
const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen
struct AudioState
{
public bool Playing;
public float Pitch;
public float Volume;
}
class Components
{
public float BuiltAt;
public Transform Root;
public readonly Dictionary Lights = new Dictionary();
public readonly Dictionary Audio = new Dictionary();
public readonly Dictionary Dials = new Dictionary();
public readonly Dictionary Particles = new Dictionary();
}
class Snapshot
{
public float ReceivedAt;
public bool Released; // nach dem Timeout einmal freigegeben
public readonly Dictionary Lights = new Dictionary();
public readonly Dictionary Audio = new Dictionary();
public readonly Dictionary Dials = new Dictionary();
public readonly Dictionary Particles = new Dictionary();
}
readonly Dictionary components = new Dictionary();
readonly Dictionary incoming = new Dictionary();
float sendTimer;
public void Reset()
{
components.Clear();
incoming.Clear();
}
// ---------------------------------------------------------------- Senden
public void Update(ObjectSync objects, Action send, float dt, bool worldAuthority)
{
sendTimer += dt;
if (sendTimer < SendInterval) return;
sendTimer = 0f;
// Eigene Fahrzeuge (fahren/schieben) und - wenn wir die Welt vorgeben - auch freie, geparkte Fahrzeuge:
// sonst würde z.B. ein brennendes, abgestelltes Auto nur bei einem Spieler brennen
List vehicles = objects.LocalVehicles();
if (worldAuthority) vehicles.AddRange(objects.FreeVehicles());
foreach (SyncedBody b in vehicles)
{
Components c = GetComponents(b);
if (c == null) continue;
int id = b.Id;
send(MsgType.VehicleFx, NetSession.Build(w =>
{
w.Write(id);
w.Write(c.Lights.Count);
foreach (KeyValuePair l in c.Lights)
{
w.Write(l.Key);
w.Write(l.Value != null && l.Value.enabled && l.Value.gameObject.activeInHierarchy);
}
int audioCount = 0;
foreach (AudioSource a in c.Audio.Values) if (a != null) audioCount++;
w.Write(audioCount);
foreach (KeyValuePair a in c.Audio)
{
if (a.Value == null) continue;
w.Write(a.Key);
w.Write(a.Value.isPlaying);
w.Write(a.Value.pitch);
w.Write(a.Value.volume);
}
// Anzeigen (am Ende angehängt - ältere Versionen lesen hier nicht weiter)
int dialCount = 0;
foreach (Transform t in c.Dials.Values) if (t != null) dialCount++;
w.Write(dialCount);
foreach (KeyValuePair d in c.Dials)
{
if (d.Value == null) continue;
Quaternion q = d.Value.localRotation;
w.Write(d.Key);
w.Write(q.x); w.Write(q.y); w.Write(q.z); w.Write(q.w);
}
// Partikel-Effekte (Feuer, Rauch, Dampf, Funken): läuft / läuft nicht
int particleCount = 0;
foreach (ParticleSystem ps in c.Particles.Values) if (ps != null) particleCount++;
w.Write(particleCount);
foreach (KeyValuePair ps in c.Particles)
{
if (ps.Value == null) continue;
w.Write(ps.Key);
w.Write(ps.Value.gameObject.activeInHierarchy && ps.Value.isPlaying);
}
}));
}
}
// ---------------------------------------------------------------- Empfangen
public void Handle(BinaryReader r)
{
int id = r.ReadInt32();
var s = new Snapshot { ReceivedAt = Time.time };
int lights = r.ReadInt32();
for (int i = 0; i < lights; i++) s.Lights[r.ReadInt32()] = r.ReadBoolean();
int audio = r.ReadInt32();
for (int i = 0; i < audio; i++)
{
int key = r.ReadInt32();
s.Audio[key] = new AudioState { Playing = r.ReadBoolean(), Pitch = r.ReadSingle(), Volume = r.ReadSingle() };
}
if (r.BaseStream.Length - r.BaseStream.Position >= 4)
{
int dials = r.ReadInt32();
for (int i = 0; i < dials; i++)
{
int key = r.ReadInt32();
var q = new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
if (!float.IsNaN(q.x) && !float.IsNaN(q.w)) s.Dials[key] = q;
}
}
if (r.BaseStream.Length - r.BaseStream.Position >= 4)
{
int particles = r.ReadInt32();
for (int i = 0; i < particles; i++) s.Particles[r.ReadInt32()] = r.ReadBoolean();
}
incoming[id] = s;
}
/// Jedes Frame nach den Spiel-FSMs (LateUpdate), damit unsere Werte gewinnen.
public void Apply(ObjectSync objects)
{
foreach (KeyValuePair kv in incoming)
{
SyncedBody b = objects.Get(kv.Key);
if (b == null || objects.IsLocalBody(b)) continue; // eigene Fahrzeuge simulieren wir selbst
Components c = GetComponents(b);
if (c == null) continue;
if (Time.time - kv.Value.ReceivedAt > ApplyTimeout)
{
// Keine Daten mehr (Fahrer ausgestiegen o.ä.): einmal alles loslassen, was wir erzwungen haben -
// sonst läuft z.B. das Anlasser-Geräusch endlos weiter ("orgelt")
if (!kv.Value.Released) Release(kv.Value, c);
continue;
}
foreach (KeyValuePair l in kv.Value.Lights)
{
Light light;
if (c.Lights.TryGetValue(l.Key, out light) && light != null) SetLight(light, l.Value, c.Root);
}
foreach (KeyValuePair a in kv.Value.Audio)
{
AudioSource src;
if (c.Audio.TryGetValue(a.Key, out src) && src != null) SetAudio(src, a.Value);
}
foreach (KeyValuePair p in kv.Value.Particles)
{
ParticleSystem ps;
if (c.Particles.TryGetValue(p.Key, out ps) && ps != null) SetParticles(ps, p.Value, c.Root);
}
float t = Mathf.Clamp01(Time.deltaTime * DialSmoothing);
foreach (KeyValuePair d in kv.Value.Dials)
{
Transform dial;
if (c.Dials.TryGetValue(d.Key, out dial) && dial != null)
dial.localRotation = Quaternion.Slerp(dial.localRotation, d.Value, t);
}
}
}
static void Release(Snapshot s, Components c)
{
s.Released = true;
foreach (KeyValuePair a in s.Audio)
{
AudioSource src;
if (a.Value.Playing && c.Audio.TryGetValue(a.Key, out src) && src != null && src.isPlaying) src.Stop();
}
foreach (KeyValuePair p in s.Particles)
{
ParticleSystem ps;
if (p.Value && c.Particles.TryGetValue(p.Key, out ps) && ps != null && ps.isPlaying) ps.Stop();
}
foreach (KeyValuePair l in s.Lights)
{
Light light;
if (l.Value && c.Lights.TryGetValue(l.Key, out light) && light != null) light.enabled = false;
}
}
static void SetLight(Light light, bool on, Transform root)
{
if (!on)
{
if (light.enabled) light.enabled = false;
return;
}
// Nur die Lampe selbst einschalten - übergeordnete Objekte NICHT aktivieren: das weckt Spiel-Logik auf
// (FSMs im OnEnable), die dort nicht laufen soll, und kann das Spiel abstürzen lassen
if (!light.enabled) light.enabled = true;
}
static void SetParticles(ParticleSystem ps, bool playing, Transform root)
{
if (playing)
{
// Nur Effekte auf bereits aktiven Objekten starten (Objekte aktivieren weckt Spiel-Logik - Absturzgefahr)
if (ps.gameObject.activeInHierarchy && !ps.isPlaying) ps.Play();
}
else if (ps.isPlaying) ps.Stop();
}
static void SetAudio(AudioSource src, AudioState state)
{
if (!src.gameObject.activeInHierarchy) return;
if (state.Playing && !src.isPlaying) src.Play();
else if (!state.Playing && src.isPlaying) src.Stop();
src.pitch = state.Pitch;
src.volume = state.Volume;
}
// ---------------------------------------------------------------- Komponenten
Components GetComponents(SyncedBody b)
{
if (b.Rb == null) return null;
Components c;
if (components.TryGetValue(b.Id, out c) && Time.time - c.BuiltAt < CacheRefresh) return c;
c = new Components { BuiltAt = Time.time, Root = b.Rb.transform };
var seen = new Dictionary();
foreach (Light l in b.Rb.GetComponentsInChildren(true))
c.Lights[Key(l.transform, c.Root, seen)] = l;
seen.Clear();
foreach (AudioSource a in b.Rb.GetComponentsInChildren(true))
if (a.loop) c.Audio[Key(a.transform, c.Root, seen)] = a; // nur Dauergeräusche
seen.Clear();
foreach (Transform t in b.Rb.GetComponentsInChildren(true))
if (IsDial(t.name)) c.Dials[Key(t, c.Root, seen)] = t;
seen.Clear();
foreach (ParticleSystem ps in b.Rb.GetComponentsInChildren(true))
c.Particles[Key(ps.transform, c.Root, seen)] = ps;
components[b.Id] = c;
return c;
}
/// Schlüssel aus dem Pfad unterhalb des Fahrzeugs (+ Nummer bei gleichen Pfaden).
static int Key(Transform t, Transform root, Dictionary seen)
{
var sb = new StringBuilder();
for (; t != null && t != root; t = t.parent) sb.Insert(0, "/" + t.name);
string path = sb.ToString();
int n;
seen.TryGetValue(path, out n);
seen[path] = n + 1;
path += "#" + n;
unchecked
{
uint h = 2166136261;
foreach (byte c in Encoding.UTF8.GetBytes(path)) { h ^= c; h *= 16777619; }
return (int)h;
}
}
}
}