Satsuma no longer falls apart when driving together; automatic passenger seat

- A driven vehicle is no longer released after 3 s without data (a short hitch
  of the driver, e.g. saving, stopped the car instantly on the passenger's side
  and ripped the parts off, which then synced to everyone): 30 s timeout, on
  release it keeps the driver's velocity and part joints stay unbreakable for
  3 more seconds
- Cargo in a moving vehicle belongs to its driver (passengers no longer claim
  items in the car, which then followed late and kinematic at the driver's end
  and knocked seats and parts off)
- Passenger: sit down automatically by stepping into the open passenger door,
  also without a driver (seat from the game's DriverHeadPivot); Space or P to
  get out

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 10:23:25 +02:00
co-authored by Claude Opus 5.5
parent 73004340ce
commit 983291a4b3
4 changed files with 262 additions and 34 deletions
+115 -2
View File
@@ -30,6 +30,14 @@ namespace MSCO
// Gelenke der angebauten Teile, solange ein anderer das Fahrzeug fährt: unzerbrechlich (vorherige Bruchkraft/-moment)
public Dictionary<Joint, Vector2> HeldJoints;
public float NextPartScan;
// Nach der Rückgabe erst so spät wieder zerbrechlich (bis dahin schwingen die Teile ein)
public float JointRestoreAt = float.MaxValue;
// Item liegt in diesem Fahrzeug (Kofferraum, Sitz ...): gehört dann dem, der das Fahrzeug fährt
public SyncedBody Carrier;
public float NextCarrierCheck;
// Fahrzeug: Umriss in lokalen Koordinaten (für "liegt im Auto")
public Bounds LocalBounds;
public bool HasLocalBounds;
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,
@@ -82,6 +90,12 @@ namespace MSCO
const float ReleaseAfterStill = 1f;
const float SnapDistance = 10f;
const float RemoteTimeout = 3f; // Besitzer meldet sich nicht mehr (z.B. Teil wurde dort montiert)
// Ein gefahrenes Auto NICHT nach 3 s freigeben: hängt das Spiel des Fahrers kurz (z.B. beim Speichern), blieb es
// bei den anderen schlagartig stehen und die Teile rissen ab. Trennt der Fahrer, gibt ReleaseAllOf es frei.
const float DrivenVehicleTimeout = 30f;
const float JointSettleTime = 3f; // so lange nach der Rückgabe bleiben die Gelenke der Teile unzerbrechlich
const float CarrierCheckInterval = 0.4f;
const float CarrierMinSpeed = 1.5f; // ab dieser Geschwindigkeit nimmt ein Fahrzeug seine Ladung mit
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
@@ -191,6 +205,9 @@ namespace MSCO
public bool IsRemoteOwnedBody(SyncedBody b) { return IsRemoteOwned(b); }
/// <summary>Alle erfassten Fahrzeuge (Liste wird alle 2 s erneuert, nicht verändern).</summary>
public List<SyncedBody> AllVehicles() { return Vehicles(); }
/// <summary>Für "mp vehicles": Zustand aller erfassten Fahrzeuge (zur Fehlersuche).</summary>
public List<string> DescribeVehicles()
{
@@ -258,7 +275,10 @@ namespace MSCO
public void Reset()
{
foreach (SyncedBody b in byId.Values)
{
if (IsRemoteOwned(b) && b.Rb != null) RestoreKinematic(b);
RestoreJoints(b);
}
byId.Clear();
byRb.Clear();
rejected.Clear();
@@ -338,13 +358,25 @@ namespace MSCO
b.LastPos = b.Rb.position;
if (!b.Rb.gameObject.activeInHierarchy) continue;
if (UpdateAttached(b)) continue; // angebaut: bewegt sich mit dem Auto
if (b.HeldJoints != null && !IsRemoteOwned(b) && Time.time >= b.JointRestoreAt) RestoreJoints(b);
bool active = b == held || b == driven;
if (!IsRemoteOwned(b)) StopRunaway(b);
// Ladung eines fahrenden Autos gehört dessen Fahrer: nur bei ihm wird sie mit dem Auto simuliert.
// Übernahm sie ein Mitfahrer, folgte sie beim Fahrer verspätet und kinematisch (unendlich schwer) und
// schlug Sitze und Teile ab.
SyncedBody carrier = active || b.IsVehicle ? null : CarrierOf(b);
bool carrierRemote = carrier != null && IsRemoteOwned(carrier);
bool carrierMine = carrier != null && IsLocal(carrier) && IsCarrying(carrier);
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 (carrierMine && IsMoving(b)) Claim(b, false);
else if (motionClaimAllowed && Time.time >= b.NoMotionClaimUntil && IsMoving(b) && IsNear(b, player) && !carrierRemote) Claim(b, false);
}
else if (IsLocal(b) && !active && carrierRemote && carrier.OwnerActive)
{
Release(b); // fährt ein anderer: der übernimmt die Ladung
}
else if (IsLocal(b))
{
@@ -367,7 +399,8 @@ namespace MSCO
// 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);
else if (carrierMine && !b.OwnerActive) Claim(b, false); // liegt in unserem fahrenden Auto
else if (Time.time - b.LastRecvTime > (b.IsVehicle && b.OwnerActive ? DrivenVehicleTimeout : RemoteTimeout)) SetFree(b);
}
}
ReportConsumed(player);
@@ -498,6 +531,7 @@ namespace MSCO
b.Rb.isKinematic = true;
if (b.IsVehicle)
{
b.JointRestoreAt = float.MaxValue;
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).
@@ -527,9 +561,24 @@ namespace MSCO
{
b.Rb.isKinematic = b.WasKinematic;
if (b.InterpolationSet) b.Rb.interpolation = b.WasInterpolation;
// Mitten in der Fahrt zurückgegeben (Fahrer übergibt, steigt aus ...): mit seiner Geschwindigkeit
// weiterrollen statt schlagartig stehenzubleiben - die Teile wollen sonst mit Tempo weiter und reißen ab
if (b.IsVehicle && !b.Rb.isKinematic)
{
b.Rb.velocity = Time.fixedTime - b.TargetTime < 0.5f ? b.TargetVel : Vector3.zero;
b.Rb.angularVelocity = Vector3.zero;
}
}
b.InterpolationSet = false;
if (b.HeldJoints == null) return;
if (b.IsVehicle) { b.JointRestoreAt = Time.time + JointSettleTime; return; } // erst nach dem Einschwingen
RestoreJoints(b);
}
void RestoreJoints(SyncedBody b)
{
b.JointRestoreAt = float.MaxValue;
if (b.HeldJoints == null) return;
foreach (KeyValuePair<Joint, Vector2> kv in b.HeldJoints)
{
if (kv.Key == null) continue; // Teil wurde inzwischen abgebaut
@@ -539,6 +588,70 @@ namespace MSCO
b.HeldJoints = null;
}
// ---------------------------------------------------------------- Ladung in Fahrzeugen
/// <summary>Fährt das Fahrzeug gerade (nimmt also seine Ladung mit)?</summary>
bool IsCarrying(SyncedBody v)
{
if (v.Rb == null) return false;
if (IsRemoteOwned(v)) return v.OwnerActive || v.TargetVel.sqrMagnitude > CarrierMinSpeed * CarrierMinSpeed;
return v == Driven || (!v.Rb.isKinematic && v.Rb.velocity.sqrMagnitude > CarrierMinSpeed * CarrierMinSpeed);
}
/// <summary>In welchem Fahrzeug liegt das Item gerade (null = in keinem)? Zwischengespeichert.</summary>
SyncedBody CarrierOf(SyncedBody item)
{
if (Time.time < item.NextCarrierCheck) return item.Carrier != null && item.Carrier.Rb != null ? item.Carrier : null;
item.NextCarrierCheck = Time.time + CarrierCheckInterval;
item.Carrier = null;
Vector3 p = item.Rb.worldCenterOfMass;
foreach (SyncedBody v in Vehicles())
{
if (v.Rb == null || (v.Rb.position - p).sqrMagnitude > 15f * 15f) continue;
if (!v.HasLocalBounds) ComputeLocalBounds(v);
if (v.LocalBounds.Contains(v.Rb.transform.InverseTransformPoint(p))) { item.Carrier = v; break; }
}
return item.Carrier;
}
readonly List<SyncedBody> vehicleList = new List<SyncedBody>();
float nextVehicleList;
List<SyncedBody> Vehicles()
{
if (Time.time < nextVehicleList) return vehicleList;
nextVehicleList = Time.time + 2f;
vehicleList.Clear();
foreach (SyncedBody b in byId.Values) if (b.IsVehicle && b.Rb != null) vehicleList.Add(b);
return vehicleList;
}
/// <summary>Umriss des Fahrzeugs (alle festen Collider) in seinen lokalen Koordinaten, etwas nach oben erweitert.</summary>
static void ComputeLocalBounds(SyncedBody v)
{
Transform t = v.Rb.transform;
bool any = false;
var local = new Bounds();
foreach (Collider c in v.Rb.GetComponentsInChildren<Collider>())
{
if (c == null || c.isTrigger || !c.enabled) continue;
Bounds wb = c.bounds;
for (int i = 0; i < 8; i++)
{
Vector3 corner = wb.center + Vector3.Scale(wb.extents, new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1));
Vector3 lp = t.InverseTransformPoint(corner);
if (!any) { local = new Bounds(lp, Vector3.zero); any = true; }
else local.Encapsulate(lp);
}
}
if (!any) local = new Bounds(Vector3.zero, new Vector3(2f, 1.5f, 4f));
// Weltachsen-Umrisse sind bei schrägem Auto zu groß - etwas verkleinern, oben Platz für Ladung lassen
local.Expand(new Vector3(-0.2f, 0f, -0.2f));
local.max = new Vector3(local.max.x, local.max.y + 0.5f, local.max.z);
v.LocalBounds = local;
v.HasLocalBounds = true;
}
void SendOwnedStates()
{
sendList.Clear();