diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs
index 7d8e0c5..2ceeb71 100644
--- a/MultiplayerManager.cs
+++ b/MultiplayerManager.cs
@@ -318,6 +318,19 @@ namespace MSCO
}
}
+ /// Admin: Satsuma neben sich holen (z.B. wenn er verschwunden oder im Boden ist).
+ public void BringCar()
+ {
+ if (!CanCheat || localPlayer == null) return;
+ Vector3 right = localPlayer.right;
+ Vector3 pos = localPlayer.position + right * 4f;
+ Quaternion rot = Quaternion.LookRotation(localPlayer.forward, Vector3.up);
+ string result = objects.BringVehicle("SATSUMA", pos, rot);
+ overlays.Log(result);
+ overlays.AddChat("", result, true);
+ if (result.EndsWith("next to you")) AnnounceCheat("brought the Satsuma to themselves");
+ }
+
/// Cheats für alle sichtbar im Chat melden.
void AnnounceCheat(string what)
{
diff --git a/ObjectSync.cs b/ObjectSync.cs
index 6e23d6d..4984abd 100644
--- a/ObjectSync.cs
+++ b/ObjectSync.cs
@@ -102,6 +102,8 @@ namespace MSCO
{
SyncedBody b;
if (!byId.TryGetValue(id, out b) || b.Rb == null || b.OwnerId != SyncedBody.NoOwner) return false;
+ if (!IsValidPose(pos, rot)) return false;
+ 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)
@@ -132,7 +134,8 @@ namespace MSCO
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")));
+ (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;
@@ -426,7 +429,7 @@ namespace MSCO
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) continue;
+ if (!byId.TryGetValue(id, out b) || b.Rb == null || !IsValidPose(pos, rot)) 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);
@@ -442,6 +445,7 @@ namespace MSCO
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)) { SetFree(b); return; }
b.Rb.transform.position = pos;
b.Rb.transform.rotation = rot;
b.Rb.isKinematic = b.WasKinematic;
@@ -457,6 +461,57 @@ namespace MSCO
}
}
+ /// 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;
diff --git a/Overlays.cs b/Overlays.cs
index 8071e10..3c2d6be 100644
--- a/Overlays.cs
+++ b/Overlays.cs
@@ -294,6 +294,8 @@ namespace MSCO
if (GUILayout.Button("+10.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(10000f);
GUILayout.Space(6);
if (GUILayout.Button("REFILL NEEDS", primary, GUILayout.Height(30))) mp.CheatNeeds();
+ GUILayout.Space(6);
+ if (GUILayout.Button("SATSUMA TO ME", primary, GUILayout.Height(30))) mp.BringCar();
GUILayout.EndHorizontal();
GUILayout.Label("Cheats are announced in the chat. GOTO / BRING teleport you to a player or a player to you.", small);
GUILayout.Space(10);
diff --git a/README.md b/README.md
index 65576e2..5665535 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern
- **T** – Chat
- **F8** – MSCO-Konsole mit allen Befehlen
- **/admin** – Admin-Menü für Server-Admins bzw. den P2P-Host: Spieler kicken/bannen, GOTO/BRING (teleportieren),
- Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen) – Cheats werden im Chat angezeigt
+ Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen) – Cheats werden im Chat angezeigt
- **P** – als Beifahrer ein-/aussteigen
- Alle Tasten sind in den MSCLoader-Einstellungen der Mod änderbar
- Discord Rich Presence („In the Menus“, Server, Spielerzahl)