Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3970ef130b | ||
|
|
c3f84895a7 | ||
|
|
dfdb78db17 | ||
|
|
d7a9479baa | ||
|
|
f4d154d4a5 | ||
|
|
e050712c0d | ||
|
|
24cd485bcc | ||
|
|
b33cd82e68 | ||
|
|
d32cdd5693 | ||
|
|
6d349399ab | ||
|
|
b408be7559 | ||
|
|
bf01651ccd |
@@ -0,0 +1,37 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2.7
|
||||||
|
|
||||||
|
Server und Mod müssen beide 2.7 sein (Server: `git pull && docker compose up -d --build`).
|
||||||
|
|
||||||
|
**Neu**
|
||||||
|
- Eigene Spielerfigur im MSC-Stil (Karohemd, Jeans, Arbeitsschuhe, Schnurrbart, Kappe oder Haare, je Spieler etwas
|
||||||
|
anders) statt kopierter NPCs – keine liegenden/verbuggten Figuren mehr
|
||||||
|
- Ping-Anzeige oben rechts: `ping` in der F8-Konsole (grün/gelb/rot)
|
||||||
|
- Admin-Menü: Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen), GOTO / BRING (teleportieren)
|
||||||
|
- Fahrzeugzustand-Sync: Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schäden, Verschleiß, Kilometerstand, Lack
|
||||||
|
- `vehicles` in der Konsole zeigt die Position jedes Fahrzeugs
|
||||||
|
|
||||||
|
**Behoben**
|
||||||
|
- Auto fiel beim Fahren eines anderen Spielers auseinander (angebaute Teile rissen an ihren Gelenken ab)
|
||||||
|
- Auto kippte mit dem Kopf des Fahrers mit und zitterte (Fahrzeug wurde als „in der Hand gehalten“ erkannt)
|
||||||
|
- Satsuma wurde weggeschleudert und war „verschwunden“ – ungültige/im Boden liegende Positionen werden jetzt
|
||||||
|
verworfen bzw. angehoben, weggeschleuderte Objekte sofort gestoppt
|
||||||
|
- Items und Autos zitterten nach dem Loslassen (Ownership-Pingpong)
|
||||||
|
- Absturz beim Beitreten durch einen Kreis in der Objekt-Hierarchie (nachgespielte Montage-Aktion)
|
||||||
|
- Tippen im Chat/in der Konsole löste Spieltasten aus (Benutzen, Beifahrer-Taste P …)
|
||||||
|
- Maus sprang bei offenem Admin-Menü/Chat immer in die Bildmitte
|
||||||
|
- Ein Fehler in einem Sync-Teil legt nicht mehr den ganzen Sync lahm
|
||||||
|
- Freie Fahrzeuge: nur noch deutliche Wertänderungen werden abgeglichen (kein gegenseitiges Überschreiben)
|
||||||
|
|
||||||
|
**Sicherheit (Server)**
|
||||||
|
- Admin-Rechte nur noch für von Steam bestätigte SteamIDs – der Server braucht dafür einen `SteamWebApiKey`
|
||||||
|
(sonst könnte sich jeder als Admin ausgeben). Unbestätigte SteamIDs können niemanden mehr vom Server drängen.
|
||||||
|
- Ungültige Positionen werden nicht gespeichert, Paketgröße und Weltzustand sind begrenzt
|
||||||
|
|
||||||
|
**Server & Keymaster**
|
||||||
|
- Master-Adresse fest (`msco.mbm.wtf`), nicht mehr änderbar
|
||||||
|
- Docker läuft als dein Linux-Benutzer (kein „Permission denied“ in `data/` mehr), klare Meldungen statt Absturz
|
||||||
|
- `docker exec msco-server msco <befehl>` für Server-Befehle
|
||||||
|
- Keymaster: Rate-Limits, Security-Header, Healthcheck, Docs-Seite
|
||||||
|
- Automatische Tests: Server `dotnet run --project Tests` (30 Checks), Keymaster `npm run smoke` (24 Checks)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using HutongGames.PlayMaker;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Admin-Cheats für den eigenen Spieler (Geld, Bedürfnisse). Nur aus dem Admin-Menü erreichbar, also nur für
|
||||||
|
/// Server-Admins bzw. den P2P-Host. Geld wird über MoneySync automatisch an alle weitergegeben.
|
||||||
|
/// </summary>
|
||||||
|
public static class Cheats
|
||||||
|
{
|
||||||
|
// Globale PlayMaker-Variablen des Spielers: Wert, auf den "auffüllen" sie setzt
|
||||||
|
static readonly KeyValuePair<string, float>[] Needs =
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, float>("PlayerHunger", 0f),
|
||||||
|
new KeyValuePair<string, float>("PlayerThirst", 0f),
|
||||||
|
new KeyValuePair<string, float>("PlayerStress", 0f),
|
||||||
|
new KeyValuePair<string, float>("PlayerUrine", 0f),
|
||||||
|
new KeyValuePair<string, float>("PlayerFatigue", 0f),
|
||||||
|
new KeyValuePair<string, float>("PlayerDirtiness", 0f),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string AddMoney(float amount)
|
||||||
|
{
|
||||||
|
FsmFloat money = FsmVariables.GlobalVariables.FindFsmFloat("PlayerMoney");
|
||||||
|
if (money == null) return "Money not found (only works in the game)";
|
||||||
|
money.Value += amount;
|
||||||
|
return "+" + amount.ToString("0") + " MK (now " + money.Value.ToString("0") + " MK)";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FillNeeds()
|
||||||
|
{
|
||||||
|
int done = 0;
|
||||||
|
foreach (KeyValuePair<string, float> need in Needs)
|
||||||
|
{
|
||||||
|
FsmFloat v = FsmVariables.GlobalVariables.FindFsmFloat(need.Key);
|
||||||
|
if (v == null) continue;
|
||||||
|
v.Value = need.Value;
|
||||||
|
done++;
|
||||||
|
}
|
||||||
|
return done > 0 ? "Hunger, thirst, stress, fatigue, bladder and dirt reset" : "Player needs not found (only works in the game)";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Eigenen Spieler an eine Position setzen (etwas darüber, damit er nicht im Boden steckt).</summary>
|
||||||
|
public static bool Teleport(Transform player, Vector3 position)
|
||||||
|
{
|
||||||
|
if (player == null) return false;
|
||||||
|
player.position = position + Vector3.up * 0.3f;
|
||||||
|
Rigidbody rb = player.GetComponent<Rigidbody>();
|
||||||
|
if (rb != null && !rb.isKinematic) rb.velocity = Vector3.zero;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-7
@@ -11,12 +11,12 @@ namespace MSCO
|
|||||||
public const string Help =
|
public const string Help =
|
||||||
"connect <host[:port]> - join a dedicated server\n" +
|
"connect <host[:port]> - join a dedicated server\n" +
|
||||||
"disconnect - leave the server / game\n" +
|
"disconnect - leave the server / game\n" +
|
||||||
"host [port] / join <ip> [port] - LAN test session\n" +
|
"host [port] / join <ip> [port] - direct LAN game (no Steam)\n" +
|
||||||
"save - save the game (host / singleplayer)\n" +
|
"save - save the game (host / singleplayer)\n" +
|
||||||
"/admin - admin menu (kick / ban)\n" +
|
"ping [on|off] - show your ping to the server (toggle)\n" +
|
||||||
|
"/admin - admin menu (kick / ban / teleport / cheats)\n" +
|
||||||
"vehicles | rig - debug info about vehicles / player models\n" +
|
"vehicles | rig - debug info about vehicles / player models\n" +
|
||||||
"debug - toggle sync debug output\n" +
|
"debug - toggle sync debug output\n" +
|
||||||
"master [url] - show / change the server list address\n" +
|
|
||||||
"clear - clear this console";
|
"clear - clear this console";
|
||||||
|
|
||||||
public static void Execute(string[] args, Action<string> print)
|
public static void Execute(string[] args, Action<string> print)
|
||||||
@@ -59,13 +59,15 @@ namespace MSCO
|
|||||||
mp.JoinServer(host, serverPort);
|
mp.JoinServer(host, serverPort);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "master":
|
|
||||||
if (args.Length > 1) ServerBrowser.MasterUrl = args[1];
|
|
||||||
print("Master server: " + ServerBrowser.MasterUrl);
|
|
||||||
break;
|
|
||||||
case "save":
|
case "save":
|
||||||
mp.SaveGame();
|
mp.SaveGame();
|
||||||
break;
|
break;
|
||||||
|
case "ping":
|
||||||
|
// "ping" schaltet die Anzeige um, "ping on/off" setzt sie
|
||||||
|
if (args.Length > 1) mp.ShowPing = args[1].ToLower() == "on" || args[1] == "1";
|
||||||
|
else mp.ShowPing = !mp.ShowPing;
|
||||||
|
print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")");
|
||||||
|
break;
|
||||||
case "admin":
|
case "admin":
|
||||||
case "/admin":
|
case "/admin":
|
||||||
mp.OpenAdminMenu();
|
mp.OpenAdminMenu();
|
||||||
|
|||||||
@@ -1,405 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace MSCO
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Mitspieler-Figur aus einem echten My-Summer-Car-NPC (Körper-Mesh "bodymesh" + Skelett mit "pelvis").
|
|
||||||
/// Der NPC wird kopiert und von seiner Logik befreit; es bleiben Aussehen und die Spiel-Animationen
|
|
||||||
/// (Idle/Walk). Nach der Animation werden die Knochen für Ducken, Lehnen, Kopf-Nicken, Sitzen und
|
|
||||||
/// das Greifen zum Item verbogen (Zwei-Knochen-IK, unabhängig von den Achsen des Skeletts).
|
|
||||||
/// </summary>
|
|
||||||
public class GameCharacter
|
|
||||||
{
|
|
||||||
const float StandingEyeHeight = 1.65f;
|
|
||||||
const float EyeAboveHeadBone = 0.1f; // Augen liegen etwas über dem Kopf-Knochen
|
|
||||||
const float AnkleHeight = 0.07f; // Fuß-Knochen über dem Boden
|
|
||||||
|
|
||||||
int frames;
|
|
||||||
bool calibrated;
|
|
||||||
|
|
||||||
public Transform Root { get; private set; }
|
|
||||||
public string Description { get; private set; }
|
|
||||||
|
|
||||||
Animation anim;
|
|
||||||
string walkClip, idleClip;
|
|
||||||
Transform pelvis, spine, head;
|
|
||||||
readonly Transform[] thigh = new Transform[2], knee = new Transform[2], foot = new Transform[2];
|
|
||||||
readonly Transform[] upperArm = new Transform[2], forearm = new Transform[2], hand = new Transform[2];
|
|
||||||
readonly Vector3[] animatedFoot = new Vector3[2];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Knochen, die wir verbiegen. Bewegt die Animation einen davon nicht selbst, würden sich unsere
|
|
||||||
/// Änderungen jedes Frame aufsummieren - dann wird er vorher auf die Grundhaltung zurückgesetzt.
|
|
||||||
/// </summary>
|
|
||||||
class BoneState
|
|
||||||
{
|
|
||||||
public Transform Bone;
|
|
||||||
public Vector3 RestPos, OutPos;
|
|
||||||
public Quaternion RestRot, OutRot;
|
|
||||||
public bool HasOut;
|
|
||||||
}
|
|
||||||
readonly List<BoneState> posed = new List<BoneState>();
|
|
||||||
|
|
||||||
public Vector3 HeadPosition { get { return head.position + Root.up * EyeAboveHeadBone; } }
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Vorlage finden und kopieren
|
|
||||||
|
|
||||||
static List<GameObject> templates;
|
|
||||||
|
|
||||||
/// <summary>Kopiert einen NPC aus der Spielszene als Figur. null, wenn keine passende Vorlage da ist.</summary>
|
|
||||||
public static GameCharacter TryCreate(Transform parent, int variant)
|
|
||||||
{
|
|
||||||
List<GameObject> candidates = FindTemplates();
|
|
||||||
if (candidates.Count == 0) return null;
|
|
||||||
GameObject template = candidates[Mathf.Abs(variant) % candidates.Count];
|
|
||||||
|
|
||||||
GameObject clone = (GameObject)UnityEngine.Object.Instantiate(template);
|
|
||||||
clone.name = "Model";
|
|
||||||
Strip(clone);
|
|
||||||
clone.SetActive(true);
|
|
||||||
clone.transform.SetParent(parent, false);
|
|
||||||
clone.transform.localPosition = Vector3.zero;
|
|
||||||
clone.transform.localRotation = Quaternion.identity;
|
|
||||||
clone.transform.localScale = Vector3.one;
|
|
||||||
|
|
||||||
var c = new GameCharacter { Root = clone.transform };
|
|
||||||
if (!c.FindBones())
|
|
||||||
{
|
|
||||||
UnityEngine.Object.Destroy(clone);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
c.Normalize();
|
|
||||||
c.SetupAnimation();
|
|
||||||
c.RememberRestPose();
|
|
||||||
c.Description = template.name + " (" + candidates.Count + " templates) - " + c.BoneReport();
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>NPC-Körper in der Szene (auch inaktive): SkinnedMeshRenderer "bodymesh" mit "pelvis"-Knochen.</summary>
|
|
||||||
static List<GameObject> FindTemplates()
|
|
||||||
{
|
|
||||||
if (templates != null)
|
|
||||||
{
|
|
||||||
templates.RemoveAll(t => t == null);
|
|
||||||
if (templates.Count > 0) return templates;
|
|
||||||
}
|
|
||||||
var scored = new List<KeyValuePair<GameObject, int>>();
|
|
||||||
var seen = new HashSet<string>();
|
|
||||||
foreach (UnityEngine.Object o in Resources.FindObjectsOfTypeAll(typeof(SkinnedMeshRenderer)))
|
|
||||||
{
|
|
||||||
var smr = (SkinnedMeshRenderer)o;
|
|
||||||
if (smr == null || !smr.name.ToLower().Contains("bodymesh") || smr.transform.parent == null) continue;
|
|
||||||
bool hasPelvis = false;
|
|
||||||
if (smr.bones != null)
|
|
||||||
foreach (Transform b in smr.bones)
|
|
||||||
if (b != null && b.name == "pelvis") { hasPelvis = true; break; }
|
|
||||||
if (!hasPelvis) continue;
|
|
||||||
|
|
||||||
GameObject root = smr.transform.parent.gameObject; // Körper + Skelett liegen gemeinsam darunter
|
|
||||||
if (root.transform.root.name == "PLAYER" || root.name.StartsWith("MSCO")) continue;
|
|
||||||
// Keine Sammelgruppen mit mehreren Personen kopieren (nur eine Figur, evtl. mit Varianten dünn/normal/dick)
|
|
||||||
if (PelvisCount(root.transform) != 1 || root.GetComponentsInChildren<Transform>(true).Length > 600) continue;
|
|
||||||
|
|
||||||
int score = AnimationScore(ref root);
|
|
||||||
if (!seen.Add(root.name + "/" + smr.sharedMesh)) continue; // gleiche Figur nicht mehrfach
|
|
||||||
scored.Add(new KeyValuePair<GameObject, int>(root, score));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nur die besten: am liebsten NPCs mit Lauf- UND Steh-Animation (ohne Gesten wie Anhalter/Betrunken)
|
|
||||||
int best = 0;
|
|
||||||
foreach (var kv in scored) best = Mathf.Max(best, kv.Value);
|
|
||||||
templates = new List<GameObject>();
|
|
||||||
foreach (var kv in scored) if (kv.Value == best) templates.Add(kv.Key);
|
|
||||||
templates.Sort((a, b) => string.CompareOrdinal(a.name, b.name));
|
|
||||||
return templates;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int PelvisCount(Transform root)
|
|
||||||
{
|
|
||||||
int count = 0;
|
|
||||||
foreach (Transform t in root.GetComponentsInChildren<Transform>(true)) if (t.name == "pelvis") count++;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animationen, die keine normale Steh-/Laufhaltung sind
|
|
||||||
static readonly string[] GestureWords = { "hitch", "drunk", "fat", "lift", "sit", "wave", "point", "dance", "angry", "back", "theatre", "car" };
|
|
||||||
|
|
||||||
static bool IsGesture(string clip)
|
|
||||||
{
|
|
||||||
string n = clip.ToLower();
|
|
||||||
foreach (string g in GestureWords) if (n.Contains(g)) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bewertet die Animationen eines NPCs (liegt die Animation eine Ebene höher, wird dieser Knoten
|
|
||||||
/// die Vorlage). 3 = normales Laufen + Stehen, 2 = nur Laufen, 1 = nur Stehen, 0 = keine.
|
|
||||||
/// </summary>
|
|
||||||
static int AnimationScore(ref GameObject root)
|
|
||||||
{
|
|
||||||
Animation[] found = root.GetComponentsInChildren<Animation>(true); // Unity 5.0: auch inaktive finden
|
|
||||||
Animation anim = found.Length > 0 ? found[0] : null;
|
|
||||||
if (anim == null)
|
|
||||||
{
|
|
||||||
Transform p = root.transform.parent;
|
|
||||||
for (int i = 0; i < 2 && p != null && anim == null; i++, p = p.parent)
|
|
||||||
{
|
|
||||||
var candidate = p.GetComponent<Animation>();
|
|
||||||
if (candidate != null && PelvisCount(p) == 1)
|
|
||||||
{
|
|
||||||
anim = candidate;
|
|
||||||
root = p.gameObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (anim == null) return 0;
|
|
||||||
|
|
||||||
bool walk = false, idle = false;
|
|
||||||
foreach (AnimationState s in anim)
|
|
||||||
{
|
|
||||||
if (IsGesture(s.name)) continue;
|
|
||||||
string n = s.name.ToLower();
|
|
||||||
if (n.Contains("walk")) walk = true;
|
|
||||||
if (n.Contains("idle")) idle = true;
|
|
||||||
}
|
|
||||||
return (walk ? 2 : 0) + (idle ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Alles außer Aussehen und Animation entfernen (KI-FSMs, Physik, Sounds, Trigger ...).</summary>
|
|
||||||
static void Strip(GameObject go)
|
|
||||||
{
|
|
||||||
foreach (Joint j in go.GetComponentsInChildren<Joint>(true)) UnityEngine.Object.DestroyImmediate(j);
|
|
||||||
foreach (Rigidbody r in go.GetComponentsInChildren<Rigidbody>(true)) UnityEngine.Object.DestroyImmediate(r);
|
|
||||||
foreach (Collider c in go.GetComponentsInChildren<Collider>(true)) UnityEngine.Object.DestroyImmediate(c);
|
|
||||||
foreach (Behaviour b in go.GetComponentsInChildren<Behaviour>(true))
|
|
||||||
{
|
|
||||||
if (b is Animation || b is Animator) continue;
|
|
||||||
UnityEngine.Object.DestroyImmediate(b);
|
|
||||||
}
|
|
||||||
foreach (AudioSource a in go.GetComponentsInChildren<AudioSource>(true)) UnityEngine.Object.DestroyImmediate(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FindBones()
|
|
||||||
{
|
|
||||||
pelvis = Find("pelvis");
|
|
||||||
spine = Find("spine_upper") ?? Find("spine_middle") ?? Find("spine_mid");
|
|
||||||
head = Find("head");
|
|
||||||
if (pelvis == null || spine == null || head == null) return false;
|
|
||||||
|
|
||||||
string[] sides = { "left", "right" };
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
knee[i] = Find("knee_" + sides[i]);
|
|
||||||
if (knee[i] != null)
|
|
||||||
{
|
|
||||||
thigh[i] = knee[i].parent;
|
|
||||||
foot[i] = knee[i].childCount > 0 ? knee[i].GetChild(0) : null;
|
|
||||||
}
|
|
||||||
hand[i] = Find("hand_" + sides[i]);
|
|
||||||
if (hand[i] != null && hand[i].parent != null)
|
|
||||||
{
|
|
||||||
forearm[i] = hand[i].parent;
|
|
||||||
upperArm[i] = forearm[i].parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Transform Find(string name)
|
|
||||||
{
|
|
||||||
foreach (Transform t in Root.GetComponentsInChildren<Transform>(true))
|
|
||||||
if (string.Equals(t.name, name, StringComparison.OrdinalIgnoreCase)) return t;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Größe auf Spielergröße bringen, Füße auf den Boden, Blickrichtung nach +Z.</summary>
|
|
||||||
void Normalize()
|
|
||||||
{
|
|
||||||
// Blickrichtung: Zehen (Kind des Fußes) liegen vor dem Fuß
|
|
||||||
Transform toe = foot[0] != null && foot[0].childCount > 0 ? foot[0].GetChild(0) : null;
|
|
||||||
if (toe != null && Root.parent.InverseTransformPoint(toe.position).z < Root.parent.InverseTransformPoint(foot[0].position).z - 0.01f)
|
|
||||||
Root.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
|
||||||
|
|
||||||
float eye = Root.parent.InverseTransformPoint(head.position).y + EyeAboveHeadBone;
|
|
||||||
if (eye > 0.5f) Root.localScale = Vector3.one * Mathf.Clamp(StandingEyeHeight / eye, 0.6f, 1.6f);
|
|
||||||
|
|
||||||
float lowest = float.MaxValue;
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
if (foot[i] != null) lowest = Mathf.Min(lowest, Root.parent.InverseTransformPoint(foot[i].position).y);
|
|
||||||
if (lowest < float.MaxValue) Root.localPosition = new Vector3(0f, -lowest + 0.06f, 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetupAnimation()
|
|
||||||
{
|
|
||||||
anim = Root.GetComponentInChildren<Animation>();
|
|
||||||
if (anim == null) return;
|
|
||||||
foreach (AnimationState s in anim)
|
|
||||||
{
|
|
||||||
if (IsGesture(s.name)) continue;
|
|
||||||
string n = s.name.ToLower();
|
|
||||||
if (walkClip == null && n.Contains("walk")) walkClip = s.name;
|
|
||||||
if (idleClip == null && n.Contains("idle")) idleClip = s.name;
|
|
||||||
}
|
|
||||||
// Keine Steh-Animation: im Stand die Laufanimation angehalten zeigen (statt einer Geste)
|
|
||||||
if (idleClip == null && walkClip == null && anim.clip != null && !IsGesture(anim.clip.name)) idleClip = anim.clip.name;
|
|
||||||
if (walkClip != null) anim[walkClip].wrapMode = WrapMode.Loop;
|
|
||||||
if (idleClip != null) anim[idleClip].wrapMode = WrapMode.Loop;
|
|
||||||
if (idleClip != null) anim.Play(idleClip);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RememberRestPose()
|
|
||||||
{
|
|
||||||
var bones = new List<Transform> { pelvis, spine, head };
|
|
||||||
bones.AddRange(thigh);
|
|
||||||
bones.AddRange(knee);
|
|
||||||
bones.AddRange(upperArm);
|
|
||||||
bones.AddRange(forearm);
|
|
||||||
foreach (Transform b in bones)
|
|
||||||
if (b != null)
|
|
||||||
posed.Add(new BoneState { Bone = b, RestPos = b.localPosition, RestRot = b.localRotation });
|
|
||||||
}
|
|
||||||
|
|
||||||
void ResetUnanimatedBones()
|
|
||||||
{
|
|
||||||
foreach (BoneState s in posed)
|
|
||||||
{
|
|
||||||
if (s.HasOut && s.Bone.localPosition == s.OutPos && s.Bone.localRotation == s.OutRot)
|
|
||||||
{
|
|
||||||
s.Bone.localPosition = s.RestPos;
|
|
||||||
s.Bone.localRotation = s.RestRot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RememberOutput()
|
|
||||||
{
|
|
||||||
foreach (BoneState s in posed)
|
|
||||||
{
|
|
||||||
s.OutPos = s.Bone.localPosition;
|
|
||||||
s.OutRot = s.Bone.localRotation;
|
|
||||||
s.HasOut = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string BoneReport()
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.Append("legs ").Append(knee[0] != null && foot[0] != null ? "ok" : "missing");
|
|
||||||
sb.Append(", arms ").Append(hand[1] != null && upperArm[1] != null ? "ok" : "missing");
|
|
||||||
sb.Append(", anim idle=").Append(idleClip ?? "-").Append(" walk=").Append(walkClip ?? "-");
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Pro Frame (LateUpdate, nach der Animation)
|
|
||||||
|
|
||||||
public void Animate(float speed)
|
|
||||||
{
|
|
||||||
if (anim == null) return;
|
|
||||||
bool walking = speed > 0.4f && walkClip != null;
|
|
||||||
string clip = walking ? walkClip : (idleClip ?? walkClip);
|
|
||||||
if (clip == null) return;
|
|
||||||
if (!anim.IsPlaying(clip)) anim.CrossFade(clip, 0.25f);
|
|
||||||
if (clip == walkClip) anim[walkClip].speed = walking ? Mathf.Clamp(speed / 1.4f, 0.6f, 2.2f) : 0f; // angehalten = stehen
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Pose(Transform body, Vector3 headTarget, float pitch, bool seated, bool holding, Vector3 heldPos)
|
|
||||||
{
|
|
||||||
Vector3 up = body.up, forward = body.forward, right = body.right;
|
|
||||||
ResetUnanimatedBones();
|
|
||||||
frames++;
|
|
||||||
|
|
||||||
// 1) Auf den Boden stellen: die (animierten) Füße stehen auf der Bodenhöhe des Spielers.
|
|
||||||
// Die Grundhaltung eines NPCs passt oft nicht zur Animation - deshalb jedes Frame nachmessen.
|
|
||||||
float lowestFoot = float.MaxValue;
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
if (foot[i] != null) lowestFoot = Mathf.Min(lowestFoot, Vector3.Dot(foot[i].position - body.position, up));
|
|
||||||
if (lowestFoot < float.MaxValue)
|
|
||||||
{
|
|
||||||
Vector3 lp = Root.localPosition;
|
|
||||||
lp.y -= lowestFoot - AnkleHeight;
|
|
||||||
Root.localPosition = lp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Größe einmal an die laufende Animation anpassen (Augenhöhe wie der Spieler)
|
|
||||||
if (!calibrated && frames > 5 && !seated)
|
|
||||||
{
|
|
||||||
calibrated = true;
|
|
||||||
float eye = Vector3.Dot(head.position + up * EyeAboveHeadBone - body.position, up);
|
|
||||||
if (eye > 0.5f) Root.localScale *= Mathf.Clamp(StandingEyeHeight / eye, 0.7f, 1.4f);
|
|
||||||
RememberOutput();
|
|
||||||
return; // nächstes Frame mit neuer Größe neu auf den Boden stellen
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fußpositionen der Animation merken (die sollen stehen bleiben)
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
if (foot[i] != null) animatedFoot[i] = foot[i].position;
|
|
||||||
|
|
||||||
// 3) Becken: beim Ducken/Sitzen absenken, beim Lehnen nach hinten ausweichen
|
|
||||||
Vector3 eyeNow = head.position + up * EyeAboveHeadBone;
|
|
||||||
Vector3 offset = headTarget - eyeNow;
|
|
||||||
Vector3 horizontal = offset - up * Vector3.Dot(offset, up);
|
|
||||||
if (horizontal.magnitude > 0.6f) horizontal = horizontal.normalized * 0.6f;
|
|
||||||
float drop = Mathf.Clamp(Vector3.Dot(offset, up), -1.1f, 0f);
|
|
||||||
pelvis.position += up * drop - horizontal * 0.25f;
|
|
||||||
|
|
||||||
// Wirbelsäule so drehen, dass der Kopf an der Kamera sitzt (nur wenn nötig)
|
|
||||||
if (horizontal.sqrMagnitude > 0.0025f || drop < -0.05f)
|
|
||||||
Aim(spine, head.position + up * EyeAboveHeadBone, head.position + up * EyeAboveHeadBone + horizontal * 0.75f);
|
|
||||||
// Kopf nicken
|
|
||||||
head.rotation = Quaternion.AngleAxis(pitch, right) * head.rotation;
|
|
||||||
|
|
||||||
// Beine
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
if (thigh[i] == null || knee[i] == null || foot[i] == null) continue;
|
|
||||||
Vector3 target = seated
|
|
||||||
? thigh[i].position + forward * 0.45f - up * 0.4f
|
|
||||||
: animatedFoot[i];
|
|
||||||
SolveTwoBone(thigh[i], knee[i], foot[i], target, forward);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arme: rechts zum gehaltenen Item, im Auto beide nach vorne (Lenkrad)
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
if (upperArm[i] == null || forearm[i] == null || hand[i] == null) continue;
|
|
||||||
if (i == 1 && holding)
|
|
||||||
SolveTwoBone(upperArm[i], forearm[i], hand[i], heldPos, -forward - up * 0.5f);
|
|
||||||
else if (seated)
|
|
||||||
SolveTwoBone(upperArm[i], forearm[i], hand[i], upperArm[i].position + forward * 0.38f - up * 0.28f, -up);
|
|
||||||
}
|
|
||||||
RememberOutput();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Knochen so drehen, dass "child" in Richtung "target" zeigt - egal wie das Skelett ausgerichtet ist.</summary>
|
|
||||||
static void Aim(Transform bone, Vector3 childPos, Vector3 target)
|
|
||||||
{
|
|
||||||
Vector3 from = childPos - bone.position;
|
|
||||||
Vector3 to = target - bone.position;
|
|
||||||
if (from.sqrMagnitude < 1e-6f || to.sqrMagnitude < 1e-6f) return;
|
|
||||||
bone.rotation = Quaternion.FromToRotation(from, to) * bone.rotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void SolveTwoBone(Transform upper, Transform lower, Transform end, Vector3 target, Vector3 bendHint)
|
|
||||||
{
|
|
||||||
float a = (lower.position - upper.position).magnitude;
|
|
||||||
float b = (end.position - lower.position).magnitude;
|
|
||||||
if (a < 0.01f || b < 0.01f) return;
|
|
||||||
|
|
||||||
Vector3 root = upper.position;
|
|
||||||
Vector3 toTarget = target - root;
|
|
||||||
float d = Mathf.Clamp(toTarget.magnitude, 0.01f, a + b - 0.001f);
|
|
||||||
Vector3 dir = toTarget.normalized;
|
|
||||||
float along = (a * a - b * b + d * d) / (2f * d);
|
|
||||||
float sideways = Mathf.Sqrt(Mathf.Max(0f, a * a - along * along));
|
|
||||||
Vector3 bend = bendHint - dir * Vector3.Dot(bendHint, dir);
|
|
||||||
if (bend.sqrMagnitude < 1e-6f) bend = Vector3.up - dir * Vector3.Dot(Vector3.up, dir);
|
|
||||||
Vector3 joint = root + dir * along + bend.normalized * sideways;
|
|
||||||
|
|
||||||
Aim(upper, lower.position, joint);
|
|
||||||
Aim(lower, end.position, root + dir * d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+45
-1
@@ -74,11 +74,15 @@ namespace MSCO
|
|||||||
Cursor.lockState = CursorLockMode.None;
|
Cursor.lockState = CursorLockMode.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>PlayMaker-Aktionen, die den Mauszeiger setzen, überspringen, solange ein Fenster offen ist.</summary>
|
/// <summary>
|
||||||
|
/// PlayMaker-Aktionen, die den Mauszeiger setzen, überspringen, solange ein Fenster offen ist. Außerdem die
|
||||||
|
/// Tastenabfragen des Spiels (cInput) sperren: sonst löst Tippen im Chat Spielaktionen aus (Benutzen, Hupe ...).
|
||||||
|
/// </summary>
|
||||||
static void PatchCursorActions()
|
static void PatchCursorActions()
|
||||||
{
|
{
|
||||||
if (patched) return;
|
if (patched) return;
|
||||||
patched = true;
|
patched = true;
|
||||||
|
PatchGameInput();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Type action = null;
|
Type action = null;
|
||||||
@@ -104,5 +108,45 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool SkipWhileLocked() { return !locked; }
|
static bool SkipWhileLocked() { return !locked; }
|
||||||
|
|
||||||
|
static void PatchGameInput()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Type input = typeof(cInput);
|
||||||
|
HarmonyInstance harmony = HarmonyInstance.Create("mbm.msco.inputlock.keys");
|
||||||
|
var boolPrefix = new HarmonyMethod(typeof(InputLock).GetMethod("NoKeyWhileLocked", BindingFlags.Static | BindingFlags.NonPublic));
|
||||||
|
var floatPrefix = new HarmonyMethod(typeof(InputLock).GetMethod("NoAxisWhileLocked", BindingFlags.Static | BindingFlags.NonPublic));
|
||||||
|
int count = 0;
|
||||||
|
foreach (MethodInfo m in input.GetMethods(BindingFlags.Static | BindingFlags.Public))
|
||||||
|
{
|
||||||
|
bool keyQuery = m.Name == "GetKey" || m.Name == "GetKeyDown" || m.Name == "GetKeyUp" ||
|
||||||
|
m.Name == "GetButton" || m.Name == "GetButtonDown" || m.Name == "GetButtonUp";
|
||||||
|
bool axisQuery = m.Name == "GetAxis" || m.Name == "GetAxisRaw";
|
||||||
|
if (keyQuery && m.ReturnType == typeof(bool)) { harmony.Patch(m, boolPrefix, null); count++; }
|
||||||
|
else if (axisQuery && m.ReturnType == typeof(float)) { harmony.Patch(m, floatPrefix, null); count++; }
|
||||||
|
}
|
||||||
|
ModConsole.Print("[MSCO] Game input is paused while typing (" + count + " cInput queries)");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
ModConsole.Warning("[MSCO] Could not pause game input while typing: " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Harmony-Prefixe: solange ein Eingabefenster offen ist, meldet cInput "nichts gedrückt"
|
||||||
|
static bool NoKeyWhileLocked(ref bool __result)
|
||||||
|
{
|
||||||
|
if (!locked) return true;
|
||||||
|
__result = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool NoAxisWhileLocked(ref float __result)
|
||||||
|
{
|
||||||
|
if (!locked) return true;
|
||||||
|
__result = 0f;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -216,7 +216,7 @@ namespace MSCO
|
|||||||
float nameX = row.x + 26;
|
float nameX = row.x + 26;
|
||||||
if (s.IsLocal)
|
if (s.IsLocal)
|
||||||
{
|
{
|
||||||
// Server auf diesem PC (wie bei FiveM "localhost")
|
// Server auf diesem PC (localhost)
|
||||||
nameX += Pill(nameX, row, "LOCAL", pillStyle);
|
nameX += Pill(nameX, row, "LOCAL", pillStyle);
|
||||||
}
|
}
|
||||||
if (!string.IsNullOrEmpty(s.Version) && s.Version != MSCO.ModVersion)
|
if (!string.IsNullOrEmpty(s.Version) && s.Version != MSCO.ModVersion)
|
||||||
@@ -283,7 +283,7 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Server-Profil wie bei FiveM: wem der Server gehört, Version, Spieler, Laufzeit ...</summary>
|
/// <summary>Server-Profil: wem der Server gehört, Version, Spieler, Laufzeit ...</summary>
|
||||||
void DrawServerProfile(MultiplayerManager mp, ServerListEntry s)
|
void DrawServerProfile(MultiplayerManager mp, ServerListEntry s)
|
||||||
{
|
{
|
||||||
if (GUILayout.Button("◂ BACK", tabStyle, GUILayout.Height(26), GUILayout.Width(90)))
|
if (GUILayout.Button("◂ BACK", tabStyle, GUILayout.Height(26), GUILayout.Width(90)))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
public class MSCO : Mod
|
public class MSCO : Mod
|
||||||
{
|
{
|
||||||
public const string ModVersion = "2.6";
|
public const string ModVersion = "2.7";
|
||||||
|
|
||||||
public override string ID => "MSCO"; // Your (unique) mod ID
|
public override string ID => "MSCO"; // Your (unique) mod ID
|
||||||
public override string Name => "MSCO"; // Your mod name
|
public override string Name => "MSCO"; // Your mod name
|
||||||
|
|||||||
+1
-1
@@ -75,6 +75,7 @@
|
|||||||
<Compile Include="MoneySync.cs" />
|
<Compile Include="MoneySync.cs" />
|
||||||
<Compile Include="MpCommand.cs" />
|
<Compile Include="MpCommand.cs" />
|
||||||
<Compile Include="Commands.cs" />
|
<Compile Include="Commands.cs" />
|
||||||
|
<Compile Include="Cheats.cs" />
|
||||||
<Compile Include="InputLock.cs" />
|
<Compile Include="InputLock.cs" />
|
||||||
<Compile Include="Overlays.cs" />
|
<Compile Include="Overlays.cs" />
|
||||||
<Compile Include="SafetyPatches.cs" />
|
<Compile Include="SafetyPatches.cs" />
|
||||||
@@ -85,7 +86,6 @@
|
|||||||
<Compile Include="DiscordPresence.cs" />
|
<Compile Include="DiscordPresence.cs" />
|
||||||
<Compile Include="DoorSync.cs" />
|
<Compile Include="DoorSync.cs" />
|
||||||
<Compile Include="FsmSync.cs" />
|
<Compile Include="FsmSync.cs" />
|
||||||
<Compile Include="GameCharacter.cs" />
|
|
||||||
<Compile Include="JoinLinks.cs" />
|
<Compile Include="JoinLinks.cs" />
|
||||||
<Compile Include="LobbyUI.cs" />
|
<Compile Include="LobbyUI.cs" />
|
||||||
<Compile Include="MenuHelper.cs" />
|
<Compile Include="MenuHelper.cs" />
|
||||||
|
|||||||
+219
-26
@@ -64,6 +64,9 @@ namespace MSCO
|
|||||||
Steamworks.CSteamID pendingLobby; // Einladung, die noch angenommen werden muss
|
Steamworks.CSteamID pendingLobby; // Einladung, die noch angenommen werden muss
|
||||||
Steamworks.CSteamID steamHost; // Client: Steam-ID des Hosts
|
Steamworks.CSteamID steamHost; // Client: Steam-ID des Hosts
|
||||||
float nextPing;
|
float nextPing;
|
||||||
|
float pingMs = -1f; // geglättete Laufzeit zum Server/Host (-1 = noch unbekannt)
|
||||||
|
const string PrefShowPing = "MSCO_ShowPing";
|
||||||
|
GUIStyle pingStyle;
|
||||||
float savingMessageUntil;
|
float savingMessageUntil;
|
||||||
GUIStyle hudStyle, savingStyle;
|
GUIStyle hudStyle, savingStyle;
|
||||||
Texture2D hudBackground;
|
Texture2D hudBackground;
|
||||||
@@ -221,6 +224,121 @@ namespace MSCO
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Ping
|
||||||
|
|
||||||
|
/// <summary>Laufzeit zum Server/Host in ms (-1 = unbekannt, z.B. alter Server oder man ist selbst Host).</summary>
|
||||||
|
public float PingMs { get { return pingMs; } }
|
||||||
|
|
||||||
|
/// <summary>Ping-Anzeige oben rechts (Konsole: "ping"), bleibt gespeichert.</summary>
|
||||||
|
public bool ShowPing
|
||||||
|
{
|
||||||
|
get { return PlayerPrefs.GetInt(PrefShowPing, 0) == 1; }
|
||||||
|
set { PlayerPrefs.SetInt(PrefShowPing, value ? 1 : 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Text für Anzeige und Konsole.</summary>
|
||||||
|
public string PingText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (session == null) return "not connected";
|
||||||
|
if (session.IsHost) return "you are the host";
|
||||||
|
return pingMs < 0f ? "measuring..." : Mathf.RoundToInt(pingMs) + " ms";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawPing()
|
||||||
|
{
|
||||||
|
if (!ShowPing || session == null) return;
|
||||||
|
if (pingStyle == null)
|
||||||
|
{
|
||||||
|
pingStyle = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.85f), 6);
|
||||||
|
pingStyle.fontSize = 12;
|
||||||
|
pingStyle.fontStyle = FontStyle.Bold;
|
||||||
|
pingStyle.alignment = TextAnchor.MiddleCenter;
|
||||||
|
pingStyle.padding = new RectOffset(10, 10, 0, 0);
|
||||||
|
}
|
||||||
|
// Farbe nach Qualität: grün < 80 ms, gelb < 150 ms, sonst rot
|
||||||
|
Color color = pingMs < 0f ? UiTheme.Muted : pingMs < 80f ? UiTheme.Online : pingMs < 150f ? UiTheme.Pending : UiTheme.Danger;
|
||||||
|
pingStyle.normal.textColor = color;
|
||||||
|
var content = new GUIContent("PING " + PingText);
|
||||||
|
float width = pingStyle.CalcSize(content).x;
|
||||||
|
GUI.Box(new Rect(Screen.width - width - 12, 8, width, 24), content, pingStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Cheats (Admin-Menü)
|
||||||
|
|
||||||
|
/// <summary>Cheats nur im Spiel und nur für Server-Admins bzw. den P2P-Host.</summary>
|
||||||
|
public bool CanCheat { get { return session != null && MenuHelper.InGame && (dedicated ? isAdmin : session.IsHost); } }
|
||||||
|
|
||||||
|
public void CheatMoney(float amount)
|
||||||
|
{
|
||||||
|
if (!CanCheat) return;
|
||||||
|
overlays.Log(Cheats.AddMoney(amount));
|
||||||
|
AnnounceCheat("gave themselves " + amount.ToString("0") + " MK");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheatNeeds()
|
||||||
|
{
|
||||||
|
if (!CanCheat) return;
|
||||||
|
overlays.Log(Cheats.FillNeeds());
|
||||||
|
AnnounceCheat("refilled their needs");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Zu einem Mitspieler teleportieren.</summary>
|
||||||
|
public void GotoPlayer(int playerId)
|
||||||
|
{
|
||||||
|
PlayerInfo p;
|
||||||
|
if (!CanCheat || !players.TryGetValue(playerId, out p) || p.Avatar == null) { overlays.Log("Player position unknown"); return; }
|
||||||
|
passenger.Exit(localPlayer, playerCamera);
|
||||||
|
PlayerState s = p.Avatar.LastState;
|
||||||
|
Vector3 side = Quaternion.Euler(0f, s.RotationY, 0f) * Vector3.right;
|
||||||
|
if (Cheats.Teleport(localPlayer, s.Position + side * TeleportSideOffset)) overlays.Log("Teleported to " + p.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Mitspieler zu sich holen (Server prüft, ob man Admin ist).</summary>
|
||||||
|
public void BringPlayer(int playerId)
|
||||||
|
{
|
||||||
|
if (!CanCheat || localPlayer == null || playerId == localId) return;
|
||||||
|
Vector3 pos = localPlayer.position + localPlayer.forward * TeleportSideOffset;
|
||||||
|
if (dedicated)
|
||||||
|
{
|
||||||
|
session.SendTo(NetSession.HostId, MsgType.AdminAction, NetSession.Build(w =>
|
||||||
|
{
|
||||||
|
w.Write((byte)3);
|
||||||
|
w.Write(playerId);
|
||||||
|
w.Write("");
|
||||||
|
w.Write(pos.x); w.Write(pos.y); w.Write(pos.z);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else if (players.ContainsKey(playerId))
|
||||||
|
{
|
||||||
|
session.SendTo(playerId, MsgType.Teleport, NetSession.Build(w => { w.Write(pos.x); w.Write(pos.y); w.Write(pos.z); }));
|
||||||
|
overlays.Log("Brought " + players[playerId].Name + " to you");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Admin: Satsuma neben sich holen (z.B. wenn er verschwunden oder im Boden ist).</summary>
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Cheats für alle sichtbar im Chat melden.</summary>
|
||||||
|
void AnnounceCheat(string what)
|
||||||
|
{
|
||||||
|
string text = OwnName() + " " + what + " (admin)";
|
||||||
|
overlays.AddChat("", text, true);
|
||||||
|
SendGame(MsgType.Chat, NetSession.Build(w => w.Write("* " + text)));
|
||||||
|
}
|
||||||
|
|
||||||
public void AdminAction(bool ban, int playerId, string reason)
|
public void AdminAction(bool ban, int playerId, string reason)
|
||||||
{
|
{
|
||||||
if (session == null) return;
|
if (session == null) return;
|
||||||
@@ -577,7 +695,8 @@ namespace MSCO
|
|||||||
if (Time.realtimeSinceStartup >= nextPing)
|
if (Time.realtimeSinceStartup >= nextPing)
|
||||||
{
|
{
|
||||||
nextPing = Time.realtimeSinceStartup + PingInterval;
|
nextPing = Time.realtimeSinceStartup + PingInterval;
|
||||||
session.SendToAll(MsgType.Ping);
|
float sentAt = Time.realtimeSinceStartup;
|
||||||
|
session.SendToAll(MsgType.Ping, NetSession.Build(w => w.Write(sentAt))); // Zeitstempel für die Ping-Messung
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (NetEvent e in session.Poll())
|
foreach (NetEvent e in session.Poll())
|
||||||
@@ -644,6 +763,7 @@ namespace MSCO
|
|||||||
if (session != null && session.IsSteam) steam.Leave();
|
if (session != null && session.IsSteam) steam.Leave();
|
||||||
session = null;
|
session = null;
|
||||||
handshakeDone = false;
|
handshakeDone = false;
|
||||||
|
pingMs = -1f;
|
||||||
teleportToHost = false;
|
teleportToHost = false;
|
||||||
dedicated = false;
|
dedicated = false;
|
||||||
connectingToServer = false;
|
connectingToServer = false;
|
||||||
@@ -692,6 +812,22 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
BinaryReader r = m.Reader();
|
BinaryReader r = m.Reader();
|
||||||
|
|
||||||
|
// Ping-Messung: der Host schickt den Zeitstempel sofort zurück, der Client rechnet die Laufzeit aus
|
||||||
|
if (m.Type == MsgType.Ping)
|
||||||
|
{
|
||||||
|
if (session.IsHost && m.Payload.Length >= 4) session.SendTo(peerId, MsgType.Pong, m.Payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m.Type == MsgType.Pong)
|
||||||
|
{
|
||||||
|
if (!session.IsHost && m.Payload.Length >= 4)
|
||||||
|
{
|
||||||
|
float rtt = (Time.realtimeSinceStartup - r.ReadSingle()) * 1000f;
|
||||||
|
if (rtt >= 0f && rtt < 60000f) pingMs = pingMs < 0f ? rtt : Mathf.Lerp(pingMs, rtt, 0.4f);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (IsGameMessage(m.Type))
|
if (IsGameMessage(m.Type))
|
||||||
{
|
{
|
||||||
int from;
|
int from;
|
||||||
@@ -835,6 +971,17 @@ namespace MSCO
|
|||||||
SaveSync.WriteFile(file, data);
|
SaveSync.WriteFile(file, data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MsgType.Teleport:
|
||||||
|
{
|
||||||
|
// Ein Admin holt uns zu sich
|
||||||
|
var pos = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
|
||||||
|
if (!MenuHelper.InGame || localPlayer == null) break;
|
||||||
|
passenger.Exit(localPlayer, playerCamera);
|
||||||
|
Cheats.Teleport(localPlayer, pos);
|
||||||
|
overlays.AddChat("", "An admin teleported you", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case MsgType.AdminStatus:
|
case MsgType.AdminStatus:
|
||||||
isAdmin = r.ReadBoolean();
|
isAdmin = r.ReadBoolean();
|
||||||
if (isAdmin) overlays.Log("You are an admin on this server - type /admin in chat or 'admin' here.");
|
if (isAdmin) overlays.Log("You are an admin on this server - type /admin in chat or 'admin' here.");
|
||||||
@@ -1086,67 +1233,107 @@ namespace MSCO
|
|||||||
StartCoroutine(ApplyWorldSnapshot(snapshot));
|
StartCoroutine(ApplyWorldSnapshot(snapshot));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MSCO.PassengerKey != null && MSCO.PassengerKey.GetKeybindDown())
|
if (MSCO.PassengerKey != null && !overlays.AnyOpen && MSCO.PassengerKey.GetKeybindDown())
|
||||||
{
|
{
|
||||||
string result = passenger.Toggle(localPlayer, playerCamera, objects, players);
|
string result = passenger.Toggle(localPlayer, playerCamera, objects, players);
|
||||||
if (result != null) SetStatus(result);
|
if (result != null)
|
||||||
|
{
|
||||||
|
SetStatus(result);
|
||||||
|
overlays.AddChat("", result, true); // im Spiel sichtbar, nicht nur in der Konsole
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ready = IsPeerReady;
|
bool ready = IsPeerReady;
|
||||||
if (ready)
|
if (ready)
|
||||||
{
|
{
|
||||||
|
// Jedes Teilsystem einzeln abgesichert: ein Fehler in einem (z.B. an einem gerade zerstörten Objekt)
|
||||||
|
// soll nicht den ganzen Sync stoppen
|
||||||
sendTimer += Time.unscaledDeltaTime; // läuft auch im Pausenmenü (timeScale = 0) weiter
|
sendTimer += Time.unscaledDeltaTime; // läuft auch im Pausenmenü (timeScale = 0) weiter
|
||||||
if (sendTimer >= SendInterval)
|
if (sendTimer >= SendInterval)
|
||||||
{
|
{
|
||||||
sendTimer = 0f;
|
sendTimer = 0f;
|
||||||
SendGame(MsgType.Player, BuildLocalState().Serialize());
|
try { SendGame(MsgType.Player, BuildLocalState().Serialize()); }
|
||||||
|
catch (System.Exception e) { ReportError("player state", e); }
|
||||||
}
|
}
|
||||||
objects.Update(localPlayer, Time.deltaTime);
|
try { objects.Update(localPlayer, Time.deltaTime); }
|
||||||
vehicleFx.Update(objects, SendGame, Time.deltaTime);
|
catch (System.Exception e) { ReportError("object sync", e); }
|
||||||
worldSync.Update(IsWorldAuthority, SendGame, Time.unscaledDeltaTime);
|
try { vehicleFx.Update(objects, SendGame, Time.deltaTime); }
|
||||||
|
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
||||||
|
try { worldSync.Update(IsWorldAuthority, SendGame, Time.unscaledDeltaTime); }
|
||||||
|
catch (System.Exception e) { ReportError("time/weather sync", e); }
|
||||||
|
|
||||||
// Geld erst beobachten, wenn das Spiel fertig geladen ist
|
// Geld erst beobachten, wenn das Spiel fertig geladen ist
|
||||||
if (objects.ScanComplete) moneySync.Begin();
|
try
|
||||||
moneySync.Update(SendGame);
|
{
|
||||||
|
if (objects.ScanComplete) moneySync.Begin();
|
||||||
|
moneySync.Update(SendGame);
|
||||||
|
}
|
||||||
|
catch (System.Exception e) { ReportError("money sync", e); }
|
||||||
|
|
||||||
// Türen, Tore, Hauben (z.B. Garagentor)
|
// Türen, Tore, Hauben (z.B. Garagentor)
|
||||||
if (objects.ScanComplete) doorSync.Update(SendGame);
|
try { if (objects.ScanComplete) doorSync.Update(SendGame); }
|
||||||
|
catch (System.Exception e) { ReportError("door sync", e); }
|
||||||
|
|
||||||
// Gesamtzustand der Fahrzeuge (Tank, Öl, Schrauben, Verschleiß ...)
|
// Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...)
|
||||||
if (objects.ScanComplete) vehicleState.Update(objects, localId, SendGame);
|
try { if (objects.ScanComplete) vehicleState.Update(objects, localId, SendGame); }
|
||||||
|
catch (System.Exception e) { ReportError("vehicle state sync", e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (PlayerInfo p in players.Values)
|
foreach (PlayerInfo p in players.Values)
|
||||||
{
|
{
|
||||||
if (p.Avatar == null) continue;
|
if (p.Avatar == null) continue;
|
||||||
p.Avatar.Tick(Time.unscaledDeltaTime);
|
try
|
||||||
|
{
|
||||||
|
p.Avatar.Tick(Time.unscaledDeltaTime);
|
||||||
|
|
||||||
// Gehaltenes Item an die Hand der Figur setzen
|
// Gehaltenes Item an die Hand der Figur setzen
|
||||||
Vector3 pos;
|
Vector3 pos;
|
||||||
Quaternion rot;
|
Quaternion rot;
|
||||||
if (p.Avatar.TryGetHeldPose(out pos, out rot))
|
if (p.Avatar.TryGetHeldPose(out pos, out rot))
|
||||||
objects.ApplyAvatarHold(p.Id, p.Avatar.HeldId, pos, rot);
|
objects.ApplyAvatarHold(p.Id, p.Avatar.HeldId, pos, rot);
|
||||||
|
}
|
||||||
|
catch (System.Exception e) { ReportError("player figure", e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abgefangene Schraub-/Montage-Aktionen verschicken
|
// Abgefangene Schraub-/Montage-Aktionen verschicken
|
||||||
FsmSync.Enabled = ready;
|
try
|
||||||
FsmSync.Tick();
|
{
|
||||||
foreach (FsmEventMsg msg in FsmSync.Outbox) SendGame(MsgType.FsmEvent, msg.Serialize());
|
FsmSync.Enabled = ready;
|
||||||
|
FsmSync.Tick();
|
||||||
|
foreach (FsmEventMsg msg in FsmSync.Outbox) SendGame(MsgType.FsmEvent, msg.Serialize());
|
||||||
|
}
|
||||||
|
catch (System.Exception e) { ReportError("part/bolt sync", e); }
|
||||||
FsmSync.Outbox.Clear();
|
FsmSync.Outbox.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly Dictionary<string, float> lastErrorReport = new Dictionary<string, float>();
|
||||||
|
|
||||||
|
/// <summary>Fehler eines Teilsystems melden (höchstens alle 30 s pro Teilsystem, damit das Log nicht vollläuft).</summary>
|
||||||
|
void ReportError(string what, System.Exception e)
|
||||||
|
{
|
||||||
|
float now = Time.realtimeSinceStartup, last;
|
||||||
|
if (lastErrorReport.TryGetValue(what, out last) && now - last < 30f) return;
|
||||||
|
lastErrorReport[what] = now;
|
||||||
|
ModConsole.Error("[MSCO] " + what + " failed: " + e);
|
||||||
|
overlays.Log("Error in " + what + ": " + e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
void FixedUpdate()
|
void FixedUpdate()
|
||||||
{
|
{
|
||||||
if (IsPeerReady && MenuHelper.InGame) objects.FixedUpdate();
|
if (!IsPeerReady || !MenuHelper.InGame) return;
|
||||||
|
try { objects.FixedUpdate(); }
|
||||||
|
catch (System.Exception e) { ReportError("object movement", e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void LateUpdate()
|
void LateUpdate()
|
||||||
{
|
{
|
||||||
InputLock.KeepCursorFree(); // das Spiel sperrt den Zeiger im selben Frame evtl. wieder
|
InputLock.KeepCursorFree(); // das Spiel sperrt den Zeiger im selben Frame evtl. wieder
|
||||||
// Nach den Spiel-FSMs, damit Lichter/Motorgeräusch der Fahrzeuge anderer gewinnen
|
// Nach den Spiel-FSMs, damit Lichter/Motorgeräusch der Fahrzeuge anderer gewinnen
|
||||||
if (IsPeerReady && MenuHelper.InGame) vehicleFx.Apply(objects);
|
try { if (IsPeerReady && MenuHelper.InGame) vehicleFx.Apply(objects); }
|
||||||
|
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
||||||
// Beifahrer nach der Fahrzeugbewegung auf den Sitz setzen
|
// Beifahrer nach der Fahrzeugbewegung auf den Sitz setzen
|
||||||
if (localPlayer != null && MenuHelper.InGame) passenger.LateUpdate(localPlayer, playerCamera, objects);
|
try { if (localPlayer != null && MenuHelper.InGame) passenger.LateUpdate(localPlayer, playerCamera, objects); }
|
||||||
|
catch (System.Exception e) { ReportError("passenger", e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1284,8 +1471,12 @@ namespace MSCO
|
|||||||
|
|
||||||
static Color PlayerColor(int id)
|
static Color PlayerColor(int id)
|
||||||
{
|
{
|
||||||
Color[] colors = { Color.cyan, new Color(1f, 0.55f, 0.1f), Color.green, Color.magenta,
|
// Gedeckte Farben wie Flanellhemden/Arbeitsjacken in MSC
|
||||||
Color.yellow, new Color(0.4f, 0.6f, 1f), Color.red, Color.white };
|
Color[] colors =
|
||||||
|
{
|
||||||
|
new Color(0.6f, 0.13f, 0.12f), new Color(0.16f, 0.25f, 0.5f), new Color(0.2f, 0.38f, 0.2f), new Color(0.7f, 0.52f, 0.14f),
|
||||||
|
new Color(0.42f, 0.28f, 0.17f), new Color(0.33f, 0.36f, 0.4f), new Color(0.45f, 0.14f, 0.3f), new Color(0.38f, 0.4f, 0.16f)
|
||||||
|
};
|
||||||
return colors[id % colors.Length];
|
return colors[id % colors.Length];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1303,6 +1494,7 @@ namespace MSCO
|
|||||||
if (MenuHelper.InMenu)
|
if (MenuHelper.InMenu)
|
||||||
{
|
{
|
||||||
lobby.Draw(this);
|
lobby.Draw(this);
|
||||||
|
DrawPing();
|
||||||
overlays.Draw(this, false);
|
overlays.Draw(this, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1312,6 +1504,7 @@ namespace MSCO
|
|||||||
GUI.Label(new Rect(0, Screen.height / 2f - 40, Screen.width, 80), "SAVING GAME...", savingStyle);
|
GUI.Label(new Rect(0, Screen.height / 2f - 40, Screen.width, 80), "SAVING GAME...", savingStyle);
|
||||||
|
|
||||||
if (session != null) DrawHud();
|
if (session != null) DrawHud();
|
||||||
|
DrawPing();
|
||||||
overlays.Draw(this, session != null);
|
overlays.Draw(this, session != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -37,7 +37,9 @@ namespace MSCO
|
|||||||
AdminAction = 26, // Client -> Server: byte (1 = kick, 2 = ban), int Spieler-ID, string Grund
|
AdminAction = 26, // Client -> Server: byte (1 = kick, 2 = ban), int Spieler-ID, string Grund
|
||||||
AdminResult = 27, // Server -> Client: string Meldung
|
AdminResult = 27, // Server -> Client: string Meldung
|
||||||
Chat = 28, // Chatnachricht (string)
|
Chat = 28, // Chatnachricht (string)
|
||||||
VarSync = 29 // Fahrzeugzustand: int n, je [int key, byte typ, wert]
|
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
|
||||||
|
Teleport = 30, // Admin holt einen Spieler zu sich: 3 float Position
|
||||||
|
Pong = 31 // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Message
|
public struct Message
|
||||||
|
|||||||
+134
-11
@@ -22,6 +22,9 @@ namespace MSCO
|
|||||||
public int OwnerId = NoOwner;
|
public int OwnerId = NoOwner;
|
||||||
public bool OwnerActive; // Besitzer hält/fährt es gerade aktiv
|
public bool OwnerActive; // Besitzer hält/fährt es gerade aktiv
|
||||||
public bool WasKinematic; // Zustand bevor ein anderer Spieler es übernommen hat
|
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 StillTime;
|
||||||
public float NoMotionClaimUntil; // kurz nach einer Übergabe nicht wegen Nachfedern/Nachrollen übernehmen
|
public float NoMotionClaimUntil; // kurz nach einer Übergabe nicht wegen Nachfedern/Nachrollen übernehmen
|
||||||
|
|
||||||
@@ -63,6 +66,7 @@ namespace MSCO
|
|||||||
const float ReleaseAfterStill = 1f;
|
const float ReleaseAfterStill = 1f;
|
||||||
const float SnapDistance = 10f;
|
const float SnapDistance = 10f;
|
||||||
const float RemoteTimeout = 3f; // Besitzer meldet sich nicht mehr (z.B. Teil wurde dort montiert)
|
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 HandoverCooldown = 3f; // nach Freigabe/Übergabe keine Übernahme durch Bewegung
|
||||||
const float MaxHoldDistance = 1.5f; // gehaltene Items sind so nah an der Kamera
|
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)
|
const float VehicleClaimSpeed = 0.7f; // Fahrzeuge erst ab echter Bewegung übernehmen (nicht Federn)
|
||||||
@@ -102,6 +106,8 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
SyncedBody b;
|
SyncedBody b;
|
||||||
if (!byId.TryGetValue(id, out b) || b.Rb == null || b.OwnerId != SyncedBody.NoOwner) return false;
|
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.position = pos;
|
||||||
b.Rb.transform.rotation = rot;
|
b.Rb.transform.rotation = rot;
|
||||||
if (!b.Rb.isKinematic)
|
if (!b.Rb.isKinematic)
|
||||||
@@ -132,7 +138,8 @@ namespace MSCO
|
|||||||
if (!b.IsVehicle) continue;
|
if (!b.IsVehicle) continue;
|
||||||
string owner = b.OwnerId == SyncedBody.NoOwner ? "free" : IsLocal(b) ? "ME" : "player " + b.OwnerId;
|
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)" : "") +
|
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)");
|
if (lines.Count == 1) lines.Add("No vehicles registered! (SATSUMA, HAYOSIKO, ... not found as root objects with a Rigidbody)");
|
||||||
return lines;
|
return lines;
|
||||||
@@ -173,7 +180,7 @@ namespace MSCO
|
|||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
foreach (SyncedBody b in byId.Values)
|
foreach (SyncedBody b in byId.Values)
|
||||||
if (IsRemoteOwned(b) && b.Rb != null) b.Rb.isKinematic = b.WasKinematic;
|
if (IsRemoteOwned(b) && b.Rb != null) RestoreKinematic(b);
|
||||||
byId.Clear();
|
byId.Clear();
|
||||||
byRb.Clear();
|
byRb.Clear();
|
||||||
rejected.Clear();
|
rejected.Clear();
|
||||||
@@ -190,7 +197,7 @@ namespace MSCO
|
|||||||
foreach (SyncedBody b in byId.Values)
|
foreach (SyncedBody b in byId.Values)
|
||||||
{
|
{
|
||||||
if (b.OwnerId != playerId || b.Rb == null) continue;
|
if (b.OwnerId != playerId || b.Rb == null) continue;
|
||||||
b.Rb.isKinematic = b.WasKinematic;
|
RestoreKinematic(b);
|
||||||
b.OwnerId = SyncedBody.NoOwner;
|
b.OwnerId = SyncedBody.NoOwner;
|
||||||
b.NoMotionClaimUntil = Time.time + HandoverCooldown;
|
b.NoMotionClaimUntil = Time.time + HandoverCooldown;
|
||||||
}
|
}
|
||||||
@@ -242,6 +249,7 @@ namespace MSCO
|
|||||||
if (!b.Rb.gameObject.activeInHierarchy) continue;
|
if (!b.Rb.gameObject.activeInHierarchy) continue;
|
||||||
|
|
||||||
bool active = b == held || b == driven;
|
bool active = b == held || b == driven;
|
||||||
|
if (!IsRemoteOwned(b)) StopRunaway(b);
|
||||||
if (b.OwnerId == SyncedBody.NoOwner)
|
if (b.OwnerId == SyncedBody.NoOwner)
|
||||||
{
|
{
|
||||||
if (active) Claim(b, true);
|
if (active) Claim(b, true);
|
||||||
@@ -285,6 +293,7 @@ namespace MSCO
|
|||||||
foreach (SyncedBody b in byId.Values)
|
foreach (SyncedBody b in byId.Values)
|
||||||
{
|
{
|
||||||
if (!IsRemoteOwned(b) || b.Rb == null) continue;
|
if (!IsRemoteOwned(b) || b.Rb == null) 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 (Time.time - b.AvatarHoldTime < 0.2f) continue; // sitzt gerade in der Hand eines Mitspielers
|
||||||
if ((b.Rb.position - b.TargetPos).sqrMagnitude > SnapDistance * SnapDistance)
|
if ((b.Rb.position - b.TargetPos).sqrMagnitude > SnapDistance * SnapDistance)
|
||||||
{
|
{
|
||||||
@@ -306,7 +315,7 @@ namespace MSCO
|
|||||||
public void ApplyAvatarHold(int playerId, int bodyId, Vector3 pos, Quaternion rot)
|
public void ApplyAvatarHold(int playerId, int bodyId, Vector3 pos, Quaternion rot)
|
||||||
{
|
{
|
||||||
SyncedBody b;
|
SyncedBody b;
|
||||||
if (!byId.TryGetValue(bodyId, out b) || b.Rb == null) return;
|
if (!byId.TryGetValue(bodyId, out b) || b.Rb == null || b.IsVehicle) return; // ältere Mods melden evtl. ein Auto als "gehalten"
|
||||||
if (b.OwnerId != playerId)
|
if (b.OwnerId != playerId)
|
||||||
{
|
{
|
||||||
if (!Wins(playerId, true, b.OwnerId, b.OwnerActive)) return;
|
if (!Wins(playerId, true, b.OwnerId, b.OwnerActive)) return;
|
||||||
@@ -328,7 +337,7 @@ namespace MSCO
|
|||||||
|
|
||||||
void Claim(SyncedBody b, bool active)
|
void Claim(SyncedBody b, bool active)
|
||||||
{
|
{
|
||||||
if (IsRemoteOwned(b)) b.Rb.isKinematic = b.WasKinematic;
|
if (IsRemoteOwned(b)) RestoreKinematic(b);
|
||||||
b.OwnerId = LocalId;
|
b.OwnerId = LocalId;
|
||||||
b.OwnerActive = active;
|
b.OwnerActive = active;
|
||||||
b.StillTime = 0f;
|
b.StillTime = 0f;
|
||||||
@@ -357,8 +366,7 @@ namespace MSCO
|
|||||||
if (b.OwnerId == ownerId) return;
|
if (b.OwnerId == ownerId) return;
|
||||||
if (!IsRemoteOwned(b))
|
if (!IsRemoteOwned(b))
|
||||||
{
|
{
|
||||||
b.WasKinematic = b.Rb.isKinematic;
|
FreezeForRemote(b);
|
||||||
b.Rb.isKinematic = true;
|
|
||||||
b.TargetPos = b.Rb.position;
|
b.TargetPos = b.Rb.position;
|
||||||
b.TargetRot = b.Rb.rotation;
|
b.TargetRot = b.Rb.rotation;
|
||||||
}
|
}
|
||||||
@@ -367,11 +375,53 @@ namespace MSCO
|
|||||||
|
|
||||||
void SetFree(SyncedBody b)
|
void SetFree(SyncedBody b)
|
||||||
{
|
{
|
||||||
if (IsRemoteOwned(b)) b.Rb.isKinematic = b.WasKinematic;
|
if (IsRemoteOwned(b)) RestoreKinematic(b);
|
||||||
b.OwnerId = SyncedBody.NoOwner;
|
b.OwnerId = SyncedBody.NoOwner;
|
||||||
b.NoMotionClaimUntil = Time.time + HandoverCooldown;
|
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()
|
void SendOwnedStates()
|
||||||
{
|
{
|
||||||
sendList.Clear();
|
sendList.Clear();
|
||||||
@@ -426,7 +476,7 @@ namespace MSCO
|
|||||||
int id; Vector3 pos; Quaternion rot; Vector3 vel;
|
int id; Vector3 pos; Quaternion rot; Vector3 vel;
|
||||||
ReadState(r, out id, out pos, out rot, out vel);
|
ReadState(r, out id, out pos, out rot, out vel);
|
||||||
SyncedBody b;
|
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)
|
// Nur dem aktuellen Besitzer folgen (oder freie Objekte übernehmen)
|
||||||
if (b.OwnerId != from && b.OwnerId != SyncedBody.NoOwner) continue;
|
if (b.OwnerId != from && b.OwnerId != SyncedBody.NoOwner) continue;
|
||||||
SetRemote(b, from, b.OwnerId == from && b.OwnerActive);
|
SetRemote(b, from, b.OwnerId == from && b.OwnerActive);
|
||||||
@@ -442,9 +492,10 @@ namespace MSCO
|
|||||||
ReadState(r, out id, out pos, out rot, out vel);
|
ReadState(r, out id, out pos, out rot, out vel);
|
||||||
SyncedBody b;
|
SyncedBody b;
|
||||||
if (!byId.TryGetValue(id, out b) || b.Rb == null || b.OwnerId != from) return;
|
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.position = pos;
|
||||||
b.Rb.transform.rotation = rot;
|
b.Rb.transform.rotation = rot;
|
||||||
b.Rb.isKinematic = b.WasKinematic;
|
RestoreKinematic(b);
|
||||||
if (!b.Rb.isKinematic)
|
if (!b.Rb.isKinematic)
|
||||||
{
|
{
|
||||||
b.Rb.velocity = vel;
|
b.Rb.velocity = vel;
|
||||||
@@ -457,6 +508,76 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
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>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)
|
static void WriteState(BinaryWriter w, SyncedBody b)
|
||||||
{
|
{
|
||||||
Vector3 p = b.Rb.position;
|
Vector3 p = b.Rb.position;
|
||||||
@@ -534,7 +655,9 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
Rigidbody rb = t.GetComponent<Rigidbody>();
|
Rigidbody rb = t.GetComponent<Rigidbody>();
|
||||||
SyncedBody b;
|
SyncedBody b;
|
||||||
if (rb != null && byRb.TryGetValue(rb, out b)) return IsReallyInHand(b) ? b : null;
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-1
@@ -263,7 +263,7 @@ namespace MSCO
|
|||||||
|
|
||||||
void DrawAdmin(MultiplayerManager mp)
|
void DrawAdmin(MultiplayerManager mp)
|
||||||
{
|
{
|
||||||
float width = Mathf.Min(600, Screen.width - 40), height = Mathf.Min(460, Screen.height - 40);
|
float width = Mathf.Min(680, Screen.width - 40), height = Mathf.Min(560, Screen.height - 40);
|
||||||
var rect = new Rect((Screen.width - width) / 2f, (Screen.height - height) / 2f, width, height);
|
var rect = new Rect((Screen.width - width) / 2f, (Screen.height - height) / 2f, width, height);
|
||||||
GUI.Box(rect, GUIContent.none, panel);
|
GUI.Box(rect, GUIContent.none, panel);
|
||||||
|
|
||||||
@@ -284,6 +284,23 @@ namespace MSCO
|
|||||||
GUILayout.Label(mp.IsDedicated ? "Players on this server. Banned players cannot join again." : "Players in your game.", small);
|
GUILayout.Label(mp.IsDedicated ? "Players on this server. Banned players cannot join again." : "Players in your game.", small);
|
||||||
GUILayout.Space(10);
|
GUILayout.Space(10);
|
||||||
|
|
||||||
|
// Cheats für den eigenen Spieler (nur im Spiel)
|
||||||
|
if (mp.CanCheat)
|
||||||
|
{
|
||||||
|
GUILayout.Label("CHEATS", caption);
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
if (GUILayout.Button("+1.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(1000f);
|
||||||
|
GUILayout.Space(6);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
GUILayout.Label("REASON (optional)", caption);
|
GUILayout.Label("REASON (optional)", caption);
|
||||||
adminReason = GUILayout.TextField(adminReason, 100, field, GUILayout.Height(32));
|
adminReason = GUILayout.TextField(adminReason, 100, field, GUILayout.Height(32));
|
||||||
GUILayout.Space(12);
|
GUILayout.Space(12);
|
||||||
@@ -304,6 +321,14 @@ namespace MSCO
|
|||||||
GUILayout.FlexibleSpace();
|
GUILayout.FlexibleSpace();
|
||||||
|
|
||||||
bool self = p.Id == mp.LocalId;
|
bool self = p.Id == mp.LocalId;
|
||||||
|
if (mp.CanCheat)
|
||||||
|
{
|
||||||
|
GUI.enabled = !self;
|
||||||
|
if (GUILayout.Button("GOTO", secondary, GUILayout.Height(30), GUILayout.Width(70))) mp.GotoPlayer(p.Id);
|
||||||
|
GUILayout.Space(6);
|
||||||
|
if (GUILayout.Button("BRING", secondary, GUILayout.Height(30), GUILayout.Width(70))) mp.BringPlayer(p.Id);
|
||||||
|
GUILayout.Space(12);
|
||||||
|
}
|
||||||
GUI.enabled = !self && !p.IsAdmin;
|
GUI.enabled = !self && !p.IsAdmin;
|
||||||
if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(80)))
|
if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(80)))
|
||||||
mp.AdminAction(false, p.Id, adminReason);
|
mp.AdminAction(false, p.Id, adminReason);
|
||||||
|
|||||||
+12
-4
@@ -11,7 +11,7 @@ namespace MSCO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Passenger
|
public class Passenger
|
||||||
{
|
{
|
||||||
const float EnterRange = 4f; // so nah muss man am Fahrzeug stehen
|
const float EnterRange = 5f; // so nah muss man am Fahrzeug stehen (Abstand zur Fahrzeugmitte)
|
||||||
const float CenterSeatThreshold = 0.2f;
|
const float CenterSeatThreshold = 0.2f;
|
||||||
const float BehindCenterSeat = 0.7f;
|
const float BehindCenterSeat = 0.7f;
|
||||||
const float ExitSideOffset = 1.4f; // beim Aussteigen so weit neben den Sitz
|
const float ExitSideOffset = 1.4f; // beim Aussteigen so weit neben den Sitz
|
||||||
@@ -40,14 +40,22 @@ namespace MSCO
|
|||||||
SyncedBody best = null;
|
SyncedBody best = null;
|
||||||
RemotePlayer driver = null;
|
RemotePlayer driver = null;
|
||||||
float bestDist = EnterRange * EnterRange;
|
float bestDist = EnterRange * EnterRange;
|
||||||
foreach (SyncedBody b in objects.RemoteDrivenVehicles())
|
List<SyncedBody> driven = objects.RemoteDrivenVehicles();
|
||||||
|
float nearest = float.MaxValue;
|
||||||
|
foreach (SyncedBody b in driven)
|
||||||
{
|
{
|
||||||
|
float d = (b.Rb.worldCenterOfMass - player.position).sqrMagnitude;
|
||||||
|
nearest = Mathf.Min(nearest, d);
|
||||||
PlayerInfo owner;
|
PlayerInfo owner;
|
||||||
if (!players.TryGetValue(b.OwnerId, out owner) || owner.Avatar == null) continue;
|
if (!players.TryGetValue(b.OwnerId, out owner) || owner.Avatar == null) continue;
|
||||||
float d = (b.Rb.worldCenterOfMass - player.position).sqrMagnitude;
|
|
||||||
if (d < bestDist) { bestDist = d; best = b; driver = owner.Avatar; }
|
if (d < bestDist) { bestDist = d; best = b; driver = owner.Avatar; }
|
||||||
}
|
}
|
||||||
if (best == null) return "No vehicle driven by another player nearby";
|
if (best == null)
|
||||||
|
{
|
||||||
|
// Genau sagen, woran es liegt
|
||||||
|
if (driven.Count == 0) return "Passenger: nobody is driving a vehicle right now (the driver must sit in the driver's seat)";
|
||||||
|
return "Passenger: get closer to the car (" + Mathf.Sqrt(nearest).ToString("0") + " m away, max " + EnterRange.ToString("0") + " m)";
|
||||||
|
}
|
||||||
|
|
||||||
Transform vt = best.Rb.transform;
|
Transform vt = best.Rb.transform;
|
||||||
Vector3 driverHead = vt.InverseTransformPoint(driver.HeadPosition);
|
Vector3 driverHead = vt.InverseTransformPoint(driver.HeadPosition);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyTitle("MSCO")]
|
[assembly: AssemblyTitle("MSCO")]
|
||||||
[assembly: AssemblyDescription("MSCLoader Mod file")]
|
[assembly: AssemblyDescription("MSCLoader Mod file")]
|
||||||
[assembly: AssemblyConfiguration("")]
|
[assembly: AssemblyConfiguration("")]
|
||||||
[assembly: AssemblyCompany("")]
|
[assembly: AssemblyCompany("MBM")]
|
||||||
[assembly: AssemblyProduct("MSCO")]
|
[assembly: AssemblyProduct("MSCO")]
|
||||||
[assembly: AssemblyCopyright("Copyright © 2026 by MBM")]
|
[assembly: AssemblyCopyright("Copyright © 2026 by MBM")]
|
||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
|
|||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
// by using the '*' as shown below:
|
// by using the '*' as shown below:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("1.0.0.0")]
|
[assembly: AssemblyVersion("2.7.0.0")]
|
||||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
[assembly: AssemblyFileVersion("2.7.0.0")]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
**Verbinden**
|
**Verbinden**
|
||||||
- Launcher im Hauptmenü im Stil von FiveM: P2P-Seite und Server-Browser
|
- Launcher im Hauptmenü: P2P-Seite und Server-Browser
|
||||||
- **P2P über Steam:** HOST GAME, Freunde einladen, Beitritt über die Steam-Freundesliste (max. 3 Spieler)
|
- **P2P über Steam:** HOST GAME, Freunde einladen, Beitritt über die Steam-Freundesliste (max. 3 Spieler)
|
||||||
- **Server-Browser:** Suche, Filter (Steam / Non-Steam / volle ausblenden), Ping, Server-Profil
|
- **Server-Browser:** Suche, Filter (Steam / Non-Steam / volle ausblenden), Ping, Server-Profil
|
||||||
(Besitzer, Version, Laufzeit, Spieler), lokale Server auf diesem PC werden automatisch gefunden
|
(Besitzer, Version, Laufzeit, Spieler), lokale Server auf diesem PC werden automatisch gefunden
|
||||||
@@ -20,7 +20,8 @@ Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern
|
|||||||
und nach dem Spielen wiederhergestellt
|
und nach dem Spielen wiederhergestellt
|
||||||
|
|
||||||
**Synchronisiert**
|
**Synchronisiert**
|
||||||
- Spieler als Spielfigur (Modell aus den NPCs des Spiels) mit Blickrichtung, Ducken/Lehnen, Namensschild
|
- Spieler als eigene Figur im MSC-Stil (Karohemd, Jeans, Schnurrbart, Kappe – je Spieler etwas anders) mit
|
||||||
|
Blickrichtung, Laufen, Ducken/Lehnen, Sitzen, gehaltenem Item und Namensschild
|
||||||
- Items, lose Teile, Schrauben und Fahrzeuge (auch mit G geschoben) mit Besitzer-System – inkl. was jemand in der Hand hält
|
- Items, lose Teile, Schrauben und Fahrzeuge (auch mit G geschoben) mit Besitzer-System – inkl. was jemand in der Hand hält
|
||||||
- Fahrzeuge: Position, Lichter, Motorgeräusch; als **Beifahrer** mitfahren (Taste P)
|
- Fahrzeuge: Position, Lichter, Motorgeräusch; als **Beifahrer** mitfahren (Taste P)
|
||||||
- **Fahrzeugzustand:** Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schrauben-Festigkeit, Schäden, Verschleiß,
|
- **Fahrzeugzustand:** Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schrauben-Festigkeit, Schäden, Verschleiß,
|
||||||
@@ -31,7 +32,8 @@ Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern
|
|||||||
**Im Spiel**
|
**Im Spiel**
|
||||||
- **T** – Chat
|
- **T** – Chat
|
||||||
- **F8** – MSCO-Konsole mit allen Befehlen
|
- **F8** – MSCO-Konsole mit allen Befehlen
|
||||||
- **/admin** – Admin-Menü (Spieler kicken/bannen) für Server-Admins bzw. den P2P-Host
|
- **/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, Satsuma zu sich holen) – Cheats werden im Chat angezeigt
|
||||||
- **P** – als Beifahrer ein-/aussteigen
|
- **P** – als Beifahrer ein-/aussteigen
|
||||||
- Alle Tasten sind in den MSCLoader-Einstellungen der Mod änderbar
|
- Alle Tasten sind in den MSCLoader-Einstellungen der Mod änderbar
|
||||||
- Discord Rich Presence („In the Menus“, Server, Spielerzahl)
|
- Discord Rich Presence („In the Menus“, Server, Spielerzahl)
|
||||||
@@ -43,6 +45,21 @@ Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern
|
|||||||
Mods-Ordner kopieren (Standard: `Documents\MySummerCar\Mods`).
|
Mods-Ordner kopieren (Standard: `Documents\MySummerCar\Mods`).
|
||||||
3. Spiel **über Steam** starten – ohne Steam gehen nur Non-Steam-Server.
|
3. Spiel **über Steam** starten – ohne Steam gehen nur Non-Steam-Server.
|
||||||
|
|
||||||
|
### Einen eigenen Server betreiben
|
||||||
|
|
||||||
|
Siehe [MSCO-Dedicated-Server](https://git.mbm.wtf/MrMaho/MSCO-Dedicated-Server) – Lizenzschlüssel gibt es kostenlos
|
||||||
|
unter https://msco.mbm.wtf/keys.
|
||||||
|
|
||||||
|
## Probleme?
|
||||||
|
|
||||||
|
| Problem | Lösung |
|
||||||
|
|---|---|
|
||||||
|
| Server fehlt im Browser | REFRESH drücken; Filter (STEAM / NON-STEAM) prüfen; der Server muss von außen erreichbar sein |
|
||||||
|
| „This server requires Steam“ | Spiel über Steam starten |
|
||||||
|
| Warnung „Version mismatch“ | Mod und Server auf dieselbe Version bringen (Releases) |
|
||||||
|
| Eigener Spielstand weg | Wird nach dem Verlassen automatisch wiederhergestellt; Sicherung liegt in `AppData\LocalLow\Amistech\My Summer Car\MSCO_Backup` |
|
||||||
|
| Absturz / Hänger beim Beitreten | `output_log.txt` im Spielordner an den Server-Betreiber bzw. als Issue schicken |
|
||||||
|
|
||||||
## Befehle
|
## Befehle
|
||||||
|
|
||||||
In der F8-Konsole direkt, in der MSCLoader-Konsole mit `mp` davor (z.B. `mp connect ...`):
|
In der F8-Konsole direkt, in der MSCLoader-Konsole mit `mp` davor (z.B. `mp connect ...`):
|
||||||
@@ -53,10 +70,10 @@ In der F8-Konsole direkt, in der MSCLoader-Konsole mit `mp` davor (z.B. `mp conn
|
|||||||
| `disconnect` | Server / Spiel verlassen |
|
| `disconnect` | Server / Spiel verlassen |
|
||||||
| `host [port]` / `join <ip> [port]` | LAN-Testsitzung ohne Steam |
|
| `host [port]` / `join <ip> [port]` | LAN-Testsitzung ohne Steam |
|
||||||
| `save` | Spiel speichern (Host / Einzelspieler) |
|
| `save` | Spiel speichern (Host / Einzelspieler) |
|
||||||
|
| `ping [on\|off]` | Ping zum Server oben rechts anzeigen (grün < 80 ms, gelb < 150 ms, rot darüber) |
|
||||||
| `admin` bzw. `/admin` im Chat | Admin-Menü |
|
| `admin` bzw. `/admin` im Chat | Admin-Menü |
|
||||||
| `vehicles`, `rig` | Diagnose: Fahrzeuge / Spielermodelle |
|
| `vehicles`, `rig` | Diagnose: Fahrzeuge / Spielermodelle |
|
||||||
| `debug` | Sync-Debugausgaben an/aus |
|
| `debug` | Sync-Debugausgaben an/aus |
|
||||||
| `master [url]` | Adresse der Serverliste anzeigen/ändern |
|
|
||||||
| `clear` | Konsole leeren |
|
| `clear` | Konsole leeren |
|
||||||
|
|
||||||
## Selbst bauen
|
## Selbst bauen
|
||||||
@@ -83,7 +100,7 @@ msbuild MSCO.csproj -p:Configuration=Release
|
|||||||
| `ObjectSync.cs` | Items/Teile/Fahrzeuge mit Besitzer-System |
|
| `ObjectSync.cs` | Items/Teile/Fahrzeuge mit Besitzer-System |
|
||||||
| `VehicleStateSync.cs`, `VehicleFx.cs`, `Passenger.cs` | Fahrzeugzustand, Lichter/Sound, Beifahrer |
|
| `VehicleStateSync.cs`, `VehicleFx.cs`, `Passenger.cs` | Fahrzeugzustand, Lichter/Sound, Beifahrer |
|
||||||
| `FsmSync.cs`, `DoorSync.cs`, `WorldSync.cs`, `MoneySync.cs` | Montage-Events, Türen, Zeit/Wetter, Geld |
|
| `FsmSync.cs`, `DoorSync.cs`, `WorldSync.cs`, `MoneySync.cs` | Montage-Events, Türen, Zeit/Wetter, Geld |
|
||||||
| `RemotePlayer.cs`, `GameCharacter.cs` | Spielfiguren der Mitspieler |
|
| `RemotePlayer.cs` | Spielfiguren der Mitspieler |
|
||||||
| `SaveSync.cs`, `MenuHelper.cs` | Spielstand übertragen/sichern, Laden aus dem Menü |
|
| `SaveSync.cs`, `MenuHelper.cs` | Spielstand übertragen/sichern, Laden aus dem Menü |
|
||||||
| `SteamLobby.cs`, `JoinLinks.cs`, `DiscordPresence.cs` | Steam-Lobbys, `msco://`-Links, Discord |
|
| `SteamLobby.cs`, `JoinLinks.cs`, `DiscordPresence.cs` | Steam-Lobbys, `msco://`-Links, Discord |
|
||||||
| `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche |
|
| `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche |
|
||||||
|
|||||||
+152
-66
@@ -3,10 +3,11 @@ using UnityEngine;
|
|||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Figur eines Mitspielers mit echter Körperhaltung. Aus Kopfposition (Kamera) und Bodenhöhe wird
|
/// Figur eines Mitspielers im Stil der My-Summer-Car-Leute (selbst gebaut, keine NPC-Kopie):
|
||||||
/// jedes Frame die Pose berechnet: Ducken beugt die Knie, nach vorne Lehnen neigt den Oberkörper,
|
/// Karohemd/Arbeitsjacke, Jeans, Arbeitsschuhe, Schnurrbart, Kappe oder Haare, leichter Bauch.
|
||||||
/// Laufen bewegt Beine und Arme, beim Halten greift der rechte Arm (mit Ellbogen) zum Item, im Auto
|
/// Aus Kopfposition (Kamera) und Bodenhöhe wird jedes Frame die Pose berechnet: Ducken beugt die Knie,
|
||||||
/// sitzt die Figur. Die Körperteile sind abgerundete Kapseln zwischen den Gelenken.
|
/// nach vorne Lehnen neigt den Oberkörper, Laufen bewegt Beine und Arme, beim Halten greift der rechte Arm
|
||||||
|
/// zum Item, im Auto sitzt die Figur.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RemotePlayer : MonoBehaviour
|
public class RemotePlayer : MonoBehaviour
|
||||||
{
|
{
|
||||||
@@ -15,22 +16,30 @@ namespace MSCO
|
|||||||
const float MaxHeadPitch = 55f;
|
const float MaxHeadPitch = 55f;
|
||||||
|
|
||||||
// Körpermaße (Meter)
|
// Körpermaße (Meter)
|
||||||
const float ThighLength = 0.45f, ShinLength = 0.45f;
|
const float ThighLength = 0.41f, ShinLength = 0.41f; // im Stehen fast gestreckt
|
||||||
const float UpperArmLength = 0.3f, ForearmLength = 0.29f;
|
const float UpperArmLength = 0.3f, ForearmLength = 0.29f;
|
||||||
const float TorsoLength = 0.55f; // Hüfte bis Hals
|
const float TorsoLength = 0.55f; // Hüfte bis Hals
|
||||||
const float HipWidth = 0.1f, ShoulderWidth = 0.21f;
|
const float HipWidth = 0.1f, ShoulderWidth = 0.21f;
|
||||||
const float AnkleHeight = 0.07f;
|
const float AnkleHeight = 0.07f;
|
||||||
|
|
||||||
static readonly Color Skin = new Color(0.93f, 0.76f, 0.62f);
|
static readonly Color Skin = new Color(0.86f, 0.66f, 0.52f);
|
||||||
static readonly Color Jeans = new Color(0.18f, 0.25f, 0.42f);
|
static readonly Color Jeans = new Color(0.2f, 0.26f, 0.38f);
|
||||||
static readonly Color Boots = new Color(0.24f, 0.16f, 0.1f);
|
static readonly Color Boots = new Color(0.22f, 0.15f, 0.09f);
|
||||||
|
static readonly Color Sole = new Color(0.07f, 0.06f, 0.05f);
|
||||||
static readonly Color Dark = new Color(0.08f, 0.08f, 0.08f);
|
static readonly Color Dark = new Color(0.08f, 0.08f, 0.08f);
|
||||||
|
static readonly Color Metal = new Color(0.62f, 0.62f, 0.6f);
|
||||||
|
static readonly Color[] HairColors =
|
||||||
|
{
|
||||||
|
new Color(0.23f, 0.15f, 0.09f), new Color(0.45f, 0.33f, 0.2f), new Color(0.12f, 0.1f, 0.08f), new Color(0.55f, 0.5f, 0.45f)
|
||||||
|
};
|
||||||
|
|
||||||
Color jacket;
|
Color jacket;
|
||||||
|
int variant;
|
||||||
Transform headPivot, view;
|
Transform headPivot, view;
|
||||||
Transform pelvis, torso, belt, neck;
|
Transform pelvis, torso, belt, neck, belly, collar, placket, buckle;
|
||||||
readonly Transform[] thigh = new Transform[2], shin = new Transform[2], boot = new Transform[2];
|
readonly Transform[] thigh = new Transform[2], shin = new Transform[2], boot = new Transform[2];
|
||||||
readonly Transform[] upperArm = new Transform[2], forearm = new Transform[2], hand = new Transform[2];
|
readonly Transform[] upperArm = new Transform[2], forearm = new Transform[2], hand = new Transform[2];
|
||||||
|
readonly Transform[] sole = new Transform[2], cuff = new Transform[2];
|
||||||
|
|
||||||
Vector3 targetFeet;
|
Vector3 targetFeet;
|
||||||
Vector3 targetHead;
|
Vector3 targetHead;
|
||||||
@@ -42,13 +51,10 @@ namespace MSCO
|
|||||||
Vector3 lastPosition;
|
Vector3 lastPosition;
|
||||||
float walkPhase, walkAmount, speed;
|
float walkPhase, walkAmount, speed;
|
||||||
|
|
||||||
// Echtes Spielmodell (NPC aus der Szene); null = eigene Figur als Rückfall
|
|
||||||
GameCharacter model;
|
|
||||||
|
|
||||||
public int HeldId { get { return last.HeldId; } }
|
public int HeldId { get { return last.HeldId; } }
|
||||||
public PlayerState LastState { get { return last; } }
|
public PlayerState LastState { get { return last; } }
|
||||||
public Vector3 HeadPosition { get { return model != null ? model.HeadPosition : headPivot.position; } }
|
public Vector3 HeadPosition { get { return headPivot.position; } }
|
||||||
public string ModelDescription { get { return model != null ? "game model: " + model.Description : "fallback figure (no NPC template found)"; } }
|
public string ModelDescription { get { return "MSCO figure, variant " + variant; } }
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Aufbau
|
// ---------------------------------------------------------------- Aufbau
|
||||||
|
|
||||||
@@ -57,43 +63,48 @@ namespace MSCO
|
|||||||
var root = new GameObject(name);
|
var root = new GameObject(name);
|
||||||
var rp = root.AddComponent<RemotePlayer>();
|
var rp = root.AddComponent<RemotePlayer>();
|
||||||
rp.jacket = jacketColor;
|
rp.jacket = jacketColor;
|
||||||
|
rp.variant = Mathf.Abs(variant);
|
||||||
rp.Build();
|
rp.Build();
|
||||||
rp.SetTarget(state);
|
rp.SetTarget(state);
|
||||||
root.transform.position = rp.targetFeet;
|
root.transform.position = rp.targetFeet;
|
||||||
root.transform.rotation = Quaternion.Euler(0f, rp.targetYaw, 0f);
|
root.transform.rotation = Quaternion.Euler(0f, rp.targetYaw, 0f);
|
||||||
|
|
||||||
// Wenn möglich einen echten NPC aus dem Spiel als Figur nehmen, sonst die eigene Figur
|
|
||||||
try { rp.model = GameCharacter.TryCreate(root.transform, variant); }
|
|
||||||
catch (System.Exception e) { MSCLoader.ModConsole.Warning("[MSCO] Game model failed: " + e.Message); }
|
|
||||||
if (rp.model != null)
|
|
||||||
foreach (Renderer r in root.GetComponentsInChildren<Renderer>(true))
|
|
||||||
if (!r.transform.IsChildOf(rp.model.Root)) r.enabled = false;
|
|
||||||
rp.headWorld = rp.targetHead;
|
rp.headWorld = rp.targetHead;
|
||||||
rp.pitch = rp.targetPitch;
|
rp.pitch = rp.targetPitch;
|
||||||
rp.view.rotation = rp.targetViewRot;
|
rp.view.rotation = rp.targetViewRot;
|
||||||
rp.lastPosition = root.transform.position;
|
rp.lastPosition = root.transform.position;
|
||||||
if (rp.model == null) rp.Pose(0f);
|
rp.Pose(0f);
|
||||||
return rp;
|
return rp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Build()
|
void Build()
|
||||||
{
|
{
|
||||||
Transform r = transform;
|
Transform r = transform;
|
||||||
Color jacketDark = jacket * 0.75f;
|
Color jacketDark = jacket * 0.7f;
|
||||||
jacketDark.a = 1f;
|
jacketDark.a = 1f;
|
||||||
|
Color hairColor = HairColors[variant % HairColors.Length];
|
||||||
|
bool wearsCap = variant % 3 != 2;
|
||||||
|
bool sunglasses = variant % 4 == 1;
|
||||||
|
Texture2D plaid = Plaid(jacket);
|
||||||
|
|
||||||
|
// Körper: Karohemd/Jacke, Jeans, Gürtel, leichter Bauch (wie die Leute in MSC)
|
||||||
pelvis = Capsule("Pelvis", r, Jeans);
|
pelvis = Capsule("Pelvis", r, Jeans);
|
||||||
torso = Capsule("Torso", r, jacket);
|
torso = Textured(Capsule("Chest", r, Color.white), plaid, new Vector2(2f, 2f));
|
||||||
belt = Capsule("Belt", r, Dark);
|
belly = Textured(Capsule("Belly", r, Color.white), plaid, new Vector2(2f, 1f));
|
||||||
|
belt = Box("Belt", r, new Color(0.2f, 0.13f, 0.08f));
|
||||||
|
buckle = Box("Buckle", r, Metal);
|
||||||
|
placket = Box("Buttons", r, jacketDark);
|
||||||
|
collar = Capsule("Collar", r, jacketDark);
|
||||||
neck = Capsule("Neck", r, Skin);
|
neck = Capsule("Neck", r, Skin);
|
||||||
|
|
||||||
for (int i = 0; i < 2; i++)
|
for (int i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
thigh[i] = Capsule("Thigh", r, Jeans);
|
thigh[i] = Capsule("Thigh", r, Jeans);
|
||||||
shin[i] = Capsule("Shin", r, Jeans);
|
shin[i] = Capsule("Shin", r, Jeans * 0.92f);
|
||||||
boot[i] = Box("Boot", r, Boots);
|
boot[i] = Box("Boot", r, Boots);
|
||||||
upperArm[i] = Capsule("UpperArm", r, jacket);
|
sole[i] = Box("Sole", r, Sole);
|
||||||
forearm[i] = Capsule("Forearm", r, jacketDark);
|
upperArm[i] = Textured(Capsule("UpperArm", r, Color.white), plaid, new Vector2(1f, 2f));
|
||||||
|
forearm[i] = Textured(Capsule("Forearm", r, Color.white), plaid, new Vector2(1f, 2f));
|
||||||
|
cuff[i] = Capsule("Cuff", r, jacketDark);
|
||||||
hand[i] = Sphere("Hand", r, Skin, 0.1f);
|
hand[i] = Sphere("Hand", r, Skin, 0.1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,31 +112,96 @@ namespace MSCO
|
|||||||
headPivot = new GameObject("Head").transform;
|
headPivot = new GameObject("Head").transform;
|
||||||
headPivot.SetParent(r, false);
|
headPivot.SetParent(r, false);
|
||||||
Transform skull = Sphere("Skull", headPivot, Skin, 0.24f);
|
Transform skull = Sphere("Skull", headPivot, Skin, 0.24f);
|
||||||
skull.localScale = new Vector3(0.22f, 0.26f, 0.24f);
|
skull.localScale = new Vector3(0.225f, 0.27f, 0.25f);
|
||||||
Transform hair = Sphere("Hair", headPivot, new Color(0.25f, 0.17f, 0.1f), 0.24f);
|
Transform jaw = Sphere("Jaw", headPivot, Skin, 0.2f);
|
||||||
hair.localPosition = new Vector3(0f, 0.02f, -0.025f);
|
jaw.localPosition = new Vector3(0f, -0.07f, 0.022f);
|
||||||
hair.localScale = new Vector3(0.23f, 0.25f, 0.23f);
|
jaw.localScale = new Vector3(0.19f, 0.14f, 0.2f);
|
||||||
Transform cap = Sphere("Cap", headPivot, jacketDark, 0.25f);
|
Transform hair = Sphere("Hair", headPivot, hairColor, 0.24f);
|
||||||
cap.localPosition = new Vector3(0f, 0.07f, -0.005f);
|
hair.localPosition = new Vector3(0f, 0.028f, -0.032f);
|
||||||
cap.localScale = new Vector3(0.245f, 0.16f, 0.255f);
|
hair.localScale = new Vector3(0.235f, 0.255f, 0.235f);
|
||||||
Transform brim = Box("CapBrim", headPivot, jacketDark);
|
|
||||||
brim.localPosition = new Vector3(0f, 0.075f, 0.13f);
|
|
||||||
brim.localScale = new Vector3(0.2f, 0.02f, 0.11f);
|
|
||||||
brim.localRotation = Quaternion.Euler(-8f, 0f, 0f);
|
|
||||||
for (int i = 0; i < 2; i++)
|
for (int i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
Transform eye = Sphere("Eye", headPivot, Dark, 0.035f);
|
float side = i == 0 ? -1f : 1f;
|
||||||
eye.localPosition = new Vector3(i == 0 ? -0.045f : 0.045f, 0.015f, 0.105f);
|
Transform ear = Sphere("Ear", headPivot, Skin * 0.96f, 0.05f);
|
||||||
|
ear.localPosition = new Vector3(side * 0.112f, -0.005f, 0f);
|
||||||
|
ear.localScale = new Vector3(0.035f, 0.065f, 0.05f);
|
||||||
|
Transform eye = Sphere("Eye", headPivot, Dark, 0.026f);
|
||||||
|
eye.localPosition = new Vector3(side * 0.045f, 0.018f, 0.112f);
|
||||||
|
Transform brow = Box("Brow", headPivot, hairColor);
|
||||||
|
brow.localPosition = new Vector3(side * 0.046f, 0.046f, 0.117f);
|
||||||
|
brow.localScale = new Vector3(0.045f, 0.012f, 0.012f);
|
||||||
|
brow.localRotation = Quaternion.Euler(0f, 0f, side * -8f);
|
||||||
|
}
|
||||||
|
Transform nose = Box("Nose", headPivot, Skin * 0.93f);
|
||||||
|
nose.localPosition = new Vector3(0f, -0.012f, 0.128f);
|
||||||
|
nose.localScale = new Vector3(0.035f, 0.055f, 0.045f);
|
||||||
|
// Der typische Schnurrbart
|
||||||
|
Transform mustache = Box("Mustache", headPivot, hairColor);
|
||||||
|
mustache.localPosition = new Vector3(0f, -0.054f, 0.12f);
|
||||||
|
mustache.localScale = new Vector3(0.085f, 0.024f, 0.03f);
|
||||||
|
Transform mouth = Box("Mouth", headPivot, new Color(0.45f, 0.25f, 0.22f));
|
||||||
|
mouth.localPosition = new Vector3(0f, -0.082f, 0.114f);
|
||||||
|
mouth.localScale = new Vector3(0.05f, 0.01f, 0.01f);
|
||||||
|
|
||||||
|
if (sunglasses)
|
||||||
|
{
|
||||||
|
Transform glasses = Box("Sunglasses", headPivot, Dark);
|
||||||
|
glasses.localPosition = new Vector3(0f, 0.018f, 0.121f);
|
||||||
|
glasses.localScale = new Vector3(0.15f, 0.035f, 0.012f);
|
||||||
|
}
|
||||||
|
if (wearsCap)
|
||||||
|
{
|
||||||
|
// Kappe ("lippis") in der Jackenfarbe
|
||||||
|
Transform cap = Sphere("Cap", headPivot, jacketDark, 0.25f);
|
||||||
|
cap.localPosition = new Vector3(0f, 0.08f, -0.005f);
|
||||||
|
cap.localScale = new Vector3(0.25f, 0.16f, 0.26f);
|
||||||
|
Transform brim = Box("CapBrim", headPivot, jacketDark);
|
||||||
|
brim.localPosition = new Vector3(0f, 0.07f, 0.15f);
|
||||||
|
brim.localScale = new Vector3(0.2f, 0.014f, 0.1f);
|
||||||
|
brim.localRotation = Quaternion.Euler(8f, 0f, 0f); // Schirm leicht nach unten
|
||||||
|
Transform patch = Box("CapPatch", headPivot, new Color(0.9f, 0.88f, 0.8f));
|
||||||
|
patch.localPosition = new Vector3(0f, 0.115f, 0.118f);
|
||||||
|
patch.localScale = new Vector3(0.06f, 0.035f, 0.01f);
|
||||||
|
patch.localRotation = Quaternion.Euler(-25f, 0f, 0f);
|
||||||
}
|
}
|
||||||
Transform nose = Box("Nose", headPivot, Skin * 0.95f);
|
|
||||||
nose.localPosition = new Vector3(0f, -0.02f, 0.12f);
|
|
||||||
nose.localScale = new Vector3(0.035f, 0.05f, 0.04f);
|
|
||||||
|
|
||||||
// Unsichtbarer "Blick" mit der exakten Kameradrehung - daran hängt das gehaltene Item
|
// Unsichtbarer "Blick" mit der exakten Kameradrehung - daran hängt das gehaltene Item
|
||||||
view = new GameObject("View").transform;
|
view = new GameObject("View").transform;
|
||||||
view.SetParent(r, false);
|
view.SetParent(r, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Kleines Karomuster (Flanellhemd) in der Grundfarbe; wird auf die Kapseln gekachelt.</summary>
|
||||||
|
static Texture2D Plaid(Color baseColor)
|
||||||
|
{
|
||||||
|
const int size = 16;
|
||||||
|
var tex = new Texture2D(size, size, TextureFormat.RGB24, false);
|
||||||
|
Color light = Color.Lerp(baseColor, Color.white, 0.18f);
|
||||||
|
Color dark = baseColor * 0.62f;
|
||||||
|
for (int y = 0; y < size; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < size; x++)
|
||||||
|
{
|
||||||
|
bool bandX = (x / 4) % 2 == 0, bandY = (y / 4) % 2 == 0;
|
||||||
|
Color c = bandX && bandY ? dark : bandX || bandY ? baseColor : light;
|
||||||
|
if (x % 8 == 1 || y % 8 == 1) c = Color.Lerp(c, Color.black, 0.45f); // dünne dunkle Linien
|
||||||
|
c.a = 1f;
|
||||||
|
tex.SetPixel(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tex.filterMode = FilterMode.Point;
|
||||||
|
tex.wrapMode = TextureWrapMode.Repeat;
|
||||||
|
tex.Apply();
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Transform Textured(Transform part, Texture2D tex, Vector2 tiling)
|
||||||
|
{
|
||||||
|
Material m = part.GetComponent<Renderer>().material;
|
||||||
|
m.mainTexture = tex;
|
||||||
|
m.mainTextureScale = tiling;
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
static Transform Capsule(string name, Transform parent, Color color) { return Primitive(PrimitiveType.Capsule, name, parent, color); }
|
static Transform Capsule(string name, Transform parent, Color color) { return Primitive(PrimitiveType.Capsule, name, parent, color); }
|
||||||
|
|
||||||
static Transform Box(string name, Transform parent, Color color) { return Primitive(PrimitiveType.Cube, name, parent, color); }
|
static Transform Box(string name, Transform parent, Color color) { return Primitive(PrimitiveType.Cube, name, parent, color); }
|
||||||
@@ -144,6 +220,9 @@ namespace MSCO
|
|||||||
Destroy(go.GetComponent<Collider>()); // stört sonst Physik/Trigger
|
Destroy(go.GetComponent<Collider>()); // stört sonst Physik/Trigger
|
||||||
var renderer = go.GetComponent<Renderer>();
|
var renderer = go.GetComponent<Renderer>();
|
||||||
renderer.material.color = color;
|
renderer.material.color = color;
|
||||||
|
// Matt wie Stoff/Haut statt glänzendem Plastik (Standard-Shader)
|
||||||
|
if (renderer.material.HasProperty("_Glossiness")) renderer.material.SetFloat("_Glossiness", 0.08f);
|
||||||
|
if (renderer.material.HasProperty("_Metallic")) renderer.material.SetFloat("_Metallic", 0f);
|
||||||
renderer.receiveShadows = true;
|
renderer.receiveShadows = true;
|
||||||
go.transform.SetParent(parent, false);
|
go.transform.SetParent(parent, false);
|
||||||
return go.transform;
|
return go.transform;
|
||||||
@@ -186,18 +265,7 @@ namespace MSCO
|
|||||||
view.rotation = Quaternion.Slerp(view.rotation, targetViewRot, t);
|
view.rotation = Quaternion.Slerp(view.rotation, targetViewRot, t);
|
||||||
|
|
||||||
UpdateWalk(dt);
|
UpdateWalk(dt);
|
||||||
if (model != null) model.Animate(speed); // Spiel-Animation (Idle/Walk) läuft vor LateUpdate
|
Pose(dt);
|
||||||
else Pose(dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Spielmodell: nach der Animation Ducken/Lehnen/Kopf/Arme anpassen.</summary>
|
|
||||||
void LateUpdate()
|
|
||||||
{
|
|
||||||
if (model == null) return;
|
|
||||||
Vector3 heldPos;
|
|
||||||
Quaternion heldRot;
|
|
||||||
bool holding = TryGetHeldPose(out heldPos, out heldRot);
|
|
||||||
model.Pose(transform, headWorld, pitch, last.Seated, holding, heldPos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateWalk(float dt)
|
void UpdateWalk(float dt)
|
||||||
@@ -240,10 +308,22 @@ namespace MSCO
|
|||||||
Vector3 forward = Vector3.Cross(right, up).normalized;
|
Vector3 forward = Vector3.Cross(right, up).normalized;
|
||||||
|
|
||||||
// Rumpf
|
// Rumpf
|
||||||
Segment(torso, hip + up * 0.08f, neckBase, 0.38f, 0.24f);
|
Quaternion torsoRot = Quaternion.LookRotation(forward, up);
|
||||||
Segment(belt, hip + up * 0.06f, hip + up * 0.1f, 0.37f, 0.23f);
|
// Brust (breit oben) und Bauch (etwas tiefer = leichter Bauch) - fließend statt Klotz mit Kugel
|
||||||
Segment(pelvis, hip - right * HipWidth, hip + right * HipWidth, 0.2f, 0.22f);
|
Segment(torso, hip + up * 0.26f, neckBase - up * 0.04f, 0.37f, 0.22f);
|
||||||
Segment(neck, neckBase, neckTop, 0.09f, 0.09f);
|
Segment(belly, hip + up * 0.07f + forward * 0.015f, hip + up * 0.3f + forward * 0.015f, 0.33f, 0.25f);
|
||||||
|
belt.localPosition = hip + up * 0.06f;
|
||||||
|
belt.localRotation = torsoRot;
|
||||||
|
belt.localScale = new Vector3(0.345f, 0.045f, 0.275f);
|
||||||
|
buckle.localPosition = hip + up * 0.06f + forward * 0.14f;
|
||||||
|
buckle.localRotation = torsoRot;
|
||||||
|
buckle.localScale = new Vector3(0.055f, 0.04f, 0.012f);
|
||||||
|
placket.localPosition = Vector3.Lerp(hip + up * 0.12f, neckBase, 0.6f) + forward * 0.124f;
|
||||||
|
placket.localRotation = torsoRot;
|
||||||
|
placket.localScale = new Vector3(0.022f, 0.3f, 0.01f);
|
||||||
|
Segment(pelvis, hip - right * (HipWidth * 0.8f) - up * 0.02f, hip + right * (HipWidth * 0.8f) - up * 0.02f, 0.2f, 0.22f);
|
||||||
|
Segment(collar, neckBase - up * 0.02f, neckBase + up * 0.035f, 0.17f, 0.15f);
|
||||||
|
Segment(neck, neckBase, neckTop, 0.1f, 0.1f);
|
||||||
|
|
||||||
// Kopf
|
// Kopf
|
||||||
headPivot.localPosition = head;
|
headPivot.localPosition = head;
|
||||||
@@ -263,11 +343,14 @@ namespace MSCO
|
|||||||
else
|
else
|
||||||
ankle = new Vector3(HipWidth * side, AnkleHeight + Mathf.Max(0f, Mathf.Cos(walkPhase + i * Mathf.PI)) * 0.07f * walkAmount, phase * 0.3f);
|
ankle = new Vector3(HipWidth * side, AnkleHeight + Mathf.Max(0f, Mathf.Cos(walkPhase + i * Mathf.PI)) * 0.07f * walkAmount, phase * 0.3f);
|
||||||
Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward);
|
Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward);
|
||||||
Segment(thigh[i], hipJoint, knee, 0.17f, 0.17f);
|
Segment(thigh[i], hipJoint, knee, 0.15f, 0.16f);
|
||||||
Segment(shin[i], knee, ankle, 0.14f, 0.14f);
|
Segment(shin[i], knee, ankle, 0.12f, 0.13f);
|
||||||
boot[i].localPosition = ankle + new Vector3(0f, -0.02f, 0.05f);
|
boot[i].localPosition = ankle + new Vector3(0f, -0.015f, 0.05f);
|
||||||
boot[i].localRotation = Quaternion.identity;
|
boot[i].localRotation = Quaternion.identity;
|
||||||
boot[i].localScale = new Vector3(0.12f, 0.1f, 0.27f);
|
boot[i].localScale = new Vector3(0.12f, 0.1f, 0.27f);
|
||||||
|
sole[i].localPosition = ankle + new Vector3(0f, -0.065f, 0.05f);
|
||||||
|
sole[i].localRotation = Quaternion.identity;
|
||||||
|
sole[i].localScale = new Vector3(0.13f, 0.025f, 0.29f);
|
||||||
|
|
||||||
// Arme
|
// Arme
|
||||||
Vector3 shoulder = neckBase - up * 0.06f + right * (ShoulderWidth * side);
|
Vector3 shoulder = neckBase - up * 0.06f + right * (ShoulderWidth * side);
|
||||||
@@ -279,7 +362,7 @@ namespace MSCO
|
|||||||
else if (last.Seated)
|
else if (last.Seated)
|
||||||
handTarget = shoulder + new Vector3(-side * 0.05f, -0.3f, 0.38f); // Hände nach vorne (Lenkrad)
|
handTarget = shoulder + new Vector3(-side * 0.05f, -0.3f, 0.38f); // Hände nach vorne (Lenkrad)
|
||||||
else
|
else
|
||||||
handTarget = shoulder + new Vector3(side * 0.03f, -(UpperArmLength + ForearmLength) + 0.04f, -phase * 0.22f);
|
handTarget = shoulder + new Vector3(side * 0.06f, -(UpperArmLength + ForearmLength) + 0.05f, 0.02f - phase * 0.22f);
|
||||||
|
|
||||||
// Nicht weiter als die Armlänge
|
// Nicht weiter als die Armlänge
|
||||||
Vector3 reach = handTarget - shoulder;
|
Vector3 reach = handTarget - shoulder;
|
||||||
@@ -287,9 +370,12 @@ namespace MSCO
|
|||||||
if (reach.magnitude > maxReach) handTarget = shoulder + reach.normalized * maxReach;
|
if (reach.magnitude > maxReach) handTarget = shoulder + reach.normalized * maxReach;
|
||||||
|
|
||||||
Vector3 elbow = Joint(shoulder, handTarget, UpperArmLength, ForearmLength, new Vector3(side * 0.4f, -0.2f, -1f));
|
Vector3 elbow = Joint(shoulder, handTarget, UpperArmLength, ForearmLength, new Vector3(side * 0.4f, -0.2f, -1f));
|
||||||
Segment(upperArm[i], shoulder, elbow, 0.12f, 0.12f);
|
Segment(upperArm[i], shoulder, elbow, 0.11f, 0.11f);
|
||||||
Segment(forearm[i], elbow, handTarget, 0.11f, 0.11f);
|
Segment(forearm[i], elbow, handTarget, 0.095f, 0.095f);
|
||||||
|
Segment(cuff[i], Vector3.Lerp(elbow, handTarget, 0.82f), Vector3.Lerp(elbow, handTarget, 0.9f), 0.105f, 0.105f);
|
||||||
hand[i].localPosition = handTarget;
|
hand[i].localPosition = handTarget;
|
||||||
|
hand[i].localRotation = Quaternion.LookRotation(Vector3.forward, (handTarget - elbow).sqrMagnitude > 0f ? elbow - handTarget : Vector3.up);
|
||||||
|
hand[i].localScale = new Vector3(0.075f, 0.115f, 0.1f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-10
@@ -37,11 +37,11 @@ namespace MSCO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ServerBrowser
|
public class ServerBrowser
|
||||||
{
|
{
|
||||||
public const string DefaultMasterUrl = "https://msco.mbm.wtf";
|
/// <summary>Offizieller Master (Serverliste) - fest eingebaut.</summary>
|
||||||
const string PrefMasterUrl = "MSCO_MasterUrl";
|
public const string MasterUrl = "https://msco.mbm.wtf";
|
||||||
const int QueryTimeoutMs = 3000;
|
const int QueryTimeoutMs = 3000;
|
||||||
|
|
||||||
// Wie bei FiveM: ein Server auf diesem PC erscheint automatisch oben in der Liste
|
// Ein Server auf diesem PC erscheint automatisch oben in der Liste
|
||||||
static readonly int[] LocalPorts = { 7777, 7778, 7779, 7780 };
|
static readonly int[] LocalPorts = { 7777, 7778, 7779, 7780 };
|
||||||
public const float AutoRefreshSeconds = 15f;
|
public const float AutoRefreshSeconds = 15f;
|
||||||
|
|
||||||
@@ -54,13 +54,6 @@ namespace MSCO
|
|||||||
/// <summary>Zeitpunkt der letzten Aktualisierung (Time.realtimeSinceStartup), für das Auto-Refresh.</summary>
|
/// <summary>Zeitpunkt der letzten Aktualisierung (Time.realtimeSinceStartup), für das Auto-Refresh.</summary>
|
||||||
public float LastRefresh { get; private set; }
|
public float LastRefresh { get; private set; }
|
||||||
|
|
||||||
/// <summary>Adresse des Master-Servers (änderbar mit "mp master <url>", z.B. zum lokalen Testen).</summary>
|
|
||||||
public static string MasterUrl
|
|
||||||
{
|
|
||||||
get { return PlayerPrefs.GetString(PrefMasterUrl, DefaultMasterUrl).TrimEnd('/'); }
|
|
||||||
set { PlayerPrefs.SetString(PrefMasterUrl, string.IsNullOrEmpty(value) ? DefaultMasterUrl : value.TrimEnd('/')); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Lokale Server zuerst, dann die öffentliche Liste.</summary>
|
/// <summary>Lokale Server zuerst, dann die öffentliche Liste.</summary>
|
||||||
public List<ServerListEntry> Servers
|
public List<ServerListEntry> Servers
|
||||||
{
|
{
|
||||||
|
|||||||
+17
-8
@@ -21,13 +21,14 @@ namespace MSCO
|
|||||||
static readonly string[] Keywords =
|
static readonly string[] Keywords =
|
||||||
{
|
{
|
||||||
"fuel", "oil", "coolant", "water", "fluid", "brake", "clutchfluid", "battery", "charge",
|
"fuel", "oil", "coolant", "water", "fluid", "brake", "clutchfluid", "battery", "charge",
|
||||||
"bolt", "tight", "damage", "wear", "condition", "odo", "km", "dirt", "paint", "color", "colour",
|
"damage", "wear", "condition", "odo", "km", "dirt", "paint", "color", "colour",
|
||||||
"tune", "tuning", "ratio", "broken", "fixed", "flat", "pressure", "level", "mixture", "timing",
|
"tune", "tuning", "ratio", "broken", "fixed", "flat", "pressure", "level", "mixture", "timing",
|
||||||
"idle", "air", "jet", "spark", "gap", "rust", "health", "burn", "cool", "temp"
|
"idle", "air", "jet", "spark", "gap", "rust", "health", "burn", "cool", "temp"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Montiert-Flags nicht direkt setzen: das echte Anbauen läuft über FsmSync (Events)
|
// Montiert-Flags nicht direkt setzen: das echte Anbauen läuft über FsmSync (Events)
|
||||||
static readonly string[] Excluded = { "install", "rpm", "speed", "steer", "throttle", "gear", "raycast", "distance" };
|
// Schrauben/Festigkeit laufen über FsmSync (Events) - ein falscher Wert hier würde Teile lockern und abfallen lassen
|
||||||
|
static readonly string[] Excluded = { "install", "bolt", "tight", "detach", "attach", "rpm", "speed", "steer", "throttle", "gear", "raycast", "distance" };
|
||||||
|
|
||||||
const float ScanInterval = 10f;
|
const float ScanInterval = 10f;
|
||||||
const float CheckInterval = 0.5f;
|
const float CheckInterval = 0.5f;
|
||||||
@@ -73,11 +74,16 @@ namespace MSCO
|
|||||||
outgoing.Clear();
|
outgoing.Clear();
|
||||||
foreach (Var v in vars.Values)
|
foreach (Var v in vars.Values)
|
||||||
{
|
{
|
||||||
if (!Changed(v)) continue;
|
// Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden
|
||||||
Remember(v);
|
|
||||||
// Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich
|
|
||||||
int owner = objects.VehicleOwner(v.Root);
|
int owner = objects.VehicleOwner(v.Root);
|
||||||
if (owner == SyncedBody.NoOwner || owner == localId) outgoing.Add(v);
|
if (owner != SyncedBody.NoOwner && owner != localId) { if (Changed(v, false)) Remember(v); continue; }
|
||||||
|
|
||||||
|
// Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) -
|
||||||
|
// langsames Abdriften (z.B. Abkühlen) rechnet jeder selbst, sonst überschreiben sich alle gegenseitig
|
||||||
|
bool big = owner == SyncedBody.NoOwner;
|
||||||
|
if (!Changed(v, big)) continue;
|
||||||
|
Remember(v);
|
||||||
|
outgoing.Add(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int start = 0; start < outgoing.Count; start += MaxPerMessage)
|
for (int start = 0; start < outgoing.Count; start += MaxPerMessage)
|
||||||
@@ -175,13 +181,16 @@ namespace MSCO
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool Changed(Var v)
|
static bool Changed(Var v, bool bigOnly)
|
||||||
{
|
{
|
||||||
switch (v.Type)
|
switch (v.Type)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
float f = ((FsmFloat)v.Variable).Value;
|
float f = ((FsmFloat)v.Variable).Value;
|
||||||
return Mathf.Abs(f - v.LastF) > Mathf.Max(0.01f, Mathf.Abs(v.LastF) * 0.002f);
|
float delta = Mathf.Abs(f - v.LastF);
|
||||||
|
return bigOnly
|
||||||
|
? delta > Mathf.Max(0.5f, Mathf.Abs(v.LastF) * 0.05f)
|
||||||
|
: delta > Mathf.Max(0.01f, Mathf.Abs(v.LastF) * 0.002f);
|
||||||
case 2: return ((FsmInt)v.Variable).Value != v.LastI;
|
case 2: return ((FsmInt)v.Variable).Value != v.LastI;
|
||||||
default: return ((FsmBool)v.Variable).Value != v.LastB;
|
default: return ((FsmBool)v.Variable).Value != v.LastB;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user