diff --git a/CHANGELOG.md b/CHANGELOG.md
index e29b559..b89a37a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,13 @@ Server und Mod müssen beide 2.7 sein (Server: `git pull && docker compose up -d
- Admin-Menü: Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen), GOTO / BRING (teleportieren)
- Fahrzeugzustand-Sync: Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schäden, Verschleiß, Kilometerstand, Lack
- `vehicles` in der Konsole zeigt die Position jedes Fahrzeugs
+- Admin-Menü mit Reitern: **PLAYERS** (Kick/Ban/GOTO/BRING, Cheats), **ITEMS** (Items spawnen – alle sehen sie, auf dem
+ Server gespeichert), **BANS** (Sperren aufheben); Server-Befehle `bans` / `unban`
+- Tacho, Drehzahlmesser, Tank-/Temperaturanzeige und Lenkrad werden beim Beifahrer mitbewegt
+- Flüssigerer Sync: Position wird aus der Geschwindigkeit vorausberechnet, 30 Updates/s
+- Letzte Position wird zuverlässig gespeichert (nicht mehr immer an der Toilette spawnen)
+- Server-Browser: Klick auf den Server öffnet das Profil (JOIN-Knopf in der Liste entfernt)
+- Alle Mod-Meldungen nur noch in der MSCO-Konsole (F7)
**Behoben**
- Auto fiel beim Fahren eines anderen Spielers auseinander (angebaute Teile rissen an ihren Gelenken ab)
diff --git a/ItemSpawner.cs b/ItemSpawner.cs
new file mode 100644
index 0000000..6ee291c
--- /dev/null
+++ b/ItemSpawner.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace MSCO
+{
+ ///
+ /// Admin: Items spawnen. Kopiert ein vorhandenes Item des Spiels (z.B. "beer case(itemx)" aus dem Laden) und
+ /// setzt die Kopie an die gewünschte Stelle. Die Kopie behält den Namen des Originals, damit sich die Spiel-Logik
+ /// (Trinken, Essen, Tanken ...) wie beim echten Item verhält und der Objekt-Sync sie bei allen gleich benennt.
+ ///
+ public static class ItemSpawner
+ {
+ /// Auswahl im Admin-Menü (Name ohne "(itemx)").
+ public static readonly string[] Items =
+ {
+ "beer case", "sausages", "pizza", "macaron box", "milk", "juice", "ground coffee", "sugar", "yeast", "booze",
+ "gasoline", "diesel", "spray can", "fire extinguisher", "flashlight", "lantern", "car jack", "floor jack",
+ "spanner set", "sledgehammer", "radio", "camera", "grill charcoal", "fireworks bag", "water bucket"
+ };
+
+ static readonly Dictionary templates = new Dictionary();
+
+ static GameObject FindTemplate(string item)
+ {
+ GameObject cached;
+ if (templates.TryGetValue(item, out cached) && cached != null) return cached;
+
+ string name = item + "(itemx)";
+ GameObject best = null;
+ foreach (Object o in Resources.FindObjectsOfTypeAll(typeof(GameObject)))
+ {
+ var go = (GameObject)o;
+ if (go.name != name || go.GetComponent() == null) continue;
+ if (best == null || (go.activeInHierarchy && !best.activeInHierarchy)) best = go; // am liebsten ein echtes in der Welt
+ }
+ templates[item] = best;
+ return best;
+ }
+
+ /// Item erzeugen. Gibt false zurück, wenn es im Spiel kein solches Item gibt.
+ public static bool Spawn(string item, Vector3 position, Quaternion rotation)
+ {
+ GameObject template = FindTemplate(item);
+ if (template == null)
+ {
+ MscoLog.Warning("Cannot spawn '" + item + "' - no such item in this game");
+ return false;
+ }
+ var clone = (GameObject)Object.Instantiate(template, position, rotation);
+ clone.name = template.name;
+ clone.transform.SetParent(null, true);
+ clone.SetActive(true);
+ Rigidbody rb = clone.GetComponent();
+ if (rb != null)
+ {
+ rb.isKinematic = false;
+ rb.velocity = Vector3.zero;
+ rb.angularVelocity = Vector3.zero;
+ }
+ return true;
+ }
+
+ public static void ClearCache() { templates.Clear(); }
+ }
+}
diff --git a/LobbyUI.cs b/LobbyUI.cs
index 4754cbb..ce3f456 100644
--- a/LobbyUI.cs
+++ b/LobbyUI.cs
@@ -217,8 +217,8 @@ namespace MSCO
Rect row = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
GUI.Box(new Rect(row.x, row.y + 2, row.width, row.height - 4), GUIContent.none, rowBox);
- // Klick auf die Zeile (außer JOIN) öffnet das Server-Profil
- if (GUI.Button(new Rect(row.x, row.y, row.width - 82, row.height), GUIContent.none, GUIStyle.none))
+ // Klick auf die Zeile öffnet das Server-Profil (dort wird verbunden)
+ if (GUI.Button(new Rect(row.x, row.y, row.width, row.height), GUIContent.none, GUIStyle.none))
selectedServer = s.Address + ":" + s.Port;
GUI.DrawTexture(new Rect(row.x + 10, row.y + 13, 8, 8), s.Ping < 0 ? texPending : texOnline);
@@ -236,12 +236,7 @@ namespace MSCO
GUI.Label(new Rect(row.x + row.width * 0.5f, row.y, 70, row.height), s.Players + "/" + s.MaxPlayers, rowNameStyle);
Pill(row.x + row.width * 0.62f, row, s.RequireSteam ? "STEAM" : "NO STEAM", s.RequireSteam ? pillStyle : pillMutedStyle);
GUI.Label(new Rect(row.x + row.width * 0.76f, row.y, 50, row.height), s.Ping < 0 ? "..." : s.Ping + " ms", rowNameStyle);
-
- // Andere Version: trotzdem joinbar, beim Verbinden kommt eine Warnung
- GUI.enabled = CanJoin(mp, s);
- if (GUI.Button(new Rect(row.xMax - 76, row.y + 4, 70, row.height - 8), s.Players >= s.MaxPlayers ? "FULL" : "JOIN", primaryStyle))
- mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers);
- GUI.enabled = true;
+ GUI.Label(new Rect(row.xMax - 30, row.y, 20, row.height), "›", headerCellStyle); // Hinweis: Profil öffnen
}
GUILayout.EndScrollView();
diff --git a/MSCO.csproj b/MSCO.csproj
index 3e4c1b7..35c6fc1 100644
--- a/MSCO.csproj
+++ b/MSCO.csproj
@@ -76,6 +76,7 @@
+
diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs
index 0dbba41..5d801ba 100644
--- a/MultiplayerManager.cs
+++ b/MultiplayerManager.cs
@@ -206,7 +206,11 @@ namespace MSCO
overlays.OpenAdmin(null);
RequestAdminList();
}
- else if (session.IsHost) overlays.OpenAdmin(LocalAdminList());
+ else if (session.IsHost)
+ {
+ overlays.OpenAdmin(LocalAdminList());
+ overlays.SetBanned(new List(bannedNames));
+ }
else overlays.Log("Only the host can kick or ban players.");
}
@@ -214,7 +218,11 @@ namespace MSCO
{
if (session == null) return;
if (dedicated) session.SendTo(NetSession.HostId, MsgType.AdminListRequest);
- else if (session.IsHost) overlays.SetAdminPlayers(LocalAdminList());
+ else if (session.IsHost)
+ {
+ overlays.SetAdminPlayers(LocalAdminList());
+ overlays.SetBanned(new List(bannedNames));
+ }
}
List LocalAdminList()
@@ -332,6 +340,56 @@ namespace MSCO
if (result.EndsWith("next to you")) AnnounceCheat("brought the Satsuma to themselves");
}
+ /// Admin: Item vor sich spawnen - bei allen, und auf dem Server gespeichert.
+ public void SpawnItem(string item)
+ {
+ if (!CanCheat || localPlayer == null) return;
+ Transform head = playerCamera != null ? playerCamera.transform : localPlayer;
+ Vector3 forward = new Vector3(head.forward.x, 0f, head.forward.z).normalized;
+ if (forward.sqrMagnitude < 0.01f) forward = localPlayer.forward;
+ Vector3 pos = head.position + forward * 1.2f;
+ Quaternion rot = Quaternion.LookRotation(forward, Vector3.up);
+ if (!ItemSpawner.Spawn(item, pos, rot)) { overlays.Log("No '" + item + "' in this game"); return; }
+ rescanSoon = true;
+ SendGame(MsgType.SpawnItem, NetSession.Build(w =>
+ {
+ w.Write(item);
+ w.Write(pos.x); w.Write(pos.y); w.Write(pos.z);
+ w.Write(rot.x); w.Write(rot.y); w.Write(rot.z); w.Write(rot.w);
+ }));
+ AnnounceCheat("spawned " + item);
+ }
+
+ /// Gespawntes Item eines Admins (live oder aus dem Weltzustand) nachbauen.
+ static bool ApplySpawn(BinaryReader r)
+ {
+ string item = r.ReadString();
+ var pos = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
+ var rot = new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
+ if (System.Array.IndexOf(ItemSpawner.Items, item) < 0) return false; // nur bekannte Items
+ return ItemSpawner.Spawn(item, pos, rot);
+ }
+
+ /// Admin: Bann aufheben (Server: SteamID oder name:..., P2P: Name).
+ public void Unban(string id)
+ {
+ if (session == null) return;
+ if (dedicated)
+ {
+ session.SendTo(NetSession.HostId, MsgType.AdminAction, NetSession.Build(w =>
+ {
+ w.Write((byte)4);
+ w.Write(-1);
+ w.Write(id);
+ }));
+ return;
+ }
+ if (!session.IsHost) return;
+ bannedNames.Remove(id);
+ overlays.SetBanned(new List(bannedNames));
+ overlays.AddChat("", id + " is no longer banned", true);
+ }
+
/// Cheats für alle sichtbar im Chat melden.
void AnnounceCheat(string what)
{
@@ -807,7 +865,7 @@ namespace MSCO
{
return t == MsgType.Player || t == MsgType.ObjStates || t == MsgType.ObjOwn
|| t == MsgType.ObjRelease || t == MsgType.FsmEvent || t == MsgType.VehicleFx || t == MsgType.WorldTime || t == MsgType.MoneyDelta || t == MsgType.DoorState
- || t == MsgType.Chat || t == MsgType.VarSync;
+ || t == MsgType.Chat || t == MsgType.VarSync || t == MsgType.SpawnItem;
}
void OnMessage(int peerId, Message m)
@@ -996,6 +1054,13 @@ namespace MSCO
for (int i = 0; i < count; i++)
list.Add(new AdminPlayer { Id = r.ReadInt32(), Name = r.ReadString(), SteamId = r.ReadString(), IsAdmin = r.ReadBoolean() });
overlays.SetAdminPlayers(list);
+ var banned = new List();
+ if (r.BaseStream.Length - r.BaseStream.Position >= 4)
+ {
+ int bans = r.ReadInt32();
+ for (int i = 0; i < bans; i++) banned.Add(r.ReadString());
+ }
+ overlays.SetBanned(banned);
break;
}
@@ -1066,6 +1131,9 @@ namespace MSCO
case MsgType.VarSync:
vehicleState.Handle(from, r, objects, localId);
break;
+ case MsgType.SpawnItem:
+ if (ApplySpawn(r)) rescanSoon = true; // neues Item gleich in den Objekt-Sync aufnehmen
+ break;
}
}
@@ -1407,13 +1475,6 @@ namespace MSCO
if ((i + 1) % SnapshotEventsPerFrame == 0) yield return null; // nicht alles in einem Frame
}
- // Abgebaute Teile sind jetzt eigene Objekte: erfassen, dann alle Positionen setzen
- yield return null;
- objects.ScanNow();
- int applied = 0;
- foreach (KeyValuePair p in poses)
- if (objects.ApplySnapshotPose(p.Key, p.Value.Position, p.Value.Rotation)) applied++;
-
// Geld: Summe aller Änderungen seit dem Spielstand des Servers
if (r.BaseStream.Length - r.BaseStream.Position >= 4) moneySync.Apply(r.ReadSingle());
@@ -1427,6 +1488,27 @@ namespace MSCO
// Fahrzeugzustand (Tank, Öl, Schrauben ...)
if (r.BaseStream.Length - r.BaseStream.Position >= 4) vehicleState.ApplySnapshot(r);
+ // Von Admins gespawnte Items (in der Reihenfolge des Servers, damit alle sie gleich benennen)
+ int spawned = 0;
+ if (r.BaseStream.Length - r.BaseStream.Position >= 4)
+ {
+ int spawns = r.ReadInt32();
+ for (int i = 0; i < spawns; i++)
+ {
+ byte[] payload = r.ReadBytes(r.ReadInt32());
+ try { if (ApplySpawn(new BinaryReader(new MemoryStream(payload)))) spawned++; }
+ catch (System.Exception e) { MscoLog.Warning("Could not spawn a saved item: " + e.Message); }
+ }
+ }
+
+ // Abgebaute Teile und gespawnte Items sind jetzt eigene Objekte: erfassen, dann alle Positionen setzen
+ yield return null;
+ objects.ScanNow();
+ int applied = 0;
+ foreach (KeyValuePair p in poses)
+ if (objects.ApplySnapshotPose(p.Key, p.Value.Position, p.Value.Rotation)) applied++;
+ if (spawned > 0) MscoLog.Info(spawned + " spawned item(s) restored");
+
MscoLog.Info("[MSCO] World synced: " + applied + "/" + objectCount + " objects, " + eventCount + " part/bolt actions");
SetStatus("Connected");
}
diff --git a/NetSession.cs b/NetSession.cs
index b34fd35..3d3835a 100644
--- a/NetSession.cs
+++ b/NetSession.cs
@@ -39,7 +39,8 @@ namespace MSCO
Chat = 28, // Chatnachricht (string)
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
Teleport = 30, // Admin holt einen Spieler zu sich: 3 float Position
- Pong = 31 // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
+ Pong = 31, // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
+ SpawnItem = 32 // Admin spawnt ein Item: string Item-Name, 3 float Position, 4 float Drehung
}
public struct Message
diff --git a/Overlays.cs b/Overlays.cs
index 88951e3..4950267 100644
--- a/Overlays.cs
+++ b/Overlays.cs
@@ -51,6 +51,8 @@ namespace MSCO
// Admin
public bool AdminOpen { get; private set; }
List adminPlayers;
+ List bannedIds;
+ int adminTab;
string adminReason = "";
int confirmBanId = -1;
Vector2 adminScroll;
@@ -92,6 +94,7 @@ namespace MSCO
}
public void SetAdminPlayers(List players) { adminPlayers = players; }
+ public void SetBanned(List ids) { bannedIds = ids; }
public void CloseAll()
{
@@ -263,7 +266,7 @@ namespace MSCO
void DrawAdmin(MultiplayerManager mp)
{
- float width = Mathf.Min(680, Screen.width - 40), height = Mathf.Min(560, 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);
GUI.Box(rect, GUIContent.none, panel);
@@ -281,13 +284,30 @@ namespace MSCO
GUILayout.Space(8);
if (GUILayout.Button("CLOSE", secondary, GUILayout.Height(30), GUILayout.Width(90))) AdminOpen = false;
GUILayout.EndHorizontal();
- GUILayout.Label(mp.IsDedicated ? "Players on this server. Banned players cannot join again." : "Players in your game.", small);
+ GUILayout.Space(6);
+
+ // Reiter
+ GUILayout.BeginHorizontal();
+ string[] tabs = { "PLAYERS", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : "") };
+ for (int i = 0; i < tabs.Length; i++)
+ {
+ if (GUILayout.Button(tabs[i], adminTab == i ? primary : secondary, GUILayout.Height(30), GUILayout.Width(130))) adminTab = i;
+ GUILayout.Space(6);
+ }
+ GUILayout.EndHorizontal();
GUILayout.Space(10);
+ if (adminTab == 1) DrawAdminItems(mp);
+ else if (adminTab == 2) DrawAdminBans(mp);
+ else DrawAdminPlayers(mp);
+ GUILayout.EndArea();
+ }
+
+ void DrawAdminPlayers(MultiplayerManager mp)
+ {
// Cheats für den eigenen Spieler (nur im Spiel)
if (mp.CanCheat)
{
- GUILayout.Label("CHEATS", caption);
GUILayout.BeginHorizontal();
if (GUILayout.Button("+1.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(1000f);
GUILayout.Space(6);
@@ -297,13 +317,13 @@ namespace MSCO
GUILayout.Space(6);
if (GUILayout.Button("SATSUMA TO ME", primary, GUILayout.Height(30))) mp.BringCar();
GUILayout.EndHorizontal();
- GUILayout.Label("Cheats are announced in the chat. GOTO / BRING teleport you to a player or a player to you.", small);
- GUILayout.Space(10);
+ GUILayout.Label("Cheats are announced in the chat. GOTO teleports you to a player, BRING teleports a player to you.", small);
+ GUILayout.Space(8);
}
- GUILayout.Label("REASON (optional)", caption);
+ GUILayout.Label("REASON FOR KICK / BAN (optional)", caption);
adminReason = GUILayout.TextField(adminReason, 100, field, GUILayout.Height(32));
- GUILayout.Space(12);
+ GUILayout.Space(10);
adminScroll = GUILayout.BeginScrollView(adminScroll);
if (adminPlayers == null) GUILayout.Label("Loading players...", small);
@@ -330,11 +350,11 @@ namespace MSCO
GUILayout.Space(12);
}
GUI.enabled = !self && !p.IsAdmin;
- if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(80)))
+ if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(70)))
mp.AdminAction(false, p.Id, adminReason);
GUILayout.Space(6);
bool confirming = confirmBanId == p.Id;
- if (GUILayout.Button(confirming ? "SURE?" : "BAN", danger, GUILayout.Height(30), GUILayout.Width(80)))
+ if (GUILayout.Button(confirming ? "SURE?" : "BAN", danger, GUILayout.Height(30), GUILayout.Width(70)))
{
if (confirming) { mp.AdminAction(true, p.Id, adminReason); confirmBanId = -1; }
else confirmBanId = p.Id;
@@ -345,7 +365,55 @@ namespace MSCO
}
}
GUILayout.EndScrollView();
- GUILayout.EndArea();
+ }
+
+ void DrawAdminItems(MultiplayerManager mp)
+ {
+ if (!mp.CanCheat)
+ {
+ GUILayout.Label("Items can only be spawned in the game.", small);
+ return;
+ }
+ GUILayout.Label("Click an item to spawn it in front of you. Everyone sees it, and it is saved on the server.", small);
+ GUILayout.Space(8);
+ adminScroll = GUILayout.BeginScrollView(adminScroll);
+ const int perRow = 4;
+ for (int i = 0; i < ItemSpawner.Items.Length; i += perRow)
+ {
+ GUILayout.BeginHorizontal();
+ for (int k = i; k < Mathf.Min(i + perRow, ItemSpawner.Items.Length); k++)
+ {
+ string item = ItemSpawner.Items[k];
+ if (GUILayout.Button(item.ToUpper(), secondary, GUILayout.Height(34), GUILayout.Width(155))) mp.SpawnItem(item);
+ GUILayout.Space(6);
+ }
+ GUILayout.EndHorizontal();
+ GUILayout.Space(6);
+ }
+ GUILayout.EndScrollView();
+ }
+
+ void DrawAdminBans(MultiplayerManager mp)
+ {
+ GUILayout.Label(mp.IsDedicated ? "Banned players cannot join this server. UNBAN lets them in again."
+ : "Players banned from your game in this session.", small);
+ GUILayout.Space(8);
+ adminScroll = GUILayout.BeginScrollView(adminScroll);
+ if (bannedIds == null) GUILayout.Label("Loading...", small);
+ else if (bannedIds.Count == 0) GUILayout.Label("Nobody is banned.", small);
+ else
+ {
+ foreach (string id in bannedIds)
+ {
+ GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(40));
+ GUILayout.Label(id.StartsWith("name:") ? id.Substring(5) + " (no Steam)" : "SteamID " + id, chatName);
+ GUILayout.FlexibleSpace();
+ if (GUILayout.Button("UNBAN", primary, GUILayout.Height(28), GUILayout.Width(90))) mp.Unban(id);
+ GUILayout.EndHorizontal();
+ GUILayout.Space(4);
+ }
+ }
+ GUILayout.EndScrollView();
}
// ---------------------------------------------------------------- Stile
diff --git a/VehicleFx.cs b/VehicleFx.cs
index 4484427..436a090 100644
--- a/VehicleFx.cs
+++ b/VehicleFx.cs
@@ -7,13 +7,22 @@ using UnityEngine;
namespace MSCO
{
///
- /// Lichter und Dauergeräusche (Motor, Blinker, Hupe, Radio ...) von Fahrzeugen.
- /// Wer ein Fahrzeug besitzt (fährt/schiebt), schickt regelmäßig deren Zustand;
- /// bei allen anderen wird dieser Zustand jedes Frame übernommen.
+ /// Lichter, Dauergeräusche (Motor, Blinker, Hupe, Radio ...) und Anzeigen (Tacho, Drehzahlmesser, Tank-/Temperatur-
+ /// zeiger, Lenkrad) von Fahrzeugen. Wer ein Fahrzeug besitzt (fährt/schiebt), schickt regelmäßig deren Zustand;
+ /// bei allen anderen wird dieser Zustand jedes Frame übernommen. Die Zeiger bewegen sich beim Beifahrer sonst nicht,
+ /// weil das Auto dort nicht selbst fährt (die Spiel-Logik sieht Geschwindigkeit/Drehzahl 0).
///
public class VehicleFx
{
- const float SendInterval = 0.2f;
+ const float SendInterval = 0.1f;
+ const float DialSmoothing = 14f; // Zeiger gleiten zwischen den Paketen weich zur neuen Stellung
+
+ /// Objekte, deren Drehung eine Anzeige ist: Tacho-/Drehzahl-/Tank-/Temperaturzeiger, Lenkrad.
+ static bool IsDial(string name)
+ {
+ string n = name.ToLower();
+ return n.Contains("needle") || n.Contains("steeringwheel");
+ }
const float CacheRefresh = 5f; // Teile können an-/abgebaut werden
const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen
@@ -30,6 +39,7 @@ namespace MSCO
public Transform Root;
public readonly Dictionary Lights = new Dictionary();
public readonly Dictionary Audio = new Dictionary();
+ public readonly Dictionary Dials = new Dictionary();
}
class Snapshot
@@ -37,6 +47,7 @@ namespace MSCO
public float ReceivedAt;
public readonly Dictionary Lights = new Dictionary();
public readonly Dictionary Audio = new Dictionary();
+ public readonly Dictionary Dials = new Dictionary();
}
readonly Dictionary components = new Dictionary();
@@ -82,6 +93,17 @@ namespace MSCO
w.Write(a.Value.pitch);
w.Write(a.Value.volume);
}
+ // Anzeigen (am Ende angehängt - ältere Versionen lesen hier nicht weiter)
+ int dialCount = 0;
+ foreach (Transform t in c.Dials.Values) if (t != null) dialCount++;
+ w.Write(dialCount);
+ foreach (KeyValuePair d in c.Dials)
+ {
+ if (d.Value == null) continue;
+ Quaternion q = d.Value.localRotation;
+ w.Write(d.Key);
+ w.Write(q.x); w.Write(q.y); w.Write(q.z); w.Write(q.w);
+ }
}));
}
}
@@ -100,6 +122,16 @@ namespace MSCO
int key = r.ReadInt32();
s.Audio[key] = new AudioState { Playing = r.ReadBoolean(), Pitch = r.ReadSingle(), Volume = r.ReadSingle() };
}
+ if (r.BaseStream.Length - r.BaseStream.Position >= 4)
+ {
+ int dials = r.ReadInt32();
+ for (int i = 0; i < dials; i++)
+ {
+ int key = r.ReadInt32();
+ var q = new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
+ if (!float.IsNaN(q.x) && !float.IsNaN(q.w)) s.Dials[key] = q;
+ }
+ }
incoming[id] = s;
}
@@ -124,6 +156,13 @@ namespace MSCO
AudioSource src;
if (c.Audio.TryGetValue(a.Key, out src) && src != null) SetAudio(src, a.Value);
}
+ float t = Mathf.Clamp01(Time.deltaTime * DialSmoothing);
+ foreach (KeyValuePair d in kv.Value.Dials)
+ {
+ Transform dial;
+ if (c.Dials.TryGetValue(d.Key, out dial) && dial != null)
+ dial.localRotation = Quaternion.Slerp(dial.localRotation, d.Value, t);
+ }
}
}
@@ -164,6 +203,9 @@ namespace MSCO
seen.Clear();
foreach (AudioSource a in b.Rb.GetComponentsInChildren(true))
if (a.loop) c.Audio[Key(a.transform, c.Root, seen)] = a; // nur Dauergeräusche
+ seen.Clear();
+ foreach (Transform t in b.Rb.GetComponentsInChildren(true))
+ if (IsDial(t.name)) c.Dials[Key(t, c.Root, seen)] = t;
components[b.Id] = c;
return c;
}