- Fixed-size admin window with equal-width tabs - VEHICLE: part search, PROBLEMS/category chips, collapsible fluids grid, one-line rows with state pill and wear bar - LOOSE state derived from actual tightness (DB "Bolted" lags behind) - ITEMS: search field and grid - Console and admin menu are mutually exclusive (console stole clicks) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
745 lines
36 KiB
C#
745 lines
36 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>Spieler im Admin-Menü.</summary>
|
|
public class AdminPlayer
|
|
{
|
|
public int Id;
|
|
public string Name;
|
|
public string SteamId;
|
|
public bool IsAdmin;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fenster über dem Spiel: F7-Konsole (alle MSCO-Befehle), Chat (T) und Admin-Menü (/admin).
|
|
/// Solange eins davon offen ist, sind Spielerbewegung und Kamera gesperrt (InputLock).
|
|
/// </summary>
|
|
public class Overlays
|
|
{
|
|
const int MaxConsoleLines = 300;
|
|
const int MaxChatLines = 60;
|
|
const float ChatFadeSeconds = 15f;
|
|
const string ConsoleControl = "msco_console";
|
|
const string ChatControl = "msco_chat";
|
|
|
|
class ChatLine
|
|
{
|
|
public string Name, Text;
|
|
public bool System;
|
|
public float Time;
|
|
}
|
|
|
|
// Konsole
|
|
public bool ConsoleOpen { get; private set; }
|
|
readonly List<string> consoleLines = new List<string>();
|
|
readonly List<string> history = new List<string>();
|
|
int historyIndex = -1;
|
|
string consoleInput = "";
|
|
Vector2 consoleScroll;
|
|
int consoleOpenedFrame = -1;
|
|
bool consoleScrollToEnd;
|
|
|
|
// Chat
|
|
public bool ChatOpen { get; private set; }
|
|
readonly List<ChatLine> chat = new List<ChatLine>();
|
|
string chatInput = "";
|
|
int chatOpenedFrame = -1;
|
|
|
|
// Admin
|
|
public bool AdminOpen { get; private set; }
|
|
List<AdminPlayer> adminPlayers;
|
|
List<string> bannedIds;
|
|
int adminTab;
|
|
string adminReason = "";
|
|
int confirmBanId = -1;
|
|
Vector2 adminScroll;
|
|
|
|
public bool AnyOpen { get { return ConsoleOpen || ChatOpen || AdminOpen; } }
|
|
|
|
// Stile
|
|
bool stylesReady;
|
|
GUIStyle panel, rowStyle, field, logText, caption, title, primary, secondary, danger, chatName, chatText, chatSystem, small;
|
|
|
|
// ---------------------------------------------------------------- Öffnen/Schließen
|
|
|
|
public void ToggleConsole()
|
|
{
|
|
ConsoleOpen = !ConsoleOpen;
|
|
if (ConsoleOpen)
|
|
{
|
|
ChatOpen = false;
|
|
AdminOpen = false; // sonst liegt die Konsole über dem Admin-Menü und schluckt dessen Klicks
|
|
consoleOpenedFrame = Time.frameCount;
|
|
consoleScrollToEnd = true;
|
|
}
|
|
}
|
|
|
|
public void OpenChat()
|
|
{
|
|
if (AnyOpen) return;
|
|
ChatOpen = true;
|
|
chatOpenedFrame = Time.frameCount;
|
|
chatInput = "";
|
|
}
|
|
|
|
public void OpenAdmin(List<AdminPlayer> players)
|
|
{
|
|
AdminOpen = true;
|
|
ConsoleOpen = false;
|
|
ChatOpen = false;
|
|
adminPlayers = players;
|
|
confirmBanId = -1;
|
|
}
|
|
|
|
public void SetAdminPlayers(List<AdminPlayer> players) { adminPlayers = players; }
|
|
public void SetBanned(List<string> ids) { bannedIds = ids; }
|
|
public void SetBackups(List<string> names) { backupNames = names; }
|
|
List<string> backupNames;
|
|
string confirmWorld; // "reset" oder Name des Backups, das wiederhergestellt werden soll
|
|
|
|
public void CloseAll()
|
|
{
|
|
ConsoleOpen = ChatOpen = AdminOpen = false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Inhalte
|
|
|
|
public void Log(string line)
|
|
{
|
|
foreach (string part in (line ?? "").Split('\n'))
|
|
{
|
|
consoleLines.Add(part);
|
|
if (consoleLines.Count > MaxConsoleLines) consoleLines.RemoveAt(0);
|
|
}
|
|
consoleScrollToEnd = true;
|
|
}
|
|
|
|
public void AddChat(string name, string text, bool system = false)
|
|
{
|
|
chat.Add(new ChatLine { Name = name, Text = text, System = system, Time = Time.realtimeSinceStartup });
|
|
if (chat.Count > MaxChatLines) chat.RemoveAt(0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Zeichnen (OnGUI)
|
|
|
|
public void Draw(MultiplayerManager mp, bool inGame)
|
|
{
|
|
EnsureStyles();
|
|
if (inGame) DrawChat(mp);
|
|
if (ConsoleOpen) DrawConsole();
|
|
if (AdminOpen) DrawAdmin(mp);
|
|
}
|
|
|
|
void DrawConsole()
|
|
{
|
|
float width = Mathf.Min(Screen.width - 40, 960);
|
|
var rect = new Rect((Screen.width - width) / 2f, 20, width, Mathf.Min(340, Screen.height * 0.5f));
|
|
UiTheme.DrawShadow(rect);
|
|
GUI.Box(rect, GUIContent.none, panel);
|
|
|
|
GUI.Label(new Rect(rect.x + 16, rect.y + 10, 300, 20), "MSCO CONSOLE", caption);
|
|
GUI.Label(new Rect(rect.xMax - 216, rect.y + 10, 200, 20), "type 'help' - F7 to close", small);
|
|
|
|
// Tasten: Enter ausführen, Esc schließen, Hoch/Runter Verlauf
|
|
Event e = Event.current;
|
|
if (e.type == EventType.KeyDown && GUI.GetNameOfFocusedControl() == ConsoleControl)
|
|
{
|
|
if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter)
|
|
{
|
|
RunConsoleLine(consoleInput);
|
|
consoleInput = "";
|
|
e.Use();
|
|
}
|
|
else if (e.keyCode == KeyCode.Escape) { ConsoleOpen = false; e.Use(); return; }
|
|
else if (e.keyCode == KeyCode.UpArrow && history.Count > 0)
|
|
{
|
|
historyIndex = historyIndex < 0 ? history.Count - 1 : Mathf.Max(0, historyIndex - 1);
|
|
consoleInput = history[historyIndex];
|
|
e.Use();
|
|
}
|
|
else if (e.keyCode == KeyCode.DownArrow && historyIndex >= 0)
|
|
{
|
|
historyIndex++;
|
|
consoleInput = historyIndex < history.Count ? history[historyIndex] : "";
|
|
if (historyIndex >= history.Count) historyIndex = -1;
|
|
e.Use();
|
|
}
|
|
}
|
|
|
|
GUILayout.BeginArea(new Rect(rect.x + 14, rect.y + 34, rect.width - 28, rect.height - 84));
|
|
if (consoleScrollToEnd && e.type == EventType.Repaint) { consoleScroll.y = float.MaxValue; consoleScrollToEnd = false; }
|
|
consoleScroll = GUILayout.BeginScrollView(consoleScroll);
|
|
foreach (string line in consoleLines) GUILayout.Label(line, logText);
|
|
GUILayout.EndScrollView();
|
|
GUILayout.EndArea();
|
|
|
|
// Taste, mit der die Konsole geöffnet wurde, nicht als Zeichen übernehmen
|
|
if (Time.frameCount == consoleOpenedFrame && e.type == EventType.KeyDown) e.Use();
|
|
GUI.SetNextControlName(ConsoleControl);
|
|
consoleInput = GUI.TextField(new Rect(rect.x + 14, rect.yMax - 46, rect.width - 28, 34), consoleInput, 200, field);
|
|
GUI.FocusControl(ConsoleControl);
|
|
}
|
|
|
|
void RunConsoleLine(string line)
|
|
{
|
|
line = (line ?? "").Trim();
|
|
if (line.Length == 0) return;
|
|
history.Add(line);
|
|
if (history.Count > 50) history.RemoveAt(0);
|
|
historyIndex = -1;
|
|
Log("> " + line);
|
|
|
|
if (line == "clear") { consoleLines.Clear(); return; }
|
|
string[] args = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (args.Length > 0 && args[0] == "mp") // "mp connect ..." geht auch
|
|
{
|
|
var rest = new string[args.Length - 1];
|
|
Array.Copy(args, 1, rest, 0, rest.Length);
|
|
args = rest;
|
|
}
|
|
Commands.Execute(args, Log);
|
|
}
|
|
|
|
void DrawChat(MultiplayerManager mp)
|
|
{
|
|
float now = Time.realtimeSinceStartup;
|
|
float width = Mathf.Min(460, Screen.width - 40);
|
|
float lineHeight = 20;
|
|
int maxLines = ChatOpen ? 12 : 6;
|
|
|
|
// Sichtbare Zeilen: offen = die letzten, geschlossen = nur neue (blenden aus)
|
|
var visible = new List<ChatLine>();
|
|
for (int i = chat.Count - 1; i >= 0 && visible.Count < maxLines; i--)
|
|
if (ChatOpen || now - chat[i].Time < ChatFadeSeconds) visible.Insert(0, chat[i]);
|
|
|
|
float bottom = Screen.height * 0.62f;
|
|
float height = visible.Count * lineHeight + (ChatOpen ? 52 : 12);
|
|
var rect = new Rect(20, bottom - height, width, height);
|
|
if (ChatOpen || visible.Count > 0) GUI.Box(rect, GUIContent.none, ChatOpen ? panel : rowStyle);
|
|
|
|
Color old = GUI.color;
|
|
float y = rect.y + 6;
|
|
foreach (ChatLine line in visible)
|
|
{
|
|
float age = now - line.Time;
|
|
float alpha = ChatOpen ? 1f : Mathf.Clamp01((ChatFadeSeconds - age) / 3f);
|
|
GUI.color = new Color(1f, 1f, 1f, alpha);
|
|
if (line.System)
|
|
{
|
|
GUI.Label(new Rect(rect.x + 12, y, width - 24, lineHeight), line.Text, chatSystem);
|
|
}
|
|
else
|
|
{
|
|
Vector2 size = chatName.CalcSize(new GUIContent(line.Name + ":"));
|
|
GUI.Label(new Rect(rect.x + 12, y, size.x, lineHeight), line.Name + ":", chatName);
|
|
GUI.Label(new Rect(rect.x + 16 + size.x, y, width - 28 - size.x, lineHeight), line.Text, chatText);
|
|
}
|
|
y += lineHeight;
|
|
}
|
|
GUI.color = old;
|
|
|
|
if (!ChatOpen) return;
|
|
|
|
Event e = Event.current;
|
|
if (e.type == EventType.KeyDown && GUI.GetNameOfFocusedControl() == ChatControl)
|
|
{
|
|
if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter)
|
|
{
|
|
string text = chatInput.Trim();
|
|
if (text.Length > 0)
|
|
{
|
|
if (text.StartsWith("/")) RunConsoleLine(text.Substring(1)); // z.B. /admin
|
|
else mp.SendChat(text);
|
|
}
|
|
chatInput = "";
|
|
ChatOpen = false;
|
|
e.Use();
|
|
return;
|
|
}
|
|
if (e.keyCode == KeyCode.Escape) { ChatOpen = false; e.Use(); return; }
|
|
}
|
|
|
|
// Taste, mit der der Chat geöffnet wurde (T), nicht als Zeichen übernehmen
|
|
if (Time.frameCount == chatOpenedFrame && e.type == EventType.KeyDown) e.Use();
|
|
GUI.SetNextControlName(ChatControl);
|
|
chatInput = GUI.TextField(new Rect(rect.x + 10, rect.yMax - 40, width - 20, 30), chatInput, 150, field);
|
|
GUI.FocusControl(ChatControl);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Admin-Menü
|
|
// Feste Fenstergröße für alle Reiter, Reiter gleich breit, Zeilen mit fest gesetzten Rechtecken (Texte mittig).
|
|
|
|
const float AdminRow = 40f, AdminGap = 6f;
|
|
GUIStyle nameMid, smallMid, captionMid, pillOk, pillWarn, pillBad, pillOff, chipOn, chipOff;
|
|
Texture2D texBarBack, texBarFill, texBarLow;
|
|
Vector2 teleportScroll, vehicleScroll;
|
|
string partFilter = "", itemFilter = "", partCategory = "";
|
|
bool partProblems, fluidsOpen = true;
|
|
|
|
void EnsureAdminStyles()
|
|
{
|
|
if (nameMid != null && texBarBack != null) return;
|
|
nameMid = new GUIStyle(chatName) { alignment = TextAnchor.MiddleLeft, wordWrap = false, clipping = TextClipping.Clip };
|
|
nameMid.margin = new RectOffset();
|
|
nameMid.padding = new RectOffset();
|
|
smallMid = new GUIStyle(small) { alignment = TextAnchor.MiddleLeft, wordWrap = false, clipping = TextClipping.Clip };
|
|
smallMid.margin = new RectOffset();
|
|
smallMid.padding = new RectOffset();
|
|
captionMid = new GUIStyle(caption) { alignment = TextAnchor.MiddleLeft };
|
|
captionMid.margin = new RectOffset();
|
|
pillOk = Pill(UiTheme.Online);
|
|
pillWarn = Pill(UiTheme.Pending);
|
|
pillBad = Pill(UiTheme.Danger);
|
|
pillOff = Pill(UiTheme.Muted);
|
|
chipOn = UiTheme.Button(new Color(UiTheme.Accent.r, UiTheme.Accent.g, UiTheme.Accent.b, 0.25f), new Color(UiTheme.Accent.r, UiTheme.Accent.g, UiTheme.Accent.b, 0.35f), UiTheme.AccentHover, 11);
|
|
chipOff = UiTheme.Button(UiTheme.Secondary, UiTheme.SecondaryHover, UiTheme.Muted, 11);
|
|
texBarBack = UiTheme.Solid(new Color(1f, 1f, 1f, 0.1f));
|
|
texBarFill = UiTheme.Solid(UiTheme.Online);
|
|
texBarLow = UiTheme.Solid(UiTheme.Danger);
|
|
}
|
|
|
|
static GUIStyle Pill(Color c)
|
|
{
|
|
GUIStyle st = UiTheme.Panel9(new Color(c.r, c.g, c.b, 0.18f), 4);
|
|
st.normal.textColor = c;
|
|
st.fontSize = 10;
|
|
st.fontStyle = FontStyle.Bold;
|
|
st.alignment = TextAnchor.MiddleCenter;
|
|
st.padding = new RectOffset(7, 7, 0, 0);
|
|
return st;
|
|
}
|
|
|
|
/// <summary>Zeile mit Hintergrund in einer Layout-Liste reservieren; gibt das Innere zurück (12 px Rand).</summary>
|
|
Rect AdminRowRect(float height)
|
|
{
|
|
Rect r = GUILayoutUtility.GetRect(10, height, GUILayout.ExpandWidth(true));
|
|
GUI.Box(new Rect(r.x, r.y + 2, r.width, r.height - 4), GUIContent.none, rowStyle);
|
|
return new Rect(r.x + 12, r.y + 2, r.width - 24, r.height - 4);
|
|
}
|
|
|
|
/// <summary>Knöpfe von rechts nach links in einer Zeile; gibt die linke Kante des belegten Bereichs zurück.</summary>
|
|
static bool RightButton(ref float right, Rect row, float width, string text, GUIStyle style, bool enabled = true)
|
|
{
|
|
right -= width;
|
|
bool old = GUI.enabled;
|
|
GUI.enabled = old && enabled;
|
|
bool clicked = GUI.Button(new Rect(right, row.y + (row.height - 28) / 2f, width, 28), text, style);
|
|
GUI.enabled = old;
|
|
right -= AdminGap;
|
|
return clicked;
|
|
}
|
|
|
|
void Bar(Rect r, float frac, float low)
|
|
{
|
|
var back = new Rect(r.x, r.y + (r.height - 6) / 2f, r.width, 6);
|
|
GUI.DrawTexture(back, texBarBack);
|
|
GUI.DrawTexture(new Rect(back.x, back.y, Mathf.Max(2f, back.width * Mathf.Clamp01(frac)), back.height), frac < low ? texBarLow : texBarFill);
|
|
}
|
|
|
|
void DrawAdmin(MultiplayerManager mp)
|
|
{
|
|
EnsureAdminStyles();
|
|
float width = Mathf.Min(1000, Screen.width - 40), height = Mathf.Min(720, Screen.height - 40);
|
|
var rect = new Rect(Mathf.Round((Screen.width - width) / 2f), Mathf.Round((Screen.height - height) / 2f), width, height);
|
|
UiTheme.DrawShadow(rect);
|
|
GUI.Box(rect, GUIContent.none, panel);
|
|
|
|
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape)
|
|
{
|
|
AdminOpen = false;
|
|
Event.current.Use();
|
|
return;
|
|
}
|
|
|
|
// Kopf
|
|
float x = rect.x + 20, w = rect.width - 40;
|
|
GUI.Label(new Rect(x, rect.y + 16, 300, 34), "Admin", title);
|
|
if (GUI.Button(new Rect(rect.xMax - 20 - 90, rect.y + 18, 90, 30), "CLOSE", secondary)) AdminOpen = false;
|
|
if (GUI.Button(new Rect(rect.xMax - 20 - 90 - 8 - 100, rect.y + 18, 100, 30), "REFRESH", secondary))
|
|
{
|
|
mp.RequestAdminList();
|
|
VehicleAdmin.Refresh(true);
|
|
}
|
|
|
|
// Reiter (gleich breit über die ganze Zeile)
|
|
string[] tabs = { "PLAYERS", "TELEPORT", "VEHICLE", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" };
|
|
float tabW = (w - AdminGap * (tabs.Length - 1)) / tabs.Length;
|
|
for (int i = 0; i < tabs.Length; i++)
|
|
if (GUI.Button(new Rect(x + i * (tabW + AdminGap), rect.y + 62, tabW, 32), tabs[i], adminTab == i ? primary : secondary)) adminTab = i;
|
|
|
|
GUILayout.BeginArea(new Rect(x, rect.y + 108, w, rect.height - 124));
|
|
if (adminTab == 1) DrawAdminTeleport(mp);
|
|
else if (adminTab == 2) DrawAdminVehicle(mp);
|
|
else if (adminTab == 3) DrawAdminItems(mp);
|
|
else if (adminTab == 4) DrawAdminBans(mp);
|
|
else if (adminTab == 5) DrawAdminWorld(mp);
|
|
else DrawAdminPlayers(mp);
|
|
GUILayout.EndArea();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- PLAYERS
|
|
|
|
void DrawAdminPlayers(MultiplayerManager mp)
|
|
{
|
|
if (mp.CanCheat)
|
|
{
|
|
Rect r = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
|
|
string[] labels = { "+1.000 MK", "+10.000 MK", "REFILL NEEDS", "SATSUMA TO ME" };
|
|
float bw = (r.width - AdminGap * 3) / 4f;
|
|
for (int i = 0; i < labels.Length; i++)
|
|
{
|
|
if (!GUI.Button(new Rect(r.x + i * (bw + AdminGap), r.y, bw, 32), labels[i], primary)) continue;
|
|
if (i == 0) mp.CheatMoney(1000f);
|
|
else if (i == 1) mp.CheatMoney(10000f);
|
|
else if (i == 2) mp.CheatNeeds();
|
|
else mp.BringCar();
|
|
}
|
|
GUILayout.Label("Cheats are announced in the chat. GOTO teleports you to a player, BRING teleports a player to you.", small);
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
Rect reason = GUILayoutUtility.GetRect(10, 32, GUILayout.ExpandWidth(true));
|
|
GUI.Label(new Rect(reason.x, reason.y, 210, 32), "REASON FOR KICK / BAN (optional)", captionMid);
|
|
adminReason = GUI.TextField(new Rect(reason.x + 214, reason.y, reason.width - 214, 32), adminReason ?? "", 100, field);
|
|
GUILayout.Space(10);
|
|
|
|
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
|
if (adminPlayers == null) GUILayout.Label("Loading players...", small);
|
|
else if (adminPlayers.Count == 0) GUILayout.Label("No players.", small);
|
|
else
|
|
{
|
|
foreach (AdminPlayer p in adminPlayers)
|
|
{
|
|
Rect row = AdminRowRect(54);
|
|
bool self = p.Id == mp.LocalId;
|
|
GUI.Label(new Rect(row.x, row.y + 6, row.width * 0.45f, 22), p.Name + (self ? " (you)" : "") + (p.IsAdmin ? " (admin)" : ""), nameMid);
|
|
GUI.Label(new Rect(row.x, row.y + 26, row.width * 0.45f, 18),
|
|
string.IsNullOrEmpty(p.SteamId) ? "id " + p.Id : p.SteamId.StartsWith("name:") ? "no Steam" : "SteamID " + p.SteamId, smallMid);
|
|
float right = row.xMax;
|
|
bool confirming = confirmBanId == p.Id;
|
|
if (RightButton(ref right, row, 76, confirming ? "SURE?" : "BAN", danger, !self && !p.IsAdmin))
|
|
{
|
|
if (confirming) { mp.AdminAction(true, p.Id, adminReason); confirmBanId = -1; }
|
|
else confirmBanId = p.Id;
|
|
}
|
|
if (RightButton(ref right, row, 76, "KICK", secondary, !self && !p.IsAdmin)) mp.AdminAction(false, p.Id, adminReason);
|
|
if (mp.IsDedicated && RightButton(ref right, row, 124, p.IsAdmin ? "REMOVE ADMIN" : "MAKE ADMIN", secondary, !self)) mp.SetAdmin(p.Id, !p.IsAdmin);
|
|
if (mp.CanCheat)
|
|
{
|
|
if (RightButton(ref right, row, 76, "BRING", secondary, !self)) mp.BringPlayer(p.Id);
|
|
if (RightButton(ref right, row, 76, "GOTO", secondary, !self)) mp.GotoPlayer(p.Id);
|
|
}
|
|
}
|
|
}
|
|
GUILayout.EndScrollView();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- TELEPORT
|
|
|
|
void DrawAdminTeleport(MultiplayerManager mp)
|
|
{
|
|
if (!mp.CanCheat)
|
|
{
|
|
GUILayout.Label("Teleporting only works in the game.", small);
|
|
return;
|
|
}
|
|
GUILayout.Label("Teleport yourself to a place. Players: use GOTO / BRING on the PLAYERS tab.", small);
|
|
GUILayout.Space(8);
|
|
teleportScroll = GUILayout.BeginScrollView(teleportScroll);
|
|
const int columns = 4;
|
|
for (int i = 0; i < Cheats.Places.Length; i += columns)
|
|
{
|
|
Rect r = GUILayoutUtility.GetRect(10, 44, GUILayout.ExpandWidth(true));
|
|
float bw = (r.width - AdminGap * (columns - 1)) / columns;
|
|
for (int c = 0; c < columns && i + c < Cheats.Places.Length; c++)
|
|
{
|
|
string[] place = Cheats.Places[i + c];
|
|
if (GUI.Button(new Rect(r.x + c * (bw + AdminGap), r.y, bw, 38), place[0].ToUpperInvariant(), secondary))
|
|
mp.TeleportToPlace(place[0], place[1]);
|
|
}
|
|
}
|
|
GUILayout.EndScrollView();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- VEHICLE
|
|
|
|
void DrawAdminVehicle(MultiplayerManager mp)
|
|
{
|
|
if (!mp.CanCheat)
|
|
{
|
|
GUILayout.Label("Only works in the game.", small);
|
|
return;
|
|
}
|
|
List<VehicleAdmin.Part> parts = VehicleAdmin.Parts;
|
|
|
|
// Werkzeugleiste: Aktionen links, Suche rechts
|
|
Rect bar = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
|
|
float bx = bar.x;
|
|
if (GUI.Button(new Rect(bx, bar.y, 120, 32), "REPAIR ALL", primary))
|
|
{
|
|
int n = VehicleAdmin.RepairAll();
|
|
Log(n + " part(s) repaired");
|
|
if (n > 0) mp.VehicleCheat("repaired the Satsuma (" + n + " parts)");
|
|
}
|
|
bx += 120 + AdminGap;
|
|
if (GUI.Button(new Rect(bx, bar.y, 120, 32), "TIGHTEN ALL", secondary))
|
|
{
|
|
int n = VehicleAdmin.SetAllBolts(true);
|
|
Log(n + " part(s) tightened");
|
|
if (n > 0) mp.VehicleCheat("tightened all bolts (" + n + " parts)");
|
|
}
|
|
bx += 120 + AdminGap;
|
|
if (GUI.Button(new Rect(bx, bar.y, 116, 32), "LOOSEN ALL", danger))
|
|
{
|
|
int n = VehicleAdmin.SetAllBolts(false);
|
|
Log(n + " part(s) loosened");
|
|
if (n > 0) mp.VehicleCheat("loosened all bolts (" + n + " parts, testing)");
|
|
}
|
|
bx += 116 + 16;
|
|
var search = new Rect(bx, bar.y, bar.xMax - bx, 32);
|
|
partFilter = GUI.TextField(search, partFilter ?? "", 30, field);
|
|
if (string.IsNullOrEmpty(partFilter)) GUI.Label(new Rect(search.x + 12, search.y, search.width, 32), "Search parts …", smallMid);
|
|
GUILayout.Space(8);
|
|
|
|
// Schnellfilter: Kategorien + nur Probleme + Füllstände ein/aus
|
|
var categories = new List<string>();
|
|
foreach (VehicleAdmin.Part p in parts) if (!categories.Contains(p.Category)) categories.Add(p.Category);
|
|
Rect chips = GUILayoutUtility.GetRect(10, 30, GUILayout.ExpandWidth(true));
|
|
float cx = chips.x;
|
|
if (GUI.Button(new Rect(cx, chips.y, 104, 26), "FLUIDS " + (fluidsOpen ? "▾" : "▸"), fluidsOpen ? chipOn : chipOff)) fluidsOpen = !fluidsOpen;
|
|
cx += 104 + 14;
|
|
if (GUI.Button(new Rect(cx, chips.y, 104, 26), "PROBLEMS", partProblems ? chipOn : chipOff)) partProblems = !partProblems;
|
|
cx += 104 + AdminGap;
|
|
if (GUI.Button(new Rect(cx, chips.y, 60, 26), "ALL", partCategory == "" ? chipOn : chipOff)) partCategory = "";
|
|
cx += 60 + AdminGap;
|
|
foreach (string cat in categories)
|
|
{
|
|
string label = (cat.Length == 0 ? "OTHER" : cat).ToUpperInvariant();
|
|
float cw = Mathf.Max(70, chipOff.CalcSize(new GUIContent(label)).x + 20);
|
|
if (cx + cw > chips.xMax) break;
|
|
if (GUI.Button(new Rect(cx, chips.y, cw, 26), label, partCategory == cat ? chipOn : chipOff)) partCategory = partCategory == cat ? "" : cat;
|
|
cx += cw + AdminGap;
|
|
}
|
|
GUILayout.Space(6);
|
|
|
|
vehicleScroll = GUILayout.BeginScrollView(vehicleScroll);
|
|
if (fluidsOpen)
|
|
{
|
|
List<VehicleAdmin.Fluid> fluids = VehicleAdmin.Fluids;
|
|
if (fluids.Count == 0) GUILayout.Label("No fluids found.", small);
|
|
// zwei Spalten
|
|
for (int i = 0; i < fluids.Count; i += 2)
|
|
{
|
|
Rect line = GUILayoutUtility.GetRect(10, AdminRow, GUILayout.ExpandWidth(true));
|
|
float colW = (line.width - 10) / 2f;
|
|
for (int c = 0; c < 2 && i + c < fluids.Count; c++)
|
|
{
|
|
VehicleAdmin.Fluid f = fluids[i + c];
|
|
var cell = new Rect(line.x + c * (colW + 10), line.y + 2, colW, AdminRow - 4);
|
|
GUI.Box(cell, GUIContent.none, rowStyle);
|
|
var inner = new Rect(cell.x + 12, cell.y, cell.width - 24, cell.height);
|
|
GUI.Label(new Rect(inner.x, inner.y, 128, inner.height), f.Label, nameMid);
|
|
float right = inner.xMax;
|
|
if (RightButton(ref right, inner, 74, "EMPTY", danger)) { VehicleAdmin.SetFluid(f, false); mp.VehicleCheat("emptied " + f.Label.ToLower()); }
|
|
if (RightButton(ref right, inner, 60, "FILL", secondary)) { VehicleAdmin.SetFluid(f, true); mp.VehicleCheat("filled up " + f.Label.ToLower()); }
|
|
GUI.Label(new Rect(right - 66, inner.y, 66, inner.height), f.Value.Value.ToString("0.0") + " / " + f.Max.ToString("0.#"), smallMid);
|
|
Bar(new Rect(inner.x + 132, inner.y, right - 72 - (inner.x + 132), inner.height), f.Max > 0f ? f.Value.Value / f.Max : 0f, 0.25f);
|
|
}
|
|
}
|
|
GUILayout.Space(10);
|
|
}
|
|
|
|
string filter = (partFilter ?? "").Trim().ToLowerInvariant();
|
|
int shown = 0;
|
|
foreach (VehicleAdmin.Part p in parts)
|
|
{
|
|
bool installed = p.Installed == null || p.Installed.Value;
|
|
bool damaged = p.Damaged != null && p.Damaged.Value;
|
|
// "Bolted" in der Database zieht das Spiel erst später nach - die Festigkeit selbst ist aktuell
|
|
bool loose = installed && p.Tightness != null && p.Tightness.Value < p.MaxTightness - 0.01f;
|
|
bool worn = p.Wear != null && p.Wear.Value < 50f;
|
|
if (partCategory.Length > 0 && p.Category != partCategory) continue;
|
|
if (partProblems && !(damaged || loose || worn)) continue;
|
|
if (filter.Length > 0 && !(p.Name + " " + p.Category).ToLowerInvariant().Contains(filter)) continue;
|
|
shown++;
|
|
|
|
Rect row = AdminRowRect(AdminRow);
|
|
GUI.Label(new Rect(row.x, row.y, 200, row.height), p.Name, nameMid);
|
|
GUI.Label(new Rect(row.x + 204, row.y, 90, row.height), p.Category, smallMid);
|
|
|
|
// Zustand als Abzeichen
|
|
string state = !installed ? "NOT INSTALLED" : damaged ? "DAMAGED" : loose ? "LOOSE" : p.Tightness != null ? "BOLTED" : "OK";
|
|
GUIStyle pill = !installed ? pillOff : damaged ? pillBad : loose ? pillWarn : pillOk;
|
|
GUI.Label(new Rect(row.x + 298, row.y + (row.height - 18) / 2f, 104, 18), state, pill);
|
|
|
|
float right = row.xMax;
|
|
if (RightButton(ref right, row, 66, "BREAK", danger)) { VehicleAdmin.Break(p); mp.VehicleCheat("broke " + p.Name + " (testing)"); }
|
|
if (RightButton(ref right, row, 70, "REPAIR", secondary)) { VehicleAdmin.Repair(p); mp.VehicleCheat("repaired " + p.Name); }
|
|
bool bolts = p.HasBolts && installed;
|
|
if (RightButton(ref right, row, 72, "LOOSEN", secondary, bolts)) { VehicleAdmin.SetBolts(p, false); mp.VehicleCheat("loosened " + p.Name); }
|
|
if (RightButton(ref right, row, 76, "TIGHTEN", secondary, bolts)) { VehicleAdmin.SetBolts(p, true); mp.VehicleCheat("tightened " + p.Name); }
|
|
|
|
// Verschleiß
|
|
float wearX = row.x + 410;
|
|
if (p.Wear != null && right - wearX > 80)
|
|
{
|
|
GUI.Label(new Rect(right - 44, row.y, 44, row.height), p.Wear.Value.ToString("0") + " %", smallMid);
|
|
Bar(new Rect(wearX, row.y, right - 50 - wearX, row.height), p.Wear.Value / 100f, 0.3f);
|
|
}
|
|
}
|
|
if (shown == 0) GUILayout.Label(parts.Count == 0 ? "No parts found." : "No part matches the filter.", small);
|
|
GUILayout.EndScrollView();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- ITEMS
|
|
|
|
void DrawAdminItems(MultiplayerManager mp)
|
|
{
|
|
if (!mp.CanCheat)
|
|
{
|
|
GUILayout.Label("Items can only be spawned in the game.", small);
|
|
return;
|
|
}
|
|
Rect top = GUILayoutUtility.GetRect(10, 32, GUILayout.ExpandWidth(true));
|
|
GUI.Label(new Rect(top.x, top.y, top.width - 330, 32), "Click an item to spawn it in front of you (everyone sees it, saved on the server).", smallMid);
|
|
var search = new Rect(top.xMax - 320, top.y, 320, 32);
|
|
itemFilter = GUI.TextField(search, itemFilter ?? "", 30, field);
|
|
if (string.IsNullOrEmpty(itemFilter)) GUI.Label(new Rect(search.x + 12, search.y, search.width, 32), "Search items …", smallMid);
|
|
GUILayout.Space(8);
|
|
|
|
var items = new List<string>();
|
|
string f = (itemFilter ?? "").Trim().ToLowerInvariant();
|
|
foreach (string item in ItemSpawner.Items) if (f.Length == 0 || item.ToLowerInvariant().Contains(f)) items.Add(item);
|
|
|
|
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
|
const int perRow = 5;
|
|
for (int i = 0; i < items.Count; i += perRow)
|
|
{
|
|
Rect r = GUILayoutUtility.GetRect(10, 42, GUILayout.ExpandWidth(true));
|
|
float bw = (r.width - AdminGap * (perRow - 1)) / perRow;
|
|
for (int k = 0; k < perRow && i + k < items.Count; k++)
|
|
if (GUI.Button(new Rect(r.x + k * (bw + AdminGap), r.y, bw, 36), items[i + k].ToUpper(), secondary)) mp.SpawnItem(items[i + k]);
|
|
}
|
|
if (items.Count == 0) GUILayout.Label("No item matches the search.", small);
|
|
GUILayout.EndScrollView();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- WORLD
|
|
|
|
/// <summary>Welt zurücksetzen, Backups anlegen und wiederherstellen (dedizierter Server).</summary>
|
|
void DrawAdminWorld(MultiplayerManager mp)
|
|
{
|
|
if (!mp.IsDedicated)
|
|
{
|
|
GUILayout.Label("World reset and save backups are only available on a dedicated server.", small);
|
|
return;
|
|
}
|
|
GUILayout.Label("Everyone reconnects after a reset or restore. The current state is backed up first.", small);
|
|
GUILayout.Space(8);
|
|
|
|
Rect r = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
|
|
float bw = (r.width - AdminGap * 2) / 3f;
|
|
var b1 = new Rect(r.x, r.y, bw, 32);
|
|
var b2 = new Rect(r.x + bw + AdminGap, r.y, bw, 32);
|
|
var b3 = new Rect(r.x + 2 * (bw + AdminGap), r.y, bw, 32);
|
|
if (confirmWorld == "reset")
|
|
{
|
|
if (GUI.Button(new Rect(b1.x, b1.y, bw / 2f - 3, 32), "REALLY RESET?", danger)) { mp.WorldAction(5, null); confirmWorld = null; }
|
|
if (GUI.Button(new Rect(b1.x + bw / 2f + 3, b1.y, bw / 2f - 3, 32), "CANCEL", secondary)) confirmWorld = null;
|
|
}
|
|
else if (GUI.Button(b1, "RESET WORLD", danger)) confirmWorld = "reset";
|
|
if (GUI.Button(b2, "BACKUP NOW", primary)) mp.WorldAction(6, null);
|
|
if (confirmWorld == "point")
|
|
{
|
|
if (GUI.Button(new Rect(b3.x, b3.y, bw / 2f - 3, 32), "REALLY?", danger)) { mp.WorldAction(8, null); confirmWorld = null; }
|
|
if (GUI.Button(new Rect(b3.x + bw / 2f + 3, b3.y, bw / 2f - 3, 32), "CANCEL", secondary)) confirmWorld = null;
|
|
}
|
|
else if (GUI.Button(b3, "SET RESET POINT", secondary)) confirmWorld = "point";
|
|
GUILayout.Space(4);
|
|
GUILayout.Label("Reset: the whole world goes back to the save game as it was uploaded to the server (the reset point). " +
|
|
"SET RESET POINT makes the current save the new reset point.", small);
|
|
GUILayout.Space(10);
|
|
|
|
GUILayout.Label("SAVE BACKUPS (NEWEST FIRST)", caption);
|
|
adminScroll = GUILayout.BeginScrollView(adminScroll);
|
|
if (backupNames == null) GUILayout.Label("Loading...", small);
|
|
else if (backupNames.Count == 0) GUILayout.Label("No backups yet. They are created automatically when the server takes over a save game, or with BACKUP NOW.", small);
|
|
else
|
|
{
|
|
foreach (string name in backupNames)
|
|
{
|
|
Rect row = AdminRowRect(AdminRow);
|
|
GUI.Label(new Rect(row.x, row.y, 300, row.height), PrettyBackup(name), nameMid);
|
|
float right = row.xMax;
|
|
if (confirmWorld == name)
|
|
{
|
|
if (RightButton(ref right, row, 90, "CANCEL", secondary)) confirmWorld = null;
|
|
if (RightButton(ref right, row, 100, "REALLY?", danger)) { mp.WorldAction(7, name); confirmWorld = null; }
|
|
}
|
|
else if (RightButton(ref right, row, 100, "RESTORE", secondary)) confirmWorld = name;
|
|
}
|
|
}
|
|
GUILayout.EndScrollView();
|
|
}
|
|
|
|
/// <summary>"20260927-104626" -> "27.09.2026 10:46:26".</summary>
|
|
static string PrettyBackup(string name)
|
|
{
|
|
if (name == null || name.Length != 15 || name[8] != '-') return name;
|
|
return name.Substring(6, 2) + "." + name.Substring(4, 2) + "." + name.Substring(0, 4) + " " +
|
|
name.Substring(9, 2) + ":" + name.Substring(11, 2) + ":" + name.Substring(13, 2);
|
|
}
|
|
|
|
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
|
|
{
|
|
EnsureAdminStyles();
|
|
foreach (string id in bannedIds)
|
|
{
|
|
Rect row = AdminRowRect(AdminRow);
|
|
GUI.Label(new Rect(row.x, row.y, row.width - 110, row.height), id.StartsWith("name:") ? id.Substring(5) + " (no Steam)" : "SteamID " + id, nameMid);
|
|
float right = row.xMax;
|
|
if (RightButton(ref right, row, 90, "UNBAN", secondary)) mp.Unban(id);
|
|
}
|
|
}
|
|
GUILayout.EndScrollView();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Stile
|
|
|
|
void EnsureStyles()
|
|
{
|
|
if (stylesReady && panel.normal.background != null) return;
|
|
panel = UiTheme.Card(UiTheme.Background, 10);
|
|
rowStyle = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.72f));
|
|
rowStyle.padding = new RectOffset(12, 12, 6, 6);
|
|
field = UiTheme.Field(14);
|
|
logText = UiTheme.Label(12, UiTheme.Text);
|
|
logText.wordWrap = true;
|
|
logText.richText = true; // Warnungen/Fehler farbig markiert (MscoLog)
|
|
logText.margin = new RectOffset(0, 0, 0, 0);
|
|
caption = UiTheme.Label(11, UiTheme.Muted, FontStyle.Bold);
|
|
title = UiTheme.Label(22, UiTheme.Text, FontStyle.Bold);
|
|
title.wordWrap = false;
|
|
small = UiTheme.Label(11, UiTheme.Muted);
|
|
primary = UiTheme.PrimaryButton();
|
|
secondary = UiTheme.Button(UiTheme.Secondary, UiTheme.SecondaryHover, UiTheme.Text, 12);
|
|
danger = UiTheme.Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 12);
|
|
chatName = UiTheme.Label(13, UiTheme.AccentHover, FontStyle.Bold, TextAnchor.MiddleLeft);
|
|
chatText = UiTheme.Label(13, UiTheme.Text, FontStyle.Normal, TextAnchor.MiddleLeft);
|
|
chatSystem = UiTheme.Label(12, UiTheme.Muted, FontStyle.Italic, TextAnchor.MiddleLeft);
|
|
stylesReady = true;
|
|
}
|
|
}
|
|
}
|