928 lines
41 KiB
C#
928 lines
41 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
|
|
// Gelenke der angebauten Teile, solange ein anderer das Fahrzeug fährt: unzerbrechlich (vorherige Bruchkraft/-moment)
|
|
public Dictionary<Joint, Vector2> HeldJoints;
|
|
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;
|
|
// Wie oft kurz hintereinander weggeschleudert (steckt es in etwas fest?)
|
|
public int RunawayCount;
|
|
public float RunawayWindowStart;
|
|
|
|
public Vector3 TargetPos;
|
|
public Quaternion TargetRot;
|
|
public Vector3 TargetVel; // Geschwindigkeit beim Besitzer: damit wird die Position vorausberechnet
|
|
public float TargetTime; // wann TargetPos ankam
|
|
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 / 30f; // unbewegte Objekte werden ohnehin nicht gesendet
|
|
const float MaxExtrapolation = 0.25f; // höchstens so weit (Sekunden) in die Zukunft vorausrechnen
|
|
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 gerade niemand besitzt (geparkt).</summary>
|
|
public List<SyncedBody> FreeVehicles()
|
|
{
|
|
var list = new List<SyncedBody>();
|
|
foreach (SyncedBody b in byId.Values)
|
|
if (b.IsVehicle && b.OwnerId == SyncedBody.NoOwner && b.Rb != null) list.Add(b);
|
|
return list;
|
|
}
|
|
|
|
public bool IsLocalBody(SyncedBody b) { return IsLocal(b); }
|
|
|
|
/// <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
|
|
{
|
|
// Trägt es gerade ein anderer, nur übernehmen, wenn wir es wirklich eben gegriffen haben - PickedObject
|
|
// zeigt oft noch auf ein altes Item; trägt es jemand an unserem Kopf vorbei, würden wir es ihm sonst entreißen
|
|
bool grab = b == driven || !b.OwnerActive || JustPicked;
|
|
if (active && grab && 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) HoldParts(b);
|
|
if (Time.time - b.AvatarHoldTime < 0.2f) continue; // sitzt gerade in der Hand eines Mitspielers
|
|
// Dort, wo das Objekt beim Besitzer JETZT ungefähr ist: letzte Position + Geschwindigkeit * Alter.
|
|
// Ohne das hinken schnelle Objekte (fahrende Autos) immer einen Paketabstand hinterher und ruckeln.
|
|
float age = Mathf.Min(Time.time - b.TargetTime, MaxExtrapolation);
|
|
Vector3 predicted = b.TargetPos + b.TargetVel * age;
|
|
if ((b.Rb.position - predicted).sqrMagnitude > SnapDistance * SnapDistance)
|
|
{
|
|
b.Rb.transform.position = predicted;
|
|
b.Rb.transform.rotation = b.TargetRot;
|
|
}
|
|
else
|
|
{
|
|
float follow = b.IsVehicle ? 0.5f : 0.35f;
|
|
b.Rb.MovePosition(Vector3.Lerp(b.Rb.position, predicted, follow));
|
|
b.Rb.MoveRotation(Quaternion.Slerp(b.Rb.rotation, b.TargetRot, follow));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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.TargetVel = Vector3.zero;
|
|
b.TargetTime = Time.time;
|
|
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.TargetVel = Vector3.zero;
|
|
b.TargetTime = Time.time;
|
|
}
|
|
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 hängen die
|
|
/// angebauten Teile (Türen, Kotflügel, Sitze ...) mit Gelenken mit Bruchkraft am Auto. Die Teile dürfen NICHT
|
|
/// selbst kinematisch geschaltet werden (das bricht die Gelenke sofort, MSC wertet es als "Teil abgefallen"),
|
|
/// und das ruckartige Folgen würde sie ebenfalls abreißen. Deshalb werden ihre Gelenke solange unzerbrechlich.
|
|
/// </summary>
|
|
void FreezeForRemote(SyncedBody b)
|
|
{
|
|
b.WasKinematic = b.Rb.isKinematic;
|
|
b.Rb.isKinematic = true;
|
|
if (b.IsVehicle) HoldParts(b);
|
|
}
|
|
|
|
void HoldParts(SyncedBody b)
|
|
{
|
|
b.NextPartScan = Time.time + 1f; // später angebaute Teile ebenfalls
|
|
if (b.HeldJoints == null) b.HeldJoints = new Dictionary<Joint, Vector2>();
|
|
foreach (Joint j in b.Rb.GetComponentsInChildren<Joint>(true))
|
|
{
|
|
if (j == null || b.HeldJoints.ContainsKey(j)) continue;
|
|
b.HeldJoints[j] = new Vector2(j.breakForce, j.breakTorque);
|
|
j.breakForce = Mathf.Infinity;
|
|
j.breakTorque = Mathf.Infinity;
|
|
}
|
|
}
|
|
|
|
/// <summary>Wieder selbst simulieren: Auto zurück auf seinen Zustand, Gelenke wieder mit ihrer Bruchkraft.</summary>
|
|
void RestoreKinematic(SyncedBody b)
|
|
{
|
|
if (b.Rb != null) b.Rb.isKinematic = b.WasKinematic;
|
|
if (b.HeldJoints == null) return;
|
|
foreach (KeyValuePair<Joint, Vector2> kv in b.HeldJoints)
|
|
{
|
|
if (kv.Key == null) continue; // Teil wurde inzwischen abgebaut
|
|
kv.Key.breakForce = kv.Value.x;
|
|
kv.Key.breakTorque = kv.Value.y;
|
|
}
|
|
b.HeldJoints = 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;
|
|
b.TargetVel = vel.sqrMagnitude < 90f * 90f ? vel : Vector3.zero;
|
|
b.TargetTime = Time.time;
|
|
}
|
|
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;
|
|
|
|
// Mehrmals kurz hintereinander: das Objekt steckt in etwas fest (z.B. abgebautes Teil im Auto).
|
|
// Dann nicht wieder an dieselbe Stelle setzen, sondern nach oben heraus heben.
|
|
if (Time.time - b.RunawayWindowStart > 5f) { b.RunawayWindowStart = Time.time; b.RunawayCount = 0; }
|
|
b.RunawayCount++;
|
|
bool stuck = b.RunawayCount >= 3;
|
|
if (stuck)
|
|
{
|
|
Bounds bounds = new Bounds(rb.worldCenterOfMass, Vector3.zero);
|
|
foreach (Collider c in rb.GetComponentsInChildren<Collider>()) bounds.Encapsulate(c.bounds);
|
|
rb.transform.position += Vector3.up * (bounds.size.y + 1.5f);
|
|
b.RunawayCount = 0;
|
|
}
|
|
else if (IsLocal(b) && b.LastSentTime > 0f && IsValidPose(b.LastSentPos, b.LastSentRot))
|
|
{
|
|
rb.transform.position = b.LastSentPos;
|
|
rb.transform.rotation = b.LastSentRot;
|
|
}
|
|
b.NoMotionClaimUntil = Time.time + HandoverCooldown;
|
|
if (b.RunawayCount <= 1 || stuck)
|
|
MscoLog.Warning("[MSCO] Stopped " + b.RootName + " - it was flung away by the physics" + (stuck ? " (stuck, lifted out)" : ""));
|
|
}
|
|
|
|
/// <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");
|
|
MscoLog.Info("[MSCO] Found pickup FSM on " + best.gameObject.name +
|
|
(best.gameObject.activeInHierarchy ? "" : " (inactive)"));
|
|
}
|
|
}
|
|
}
|
|
|
|
GameObject lastPicked; // letzter Wert von PickedObject - ändert er sich, hat der Spieler wirklich neu gegriffen
|
|
float pickTime = -100f;
|
|
|
|
/// <summary>Hat der Spieler gerade eben (nicht vor Minuten) etwas gegriffen?</summary>
|
|
bool JustPicked { get { return Time.time - pickTime < 0.6f; } }
|
|
|
|
SyncedBody HeldBody()
|
|
{
|
|
GameObject picked = pickedObject != null ? pickedObject.Value : null;
|
|
if (picked != lastPicked)
|
|
{
|
|
lastPicked = picked;
|
|
if (picked != null) pickTime = Time.time;
|
|
}
|
|
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>Sofort nach neuen Objekten suchen (z.B. nach dem Nachspielen von Montage-Aktionen).</summary>
|
|
public void ScanNow()
|
|
{
|
|
Scan();
|
|
nextScan = Time.time + ScanInterval;
|
|
}
|
|
|
|
/// <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) MscoLog.Info("[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
|
|
}
|
|
}
|
|
}
|