Files
MSCO/ObjectSync.cs
T

866 lines
37 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using HutongGames.PlayMaker;
using MSCLoader;
using UnityEngine;
namespace MSCO
{
/// <summary>Ein synchronisierter Rigidbody (Item, loses Teil oder Fahrzeug).</summary>
public class SyncedBody
{
public const int NoOwner = -1;
public int Id;
public string Path;
public Rigidbody Rb;
public bool IsVehicle;
public string RootName;
public int OwnerId = NoOwner;
public bool OwnerActive; // Besitzer hält/fährt es gerade aktiv
public bool WasKinematic; // Zustand bevor ein anderer Spieler es übernommen hat
// Angebaute Teile eines Fahrzeugs (eigene Rigidbodies mit Gelenken), solange ein anderer fährt: kinematisch
public Dictionary<Rigidbody, bool> FrozenParts;
public float NextPartScan;
public float StillTime;
public float NoMotionClaimUntil; // kurz nach einer Übergabe nicht wegen Nachfedern/Nachrollen übernehmen
// Am Auto angebautes Teil (Gelenk zum Fahrzeug oder unter einem anderen Rigidbody): bewegt sich mit dem Auto,
// wird dann NICHT als loses Objekt synchronisiert - sonst reißt das Gelenk und das Teil fliegt weg
public bool Attached;
public float NextAttachCheck;
public Vector3 TargetPos;
public Quaternion TargetRot;
public float LastRecvTime;
public float AvatarHoldTime = -100f; // zuletzt von der Hand eines Mitspielers platziert
public Vector3 LastSentPos;
public Quaternion LastSentRot;
public float LastSentTime;
}
/// <summary>
/// Synchronisiert Items und Fahrzeuge mit Ownership für beliebig viele Spieler:
/// Wer ein Objekt bewegt (aufheben, fahren, mit G schieben, anstoßen), wird Besitzer
/// und sendet dessen Position. Bei allen anderen ist es solange kinematisch
/// und folgt nur. Kommt das Objekt zur Ruhe, wird es wieder freigegeben.
/// </summary>
public class ObjectSync
{
public static readonly string[] VehicleRoots =
{
"SATSUMA", "HAYOSIKO", "KEKMET", "FERNDALE", "GIFU", "JONNEZ",
"RCO_RUSCKO", "FLATBED", "COMBINE", "BOAT"
};
// Wurzelobjekte, die jeder Spieler selbst simuliert (Verkehr, NPCs, man selbst)
static readonly string[] IgnoredRoots = { "PLAYER", "TRAFFIC", "NPC_CARS" };
const float MaxSmallItemMass = 60f; // schwerere Dinge ohne ITEM/PART-Tag sind Teil der Welt
const float SendInterval = 1f / 15f;
const float KeepAliveInterval = 1f;
const float ScanInterval = 3f;
const float FirstScanDelay = 3f;
const float MotionClaimGrace = 10f; // nach dem Laden fällt noch alles an seinen Platz
const float ItemClaimRadius = 4f;
const float VehicleClaimRadius = 8f;
const float ReleaseAfterStill = 1f;
const float SnapDistance = 10f;
const float RemoteTimeout = 3f; // Besitzer meldet sich nicht mehr (z.B. Teil wurde dort montiert)
const float RunawaySpeed = 90f; // m/s - schneller fährt in MSC nichts; darüber hat die Physik es weggeschleudert
const float HandoverCooldown = 3f; // nach Freigabe/Übergabe keine Übernahme durch Bewegung
const float MaxHoldDistance = 1.5f; // gehaltene Items sind so nah an der Kamera
const float VehicleClaimSpeed = 0.7f; // Fahrzeuge erst ab echter Bewegung übernehmen (nicht Federn)
/// <summary>Kamera des lokalen Spielers (zum Prüfen, ob ein Item wirklich in der Hand ist).</summary>
public Transform Head;
/// <summary>Eigene Spieler-ID (Host = 0).</summary>
public int LocalId;
/// <summary>Senden an alle anderen Spieler (setzt der MultiplayerManager).</summary>
public Action<MsgType, byte[]> Send;
readonly Dictionary<int, SyncedBody> byId = new Dictionary<int, SyncedBody>();
readonly Dictionary<Rigidbody, SyncedBody> byRb = new Dictionary<Rigidbody, SyncedBody>();
readonly List<SyncedBody> removeList = new List<SyncedBody>();
readonly List<SyncedBody> sendList = new List<SyncedBody>();
readonly HashSet<Rigidbody> rejected = new HashSet<Rigidbody>(); // schon geprüft, gehört nicht dazu
float nextRejectClear;
string drivenName;
SyncedBody drivenCache;
float loadTime;
float nextScan;
float sendTimer;
FsmGameObject pickedObject;
FsmString currentVehicle;
float nextFsmSearch;
public int Count { get { return byId.Count; } }
/// <summary>Erster Suchlauf nach dem Laden erledigt (vorher kann kein Snapshot angewendet werden).</summary>
public bool ScanComplete { get; private set; }
/// <summary>Position aus dem Server-Snapshot übernehmen (nur für Objekte, die gerade niemand bewegt).</summary>
public bool ApplySnapshotPose(int id, Vector3 pos, Quaternion rot)
{
SyncedBody b;
if (!byId.TryGetValue(id, out b) || b.Rb == null || b.OwnerId != SyncedBody.NoOwner) return false;
if (!IsValidPose(pos, rot)) return false;
if (IsAttached(b)) return false; // inzwischen angebaut: Position kommt vom Auto
if (b.IsVehicle) pos = AboveGround(pos, b.Rb.transform); // gespeicherte Position evtl. knapp im Boden
b.Rb.transform.position = pos;
b.Rb.transform.rotation = rot;
if (!b.Rb.isKinematic)
{
b.Rb.velocity = Vector3.zero;
b.Rb.angularVelocity = Vector3.zero;
b.Rb.Sleep();
}
return true;
}
public SyncedBody Get(int id)
{
SyncedBody b;
return byId.TryGetValue(id, out b) ? b : null;
}
public bool IsRemoteOwnedBody(SyncedBody b) { return IsRemoteOwned(b); }
/// <summary>Für "mp vehicles": Zustand aller erfassten Fahrzeuge (zur Fehlersuche).</summary>
public List<string> DescribeVehicles()
{
var lines = new List<string>();
string driving = currentVehicle != null ? currentVehicle.Value : "";
lines.Add("PlayerCurrentVehicle = '" + driving + "', my id = " + LocalId + ", synced objects = " + byId.Count);
foreach (SyncedBody b in byId.Values)
{
if (!b.IsVehicle) continue;
string owner = b.OwnerId == SyncedBody.NoOwner ? "free" : IsLocal(b) ? "ME" : "player " + b.OwnerId;
lines.Add(b.RootName + " id=" + b.Id + " owner=" + owner + (b.OwnerActive ? " (active)" : "") +
(b.Rb == null ? " [rigidbody gone]" : " kinematic=" + b.Rb.isKinematic + " speed=" + b.Rb.velocity.magnitude.ToString("0.0") +
" pos=" + b.Rb.position.ToString("F0") + (b.Rb.gameObject.activeInHierarchy ? "" : " [inactive]")));
}
if (lines.Count == 1) lines.Add("No vehicles registered! (SATSUMA, HAYOSIKO, ... not found as root objects with a Rigidbody)");
return lines;
}
/// <summary>Fährt der lokale Spieler gerade selbst?</summary>
public bool IsDriving { get; private set; }
/// <summary>Fahrzeuge, die gerade ein anderer Spieler aktiv fährt.</summary>
public List<SyncedBody> RemoteDrivenVehicles()
{
var list = new List<SyncedBody>();
foreach (SyncedBody b in byId.Values)
if (b.IsVehicle && IsRemoteOwned(b) && b.OwnerActive && b.Rb != null) list.Add(b);
return list;
}
/// <summary>Fahrzeuge, die der lokale Spieler gerade besitzt (fährt/schiebt).</summary>
public List<SyncedBody> LocalVehicles()
{
var list = new List<SyncedBody>();
foreach (SyncedBody b in byId.Values)
if (b.IsVehicle && IsLocal(b) && b.Rb != null) list.Add(b);
return list;
}
/// <summary>Was der lokale Spieler gerade in der Hand hat (nur wenn er es besitzt).</summary>
public SyncedBody Held { get; private set; }
public void OnGameLoaded()
{
Reset();
loadTime = Time.time;
nextScan = Time.time + FirstScanDelay;
}
/// <summary>Alles freigeben und vergessen (Szenenwechsel oder Verbindungsende).</summary>
public void Reset()
{
foreach (SyncedBody b in byId.Values)
if (IsRemoteOwned(b) && b.Rb != null) RestoreKinematic(b);
byId.Clear();
byRb.Clear();
rejected.Clear();
drivenName = null;
drivenCache = null;
ScanComplete = false;
pickedObject = null;
currentVehicle = null;
}
/// <summary>Spieler hat das Spiel verlassen: seine Objekte freigeben.</summary>
public void ReleaseAllOf(int playerId)
{
foreach (SyncedBody b in byId.Values)
{
if (b.OwnerId != playerId || b.Rb == null) continue;
RestoreKinematic(b);
b.OwnerId = SyncedBody.NoOwner;
b.NoMotionClaimUntil = Time.time + HandoverCooldown;
}
}
/// <summary>Besitzer eines Fahrzeugs (nach Wurzelname), NoOwner wenn frei oder unbekannt.</summary>
public int VehicleOwner(string rootName)
{
foreach (SyncedBody b in byId.Values)
if (b.IsVehicle && b.RootName == rootName) return b.OwnerId;
return SyncedBody.NoOwner;
}
bool IsLocal(SyncedBody b) { return b.OwnerId == LocalId; }
bool IsRemoteOwned(SyncedBody b) { return b.OwnerId != SyncedBody.NoOwner && b.OwnerId != LocalId; }
/// <summary>
/// Konfliktregel, die jeder Spieler gleich anwendet: Wer das Objekt aktiv hält/fährt
/// gewinnt, sonst die kleinere Spieler-ID (Host = 0 gewinnt immer bei Gleichstand).
/// </summary>
static bool Wins(int newId, bool newActive, int curId, bool curActive)
{
if (curId == SyncedBody.NoOwner || newId == curId) return true;
if (newActive != curActive) return newActive;
return newId < curId;
}
// ---------------------------------------------------------------- Update
public void Update(Transform player, float dt)
{
if (Time.time >= nextScan)
{
nextScan = Time.time + ScanInterval;
Scan();
ScanComplete = true;
}
FindPlayerFsms();
SyncedBody held = HeldBody();
SyncedBody driven = DrivenBody();
IsDriving = driven != null;
bool motionClaimAllowed = Time.time - loadTime > MotionClaimGrace;
removeList.Clear();
foreach (SyncedBody b in byId.Values)
{
if (b.Rb == null) { removeList.Add(b); continue; } // z.B. Teil wurde montiert
if (!b.Rb.gameObject.activeInHierarchy) continue;
if (UpdateAttached(b)) continue; // angebaut: bewegt sich mit dem Auto
bool active = b == held || b == driven;
if (!IsRemoteOwned(b)) StopRunaway(b);
if (b.OwnerId == SyncedBody.NoOwner)
{
if (active) Claim(b, true);
else if (motionClaimAllowed && Time.time >= b.NoMotionClaimUntil && IsMoving(b) && IsNear(b, player)) Claim(b, false);
}
else if (IsLocal(b))
{
if (b.OwnerActive != active)
{
// z.B. erst angeschoben, dann eingestiegen: allen Bescheid geben
b.OwnerActive = active;
SendOwn(b);
}
if (!active && (b.Rb.IsSleeping() || IsSlow(b.Rb)))
{
b.StillTime += dt;
if (b.StillTime >= ReleaseAfterStill) Release(b);
}
else b.StillTime = 0f;
}
else // gehört einem anderen Spieler
{
if (active && Wins(LocalId, true, b.OwnerId, b.OwnerActive)) Claim(b, true); // Spieler greift sich das Objekt
else if (Time.time - b.LastRecvTime > RemoteTimeout) SetFree(b);
}
}
foreach (SyncedBody b in removeList) Unregister(b);
Held = held != null && held.Rb != null && IsLocal(held) ? held : null;
sendTimer += dt;
if (sendTimer >= SendInterval)
{
sendTimer = 0f;
SendOwnedStates();
}
}
/// <summary>Objekte anderer Spieler weich zur Zielposition bewegen.</summary>
public void FixedUpdate()
{
foreach (SyncedBody b in byId.Values)
{
if (!IsRemoteOwned(b) || b.Rb == null || b.Attached) continue;
if (b.IsVehicle && Time.time >= b.NextPartScan) FreezeParts(b);
if (Time.time - b.AvatarHoldTime < 0.2f) continue; // sitzt gerade in der Hand eines Mitspielers
if ((b.Rb.position - b.TargetPos).sqrMagnitude > SnapDistance * SnapDistance)
{
b.Rb.transform.position = b.TargetPos;
b.Rb.transform.rotation = b.TargetRot;
}
else
{
b.Rb.MovePosition(Vector3.Lerp(b.Rb.position, b.TargetPos, 0.35f));
b.Rb.MoveRotation(Quaternion.Slerp(b.Rb.rotation, b.TargetRot, 0.35f));
}
}
}
/// <summary>
/// Ein Mitspieler hält dieses Objekt: direkt an seine Hand setzen (Position und Winkel
/// relativ zu seinem Kopf), statt der verzögerten Weltposition zu folgen.
/// </summary>
public void ApplyAvatarHold(int playerId, int bodyId, Vector3 pos, Quaternion rot)
{
SyncedBody b;
if (!byId.TryGetValue(bodyId, out b) || b.Rb == null || b.IsVehicle || b.Attached) return; // ältere Mods melden evtl. ein Auto als "gehalten"
if (b.OwnerId != playerId)
{
if (!Wins(playerId, true, b.OwnerId, b.OwnerActive)) return;
SetRemote(b, playerId, true);
}
else
{
b.LastRecvTime = Time.time;
b.OwnerActive = true;
}
b.Rb.transform.position = pos;
b.Rb.transform.rotation = rot;
b.TargetPos = pos;
b.TargetRot = rot;
b.AvatarHoldTime = Time.time;
}
// ---------------------------------------------------------------- Ownership
void Claim(SyncedBody b, bool active)
{
if (IsRemoteOwned(b)) RestoreKinematic(b);
b.OwnerId = LocalId;
b.OwnerActive = active;
b.StillTime = 0f;
b.LastSentTime = -100f; // Zustand sofort senden
SendOwn(b);
}
void SendOwn(SyncedBody b)
{
int id = b.Id;
bool active = b.OwnerActive;
Send(MsgType.ObjOwn, NetSession.Build(w => { w.Write(id); w.Write(active); }));
}
void Release(SyncedBody b)
{
b.OwnerId = SyncedBody.NoOwner;
b.NoMotionClaimUntil = Time.time + HandoverCooldown;
Send(MsgType.ObjRelease, NetSession.Build(w => WriteState(w, b)));
}
void SetRemote(SyncedBody b, int ownerId, bool active)
{
b.LastRecvTime = Time.time;
b.OwnerActive = active;
if (b.OwnerId == ownerId) return;
if (!IsRemoteOwned(b))
{
FreezeForRemote(b);
b.TargetPos = b.Rb.position;
b.TargetRot = b.Rb.rotation;
}
b.OwnerId = ownerId;
}
void SetFree(SyncedBody b)
{
if (IsRemoteOwned(b)) RestoreKinematic(b);
b.OwnerId = SyncedBody.NoOwner;
b.NoMotionClaimUntil = Time.time + HandoverCooldown;
}
/// <summary>
/// Ein anderer Spieler bewegt das Objekt: bei uns nur noch folgen (kinematisch). Bei Fahrzeugen auch alle
/// angebauten Teile - die hängen in MSC mit Gelenken (mit Bruchkraft) am Auto. Würde nur das Auto ruckartig
/// versetzt, rissen die Gelenke und das Auto fiele auseinander.
/// </summary>
void FreezeForRemote(SyncedBody b)
{
b.WasKinematic = b.Rb.isKinematic;
b.Rb.isKinematic = true;
if (b.IsVehicle) FreezeParts(b);
}
void FreezeParts(SyncedBody b)
{
b.NextPartScan = Time.time + 1f;
if (b.FrozenParts == null) b.FrozenParts = new Dictionary<Rigidbody, bool>();
foreach (Rigidbody part in b.Rb.GetComponentsInChildren<Rigidbody>())
{
if (part == b.Rb || b.FrozenParts.ContainsKey(part)) continue;
b.FrozenParts[part] = part.isKinematic;
part.isKinematic = true;
}
}
/// <summary>Wieder selbst simulieren: Auto und angebaute Teile zurück auf ihren vorherigen Zustand.</summary>
void RestoreKinematic(SyncedBody b)
{
if (b.Rb != null) b.Rb.isKinematic = b.WasKinematic;
if (b.FrozenParts == null) return;
foreach (KeyValuePair<Rigidbody, bool> kv in b.FrozenParts)
{
if (kv.Key == null) continue;
kv.Key.isKinematic = kv.Value;
if (!kv.Value)
{
kv.Key.velocity = Vector3.zero; // keinen Schwung aus dem Folgen mitnehmen
kv.Key.angularVelocity = Vector3.zero;
}
}
b.FrozenParts = null;
}
void SendOwnedStates()
{
sendList.Clear();
float now = Time.time;
foreach (SyncedBody b in byId.Values)
{
if (!IsLocal(b) || b.Rb == null || b.Attached) continue;
bool changed = (b.Rb.position - b.LastSentPos).sqrMagnitude > 0.0001f
|| Quaternion.Angle(b.Rb.rotation, b.LastSentRot) > 0.5f;
if (!changed && now - b.LastSentTime < KeepAliveInterval) continue;
b.LastSentPos = b.Rb.position;
b.LastSentRot = b.Rb.rotation;
b.LastSentTime = now;
sendList.Add(b);
}
if (sendList.Count == 0) return;
Send(MsgType.ObjStates, NetSession.Build(w =>
{
w.Write(sendList.Count);
foreach (SyncedBody b in sendList) WriteState(w, b);
}));
}
// ---------------------------------------------------------------- Empfang
public void Handle(int from, MsgType type, BinaryReader r)
{
switch (type)
{
case MsgType.ObjOwn:
{
int id = r.ReadInt32();
bool active = r.ReadBoolean();
SyncedBody b;
if (!byId.TryGetValue(id, out b) || b.Rb == null || IsAttached(b)) return;
if (!Wins(from, active, b.OwnerId, b.OwnerActive))
{
// Wir besitzen es und gewinnen: nochmal melden, damit alle es wissen
if (IsLocal(b)) SendOwn(b);
return;
}
SetRemote(b, from, active);
break;
}
case MsgType.ObjStates:
{
int count = r.ReadInt32();
for (int i = 0; i < count; i++)
{
int id; Vector3 pos; Quaternion rot; Vector3 vel;
ReadState(r, out id, out pos, out rot, out vel);
SyncedBody b;
if (!byId.TryGetValue(id, out b) || b.Rb == null || !IsValidPose(pos, rot) || IsAttached(b)) continue;
// Nur dem aktuellen Besitzer folgen (oder freie Objekte übernehmen)
if (b.OwnerId != from && b.OwnerId != SyncedBody.NoOwner) continue;
SetRemote(b, from, b.OwnerId == from && b.OwnerActive);
b.TargetPos = pos;
b.TargetRot = rot;
}
break;
}
case MsgType.ObjRelease:
{
int id; Vector3 pos; Quaternion rot; Vector3 vel;
ReadState(r, out id, out pos, out rot, out vel);
SyncedBody b;
if (!byId.TryGetValue(id, out b) || b.Rb == null || b.OwnerId != from) return;
if (!IsValidPose(pos, rot) || IsAttached(b)) { SetFree(b); return; }
b.Rb.transform.position = pos;
b.Rb.transform.rotation = rot;
RestoreKinematic(b);
if (!b.Rb.isKinematic)
{
b.Rb.velocity = vel;
b.Rb.angularVelocity = Vector3.zero;
}
b.OwnerId = SyncedBody.NoOwner;
b.NoMotionClaimUntil = Time.time + HandoverCooldown; // Nachrollen/Nachfedern nicht als "ich bewege es" werten
break;
}
}
}
/// <summary>
/// Wird ein Objekt von der Physik weggeschleudert (z.B. nach dem Setzen einer Position, die im Boden lag),
/// sofort anhalten und auf die letzte gute Position zurücksetzen - statt es quer über die Karte fliegen zu lassen.
/// </summary>
void StopRunaway(SyncedBody b)
{
if (b.Attached) return;
Rigidbody rb = b.Rb;
if (rb.isKinematic || rb.velocity.sqrMagnitude < RunawaySpeed * RunawaySpeed) return;
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
if (IsLocal(b) && b.LastSentTime > 0f && IsValidPose(b.LastSentPos, b.LastSentRot))
{
rb.transform.position = AboveGround(b.LastSentPos, rb.transform);
rb.transform.rotation = b.LastSentRot;
}
else rb.transform.position = AboveGround(rb.position, rb.transform);
ModConsole.Warning("[MSCO] Stopped " + b.RootName + " - it was flung away by the physics");
}
/// <summary>
/// Ist das Objekt gerade an ein Fahrzeug angebaut? Dann Besitz aufgeben und nicht mehr als loses Objekt behandeln.
/// Wird es wieder abgebaut, läuft der Sync ganz normal weiter. Gibt true zurück, solange es angebaut ist.
/// </summary>
bool UpdateAttached(SyncedBody b)
{
if (Time.time < b.NextAttachCheck) return b.Attached;
b.NextAttachCheck = Time.time + 0.5f;
bool attached = IsAttached(b);
if (attached == b.Attached) return attached;
b.Attached = attached;
if (attached)
{
if (IsRemoteOwned(b)) RestoreKinematic(b);
b.OwnerId = SyncedBody.NoOwner; // jeder Spieler erkennt das Anbauen selbst (Montage kommt über FsmSync)
b.OwnerActive = false;
}
else b.NoMotionClaimUntil = Time.time + HandoverCooldown; // gerade abgebaut: kurz zur Ruhe kommen lassen
return attached;
}
static bool IsAttached(SyncedBody b)
{
if (b.IsVehicle || b.Rb == null) return false;
// Unter einem anderen Rigidbody (z.B. am Auto eingehängt)
for (Transform p = b.Rb.transform.parent; p != null; p = p.parent)
if (p.GetComponent<Rigidbody>() != null) return true;
// Per Gelenk mit etwas verbunden, das nicht der Spieler ist (die Hand hält Items nicht über Gelenke fest)
foreach (Joint j in b.Rb.GetComponents<Joint>())
if (j != null && j.connectedBody != null && j.connectedBody.transform.root.name != "PLAYER") return true;
return false;
}
/// <summary>Position innerhalb der Karte, Drehung gültig (gleiche Regel wie auf dem Server).</summary>
static bool IsValidPose(Vector3 p, Quaternion q)
{
if (float.IsNaN(p.x) || float.IsNaN(p.y) || float.IsNaN(p.z) || float.IsInfinity(p.x) || float.IsInfinity(p.y) || float.IsInfinity(p.z))
return false;
if (Mathf.Abs(p.x) > 6000f || Mathf.Abs(p.z) > 6000f || p.y < -200f || p.y > 3000f) return false;
float len = Mathf.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
return len > 0.9f && len < 1.1f;
}
/// <summary>
/// Liegt die Position im Boden, auf die Bodenoberfläche heben. Kurzer Strahl ab 2 m über dem Punkt
/// (nicht aufs Garagendach), die eigenen Teile des Fahrzeugs werden ignoriert.
/// </summary>
static Vector3 AboveGround(Vector3 pos, Transform ignore)
{
float ground = float.MinValue;
foreach (RaycastHit hit in Physics.RaycastAll(pos + Vector3.up * 2f, Vector3.down, 4f))
{
if (hit.collider.isTrigger || (ignore != null && hit.transform.IsChildOf(ignore))) continue;
if (hit.transform.root.name == "PLAYER") continue;
ground = Mathf.Max(ground, hit.point.y);
}
return ground > pos.y - 0.2f ? new Vector3(pos.x, ground + 0.6f, pos.z) : pos;
}
/// <summary>
/// Admin: Fahrzeug (Wurzelname beginnt mit rootPrefix) an eine Position holen. Fährt es gerade ein
/// anderer Spieler, geht das nicht. Gibt einen Statustext zurück.
/// </summary>
public string BringVehicle(string rootPrefix, Vector3 pos, Quaternion rot)
{
foreach (SyncedBody b in byId.Values)
{
if (!b.IsVehicle || b.Rb == null || !b.RootName.StartsWith(rootPrefix)) continue;
if (IsRemoteOwned(b) && b.OwnerActive) return rootPrefix + " is being driven by another player";
Claim(b, false); // wir besitzen es jetzt -> Position geht an alle und an den Server
b.Rb.transform.position = AboveGround(pos, b.Rb.transform);
b.Rb.transform.rotation = rot;
if (!b.Rb.isKinematic)
{
b.Rb.velocity = Vector3.zero;
b.Rb.angularVelocity = Vector3.zero;
}
b.StillTime = 0f;
b.LastSentTime = -100f;
return rootPrefix + " is next to you";
}
return rootPrefix + " not found (mp vehicles shows all vehicles)";
}
static void WriteState(BinaryWriter w, SyncedBody b)
{
Vector3 p = b.Rb.position;
Quaternion q = b.Rb.rotation;
Vector3 v = b.Rb.isKinematic ? Vector3.zero : b.Rb.velocity;
w.Write(b.Id);
w.Write(p.x); w.Write(p.y); w.Write(p.z);
w.Write(q.x); w.Write(q.y); w.Write(q.z); w.Write(q.w);
w.Write(v.x); w.Write(v.y); w.Write(v.z);
}
static void ReadState(BinaryReader r, out int id, out Vector3 pos, out Quaternion rot, out Vector3 vel)
{
id = r.ReadInt32();
pos = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
rot = new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
vel = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
}
// ---------------------------------------------------------------- Erkennung
static bool IsMoving(SyncedBody b)
{
Rigidbody rb = b.Rb;
if (rb.isKinematic || rb.IsSleeping()) return false;
// Fahrzeuge federn/wackeln ständig ein bisschen - nur echtes Rollen/Schieben zählt
if (b.IsVehicle) return rb.velocity.sqrMagnitude > VehicleClaimSpeed * VehicleClaimSpeed;
return rb.velocity.sqrMagnitude > 0.04f || rb.angularVelocity.sqrMagnitude > 0.25f;
}
static bool IsSlow(Rigidbody rb)
{
if (rb.isKinematic) return true;
return rb.velocity.sqrMagnitude < 0.01f && rb.angularVelocity.sqrMagnitude < 0.04f;
}
static bool IsNear(SyncedBody b, Transform player)
{
float radius = b.IsVehicle ? VehicleClaimRadius : ItemClaimRadius;
return (b.Rb.worldCenterOfMass - player.position).sqrMagnitude < radius * radius;
}
void FindPlayerFsms()
{
if ((pickedObject != null && currentVehicle != null) || Time.time < nextFsmSearch) return;
nextFsmSearch = Time.time + 2f;
if (currentVehicle == null)
currentVehicle = FsmVariables.GlobalVariables.FindFsmString("PlayerCurrentVehicle");
if (pickedObject == null)
{
GameObject player = GameObject.Find("PLAYER");
if (player == null) return;
// Es gibt mehrere Hände (z.B. "HandOld"); die aktive bevorzugen
PlayMakerFSM best = null;
foreach (PlayMakerFSM fsm in player.GetComponentsInChildren<PlayMakerFSM>(true))
{
if (fsm.FsmName != "PickUp" || fsm.FsmVariables.FindFsmGameObject("PickedObject") == null) continue;
if (best == null || (!best.gameObject.activeInHierarchy && fsm.gameObject.activeInHierarchy)) best = fsm;
}
if (best != null)
{
pickedObject = best.FsmVariables.FindFsmGameObject("PickedObject");
ModConsole.Print("[MSCO] Found pickup FSM on " + best.gameObject.name +
(best.gameObject.activeInHierarchy ? "" : " (inactive)"));
}
}
}
SyncedBody HeldBody()
{
if (pickedObject == null || pickedObject.Value == null) return null;
for (Transform t = pickedObject.Value.transform; t != null; t = t.parent)
{
Rigidbody rb = t.GetComponent<Rigidbody>();
SyncedBody b;
// Fahrzeuge hält man nie in der Hand: im Auto zeigt PickedObject z.B. aufs Lenkrad, dessen
// Physik-Körper ist das Auto selbst - das Auto würde sonst mit dem Kopf mitkippen
if (rb != null && byRb.TryGetValue(rb, out b)) return !b.IsVehicle && IsReallyInHand(b) ? b : null;
}
return null;
}
/// <summary>
/// PickedObject bleibt nach dem Loslassen oft gesetzt. Dann würde die Mod das Item weiter als
/// "in der Hand" melden und sich mit dem Mitspieler darum streiten (Item zittert bei beiden).
/// Deshalb zusätzlich prüfen: liegt es wirklich direkt vor der Kamera?
/// </summary>
bool IsReallyInHand(SyncedBody b)
{
if (Head == null) return true;
Vector3 offset = b.Rb.worldCenterOfMass - Head.position;
if (offset.sqrMagnitude > MaxHoldDistance * MaxHoldDistance) return false;
return Vector3.Dot(offset.normalized, Head.forward) > 0.2f; // vor dem Spieler, nicht unter den Füßen
}
SyncedBody DrivenBody()
{
if (currentVehicle == null || string.IsNullOrEmpty(currentVehicle.Value)) return null;
// Nur neu suchen, wenn sich das Fahrzeug geändert hat (wird jeden Frame gefragt)
if (currentVehicle.Value == drivenName && drivenCache != null && drivenCache.Rb != null) return drivenCache;
drivenName = currentVehicle.Value;
drivenCache = null;
string vehicle = drivenName.ToUpper();
foreach (SyncedBody b in byId.Values)
if (b.IsVehicle && b.RootName.ToUpper().Contains(vehicle)) { drivenCache = b; break; }
return drivenCache;
}
// ---------------------------------------------------------------- Registrierung
/// <summary>Neue Rigidbodies suchen (gekaufte Items, abmontierte Teile ...).</summary>
void Scan()
{
// Ab und zu neu prüfen (z.B. abmontierte Teile, die jetzt frei beweglich sind)
if (Time.time >= nextRejectClear) { rejected.Clear(); nextRejectClear = Time.time + 30f; }
var candidates = new List<KeyValuePair<Rigidbody, bool>>();
var rootsByName = new Dictionary<string, List<Transform>>();
foreach (UnityEngine.Object o in UnityEngine.Object.FindObjectsOfType(typeof(Rigidbody)))
{
var rb = (Rigidbody)o;
Transform root = rb.transform.root;
List<Transform> roots;
if (!rootsByName.TryGetValue(root.name, out roots)) rootsByName[root.name] = roots = new List<Transform>();
if (!roots.Contains(root)) roots.Add(root);
bool isVehicle;
if (byRb.ContainsKey(rb) || rejected.Contains(rb)) continue;
if (ShouldSync(rb, out isVehicle)) candidates.Add(new KeyValuePair<Rigidbody, bool>(rb, isVehicle));
else rejected.Add(rb); // nicht bei jedem Suchlauf wieder die ganze Hierarchie prüfen
}
if (candidates.Count == 0) return;
foreach (List<Transform> roots in rootsByName.Values)
roots.Sort((a, b) => a.GetSiblingIndex().CompareTo(b.GetSiblingIndex()));
int added = 0;
foreach (KeyValuePair<Rigidbody, bool> c in candidates)
{
string path = BuildPath(c.Key.transform, rootsByName);
int id = Hash(path);
if (byId.ContainsKey(id)) continue; // gleicher Pfad doppelt - nicht eindeutig zuordenbar
var b = new SyncedBody
{
Id = id,
Path = path,
Rb = c.Key,
IsVehicle = c.Value,
RootName = c.Key.transform.root.name
};
byId[id] = b;
byRb[c.Key] = b;
added++;
}
if (added > 0) ModConsole.Print("[MSCO] Syncing " + byId.Count + " objects (+" + added + ")");
}
static bool ShouldSync(Rigidbody rb, out bool isVehicle)
{
isVehicle = false;
Transform t = rb.transform;
string rootName = t.root.name;
if (rootName.StartsWith("MSCO_") || Array.IndexOf(IgnoredRoots, rootName) >= 0) return false;
// Rigidbodies unter einem anderen Rigidbody (Türen, Motorhaube ...) bewegen sich mit
for (Transform p = t.parent; p != null; p = p.parent)
if (p.GetComponent<Rigidbody>() != null) return false;
bool underVehicle = IsVehicleRoot(rootName);
if (underVehicle && t.parent == null)
{
isVehicle = true;
return true;
}
// Items und lose Teile sind im Spiel getaggt
if (rb.CompareTag("ITEM") || rb.CompareTag("PART")) return true;
// Alle anderen kleinen, frei beweglichen Dinge (Flaschen, Essen, Werkzeug ...),
// aber keine Gelenk-Objekte (Türen, Ragdolls) und nichts Animiertes (NPCs)
if (underVehicle || rb.mass > MaxSmallItemMass) return false;
if (rb.GetComponent<Joint>() != null) return false;
for (Transform p = t; p != null; p = p.parent)
if (p.GetComponent<Animator>() != null || p.GetComponent<Animation>() != null) return false;
return true;
}
static bool IsVehicleRoot(string name)
{
foreach (string v in VehicleRoots)
if (name.StartsWith(v)) return true;
return false;
}
/// <summary>
/// Pfad wie "ITEMS#0/beer case(itemx)#3": Name plus Nummer unter gleichnamigen Geschwistern.
/// Alle Spieler laden denselben Spielstand, dadurch ist der Pfad überall gleich.
/// </summary>
static string BuildPath(Transform t, Dictionary<string, List<Transform>> rootsByName)
{
var segments = new List<string>();
for (; t != null; t = t.parent)
{
int ordinal = 0;
if (t.parent == null)
{
List<Transform> roots;
if (rootsByName.TryGetValue(t.name, out roots)) ordinal = roots.IndexOf(t);
}
else
{
Transform parent = t.parent;
for (int i = 0; i < parent.childCount; i++)
{
Transform sibling = parent.GetChild(i);
if (sibling == t) break;
if (sibling.name == t.name) ordinal++;
}
}
segments.Add(t.name + "#" + ordinal);
}
segments.Reverse();
return string.Join("/", segments.ToArray());
}
static int Hash(string s)
{
unchecked
{
uint h = 2166136261;
foreach (byte c in Encoding.UTF8.GetBytes(s)) { h ^= c; h *= 16777619; }
return (int)h;
}
}
void Unregister(SyncedBody b)
{
byId.Remove(b.Id);
byRb.Remove(b.Rb); // funktioniert auch, wenn der Rigidbody schon zerstört wurde
}
}
}