using System; using System.Collections.Generic; using System.IO; using System.Text; using HutongGames.PlayMaker; using MSCLoader; using UnityEngine; namespace MSCO { /// Ein synchronisierter Rigidbody (Item, loses Teil oder Fahrzeug). public class SyncedBody { public const int NoOwner = -1; public int Id; public string Path; public Rigidbody Rb; public string Name; public GameObject Go; // zum Unterscheiden: nur Rigidbody weg (angebaut) oder ganzes Objekt weg (verbraucht) public Vector3 LastPos; 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 public RigidbodyInterpolation WasInterpolation; public bool InterpolationSet; // Fahrzeug eines anderen: bei uns interpoliert (siehe FreezeForRemote) // Gelenke der angebauten Teile, solange ein anderer das Fahrzeug fährt: unzerbrechlich (vorherige Bruchkraft/-moment) public Dictionary 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 float LastRunawayLog = -100f; public Vector3 TargetPos; public Quaternion TargetRot; public Vector3 TargetVel; // Geschwindigkeit beim Besitzer: damit wird die Position vorausberechnet public float TargetTime; // wann TargetPos ankam (Physik-Zeit, Time.fixedTime) public float LastRecvTime; public float AvatarHoldTime = -100f; // zuletzt von der Hand eines Mitspielers platziert public Vector3 LastSentPos; public Quaternion LastSentRot; public float LastSentTime; } /// /// 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. /// 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) /// Kamera des lokalen Spielers (zum Prüfen, ob ein Item wirklich in der Hand ist). public Transform Head; /// Eigene Spieler-ID (Host = 0). public int LocalId; /// Senden an alle anderen Spieler (setzt der MultiplayerManager). public Action Send; /// Neues Item bei uns aufgetaucht (Einkauf ...) bzw. bei uns verbraucht - der Manager meldet es allen. public Action ItemAppeared, ItemDestroyed; // Übers Netz erzeugte Items (nicht nochmal als "neu" melden) readonly HashSet networkCreated = new HashSet(); Vector3 lastPlayerPos; const float LocalEventRange = 8f; // Aufgetaucht/verbraucht in dieser Nähe = wir haben es ausgelöst public void MarkNetworkCreated(GameObject go) { if (go != null) networkCreated.Add(go); } /// /// Verschwundene Items melden (gegessen, getrunken, verbraucht) - nur wenn das ganze Objekt weg ist (nicht nur der /// Rigidbody wie beim Anbauen), es ein Item ist und es bei uns passiert ist. Verschwinden viele auf einmal, wird /// gerade die Szene entladen (Neu laden, Hauptmenü) - dann NICHTS melden, sonst löscht man bei allen die Welt. /// void ReportConsumed(Transform player) { if (ItemDestroyed == null || removeList.Count == 0) return; if (player == null || removeList.Count > 2 || Time.time - loadTime < 30f) return; foreach (SyncedBody b in removeList) { if (b.Go != null || b.IsVehicle || b.Name == null || !b.Name.EndsWith("(itemx)")) continue; if (IsLocal(b) || (b.LastPos - lastPlayerPos).sqrMagnitude < LocalEventRange * LocalEventRange) ItemDestroyed(b); } } /// Gibt es dieses Objekt bei uns schon an dieser Stelle? (dann nicht nochmal erzeugen) public bool HasNear(int id, Vector3 pos, float range) { SyncedBody b; return byId.TryGetValue(id, out b) && b.Rb != null && (b.Rb.position - pos).sqrMagnitude < range * range; } /// Ein anderer Spieler hat dieses Item verbraucht: bei uns auch entfernen. public bool DestroyRemote(int id) { SyncedBody b; if (!byId.TryGetValue(id, out b)) return false; // Nur echte Items (Essen, Getränke ...) - nie Fahrzeuge oder Teile, egal was ein anderer meldet if (b.IsVehicle || b.Name == null || !b.Name.EndsWith("(itemx)")) return false; if (b.Go != null) UnityEngine.Object.Destroy(b.Go); Unregister(b); return true; } readonly Dictionary byId = new Dictionary(); readonly Dictionary byRb = new Dictionary(); readonly List removeList = new List(); readonly List sendList = new List(); readonly HashSet rejected = new HashSet(); // schon geprüft, gehört nicht dazu float nextRejectClear; string drivenName; SyncedBody drivenCache; float loadTime; float nextScan; int frame; readonly List drivenList = new List(); float sendTimer; FsmGameObject pickedObject; FsmString currentVehicle; float nextFsmSearch; public int Count { get { return byId.Count; } } /// Erster Suchlauf nach dem Laden erledigt (vorher kann kein Snapshot angewendet werden). public bool ScanComplete { get; private set; } /// Position aus dem Server-Snapshot übernehmen (nur für Objekte, die gerade niemand bewegt). 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); } /// Für "mp vehicles": Zustand aller erfassten Fahrzeuge (zur Fehlersuche). public List DescribeVehicles() { var lines = new List(); 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; } /// Fährt der lokale Spieler gerade selbst? public bool IsDriving { get; private set; } /// Das Fahrzeug, das der lokale Spieler gerade fährt (sonst null). public SyncedBody Driven { get; private set; } /// Fahrzeuge, die gerade ein anderer Spieler aktiv fährt. public List RemoteDrivenVehicles() { var list = drivenList; // wird jedes Frame gebraucht: Liste wiederverwenden list.Clear(); foreach (SyncedBody b in byId.Values) if (b.IsVehicle && IsRemoteOwned(b) && b.OwnerActive && b.Rb != null) list.Add(b); return list; } /// Fahrzeuge, die gerade niemand besitzt (geparkt). public List FreeVehicles() { var list = new List(); 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); } /// Fahrzeuge, die der lokale Spieler gerade besitzt (fährt/schiebt). public List LocalVehicles() { var list = new List(); foreach (SyncedBody b in byId.Values) if (b.IsVehicle && IsLocal(b) && b.Rb != null) list.Add(b); return list; } /// Was der lokale Spieler gerade in der Hand hat (nur wenn er es besitzt). public SyncedBody Held { get; private set; } public void OnGameLoaded() { Reset(); loadTime = Time.time; nextScan = Time.time + FirstScanDelay; } /// Alles freigeben und vergessen (Szenenwechsel oder Verbindungsende). public void Reset() { foreach (SyncedBody b in byId.Values) if (IsRemoteOwned(b) && b.Rb != null) RestoreKinematic(b); byId.Clear(); byRb.Clear(); rejected.Clear(); networkCreated.Clear(); drivenName = null; drivenCache = null; ScanComplete = false; pickedObject = null; currentVehicle = null; } /// Spieler hat das Spiel verlassen: seine Objekte freigeben. 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; } } /// Besitzer eines Fahrzeugs (nach Wurzelname), NoOwner wenn frei oder unbekannt. 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; } /// /// 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). /// 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 (player != null) lastPlayerPos = player.position; if (Time.time >= nextScan) { nextScan = Time.time + ScanInterval; Scan(); ScanComplete = true; } FindPlayerFsms(); SyncedBody held = HeldBody(); SyncedBody driven = DrivenBody(); Driven = driven; IsDriving = driven != null; bool motionClaimAllowed = Time.time - loadTime > MotionClaimGrace; removeList.Clear(); frame++; foreach (SyncedBody b in byId.Values) { // Leistung: freie Objekte (der Großteil, meist liegen sie nur herum) nur jedes 4. Frame prüfen - // eigene, fremde, gehaltene und gefahrene jedes Frame if (b.OwnerId == SyncedBody.NoOwner && ((b.Id ^ frame) & 3) != 0 && b != held && b != driven) continue; if (b.Rb == null) { removeList.Add(b); continue; } b.LastPos = b.Rb.position; 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); } } ReportConsumed(player); 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(); } } /// Objekte anderer Spieler weich zur Zielposition bewegen. 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. // Beides in Physik-Zeit: mit der Bildzeit (Time.time beim Empfang) wurde das Alter in den ersten // Physik-Schritten danach negativ - das Auto sprang bei jedem Paket ein Stück zurück (Ruckeln) float age = Mathf.Clamp(Time.fixedTime - b.TargetTime, 0f, 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)); } } } /// /// 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. /// 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; } /// /// 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. /// void FreezeForRemote(SyncedBody b) { b.WasKinematic = b.Rb.isKinematic; b.Rb.isKinematic = true; if (b.IsVehicle) { HoldParts(b); // Physik läuft mit 50 Hz, das Bild öfter: ohne Interpolation springt das Auto nur in Physik-Schritten // weiter - wer darin mitfährt, sieht die Welt ruckeln. Nur bei Fahrzeugen (Items setzt die Hand direkt). b.WasInterpolation = b.Rb.interpolation; b.InterpolationSet = true; b.Rb.interpolation = RigidbodyInterpolation.Interpolate; } } void HoldParts(SyncedBody b) { b.NextPartScan = Time.time + 2f; // später angebaute Teile ebenfalls if (b.HeldJoints == null) b.HeldJoints = new Dictionary(); foreach (Joint j in b.Rb.GetComponentsInChildren(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; } } /// Wieder selbst simulieren: Auto zurück auf seinen Zustand, Gelenke wieder mit ihrer Bruchkraft. void RestoreKinematic(SyncedBody b) { if (b.Rb != null) { b.Rb.isKinematic = b.WasKinematic; if (b.InterpolationSet) b.Rb.interpolation = b.WasInterpolation; } b.InterpolationSet = false; if (b.HeldJoints == null) return; foreach (KeyValuePair 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.fixedTime; } 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; } } } /// /// 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. /// 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; b.NoMotionClaimUntil = Time.time + HandoverCooldown; // Wird es kurz hintereinander wieder weggeschleudert, steckt es in etwas fest (z.B. loses Teil im Auto). // NICHT an die alte Stelle zurücksetzen (dort steckt es ja) - sondern von oben auf die nächste freie Fläche. if (Time.time - b.RunawayWindowStart > 5f) { b.RunawayWindowStart = Time.time; b.RunawayCount = 0; } b.RunawayCount++; bool stuck = b.RunawayCount >= 2; if (stuck) { PlaceOnTop(rb); b.RunawayCount = 0; } rb.Sleep(); // Nicht das Log fluten: pro Objekt höchstens alle 30 s melden if (Time.time - b.LastRunawayLog > 30f) { b.LastRunawayLog = Time.time; MscoLog.Warning("Stopped " + b.RootName + " - it was flung away by the physics" + (stuck ? " (was stuck, placed on top)" : "")); } } /// Objekt von oben auf die höchste Fläche an seiner Stelle legen (eigene Collider und Spieler ignoriert). static void PlaceOnTop(Rigidbody rb) { Bounds bounds = new Bounds(rb.worldCenterOfMass, Vector3.zero); foreach (Collider c in rb.GetComponentsInChildren()) bounds.Encapsulate(c.bounds); Vector3 from = new Vector3(bounds.center.x, bounds.max.y + 50f, bounds.center.z); float top = float.MinValue; foreach (RaycastHit hit in Physics.RaycastAll(from, Vector3.down, 120f)) { if (hit.collider.isTrigger || hit.transform.IsChildOf(rb.transform) || hit.transform.root.name == "PLAYER") continue; if (hit.point.y > top) top = hit.point.y; } if (top == float.MinValue) return; float lift = top + bounds.extents.y + 0.3f - bounds.center.y; rb.transform.position += Vector3.up * lift; } /// /// 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. /// 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() != 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()) if (j != null && j.connectedBody != null && j.connectedBody.transform.root.name != "PLAYER") return true; return false; } /// Position innerhalb der Karte, Drehung gültig (gleiche Regel wie auf dem Server). 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; } /// /// 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. /// 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; } /// /// 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. /// 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(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; /// Hat der Spieler gerade eben (nicht vor Minuten) etwas gegriffen? 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(); 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; } /// /// 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? /// 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 /// Sofort nach neuen Objekten suchen (z.B. nach dem Nachspielen von Montage-Aktionen). public void ScanNow() { Scan(); nextScan = Time.time + ScanInterval; } /// Neue Rigidbodies suchen (gekaufte Items, abmontierte Teile ...). 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>(); var rootsByName = new Dictionary>(); foreach (Rigidbody rb in SceneCache.Rigidbodies(1f)) { if (rb == null) continue; Transform root = rb.transform.root; List roots; if (!rootsByName.TryGetValue(root.name, out roots)) rootsByName[root.name] = roots = new List(); 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(rb, isVehicle)); else rejected.Add(rb); // nicht bei jedem Suchlauf wieder die ganze Hierarchie prüfen } if (candidates.Count == 0) return; foreach (List roots in rootsByName.Values) roots.Sort((a, b) => a.GetSiblingIndex().CompareTo(b.GetSiblingIndex())); int added = 0; foreach (KeyValuePair 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, Go = c.Key.gameObject, Name = c.Key.gameObject.name, LastPos = c.Key.position, IsVehicle = c.Value, RootName = c.Key.transform.root.name }; byId[id] = b; byRb[c.Key] = b; added++; // Neues Item nach dem ersten Suchlauf, bei uns in der Nähe und nicht übers Netz entstanden: // wahrscheinlich gekauft / ausgepackt / geliefert - den anderen melden bool isItem = !b.IsVehicle && b.Go.name.EndsWith("(itemx)"); if (ScanComplete && isItem && ItemAppeared != null && !networkCreated.Contains(b.Go) && Time.time - loadTime > 30f && (b.LastPos - lastPlayerPos).sqrMagnitude < LocalEventRange * LocalEventRange) ItemAppeared(b); } 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() != 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() != null) return false; for (Transform p = t; p != null; p = p.parent) if (p.GetComponent() != null || p.GetComponent() != null) return false; return true; } static bool IsVehicleRoot(string name) { foreach (string v in VehicleRoots) if (name.StartsWith(v)) return true; return false; } /// /// 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. /// static string BuildPath(Transform t, Dictionary> rootsByName) { var segments = new List(); for (; t != null; t = t.parent) { int ordinal = 0; if (t.parent == null) { List 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 } } }