using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace MSCO
{
///
/// Türen, Motorhaube und Kofferraum an Fahrzeugen: hängen mit einem Scharnier (HingeJoint) an der Karosserie
/// und werden deshalb nicht vom Objekt-Sync bewegt. Übertragen wird der Öffnungswinkel des Scharniers.
/// Beim Empfänger wird die Tür NICHT hingesetzt (Teleportieren gegen das Gelenk erzeugt riesige Kräfte - Teile
/// reißen ab, im schlimmsten Fall stürzt die Physik ab), sondern die Feder des Scharniers zieht sie sanft auf
/// den Winkel. Danach bekommt das Scharnier seine ursprüngliche Einstellung zurück.
/// Wer sendet: Fährt jemand das Auto, der Fahrer. Ist es frei, wer gerade daneben steht (Tür auf/zu gemacht).
///
public class HingeSync
{
const float ScanInterval = 10f;
const float SendInterval = 0.1f;
const float MinAngle = 2f; // Grad - kleiner ist Zittern
const float NearRange = 4f; // so nah muss man an einer Tür stehen, um sie zu bewegen
const float ApplyHold = 3f; // so lange nach einer Nachricht die Feder halten und selbst nicht senden
// Steht das Fahrzeug, schickt nur, wer die Tür gerade selbst bewegt hat (Klick in der Nähe). Vorher schickte
// jeder in der Nähe: setzte sich die Tür beim anderen nach dem Übernehmen etwas anders, schickte er zurück -
// die Türen schaukelten hin und her.
const float InteractRange = 2.5f;
const float InteractAuthority = 4f; // so lange nach dem Klick (Tür schwingt aus)
const float SpringForce = 120f;
const float SpringDamper = 12f;
class Hinge
{
public int Key;
public string Root;
public HingeJoint Joint;
public Rigidbody Rb;
public float SentAngle;
public bool HasSent;
public float TargetAngle;
public float TargetTime = -100f;
public bool Driving; // Feder gerade von uns gesetzt
public float LocalAuthorityUntil = -100f;
public Collider Collider; // für den Abstand zum Spieler (große Türen wie das Garagentor)
public bool OrigUseSpring;
public JointSpring OrigSpring;
}
readonly Dictionary hinges = new Dictionary();
readonly HashSet seen = new HashSet();
readonly List outgoing = new List();
float nextScan, sendTimer;
public int Count { get { return hinges.Count; } }
public void Reset()
{
foreach (Hinge h in hinges.Values) StopDriving(h);
hinges.Clear();
seen.Clear();
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
}
public void Update(ObjectSync objects, int localId, Vector3 playerPos, float dt, Action send)
{
if (nextScan < 0f) nextScan = Time.time + 1.7f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan)
{
nextScan = Time.time + ScanInterval;
Scan();
}
sendTimer += dt;
if (sendTimer < SendInterval) return;
sendTimer = 0f;
outgoing.Clear();
foreach (Hinge h in hinges.Values)
{
if (h.Joint == null || h.Rb == null || !h.Rb.gameObject.activeInHierarchy) continue;
bool mine = Time.time < h.LocalAuthorityUntil;
if (!mine && Time.time - h.TargetTime < ApplyHold) continue; // gerade von anderen gesetzt
int owner = objects.VehicleOwner(h.Root);
// Fährt jemand: der Fahrer schickt (Türen schwingen während der Fahrt). Hat man selbst gerade an der Tür
// geklickt, schickt man auch - sonst würde die Feder sie beim Fahrer gleich wieder zudrücken.
if (!mine && owner != SyncedBody.NoOwner && owner != localId) continue;
if (!mine && owner == SyncedBody.NoOwner) { h.SentAngle = SafeAngle(h, h.SentAngle); continue; } // stehendes Auto: nur wer klickt
float angle = h.Joint.angle;
if (float.IsNaN(angle) || float.IsInfinity(angle)) continue;
if (h.HasSent && Mathf.Abs(angle - h.SentAngle) < MinAngle) continue;
h.SentAngle = angle;
h.HasSent = true;
outgoing.Add(h);
}
if (outgoing.Count == 0) return;
send(MsgType.HingeStates, NetSession.Build(w =>
{
w.Write(outgoing.Count);
foreach (Hinge h in outgoing) { w.Write(h.Key); w.Write(h.SentAngle); }
}));
}
/// Jedes Frame: Klick (linke Maustaste / Benutzen) in der Nähe einer Tür = wir bewegen sie gerade.
public void NoteInteraction(Vector3 playerPos)
{
// Links öffnet, rechts schließt (Garagentor), F = Benutzen
if (!Input.GetMouseButtonDown(0) && !Input.GetMouseButtonDown(1) && !Input.GetKeyDown(KeyCode.F)) return;
foreach (Hinge h in hinges.Values)
{
if (h.Rb == null) continue;
Vector3 near = h.Collider != null ? h.Collider.ClosestPointOnBounds(playerPos) : h.Rb.worldCenterOfMass;
if ((near - playerPos).sqrMagnitude > InteractRange * InteractRange) continue;
h.LocalAuthorityUntil = Time.time + InteractAuthority;
h.TargetTime = -100f; // eigene Bewegung: keine Feder mehr auf den alten Winkel
}
}
static float SafeAngle(Hinge h, float fallback)
{
float a = h.Joint.angle;
return float.IsNaN(a) || float.IsInfinity(a) ? fallback : a;
}
public void Handle(BinaryReader r)
{
int count = r.ReadInt32();
for (int i = 0; i < count; i++)
{
int key = r.ReadInt32();
float angle = r.ReadSingle();
Hinge h;
if (!hinges.TryGetValue(key, out h)) { nextScan = 0f; continue; }
if (float.IsNaN(angle) || float.IsInfinity(angle) || Mathf.Abs(angle) > 180f) continue;
if (Time.time < h.LocalAuthorityUntil) continue; // wir bewegen sie gerade selbst
h.TargetAngle = angle;
h.TargetTime = Time.time;
h.SentAngle = angle; // nicht als eigene Bewegung zurückschicken
h.HasSent = true;
}
}
/// Feder des Scharniers auf den empfangenen Winkel stellen bzw. wieder zurückgeben.
public void FixedUpdate()
{
foreach (Hinge h in hinges.Values)
{
if (h.Joint == null || h.Rb == null) continue;
bool fresh = Time.time - h.TargetTime < ApplyHold && !h.Rb.isKinematic;
if (!fresh) { StopDriving(h); continue; }
if (!h.Driving)
{
h.OrigUseSpring = h.Joint.useSpring;
h.OrigSpring = h.Joint.spring;
h.Driving = true;
}
float target = h.TargetAngle;
if (h.Joint.useLimits)
{
JointLimits l = h.Joint.limits;
target = Mathf.Clamp(target, Mathf.Min(l.min, l.max), Mathf.Max(l.min, l.max));
}
JointSpring s = h.Joint.spring;
s.spring = SpringForce;
s.damper = SpringDamper;
s.targetPosition = target;
h.Joint.spring = s;
h.Joint.useSpring = true;
h.Rb.WakeUp();
}
}
static void StopDriving(Hinge h)
{
if (!h.Driving) return;
h.Driving = false;
if (h.Joint == null) return;
h.Joint.spring = h.OrigSpring;
h.Joint.useSpring = h.OrigUseSpring;
}
void Scan()
{
int before = hinges.Count;
foreach (HingeJoint j in UnityEngine.Object.FindObjectsOfType())
{
if (j == null || !seen.Add(j)) continue;
Rigidbody rb = j.GetComponent();
Rigidbody body = j.connectedBody;
if (rb == null) continue;
// Fahrzeugtüren/-hauben - und Türen/Tore der Welt mit Scharnier (z.B. das Garagentor)
bool vehicle = body != null && IsVehicle(body.transform.root.name);
if (!vehicle && !IsWorldDoor(j)) continue;
int key = Hash(FsmSync.PathOf(j.transform) + "|" + (body != null ? FsmSync.PathOf(body.transform) : "world"));
if (hinges.ContainsKey(key)) continue;
hinges[key] = new Hinge
{
Key = key, Root = (body != null ? body.transform : j.transform).root.name, Joint = j, Rb = rb,
Collider = rb.GetComponentInChildren()
};
}
// Abgebaute Teile (Scharnier weg) vergessen
var gone = new List();
foreach (KeyValuePair kv in hinges)
if (kv.Value.Joint == null || kv.Value.Rb == null) gone.Add(kv.Key);
foreach (int k in gone) hinges.Remove(k);
if (gone.Count > 0) seen.RemoveWhere(x => x == null);
if (hinges.Count != before) MscoLog.Info("[MSCO] Vehicle doors/hood/trunk: " + hinges.Count + " synced");
}
/// Tür/Tor der Welt (nicht Verkehr/NPC-Autos, nicht der Spieler): Name enthält door/gate.
static bool IsWorldDoor(HingeJoint j)
{
string root = j.transform.root.name;
if (root == "PLAYER" || root == "NPC_CARS" || root == "TRAFFIC" || root.StartsWith("MSCO_")) return false;
string path = FsmSync.PathOf(j.transform).ToLowerInvariant();
return path.Contains("door") || path.Contains("gate");
}
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 (char c in s) { h ^= c; h *= 16777619; }
return (int)h;
}
}
}
}