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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user