Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e125ff9d74 | ||
|
|
56c3990650 | ||
|
|
ea1b0f74d9 | ||
|
|
f707e67e5c | ||
|
|
dd31587d61 | ||
|
|
5f8062c649 | ||
|
|
52b1219934 | ||
|
|
3b83644ce9 | ||
|
|
0b2834282d | ||
|
|
cb116da2b8 | ||
|
|
cd8c1293e0 | ||
|
|
983291a4b3 | ||
|
|
73004340ce | ||
|
|
2faf03035a | ||
|
|
4801539b9d | ||
|
|
7d341126e8 | ||
|
|
0b55d270e5 | ||
|
|
e844446a21 | ||
|
|
ba4a3d1b9a | ||
|
|
ce2fbdfc10 | ||
|
|
33a415139b |
@@ -1,9 +1,104 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2.8.1
|
||||||
|
|
||||||
|
**Mehr Events synchron**
|
||||||
|
- Zusätzlich zu Jobs, Laden, Werkstatt, Prüfstelle, Müllkippe, Tanzhalle, Ferienhaus und Rallye: Dragrace, Kilju-Käufer,
|
||||||
|
Theater, Fußball, Peräjärvi (Dorf) und die Hütte - ständig wechselnde interne Zähler werden weiterhin automatisch
|
||||||
|
ausgelassen
|
||||||
|
|
||||||
|
**Admin-Menü**
|
||||||
|
- Im Admin-Menü (Spieler-Reiter) gibt es **MAKE ADMIN / REMOVE ADMIN**: Admins können anderen Spielern Admin-Rechte
|
||||||
|
geben oder nehmen (dedizierter Server ab 2.9.4; der Spieler braucht Steam)
|
||||||
|
|
||||||
|
**Kleine Updates**
|
||||||
|
- Mod-Version (2.8.1 für MSCLoader/MSC Launcher) und Netzwerk-Version (2.8) sind getrennt: Updates an der dritten Stelle
|
||||||
|
passen zu allen 2.8-Servern und -Mitspielern. Anleitung im README („Updates veröffentlichen“)
|
||||||
|
|
||||||
|
**Server-Browser**
|
||||||
|
- **Hervorgehobene Server**: vom Keymaster-Admin markierte Server (Tag `featured`) stehen ganz oben, mit goldener
|
||||||
|
Zeile und **FEATURED**-Abzeichen (Liste und Profil)
|
||||||
|
- **Tags** der Server erscheinen im Server-Profil; die Suche findet auch Tags. Serverbetreiber setzen eigene Tags im
|
||||||
|
Web-Panel (Einstellungen), **OFFICIAL** vergeben nur die MSCO-Admins und wird hervorgehoben
|
||||||
|
- Braucht keinen neuen Server: die Infos kommen vom Master (msco.mbm.wtf) und funktionieren mit 2.8-Servern
|
||||||
|
|
||||||
|
**Mitspieler als echte MSC-Menschen**
|
||||||
|
- Andere Spieler sehen aus wie die Leute im Spiel: derselbe Körper wie alle MSC-Menschen, mit einem normalen Hemd,
|
||||||
|
einer Hose und einem Gesicht aus dem Spiel (keine Kleider/Vorhänge/Geister). Bewegt wird ein eigenes Skelett mit der
|
||||||
|
bisherigen Animation - laufen, springen, ducken, sitzen, Items halten
|
||||||
|
- Jeder Spieler bekommt (nach Spieler-Nummer) eine andere Kombination; alle Mitspieler sehen dieselbe
|
||||||
|
- Konsole (F7): `model classic` zeigt wieder die MSCO-Figur, `model game` die MSC-Menschen; `rig` zeigt, wer welches
|
||||||
|
Modell hat. Findet MSCO den Körper nicht, bleibt automatisch die MSCO-Figur
|
||||||
|
|
||||||
|
**Türen an allen Fahrzeugen**
|
||||||
|
- Türen hingen schief, sprangen zurück oder lösten sich: der Tür-Sync erfasste auch die FSMs zum An-/Abbauen,
|
||||||
|
Festschrauben und Lackieren der Tür-Teile und erzwang deren Zustände beim Mitspieler. Jetzt nur noch Öffnen/Schließen
|
||||||
|
- Türen schaukelten hin und her: an einem stehenden Auto schickte jeder in der Nähe den Türwinkel zurück. Jetzt schickt
|
||||||
|
nur, wer an der Tür geklickt hat (bzw. beim fahrenden Auto der Fahrer); empfangene Winkel werden länger gehalten
|
||||||
|
|
||||||
|
**Mitfahren**
|
||||||
|
- **Wer im Fahrzeug ist, fährt mit - genau an seiner Stelle**: Kopf in der Kabine, in der offenen Tür oder auf der
|
||||||
|
Ladefläche -> die Position im Fahrzeug wird festgehalten, kein Umsetzen auf einen berechneten Sitz. Geht auch, wenn
|
||||||
|
noch niemand fährt. Aussteigen mit Leertaste oder der Beifahrer-Taste (P) - man steht dann auf der näheren Seite
|
||||||
|
neben dem Fahrzeug. P neben dem Auto setzt einen wie bisher auf den Beifahrersitz
|
||||||
|
- Beim Ein-/Aussteigen am Fahrersitz und kurz nach dem Fahren wird man nicht festgehalten
|
||||||
|
|
||||||
|
**Satsuma zerfällt nicht mehr beim gemeinsamen Fahren**
|
||||||
|
- Hing das Spiel des Fahrers kurz (z.B. beim Speichern für den Server), gab der Mitfahrer das Auto nach 3 s frei: es
|
||||||
|
blieb bei ihm schlagartig stehen, die Teile wollten mit Tempo weiter und rissen ab - und das „Teil abgefallen“ ging an
|
||||||
|
alle. Ein gefahrenes Auto wird jetzt erst nach 30 s ohne Daten freigegeben, rollt dann mit der Geschwindigkeit des
|
||||||
|
Fahrers weiter, und die Teile bleiben noch 3 s fest, bis alles eingeschwungen ist
|
||||||
|
- Ladung im Auto (Kisten, Werkzeug, Teile im Kofferraum oder auf dem Sitz) gehört jetzt immer dem Fahrer. Vorher
|
||||||
|
übernahm sie oft der Mitfahrer; beim Fahrer folgte sie dann verspätet und „unendlich schwer“ und schlug Sitze und Teile ab
|
||||||
|
|
||||||
|
**Mitfahren ohne Ruckeln**
|
||||||
|
- Fahrer und Beifahrer sitzen beim anderen fest auf dem Sitz: die Figur wird relativ zum Auto übertragen und geglättet,
|
||||||
|
statt in Weltkoordinaten hinter dem fahrenden Auto herzuhinken (vorher wurde der Beifahrer bei Tempo nach hinten gezogen)
|
||||||
|
- Der Beifahrersitz wird jetzt aus der richtigen Fahrerposition berechnet (vorher aus der hinterherhinkenden Figur)
|
||||||
|
- Autos anderer Spieler ruckeln nicht mehr bei jedem Netzwerkpaket zurück (Vorausberechnung in Physik-Zeit) und werden
|
||||||
|
zwischen den Physik-Schritten flüssig bewegt – auch der NPC-Verkehr
|
||||||
|
- Kompatibel: ältere Mods und der Server ignorieren die neuen Sitz-Daten
|
||||||
|
|
||||||
|
**Schönere Oberfläche**
|
||||||
|
- Fenster (Launcher, Konsole, Admin-Menü, Hinweise) mit weichem Schatten und feinem Rand, orange Knöpfe mit Verlauf
|
||||||
|
- Server-Browser: mehr Platz für die Liste, Zeilen leuchten beim Überfahren auf, Füllbalken für die Spielerzahl,
|
||||||
|
Ping farbig (grün/gelb/rot), goldener Streifen bei hervorgehobenen Servern
|
||||||
|
- Seitenleiste mit Trennlinie, Menüpunkte werden beim Überfahren hell
|
||||||
|
|
||||||
## 2.8
|
## 2.8
|
||||||
|
|
||||||
Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d --build`).
|
Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d --build`).
|
||||||
|
|
||||||
|
**Weniger Hänger**
|
||||||
|
- Der Server holt sich den Spielstand nur noch, wenn sich seit dem letzten etwas geändert hat (Speichern lässt das Spiel
|
||||||
|
kurz hängen), Standard für neue Server alle 10 statt 5 Minuten
|
||||||
|
- Die teure Suche über alle PlayMaker-FSMs läuft seltener; die Schadens-Prüfung liest ab und zu nur noch ein Mesh
|
||||||
|
- Hänger-Erkennung: dauert ein Bild länger als 0,2 s, steht in der F7-Konsole, ob MSCO schuld war (und welcher Teil)
|
||||||
|
oder das Spiel selbst (z.B. beim Speichern)
|
||||||
|
|
||||||
|
**Behoben: Öl lief aus dem Satsuma**
|
||||||
|
- Beim Beitreten lief Öl aus, obwohl der Spielstand lokal dicht war: die vom Spiel selbst berechneten Werte
|
||||||
|
`LeakOilpan`, `LeakOilfilter`, `LeakRockerCover`, `LeakHeadgasket` (und Raten wie `…LeakRate`) wurden wie echte
|
||||||
|
Fahrzeugwerte synchronisiert und überschrieben den Stand aus dem Spielstand. Werden jetzt nicht mehr übertragen
|
||||||
|
- Diagnose: `vars <filter>` in der F7-Konsole zeigt die synchronisierten Fahrzeugwerte (z.B. `vars oil`)
|
||||||
|
|
||||||
|
**Scharfe Oberfläche, FPS, Leistung**
|
||||||
|
- Launcher wird in voller Auflösung gezeichnet (Schrift und Kanten scharf statt hochgezogen/verpixelt)
|
||||||
|
- `fps` in der F7-Konsole: FPS-Anzeige oben rechts (grün/gelb/rot), Konsole zeigt auch das längste Bild (Ruckler)
|
||||||
|
- Weniger Ruckler: Sync-Teile suchen nach dem Laden nicht mehr alle im selben Frame, gemeinsame Suche nach Rigidbodys,
|
||||||
|
Unfallschäden werden nur noch nach einem Aufprall geprüft (vorher jede Sekunde große Mesh-Kopien), Fahrzeug-Effekte
|
||||||
|
schlagen ihre Bauteile seltener nach, Ping-/FPS-Anzeige ohne Registry-Zugriff pro Bild
|
||||||
|
|
||||||
|
**Launcher im Vollbild**
|
||||||
|
- Der MSCO-Launcher füllt das ganze Hauptmenü und **skaliert automatisch** mit der Auflösung; Listen wachsen mit dem
|
||||||
|
Platz (keine abgeschnittenen Knöpfe mehr). Das Spielmenü dahinter (Logo, Farbauswahl, MP3 …) ist komplett verdeckt
|
||||||
|
und gesperrt, die Menü-Musik ist stumm. MINIMIZE entfällt
|
||||||
|
|
||||||
|
**P2P: Weiterspielen oder neues Spiel**
|
||||||
|
- In der Host-Lobby: **CONTINUE** (eigenen Spielstand weiterspielen, mit Datum) oder **NEW GAME** (frische Welt für
|
||||||
|
alle). Der bisherige Spielstand wird vorher nach `MSCO_Saves/<Datum>` gesichert (die letzten 10 bleiben), mit
|
||||||
|
Sicherheitsabfrage. Mitspieler bekommen die neue Welt automatisch, sobald sie einmal gespeichert ist
|
||||||
|
|
||||||
**Mehr Sync**
|
**Mehr Sync**
|
||||||
- **Einkäufe**: gekaufte/ausgepackte Items tauchen bei allen auf (auch bei später Beitretenden)
|
- **Einkäufe**: gekaufte/ausgepackte Items tauchen bei allen auf (auch bei später Beitretenden)
|
||||||
- **Verbrauchte Items**: Gegessenes, Getrunkenes und Verbrauchtes verschwindet bei allen
|
- **Verbrauchte Items**: Gegessenes, Getrunkenes und Verbrauchtes verschwindet bei allen
|
||||||
|
|||||||
+20
@@ -18,8 +18,11 @@ namespace MSCO
|
|||||||
"host [port] / join <ip> [port] - direct LAN game (no Steam)\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" +
|
||||||
"ping [on|off] - show your ping to the server (toggle)\n" +
|
"ping [on|off] - show your ping to the server (toggle)\n" +
|
||||||
|
"fps [on|off] - show the frames per second (toggle)\n" +
|
||||||
"/admin - admin menu (kick / ban / teleport / cheats)\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" +
|
||||||
|
"model [game|classic] - other players as MSC characters or the MSCO figure\n" +
|
||||||
|
"vars <filter> - synced vehicle values, e.g. vars oil (debug)\n" +
|
||||||
"debug - toggle sync debug output\n" +
|
"debug - toggle sync debug output\n" +
|
||||||
"clear - clear this console";
|
"clear - clear this console";
|
||||||
|
|
||||||
@@ -112,6 +115,12 @@ namespace MSCO
|
|||||||
else mp.ShowPing = !mp.ShowPing;
|
else mp.ShowPing = !mp.ShowPing;
|
||||||
print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")");
|
print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")");
|
||||||
break;
|
break;
|
||||||
|
case "fps":
|
||||||
|
// "fps" schaltet die Anzeige um, "fps on/off" setzt sie
|
||||||
|
if (args.Length > 1) mp.ShowFps = args[1].ToLower() == "on" || args[1] == "1";
|
||||||
|
else mp.ShowFps = !mp.ShowFps;
|
||||||
|
print("FPS: " + mp.FpsText + " (display " + (mp.ShowFps ? "on" : "off") + ")");
|
||||||
|
break;
|
||||||
case "admin":
|
case "admin":
|
||||||
case "/admin":
|
case "/admin":
|
||||||
mp.OpenAdminMenu();
|
mp.OpenAdminMenu();
|
||||||
@@ -119,9 +128,20 @@ namespace MSCO
|
|||||||
case "rig":
|
case "rig":
|
||||||
foreach (string line in mp.DescribeRigs()) print(line);
|
foreach (string line in mp.DescribeRigs()) print(line);
|
||||||
break;
|
break;
|
||||||
|
case "model":
|
||||||
|
// Mitspieler als echte MSC-Menschen ("game") oder als MSCO-Figur ("classic")
|
||||||
|
if (args.Length > 1) RemotePlayer.UseGameModels = args[1].ToLower() != "classic";
|
||||||
|
else RemotePlayer.UseGameModels = !RemotePlayer.UseGameModels;
|
||||||
|
mp.RebuildAvatars();
|
||||||
|
print("Player models: " + (RemotePlayer.UseGameModels ? "MSC characters (model classic = MSCO figure)" : "MSCO figure (model game = MSC characters)"));
|
||||||
|
break;
|
||||||
case "vehicles":
|
case "vehicles":
|
||||||
foreach (string line in mp.DescribeVehicles()) print(line);
|
foreach (string line in mp.DescribeVehicles()) print(line);
|
||||||
break;
|
break;
|
||||||
|
case "vars":
|
||||||
|
// Diagnose: welche Fahrzeugwerte synchronisiert werden (z.B. "vars oil")
|
||||||
|
foreach (string line in mp.DescribeVehicleValues(args.Length > 1 ? args[1] : "")) print(line);
|
||||||
|
break;
|
||||||
case "debug":
|
case "debug":
|
||||||
FsmSync.Debug = !FsmSync.Debug;
|
FsmSync.Debug = !FsmSync.Debug;
|
||||||
print("Sync debug output " + (FsmSync.Debug ? "ON" : "OFF"));
|
print("Sync debug output " + (FsmSync.Debug ? "ON" : "OFF"));
|
||||||
|
|||||||
+52
-10
@@ -21,7 +21,9 @@ namespace MSCO
|
|||||||
static readonly string[] Keywords =
|
static readonly string[] Keywords =
|
||||||
{
|
{
|
||||||
"ignition", "starter", "engine", "light", "choke", "wiper", "hazard", "indicator", "turnsignal", "blinker",
|
"ignition", "starter", "engine", "light", "choke", "wiper", "hazard", "indicator", "turnsignal", "blinker",
|
||||||
"horn", "heater", "fan", "radio", "key", "switch", "knob", "button"
|
"horn", "heater", "fan", "radio", "key", "switch", "knob", "button",
|
||||||
|
// Handbremse, Sonnenblenden, Sitzlehne, Gurt, CD-Spieler, Innenlicht, Tankdeckel
|
||||||
|
"handbrake", "sunvisor", "seattop", "seatbelt", "cdplayer", "interiorlight", "opencap"
|
||||||
};
|
};
|
||||||
// Montage/Speichern/Schrauben - läuft über den Objekt- bzw. FSM-Sync, hier nicht anfassen
|
// Montage/Speichern/Schrauben - läuft über den Objekt- bzw. FSM-Sync, hier nicht anfassen
|
||||||
static readonly string[] Excluded =
|
static readonly string[] Excluded =
|
||||||
@@ -51,6 +53,8 @@ namespace MSCO
|
|||||||
readonly Dictionary<int, Control> controls = new Dictionary<int, Control>();
|
readonly Dictionary<int, Control> controls = new Dictionary<int, Control>();
|
||||||
readonly HashSet<PlayMakerFSM> scanned = new HashSet<PlayMakerFSM>();
|
readonly HashSet<PlayMakerFSM> scanned = new HashSet<PlayMakerFSM>();
|
||||||
readonly List<Control> outgoing = new List<Control>();
|
readonly List<Control> outgoing = new List<Control>();
|
||||||
|
// Zustände für Schalter, die wir noch nicht kennen (Teil noch nicht angebaut/aktiv, Server-Snapshot vor dem Suchlauf)
|
||||||
|
readonly Dictionary<int, string> pending = new Dictionary<int, string>();
|
||||||
float nextScan, nextCheck;
|
float nextScan, nextCheck;
|
||||||
|
|
||||||
public int Count { get { return controls.Count; } }
|
public int Count { get { return controls.Count; } }
|
||||||
@@ -60,11 +64,13 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
controls.Clear();
|
controls.Clear();
|
||||||
scanned.Clear();
|
scanned.Clear();
|
||||||
nextScan = 0f;
|
pending.Clear();
|
||||||
|
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(ObjectSync objects, int localId, Vector3 playerPos, Action<MsgType, byte[]> send)
|
public void Update(ObjectSync objects, int localId, Vector3 playerPos, Action<MsgType, byte[]> send)
|
||||||
{
|
{
|
||||||
|
if (nextScan < 0f) nextScan = Time.time + 2.4f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
||||||
if (Time.time >= nextScan)
|
if (Time.time >= nextScan)
|
||||||
{
|
{
|
||||||
nextScan = Time.time + ScanInterval;
|
nextScan = Time.time + ScanInterval;
|
||||||
@@ -114,32 +120,68 @@ namespace MSCO
|
|||||||
int key = r.ReadInt32();
|
int key = r.ReadInt32();
|
||||||
string state = r.ReadString();
|
string state = r.ReadString();
|
||||||
Control c;
|
Control c;
|
||||||
if (!controls.TryGetValue(key, out c) || c.Fsm == null) { nextScan = 0f; continue; }
|
if (!controls.TryGetValue(key, out c) || c.Fsm == null) { pending[key] = state; nextScan = 0f; continue; }
|
||||||
if (objects.VehicleOwner(c.Root) == localId) continue; // ich fahre: meine Schalter gelten
|
if (objects.VehicleOwner(c.Root) == localId) continue; // ich fahre: meine Schalter gelten
|
||||||
if (c.Fsm.ActiveStateName == state) { c.LastState = state; continue; }
|
SwitchTo(c, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SwitchTo(Control c, string state)
|
||||||
|
{
|
||||||
|
if (c.Fsm.ActiveStateName == state) { c.LastState = state; return; }
|
||||||
FsmState target = c.Fsm.Fsm.GetState(state);
|
FsmState target = c.Fsm.Fsm.GetState(state);
|
||||||
if (target == null) continue;
|
if (target == null) return;
|
||||||
c.LastState = state; // nicht als eigene Änderung zurückschicken
|
c.LastState = state; // nicht als eigene Änderung zurückschicken
|
||||||
FsmSync.SwitchStateSilently(c.Fsm, target);
|
FsmSync.SwitchStateSilently(c.Fsm, target);
|
||||||
c.LastState = c.Fsm.ActiveStateName; // FSM kann sofort weiterschalten (z.B. "Start" -> "Running")
|
c.LastState = c.Fsm.ActiveStateName; // FSM kann sofort weiterschalten (z.B. "Start" -> "Running")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void Scan()
|
void Scan()
|
||||||
{
|
{
|
||||||
int before = controls.Count;
|
int before = controls.Count;
|
||||||
foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f))
|
foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f))
|
||||||
{
|
{
|
||||||
if (fsm == null || !scanned.Add(fsm)) continue;
|
if (fsm == null || scanned.Contains(fsm)) continue;
|
||||||
string root = fsm.transform.root.name;
|
string root = fsm.transform.root.name;
|
||||||
if (!IsVehicle(root) || !Wanted(fsm.gameObject.name, fsm.FsmName)) continue;
|
// Noch nicht am Fahrzeug (z.B. Radio liegt in CARPARTS): beim nächsten Suchlauf nochmal ansehen
|
||||||
|
if (!IsVehicle(root)) continue;
|
||||||
|
scanned.Add(fsm);
|
||||||
|
if (!Wanted(fsm.gameObject.name, fsm.FsmName)) continue;
|
||||||
int key = Hash(FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName);
|
int key = Hash(FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName);
|
||||||
if (controls.ContainsKey(key)) continue;
|
if (controls.ContainsKey(key)) continue;
|
||||||
controls[key] = new Control { Key = key, Root = root, Fsm = fsm, LastState = fsm.ActiveStateName, WindowStart = Time.time };
|
var control = new Control { Key = key, Root = root, Fsm = fsm, LastState = fsm.ActiveStateName, WindowStart = Time.time };
|
||||||
|
controls[key] = control;
|
||||||
|
string waiting;
|
||||||
|
if (pending.TryGetValue(key, out waiting)) { pending.Remove(key); SwitchTo(control, waiting); }
|
||||||
}
|
}
|
||||||
if (controls.Count != before) MscoLog.Info("[MSCO] Vehicle controls (ignition, lights, engine ...): " + controls.Count + " synced");
|
if (controls.Count != before) MscoLog.Info("[MSCO] Vehicle controls (ignition, lights, engine ...): " + controls.Count + " synced");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// P2P-Host: alle Schalterzustände nochmal schicken (Licht an, Motor läuft ...) - für später Beigetretene und
|
||||||
|
/// falls eine Nachricht verloren ging. Fahrzeuge, die ein anderer fährt, gibt dieser vor.
|
||||||
|
/// </summary>
|
||||||
|
public void ResendAll(ObjectSync objects, int localId, Action<MsgType, byte[]> send)
|
||||||
|
{
|
||||||
|
outgoing.Clear();
|
||||||
|
foreach (Control c in controls.Values)
|
||||||
|
{
|
||||||
|
if (c.Fsm == null || c.Muted || string.IsNullOrEmpty(c.Fsm.ActiveStateName)) continue;
|
||||||
|
int owner = objects.VehicleOwner(c.Root);
|
||||||
|
if (owner != SyncedBody.NoOwner && owner != localId) continue;
|
||||||
|
outgoing.Add(c);
|
||||||
|
}
|
||||||
|
for (int start = 0; start < outgoing.Count; start += MaxPerMessage)
|
||||||
|
{
|
||||||
|
int from = start, count = Math.Min(MaxPerMessage, outgoing.Count - start);
|
||||||
|
send(MsgType.VehicleControls, NetSession.Build(w =>
|
||||||
|
{
|
||||||
|
w.Write(count);
|
||||||
|
for (int i = from; i < from + count; i++) { w.Write(outgoing[i].Key); w.Write(outgoing[i].Fsm.ActiveStateName); }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static bool Wanted(string objectName, string fsmName)
|
static bool Wanted(string objectName, string fsmName)
|
||||||
{
|
{
|
||||||
string o = objectName.ToLower(), f = fsmName.ToLower();
|
string o = objectName.ToLower(), f = fsmName.ToLower();
|
||||||
|
|||||||
+38
-8
@@ -14,8 +14,10 @@ namespace MSCO
|
|||||||
public class DamageSync
|
public class DamageSync
|
||||||
{
|
{
|
||||||
const float ScanInterval = 10f;
|
const float ScanInterval = 10f;
|
||||||
const float CheckInterval = 1f; // Fahrzeuge, die wir fahren
|
const float CheckInterval = 0.5f; // so oft nach einem Aufprall prüfen
|
||||||
const float FreeCheckInterval = 5f; // geparkte Fahrzeuge (Unfall nur durch Anstoßen) seltener
|
const float ImpactWindow = 4f; // so lange nach einem Aufprall prüfen (Verformung läuft ein paar Frames)
|
||||||
|
const float ImpactDeltaV = 2.5f; // plötzliche Geschwindigkeitsänderung (m/s pro Frame) = Aufprall
|
||||||
|
const float SafetyCheckInterval = 2f; // sonst nur ab und zu EIN Mesh (z.B. Reparatur in der Werkstatt) - verteilt statt alle auf einmal
|
||||||
const float MinMove = 0.002f;
|
const float MinMove = 0.002f;
|
||||||
const int MaxPerMessage = 4000;
|
const int MaxPerMessage = 4000;
|
||||||
|
|
||||||
@@ -26,13 +28,19 @@ namespace MSCO
|
|||||||
public int Key;
|
public int Key;
|
||||||
public string Root;
|
public string Root;
|
||||||
public MeshFilter Filter;
|
public MeshFilter Filter;
|
||||||
|
public Rigidbody Body; // Fahrzeug, zu dem das Mesh gehört
|
||||||
public Vector3[] Known; // zuletzt gesendet/empfangen
|
public Vector3[] Known; // zuletzt gesendet/empfangen
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly Dictionary<int, DamageMesh> meshes = new Dictionary<int, DamageMesh>();
|
readonly Dictionary<int, DamageMesh> meshes = new Dictionary<int, DamageMesh>();
|
||||||
readonly HashSet<MeshFilter> seen = new HashSet<MeshFilter>();
|
readonly HashSet<MeshFilter> seen = new HashSet<MeshFilter>();
|
||||||
readonly List<int> changed = new List<int>();
|
readonly List<int> changed = new List<int>();
|
||||||
float nextScan, nextCheck, nextFreeCheck;
|
float nextScan, nextCheck, nextSafetyCheck;
|
||||||
|
int safetyIndex;
|
||||||
|
// Mesh-Daten lesen ist teuer (große Arrays, Speicher-Müll) - nur nach einem Aufprall
|
||||||
|
readonly Dictionary<Rigidbody, Vector3> lastVelocity = new Dictionary<Rigidbody, Vector3>();
|
||||||
|
readonly Dictionary<Rigidbody, float> lastImpact = new Dictionary<Rigidbody, float>();
|
||||||
|
readonly List<Rigidbody> bodies = new List<Rigidbody>();
|
||||||
static List<Type> types;
|
static List<Type> types;
|
||||||
|
|
||||||
public int Count { get { return meshes.Count; } }
|
public int Count { get { return meshes.Count; } }
|
||||||
@@ -41,27 +49,47 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
meshes.Clear();
|
meshes.Clear();
|
||||||
seen.Clear();
|
seen.Clear();
|
||||||
nextScan = 0f;
|
lastVelocity.Clear();
|
||||||
|
lastImpact.Clear();
|
||||||
|
bodies.Clear();
|
||||||
|
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send)
|
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send)
|
||||||
{
|
{
|
||||||
|
if (nextScan < 0f) nextScan = Time.time + 1.0f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
||||||
if (Time.time >= nextScan)
|
if (Time.time >= nextScan)
|
||||||
{
|
{
|
||||||
nextScan = Time.time + ScanInterval;
|
nextScan = Time.time + ScanInterval;
|
||||||
Scan();
|
Scan();
|
||||||
}
|
}
|
||||||
|
// Aufprall erkennen: Geschwindigkeit ändert sich schlagartig (billig, jedes Frame)
|
||||||
|
for (int i = 0; i < bodies.Count; i++)
|
||||||
|
{
|
||||||
|
Rigidbody rb = bodies[i];
|
||||||
|
if (rb == null || rb.isKinematic) continue;
|
||||||
|
Vector3 v = rb.velocity, before;
|
||||||
|
if (lastVelocity.TryGetValue(rb, out before) && (v - before).sqrMagnitude > ImpactDeltaV * ImpactDeltaV)
|
||||||
|
lastImpact[rb] = Time.time;
|
||||||
|
lastVelocity[rb] = v;
|
||||||
|
}
|
||||||
|
|
||||||
if (Time.time < nextCheck) return;
|
if (Time.time < nextCheck) return;
|
||||||
nextCheck = Time.time + CheckInterval;
|
nextCheck = Time.time + CheckInterval;
|
||||||
bool checkFree = Time.time >= nextFreeCheck;
|
bool safety = Time.time >= nextSafetyCheck;
|
||||||
if (checkFree) nextFreeCheck = Time.time + FreeCheckInterval;
|
if (safety) { nextSafetyCheck = Time.time + SafetyCheckInterval; safetyIndex++; }
|
||||||
|
int index = -1;
|
||||||
|
|
||||||
foreach (DamageMesh m in meshes.Values)
|
foreach (DamageMesh m in meshes.Values)
|
||||||
{
|
{
|
||||||
|
index++;
|
||||||
if (m.Filter == null) continue;
|
if (m.Filter == null) continue;
|
||||||
|
bool safetyThis = safety && meshes.Count > 0 && index == safetyIndex % meshes.Count;
|
||||||
int owner = objects.VehicleOwner(m.Root);
|
int owner = objects.VehicleOwner(m.Root);
|
||||||
if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt die Schäden
|
if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt die Schäden
|
||||||
if (owner == SyncedBody.NoOwner && !checkFree) continue;
|
float impact;
|
||||||
|
bool recentImpact = m.Body != null && lastImpact.TryGetValue(m.Body, out impact) && Time.time - impact < ImpactWindow;
|
||||||
|
if (!recentImpact && !safetyThis) continue;
|
||||||
|
|
||||||
Mesh mesh = m.Filter.sharedMesh;
|
Mesh mesh = m.Filter.sharedMesh;
|
||||||
if (mesh == null) continue;
|
if (mesh == null) continue;
|
||||||
@@ -164,7 +192,9 @@ namespace MSCO
|
|||||||
if (mesh == null || mesh.vertexCount == 0 || mesh.vertexCount > 65000) return;
|
if (mesh == null || mesh.vertexCount == 0 || mesh.vertexCount > 65000) return;
|
||||||
int key = Hash(FsmSync.PathOf(mf.transform));
|
int key = Hash(FsmSync.PathOf(mf.transform));
|
||||||
if (meshes.ContainsKey(key)) return; // nicht eindeutig
|
if (meshes.ContainsKey(key)) return; // nicht eindeutig
|
||||||
meshes[key] = new DamageMesh { Key = key, Root = mf.transform.root.name, Filter = mf, Known = mesh.vertices };
|
Rigidbody body = mf.transform.root.GetComponent<Rigidbody>();
|
||||||
|
meshes[key] = new DamageMesh { Key = key, Root = mf.transform.root.name, Filter = mf, Body = body, Known = mesh.vertices };
|
||||||
|
if (body != null && !bodies.Contains(body)) bodies.Add(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsVehicle(string root)
|
static bool IsVehicle(string root)
|
||||||
|
|||||||
+17
-1
@@ -16,6 +16,13 @@ namespace MSCO
|
|||||||
public class DoorSync
|
public class DoorSync
|
||||||
{
|
{
|
||||||
static readonly string[] Keywords = { "door", "gate", "hood", "bonnet", "trunk", "lid", "hatch" };
|
static readonly string[] Keywords = { "door", "gate", "hood", "bonnet", "trunk", "lid", "hatch" };
|
||||||
|
// Nicht die FSMs zum An-/Abbauen, Festschrauben, Lackieren, Speichern ... der Tür-TEILE: deren Zustände wurden
|
||||||
|
// beim Mitspieler erzwungen - Türen hingen schief, lösten sich oder sprangen zurück. Montage läuft über FsmSync.
|
||||||
|
static readonly string[] ExcludedFsms =
|
||||||
|
{
|
||||||
|
"removal", "assembly", "assemble", "boltcheck", "bolt", "screw", "paint", "data", "save", "load", "wiring",
|
||||||
|
"setup", "damage", "tightness", "detach", "attach", "install"
|
||||||
|
};
|
||||||
|
|
||||||
const float Cooldown = 1.5f; // Folge-Zustände der Animation nicht nochmal senden
|
const float Cooldown = 1.5f; // Folge-Zustände der Animation nicht nochmal senden
|
||||||
const float RescanInterval = 10f; // neue Objekte (z.B. angebaute Autotüren) aufnehmen
|
const float RescanInterval = 10f; // neue Objekte (z.B. angebaute Autotüren) aufnehmen
|
||||||
@@ -96,12 +103,13 @@ namespace MSCO
|
|||||||
|
|
||||||
void Scan()
|
void Scan()
|
||||||
{
|
{
|
||||||
foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f))
|
foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f))
|
||||||
{
|
{
|
||||||
if (fsm == null) continue;
|
if (fsm == null) continue;
|
||||||
string root = fsm.transform.root.name;
|
string root = fsm.transform.root.name;
|
||||||
if (root == "PLAYER" || root.StartsWith("MSCO_")) continue;
|
if (root == "PLAYER" || root.StartsWith("MSCO_")) continue;
|
||||||
if (!Matches(fsm.gameObject.name) && !Matches(fsm.FsmName) && !Matches(fsm.transform.parent != null ? fsm.transform.parent.name : "")) continue;
|
if (!Matches(fsm.gameObject.name) && !Matches(fsm.FsmName) && !Matches(fsm.transform.parent != null ? fsm.transform.parent.name : "")) continue;
|
||||||
|
if (IsPartLogic(fsm)) continue;
|
||||||
|
|
||||||
string key = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName;
|
string key = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName;
|
||||||
Tracked t;
|
Tracked t;
|
||||||
@@ -110,6 +118,14 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool IsPartLogic(PlayMakerFSM fsm)
|
||||||
|
{
|
||||||
|
string f = fsm.FsmName.ToLower(), o = fsm.gameObject.name.ToLower();
|
||||||
|
if (o.StartsWith("trigger_") || o.StartsWith("bolt")) return true;
|
||||||
|
foreach (string x in ExcludedFsms) if (f.Contains(x)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static bool Matches(string name)
|
static bool Matches(string name)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(name)) return false;
|
if (string.IsNullOrEmpty(name)) return false;
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mitspieler als MSC-Mensch: Alle Leute im Spiel benutzen denselben Körper ("bodymesh", 20 Knochen) und
|
||||||
|
/// unterscheiden sich nur durch drei Materialien - Hemd (char_shirtXX), Hose (char_pantsXX) und Gesicht
|
||||||
|
/// (char_faceXX). MSCO nimmt nur dieses Mesh, baut ein eigenes Skelett aus seiner Bind-Pose und gibt jedem
|
||||||
|
/// Spieler ein normales Hemd/Hose/Gesicht (keine Kleider, Vorhänge oder Geister). Die Knochen werden jedes Frame
|
||||||
|
/// direkt aus den Gelenkpunkten der MSCO-Figur gesetzt (laufen, springen, ducken, sitzen, Items halten).
|
||||||
|
///
|
||||||
|
/// Knochen-Reihenfolge ist bei allen MSC-Menschen gleich (nur die Namen unterscheiden sich):
|
||||||
|
/// 0 Becken, 1-2 Wirbelsäule, 3 Kopf, 4/9 Schlüsselbein, 5/10 Schulter, 6/11 Ellbogen, 7/12 Hand, 8/13 Finger,
|
||||||
|
/// 14/17 Hüfte, 15/18 Knie, 16/19 Knöchel (links/rechts). Bind-Pose: Z oben, X vorne, -Y links.
|
||||||
|
/// </summary>
|
||||||
|
public class GameCharacter
|
||||||
|
{
|
||||||
|
/// <summary>Gelenkpunkte der MSCO-Figur im lokalen Raum des Spielers (jedes Frame von RemotePlayer.Pose).</summary>
|
||||||
|
public class Rig
|
||||||
|
{
|
||||||
|
public Vector3 Hip, NeckBase, Head;
|
||||||
|
public Vector3 Up = Vector3.up;
|
||||||
|
public float Pitch;
|
||||||
|
public readonly Vector3[] HipJoint = new Vector3[2], Knee = new Vector3[2], Ankle = new Vector3[2];
|
||||||
|
public readonly Vector3[] Shoulder = new Vector3[2], Elbow = new Vector3[2], Hand = new Vector3[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
const int BoneCount = 20;
|
||||||
|
const int Pelvis = 0, Head = 3;
|
||||||
|
static readonly int[] Torso = { 0, 1, 2, 4, 9 };
|
||||||
|
static readonly int[] Shoulder = { 5, 10 }, Elbow = { 6, 11 }, Wrist = { 7, 12 }, Finger = { 8, 13 };
|
||||||
|
static readonly int[] HipBone = { 14, 17 }, Knee = { 15, 18 }, Ankle = { 16, 19 };
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Mesh und Kleidung aus dem Spiel
|
||||||
|
|
||||||
|
static Mesh bodyMesh;
|
||||||
|
static List<Material> shirts, pants, faces;
|
||||||
|
static float nextSearch;
|
||||||
|
|
||||||
|
static bool FindAssets()
|
||||||
|
{
|
||||||
|
if (bodyMesh != null && shirts.Count > 0 && pants.Count > 0 && faces.Count > 0) return true;
|
||||||
|
if (Time.realtimeSinceStartup < nextSearch) return false;
|
||||||
|
nextSearch = Time.realtimeSinceStartup + 10f;
|
||||||
|
|
||||||
|
var users = new Dictionary<Mesh, int>();
|
||||||
|
var shirtByName = new Dictionary<string, Material>();
|
||||||
|
var pantsByName = new Dictionary<string, Material>();
|
||||||
|
var faceByName = new Dictionary<string, Material>();
|
||||||
|
foreach (SkinnedMeshRenderer smr in Resources.FindObjectsOfTypeAll<SkinnedMeshRenderer>())
|
||||||
|
{
|
||||||
|
if (smr == null || smr.name != "bodymesh") continue;
|
||||||
|
Mesh m = smr.sharedMesh;
|
||||||
|
if (m == null || m.bindposes == null || m.bindposes.Length != BoneCount || m.subMeshCount != 3) continue;
|
||||||
|
int n;
|
||||||
|
users.TryGetValue(m, out n);
|
||||||
|
users[m] = n + 1;
|
||||||
|
Material[] mats = smr.sharedMaterials;
|
||||||
|
if (mats == null || mats.Length != 3) continue;
|
||||||
|
Add(shirtByName, mats[0], "char_shirt");
|
||||||
|
Add(pantsByName, mats[1], "char_pants");
|
||||||
|
if (mats[2] != null && !mats[2].name.Contains("woman")) Add(faceByName, mats[2], "char_face");
|
||||||
|
}
|
||||||
|
// Das Mesh, das die meisten Leute benutzen = der normale Mann
|
||||||
|
Mesh best = null;
|
||||||
|
int bestUsers = 0;
|
||||||
|
foreach (KeyValuePair<Mesh, int> kv in users)
|
||||||
|
if (kv.Value > bestUsers && LooksHuman(kv.Key)) { best = kv.Key; bestUsers = kv.Value; }
|
||||||
|
bodyMesh = best;
|
||||||
|
shirts = Sorted(shirtByName);
|
||||||
|
pants = Sorted(pantsByName);
|
||||||
|
faces = Sorted(faceByName);
|
||||||
|
bool ok = bodyMesh != null && shirts.Count > 0 && pants.Count > 0 && faces.Count > 0;
|
||||||
|
if (ok) MscoLog.Info("[MSCO] Player models: MSC body with " + shirts.Count + " shirts, " + pants.Count + " trousers, " + faces.Count + " faces");
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Add(Dictionary<string, Material> into, Material m, string prefix)
|
||||||
|
{
|
||||||
|
if (m != null && m.name.StartsWith(prefix) && !into.ContainsKey(m.name)) into[m.name] = m;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Material> Sorted(Dictionary<string, Material> d)
|
||||||
|
{
|
||||||
|
var names = new List<string>(d.Keys);
|
||||||
|
names.Sort(string.CompareOrdinal); // bei allen Spielern gleiche Reihenfolge -> gleiches Aussehen
|
||||||
|
return names.ConvertAll(n => d[n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prüft die erwartete Knochen-Reihenfolge an der Bind-Pose (Kopf oben, Knöchel unten, Beine lang).</summary>
|
||||||
|
static bool LooksHuman(Mesh m)
|
||||||
|
{
|
||||||
|
Vector3[] p = BindPositions(m);
|
||||||
|
if (p[Head].z < p[Pelvis].z + 0.4f) return false;
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
if (p[Ankle[i]].z > p[Knee[i]].z || p[Knee[i]].z > p[HipBone[i]].z) return false;
|
||||||
|
if (p[Wrist[i]].z > p[Shoulder[i]].z) return false;
|
||||||
|
}
|
||||||
|
return p[Shoulder[0]].y < 0f && p[Shoulder[1]].y > 0f; // links = -Y
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector3[] BindPositions(Mesh m)
|
||||||
|
{
|
||||||
|
var p = new Vector3[BoneCount];
|
||||||
|
for (int i = 0; i < BoneCount; i++) p[i] = m.bindposes[i].inverse.GetColumn(3);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Instanz
|
||||||
|
|
||||||
|
GameObject model;
|
||||||
|
Transform owner;
|
||||||
|
readonly Transform[] bones = new Transform[BoneCount];
|
||||||
|
readonly Vector3[] restPos = new Vector3[BoneCount]; // relativ zum Spieler
|
||||||
|
readonly Quaternion[] restRot = new Quaternion[BoneCount];
|
||||||
|
Vector3 restHipCenter;
|
||||||
|
public string Description { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Figur bauen (variant wählt Hemd, Hose und Gesicht). null, wenn das Spiel-Mesh nicht gefunden wird.</summary>
|
||||||
|
public static GameCharacter Create(Transform owner, int variant)
|
||||||
|
{
|
||||||
|
if (!FindAssets()) return null;
|
||||||
|
var c = new GameCharacter();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
c.Build(owner, Mathf.Abs(variant));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
MscoLog.Warning("[MSCO] Player model could not be built: " + e.Message);
|
||||||
|
c.Destroy();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Build(Transform owner, int variant)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
Vector3[] bind = BindPositions(bodyMesh);
|
||||||
|
float leg = (bind[Knee[0]] - bind[HipBone[0]]).magnitude + (bind[Ankle[0]] - bind[Knee[0]]).magnitude;
|
||||||
|
float scale = 0.84f / Mathf.Max(0.1f, leg); // Bein der MSCO-Figur
|
||||||
|
|
||||||
|
model = new GameObject("MSCO_Model");
|
||||||
|
model.transform.SetParent(owner, false);
|
||||||
|
// Bind-Pose: X vorne, Z oben -> Spieler: Z vorne, Y oben
|
||||||
|
model.transform.localRotation = Quaternion.Inverse(Quaternion.LookRotation(Vector3.right, Vector3.forward));
|
||||||
|
model.transform.localScale = Vector3.one * scale;
|
||||||
|
|
||||||
|
Matrix4x4[] bindposes = bodyMesh.bindposes;
|
||||||
|
for (int i = 0; i < BoneCount; i++)
|
||||||
|
{
|
||||||
|
var b = new GameObject("bone" + i).transform;
|
||||||
|
b.SetParent(model.transform, false);
|
||||||
|
Matrix4x4 m = bindposes[i].inverse;
|
||||||
|
b.localPosition = m.GetColumn(3);
|
||||||
|
b.localRotation = Quaternion.LookRotation(m.GetColumn(2), m.GetColumn(1));
|
||||||
|
bones[i] = b;
|
||||||
|
restPos[i] = owner.InverseTransformPoint(b.position);
|
||||||
|
restRot[i] = Quaternion.Inverse(owner.rotation) * b.rotation;
|
||||||
|
}
|
||||||
|
restHipCenter = (restPos[HipBone[0]] + restPos[HipBone[1]]) * 0.5f;
|
||||||
|
|
||||||
|
var smr = model.AddComponent<SkinnedMeshRenderer>();
|
||||||
|
smr.sharedMesh = bodyMesh;
|
||||||
|
smr.bones = bones;
|
||||||
|
smr.rootBone = bones[Pelvis];
|
||||||
|
smr.updateWhenOffscreen = true; // die Knochen bewegen sich weit weg von der gespeicherten Hülle
|
||||||
|
Material shirt = shirts[(variant * 7 + 3) % shirts.Count];
|
||||||
|
Material trousers = pants[(variant * 3 + 1) % pants.Count];
|
||||||
|
Material face = faces[(variant * 5 + 2) % faces.Count];
|
||||||
|
smr.sharedMaterials = new[] { shirt, trousers, face };
|
||||||
|
Description = "MSC body (" + shirt.name + ", " + trousers.name + ", " + face.name + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- jedes Frame
|
||||||
|
|
||||||
|
public void Apply(Rig rig)
|
||||||
|
{
|
||||||
|
if (model == null) return;
|
||||||
|
Quaternion root = owner.rotation;
|
||||||
|
Quaternion lean = Quaternion.FromToRotation(Vector3.up, rig.Up);
|
||||||
|
Quaternion torso = root * lean;
|
||||||
|
|
||||||
|
foreach (int i in Torso) Place(i, TorsoPoint(rig, lean, restPos[i]), torso * restRot[i]);
|
||||||
|
Place(Head, TorsoPoint(rig, lean, restPos[Head]), root * Quaternion.Euler(rig.Pitch, 0f, 0f) * restRot[Head]);
|
||||||
|
|
||||||
|
for (int s = 0; s < 2; s++)
|
||||||
|
{
|
||||||
|
// Bein: Hüfte am Becken, dann Richtung Knie bzw. Knöchel der MSCO-Figur
|
||||||
|
Vector3 hip = TorsoPoint(rig, lean, restPos[HipBone[s]]);
|
||||||
|
Quaternion thigh = AimDelta(HipBone[s], Knee[s], rig.Knee[s] - rig.HipJoint[s]);
|
||||||
|
Place(HipBone[s], hip, root * thigh * restRot[HipBone[s]]);
|
||||||
|
Vector3 knee = hip + root * (thigh * (restPos[Knee[s]] - restPos[HipBone[s]]));
|
||||||
|
Quaternion shin = AimDelta(Knee[s], Ankle[s], rig.Ankle[s] - rig.Knee[s]);
|
||||||
|
Place(Knee[s], knee, root * shin * restRot[Knee[s]]);
|
||||||
|
Vector3 ankle = knee + root * (shin * (restPos[Ankle[s]] - restPos[Knee[s]]));
|
||||||
|
Place(Ankle[s], ankle, root * restRot[Ankle[s]]); // Fuß bleibt flach
|
||||||
|
|
||||||
|
// Arm: Schulter am Oberkörper, dann Richtung Ellbogen bzw. Hand
|
||||||
|
Vector3 shoulder = TorsoPoint(rig, lean, restPos[Shoulder[s]]);
|
||||||
|
Quaternion upper = AimDelta(Shoulder[s], Elbow[s], rig.Elbow[s] - rig.Shoulder[s]);
|
||||||
|
Place(Shoulder[s], shoulder, root * upper * restRot[Shoulder[s]]);
|
||||||
|
Vector3 elbow = shoulder + root * (upper * (restPos[Elbow[s]] - restPos[Shoulder[s]]));
|
||||||
|
Quaternion fore = AimDelta(Elbow[s], Wrist[s], rig.Hand[s] - rig.Elbow[s]);
|
||||||
|
Place(Elbow[s], elbow, root * fore * restRot[Elbow[s]]);
|
||||||
|
Vector3 wrist = elbow + root * (fore * (restPos[Wrist[s]] - restPos[Elbow[s]]));
|
||||||
|
Place(Wrist[s], wrist, root * fore * restRot[Wrist[s]]);
|
||||||
|
Vector3 finger = wrist + root * (fore * (restPos[Finger[s]] - restPos[Wrist[s]]));
|
||||||
|
Place(Finger[s], finger, root * fore * restRot[Finger[s]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Punkt des Oberkörpers: an der Hüfte der MSCO-Figur, mit ihr geneigt.</summary>
|
||||||
|
Vector3 TorsoPoint(Rig rig, Quaternion lean, Vector3 rest)
|
||||||
|
{
|
||||||
|
return owner.TransformPoint(rig.Hip + lean * (rest - restHipCenter));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Drehung, die das Knochenstück from->to aus der Ruhelage in die gewünschte Richtung bringt.</summary>
|
||||||
|
Quaternion AimDelta(int from, int to, Vector3 desired)
|
||||||
|
{
|
||||||
|
Vector3 rest = restPos[to] - restPos[from];
|
||||||
|
if (desired.sqrMagnitude < 1e-6f || rest.sqrMagnitude < 1e-6f) return Quaternion.identity;
|
||||||
|
return Quaternion.FromToRotation(rest, desired);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Place(int i, Vector3 pos, Quaternion rot)
|
||||||
|
{
|
||||||
|
bones[i].position = pos;
|
||||||
|
bones[i].rotation = rot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Destroy()
|
||||||
|
{
|
||||||
|
if (model != null) Object.Destroy(model);
|
||||||
|
model = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-5
@@ -19,7 +19,12 @@ namespace MSCO
|
|||||||
const float SendInterval = 0.1f;
|
const float SendInterval = 0.1f;
|
||||||
const float MinAngle = 2f; // Grad - kleiner ist Zittern
|
const float MinAngle = 2f; // Grad - kleiner ist Zittern
|
||||||
const float NearRange = 4f; // so nah muss man an einer Tür stehen, um sie zu bewegen
|
const float NearRange = 4f; // so nah muss man an einer Tür stehen, um sie zu bewegen
|
||||||
const float ApplyHold = 1.5f; // so lange nach einer Nachricht die Feder halten und selbst nicht senden
|
const float ApplyHold = 3f; // so lange nach einer Nachricht die Feder halten und selbst nicht senden
|
||||||
|
// Steht das Fahrzeug, schickt nur, wer die Tür gerade selbst bewegt hat (Klick in der Nähe). Vorher schickte
|
||||||
|
// jeder in der Nähe: setzte sich die Tür beim anderen nach dem Übernehmen etwas anders, schickte er zurück -
|
||||||
|
// die Türen schaukelten hin und her.
|
||||||
|
const float InteractRange = 2.5f;
|
||||||
|
const float InteractAuthority = 4f; // so lange nach dem Klick (Tür schwingt aus)
|
||||||
const float SpringForce = 120f;
|
const float SpringForce = 120f;
|
||||||
const float SpringDamper = 12f;
|
const float SpringDamper = 12f;
|
||||||
|
|
||||||
@@ -34,6 +39,7 @@ namespace MSCO
|
|||||||
public float TargetAngle;
|
public float TargetAngle;
|
||||||
public float TargetTime = -100f;
|
public float TargetTime = -100f;
|
||||||
public bool Driving; // Feder gerade von uns gesetzt
|
public bool Driving; // Feder gerade von uns gesetzt
|
||||||
|
public float LocalAuthorityUntil = -100f;
|
||||||
public bool OrigUseSpring;
|
public bool OrigUseSpring;
|
||||||
public JointSpring OrigSpring;
|
public JointSpring OrigSpring;
|
||||||
}
|
}
|
||||||
@@ -50,11 +56,12 @@ namespace MSCO
|
|||||||
foreach (Hinge h in hinges.Values) StopDriving(h);
|
foreach (Hinge h in hinges.Values) StopDriving(h);
|
||||||
hinges.Clear();
|
hinges.Clear();
|
||||||
seen.Clear();
|
seen.Clear();
|
||||||
nextScan = 0f;
|
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(ObjectSync objects, int localId, Vector3 playerPos, float dt, Action<MsgType, byte[]> send)
|
public void Update(ObjectSync objects, int localId, Vector3 playerPos, float dt, Action<MsgType, byte[]> send)
|
||||||
{
|
{
|
||||||
|
if (nextScan < 0f) nextScan = Time.time + 1.7f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
||||||
if (Time.time >= nextScan)
|
if (Time.time >= nextScan)
|
||||||
{
|
{
|
||||||
nextScan = Time.time + ScanInterval;
|
nextScan = Time.time + ScanInterval;
|
||||||
@@ -68,10 +75,13 @@ namespace MSCO
|
|||||||
foreach (Hinge h in hinges.Values)
|
foreach (Hinge h in hinges.Values)
|
||||||
{
|
{
|
||||||
if (h.Joint == null || h.Rb == null || !h.Rb.gameObject.activeInHierarchy) continue;
|
if (h.Joint == null || h.Rb == null || !h.Rb.gameObject.activeInHierarchy) continue;
|
||||||
if (Time.time - h.TargetTime < ApplyHold) continue; // gerade von anderen gesetzt
|
bool mine = Time.time < h.LocalAuthorityUntil;
|
||||||
|
if (!mine && Time.time - h.TargetTime < ApplyHold) continue; // gerade von anderen gesetzt
|
||||||
int owner = objects.VehicleOwner(h.Root);
|
int owner = objects.VehicleOwner(h.Root);
|
||||||
if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt
|
// Fährt jemand: der Fahrer schickt (Türen schwingen während der Fahrt). Hat man selbst gerade an der Tür
|
||||||
if (owner == SyncedBody.NoOwner && (h.Rb.position - playerPos).sqrMagnitude > NearRange * NearRange) continue;
|
// geklickt, schickt man auch - sonst würde die Feder sie beim Fahrer gleich wieder zudrücken.
|
||||||
|
if (!mine && owner != SyncedBody.NoOwner && owner != localId) continue;
|
||||||
|
if (!mine && owner == SyncedBody.NoOwner) { h.SentAngle = SafeAngle(h, h.SentAngle); continue; } // stehendes Auto: nur wer klickt
|
||||||
|
|
||||||
float angle = h.Joint.angle;
|
float angle = h.Joint.angle;
|
||||||
if (float.IsNaN(angle) || float.IsInfinity(angle)) continue;
|
if (float.IsNaN(angle) || float.IsInfinity(angle)) continue;
|
||||||
@@ -89,6 +99,24 @@ namespace MSCO
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Jedes Frame: Klick (linke Maustaste / Benutzen) in der Nähe einer Tür = wir bewegen sie gerade.</summary>
|
||||||
|
public void NoteInteraction(Vector3 playerPos)
|
||||||
|
{
|
||||||
|
if (!Input.GetMouseButtonDown(0) && !Input.GetKeyDown(KeyCode.F)) return;
|
||||||
|
foreach (Hinge h in hinges.Values)
|
||||||
|
{
|
||||||
|
if (h.Rb == null || (h.Rb.worldCenterOfMass - playerPos).sqrMagnitude > InteractRange * InteractRange) continue;
|
||||||
|
h.LocalAuthorityUntil = Time.time + InteractAuthority;
|
||||||
|
h.TargetTime = -100f; // eigene Bewegung: keine Feder mehr auf den alten Winkel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static float SafeAngle(Hinge h, float fallback)
|
||||||
|
{
|
||||||
|
float a = h.Joint.angle;
|
||||||
|
return float.IsNaN(a) || float.IsInfinity(a) ? fallback : a;
|
||||||
|
}
|
||||||
|
|
||||||
public void Handle(BinaryReader r)
|
public void Handle(BinaryReader r)
|
||||||
{
|
{
|
||||||
int count = r.ReadInt32();
|
int count = r.ReadInt32();
|
||||||
@@ -99,6 +127,7 @@ namespace MSCO
|
|||||||
Hinge h;
|
Hinge h;
|
||||||
if (!hinges.TryGetValue(key, out h)) { nextScan = 0f; continue; }
|
if (!hinges.TryGetValue(key, out h)) { nextScan = 0f; continue; }
|
||||||
if (float.IsNaN(angle) || float.IsInfinity(angle) || Mathf.Abs(angle) > 180f) continue;
|
if (float.IsNaN(angle) || float.IsInfinity(angle) || Mathf.Abs(angle) > 180f) continue;
|
||||||
|
if (Time.time < h.LocalAuthorityUntil) continue; // wir bewegen sie gerade selbst
|
||||||
h.TargetAngle = angle;
|
h.TargetAngle = angle;
|
||||||
h.TargetTime = Time.time;
|
h.TargetTime = Time.time;
|
||||||
h.SentAngle = angle; // nicht als eigene Bewegung zurückschicken
|
h.SentAngle = angle; // nicht als eigene Bewegung zurückschicken
|
||||||
|
|||||||
+374
-224
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,30 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
public class MSCO : Mod
|
public class MSCO : Mod
|
||||||
{
|
{
|
||||||
public const string ModVersion = "2.8";
|
/// <summary>
|
||||||
|
/// Version der Mod (MSCLoader / MSC Launcher). Kleine Updates: nur die dritte Stelle erhöhen (2.8.1, 2.8.2 ...) -
|
||||||
|
/// sie bleiben mit 2.8-Servern und -Mitspielern kompatibel.
|
||||||
|
/// </summary>
|
||||||
|
public const string ModVersion = "2.8.1";
|
||||||
|
/// <summary>
|
||||||
|
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
|
||||||
|
/// auch GameServer.ModVersion im Server und eine neue Mod-Version 2.9.0 o.ä.
|
||||||
|
/// </summary>
|
||||||
|
public const string ProtocolVersion = "2.8";
|
||||||
|
|
||||||
|
/// <summary>Passt eine andere Version (Server, Host, Mitspieler) zu uns? Verglichen werden nur die ersten zwei Stellen.</summary>
|
||||||
|
public static bool Compatible(string other)
|
||||||
|
{
|
||||||
|
return Protocol(other) == ProtocolVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>"2.8.3" -> "2.8"</summary>
|
||||||
|
public static string Protocol(string version)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(version)) return "";
|
||||||
|
string[] parts = version.Trim().Split('.');
|
||||||
|
return parts.Length >= 2 ? parts[0] + "." + parts[1] : version.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
<Compile Include="NetSession.cs" />
|
<Compile Include="NetSession.cs" />
|
||||||
<Compile Include="ObjectSync.cs" />
|
<Compile Include="ObjectSync.cs" />
|
||||||
<Compile Include="Passenger.cs" />
|
<Compile Include="Passenger.cs" />
|
||||||
|
<Compile Include="GameCharacter.cs" />
|
||||||
<Compile Include="SaveSync.cs" />
|
<Compile Include="SaveSync.cs" />
|
||||||
<Compile Include="ServerBrowser.cs" />
|
<Compile Include="ServerBrowser.cs" />
|
||||||
<Compile Include="SteamLobby.cs" />
|
<Compile Include="SteamLobby.cs" />
|
||||||
|
|||||||
@@ -72,6 +72,33 @@ namespace MSCO
|
|||||||
return hidden;
|
return hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Geräusche und Musik des Spiel-Hauptmenüs stummschalten - der MSCO-Launcher deckt das ganze Menü ab.
|
||||||
|
/// Gibt die Anzahl der stummgeschalteten Quellen zurück.
|
||||||
|
/// </summary>
|
||||||
|
public static int MuteMenuAudio()
|
||||||
|
{
|
||||||
|
int muted = 0;
|
||||||
|
foreach (Object o in Object.FindObjectsOfType(typeof(AudioSource)))
|
||||||
|
{
|
||||||
|
var src = (AudioSource)o;
|
||||||
|
if (src == null || src.mute || src.transform.root.name.StartsWith("MSCO")) continue;
|
||||||
|
src.mute = true;
|
||||||
|
mutedByUs.Add(src);
|
||||||
|
muted++;
|
||||||
|
}
|
||||||
|
return muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly System.Collections.Generic.List<AudioSource> mutedByUs = new System.Collections.Generic.List<AudioSource>();
|
||||||
|
|
||||||
|
/// <summary>Launcher eingeklappt: die von uns stummgeschalteten Menü-Geräusche wieder an.</summary>
|
||||||
|
public static void UnmuteMenuAudio()
|
||||||
|
{
|
||||||
|
foreach (AudioSource src in mutedByUs) if (src != null) src.mute = false;
|
||||||
|
mutedByUs.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
public static void StartGame()
|
public static void StartGame()
|
||||||
{
|
{
|
||||||
PlayMakerFSM fsm;
|
PlayMakerFSM fsm;
|
||||||
@@ -88,6 +115,53 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Neues Spiel starten (der Spielstand wurde vorher geleert): über die Logik des NEW-GAME-Knopfs des Spiels,
|
||||||
|
/// sonst wie CONTINUE - ohne Spielstand legt das Spiel dann eine frische Welt an.
|
||||||
|
/// </summary>
|
||||||
|
public static void StartNewGame()
|
||||||
|
{
|
||||||
|
PlayMakerFSM fsm;
|
||||||
|
FsmState state;
|
||||||
|
if (switchState != null && FindLoadState("newgame", out fsm, out state))
|
||||||
|
{
|
||||||
|
MscoLog.Info("[MSCO] Starting a new game via NEW GAME (" + fsm.name + " / " + state.Name + ")");
|
||||||
|
switchState.Invoke(fsm.Fsm, new object[] { state });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MscoLog.Info("[MSCO] NEW GAME button not found - starting with an empty save");
|
||||||
|
StartGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>FSM unter einem Objekt, dessen Name "hint" enthält, mit einem Zustand, der eine Szene lädt.</summary>
|
||||||
|
static bool FindLoadState(string hint, out PlayMakerFSM fsm, out FsmState state)
|
||||||
|
{
|
||||||
|
fsm = null;
|
||||||
|
state = null;
|
||||||
|
// Nur aktive Objekte der Menü-Szene (die Spiel-Knöpfe sind nur unsichtbar, nicht abgeschaltet)
|
||||||
|
foreach (Object o in Object.FindObjectsOfType(typeof(PlayMakerFSM)))
|
||||||
|
{
|
||||||
|
var candidate = (PlayMakerFSM)o;
|
||||||
|
if (!HierarchyContains(candidate.transform, hint)) continue;
|
||||||
|
foreach (FsmState s in candidate.FsmStates)
|
||||||
|
{
|
||||||
|
if (!s.ActionsLoaded) s.LoadActions();
|
||||||
|
if (s.Actions == null) continue;
|
||||||
|
foreach (FsmStateAction action in s.Actions)
|
||||||
|
{
|
||||||
|
var load = action as LoadLevel;
|
||||||
|
if (load != null && load.levelName != null && !string.IsNullOrEmpty(load.levelName.Value))
|
||||||
|
{
|
||||||
|
fsm = candidate;
|
||||||
|
state = s;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static bool HierarchyContains(Transform t, string text)
|
static bool HierarchyContains(Transform t, string text)
|
||||||
{
|
{
|
||||||
for (; t != null; t = t.parent)
|
for (; t != null; t = t.parent)
|
||||||
|
|||||||
+310
-18
@@ -65,6 +65,8 @@ namespace MSCO
|
|||||||
readonly TrafficSync traffic = new TrafficSync();
|
readonly TrafficSync traffic = new TrafficSync();
|
||||||
readonly HingeSync hinges = new HingeSync();
|
readonly HingeSync hinges = new HingeSync();
|
||||||
readonly ControlSync controls = new ControlSync();
|
readonly ControlSync controls = new ControlSync();
|
||||||
|
bool lobbyWasMinimized;
|
||||||
|
float nextControlResync;
|
||||||
readonly Toasts toasts = new Toasts();
|
readonly Toasts toasts = new Toasts();
|
||||||
float nextNeedsSend;
|
float nextNeedsSend;
|
||||||
const float NeedsInterval = 10f;
|
const float NeedsInterval = 10f;
|
||||||
@@ -183,6 +185,7 @@ namespace MSCO
|
|||||||
public void Host(int port)
|
public void Host(int port)
|
||||||
{
|
{
|
||||||
Disconnect();
|
Disconnect();
|
||||||
|
PartStart();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
session = NetSession.StartHost(port);
|
session = NetSession.StartHost(port);
|
||||||
@@ -210,6 +213,15 @@ namespace MSCO
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Konsole "model": Figuren neu bauen (kommen mit dem nächsten Spielerzustand wieder).</summary>
|
||||||
|
public void RebuildAvatars()
|
||||||
|
{
|
||||||
|
foreach (PlayerInfo p in players.Values) DestroyAvatar(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Konsole "vars <filter>": synchronisierte Fahrzeugwerte (Diagnose).</summary>
|
||||||
|
public List<string> DescribeVehicleValues(string filter) { return vehicleState.Describe(filter, 60); }
|
||||||
|
|
||||||
public List<string> DescribeVehicles()
|
public List<string> DescribeVehicles()
|
||||||
{
|
{
|
||||||
var lines = objects.DescribeVehicles();
|
var lines = objects.DescribeVehicles();
|
||||||
@@ -279,8 +291,84 @@ namespace MSCO
|
|||||||
/// <summary>Ping-Anzeige oben rechts (Konsole: "ping"), bleibt gespeichert.</summary>
|
/// <summary>Ping-Anzeige oben rechts (Konsole: "ping"), bleibt gespeichert.</summary>
|
||||||
public bool ShowPing
|
public bool ShowPing
|
||||||
{
|
{
|
||||||
get { return PlayerPrefs.GetInt(PrefShowPing, 0) == 1; }
|
get
|
||||||
set { PlayerPrefs.SetInt(PrefShowPing, value ? 1 : 0); }
|
{
|
||||||
|
if (showPing < 0) showPing = PlayerPrefs.GetInt(PrefShowPing, 0); // einmal lesen, nicht bei jedem Zeichnen
|
||||||
|
return showPing == 1;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
showPing = value ? 1 : 0;
|
||||||
|
PlayerPrefs.SetInt(PrefShowPing, showPing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int showPing = -1, showFps = -1;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- FPS-Anzeige (Konsole: "fps")
|
||||||
|
const string PrefShowFps = "MSCO_ShowFps";
|
||||||
|
float fpsTimer, fpsValue = -1f, frameMsWorst;
|
||||||
|
int fpsFrames;
|
||||||
|
float frameMsWorstWindow;
|
||||||
|
|
||||||
|
/// <summary>FPS-Anzeige oben rechts, bleibt gespeichert.</summary>
|
||||||
|
public bool ShowFps
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (showFps < 0) showFps = PlayerPrefs.GetInt(PrefShowFps, 0);
|
||||||
|
return showFps == 1;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
showFps = value ? 1 : 0;
|
||||||
|
PlayerPrefs.SetInt(PrefShowFps, showFps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bilder pro Sekunde (Mittel über eine halbe Sekunde) und längstes Bild darin (Ruckler).</summary>
|
||||||
|
public string FpsText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (fpsValue < 0f) return "measuring...";
|
||||||
|
return Mathf.RoundToInt(fpsValue) + " FPS (" + (1000f / Mathf.Max(1f, fpsValue)).ToString("0.0") + " ms, worst " +
|
||||||
|
frameMsWorst.ToString("0") + " ms)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeasureFps()
|
||||||
|
{
|
||||||
|
float dt = Time.unscaledDeltaTime;
|
||||||
|
fpsFrames++;
|
||||||
|
fpsTimer += dt;
|
||||||
|
frameMsWorstWindow = Mathf.Max(frameMsWorstWindow, dt * 1000f);
|
||||||
|
if (fpsTimer < 0.5f) return;
|
||||||
|
fpsValue = fpsFrames / fpsTimer;
|
||||||
|
frameMsWorst = frameMsWorstWindow;
|
||||||
|
fpsFrames = 0;
|
||||||
|
fpsTimer = 0f;
|
||||||
|
frameMsWorstWindow = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
GUIStyle fpsStyle;
|
||||||
|
|
||||||
|
/// <summary>Oben rechts, links neben dem Ping.</summary>
|
||||||
|
void DrawFps(float rightEdge)
|
||||||
|
{
|
||||||
|
if (!ShowFps) return;
|
||||||
|
if (fpsStyle == null)
|
||||||
|
{
|
||||||
|
fpsStyle = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.85f), 6);
|
||||||
|
fpsStyle.fontSize = 12;
|
||||||
|
fpsStyle.fontStyle = FontStyle.Bold;
|
||||||
|
fpsStyle.alignment = TextAnchor.MiddleCenter;
|
||||||
|
fpsStyle.padding = new RectOffset(10, 10, 0, 0);
|
||||||
|
}
|
||||||
|
// grün ab 50, gelb ab 30, sonst rot
|
||||||
|
fpsStyle.normal.textColor = fpsValue < 0f ? UiTheme.Muted : fpsValue >= 50f ? UiTheme.Online : fpsValue >= 30f ? UiTheme.Pending : UiTheme.Danger;
|
||||||
|
var content = new GUIContent(fpsValue < 0f ? "FPS ..." : "FPS " + Mathf.RoundToInt(fpsValue));
|
||||||
|
float width = fpsStyle.CalcSize(content).x;
|
||||||
|
GUI.Box(new Rect(rightEdge - width, 8, width, 24), content, fpsStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Text für Anzeige und Konsole.</summary>
|
/// <summary>Text für Anzeige und Konsole.</summary>
|
||||||
@@ -311,7 +399,9 @@ namespace MSCO
|
|||||||
var content = new GUIContent("PING " + PingText);
|
var content = new GUIContent("PING " + PingText);
|
||||||
float width = pingStyle.CalcSize(content).x;
|
float width = pingStyle.CalcSize(content).x;
|
||||||
GUI.Box(new Rect(Screen.width - width - 12, 8, width, 24), content, pingStyle);
|
GUI.Box(new Rect(Screen.width - width - 12, 8, width, 24), content, pingStyle);
|
||||||
|
pingWidth = width + 8;
|
||||||
}
|
}
|
||||||
|
float pingWidth; // Platz, den die Ping-Anzeige belegt (FPS steht links daneben)
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Cheats (Admin-Menü)
|
// ---------------------------------------------------------------- Cheats (Admin-Menü)
|
||||||
|
|
||||||
@@ -471,6 +561,18 @@ namespace MSCO
|
|||||||
SendGame(MsgType.Chat, NetSession.Build(w => w.Write("* " + text)));
|
SendGame(MsgType.Chat, NetSession.Build(w => w.Write("* " + text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Admin (dedizierter Server): einem Spieler Admin-Rechte geben (9) bzw. nehmen (10).</summary>
|
||||||
|
public void SetAdmin(int playerId, bool admin)
|
||||||
|
{
|
||||||
|
if (session == null || !dedicated) return;
|
||||||
|
session.SendTo(NetSession.HostId, MsgType.AdminAction, NetSession.Build(w =>
|
||||||
|
{
|
||||||
|
w.Write((byte)(admin ? 9 : 10));
|
||||||
|
w.Write(playerId);
|
||||||
|
w.Write("");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -585,7 +687,7 @@ namespace MSCO
|
|||||||
string name = string.IsNullOrEmpty(displayName) ? info.Name : displayName;
|
string name = string.IsNullOrEmpty(displayName) ? info.Name : displayName;
|
||||||
int max = info.MaxPlayers > 0 ? info.MaxPlayers : maxPlayers;
|
int max = info.MaxPlayers > 0 ? info.MaxPlayers : maxPlayers;
|
||||||
|
|
||||||
if (info.Version != MSCO.ModVersion)
|
if (!MSCO.Compatible(info.Version))
|
||||||
{
|
{
|
||||||
PendingVersionWarning = new VersionWarning { Host = host, Port = port, Name = name, MaxPlayers = max, ServerVersion = info.Version };
|
PendingVersionWarning = new VersionWarning { Host = host, Port = port, Name = name, MaxPlayers = max, ServerVersion = info.Version };
|
||||||
SetStatus("Version mismatch: server v" + info.Version + ", you v" + MSCO.ModVersion);
|
SetStatus("Version mismatch: server v" + info.Version + ", you v" + MSCO.ModVersion);
|
||||||
@@ -790,11 +892,57 @@ namespace MSCO
|
|||||||
public void StartHostGame()
|
public void StartHostGame()
|
||||||
{
|
{
|
||||||
if (!IsHost || !MenuHelper.InMenu) return;
|
if (!IsHost || !MenuHelper.InMenu) return;
|
||||||
|
newGamePending = false;
|
||||||
SendSaveToWaitingPlayers();
|
SendSaveToWaitingPlayers();
|
||||||
SetStatus("Starting game...");
|
SetStatus("Starting game...");
|
||||||
MenuHelper.StartGame();
|
MenuHelper.StartGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host hat ein neues Spiel gestartet: es gibt noch keinen Spielstand zum Verschicken - erst im Spiel speichern
|
||||||
|
bool newGamePending;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host: neues Spiel statt des eigenen Spielstands. Der bisherige Spielstand wird gesichert (MSCO_Saves), die
|
||||||
|
/// Welt frisch angelegt; die Mitspieler bekommen sie, sobald sie einmal gespeichert ist.
|
||||||
|
/// </summary>
|
||||||
|
public void StartHostNewGame()
|
||||||
|
{
|
||||||
|
if (!IsHost || !MenuHelper.InMenu) return;
|
||||||
|
string archived;
|
||||||
|
try { archived = SaveSync.ArchiveForNewGame(); }
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
// Lieber gar nicht starten, als den Spielstand ungesichert zu löschen
|
||||||
|
MscoLog.Error("[MSCO] Could not back up your save game - new game cancelled: " + e.Message);
|
||||||
|
SetStatus("Could not back up your save game - new game cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newGamePending = true;
|
||||||
|
if (archived != null) overlays.Log("Your previous save game was backed up to " + archived);
|
||||||
|
SetStatus("Starting a new game...");
|
||||||
|
MenuHelper.StartNewGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Neues Spiel geladen: warten, bis die Welt steht, speichern und an die wartenden Mitspieler schicken.</summary>
|
||||||
|
System.Collections.IEnumerator NewGameSaveAndSend()
|
||||||
|
{
|
||||||
|
float give = Time.realtimeSinceStartup + 60f;
|
||||||
|
while (localPlayer == null && Time.realtimeSinceStartup < give) yield return new WaitForSeconds(0.5f);
|
||||||
|
yield return new WaitForSeconds(3f); // Spielwelt erst fertig aufbauen lassen
|
||||||
|
if (!newGamePending || session == null || !session.IsHost || !MenuHelper.InGame) yield break;
|
||||||
|
SaveGame();
|
||||||
|
yield return new WaitForSeconds(LateJoinSaveDelay);
|
||||||
|
newGamePending = false;
|
||||||
|
var targets = new List<PlayerInfo>();
|
||||||
|
foreach (PlayerInfo p in players.Values)
|
||||||
|
if (!p.IsLocal && !p.SaveSent) targets.Add(p);
|
||||||
|
if (targets.Count > 0)
|
||||||
|
{
|
||||||
|
SendSave(targets, true);
|
||||||
|
SetStatus("New game sent to " + targets.Count + " player(s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Speichert wie die Toilette: das Spiel schickt allen FSMs das globale Event "SAVEGAME".
|
/// Speichert wie die Toilette: das Spiel schickt allen FSMs das globale Event "SAVEGAME".
|
||||||
/// Nur der Host speichert, Clients haben nur eine Kopie seines Spielstands.
|
/// Nur der Host speichert, Clients haben nur eine Kopie seines Spielstands.
|
||||||
@@ -817,6 +965,7 @@ namespace MSCO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
savingMessageUntil = Time.realtimeSinceStartup + 2.5f; // "SAVING GAME..." in der Bildschirmmitte
|
savingMessageUntil = Time.realtimeSinceStartup + 2.5f; // "SAVING GAME..." in der Bildschirmmitte
|
||||||
|
lastSaveTriggered = Time.realtimeSinceStartup;
|
||||||
PlayMakerFSM.BroadcastEvent("SAVEGAME");
|
PlayMakerFSM.BroadcastEvent("SAVEGAME");
|
||||||
MscoLog.Info("[MSCO] Game saved.");
|
MscoLog.Info("[MSCO] Game saved.");
|
||||||
}
|
}
|
||||||
@@ -825,6 +974,8 @@ namespace MSCO
|
|||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
|
CheckHitch();
|
||||||
|
MeasureFps();
|
||||||
// Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog)
|
// Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog)
|
||||||
// Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht;
|
// Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht;
|
||||||
// erst nach MINIMIZE wieder. Über der eingeklappten Leiste ebenfalls nicht (sonst öffnet sich z.B. der Changelog).
|
// erst nach MINIMIZE wieder. Über der eingeklappten Leiste ebenfalls nicht (sonst öffnet sich z.B. der Changelog).
|
||||||
@@ -834,6 +985,13 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
nextMenuButtonCheck = Time.realtimeSinceStartup + 0.5f;
|
nextMenuButtonCheck = Time.realtimeSinceStartup + 0.5f;
|
||||||
menuButtonsHidden = MenuHelper.HideGameMenuButtons() > 0;
|
menuButtonsHidden = MenuHelper.HideGameMenuButtons() > 0;
|
||||||
|
if (!lobby.Minimized) MenuHelper.MuteMenuAudio(); // Launcher im Vollbild: Menü-Musik/-Geräusche aus
|
||||||
|
}
|
||||||
|
// Launcher eingeklappt: Menü-Musik wieder an; aufgeklappt: wieder aus
|
||||||
|
if (MenuHelper.InMenu && lobby.Minimized != lobbyWasMinimized)
|
||||||
|
{
|
||||||
|
lobbyWasMinimized = lobby.Minimized;
|
||||||
|
if (lobby.Minimized) MenuHelper.UnmuteMenuAudio(); else MenuHelper.MuteMenuAudio();
|
||||||
}
|
}
|
||||||
UpdateOverlayKeys();
|
UpdateOverlayKeys();
|
||||||
PersistPendingState(); // Kick-Grund / Neu-Verbinden über das Neuladen der Mods hinweg merken
|
PersistPendingState(); // Kick-Grund / Neu-Verbinden über das Neuladen der Mods hinweg merken
|
||||||
@@ -918,7 +1076,7 @@ namespace MSCO
|
|||||||
byte[] ticket = SteamLobby.CreateAuthTicket();
|
byte[] ticket = SteamLobby.CreateAuthTicket();
|
||||||
session.SendTo(NetSession.HostId, MsgType.Hello, NetSession.Build(w =>
|
session.SendTo(NetSession.HostId, MsgType.Hello, NetSession.Build(w =>
|
||||||
{
|
{
|
||||||
w.Write(MSCO.ModVersion);
|
w.Write(MSCO.ProtocolVersion); // nur die Netzwerk-Version: ältere Hosts vergleichen genau
|
||||||
w.Write(name);
|
w.Write(name);
|
||||||
// Steam-Identität: dedizierte Server lassen nur Spieler mit Steam rein
|
// Steam-Identität: dedizierte Server lassen nur Spieler mit Steam rein
|
||||||
w.Write(SteamLobby.OwnSteamId);
|
w.Write(SteamLobby.OwnSteamId);
|
||||||
@@ -1246,7 +1404,7 @@ namespace MSCO
|
|||||||
|
|
||||||
string version = r.ReadString();
|
string version = r.ReadString();
|
||||||
string name = r.ReadString();
|
string name = r.ReadString();
|
||||||
if (version != MSCO.ModVersion)
|
if (!MSCO.Compatible(version))
|
||||||
{
|
{
|
||||||
Reject(peerId, "Version mismatch (host " + MSCO.ModVersion + ", you " + version + ")");
|
Reject(peerId, "Version mismatch (host " + MSCO.ModVersion + ", you " + version + ")");
|
||||||
return;
|
return;
|
||||||
@@ -1450,6 +1608,15 @@ namespace MSCO
|
|||||||
receivingSave = false;
|
receivingSave = false;
|
||||||
// Später beigetreten: nach dem Laden neben den Host setzen
|
// Später beigetreten: nach dem Laden neben den Host setzen
|
||||||
teleportToHost = r.BaseStream.Length > 0 && r.ReadBoolean();
|
teleportToHost = r.BaseStream.Length > 0 && r.ReadBoolean();
|
||||||
|
// Server mit "neues Spiel" (Panel): kein Spielstand - hier ein neues Spiel starten (Server ab 2.9.3)
|
||||||
|
bool serverNewGame = r.BaseStream.Length - r.BaseStream.Position >= 1 && r.ReadBoolean();
|
||||||
|
if (serverNewGame)
|
||||||
|
{
|
||||||
|
SetStatus("The server starts a new game - creating a fresh world...");
|
||||||
|
MscoLog.Info("[MSCO] Server has a new game - starting a fresh world");
|
||||||
|
MenuHelper.StartNewGame();
|
||||||
|
break;
|
||||||
|
}
|
||||||
SetStatus(dedicated ? "Loading server world..." : "Loading host's game...");
|
SetStatus(dedicated ? "Loading server world..." : "Loading host's game...");
|
||||||
MenuHelper.StartGame();
|
MenuHelper.StartGame();
|
||||||
break;
|
break;
|
||||||
@@ -1582,6 +1749,7 @@ namespace MSCO
|
|||||||
|
|
||||||
void SendSaveToWaitingPlayers()
|
void SendSaveToWaitingPlayers()
|
||||||
{
|
{
|
||||||
|
if (newGamePending) return; // neues Spiel: es gibt noch keinen Spielstand - kommt nach dem ersten Speichern
|
||||||
var targets = new List<PlayerInfo>();
|
var targets = new List<PlayerInfo>();
|
||||||
foreach (PlayerInfo p in players.Values)
|
foreach (PlayerInfo p in players.Values)
|
||||||
if (!p.IsLocal && !p.SaveSent) targets.Add(p);
|
if (!p.IsLocal && !p.SaveSent) targets.Add(p);
|
||||||
@@ -1630,8 +1798,12 @@ namespace MSCO
|
|||||||
nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren
|
nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren
|
||||||
if (MenuHelper.InGame)
|
if (MenuHelper.InGame)
|
||||||
{
|
{
|
||||||
// Falls der CONTINUE-Klick nicht erkannt wurde: jetzt nachholen
|
// Falls der CONTINUE-Klick nicht erkannt wurde: jetzt nachholen. Neues Spiel: erst speichern, dann senden
|
||||||
if (session != null && session.IsHost) SendSaveToWaitingPlayers();
|
if (session != null && session.IsHost)
|
||||||
|
{
|
||||||
|
if (newGamePending) StartCoroutine(NewGameSaveAndSend());
|
||||||
|
else SendSaveToWaitingPlayers();
|
||||||
|
}
|
||||||
passenger.Reset();
|
passenger.Reset();
|
||||||
localPlayer = null;
|
localPlayer = null;
|
||||||
objects.OnGameLoaded();
|
objects.OnGameLoaded();
|
||||||
@@ -1653,6 +1825,7 @@ namespace MSCO
|
|||||||
DestroyAvatar(p);
|
DestroyAvatar(p);
|
||||||
}
|
}
|
||||||
continueFsm = null;
|
continueFsm = null;
|
||||||
|
newGamePending = false;
|
||||||
passenger.Reset();
|
passenger.Reset();
|
||||||
localPlayer = null;
|
localPlayer = null;
|
||||||
objects.Reset();
|
objects.Reset();
|
||||||
@@ -1712,14 +1885,23 @@ namespace MSCO
|
|||||||
StartCoroutine(ApplyWorldSnapshot(snapshot));
|
StartCoroutine(ApplyWorldSnapshot(snapshot));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string passengerResult = null;
|
||||||
if (MSCO.PassengerKey != null && !overlays.AnyOpen && MSCO.PassengerKey.GetKeybindDown())
|
if (MSCO.PassengerKey != null && !overlays.AnyOpen && MSCO.PassengerKey.GetKeybindDown())
|
||||||
|
passengerResult = passenger.Toggle(localPlayer, playerCamera, objects, players);
|
||||||
|
else if (passenger.IsRiding && !overlays.AnyOpen && Input.GetKeyDown(KeyCode.Space))
|
||||||
{
|
{
|
||||||
string result = passenger.Toggle(localPlayer, playerCamera, objects, players);
|
passenger.Exit(localPlayer, playerCamera); // aussteigen wie zu Fuß losspringen
|
||||||
if (result != null)
|
passengerResult = "Got out";
|
||||||
{
|
|
||||||
SetStatus(result);
|
|
||||||
overlays.AddChat("", result, true); // im Spiel sichtbar, nicht nur in der Konsole
|
|
||||||
}
|
}
|
||||||
|
else if (IsPeerReady && MenuHelper.InGame && localPlayer != null && !overlays.AnyOpen)
|
||||||
|
passengerResult = passenger.AutoUpdate(localPlayer, playerCamera, objects, players); // in die Beifahrertür treten
|
||||||
|
// Klick an einer Fahrzeugtür: wir bewegen sie (nur dann wird ihr Winkel geschickt, siehe HingeSync)
|
||||||
|
if (MenuHelper.InGame && localPlayer != null && !overlays.AnyOpen)
|
||||||
|
hinges.NoteInteraction(playerCamera != null ? playerCamera.transform.position : localPlayer.position);
|
||||||
|
if (passengerResult != null)
|
||||||
|
{
|
||||||
|
SetStatus(passengerResult);
|
||||||
|
overlays.AddChat("", passengerResult, true); // im Spiel sichtbar, nicht nur in der Konsole
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ready = IsPeerReady;
|
bool ready = IsPeerReady;
|
||||||
@@ -1731,64 +1913,104 @@ namespace MSCO
|
|||||||
if (sendTimer >= SendInterval)
|
if (sendTimer >= SendInterval)
|
||||||
{
|
{
|
||||||
sendTimer = 0f;
|
sendTimer = 0f;
|
||||||
|
PartStart();
|
||||||
try { SendGame(MsgType.Player, BuildLocalState().Serialize()); }
|
try { SendGame(MsgType.Player, BuildLocalState().Serialize()); }
|
||||||
catch (System.Exception e) { ReportError("player state", e); }
|
catch (System.Exception e) { ReportError("player state", e); }
|
||||||
|
PartEnd("player state");
|
||||||
}
|
}
|
||||||
// Eigene Bedürfnisse (Hunger, Durst ...) pro Spieler auf dem Server sichern
|
// Eigene Bedürfnisse (Hunger, Durst ...) pro Spieler auf dem Server sichern
|
||||||
if (dedicated && worldSynced && Time.realtimeSinceStartup >= nextNeedsSend)
|
if (dedicated && worldSynced && Time.realtimeSinceStartup >= nextNeedsSend)
|
||||||
{
|
{
|
||||||
nextNeedsSend = Time.realtimeSinceStartup + NeedsInterval;
|
nextNeedsSend = Time.realtimeSinceStartup + NeedsInterval;
|
||||||
|
PartStart();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
byte[] needs = PlayerNeeds.Build();
|
byte[] needs = PlayerNeeds.Build();
|
||||||
if (needs != null) session.SendTo(NetSession.HostId, MsgType.PlayerNeeds, needs);
|
if (needs != null) session.SendTo(NetSession.HostId, MsgType.PlayerNeeds, needs);
|
||||||
}
|
}
|
||||||
catch (System.Exception e) { ReportError("player needs", e); }
|
catch (System.Exception e) { ReportError("player needs", e); }
|
||||||
|
PartEnd("player needs");
|
||||||
}
|
}
|
||||||
|
PartStart();
|
||||||
try { objects.Update(localPlayer, Time.deltaTime); }
|
try { objects.Update(localPlayer, Time.deltaTime); }
|
||||||
catch (System.Exception e) { ReportError("object sync", e); }
|
catch (System.Exception e) { ReportError("object sync", e); }
|
||||||
|
PartStart();
|
||||||
|
PartEnd("object sync");
|
||||||
try { vehicleFx.Update(objects, SendGame, Time.deltaTime, IsWorldAuthority); }
|
try { vehicleFx.Update(objects, SendGame, Time.deltaTime, IsWorldAuthority); }
|
||||||
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
||||||
|
PartStart();
|
||||||
|
PartEnd("vehicle lights/sound");
|
||||||
try { worldSync.Update(IsWorldAuthority, SendGame, Time.unscaledDeltaTime); }
|
try { worldSync.Update(IsWorldAuthority, SendGame, Time.unscaledDeltaTime); }
|
||||||
catch (System.Exception e) { ReportError("time/weather sync", e); }
|
catch (System.Exception e) { ReportError("time/weather sync", e); }
|
||||||
|
PartEnd("time/weather sync");
|
||||||
|
|
||||||
// Geld erst beobachten, wenn das Spiel fertig geladen ist
|
// Geld erst beobachten, wenn das Spiel fertig geladen ist
|
||||||
|
PartStart();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (objects.ScanComplete) moneySync.Begin();
|
if (objects.ScanComplete) moneySync.Begin();
|
||||||
moneySync.Update(SendGame);
|
moneySync.Update(SendGame);
|
||||||
}
|
}
|
||||||
catch (System.Exception e) { ReportError("money sync", e); }
|
catch (System.Exception e) { ReportError("money sync", e); }
|
||||||
|
PartEnd("money sync");
|
||||||
|
|
||||||
// Türen, Tore, Hauben (z.B. Garagentor)
|
// Türen, Tore, Hauben (z.B. Garagentor)
|
||||||
|
PartStart();
|
||||||
try { if (objects.ScanComplete) doorSync.Update(SendGame); }
|
try { if (objects.ScanComplete) doorSync.Update(SendGame); }
|
||||||
catch (System.Exception e) { ReportError("door sync", e); }
|
catch (System.Exception e) { ReportError("door sync", e); }
|
||||||
|
PartEnd("door sync");
|
||||||
|
|
||||||
// Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...)
|
// Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...)
|
||||||
|
PartStart();
|
||||||
try { 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); }
|
catch (System.Exception e) { ReportError("vehicle state sync", e); }
|
||||||
|
PartEnd("vehicle state sync");
|
||||||
// Unfallschäden (verbeulte Karosserie)
|
// Unfallschäden (verbeulte Karosserie)
|
||||||
|
PartStart();
|
||||||
try { if (objects.ScanComplete) damageSync.Update(objects, localId, SendGame); }
|
try { if (objects.ScanComplete) damageSync.Update(objects, localId, SendGame); }
|
||||||
catch (System.Exception e) { ReportError("crash damage sync", e); }
|
catch (System.Exception e) { ReportError("crash damage sync", e); }
|
||||||
|
PartEnd("crash damage sync");
|
||||||
// Zündung, Motor, Licht, Choke ... der Fahrzeuge
|
// Zündung, Motor, Licht, Choke ... der Fahrzeuge
|
||||||
try { if (objects.ScanComplete && localPlayer != null) controls.Update(objects, localId, localPlayer.position, SendGame); }
|
PartStart();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (objects.ScanComplete && localPlayer != null) controls.Update(objects, localId, localPlayer.position, SendGame);
|
||||||
|
// P2P: der Host gleicht regelmäßig alle Schalter ab (später Beigetretene, verlorene Nachrichten)
|
||||||
|
if (objects.ScanComplete && IsHost && !dedicated && Time.time >= nextControlResync)
|
||||||
|
{
|
||||||
|
nextControlResync = Time.time + 15f;
|
||||||
|
controls.ResendAll(objects, localId, SendGame);
|
||||||
|
}
|
||||||
|
}
|
||||||
catch (System.Exception e) { ReportError("vehicle controls sync", e); }
|
catch (System.Exception e) { ReportError("vehicle controls sync", e); }
|
||||||
|
PartEnd("vehicle controls sync");
|
||||||
// Türen, Motorhaube, Kofferraum der Fahrzeuge
|
// Türen, Motorhaube, Kofferraum der Fahrzeuge
|
||||||
|
PartStart();
|
||||||
try { if (objects.ScanComplete && localPlayer != null) hinges.Update(objects, localId, localPlayer.position, Time.deltaTime, SendGame); }
|
try { if (objects.ScanComplete && localPlayer != null) hinges.Update(objects, localId, localPlayer.position, Time.deltaTime, SendGame); }
|
||||||
catch (System.Exception e) { ReportError("car door sync", e); }
|
catch (System.Exception e) { ReportError("car door sync", e); }
|
||||||
|
PartEnd("car door sync");
|
||||||
// Jobs, Aufträge, Laden
|
// Jobs, Aufträge, Laden
|
||||||
|
PartStart();
|
||||||
try { if (objects.ScanComplete) worldVars.Update(SendGame); }
|
try { if (objects.ScanComplete) worldVars.Update(SendGame); }
|
||||||
catch (System.Exception e) { ReportError("jobs/shop sync", e); }
|
catch (System.Exception e) { ReportError("jobs/shop sync", e); }
|
||||||
|
PartEnd("jobs/shop sync");
|
||||||
// Verkehr: die Welt-Autorität simuliert, alle anderen fahren nach
|
// Verkehr: die Welt-Autorität simuliert, alle anderen fahren nach
|
||||||
|
PartStart();
|
||||||
try { if (objects.ScanComplete) traffic.Update(IsWorldAuthority, Time.deltaTime, SendGame); }
|
try { if (objects.ScanComplete) traffic.Update(IsWorldAuthority, Time.deltaTime, SendGame); }
|
||||||
catch (System.Exception e) { ReportError("traffic sync", e); }
|
catch (System.Exception e) { ReportError("traffic sync", e); }
|
||||||
|
PartEnd("traffic sync");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (PlayerInfo p in players.Values)
|
foreach (PlayerInfo p in players.Values)
|
||||||
{
|
{
|
||||||
if (p.Avatar == null) continue;
|
if (p.Avatar == null) continue;
|
||||||
|
PartStart();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Sitzt der Mitspieler in einem Fahrzeug, die Figur an unsere Kopie dieses Fahrzeugs hängen
|
||||||
|
int seatId = p.Avatar.LastState.SeatId;
|
||||||
|
SyncedBody seat = seatId != 0 ? objects.Get(seatId) : null;
|
||||||
|
p.Avatar.Seat = seat != null && seat.Rb != null && seat.IsVehicle ? seat.Rb.transform : null;
|
||||||
p.Avatar.Tick(Time.unscaledDeltaTime);
|
p.Avatar.Tick(Time.unscaledDeltaTime);
|
||||||
|
|
||||||
// Gehaltenes Item an die Hand der Figur setzen
|
// Gehaltenes Item an die Hand der Figur setzen
|
||||||
@@ -1798,6 +2020,7 @@ namespace MSCO
|
|||||||
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); }
|
catch (System.Exception e) { ReportError("player figure", e); }
|
||||||
|
PartEnd("player figure");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abgefangene Schraub-/Montage-Aktionen verschicken
|
// Abgefangene Schraub-/Montage-Aktionen verschicken
|
||||||
@@ -1808,12 +2031,52 @@ namespace MSCO
|
|||||||
foreach (FsmEventMsg msg in FsmSync.Outbox) SendGame(MsgType.FsmEvent, msg.Serialize());
|
foreach (FsmEventMsg msg in FsmSync.Outbox) SendGame(MsgType.FsmEvent, msg.Serialize());
|
||||||
}
|
}
|
||||||
catch (System.Exception e) { ReportError("part/bolt sync", e); }
|
catch (System.Exception e) { ReportError("part/bolt sync", e); }
|
||||||
|
PartEnd("part/bolt sync");
|
||||||
FsmSync.Outbox.Clear();
|
FsmSync.Outbox.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly Dictionary<string, float> lastErrorReport = new Dictionary<string, float>();
|
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>
|
/// <summary>Fehler eines Teilsystems melden (höchstens alle 30 s pro Teilsystem, damit das Log nicht vollläuft).</summary>
|
||||||
|
// ---------------------------------------------------------------- Hänger erkennen
|
||||||
|
// Jeder Sync-Teil wird gemessen. Dauert ein Bild sehr lange, steht in der F7-Konsole, ob MSCO schuld war
|
||||||
|
// (und welcher Teil) oder das Spiel selbst (z.B. beim Speichern).
|
||||||
|
readonly System.Diagnostics.Stopwatch partWatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
long partStart;
|
||||||
|
double frameMsco, slowestMs;
|
||||||
|
string slowestPart;
|
||||||
|
float nextHitchReport, lastSaveTriggered = -100f;
|
||||||
|
const float HitchSeconds = 0.2f;
|
||||||
|
|
||||||
|
void PartStart() { partStart = partWatch.ElapsedTicks; }
|
||||||
|
|
||||||
|
void PartEnd(string name)
|
||||||
|
{
|
||||||
|
double ms = (partWatch.ElapsedTicks - partStart) * 1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
||||||
|
frameMsco += ms;
|
||||||
|
if (ms > slowestMs) { slowestMs = ms; slowestPart = name; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Zu Beginn jedes Bilds: war das letzte Bild ein Hänger? Dann Ursache melden (höchstens alle 20 s).</summary>
|
||||||
|
void CheckHitch()
|
||||||
|
{
|
||||||
|
float dt = Time.unscaledDeltaTime;
|
||||||
|
if (dt > HitchSeconds && MenuHelper.InGame && Time.realtimeSinceStartup >= nextHitchReport)
|
||||||
|
{
|
||||||
|
nextHitchReport = Time.realtimeSinceStartup + 20f;
|
||||||
|
int total = Mathf.RoundToInt(dt * 1000f);
|
||||||
|
if (frameMsco > dt * 1000f * 0.4f)
|
||||||
|
MscoLog.Warning("[MSCO] Hitch " + total + " ms - caused by MSCO: " + slowestPart + " " + slowestMs.ToString("0") +
|
||||||
|
" ms (MSCO total " + frameMsco.ToString("0") + " ms)");
|
||||||
|
else
|
||||||
|
MscoLog.Info("[MSCO] Hitch " + total + " ms - not MSCO (MSCO used " + frameMsco.ToString("0") + " ms)" +
|
||||||
|
(Time.realtimeSinceStartup - lastSaveTriggered < 5f ? " - the game was saving" : " - the game itself (loading, saving, physics)"));
|
||||||
|
}
|
||||||
|
frameMsco = 0;
|
||||||
|
slowestMs = 0;
|
||||||
|
slowestPart = null;
|
||||||
|
}
|
||||||
|
|
||||||
void ReportError(string what, System.Exception e)
|
void ReportError(string what, System.Exception e)
|
||||||
{
|
{
|
||||||
float now = Time.realtimeSinceStartup, last;
|
float now = Time.realtimeSinceStartup, last;
|
||||||
@@ -1826,25 +2089,37 @@ namespace MSCO
|
|||||||
void FixedUpdate()
|
void FixedUpdate()
|
||||||
{
|
{
|
||||||
if (!IsPeerReady || !MenuHelper.InGame) return;
|
if (!IsPeerReady || !MenuHelper.InGame) return;
|
||||||
|
PartStart();
|
||||||
try { objects.FixedUpdate(); }
|
try { objects.FixedUpdate(); }
|
||||||
catch (System.Exception e) { ReportError("object movement", e); }
|
catch (System.Exception e) { ReportError("object movement", e); }
|
||||||
|
PartEnd("object movement");
|
||||||
|
PartStart();
|
||||||
try { traffic.FixedUpdate(); }
|
try { traffic.FixedUpdate(); }
|
||||||
catch (System.Exception e) { ReportError("traffic movement", e); }
|
catch (System.Exception e) { ReportError("traffic movement", e); }
|
||||||
|
PartEnd("traffic movement");
|
||||||
|
PartStart();
|
||||||
try { hinges.FixedUpdate(); }
|
try { hinges.FixedUpdate(); }
|
||||||
catch (System.Exception e) { ReportError("car door movement", e); }
|
catch (System.Exception e) { ReportError("car door movement", e); }
|
||||||
|
PartEnd("car door movement");
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
PartStart();
|
||||||
try { if (IsPeerReady && MenuHelper.InGame) vehicleFx.Apply(objects); }
|
try { if (IsPeerReady && MenuHelper.InGame) vehicleFx.Apply(objects); }
|
||||||
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
||||||
|
PartEnd("vehicle lights/sound");
|
||||||
|
PartStart();
|
||||||
try { if (IsPeerReady && MenuHelper.InGame) wheelSpin.Apply(objects, Time.deltaTime); }
|
try { if (IsPeerReady && MenuHelper.InGame) wheelSpin.Apply(objects, Time.deltaTime); }
|
||||||
catch (System.Exception e) { ReportError("wheels", e); }
|
catch (System.Exception e) { ReportError("wheels", e); }
|
||||||
|
PartEnd("wheels");
|
||||||
// Beifahrer nach der Fahrzeugbewegung auf den Sitz setzen
|
// Beifahrer nach der Fahrzeugbewegung auf den Sitz setzen
|
||||||
|
PartStart();
|
||||||
try { 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); }
|
catch (System.Exception e) { ReportError("passenger", e); }
|
||||||
|
PartEnd("passenger");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1954,6 +2229,9 @@ namespace MSCO
|
|||||||
int needs = PlayerNeeds.Apply(r);
|
int needs = PlayerNeeds.Apply(r);
|
||||||
if (needs > 0) MscoLog.Info("[MSCO] Your own hunger/thirst/... restored (" + needs + " values)");
|
if (needs > 0) MscoLog.Info("[MSCO] Your own hunger/thirst/... restored (" + needs + " values)");
|
||||||
}
|
}
|
||||||
|
// Jobs/Laden und Schalter (Licht, Zündung, Motor ...) - Server ab 2.9.2
|
||||||
|
if (r.BaseStream.Length - r.BaseStream.Position >= 4) worldVars.Handle(r);
|
||||||
|
if (r.BaseStream.Length - r.BaseStream.Position >= 4) controls.Handle(r, objects, localId);
|
||||||
|
|
||||||
// Abgebaute Teile und gespawnte Items sind jetzt eigene Objekte: erfassen, dann alle Positionen setzen
|
// Abgebaute Teile und gespawnte Items sind jetzt eigene Objekte: erfassen, dann alle Positionen setzen
|
||||||
yield return null;
|
yield return null;
|
||||||
@@ -1983,6 +2261,7 @@ namespace MSCO
|
|||||||
System.Collections.IEnumerator UploadSave(int requestId)
|
System.Collections.IEnumerator UploadSave(int requestId)
|
||||||
{
|
{
|
||||||
if (!MenuHelper.InGame || !dedicated || session == null) yield break;
|
if (!MenuHelper.InGame || !dedicated || session == null) yield break;
|
||||||
|
lastSaveTriggered = Time.realtimeSinceStartup;
|
||||||
PlayMakerFSM.BroadcastEvent("SAVEGAME");
|
PlayMakerFSM.BroadcastEvent("SAVEGAME");
|
||||||
float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames
|
float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames
|
||||||
while (Time.realtimeSinceStartup < until) yield return null;
|
while (Time.realtimeSinceStartup < until) yield return null;
|
||||||
@@ -2057,9 +2336,22 @@ namespace MSCO
|
|||||||
CamRot = head.rotation,
|
CamRot = head.rotation,
|
||||||
Seated = passenger.IsRiding || objects.IsDriving,
|
Seated = passenger.IsRiding || objects.IsDriving,
|
||||||
FeetY = GroundBelowPlayer(head.position),
|
FeetY = GroundBelowPlayer(head.position),
|
||||||
HeldLocalRot = Quaternion.identity
|
HeldLocalRot = Quaternion.identity,
|
||||||
|
SeatView = Quaternion.identity
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Im Fahrzeug: Kopf und Blick relativ zum Fahrzeug mitschicken
|
||||||
|
SyncedBody seat = passenger.IsRiding ? passenger.Vehicle : objects.Driven;
|
||||||
|
if (seat != null && seat.Rb != null)
|
||||||
|
{
|
||||||
|
Transform vt = seat.Rb.transform;
|
||||||
|
state.SeatId = seat.Id;
|
||||||
|
// Beifahrer: der Sitz ist bekannt - die Weltposition stammt noch vom letzten Frame, das Auto ist seitdem
|
||||||
|
// weitergefahren (bei Tempo ein halber Meter)
|
||||||
|
state.SeatHead = passenger.IsRiding ? passenger.SeatHeadLocal : vt.InverseTransformPoint(head.position);
|
||||||
|
state.SeatView = Quaternion.Inverse(vt.rotation) * head.rotation;
|
||||||
|
}
|
||||||
|
|
||||||
SyncedBody held = objects.Held;
|
SyncedBody held = objects.Held;
|
||||||
if (held != null)
|
if (held != null)
|
||||||
{
|
{
|
||||||
@@ -2077,9 +2369,9 @@ namespace MSCO
|
|||||||
if (!players.TryGetValue(from, out p)) return;
|
if (!players.TryGetValue(from, out p)) return;
|
||||||
if (p.Avatar == null)
|
if (p.Avatar == null)
|
||||||
{
|
{
|
||||||
// Platzhalter-Figur: Kapsel mit Kopf und Hand
|
// Echter MSC-Mensch (oder die eigene MSCO-Figur, wenn keiner passt / "model classic")
|
||||||
p.Avatar = RemotePlayer.Create("MSCO_Player_" + from, PlayerColor(from), state, from);
|
p.Avatar = RemotePlayer.Create("MSCO_Player_" + from, PlayerColor(from), state, from);
|
||||||
MscoLog.Info("[MSCO] " + p.Name + " is in the game");
|
MscoLog.Info("[MSCO] " + p.Name + " is in the game (" + p.Avatar.ModelDescription + ")");
|
||||||
}
|
}
|
||||||
p.Avatar.SetTarget(state);
|
p.Avatar.SetTarget(state);
|
||||||
}
|
}
|
||||||
@@ -2109,7 +2401,7 @@ namespace MSCO
|
|||||||
if (MenuHelper.InMenu)
|
if (MenuHelper.InMenu)
|
||||||
{
|
{
|
||||||
lobby.Draw(this);
|
lobby.Draw(this);
|
||||||
DrawPing(); toasts.Draw();
|
pingWidth = 0f; DrawPing(); DrawFps(Screen.width - 12 - pingWidth); toasts.Draw();
|
||||||
overlays.Draw(this, false);
|
overlays.Draw(this, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2126,7 +2418,7 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (session != null) DrawHud();
|
if (session != null) DrawHud();
|
||||||
DrawPing(); toasts.Draw();
|
pingWidth = 0f; DrawPing(); DrawFps(Screen.width - 12 - pingWidth); toasts.Draw();
|
||||||
overlays.Draw(this, session != null);
|
overlays.Draw(this, session != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-2
@@ -77,6 +77,12 @@ namespace MSCO
|
|||||||
public Quaternion HeldLocalRot; // relativ zur Kamera
|
public Quaternion HeldLocalRot; // relativ zur Kamera
|
||||||
public bool Seated; // fährt oder sitzt als Beifahrer
|
public bool Seated; // fährt oder sitzt als Beifahrer
|
||||||
public float FeetY; // Bodenhöhe unter dem Spieler (für Ducken/Lehnen der Figur)
|
public float FeetY; // Bodenhöhe unter dem Spieler (für Ducken/Lehnen der Figur)
|
||||||
|
// Im Fahrzeug: Kopf und Blick relativ zum Fahrzeug (ID des SyncedBody, 0 = nicht im Fahrzeug). Damit sitzt die
|
||||||
|
// Figur beim anderen fest auf dem Sitz, statt in Weltkoordinaten hinter dem fahrenden Auto herzuhinken.
|
||||||
|
// Steht am Ende der Nachricht: ältere Versionen und der Server lesen es einfach nicht mit.
|
||||||
|
public int SeatId;
|
||||||
|
public Vector3 SeatHead;
|
||||||
|
public Quaternion SeatView;
|
||||||
|
|
||||||
public byte[] Serialize()
|
public byte[] Serialize()
|
||||||
{
|
{
|
||||||
@@ -90,9 +96,15 @@ namespace MSCO
|
|||||||
w.Write(s.Seated);
|
w.Write(s.Seated);
|
||||||
w.Write(s.FeetY);
|
w.Write(s.FeetY);
|
||||||
w.Write(s.HeldId);
|
w.Write(s.HeldId);
|
||||||
if (s.HeldId == 0) return;
|
if (s.HeldId != 0)
|
||||||
|
{
|
||||||
Write(w, s.HeldLocalPos);
|
Write(w, s.HeldLocalPos);
|
||||||
Write(w, s.HeldLocalRot);
|
Write(w, s.HeldLocalRot);
|
||||||
|
}
|
||||||
|
w.Write(s.SeatId);
|
||||||
|
if (s.SeatId == 0) return;
|
||||||
|
Write(w, s.SeatHead);
|
||||||
|
Write(w, s.SeatView);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +119,24 @@ namespace MSCO
|
|||||||
Seated = r.ReadBoolean(),
|
Seated = r.ReadBoolean(),
|
||||||
FeetY = r.ReadSingle(),
|
FeetY = r.ReadSingle(),
|
||||||
HeldId = r.ReadInt32(),
|
HeldId = r.ReadInt32(),
|
||||||
HeldLocalRot = Quaternion.identity
|
HeldLocalRot = Quaternion.identity,
|
||||||
|
SeatView = Quaternion.identity
|
||||||
};
|
};
|
||||||
if (s.HeldId != 0)
|
if (s.HeldId != 0)
|
||||||
{
|
{
|
||||||
s.HeldLocalPos = ReadVector(r);
|
s.HeldLocalPos = ReadVector(r);
|
||||||
s.HeldLocalRot = ReadQuaternion(r);
|
s.HeldLocalRot = ReadQuaternion(r);
|
||||||
}
|
}
|
||||||
|
// Ältere Versionen senden keinen Sitz
|
||||||
|
if (r.BaseStream.Length - r.BaseStream.Position >= 4)
|
||||||
|
{
|
||||||
|
s.SeatId = r.ReadInt32();
|
||||||
|
if (s.SeatId != 0)
|
||||||
|
{
|
||||||
|
s.SeatHead = ReadVector(r);
|
||||||
|
s.SeatView = ReadQuaternion(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+143
-9
@@ -25,9 +25,19 @@ 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
|
||||||
|
public RigidbodyInterpolation WasInterpolation;
|
||||||
|
public bool InterpolationSet; // Fahrzeug eines anderen: bei uns interpoliert (siehe FreezeForRemote)
|
||||||
// Gelenke der angebauten Teile, solange ein anderer das Fahrzeug fährt: unzerbrechlich (vorherige Bruchkraft/-moment)
|
// Gelenke der angebauten Teile, solange ein anderer das Fahrzeug fährt: unzerbrechlich (vorherige Bruchkraft/-moment)
|
||||||
public Dictionary<Joint, Vector2> HeldJoints;
|
public Dictionary<Joint, Vector2> HeldJoints;
|
||||||
public float NextPartScan;
|
public float NextPartScan;
|
||||||
|
// Nach der Rückgabe erst so spät wieder zerbrechlich (bis dahin schwingen die Teile ein)
|
||||||
|
public float JointRestoreAt = float.MaxValue;
|
||||||
|
// Item liegt in diesem Fahrzeug (Kofferraum, Sitz ...): gehört dann dem, der das Fahrzeug fährt
|
||||||
|
public SyncedBody Carrier;
|
||||||
|
public float NextCarrierCheck;
|
||||||
|
// Fahrzeug: Umriss in lokalen Koordinaten (für "liegt im Auto")
|
||||||
|
public Bounds LocalBounds;
|
||||||
|
public bool HasLocalBounds;
|
||||||
public float StillTime;
|
public float 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
|
||||||
// Am Auto angebautes Teil (Gelenk zum Fahrzeug oder unter einem anderen Rigidbody): bewegt sich mit dem Auto,
|
// Am Auto angebautes Teil (Gelenk zum Fahrzeug oder unter einem anderen Rigidbody): bewegt sich mit dem Auto,
|
||||||
@@ -42,7 +52,7 @@ namespace MSCO
|
|||||||
public Vector3 TargetPos;
|
public Vector3 TargetPos;
|
||||||
public Quaternion TargetRot;
|
public Quaternion TargetRot;
|
||||||
public Vector3 TargetVel; // Geschwindigkeit beim Besitzer: damit wird die Position vorausberechnet
|
public Vector3 TargetVel; // Geschwindigkeit beim Besitzer: damit wird die Position vorausberechnet
|
||||||
public float TargetTime; // wann TargetPos ankam
|
public float TargetTime; // wann TargetPos ankam (Physik-Zeit, Time.fixedTime)
|
||||||
public float LastRecvTime;
|
public float LastRecvTime;
|
||||||
public float AvatarHoldTime = -100f; // zuletzt von der Hand eines Mitspielers platziert
|
public float AvatarHoldTime = -100f; // zuletzt von der Hand eines Mitspielers platziert
|
||||||
|
|
||||||
@@ -80,6 +90,12 @@ 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)
|
||||||
|
// Ein gefahrenes Auto NICHT nach 3 s freigeben: hängt das Spiel des Fahrers kurz (z.B. beim Speichern), blieb es
|
||||||
|
// bei den anderen schlagartig stehen und die Teile rissen ab. Trennt der Fahrer, gibt ReleaseAllOf es frei.
|
||||||
|
const float DrivenVehicleTimeout = 30f;
|
||||||
|
const float JointSettleTime = 3f; // so lange nach der Rückgabe bleiben die Gelenke der Teile unzerbrechlich
|
||||||
|
const float CarrierCheckInterval = 0.4f;
|
||||||
|
const float CarrierMinSpeed = 1.5f; // ab dieser Geschwindigkeit nimmt ein Fahrzeug seine Ladung mit
|
||||||
const float RunawaySpeed = 90f; // m/s - schneller fährt in MSC nichts; darüber hat die Physik es weggeschleudert
|
const float 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
|
||||||
@@ -189,6 +205,9 @@ namespace MSCO
|
|||||||
|
|
||||||
public bool IsRemoteOwnedBody(SyncedBody b) { return IsRemoteOwned(b); }
|
public bool IsRemoteOwnedBody(SyncedBody b) { return IsRemoteOwned(b); }
|
||||||
|
|
||||||
|
/// <summary>Alle erfassten Fahrzeuge (Liste wird alle 2 s erneuert, nicht verändern).</summary>
|
||||||
|
public List<SyncedBody> AllVehicles() { return Vehicles(); }
|
||||||
|
|
||||||
/// <summary>Für "mp vehicles": Zustand aller erfassten Fahrzeuge (zur Fehlersuche).</summary>
|
/// <summary>Für "mp vehicles": Zustand aller erfassten Fahrzeuge (zur Fehlersuche).</summary>
|
||||||
public List<string> DescribeVehicles()
|
public List<string> DescribeVehicles()
|
||||||
{
|
{
|
||||||
@@ -209,6 +228,8 @@ namespace MSCO
|
|||||||
|
|
||||||
/// <summary>Fährt der lokale Spieler gerade selbst?</summary>
|
/// <summary>Fährt der lokale Spieler gerade selbst?</summary>
|
||||||
public bool IsDriving { get; private set; }
|
public bool IsDriving { get; private set; }
|
||||||
|
/// <summary>Das Fahrzeug, das der lokale Spieler gerade fährt (sonst null).</summary>
|
||||||
|
public SyncedBody Driven { get; private set; }
|
||||||
|
|
||||||
/// <summary>Fahrzeuge, die gerade ein anderer Spieler aktiv fährt.</summary>
|
/// <summary>Fahrzeuge, die gerade ein anderer Spieler aktiv fährt.</summary>
|
||||||
public List<SyncedBody> RemoteDrivenVehicles()
|
public List<SyncedBody> RemoteDrivenVehicles()
|
||||||
@@ -254,7 +275,10 @@ 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) RestoreKinematic(b);
|
if (IsRemoteOwned(b) && b.Rb != null) RestoreKinematic(b);
|
||||||
|
RestoreJoints(b);
|
||||||
|
}
|
||||||
byId.Clear();
|
byId.Clear();
|
||||||
byRb.Clear();
|
byRb.Clear();
|
||||||
rejected.Clear();
|
rejected.Clear();
|
||||||
@@ -281,6 +305,7 @@ namespace MSCO
|
|||||||
/// <summary>Besitzer eines Fahrzeugs (nach Wurzelname), NoOwner wenn frei oder unbekannt.</summary>
|
/// <summary>Besitzer eines Fahrzeugs (nach Wurzelname), NoOwner wenn frei oder unbekannt.</summary>
|
||||||
public int VehicleOwner(string rootName)
|
public int VehicleOwner(string rootName)
|
||||||
{
|
{
|
||||||
|
if (rootName == null) return SyncedBody.NoOwner;
|
||||||
foreach (SyncedBody b in byId.Values)
|
foreach (SyncedBody b in byId.Values)
|
||||||
if (b.IsVehicle && b.RootName == rootName) return b.OwnerId;
|
if (b.IsVehicle && b.RootName == rootName) return b.OwnerId;
|
||||||
return SyncedBody.NoOwner;
|
return SyncedBody.NoOwner;
|
||||||
@@ -315,6 +340,7 @@ namespace MSCO
|
|||||||
|
|
||||||
SyncedBody held = HeldBody();
|
SyncedBody held = HeldBody();
|
||||||
SyncedBody driven = DrivenBody();
|
SyncedBody driven = DrivenBody();
|
||||||
|
Driven = driven;
|
||||||
IsDriving = driven != null;
|
IsDriving = driven != null;
|
||||||
bool motionClaimAllowed = Time.time - loadTime > MotionClaimGrace;
|
bool motionClaimAllowed = Time.time - loadTime > MotionClaimGrace;
|
||||||
|
|
||||||
@@ -333,13 +359,25 @@ namespace MSCO
|
|||||||
b.LastPos = b.Rb.position;
|
b.LastPos = b.Rb.position;
|
||||||
if (!b.Rb.gameObject.activeInHierarchy) continue;
|
if (!b.Rb.gameObject.activeInHierarchy) continue;
|
||||||
if (UpdateAttached(b)) continue; // angebaut: bewegt sich mit dem Auto
|
if (UpdateAttached(b)) continue; // angebaut: bewegt sich mit dem Auto
|
||||||
|
if (b.HeldJoints != null && !IsRemoteOwned(b) && Time.time >= b.JointRestoreAt) RestoreJoints(b);
|
||||||
|
|
||||||
bool active = b == held || b == driven;
|
bool active = b == held || b == driven;
|
||||||
if (!IsRemoteOwned(b)) StopRunaway(b);
|
if (!IsRemoteOwned(b)) StopRunaway(b);
|
||||||
|
// Ladung eines fahrenden Autos gehört dessen Fahrer: nur bei ihm wird sie mit dem Auto simuliert.
|
||||||
|
// Übernahm sie ein Mitfahrer, folgte sie beim Fahrer verspätet und kinematisch (unendlich schwer) und
|
||||||
|
// schlug Sitze und Teile ab.
|
||||||
|
SyncedBody carrier = active || b.IsVehicle ? null : CarrierOf(b);
|
||||||
|
bool carrierRemote = carrier != null && IsRemoteOwned(carrier);
|
||||||
|
bool carrierMine = carrier != null && IsLocal(carrier) && IsCarrying(carrier);
|
||||||
if (b.OwnerId == SyncedBody.NoOwner)
|
if (b.OwnerId == SyncedBody.NoOwner)
|
||||||
{
|
{
|
||||||
if (active) Claim(b, true);
|
if (active) Claim(b, true);
|
||||||
else if (motionClaimAllowed && Time.time >= b.NoMotionClaimUntil && IsMoving(b) && IsNear(b, player)) Claim(b, false);
|
else if (carrierMine && IsMoving(b)) Claim(b, false);
|
||||||
|
else if (motionClaimAllowed && Time.time >= b.NoMotionClaimUntil && IsMoving(b) && IsNear(b, player) && !carrierRemote) Claim(b, false);
|
||||||
|
}
|
||||||
|
else if (IsLocal(b) && !active && carrierRemote && carrier.OwnerActive)
|
||||||
|
{
|
||||||
|
Release(b); // fährt ein anderer: der übernimmt die Ladung
|
||||||
}
|
}
|
||||||
else if (IsLocal(b))
|
else if (IsLocal(b))
|
||||||
{
|
{
|
||||||
@@ -362,7 +400,8 @@ namespace MSCO
|
|||||||
// zeigt oft noch auf ein altes Item; trägt es jemand an unserem Kopf vorbei, würden wir es ihm sonst entreißen
|
// zeigt oft noch auf ein altes Item; trägt es jemand an unserem Kopf vorbei, würden wir es ihm sonst entreißen
|
||||||
bool grab = b == driven || !b.OwnerActive || JustPicked;
|
bool grab = b == driven || !b.OwnerActive || JustPicked;
|
||||||
if (active && grab && Wins(LocalId, true, b.OwnerId, b.OwnerActive)) Claim(b, true); // Spieler greift sich das Objekt
|
if (active && grab && Wins(LocalId, true, b.OwnerId, b.OwnerActive)) Claim(b, true); // Spieler greift sich das Objekt
|
||||||
else if (Time.time - b.LastRecvTime > RemoteTimeout) SetFree(b);
|
else if (carrierMine && !b.OwnerActive) Claim(b, false); // liegt in unserem fahrenden Auto
|
||||||
|
else if (Time.time - b.LastRecvTime > (b.IsVehicle && b.OwnerActive ? DrivenVehicleTimeout : RemoteTimeout)) SetFree(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ReportConsumed(player);
|
ReportConsumed(player);
|
||||||
@@ -387,7 +426,9 @@ namespace MSCO
|
|||||||
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
|
||||||
// Dort, wo das Objekt beim Besitzer JETZT ungefähr ist: letzte Position + Geschwindigkeit * Alter.
|
// Dort, wo das Objekt beim Besitzer JETZT ungefähr ist: letzte Position + Geschwindigkeit * Alter.
|
||||||
// Ohne das hinken schnelle Objekte (fahrende Autos) immer einen Paketabstand hinterher und ruckeln.
|
// Ohne das hinken schnelle Objekte (fahrende Autos) immer einen Paketabstand hinterher und ruckeln.
|
||||||
float age = Mathf.Min(Time.time - b.TargetTime, MaxExtrapolation);
|
// Beides in Physik-Zeit: mit der Bildzeit (Time.time beim Empfang) wurde das Alter in den ersten
|
||||||
|
// Physik-Schritten danach negativ - das Auto sprang bei jedem Paket ein Stück zurück (Ruckeln)
|
||||||
|
float age = Mathf.Clamp(Time.fixedTime - b.TargetTime, 0f, MaxExtrapolation);
|
||||||
Vector3 predicted = b.TargetPos + b.TargetVel * age;
|
Vector3 predicted = b.TargetPos + b.TargetVel * age;
|
||||||
if ((b.Rb.position - predicted).sqrMagnitude > SnapDistance * SnapDistance)
|
if ((b.Rb.position - predicted).sqrMagnitude > SnapDistance * SnapDistance)
|
||||||
{
|
{
|
||||||
@@ -489,7 +530,16 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
b.WasKinematic = b.Rb.isKinematic;
|
b.WasKinematic = b.Rb.isKinematic;
|
||||||
b.Rb.isKinematic = true;
|
b.Rb.isKinematic = true;
|
||||||
if (b.IsVehicle) HoldParts(b);
|
if (b.IsVehicle)
|
||||||
|
{
|
||||||
|
b.JointRestoreAt = float.MaxValue;
|
||||||
|
HoldParts(b);
|
||||||
|
// Physik läuft mit 50 Hz, das Bild öfter: ohne Interpolation springt das Auto nur in Physik-Schritten
|
||||||
|
// weiter - wer darin mitfährt, sieht die Welt ruckeln. Nur bei Fahrzeugen (Items setzt die Hand direkt).
|
||||||
|
b.WasInterpolation = b.Rb.interpolation;
|
||||||
|
b.InterpolationSet = true;
|
||||||
|
b.Rb.interpolation = RigidbodyInterpolation.Interpolate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HoldParts(SyncedBody b)
|
void HoldParts(SyncedBody b)
|
||||||
@@ -508,7 +558,27 @@ namespace MSCO
|
|||||||
/// <summary>Wieder selbst simulieren: Auto zurück auf seinen Zustand, Gelenke wieder mit ihrer Bruchkraft.</summary>
|
/// <summary>Wieder selbst simulieren: Auto zurück auf seinen Zustand, Gelenke wieder mit ihrer Bruchkraft.</summary>
|
||||||
void RestoreKinematic(SyncedBody b)
|
void RestoreKinematic(SyncedBody b)
|
||||||
{
|
{
|
||||||
if (b.Rb != null) b.Rb.isKinematic = b.WasKinematic;
|
if (b.Rb != null)
|
||||||
|
{
|
||||||
|
b.Rb.isKinematic = b.WasKinematic;
|
||||||
|
if (b.InterpolationSet) b.Rb.interpolation = b.WasInterpolation;
|
||||||
|
// Mitten in der Fahrt zurückgegeben (Fahrer übergibt, steigt aus ...): mit seiner Geschwindigkeit
|
||||||
|
// weiterrollen statt schlagartig stehenzubleiben - die Teile wollen sonst mit Tempo weiter und reißen ab
|
||||||
|
if (b.IsVehicle && !b.Rb.isKinematic)
|
||||||
|
{
|
||||||
|
b.Rb.velocity = Time.fixedTime - b.TargetTime < 0.5f ? b.TargetVel : Vector3.zero;
|
||||||
|
b.Rb.angularVelocity = Vector3.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.InterpolationSet = false;
|
||||||
|
if (b.HeldJoints == null) return;
|
||||||
|
if (b.IsVehicle) { b.JointRestoreAt = Time.time + JointSettleTime; return; } // erst nach dem Einschwingen
|
||||||
|
RestoreJoints(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RestoreJoints(SyncedBody b)
|
||||||
|
{
|
||||||
|
b.JointRestoreAt = float.MaxValue;
|
||||||
if (b.HeldJoints == null) return;
|
if (b.HeldJoints == null) return;
|
||||||
foreach (KeyValuePair<Joint, Vector2> kv in b.HeldJoints)
|
foreach (KeyValuePair<Joint, Vector2> kv in b.HeldJoints)
|
||||||
{
|
{
|
||||||
@@ -519,6 +589,70 @@ namespace MSCO
|
|||||||
b.HeldJoints = null;
|
b.HeldJoints = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Ladung in Fahrzeugen
|
||||||
|
|
||||||
|
/// <summary>Fährt das Fahrzeug gerade (nimmt also seine Ladung mit)?</summary>
|
||||||
|
bool IsCarrying(SyncedBody v)
|
||||||
|
{
|
||||||
|
if (v.Rb == null) return false;
|
||||||
|
if (IsRemoteOwned(v)) return v.OwnerActive || v.TargetVel.sqrMagnitude > CarrierMinSpeed * CarrierMinSpeed;
|
||||||
|
return v == Driven || (!v.Rb.isKinematic && v.Rb.velocity.sqrMagnitude > CarrierMinSpeed * CarrierMinSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>In welchem Fahrzeug liegt das Item gerade (null = in keinem)? Zwischengespeichert.</summary>
|
||||||
|
SyncedBody CarrierOf(SyncedBody item)
|
||||||
|
{
|
||||||
|
if (Time.time < item.NextCarrierCheck) return item.Carrier != null && item.Carrier.Rb != null ? item.Carrier : null;
|
||||||
|
item.NextCarrierCheck = Time.time + CarrierCheckInterval;
|
||||||
|
item.Carrier = null;
|
||||||
|
Vector3 p = item.Rb.worldCenterOfMass;
|
||||||
|
foreach (SyncedBody v in Vehicles())
|
||||||
|
{
|
||||||
|
if (v.Rb == null || (v.Rb.position - p).sqrMagnitude > 15f * 15f) continue;
|
||||||
|
if (!v.HasLocalBounds) ComputeLocalBounds(v);
|
||||||
|
if (v.LocalBounds.Contains(v.Rb.transform.InverseTransformPoint(p))) { item.Carrier = v; break; }
|
||||||
|
}
|
||||||
|
return item.Carrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly List<SyncedBody> vehicleList = new List<SyncedBody>();
|
||||||
|
float nextVehicleList;
|
||||||
|
|
||||||
|
List<SyncedBody> Vehicles()
|
||||||
|
{
|
||||||
|
if (Time.time < nextVehicleList) return vehicleList;
|
||||||
|
nextVehicleList = Time.time + 2f;
|
||||||
|
vehicleList.Clear();
|
||||||
|
foreach (SyncedBody b in byId.Values) if (b.IsVehicle && b.Rb != null) vehicleList.Add(b);
|
||||||
|
return vehicleList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Umriss des Fahrzeugs (alle festen Collider) in seinen lokalen Koordinaten, etwas nach oben erweitert.</summary>
|
||||||
|
static void ComputeLocalBounds(SyncedBody v)
|
||||||
|
{
|
||||||
|
Transform t = v.Rb.transform;
|
||||||
|
bool any = false;
|
||||||
|
var local = new Bounds();
|
||||||
|
foreach (Collider c in v.Rb.GetComponentsInChildren<Collider>())
|
||||||
|
{
|
||||||
|
if (c == null || c.isTrigger || !c.enabled) continue;
|
||||||
|
Bounds wb = c.bounds;
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
Vector3 corner = wb.center + Vector3.Scale(wb.extents, new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1));
|
||||||
|
Vector3 lp = t.InverseTransformPoint(corner);
|
||||||
|
if (!any) { local = new Bounds(lp, Vector3.zero); any = true; }
|
||||||
|
else local.Encapsulate(lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any) local = new Bounds(Vector3.zero, new Vector3(2f, 1.5f, 4f));
|
||||||
|
// Weltachsen-Umrisse sind bei schrägem Auto zu groß - etwas verkleinern, oben Platz für Ladung lassen
|
||||||
|
local.Expand(new Vector3(-0.2f, 0f, -0.2f));
|
||||||
|
local.max = new Vector3(local.max.x, local.max.y + 0.5f, local.max.z);
|
||||||
|
v.LocalBounds = local;
|
||||||
|
v.HasLocalBounds = true;
|
||||||
|
}
|
||||||
|
|
||||||
void SendOwnedStates()
|
void SendOwnedStates()
|
||||||
{
|
{
|
||||||
sendList.Clear();
|
sendList.Clear();
|
||||||
@@ -580,7 +714,7 @@ namespace MSCO
|
|||||||
b.TargetPos = pos;
|
b.TargetPos = pos;
|
||||||
b.TargetRot = rot;
|
b.TargetRot = rot;
|
||||||
b.TargetVel = vel.sqrMagnitude < 90f * 90f ? vel : Vector3.zero;
|
b.TargetVel = vel.sqrMagnitude < 90f * 90f ? vel : Vector3.zero;
|
||||||
b.TargetTime = Time.time;
|
b.TargetTime = Time.fixedTime;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -880,9 +1014,9 @@ namespace MSCO
|
|||||||
var candidates = new List<KeyValuePair<Rigidbody, bool>>();
|
var candidates = new List<KeyValuePair<Rigidbody, bool>>();
|
||||||
var rootsByName = new Dictionary<string, List<Transform>>();
|
var rootsByName = new Dictionary<string, List<Transform>>();
|
||||||
|
|
||||||
foreach (UnityEngine.Object o in UnityEngine.Object.FindObjectsOfType(typeof(Rigidbody)))
|
foreach (Rigidbody rb in SceneCache.Rigidbodies(1f))
|
||||||
{
|
{
|
||||||
var rb = (Rigidbody)o;
|
if (rb == null) continue;
|
||||||
Transform root = rb.transform.root;
|
Transform root = rb.transform.root;
|
||||||
List<Transform> roots;
|
List<Transform> roots;
|
||||||
if (!rootsByName.TryGetValue(root.name, out roots)) rootsByName[root.name] = roots = new List<Transform>();
|
if (!rootsByName.TryGetValue(root.name, out roots)) rootsByName[root.name] = roots = new List<Transform>();
|
||||||
|
|||||||
+12
-2
@@ -136,6 +136,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
float width = Mathf.Min(Screen.width - 40, 960);
|
float width = Mathf.Min(Screen.width - 40, 960);
|
||||||
var rect = new Rect((Screen.width - width) / 2f, 20, width, Mathf.Min(340, Screen.height * 0.5f));
|
var rect = new Rect((Screen.width - width) / 2f, 20, width, Mathf.Min(340, Screen.height * 0.5f));
|
||||||
|
UiTheme.DrawShadow(rect);
|
||||||
GUI.Box(rect, GUIContent.none, panel);
|
GUI.Box(rect, GUIContent.none, panel);
|
||||||
|
|
||||||
GUI.Label(new Rect(rect.x + 16, rect.y + 10, 300, 20), "MSCO CONSOLE", caption);
|
GUI.Label(new Rect(rect.x + 16, rect.y + 10, 300, 20), "MSCO CONSOLE", caption);
|
||||||
@@ -271,6 +272,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
float width = Mathf.Min(720, Screen.width - 40), height = Mathf.Min(580, Screen.height - 40);
|
float width = Mathf.Min(720, Screen.width - 40), height = Mathf.Min(580, 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);
|
||||||
|
UiTheme.DrawShadow(rect);
|
||||||
GUI.Box(rect, GUIContent.none, panel);
|
GUI.Box(rect, GUIContent.none, panel);
|
||||||
|
|
||||||
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape)
|
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape)
|
||||||
@@ -353,6 +355,14 @@ namespace MSCO
|
|||||||
if (GUILayout.Button("BRING", secondary, GUILayout.Height(30), GUILayout.Width(70))) mp.BringPlayer(p.Id);
|
if (GUILayout.Button("BRING", secondary, GUILayout.Height(30), GUILayout.Width(70))) mp.BringPlayer(p.Id);
|
||||||
GUILayout.Space(12);
|
GUILayout.Space(12);
|
||||||
}
|
}
|
||||||
|
if (mp.IsDedicated)
|
||||||
|
{
|
||||||
|
// Admin geben/nehmen (braucht Steam; der Server meldet, wenn es nicht geht)
|
||||||
|
GUI.enabled = !self;
|
||||||
|
if (GUILayout.Button(p.IsAdmin ? "REMOVE ADMIN" : "MAKE ADMIN", secondary, GUILayout.Height(30), GUILayout.Width(120)))
|
||||||
|
mp.SetAdmin(p.Id, !p.IsAdmin);
|
||||||
|
GUILayout.Space(12);
|
||||||
|
}
|
||||||
GUI.enabled = !self && !p.IsAdmin;
|
GUI.enabled = !self && !p.IsAdmin;
|
||||||
if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(70)))
|
if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(70)))
|
||||||
mp.AdminAction(false, p.Id, adminReason);
|
mp.AdminAction(false, p.Id, adminReason);
|
||||||
@@ -493,7 +503,7 @@ namespace MSCO
|
|||||||
void EnsureStyles()
|
void EnsureStyles()
|
||||||
{
|
{
|
||||||
if (stylesReady && panel.normal.background != null) return;
|
if (stylesReady && panel.normal.background != null) return;
|
||||||
panel = UiTheme.Panel9(UiTheme.Background);
|
panel = UiTheme.Card(UiTheme.Background, 10);
|
||||||
rowStyle = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.72f));
|
rowStyle = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.72f));
|
||||||
rowStyle.padding = new RectOffset(12, 12, 6, 6);
|
rowStyle.padding = new RectOffset(12, 12, 6, 6);
|
||||||
field = UiTheme.Field(14);
|
field = UiTheme.Field(14);
|
||||||
@@ -505,7 +515,7 @@ namespace MSCO
|
|||||||
title = UiTheme.Label(22, UiTheme.Text, FontStyle.Bold);
|
title = UiTheme.Label(22, UiTheme.Text, FontStyle.Bold);
|
||||||
title.wordWrap = false;
|
title.wordWrap = false;
|
||||||
small = UiTheme.Label(11, UiTheme.Muted);
|
small = UiTheme.Label(11, UiTheme.Muted);
|
||||||
primary = UiTheme.Button(UiTheme.Accent, UiTheme.AccentHover, Color.white);
|
primary = UiTheme.PrimaryButton();
|
||||||
secondary = UiTheme.Button(UiTheme.Secondary, UiTheme.SecondaryHover, UiTheme.Text, 12);
|
secondary = UiTheme.Button(UiTheme.Secondary, UiTheme.SecondaryHover, UiTheme.Text, 12);
|
||||||
danger = UiTheme.Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 12);
|
danger = UiTheme.Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 12);
|
||||||
chatName = UiTheme.Label(13, UiTheme.AccentHover, FontStyle.Bold, TextAnchor.MiddleLeft);
|
chatName = UiTheme.Label(13, UiTheme.AccentHover, FontStyle.Bold, TextAnchor.MiddleLeft);
|
||||||
|
|||||||
+180
-27
@@ -4,14 +4,20 @@ using UnityEngine;
|
|||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Beifahrer: Neben einem Fahrzeug, das ein anderer Spieler fährt, per Taste einsteigen.
|
/// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten und
|
||||||
/// Der Sitz ist die Fahrerposition, auf die andere Seite gespiegelt (bei Fahrzeugen mit
|
/// fährt mit - egal ob schon jemand fährt oder das Auto noch steht. Aussteigen mit Leertaste oder der Beifahrer-Taste.
|
||||||
/// Sitz in der Mitte, z.B. Traktor oder Moped: dahinter). Während der Fahrt wird der
|
/// Steht man per Taste neben dem Auto, kommt man auf den Beifahrersitz (Fahrerposition des Spiels, gespiegelt).
|
||||||
/// Spieler jedes Frame auf den Sitz gesetzt und dreht mit dem Fahrzeug mit; umschauen geht weiter.
|
/// Während der Fahrt wird der Spieler jedes Frame an seine Stelle im Fahrzeug gesetzt und dreht mit; umschauen geht.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Passenger
|
public class Passenger
|
||||||
{
|
{
|
||||||
const float EnterRange = 3f; // so nah muss man an der Karosserie stehen
|
const float EnterRange = 3f; // per Taste: so nah muss man an der Karosserie stehen
|
||||||
|
const float AutoEnterRange = 1.0f; // automatisch: Kopf so nah am Beifahrersitz ...
|
||||||
|
const float AutoEnterSide = 0.6f; // ... und seitlich höchstens so weit außerhalb des Sitzes - an der
|
||||||
|
// geschlossenen Tür steht man weiter draußen, nur in der offenen Tür so nah
|
||||||
|
const float AutoEnterDwell = 0.35f; // ... so lange (nicht beim bloßen Vorbeilaufen)
|
||||||
|
const float AutoRearmRange = 2f; // nach dem Aussteigen erst wieder, wenn man so weit weg war
|
||||||
|
const float AutoCheckInterval = 0.15f;
|
||||||
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
|
||||||
@@ -27,8 +33,20 @@ namespace MSCO
|
|||||||
// Feste Collider des Spielers, die während der Fahrt aus sind (sonst stecken sie in Sitzen und Teilen des Autos)
|
// Feste Collider des Spielers, die während der Fahrt aus sind (sonst stecken sie in Sitzen und Teilen des Autos)
|
||||||
readonly List<Collider> disabledColliders = new List<Collider>();
|
readonly List<Collider> disabledColliders = new List<Collider>();
|
||||||
readonly List<Rigidbody> frozenBodies = new List<Rigidbody>();
|
readonly List<Rigidbody> frozenBodies = new List<Rigidbody>();
|
||||||
|
// Fahrerkopf je Fahrzeug (lokal), aus DriverHeadPivot - null = nicht gefunden
|
||||||
|
readonly Dictionary<Rigidbody, Vector3?> driverHeads = new Dictionary<Rigidbody, Vector3?>();
|
||||||
|
|
||||||
|
float nextAutoCheck, dwellStart = -1f, lastDriving = -100f;
|
||||||
|
const float AfterDrivingPause = 4f; // nach dem Fahren (Ein-/Aussteigen am Fahrersitz) nicht automatisch
|
||||||
|
SyncedBody dwellVehicle;
|
||||||
|
bool autoArmed = true;
|
||||||
|
Vector3 lastExitSeat;
|
||||||
|
|
||||||
public bool IsRiding { get { return vehicle != null; } }
|
public bool IsRiding { get { return vehicle != null; } }
|
||||||
|
/// <summary>Fahrzeug, in dem wir mitfahren (sonst null).</summary>
|
||||||
|
public SyncedBody Vehicle { get { return vehicle; } }
|
||||||
|
/// <summary>Kopf auf dem Sitz, relativ zum Fahrzeug (genau, nicht aus der Weltposition des letzten Frames).</summary>
|
||||||
|
public Vector3 SeatHeadLocal { get { return seatHeadLocal; } }
|
||||||
|
|
||||||
/// <summary>Taste gedrückt: einsteigen oder aussteigen. Gibt einen Statustext zurück.</summary>
|
/// <summary>Taste gedrückt: einsteigen oder aussteigen. Gibt einen Statustext zurück.</summary>
|
||||||
public string Toggle(Transform player, Camera cam, ObjectSync objects, IDictionary<int, PlayerInfo> players)
|
public string Toggle(Transform player, Camera cam, ObjectSync objects, IDictionary<int, PlayerInfo> players)
|
||||||
@@ -38,38 +56,158 @@ namespace MSCO
|
|||||||
Exit(player, cam);
|
Exit(player, cam);
|
||||||
return "Got out";
|
return "Got out";
|
||||||
}
|
}
|
||||||
if (cam == null) return null;
|
if (cam == null || objects.IsDriving) return null;
|
||||||
|
SyncedBody inside = VehicleAround(objects, cam.transform.position);
|
||||||
|
if (inside != null)
|
||||||
|
{
|
||||||
|
Enter(inside, inside.Rb.transform.InverseTransformPoint(cam.transform.position), player);
|
||||||
|
return "Riding along (press again or Space to get out)";
|
||||||
|
}
|
||||||
|
|
||||||
SyncedBody best = null;
|
SyncedBody best = null;
|
||||||
RemotePlayer driver = null;
|
Vector3 bestSeat = Vector3.zero;
|
||||||
float bestDist = EnterRange * EnterRange;
|
float bestDist = EnterRange * EnterRange, nearest = float.MaxValue;
|
||||||
List<SyncedBody> driven = objects.RemoteDrivenVehicles();
|
foreach (SyncedBody b in objects.AllVehicles())
|
||||||
float nearest = float.MaxValue;
|
|
||||||
foreach (SyncedBody b in driven)
|
|
||||||
{
|
{
|
||||||
|
if (b.Rb == null || (b.Rb.position - player.position).sqrMagnitude > 30f * 30f) continue;
|
||||||
float d = DistanceToVehicleSq(b, player.position);
|
float d = DistanceToVehicleSq(b, player.position);
|
||||||
|
Vector3? seat = SeatFor(b, objects, players);
|
||||||
|
if (seat == null) continue;
|
||||||
nearest = Mathf.Min(nearest, d);
|
nearest = Mathf.Min(nearest, d);
|
||||||
PlayerInfo owner;
|
if (d < bestDist) { bestDist = d; best = b; bestSeat = seat.Value; }
|
||||||
if (!players.TryGetValue(b.OwnerId, out owner) || owner.Avatar == null) continue;
|
|
||||||
if (d < bestDist) { bestDist = d; best = b; driver = owner.Avatar; }
|
|
||||||
}
|
}
|
||||||
if (best == null)
|
if (best == null)
|
||||||
{
|
{
|
||||||
// Genau sagen, woran es liegt
|
if (nearest == float.MaxValue) return "Passenger: no vehicle nearby";
|
||||||
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)";
|
return "Passenger: get closer to the car (" + Mathf.Sqrt(nearest).ToString("0") + " m away, max " + EnterRange.ToString("0") + " m)";
|
||||||
}
|
}
|
||||||
|
Enter(best, bestSeat, player);
|
||||||
|
return "Riding as passenger (press again or Space to get out)";
|
||||||
|
}
|
||||||
|
|
||||||
Transform vt = best.Rb.transform;
|
/// <summary>
|
||||||
Vector3 driverHead = vt.InverseTransformPoint(driver.HeadPosition);
|
/// Jedes Frame (zu Fuß): ist man mit dem Kopf IM Fahrzeug (Kabine, Ladefläche ...), wird die aktuelle Position im
|
||||||
seatHeadLocal = Mathf.Abs(driverHead.x) < CenterSeatThreshold
|
/// Fahrzeug festgehalten - man fährt genau dort mit, wo man ist (kein Umsetzen auf einen berechneten Sitz).
|
||||||
? driverHead - new Vector3(0f, 0f, BehindCenterSeat) // Sitz in der Mitte: dahinter
|
/// Gibt einen Statustext zurück, wenn man "eingestiegen" ist.
|
||||||
: new Vector3(-driverHead.x, driverHead.y, driverHead.z); // gegenüber vom Fahrer
|
/// </summary>
|
||||||
|
public string AutoUpdate(Transform player, Camera cam, ObjectSync objects, IDictionary<int, PlayerInfo> players)
|
||||||
|
{
|
||||||
|
if (objects.IsDriving) lastDriving = Time.time;
|
||||||
|
if (IsRiding || cam == null || player == null || objects.IsDriving) { dwellStart = -1f; return null; }
|
||||||
|
// Beim Ein-/Aussteigen am Fahrersitz übernimmt das Spiel die Steuerung (CharacterController aus, Spieler an ein
|
||||||
|
// Fahrzeug gehängt) - dann und kurz nach dem Fahren nicht festhalten
|
||||||
|
if (Time.time - lastDriving < AfterDrivingPause) { dwellStart = -1f; return null; }
|
||||||
|
CharacterController cc = player.GetComponent<CharacterController>();
|
||||||
|
if ((cc != null && !cc.enabled) || player.parent != null) { lastDriving = Time.time; dwellStart = -1f; return null; }
|
||||||
|
if (Time.time < nextAutoCheck) return null;
|
||||||
|
nextAutoCheck = Time.time + AutoCheckInterval;
|
||||||
|
Vector3 head = cam.transform.position;
|
||||||
|
|
||||||
vehicle = best;
|
// Nach dem Aussteigen erst einmal aus dem Fahrzeug heraus, sonst säße man sofort wieder drin
|
||||||
lastYaw = vt.eulerAngles.y;
|
if (!autoArmed)
|
||||||
|
{
|
||||||
|
if (VehicleAround(objects, head) == null) autoArmed = true;
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncedBody found = VehicleAround(objects, head);
|
||||||
|
if (found == null || found != dwellVehicle)
|
||||||
|
{
|
||||||
|
dwellVehicle = found;
|
||||||
|
dwellStart = found != null ? Time.time : -1f;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Time.time - dwellStart < AutoEnterDwell) return null;
|
||||||
|
dwellStart = -1f;
|
||||||
|
dwellVehicle = null;
|
||||||
|
Enter(found, found.Rb.transform.InverseTransformPoint(head), player); // genau hier festhalten
|
||||||
|
return "Riding along (Space or the passenger key to get out)";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Fahrzeug, in dessen Umriss der Kopf gerade ist (null = in keinem).</summary>
|
||||||
|
SyncedBody VehicleAround(ObjectSync objects, Vector3 head)
|
||||||
|
{
|
||||||
|
foreach (SyncedBody b in objects.AllVehicles())
|
||||||
|
{
|
||||||
|
if (b.Rb == null || (b.Rb.position - head).sqrMagnitude > 10f * 10f) continue;
|
||||||
|
Bounds inner = InnerBounds(b);
|
||||||
|
if (inner.Contains(b.Rb.transform.InverseTransformPoint(head))) return b;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly Dictionary<Rigidbody, Bounds> innerBounds = new Dictionary<Rigidbody, Bounds>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Innenraum des Fahrzeugs (lokal): Umriss aller festen Collider, seitlich und vorne/hinten etwas kleiner - neben
|
||||||
|
/// der geschlossenen Tür steht man noch draußen, erst in der offenen Tür bzw. drin ist man "im Auto".
|
||||||
|
/// </summary>
|
||||||
|
Bounds InnerBounds(SyncedBody b)
|
||||||
|
{
|
||||||
|
Bounds result;
|
||||||
|
if (innerBounds.TryGetValue(b.Rb, out result)) return result;
|
||||||
|
Transform t = b.Rb.transform;
|
||||||
|
bool any = false;
|
||||||
|
foreach (Collider c in b.Rb.GetComponentsInChildren<Collider>())
|
||||||
|
{
|
||||||
|
if (c == null || c.isTrigger || !c.enabled) continue;
|
||||||
|
Bounds wb = c.bounds;
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
Vector3 corner = wb.center + Vector3.Scale(wb.extents, new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1));
|
||||||
|
Vector3 lp = t.InverseTransformPoint(corner);
|
||||||
|
if (!any) { result = new Bounds(lp, Vector3.zero); any = true; }
|
||||||
|
else result.Encapsulate(lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any) result = new Bounds(Vector3.zero, Vector3.zero);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Vector3 size = result.size;
|
||||||
|
// Weltachsen-Umrisse schräger Teile sind zu groß: seitlich 0.25 m, vorne/hinten 0.3 m kleiner,
|
||||||
|
// oben 0.6 m Luft (Kopf über dem Dach der Ladefläche / im offenen Fahrzeug)
|
||||||
|
result.size = new Vector3(Mathf.Max(0.1f, size.x - 0.5f), size.y + 0.6f, Mathf.Max(0.1f, size.z - 0.6f));
|
||||||
|
result.center += new Vector3(0f, 0.3f, 0f);
|
||||||
|
}
|
||||||
|
innerBounds[b.Rb] = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Enter(SyncedBody b, Vector3 seat, Transform player)
|
||||||
|
{
|
||||||
|
seatHeadLocal = seat;
|
||||||
|
vehicle = b;
|
||||||
|
lastYaw = b.Rb.transform.eulerAngles.y;
|
||||||
SetMovementEnabled(player, false);
|
SetMovementEnabled(player, false);
|
||||||
return "Riding as passenger (press again to get out)";
|
}
|
||||||
|
|
||||||
|
/// <summary>Beifahrersitz (Kopfposition, lokal im Fahrzeug) - aus dem Fahrersitz des Spiels oder dem Fahrer.</summary>
|
||||||
|
Vector3? SeatFor(SyncedBody b, ObjectSync objects, IDictionary<int, PlayerInfo> players)
|
||||||
|
{
|
||||||
|
Vector3? driverHead;
|
||||||
|
if (!driverHeads.TryGetValue(b.Rb, out driverHead))
|
||||||
|
{
|
||||||
|
driverHead = null;
|
||||||
|
foreach (Transform t in b.Rb.GetComponentsInChildren<Transform>(true))
|
||||||
|
{
|
||||||
|
if (t.name != "DriverHeadPivot") continue;
|
||||||
|
driverHead = b.Rb.transform.InverseTransformPoint(t.position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
driverHeads[b.Rb] = driverHead;
|
||||||
|
}
|
||||||
|
// Ohne DriverHeadPivot: nur, wenn gerade jemand fährt (dessen Kopf ist die Fahrerposition)
|
||||||
|
if (driverHead == null && objects.IsRemoteOwnedBody(b) && b.OwnerActive)
|
||||||
|
{
|
||||||
|
PlayerInfo owner;
|
||||||
|
if (players.TryGetValue(b.OwnerId, out owner) && owner.Avatar != null)
|
||||||
|
driverHead = b.Rb.transform.InverseTransformPoint(owner.Avatar.HeadPosition);
|
||||||
|
}
|
||||||
|
if (driverHead == null) return null;
|
||||||
|
Vector3 d = driverHead.Value;
|
||||||
|
return Mathf.Abs(d.x) < CenterSeatThreshold
|
||||||
|
? d - new Vector3(0f, 0f, BehindCenterSeat) // Sitz in der Mitte: dahinter
|
||||||
|
: new Vector3(-d.x, d.y, d.z); // gegenüber vom Fahrer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -92,12 +230,17 @@ namespace MSCO
|
|||||||
public void LateUpdate(Transform player, Camera cam, ObjectSync objects)
|
public void LateUpdate(Transform player, Camera cam, ObjectSync objects)
|
||||||
{
|
{
|
||||||
if (!IsRiding) return;
|
if (!IsRiding) return;
|
||||||
// Fahrer ausgestiegen oder Fahrzeug weg: automatisch aussteigen
|
if (vehicle.Rb == null || cam == null)
|
||||||
if (vehicle.Rb == null || cam == null || !objects.IsRemoteOwnedBody(vehicle))
|
|
||||||
{
|
{
|
||||||
Exit(player, cam);
|
Exit(player, cam);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Selbst ans Steuer gesetzt (Fahrersitz des Spiels): Beifahrer-Modus ohne Umsetzen beenden
|
||||||
|
if (objects.IsDriving)
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Transform vt = vehicle.Rb.transform;
|
Transform vt = vehicle.Rb.transform;
|
||||||
float yaw = vt.eulerAngles.y;
|
float yaw = vt.eulerAngles.y;
|
||||||
@@ -113,9 +256,15 @@ namespace MSCO
|
|||||||
if (!IsRiding) return;
|
if (!IsRiding) return;
|
||||||
if (player != null && vehicle.Rb != null)
|
if (player != null && vehicle.Rb != null)
|
||||||
{
|
{
|
||||||
|
lastExitSeat = vehicle.Rb.transform.TransformPoint(seatHeadLocal);
|
||||||
|
autoArmed = false;
|
||||||
// Auf der Seite des Sitzes neben das Fahrzeug stellen
|
// Auf der Seite des Sitzes neben das Fahrzeug stellen
|
||||||
|
// Zur näheren Seite neben das Fahrzeug (außerhalb seines Umrisses), auf Kopfhöhe von vorher
|
||||||
float side = seatHeadLocal.x >= 0f ? 1f : -1f;
|
float side = seatHeadLocal.x >= 0f ? 1f : -1f;
|
||||||
Vector3 outside = vehicle.Rb.transform.TransformPoint(seatHeadLocal + new Vector3(side * ExitSideOffset, 0f, 0f));
|
Bounds inner = InnerBounds(vehicle);
|
||||||
|
float x = inner.size.sqrMagnitude > 0f ? inner.center.x + side * (inner.extents.x + 0.85f) : seatHeadLocal.x + side * ExitSideOffset;
|
||||||
|
var local = new Vector3(x, seatHeadLocal.y, seatHeadLocal.z);
|
||||||
|
Vector3 outside = vehicle.Rb.transform.TransformPoint(local);
|
||||||
Vector3 cameraOffset = cam != null ? cam.transform.position - player.position : Vector3.up * 1.6f;
|
Vector3 cameraOffset = cam != null ? cam.transform.position - player.position : Vector3.up * 1.6f;
|
||||||
player.position = outside - cameraOffset + Vector3.up * 0.1f;
|
player.position = outside - cameraOffset + Vector3.up * 0.1f;
|
||||||
}
|
}
|
||||||
@@ -127,6 +276,10 @@ namespace MSCO
|
|||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
vehicle = null;
|
vehicle = null;
|
||||||
|
dwellVehicle = null;
|
||||||
|
dwellStart = -1f;
|
||||||
|
driverHeads.Clear();
|
||||||
|
innerBounds.Clear();
|
||||||
SetMovementEnabled(null, true);
|
SetMovementEnabled(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern
|
|||||||
Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen) – Cheats werden im Chat angezeigt
|
Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen) – Cheats werden im Chat angezeigt
|
||||||
- **P** – als Beifahrer ein-/aussteigen
|
- **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)
|
- Rich Presence: zeigt Freunden deinen Status („In the Menus“, Server, Spielerzahl)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ In der F7-Konsole direkt, in der MSCLoader-Konsole mit `mp` davor (z.B. `mp conn
|
|||||||
|
|
||||||
## Selbst bauen
|
## Selbst bauen
|
||||||
|
|
||||||
- Visual Studio 2022 oder neuer, .NET Framework 3.5 (Unity 5.0 / Mono)
|
- Eine C#-Entwicklungsumgebung, .NET Framework 3.5 (Unity 5.0 / Mono)
|
||||||
- Die Verweise in `MSCO.csproj` zeigen auf `F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\`
|
- Die Verweise in `MSCO.csproj` zeigen auf `F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\`
|
||||||
(UnityEngine, PlayMaker, Assembly-CSharp, Assembly-CSharp-firstpass, MSCLoader, 0Harmony, Newtonsoft.Json, cInput) –
|
(UnityEngine, PlayMaker, Assembly-CSharp, Assembly-CSharp-firstpass, MSCLoader, 0Harmony, Newtonsoft.Json, cInput) –
|
||||||
bei anderem Installationsort anpassen.
|
bei anderem Installationsort anpassen.
|
||||||
@@ -88,6 +88,15 @@ In der F7-Konsole direkt, in der MSCLoader-Konsole mit `mp` davor (z.B. `mp conn
|
|||||||
msbuild MSCO.csproj -p:Configuration=Release
|
msbuild MSCO.csproj -p:Configuration=Release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Updates veröffentlichen (MSC Launcher / MSCLoader)
|
||||||
|
|
||||||
|
- **Kleines Update** (Fehlerbehebungen, neue Funktionen, Netzwerk unverändert): in `MSCO.cs` nur die dritte Stelle von
|
||||||
|
`ModVersion` erhöhen (`2.8.1` → `2.8.2`), Release bauen und `bin/Release/MSCO.dll` hochladen. Spieler mit 2.8.x
|
||||||
|
spielen weiter zusammen, Server 2.8 müssen nicht aktualisiert werden.
|
||||||
|
- **Großes Update** (Protokoll inkompatibel geändert): `ProtocolVersion` in `MSCO.cs` **und** `GameServer.ModVersion` im
|
||||||
|
Server erhöhen (z.B. `2.9`), `ModVersion` auf `2.9.0`. Dann brauchen alle die neue Mod und den neuen Server.
|
||||||
|
- Verglichen werden immer nur die ersten zwei Stellen; übers Netz geht nur die Netzwerk-Version.
|
||||||
|
|
||||||
## Aufbau
|
## Aufbau
|
||||||
|
|
||||||
| Datei | Aufgabe |
|
| Datei | Aufgabe |
|
||||||
@@ -102,7 +111,7 @@ msbuild MSCO.csproj -p:Configuration=Release
|
|||||||
| `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` | 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, Rich Presence |
|
||||||
| `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche |
|
| `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche |
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
## Bekannte Einschränkungen
|
||||||
|
|||||||
+76
-1
@@ -49,6 +49,12 @@ namespace MSCO
|
|||||||
Quaternion targetViewRot = Quaternion.identity;
|
Quaternion targetViewRot = Quaternion.identity;
|
||||||
PlayerState last;
|
PlayerState last;
|
||||||
|
|
||||||
|
// Fahrzeug, in dem die Figur sitzt (setzt der Manager jedes Frame; null = läuft frei)
|
||||||
|
public Transform Seat;
|
||||||
|
Transform lastSeat;
|
||||||
|
Vector3 seatHead;
|
||||||
|
Quaternion seatView = Quaternion.identity;
|
||||||
|
|
||||||
Vector3 lastPosition;
|
Vector3 lastPosition;
|
||||||
float walkPhase, walkAmount, speed;
|
float walkPhase, walkAmount, speed;
|
||||||
float airAmount; // 0 = steht am Boden, 1 = in der Luft (Sprung, Fallen)
|
float airAmount; // 0 = steht am Boden, 1 = in der Luft (Sprung, Fallen)
|
||||||
@@ -56,7 +62,18 @@ namespace MSCO
|
|||||||
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 headPivot.position; } }
|
public Vector3 HeadPosition { get { return headPivot.position; } }
|
||||||
public string ModelDescription { get { return "MSCO figure, variant " + variant; } }
|
public string ModelDescription { get { return model != null ? model.Description : "MSCO figure, variant " + variant; } }
|
||||||
|
|
||||||
|
/// <summary>Mitspieler als echte MSC-Menschen zeigen (Konsole "model game|classic", gespeichert).</summary>
|
||||||
|
public static bool UseGameModels
|
||||||
|
{
|
||||||
|
get { return PlayerPrefs.GetInt("MSCO_GameModels", 1) == 1; }
|
||||||
|
set { PlayerPrefs.SetInt("MSCO_GameModels", value ? 1 : 0); PlayerPrefs.Save(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Echte Spielfigur (null = eigene MSCO-Figur sichtbar); die MSCO-Figur liefert dann nur noch die Gelenkpunkte
|
||||||
|
GameCharacter model;
|
||||||
|
readonly GameCharacter.Rig rig = new GameCharacter.Rig();
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Aufbau
|
// ---------------------------------------------------------------- Aufbau
|
||||||
|
|
||||||
@@ -67,6 +84,7 @@ namespace MSCO
|
|||||||
rp.jacket = jacketColor;
|
rp.jacket = jacketColor;
|
||||||
rp.variant = Mathf.Abs(variant);
|
rp.variant = Mathf.Abs(variant);
|
||||||
rp.Build();
|
rp.Build();
|
||||||
|
if (UseGameModels) rp.UseGameModel();
|
||||||
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);
|
||||||
@@ -174,6 +192,16 @@ namespace MSCO
|
|||||||
view.SetParent(r, false);
|
view.SetParent(r, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UseGameModel()
|
||||||
|
{
|
||||||
|
model = GameCharacter.Create(transform, variant);
|
||||||
|
if (model == null) return;
|
||||||
|
// Eigene Figur unsichtbar (sie bewegt sich weiter und gibt die Gelenke vor)
|
||||||
|
Transform modelRoot = transform.Find("MSCO_Model");
|
||||||
|
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||||||
|
if (modelRoot == null || !r.transform.IsChildOf(modelRoot)) r.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Kleines Karomuster (Flanellhemd) in der Grundfarbe; wird auf die Kapseln gekachelt.</summary>
|
/// <summary>Kleines Karomuster (Flanellhemd) in der Grundfarbe; wird auf die Kapseln gekachelt.</summary>
|
||||||
static Texture2D Plaid(Color baseColor)
|
static Texture2D Plaid(Color baseColor)
|
||||||
{
|
{
|
||||||
@@ -258,6 +286,15 @@ namespace MSCO
|
|||||||
|
|
||||||
public void Tick(float dt)
|
public void Tick(float dt)
|
||||||
{
|
{
|
||||||
|
if (Seat != null && last.SeatId != 0)
|
||||||
|
{
|
||||||
|
TickSeated(dt);
|
||||||
|
UpdateWalk(dt);
|
||||||
|
Pose(dt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastSeat = null;
|
||||||
|
|
||||||
// Bei großen Sprüngen (Teleport, Laden) direkt setzen statt gleiten
|
// Bei großen Sprüngen (Teleport, Laden) direkt setzen statt gleiten
|
||||||
bool snap = (transform.position - targetFeet).sqrMagnitude > 100f;
|
bool snap = (transform.position - targetFeet).sqrMagnitude > 100f;
|
||||||
float t = snap ? 1f : Mathf.Clamp01(dt * Smoothing);
|
float t = snap ? 1f : Mathf.Clamp01(dt * Smoothing);
|
||||||
@@ -273,6 +310,32 @@ namespace MSCO
|
|||||||
Pose(dt);
|
Pose(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Im Fahrzeug: relativ zum Fahrzeug glätten und jedes Frame an dessen aktuelle Lage setzen. In Weltkoordinaten
|
||||||
|
/// hinkt die Figur einem fahrenden Auto um Tempo / Glättung hinterher (bei 90 km/h fast 2 m) und ruckelt.
|
||||||
|
/// </summary>
|
||||||
|
void TickSeated(float dt)
|
||||||
|
{
|
||||||
|
bool fresh = lastSeat != Seat; // gerade eingestiegen: direkt auf den Sitz
|
||||||
|
lastSeat = Seat;
|
||||||
|
float t = fresh ? 1f : Mathf.Clamp01(dt * Smoothing);
|
||||||
|
seatHead = Vector3.Lerp(seatHead, last.SeatHead, t);
|
||||||
|
seatView = Quaternion.Slerp(seatView, last.SeatView, t);
|
||||||
|
|
||||||
|
Vector3 up = Seat.up;
|
||||||
|
Quaternion viewRot = Seat.rotation * seatView;
|
||||||
|
Vector3 forward = viewRot * Vector3.forward;
|
||||||
|
Vector3 flat = forward - up * Vector3.Dot(forward, up);
|
||||||
|
if (flat.sqrMagnitude < 0.0001f) flat = Seat.forward;
|
||||||
|
|
||||||
|
headWorld = Seat.TransformPoint(seatHead);
|
||||||
|
transform.rotation = Quaternion.LookRotation(flat, up); // neigt sich mit dem Auto (Berg, Kurve)
|
||||||
|
transform.position = headWorld - up * 1.2f;
|
||||||
|
pitch = Mathf.Clamp(-Mathf.Asin(Mathf.Clamp(Vector3.Dot(forward.normalized, up), -1f, 1f)) * Mathf.Rad2Deg, -MaxHeadPitch, MaxHeadPitch);
|
||||||
|
view.position = headWorld;
|
||||||
|
view.rotation = viewRot;
|
||||||
|
}
|
||||||
|
|
||||||
void UpdateWalk(float dt)
|
void UpdateWalk(float dt)
|
||||||
{
|
{
|
||||||
Vector3 moved = transform.position - lastPosition;
|
Vector3 moved = transform.position - lastPosition;
|
||||||
@@ -338,6 +401,11 @@ namespace MSCO
|
|||||||
// Kopf
|
// Kopf
|
||||||
headPivot.localPosition = head;
|
headPivot.localPosition = head;
|
||||||
headPivot.localRotation = Quaternion.Euler(pitch, 0f, 0f);
|
headPivot.localRotation = Quaternion.Euler(pitch, 0f, 0f);
|
||||||
|
rig.Hip = hip;
|
||||||
|
rig.NeckBase = neckBase;
|
||||||
|
rig.Head = head;
|
||||||
|
rig.Up = up;
|
||||||
|
rig.Pitch = pitch;
|
||||||
|
|
||||||
float swing = Mathf.Sin(walkPhase) * walkAmount * (1f - airAmount);
|
float swing = Mathf.Sin(walkPhase) * walkAmount * (1f - airAmount);
|
||||||
for (int i = 0; i < 2; i++)
|
for (int i = 0; i < 2; i++)
|
||||||
@@ -359,6 +427,9 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward);
|
Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward);
|
||||||
Segment(thigh[i], hipJoint, knee, 0.15f, 0.16f);
|
Segment(thigh[i], hipJoint, knee, 0.15f, 0.16f);
|
||||||
|
rig.HipJoint[i] = hipJoint;
|
||||||
|
rig.Knee[i] = knee;
|
||||||
|
rig.Ankle[i] = ankle;
|
||||||
Segment(shin[i], knee, ankle, 0.12f, 0.13f);
|
Segment(shin[i], knee, ankle, 0.12f, 0.13f);
|
||||||
// Stiefel: Kapsel von der Ferse zur Spitze, darunter eine flache Sohle
|
// Stiefel: Kapsel von der Ferse zur Spitze, darunter eine flache Sohle
|
||||||
Segment(boot[i], ankle + new Vector3(0f, -0.025f, -0.06f), ankle + new Vector3(0f, -0.03f, 0.14f), 0.12f, 0.1f);
|
Segment(boot[i], ankle + new Vector3(0f, -0.025f, -0.06f), ankle + new Vector3(0f, -0.03f, 0.14f), 0.12f, 0.1f);
|
||||||
@@ -386,12 +457,16 @@ namespace MSCO
|
|||||||
|
|
||||||
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.11f, 0.11f);
|
Segment(upperArm[i], shoulder, elbow, 0.11f, 0.11f);
|
||||||
|
rig.Shoulder[i] = shoulder;
|
||||||
|
rig.Elbow[i] = elbow;
|
||||||
|
rig.Hand[i] = handTarget;
|
||||||
Segment(forearm[i], elbow, handTarget, 0.095f, 0.095f);
|
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);
|
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].localRotation = Quaternion.LookRotation(Vector3.forward, (handTarget - elbow).sqrMagnitude > 0f ? elbow - handTarget : Vector3.up);
|
||||||
hand[i].localScale = new Vector3(0.085f, 0.125f, 0.11f);
|
hand[i].localScale = new Vector3(0.085f, 0.125f, 0.11f);
|
||||||
}
|
}
|
||||||
|
if (model != null) model.Apply(rig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Zwei-Knochen-Gelenk (Knie/Ellbogen): Punkt zwischen a und b, zur Seite "bend" geknickt.</summary>
|
/// <summary>Zwei-Knochen-Gelenk (Knie/Ellbogen): Punkt zwischen a und b, zur Seite "bend" geknickt.</summary>
|
||||||
|
|||||||
+48
@@ -24,6 +24,54 @@ namespace MSCO
|
|||||||
|
|
||||||
public static bool HasBackup { get { return File.Exists(Marker); } }
|
public static bool HasBackup { get { return File.Exists(Marker); } }
|
||||||
|
|
||||||
|
static string ArchiveDir { get { return Path.Combine(SaveDir, "MSCO_Saves"); } }
|
||||||
|
const int KeepArchives = 10;
|
||||||
|
|
||||||
|
/// <summary>Gibt es einen eigenen Spielstand zum Weiterspielen?</summary>
|
||||||
|
public static bool HasLocalSave { get { return File.Exists(Path.Combine(SaveDir, "defaultES2File.txt")); } }
|
||||||
|
|
||||||
|
/// <summary>Wann der eigene Spielstand zuletzt gespeichert wurde (null = keiner).</summary>
|
||||||
|
public static System.DateTime? LocalSaveTime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string file = Path.Combine(SaveDir, "defaultES2File.txt");
|
||||||
|
return File.Exists(file) ? File.GetLastWriteTime(file) : (System.DateTime?)null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ordner mit den Spielständen, die vor einem neuen Spiel gesichert wurden.</summary>
|
||||||
|
public static string ArchiveFolder { get { return ArchiveDir; } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Neues Spiel: den bisherigen Spielstand nach MSCO_Saves/Datum kopieren (nichts geht verloren) und die
|
||||||
|
/// Spielstand-Dateien leeren, damit das Spiel eine frische Welt anlegt. Gibt den Sicherungsordner zurück (oder null).
|
||||||
|
/// </summary>
|
||||||
|
public static string ArchiveForNewGame()
|
||||||
|
{
|
||||||
|
// War noch der Spielstand eines anderen Hosts aktiv, erst den eigenen zurückholen
|
||||||
|
if (HasBackup) RestoreBackup();
|
||||||
|
string target = null;
|
||||||
|
if (HasLocalSave)
|
||||||
|
{
|
||||||
|
target = Path.Combine(ArchiveDir, System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"));
|
||||||
|
Directory.CreateDirectory(target);
|
||||||
|
foreach (string name in Files)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(SaveDir, name);
|
||||||
|
if (File.Exists(path)) File.Copy(path, Path.Combine(target, name), true);
|
||||||
|
}
|
||||||
|
MscoLog.Info("[MSCO] Your previous save game was backed up to " + target);
|
||||||
|
// Nur die letzten Sicherungen behalten
|
||||||
|
string[] all = Directory.GetDirectories(ArchiveDir);
|
||||||
|
System.Array.Sort(all);
|
||||||
|
for (int i = 0; i < all.Length - KeepArchives; i++)
|
||||||
|
try { Directory.Delete(all[i], true); } catch { }
|
||||||
|
}
|
||||||
|
DeleteSaveFiles();
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
public static List<KeyValuePair<string, byte[]>> ReadLocalSave()
|
public static List<KeyValuePair<string, byte[]>> ReadLocalSave()
|
||||||
{
|
{
|
||||||
var result = new List<KeyValuePair<string, byte[]>>();
|
var result = new List<KeyValuePair<string, byte[]>>();
|
||||||
|
|||||||
@@ -14,6 +14,27 @@ namespace MSCO
|
|||||||
static float builtAt = -1000f;
|
static float builtAt = -1000f;
|
||||||
static int builtForLevel = -1;
|
static int builtForLevel = -1;
|
||||||
|
|
||||||
|
static Rigidbody[] bodies = new Rigidbody[0];
|
||||||
|
static float bodiesAt = -1000f;
|
||||||
|
static int bodiesForLevel = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Alle Rigidbodies; höchstens maxAge Sekunden alt (Objekt-Sync und Verkehr teilen sich die teure Suche).
|
||||||
|
/// Einträge können inzwischen zerstört sein (== null prüfen).
|
||||||
|
/// </summary>
|
||||||
|
public static Rigidbody[] Rigidbodies(float maxAge)
|
||||||
|
{
|
||||||
|
if (Time.realtimeSinceStartup - bodiesAt > maxAge || bodiesForLevel != Application.loadedLevel)
|
||||||
|
{
|
||||||
|
Object[] found = Object.FindObjectsOfType(typeof(Rigidbody));
|
||||||
|
bodies = new Rigidbody[found.Length];
|
||||||
|
for (int i = 0; i < found.Length; i++) bodies[i] = (Rigidbody)found[i];
|
||||||
|
bodiesAt = Time.realtimeSinceStartup;
|
||||||
|
bodiesForLevel = Application.loadedLevel;
|
||||||
|
}
|
||||||
|
return bodies;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Alle FSMs; höchstens maxAge Sekunden alt. Einträge können inzwischen zerstört sein (== null prüfen).</summary>
|
/// <summary>Alle FSMs; höchstens maxAge Sekunden alt. Einträge können inzwischen zerstört sein (== null prüfen).</summary>
|
||||||
public static PlayMakerFSM[] Fsms(float maxAge)
|
public static PlayMakerFSM[] Fsms(float maxAge)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ namespace MSCO
|
|||||||
public string Version;
|
public string Version;
|
||||||
public bool RequireSteam = true; // false = Non-Steam-Server
|
public bool RequireSteam = true; // false = Non-Steam-Server
|
||||||
public string Owner; // Besitzer des Lizenzschlüssels
|
public string Owner; // Besitzer des Lizenzschlüssels
|
||||||
|
public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben)
|
||||||
|
public List<string> Tags = new List<string>(); // Admin-Tags (z.B. "official") und eigene Tags des Servers (Web-Panel)
|
||||||
|
|
||||||
[JsonIgnore] public int UptimeSeconds = -1;
|
[JsonIgnore] public int UptimeSeconds = -1;
|
||||||
[JsonIgnore] public List<string> PlayerNames = new List<string>();
|
[JsonIgnore] public List<string> PlayerNames = new List<string>();
|
||||||
|
|||||||
+2
-2
@@ -164,7 +164,7 @@ namespace MSCO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LobbyId = new CSteamID(result.m_ulSteamIDLobby);
|
LobbyId = new CSteamID(result.m_ulSteamIDLobby);
|
||||||
SteamMatchmaking.SetLobbyData(LobbyId, VersionKey, MSCO.ModVersion);
|
SteamMatchmaking.SetLobbyData(LobbyId, VersionKey, MSCO.ProtocolVersion); // ältere Mods vergleichen genau
|
||||||
if (Created != null) Created(LobbyId);
|
if (Created != null) Created(LobbyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ namespace MSCO
|
|||||||
LobbyId = new CSteamID(result.m_ulSteamIDLobby);
|
LobbyId = new CSteamID(result.m_ulSteamIDLobby);
|
||||||
|
|
||||||
string hostVersion = SteamMatchmaking.GetLobbyData(LobbyId, VersionKey);
|
string hostVersion = SteamMatchmaking.GetLobbyData(LobbyId, VersionKey);
|
||||||
if (!string.IsNullOrEmpty(hostVersion) && hostVersion != MSCO.ModVersion)
|
if (!string.IsNullOrEmpty(hostVersion) && !MSCO.Compatible(hostVersion))
|
||||||
{
|
{
|
||||||
Leave();
|
Leave();
|
||||||
Fail("Version mismatch (host " + hostVersion + ", you " + MSCO.ModVersion + ")");
|
Fail("Version mismatch (host " + hostVersion + ", you " + MSCO.ModVersion + ")");
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ namespace MSCO
|
|||||||
|
|
||||||
var r = new Rect(Screen.width - 12 - Width + (1f - slide) * (Width + 20f), y, Width, Height);
|
var r = new Rect(Screen.width - 12 - Width + (1f - slide) * (Width + 20f), y, Width, Height);
|
||||||
GUI.color = new Color(1f, 1f, 1f, alpha);
|
GUI.color = new Color(1f, 1f, 1f, alpha);
|
||||||
|
UiTheme.DrawShadow(r);
|
||||||
GUI.Box(r, GUIContent.none, box);
|
GUI.Box(r, GUIContent.none, box);
|
||||||
|
|
||||||
GUI.color = new Color(t.Dot.r, t.Dot.g, t.Dot.b, alpha);
|
GUI.color = new Color(t.Dot.r, t.Dot.g, t.Dot.b, alpha);
|
||||||
@@ -73,7 +74,7 @@ namespace MSCO
|
|||||||
void EnsureStyles()
|
void EnsureStyles()
|
||||||
{
|
{
|
||||||
if (box != null && box.normal.background != null) return;
|
if (box != null && box.normal.background != null) return;
|
||||||
box = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.88f), 6);
|
box = UiTheme.Card(new Color(0.06f, 0.06f, 0.07f, 0.92f), 6);
|
||||||
title = new GUIStyle(GUI.skin.label) { fontSize = 13, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleLeft };
|
title = new GUIStyle(GUI.skin.label) { fontSize = 13, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleLeft };
|
||||||
title.normal.textColor = Color.white;
|
title.normal.textColor = Color.white;
|
||||||
title.wordWrap = false;
|
title.wordWrap = false;
|
||||||
|
|||||||
+5
-4
@@ -42,11 +42,12 @@ namespace MSCO
|
|||||||
foreach (Car c in cars.Values) StopFollowing(c);
|
foreach (Car c in cars.Values) StopFollowing(c);
|
||||||
cars.Clear();
|
cars.Clear();
|
||||||
seen.Clear();
|
seen.Clear();
|
||||||
nextScan = 0f;
|
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(bool authority, float dt, Action<MsgType, byte[]> send)
|
public void Update(bool authority, float dt, Action<MsgType, byte[]> send)
|
||||||
{
|
{
|
||||||
|
if (nextScan < 0f) nextScan = Time.time + 4.5f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
||||||
if (Time.time >= nextScan)
|
if (Time.time >= nextScan)
|
||||||
{
|
{
|
||||||
nextScan = Time.time + ScanInterval;
|
nextScan = Time.time + ScanInterval;
|
||||||
@@ -100,7 +101,7 @@ namespace MSCO
|
|||||||
c.TargetPos = p;
|
c.TargetPos = p;
|
||||||
c.TargetRot = q;
|
c.TargetRot = q;
|
||||||
c.TargetVel = v;
|
c.TargetVel = v;
|
||||||
c.TargetTime = Time.time;
|
c.TargetTime = Time.fixedTime; // Physik-Zeit wie in FixedUpdate (sonst negatives Alter = Rucken)
|
||||||
if (!c.Following)
|
if (!c.Following)
|
||||||
{
|
{
|
||||||
c.WasKinematic = c.Rb.isKinematic;
|
c.WasKinematic = c.Rb.isKinematic;
|
||||||
@@ -117,7 +118,7 @@ namespace MSCO
|
|||||||
foreach (Car c in cars.Values)
|
foreach (Car c in cars.Values)
|
||||||
{
|
{
|
||||||
if (!c.Following || c.Rb == null) continue;
|
if (!c.Following || c.Rb == null) continue;
|
||||||
float ahead = Mathf.Min(Time.time - c.TargetTime, MaxExtrapolation);
|
float ahead = Mathf.Clamp(Time.fixedTime - c.TargetTime, 0f, MaxExtrapolation);
|
||||||
Vector3 goal = c.TargetPos + c.TargetVel * ahead;
|
Vector3 goal = c.TargetPos + c.TargetVel * ahead;
|
||||||
float t = Mathf.Clamp01(Time.fixedDeltaTime * Smoothing);
|
float t = Mathf.Clamp01(Time.fixedDeltaTime * Smoothing);
|
||||||
if ((c.Rb.position - goal).sqrMagnitude > 25f) { c.Rb.position = goal; c.Rb.rotation = c.TargetRot; continue; }
|
if ((c.Rb.position - goal).sqrMagnitude > 25f) { c.Rb.position = goal; c.Rb.rotation = c.TargetRot; continue; }
|
||||||
@@ -136,7 +137,7 @@ namespace MSCO
|
|||||||
void Scan()
|
void Scan()
|
||||||
{
|
{
|
||||||
int before = cars.Count;
|
int before = cars.Count;
|
||||||
foreach (Rigidbody rb in UnityEngine.Object.FindObjectsOfType<Rigidbody>())
|
foreach (Rigidbody rb in SceneCache.Rigidbodies(5f)) // teilt sich die Suche mit dem Objekt-Sync
|
||||||
{
|
{
|
||||||
if (rb == null || !seen.Add(rb)) continue;
|
if (rb == null || !seen.Add(rb)) continue;
|
||||||
if (Array.IndexOf(Roots, rb.transform.root.name) < 0) continue;
|
if (Array.IndexOf(Roots, rb.transform.root.name) < 0) continue;
|
||||||
|
|||||||
+91
@@ -26,8 +26,13 @@ namespace MSCO
|
|||||||
public static readonly Color Muted = Hex(0x8e, 0x8e, 0x99);
|
public static readonly Color Muted = Hex(0x8e, 0x8e, 0x99);
|
||||||
public static readonly Color Online = Hex(0x3d, 0xd6, 0x8c);
|
public static readonly Color Online = Hex(0x3d, 0xd6, 0x8c);
|
||||||
public static readonly Color Pending = Hex(0xf2, 0xc2, 0x1d);
|
public static readonly Color Pending = Hex(0xf2, 0xc2, 0x1d);
|
||||||
|
public static readonly Color Gold = Hex(0xff, 0xb4, 0x5a);
|
||||||
|
/// <summary>Feiner heller Rand um Fenster und Karten (wie auf der Website).</summary>
|
||||||
|
public static readonly Color Border = new Color(1f, 1f, 1f, 0.08f);
|
||||||
|
|
||||||
public const int Radius = 6;
|
public const int Radius = 6;
|
||||||
|
const int ShadowBlur = 16;
|
||||||
|
static GUIStyle shadowStyle;
|
||||||
|
|
||||||
public static Color Hex(int r, int g, int b, float a = 1f)
|
public static Color Hex(int r, int g, int b, float a = 1f)
|
||||||
{
|
{
|
||||||
@@ -58,6 +63,92 @@ namespace MSCO
|
|||||||
return tex;
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wie Rounded, aber mit senkrechtem Verlauf (oben -> unten) und optional 1 px Rand. Die Textur ist höher als
|
||||||
|
/// breit, damit der Verlauf beim 9-Slice-Strecken im Mittelteil erhalten bleibt.
|
||||||
|
/// </summary>
|
||||||
|
public static Texture2D RoundedEx(Color top, Color bottom, Color border, bool withBorder, int radius = Radius, int height = 48)
|
||||||
|
{
|
||||||
|
int width = radius * 2 + 4;
|
||||||
|
height = Mathf.Max(height, width);
|
||||||
|
var tex = new Texture2D(width, height, TextureFormat.ARGB32, false);
|
||||||
|
tex.wrapMode = TextureWrapMode.Clamp;
|
||||||
|
tex.filterMode = FilterMode.Bilinear;
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
// Texturzeile 0 ist unten
|
||||||
|
Color fill = Color.Lerp(bottom, top, (y + 0.5f) / height);
|
||||||
|
// Rand deckend auf die Fläche mischen (ein durchsichtiger Rand ließe das Spiel durchscheinen)
|
||||||
|
Color edge = Color.Lerp(fill, new Color(border.r, border.g, border.b, fill.a), border.a);
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
float cx = Mathf.Clamp(x + 0.5f, radius, width - radius);
|
||||||
|
float cy = Mathf.Clamp(y + 0.5f, radius, height - radius);
|
||||||
|
float d = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx, cy));
|
||||||
|
Color c = withBorder ? Color.Lerp(edge, fill, Mathf.Clamp01(radius - 1f - d + 0.5f)) : fill;
|
||||||
|
c.a *= Mathf.Clamp01(radius - d + 0.5f);
|
||||||
|
tex.SetPixel(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tex.Apply();
|
||||||
|
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Fenster/Karte mit feinem Rand; mit GUI.Box zeichnen (vorher DrawShadow für Tiefe).</summary>
|
||||||
|
public static GUIStyle Card(Color color, int radius = Radius)
|
||||||
|
{
|
||||||
|
var s = new GUIStyle();
|
||||||
|
s.normal.background = RoundedEx(color, color, Border, true, radius, radius * 2 + 4);
|
||||||
|
s.border = new RectOffset(radius + 2, radius + 2, radius + 2, radius + 2);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Orange Hauptknopf mit leichtem Verlauf (wie .btn auf der Website).</summary>
|
||||||
|
public static GUIStyle PrimaryButton(int fontSize = 13)
|
||||||
|
{
|
||||||
|
GUIStyle s = Button(Accent, AccentHover, Color.white, fontSize);
|
||||||
|
ApplyPrimaryGradient(s);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verlauf auf einen vorhandenen Knopf-Stil legen (Rundung und Rand bleiben).</summary>
|
||||||
|
public static void ApplyPrimaryGradient(GUIStyle s)
|
||||||
|
{
|
||||||
|
s.normal.background = s.focused.background = RoundedEx(AccentHover, Accent, Color.clear, false);
|
||||||
|
s.hover.background = s.active.background = s.onNormal.background =
|
||||||
|
RoundedEx(Color.Lerp(AccentHover, Color.white, 0.12f), AccentHover, Color.clear, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Weicher Schatten unter einem Fenster (etwas nach unten versetzt). Vor dem Fenster zeichnen.</summary>
|
||||||
|
public static void DrawShadow(Rect r)
|
||||||
|
{
|
||||||
|
if (shadowStyle == null || shadowStyle.normal.background == null)
|
||||||
|
{
|
||||||
|
const int radius = 10;
|
||||||
|
int reach = radius + ShadowBlur;
|
||||||
|
int size = reach * 2 + 4;
|
||||||
|
var tex = new Texture2D(size, size, TextureFormat.ARGB32, false);
|
||||||
|
tex.wrapMode = TextureWrapMode.Clamp;
|
||||||
|
tex.filterMode = FilterMode.Bilinear;
|
||||||
|
for (int y = 0; y < size; y++)
|
||||||
|
for (int x = 0; x < size; x++)
|
||||||
|
{
|
||||||
|
float cx = Mathf.Clamp(x + 0.5f, reach, size - reach);
|
||||||
|
float cy = Mathf.Clamp(y + 0.5f, reach, size - reach);
|
||||||
|
float outside = Mathf.Max(0f, Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx, cy)) - radius);
|
||||||
|
float fade = 1f - Mathf.Clamp01(outside / ShadowBlur);
|
||||||
|
tex.SetPixel(x, y, new Color(0f, 0f, 0f, 0.45f * fade * fade));
|
||||||
|
}
|
||||||
|
tex.Apply();
|
||||||
|
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
shadowStyle = new GUIStyle();
|
||||||
|
shadowStyle.normal.background = tex;
|
||||||
|
shadowStyle.border = new RectOffset(reach + 2, reach + 2, reach + 2, reach + 2);
|
||||||
|
}
|
||||||
|
GUI.Box(new Rect(r.x - ShadowBlur, r.y - ShadowBlur + 6, r.width + ShadowBlur * 2, r.height + ShadowBlur * 2), GUIContent.none, shadowStyle);
|
||||||
|
}
|
||||||
|
|
||||||
public static Texture2D Solid(Color color)
|
public static Texture2D Solid(Color color)
|
||||||
{
|
{
|
||||||
var tex = new Texture2D(1, 1);
|
var tex = new Texture2D(1, 1);
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ namespace MSCO
|
|||||||
string n = name.ToLower();
|
string n = name.ToLower();
|
||||||
return n.Contains("needle") || n.Contains("steeringwheel");
|
return n.Contains("needle") || n.Contains("steeringwheel");
|
||||||
}
|
}
|
||||||
const float CacheRefresh = 5f; // Teile können an-/abgebaut werden
|
const float CacheRefresh = 15f; // Teile können an-/abgebaut werden (Nachschlagen ist teuer: ganzes Fahrzeug)
|
||||||
const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen
|
const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen
|
||||||
|
|
||||||
struct AudioState
|
struct AudioState
|
||||||
|
|||||||
+148
-20
@@ -28,7 +28,22 @@ namespace MSCO
|
|||||||
|
|
||||||
// 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)
|
||||||
// Schrauben/Festigkeit laufen über FsmSync (Events) - ein falscher Wert hier würde Teile lockern und abfallen lassen
|
// 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" };
|
// Vom Spiel selbst berechnete Werte ebenfalls nicht: z.B. LeakOilpan/LeakOilfilter (leckt Öl?) ergibt sich aus
|
||||||
|
// angebauten/festgezogenen Teilen - ein übertragener alter Wert ließ beim Beitreten Öl auslaufen. Raten (…Rate)
|
||||||
|
// rechnet das Spiel laufend neu aus.
|
||||||
|
static readonly string[] Excluded =
|
||||||
|
{
|
||||||
|
"install", "bolt", "tight", "detach", "attach", "rpm", "speed", "steer", "throttle", "gear", "raycast", "distance",
|
||||||
|
"leak", "rate",
|
||||||
|
// Spieler-Werte (Database/PlayerDatabase: Schmutz, Verbrennungen ...) sind pro Spieler; Fluch-Geräusche,
|
||||||
|
// Ton-Verzerrung und Anzeige-Hilfswerte (GUI) rechnet jeder selbst
|
||||||
|
"player", "swear", "distortion", "gui"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wurzeln außer den Fahrzeugen: Satsuma-Teile (lose und angebaut) und ihre Datenbank (Verschleiß, beschädigt,
|
||||||
|
// Lack, Füllstände ...). Maßgeblich ist dort wie beim Satsuma selbst, wer den Satsuma gerade fährt.
|
||||||
|
static readonly string[] PartRoots = { "CARPARTS", "Database" };
|
||||||
|
const string PartOwnerPrefix = "SATSUMA";
|
||||||
|
|
||||||
const float ScanInterval = 10f;
|
const float ScanInterval = 10f;
|
||||||
const float CheckInterval = 0.5f;
|
const float CheckInterval = 0.5f;
|
||||||
@@ -37,9 +52,12 @@ namespace MSCO
|
|||||||
class Var
|
class Var
|
||||||
{
|
{
|
||||||
public int Key;
|
public int Key;
|
||||||
|
public string Name; // Pfad|FSM|Variable (für die Diagnose "vars")
|
||||||
public string Root;
|
public string Root;
|
||||||
public NamedVariable Variable;
|
public NamedVariable Variable;
|
||||||
public byte Type; // 1 float, 2 int, 3 bool
|
public byte Type; // 1 float, 2 int, 3 bool (übertragen); Farben als 4 einzelne Kanäle vom Typ 1
|
||||||
|
public int Channel = -1; // FsmColor: 0-3 = r/g/b/a
|
||||||
|
public PlayMakerFSM Fsm;
|
||||||
public float LastF;
|
public float LastF;
|
||||||
public int LastI;
|
public int LastI;
|
||||||
public bool LastB;
|
public bool LastB;
|
||||||
@@ -48,21 +66,47 @@ namespace MSCO
|
|||||||
readonly Dictionary<int, Var> vars = new Dictionary<int, Var>();
|
readonly Dictionary<int, Var> vars = new Dictionary<int, Var>();
|
||||||
readonly Dictionary<int, byte[]> pending = new Dictionary<int, byte[]>(); // noch nicht gefundene Werte
|
readonly Dictionary<int, byte[]> pending = new Dictionary<int, byte[]>(); // noch nicht gefundene Werte
|
||||||
readonly List<Var> outgoing = new List<Var>();
|
readonly List<Var> outgoing = new List<Var>();
|
||||||
readonly HashSet<PlayMakerFSM> scanned = new HashSet<PlayMakerFSM>();
|
// Erfasste FSMs mit dem Elternobjekt beim Erfassen: wird ein Teil an-/abgebaut, ändert sich sein Pfad (und damit
|
||||||
|
// die Schlüssel) - dann neu erfassen. FSMs außerhalb der Fahrzeuge/Teile werden bei jedem Suchlauf neu geprüft
|
||||||
|
// (vorher einmal als "nicht Fahrzeug" gemerkt und nie wieder angesehen: später angebaute Teile fehlten)
|
||||||
|
readonly Dictionary<PlayMakerFSM, Transform> scanned = new Dictionary<PlayMakerFSM, Transform>();
|
||||||
|
readonly Dictionary<PlayMakerFSM, List<int>> keysByFsm = new Dictionary<PlayMakerFSM, List<int>>();
|
||||||
|
string satsumaRoot;
|
||||||
float nextScan, nextCheck;
|
float nextScan, nextCheck;
|
||||||
|
|
||||||
public int Count { get { return vars.Count; } }
|
public int Count { get { return vars.Count; } }
|
||||||
|
|
||||||
|
/// <summary>Diagnose (Konsole "vars <filter>"): synchronisierte Fahrzeugwerte mit aktuellem Wert.</summary>
|
||||||
|
public List<string> Describe(string filter, int max)
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
string f = (filter ?? "").ToLower();
|
||||||
|
foreach (Var v in vars.Values)
|
||||||
|
{
|
||||||
|
if (f.Length > 0 && (v.Name == null || !v.Name.ToLower().Contains(f))) continue;
|
||||||
|
string value = v.Type == 1 ? GetFloat(v).ToString("0.###")
|
||||||
|
: v.Type == 2 ? ((FsmInt)v.Variable).Value.ToString()
|
||||||
|
: ((FsmBool)v.Variable).Value.ToString();
|
||||||
|
lines.Add(" " + v.Name + " = " + value);
|
||||||
|
if (lines.Count >= max) { lines.Add(" ... (more - use a narrower filter)"); break; }
|
||||||
|
}
|
||||||
|
if (lines.Count == 0) lines.Add(" no synced vehicle value matches '" + filter + "'");
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
vars.Clear();
|
vars.Clear();
|
||||||
pending.Clear();
|
pending.Clear();
|
||||||
scanned.Clear();
|
scanned.Clear();
|
||||||
nextScan = 0f;
|
keysByFsm.Clear();
|
||||||
|
satsumaRoot = null;
|
||||||
|
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send)
|
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send)
|
||||||
{
|
{
|
||||||
|
if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
||||||
if (Time.time >= nextScan)
|
if (Time.time >= nextScan)
|
||||||
{
|
{
|
||||||
nextScan = Time.time + ScanInterval;
|
nextScan = Time.time + ScanInterval;
|
||||||
@@ -72,10 +116,11 @@ namespace MSCO
|
|||||||
nextCheck = Time.time + CheckInterval;
|
nextCheck = Time.time + CheckInterval;
|
||||||
|
|
||||||
outgoing.Clear();
|
outgoing.Clear();
|
||||||
|
ResolveSatsuma(objects);
|
||||||
foreach (Var v in vars.Values)
|
foreach (Var v in vars.Values)
|
||||||
{
|
{
|
||||||
// Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden
|
// Fährt/schiebt ein anderer das Fahrzeug, ist seine Version maßgeblich - nichts senden
|
||||||
int owner = objects.VehicleOwner(v.Root);
|
int owner = objects.VehicleOwner(OwnerRoot(v.Root));
|
||||||
if (owner != SyncedBody.NoOwner && owner != localId) { if (Changed(v, false)) Remember(v); continue; }
|
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 ...) -
|
// Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) -
|
||||||
@@ -107,7 +152,7 @@ namespace MSCO
|
|||||||
byte[] value = r.ReadBytes(type == 3 ? 1 : 4);
|
byte[] value = r.ReadBytes(type == 3 ? 1 : 4);
|
||||||
Var v;
|
Var v;
|
||||||
if (!vars.TryGetValue(key, out v)) { pending[key] = Combine(type, value); continue; }
|
if (!vars.TryGetValue(key, out v)) { pending[key] = Combine(type, value); continue; }
|
||||||
if (from >= 0 && objects.VehicleOwner(v.Root) == localId) continue; // ich fahre gerade: meine Werte gelten
|
if (from >= 0 && objects.VehicleOwner(OwnerRoot(v.Root)) == localId) continue; // ich fahre gerade: meine Werte gelten
|
||||||
Apply(v, type, value);
|
Apply(v, type, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,20 +177,31 @@ namespace MSCO
|
|||||||
void Scan()
|
void Scan()
|
||||||
{
|
{
|
||||||
int before = vars.Count;
|
int before = vars.Count;
|
||||||
foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f))
|
foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f))
|
||||||
{
|
{
|
||||||
// Schon erfasste FSMs nicht jedes Mal neu durchgehen (Pfad + Hash kosten)
|
if (fsm == null) continue;
|
||||||
if (fsm == null || !scanned.Add(fsm)) continue;
|
// Schon erfasst und nicht umgehängt (an-/abgebaut): nichts zu tun (Pfad + Hash kosten)
|
||||||
|
Transform parent;
|
||||||
|
if (scanned.TryGetValue(fsm, out parent) && parent == fsm.transform.parent) continue;
|
||||||
string root = fsm.transform.root.name;
|
string root = fsm.transform.root.name;
|
||||||
if (!IsVehicle(root)) continue;
|
if (!IsTracked(root)) { Forget(fsm); continue; }
|
||||||
|
scanned[fsm] = fsm.transform.parent;
|
||||||
|
Forget(fsm);
|
||||||
// FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert
|
// FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert
|
||||||
// kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed")
|
// kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed")
|
||||||
if (TouchesTransform(fsm)) continue;
|
if (TouchesTransform(fsm)) continue;
|
||||||
|
if (fsm.transform.root.name == "Database" && FsmSync.PathOf(fsm.transform).Contains("PlayerDatabase")) continue;
|
||||||
string prefix = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|";
|
string prefix = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|";
|
||||||
FsmVariables fv = fsm.FsmVariables;
|
FsmVariables fv = fsm.FsmVariables;
|
||||||
foreach (FsmFloat f in fv.FloatVariables) Register(prefix, root, f, 1);
|
foreach (FsmFloat f in fv.FloatVariables) Register(fsm, prefix, root, f, 1, -1);
|
||||||
foreach (FsmInt n in fv.IntVariables) Register(prefix, root, n, 2);
|
foreach (FsmInt n in fv.IntVariables) Register(fsm, prefix, root, n, 2, -1);
|
||||||
foreach (FsmBool b in fv.BoolVariables) Register(prefix, root, b, 3);
|
foreach (FsmBool b in fv.BoolVariables) Register(fsm, prefix, root, b, 3, -1);
|
||||||
|
// Lack-/Farbwerte (FsmColor) als vier Zahlen: alte Server/Mods kennen nur float/int/bool
|
||||||
|
foreach (FsmColor c in fv.ColorVariables)
|
||||||
|
{
|
||||||
|
if (c == null || !IsColorName(c.Name)) continue;
|
||||||
|
for (int ch = 0; ch < 4; ch++) Register(fsm, prefix, root, c, 1, ch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced");
|
if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced");
|
||||||
}
|
}
|
||||||
@@ -173,15 +229,20 @@ namespace MSCO
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Register(string prefix, string root, NamedVariable variable, byte type)
|
void Register(PlayMakerFSM fsm, string prefix, string root, NamedVariable variable, byte type, int channel)
|
||||||
{
|
{
|
||||||
if (variable == null || string.IsNullOrEmpty(variable.Name) || !Wanted(variable.Name)) return;
|
if (variable == null || string.IsNullOrEmpty(variable.Name)) return;
|
||||||
int key = Hash(prefix + variable.Name);
|
if (channel < 0 && !Wanted(variable.Name)) return;
|
||||||
|
string name = prefix + variable.Name + (channel >= 0 ? "#" + "rgba"[channel] : "");
|
||||||
|
int key = Hash(name);
|
||||||
Var existing;
|
Var existing;
|
||||||
if (vars.TryGetValue(key, out existing) && existing.Variable == variable) return;
|
if (vars.TryGetValue(key, out existing) && existing.Variable == variable) return;
|
||||||
var v = new Var { Key = key, Root = root, Variable = variable, Type = type };
|
var v = new Var { Key = key, Name = name, Root = root, Variable = variable, Type = type, Channel = channel, Fsm = fsm };
|
||||||
Remember(v);
|
Remember(v);
|
||||||
vars[key] = v;
|
vars[key] = v;
|
||||||
|
List<int> keys;
|
||||||
|
if (!keysByFsm.TryGetValue(fsm, out keys)) keysByFsm[fsm] = keys = new List<int>();
|
||||||
|
keys.Add(key);
|
||||||
|
|
||||||
byte[] stored;
|
byte[] stored;
|
||||||
if (pending.TryGetValue(key, out stored))
|
if (pending.TryGetValue(key, out stored))
|
||||||
@@ -207,13 +268,80 @@ namespace MSCO
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool IsTracked(string root)
|
||||||
|
{
|
||||||
|
return IsVehicle(root) || Array.IndexOf(PartRoots, root) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsColorName(string name)
|
||||||
|
{
|
||||||
|
string n = name.ToLower();
|
||||||
|
return n.Contains("paint") || n.Contains("color") || n.Contains("colour");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Werte einer umgehängten/verschwundenen FSM vergessen (Pfad und damit Schlüssel ändern sich).</summary>
|
||||||
|
void Forget(PlayMakerFSM fsm)
|
||||||
|
{
|
||||||
|
List<int> keys;
|
||||||
|
if (!keysByFsm.TryGetValue(fsm, out keys)) return;
|
||||||
|
foreach (int k in keys)
|
||||||
|
{
|
||||||
|
Var v;
|
||||||
|
if (vars.TryGetValue(k, out v) && v.Fsm == fsm) vars.Remove(k);
|
||||||
|
}
|
||||||
|
keysByFsm.Remove(fsm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Satsuma-Teile gehören dem, der den Satsuma fährt.</summary>
|
||||||
|
string OwnerRoot(string root)
|
||||||
|
{
|
||||||
|
return Array.IndexOf(PartRoots, root) >= 0 ? satsumaRoot : root;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveSatsuma(ObjectSync objects)
|
||||||
|
{
|
||||||
|
if (satsumaRoot != null) return;
|
||||||
|
foreach (SyncedBody b in objects.AllVehicles())
|
||||||
|
if (b.RootName != null && b.RootName.StartsWith(PartOwnerPrefix)) { satsumaRoot = b.RootName; return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static float GetFloat(Var v)
|
||||||
|
{
|
||||||
|
if (v.Channel < 0) return ((FsmFloat)v.Variable).Value;
|
||||||
|
Color c = ((FsmColor)v.Variable).Value;
|
||||||
|
return c[v.Channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetFloat(Var v, float f)
|
||||||
|
{
|
||||||
|
if (v.Channel < 0) { ((FsmFloat)v.Variable).Value = f; return; }
|
||||||
|
var cv = (FsmColor)v.Variable;
|
||||||
|
Color c = cv.Value;
|
||||||
|
c[v.Channel] = Mathf.Clamp(f, 0f, 4f);
|
||||||
|
cv.Value = c;
|
||||||
|
ShowPaint(v, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lack sichtbar machen: Die Lackier-FSM eines Teils setzt beim Lackieren die Materialfarbe ihres eigenen Meshes.
|
||||||
|
/// Beim Mitspieler kommt nur der Farbwert an - also dieselbe Farbe aufs Mesh des Teils legen.
|
||||||
|
/// </summary>
|
||||||
|
static void ShowPaint(Var v, Color c)
|
||||||
|
{
|
||||||
|
if (v.Fsm == null || v.Fsm.FsmName.IndexOf("paint", StringComparison.OrdinalIgnoreCase) < 0) return;
|
||||||
|
Renderer r = v.Fsm.GetComponent<Renderer>();
|
||||||
|
if (r == null || r.material == null) return;
|
||||||
|
r.material.color = c;
|
||||||
|
}
|
||||||
|
|
||||||
static bool Changed(Var v, bool bigOnly)
|
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 = GetFloat(v);
|
||||||
float delta = Mathf.Abs(f - v.LastF);
|
float delta = Mathf.Abs(f - v.LastF);
|
||||||
|
if (v.Channel >= 0) return delta > 0.002f; // Farben: jede Änderung (Lackieren)
|
||||||
return bigOnly
|
return bigOnly
|
||||||
? delta > Mathf.Max(0.5f, Mathf.Abs(v.LastF) * 0.05f)
|
? delta > Mathf.Max(0.5f, Mathf.Abs(v.LastF) * 0.05f)
|
||||||
: delta > Mathf.Max(0.01f, Mathf.Abs(v.LastF) * 0.002f);
|
: delta > Mathf.Max(0.01f, Mathf.Abs(v.LastF) * 0.002f);
|
||||||
@@ -226,7 +354,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
switch (v.Type)
|
switch (v.Type)
|
||||||
{
|
{
|
||||||
case 1: v.LastF = ((FsmFloat)v.Variable).Value; break;
|
case 1: v.LastF = GetFloat(v); break;
|
||||||
case 2: v.LastI = ((FsmInt)v.Variable).Value; break;
|
case 2: v.LastI = ((FsmInt)v.Variable).Value; break;
|
||||||
default: v.LastB = ((FsmBool)v.Variable).Value; break;
|
default: v.LastB = ((FsmBool)v.Variable).Value; break;
|
||||||
}
|
}
|
||||||
@@ -240,7 +368,7 @@ namespace MSCO
|
|||||||
case 1:
|
case 1:
|
||||||
float f = BitConverter.ToSingle(value, 0);
|
float f = BitConverter.ToSingle(value, 0);
|
||||||
if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen
|
if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen
|
||||||
((FsmFloat)v.Variable).Value = f;
|
SetFloat(v, f);
|
||||||
break;
|
break;
|
||||||
case 2: ((FsmInt)v.Variable).Value = BitConverter.ToInt32(value, 0); break;
|
case 2: ((FsmInt)v.Variable).Value = BitConverter.ToInt32(value, 0); break;
|
||||||
default: ((FsmBool)v.Variable).Value = value[0] != 0; break;
|
default: ((FsmBool)v.Variable).Value = value[0] != 0; break;
|
||||||
|
|||||||
+1
-1
@@ -163,7 +163,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
nextDiscover = Time.time + DiscoverRetry;
|
nextDiscover = Time.time + DiscoverRetry;
|
||||||
tracked.Clear();
|
tracked.Clear();
|
||||||
foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f))
|
foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f))
|
||||||
{
|
{
|
||||||
if (fsm == null) continue;
|
if (fsm == null) continue;
|
||||||
if (Array.IndexOf(TrackedFsmNames, fsm.FsmName) < 0) continue;
|
if (Array.IndexOf(TrackedFsmNames, fsm.FsmName) < 0) continue;
|
||||||
|
|||||||
+31
-6
@@ -16,7 +16,12 @@ namespace MSCO
|
|||||||
public class WorldVarSync
|
public class WorldVarSync
|
||||||
{
|
{
|
||||||
/// <summary>Wurzelobjekte mit Jobs/Aufträgen/Laden.</summary>
|
/// <summary>Wurzelobjekte mit Jobs/Aufträgen/Laden.</summary>
|
||||||
public static readonly string[] Roots = { "JOBS", "STORE", "REPAIRSHOP", "INSPECTION", "LANDFILL", "DANCEHALL", "COTTAGE", "RALLY" };
|
public static readonly string[] Roots =
|
||||||
|
{
|
||||||
|
"JOBS", "STORE", "REPAIRSHOP", "INSPECTION", "LANDFILL", "DANCEHALL", "COTTAGE", "RALLY",
|
||||||
|
// Weitere Events/Orte: Dragrace (Zeiten, Anmeldung), Kilju-Käufer, Theater, Fußball, Dorf, Hütte
|
||||||
|
"DRAGRACE", "KILJUGUY", "THEATRE", "SOCCER", "PERAJARVI", "CABIN"
|
||||||
|
};
|
||||||
|
|
||||||
const float ScanInterval = 15f;
|
const float ScanInterval = 15f;
|
||||||
const float CheckInterval = 0.5f;
|
const float CheckInterval = 0.5f;
|
||||||
@@ -41,6 +46,8 @@ namespace MSCO
|
|||||||
readonly Dictionary<int, Var> vars = new Dictionary<int, Var>();
|
readonly Dictionary<int, Var> vars = new Dictionary<int, Var>();
|
||||||
readonly HashSet<PlayMakerFSM> scanned = new HashSet<PlayMakerFSM>();
|
readonly HashSet<PlayMakerFSM> scanned = new HashSet<PlayMakerFSM>();
|
||||||
readonly List<Var> outgoing = new List<Var>();
|
readonly List<Var> outgoing = new List<Var>();
|
||||||
|
// Werte für noch nicht gefundene FSMs (Server-Snapshot vor dem ersten Suchlauf): [typ, wert]
|
||||||
|
readonly Dictionary<int, KeyValuePair<byte, int>> pending = new Dictionary<int, KeyValuePair<byte, int>>();
|
||||||
float nextScan, nextCheck;
|
float nextScan, nextCheck;
|
||||||
|
|
||||||
public int Count { get { return vars.Count; } }
|
public int Count { get { return vars.Count; } }
|
||||||
@@ -49,11 +56,13 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
vars.Clear();
|
vars.Clear();
|
||||||
scanned.Clear();
|
scanned.Clear();
|
||||||
nextScan = 0f;
|
pending.Clear();
|
||||||
|
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(Action<MsgType, byte[]> send)
|
public void Update(Action<MsgType, byte[]> send)
|
||||||
{
|
{
|
||||||
|
if (nextScan < 0f) nextScan = Time.time + 3.8f; // nicht alle Sync-Teile im selben Frame suchen lassen
|
||||||
if (Time.time >= nextScan)
|
if (Time.time >= nextScan)
|
||||||
{
|
{
|
||||||
nextScan = Time.time + ScanInterval;
|
nextScan = Time.time + ScanInterval;
|
||||||
@@ -101,16 +110,20 @@ namespace MSCO
|
|||||||
int iv = 0; bool bv = false;
|
int iv = 0; bool bv = false;
|
||||||
if (type == 2) iv = r.ReadInt32(); else if (type == 3) bv = r.ReadBoolean(); else return;
|
if (type == 2) iv = r.ReadInt32(); else if (type == 3) bv = r.ReadBoolean(); else return;
|
||||||
Var v;
|
Var v;
|
||||||
if (!vars.TryGetValue(key, out v) || v.Type != type) continue;
|
if (!vars.TryGetValue(key, out v))
|
||||||
if (type == 2) ((FsmInt)v.Variable).Value = iv; else ((FsmBool)v.Variable).Value = bv;
|
{
|
||||||
Remember(v); // nicht zurückschicken
|
if (pending.Count < 20000) pending[key] = new KeyValuePair<byte, int>(type, type == 2 ? iv : (bv ? 1 : 0));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (v.Type != type) continue;
|
||||||
|
Set(v, type == 2 ? iv : (bv ? 1 : 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scan()
|
void Scan()
|
||||||
{
|
{
|
||||||
int before = vars.Count;
|
int before = vars.Count;
|
||||||
foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f))
|
foreach (PlayMakerFSM fsm in SceneCache.Fsms(20f))
|
||||||
{
|
{
|
||||||
if (fsm == null || !scanned.Add(fsm)) continue;
|
if (fsm == null || !scanned.Add(fsm)) continue;
|
||||||
if (Array.IndexOf(Roots, fsm.transform.root.name) < 0) continue;
|
if (Array.IndexOf(Roots, fsm.transform.root.name) < 0) continue;
|
||||||
@@ -130,6 +143,18 @@ namespace MSCO
|
|||||||
var v = new Var { Key = key, Variable = variable, Type = type };
|
var v = new Var { Key = key, Variable = variable, Type = type };
|
||||||
Remember(v);
|
Remember(v);
|
||||||
vars[key] = v;
|
vars[key] = v;
|
||||||
|
KeyValuePair<byte, int> waiting;
|
||||||
|
if (pending.TryGetValue(key, out waiting))
|
||||||
|
{
|
||||||
|
pending.Remove(key);
|
||||||
|
if (waiting.Key == type) Set(v, waiting.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Set(Var v, int value)
|
||||||
|
{
|
||||||
|
if (v.Type == 2) ((FsmInt)v.Variable).Value = value; else ((FsmBool)v.Variable).Value = value != 0;
|
||||||
|
Remember(v); // nicht zurückschicken
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool Changed(Var v)
|
static bool Changed(Var v)
|
||||||
|
|||||||
Reference in New Issue
Block a user