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)
|
- 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
|
- Fahrzeugzustand-Sync: Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schäden, Verschleiß, Kilometerstand, Lack
|
||||||
- `vehicles` in der Konsole zeigt die Position jedes Fahrzeugs
|
- `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**
|
**Behoben**
|
||||||
- Auto fiel beim Fahren eines anderen Spielers auseinander (angebaute Teile rissen an ihren Gelenken ab)
|
- 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));
|
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);
|
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
|
// Klick auf die Zeile öffnet das Server-Profil (dort wird verbunden)
|
||||||
if (GUI.Button(new Rect(row.x, row.y, row.width - 82, row.height), GUIContent.none, GUIStyle.none))
|
if (GUI.Button(new Rect(row.x, row.y, row.width, row.height), GUIContent.none, GUIStyle.none))
|
||||||
selectedServer = s.Address + ":" + s.Port;
|
selectedServer = s.Address + ":" + s.Port;
|
||||||
|
|
||||||
GUI.DrawTexture(new Rect(row.x + 10, row.y + 13, 8, 8), s.Ping < 0 ? texPending : texOnline);
|
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);
|
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);
|
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);
|
GUI.Label(new Rect(row.x + row.width * 0.76f, row.y, 50, row.height), s.Ping < 0 ? "..." : s.Ping + " ms", rowNameStyle);
|
||||||
|
GUI.Label(new Rect(row.xMax - 30, row.y, 20, row.height), "›", headerCellStyle); // Hinweis: Profil öffnen
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
GUILayout.EndScrollView();
|
GUILayout.EndScrollView();
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
<Compile Include="MpCommand.cs" />
|
<Compile Include="MpCommand.cs" />
|
||||||
<Compile Include="Commands.cs" />
|
<Compile Include="Commands.cs" />
|
||||||
<Compile Include="MscoLog.cs" />
|
<Compile Include="MscoLog.cs" />
|
||||||
|
<Compile Include="ItemSpawner.cs" />
|
||||||
<Compile Include="Cheats.cs" />
|
<Compile Include="Cheats.cs" />
|
||||||
<Compile Include="InputLock.cs" />
|
<Compile Include="InputLock.cs" />
|
||||||
<Compile Include="Overlays.cs" />
|
<Compile Include="Overlays.cs" />
|
||||||
|
|||||||
+92
-10
@@ -206,7 +206,11 @@ namespace MSCO
|
|||||||
overlays.OpenAdmin(null);
|
overlays.OpenAdmin(null);
|
||||||
RequestAdminList();
|
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.");
|
else overlays.Log("Only the host can kick or ban players.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +218,11 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
if (session == null) return;
|
if (session == null) return;
|
||||||
if (dedicated) session.SendTo(NetSession.HostId, MsgType.AdminListRequest);
|
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()
|
List<AdminPlayer> LocalAdminList()
|
||||||
@@ -332,6 +340,56 @@ namespace MSCO
|
|||||||
if (result.EndsWith("next to you")) AnnounceCheat("brought the Satsuma to themselves");
|
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>
|
/// <summary>Cheats für alle sichtbar im Chat melden.</summary>
|
||||||
void AnnounceCheat(string what)
|
void AnnounceCheat(string what)
|
||||||
{
|
{
|
||||||
@@ -807,7 +865,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
return t == MsgType.Player || t == MsgType.ObjStates || t == MsgType.ObjOwn
|
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.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)
|
void OnMessage(int peerId, Message m)
|
||||||
@@ -996,6 +1054,13 @@ namespace MSCO
|
|||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
list.Add(new AdminPlayer { Id = r.ReadInt32(), Name = r.ReadString(), SteamId = r.ReadString(), IsAdmin = r.ReadBoolean() });
|
list.Add(new AdminPlayer { Id = r.ReadInt32(), Name = r.ReadString(), SteamId = r.ReadString(), IsAdmin = r.ReadBoolean() });
|
||||||
overlays.SetAdminPlayers(list);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1066,6 +1131,9 @@ namespace MSCO
|
|||||||
case MsgType.VarSync:
|
case MsgType.VarSync:
|
||||||
vehicleState.Handle(from, r, objects, localId);
|
vehicleState.Handle(from, r, objects, localId);
|
||||||
break;
|
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
|
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
|
// Geld: Summe aller Änderungen seit dem Spielstand des Servers
|
||||||
if (r.BaseStream.Length - r.BaseStream.Position >= 4) moneySync.Apply(r.ReadSingle());
|
if (r.BaseStream.Length - r.BaseStream.Position >= 4) moneySync.Apply(r.ReadSingle());
|
||||||
|
|
||||||
@@ -1427,6 +1488,27 @@ namespace MSCO
|
|||||||
// Fahrzeugzustand (Tank, Öl, Schrauben ...)
|
// Fahrzeugzustand (Tank, Öl, Schrauben ...)
|
||||||
if (r.BaseStream.Length - r.BaseStream.Position >= 4) vehicleState.ApplySnapshot(r);
|
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");
|
MscoLog.Info("[MSCO] World synced: " + applied + "/" + objectCount + " objects, " + eventCount + " part/bolt actions");
|
||||||
SetStatus("Connected");
|
SetStatus("Connected");
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -39,7 +39,8 @@ namespace MSCO
|
|||||||
Chat = 28, // Chatnachricht (string)
|
Chat = 28, // Chatnachricht (string)
|
||||||
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
|
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
|
||||||
Teleport = 30, // Admin holt einen Spieler zu sich: 3 float Position
|
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
|
public struct Message
|
||||||
|
|||||||
+78
-10
@@ -51,6 +51,8 @@ namespace MSCO
|
|||||||
// Admin
|
// Admin
|
||||||
public bool AdminOpen { get; private set; }
|
public bool AdminOpen { get; private set; }
|
||||||
List<AdminPlayer> adminPlayers;
|
List<AdminPlayer> adminPlayers;
|
||||||
|
List<string> bannedIds;
|
||||||
|
int adminTab;
|
||||||
string adminReason = "";
|
string adminReason = "";
|
||||||
int confirmBanId = -1;
|
int confirmBanId = -1;
|
||||||
Vector2 adminScroll;
|
Vector2 adminScroll;
|
||||||
@@ -92,6 +94,7 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void SetAdminPlayers(List<AdminPlayer> players) { adminPlayers = players; }
|
public void SetAdminPlayers(List<AdminPlayer> players) { adminPlayers = players; }
|
||||||
|
public void SetBanned(List<string> ids) { bannedIds = ids; }
|
||||||
|
|
||||||
public void CloseAll()
|
public void CloseAll()
|
||||||
{
|
{
|
||||||
@@ -263,7 +266,7 @@ namespace MSCO
|
|||||||
|
|
||||||
void DrawAdmin(MultiplayerManager mp)
|
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);
|
var rect = new Rect((Screen.width - width) / 2f, (Screen.height - height) / 2f, width, height);
|
||||||
GUI.Box(rect, GUIContent.none, panel);
|
GUI.Box(rect, GUIContent.none, panel);
|
||||||
|
|
||||||
@@ -281,13 +284,30 @@ namespace MSCO
|
|||||||
GUILayout.Space(8);
|
GUILayout.Space(8);
|
||||||
if (GUILayout.Button("CLOSE", secondary, GUILayout.Height(30), GUILayout.Width(90))) AdminOpen = false;
|
if (GUILayout.Button("CLOSE", secondary, GUILayout.Height(30), GUILayout.Width(90))) AdminOpen = false;
|
||||||
GUILayout.EndHorizontal();
|
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);
|
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)
|
// Cheats für den eigenen Spieler (nur im Spiel)
|
||||||
if (mp.CanCheat)
|
if (mp.CanCheat)
|
||||||
{
|
{
|
||||||
GUILayout.Label("CHEATS", caption);
|
|
||||||
GUILayout.BeginHorizontal();
|
GUILayout.BeginHorizontal();
|
||||||
if (GUILayout.Button("+1.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(1000f);
|
if (GUILayout.Button("+1.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(1000f);
|
||||||
GUILayout.Space(6);
|
GUILayout.Space(6);
|
||||||
@@ -297,13 +317,13 @@ namespace MSCO
|
|||||||
GUILayout.Space(6);
|
GUILayout.Space(6);
|
||||||
if (GUILayout.Button("SATSUMA TO ME", primary, GUILayout.Height(30))) mp.BringCar();
|
if (GUILayout.Button("SATSUMA TO ME", primary, GUILayout.Height(30))) mp.BringCar();
|
||||||
GUILayout.EndHorizontal();
|
GUILayout.EndHorizontal();
|
||||||
GUILayout.Label("Cheats are announced in the chat. GOTO / BRING teleport you to a player or a player to you.", small);
|
GUILayout.Label("Cheats are announced in the chat. GOTO teleports you to a player, BRING teleports a player to you.", small);
|
||||||
GUILayout.Space(10);
|
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));
|
adminReason = GUILayout.TextField(adminReason, 100, field, GUILayout.Height(32));
|
||||||
GUILayout.Space(12);
|
GUILayout.Space(10);
|
||||||
|
|
||||||
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
||||||
if (adminPlayers == null) GUILayout.Label("Loading players...", small);
|
if (adminPlayers == null) GUILayout.Label("Loading players...", small);
|
||||||
@@ -330,11 +350,11 @@ namespace MSCO
|
|||||||
GUILayout.Space(12);
|
GUILayout.Space(12);
|
||||||
}
|
}
|
||||||
GUI.enabled = !self && !p.IsAdmin;
|
GUI.enabled = !self && !p.IsAdmin;
|
||||||
if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(80)))
|
if (GUILayout.Button("KICK", secondary, GUILayout.Height(30), GUILayout.Width(70)))
|
||||||
mp.AdminAction(false, p.Id, adminReason);
|
mp.AdminAction(false, p.Id, adminReason);
|
||||||
GUILayout.Space(6);
|
GUILayout.Space(6);
|
||||||
bool confirming = confirmBanId == p.Id;
|
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; }
|
if (confirming) { mp.AdminAction(true, p.Id, adminReason); confirmBanId = -1; }
|
||||||
else confirmBanId = p.Id;
|
else confirmBanId = p.Id;
|
||||||
@@ -345,7 +365,55 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
GUILayout.EndScrollView();
|
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
|
// ---------------------------------------------------------------- Stile
|
||||||
|
|||||||
+46
-4
@@ -7,13 +7,22 @@ using UnityEngine;
|
|||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lichter und Dauergeräusche (Motor, Blinker, Hupe, Radio ...) von Fahrzeugen.
|
/// Lichter, Dauergeräusche (Motor, Blinker, Hupe, Radio ...) und Anzeigen (Tacho, Drehzahlmesser, Tank-/Temperatur-
|
||||||
/// Wer ein Fahrzeug besitzt (fährt/schiebt), schickt regelmäßig deren Zustand;
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
public class VehicleFx
|
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 CacheRefresh = 5f; // Teile können an-/abgebaut werden
|
||||||
const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen
|
const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen
|
||||||
|
|
||||||
@@ -30,6 +39,7 @@ namespace MSCO
|
|||||||
public Transform Root;
|
public Transform Root;
|
||||||
public readonly Dictionary<int, Light> Lights = new Dictionary<int, Light>();
|
public readonly Dictionary<int, Light> Lights = new Dictionary<int, Light>();
|
||||||
public readonly Dictionary<int, AudioSource> Audio = new Dictionary<int, AudioSource>();
|
public readonly Dictionary<int, AudioSource> Audio = new Dictionary<int, AudioSource>();
|
||||||
|
public readonly Dictionary<int, Transform> Dials = new Dictionary<int, Transform>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class Snapshot
|
class Snapshot
|
||||||
@@ -37,6 +47,7 @@ namespace MSCO
|
|||||||
public float ReceivedAt;
|
public float ReceivedAt;
|
||||||
public readonly Dictionary<int, bool> Lights = new Dictionary<int, bool>();
|
public readonly Dictionary<int, bool> Lights = new Dictionary<int, bool>();
|
||||||
public readonly Dictionary<int, AudioState> Audio = new Dictionary<int, AudioState>();
|
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>();
|
readonly Dictionary<int, Components> components = new Dictionary<int, Components>();
|
||||||
@@ -82,6 +93,17 @@ namespace MSCO
|
|||||||
w.Write(a.Value.pitch);
|
w.Write(a.Value.pitch);
|
||||||
w.Write(a.Value.volume);
|
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();
|
int key = r.ReadInt32();
|
||||||
s.Audio[key] = new AudioState { Playing = r.ReadBoolean(), Pitch = r.ReadSingle(), Volume = r.ReadSingle() };
|
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;
|
incoming[id] = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +156,13 @@ namespace MSCO
|
|||||||
AudioSource src;
|
AudioSource src;
|
||||||
if (c.Audio.TryGetValue(a.Key, out src) && src != null) SetAudio(src, a.Value);
|
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();
|
seen.Clear();
|
||||||
foreach (AudioSource a in b.Rb.GetComponentsInChildren<AudioSource>(true))
|
foreach (AudioSource a in b.Rb.GetComponentsInChildren<AudioSource>(true))
|
||||||
if (a.loop) c.Audio[Key(a.transform, c.Root, seen)] = a; // nur Dauergeräusche
|
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;
|
components[b.Id] = c;
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user