Admin: spawn items (saved on the server), unban (menu tab + console), tabbed admin menu; dashboard needles and steering wheel synced; server browser row opens the profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 10:15:01 +02:00
co-authored by Claude Opus 5.5
parent 0c15459302
commit 2000fc677f
8 changed files with 294 additions and 33 deletions
+7
View File
@@ -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)
+65
View File
@@ -0,0 +1,65 @@
using System.Collections.Generic;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// 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.
/// </summary>
public static class ItemSpawner
{
/// <summary>Auswahl im Admin-Menü (Name ohne "(itemx)").</summary>
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<string, GameObject> templates = new Dictionary<string, GameObject>();
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<Rigidbody>() == null) continue;
if (best == null || (go.activeInHierarchy && !best.activeInHierarchy)) best = go; // am liebsten ein echtes in der Welt
}
templates[item] = best;
return best;
}
/// <summary>Item erzeugen. Gibt false zurück, wenn es im Spiel kein solches Item gibt.</summary>
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<Rigidbody>();
if (rb != null)
{
rb.isKinematic = false;
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
}
return true;
}
public static void ClearCache() { templates.Clear(); }
}
}
+3 -8
View File
@@ -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();
+1
View File
@@ -76,6 +76,7 @@
<Compile Include="MpCommand.cs" />
<Compile Include="Commands.cs" />
<Compile Include="MscoLog.cs" />
<Compile Include="ItemSpawner.cs" />
<Compile Include="Cheats.cs" />
<Compile Include="InputLock.cs" />
<Compile Include="Overlays.cs" />
+92 -10
View File
@@ -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<string>(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<string>(bannedNames));
}
}
List<AdminPlayer> LocalAdminList()
@@ -332,6 +340,56 @@ namespace MSCO
if (result.EndsWith("next to you")) AnnounceCheat("brought the Satsuma to themselves");
}
/// <summary>Admin: Item vor sich spawnen - bei allen, und auf dem Server gespeichert.</summary>
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);
}
/// <summary>Gespawntes Item eines Admins (live oder aus dem Weltzustand) nachbauen.</summary>
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);
}
/// <summary>Admin: Bann aufheben (Server: SteamID oder name:..., P2P: Name).</summary>
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<string>(bannedNames));
overlays.AddChat("", id + " is no longer banned", true);
}
/// <summary>Cheats für alle sichtbar im Chat melden.</summary>
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<string>();
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<int, Pose> 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<int, Pose> 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");
}
+2 -1
View File
@@ -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
+78 -10
View File
@@ -51,6 +51,8 @@ namespace MSCO
// Admin
public bool AdminOpen { get; private set; }
List<AdminPlayer> adminPlayers;
List<string> bannedIds;
int adminTab;
string adminReason = "";
int confirmBanId = -1;
Vector2 adminScroll;
@@ -92,6 +94,7 @@ namespace MSCO
}
public void SetAdminPlayers(List<AdminPlayer> players) { adminPlayers = players; }
public void SetBanned(List<string> 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
+46 -4
View File
@@ -7,13 +7,22 @@ using UnityEngine;
namespace MSCO
{
/// <summary>
/// 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).
/// </summary>
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
/// <summary>Objekte, deren Drehung eine Anzeige ist: Tacho-/Drehzahl-/Tank-/Temperaturzeiger, Lenkrad.</summary>
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<int, Light> Lights = new Dictionary<int, Light>();
public readonly Dictionary<int, AudioSource> Audio = new Dictionary<int, AudioSource>();
public readonly Dictionary<int, Transform> Dials = new Dictionary<int, Transform>();
}
class Snapshot
@@ -37,6 +47,7 @@ namespace MSCO
public float ReceivedAt;
public readonly Dictionary<int, bool> Lights = new Dictionary<int, bool>();
public readonly Dictionary<int, AudioState> Audio = new Dictionary<int, AudioState>();
public readonly Dictionary<int, Quaternion> Dials = new Dictionary<int, Quaternion>();
}
readonly Dictionary<int, Components> components = new Dictionary<int, Components>();
@@ -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<int, Transform> 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<int, Quaternion> 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<AudioSource>(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<Transform>(true))
if (IsDial(t.name)) c.Dials[Key(t, c.Root, seen)] = t;
components[b.Id] = c;
return c;
}