using System; using System.Collections.Generic; using UnityEngine; namespace MSCO { /// Spieler im Admin-Menü. public class AdminPlayer { public int Id; public string Name; public string SteamId; public bool IsAdmin; } /// /// 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). /// 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 consoleLines = new List(); readonly List history = new List(); int historyIndex = -1; string consoleInput = ""; Vector2 consoleScroll; int consoleOpenedFrame = -1; bool consoleScrollToEnd; // Chat public bool ChatOpen { get; private set; } readonly List chat = new List(); string chatInput = ""; int chatOpenedFrame = -1; // Admin public bool AdminOpen { get; private set; } List adminPlayers; List 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; consoleOpenedFrame = Time.frameCount; consoleScrollToEnd = true; } } public void OpenChat() { if (AnyOpen) return; ChatOpen = true; chatOpenedFrame = Time.frameCount; chatInput = ""; } public void OpenAdmin(List players) { AdminOpen = true; ConsoleOpen = false; ChatOpen = false; adminPlayers = players; confirmBanId = -1; } public void SetAdminPlayers(List players) { adminPlayers = players; } public void SetBanned(List ids) { bannedIds = ids; } public void SetBackups(List names) { backupNames = names; } List 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(); 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); } void DrawAdmin(MultiplayerManager mp) { 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); 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; } GUILayout.BeginArea(new Rect(rect.x + 20, rect.y + 16, rect.width - 40, rect.height - 32)); GUILayout.BeginHorizontal(); GUILayout.Label("Admin", title, GUILayout.ExpandWidth(true)); // nicht von den Buttons zusammendrücken lassen if (GUILayout.Button("REFRESH", secondary, GUILayout.Height(30), GUILayout.Width(100))) mp.RequestAdminList(); GUILayout.Space(8); if (GUILayout.Button("CLOSE", secondary, GUILayout.Height(30), GUILayout.Width(90))) AdminOpen = false; GUILayout.EndHorizontal(); GUILayout.Space(6); // Reiter GUILayout.BeginHorizontal(); string[] tabs = { "PLAYERS", "TELEPORT", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" }; for (int i = 0; i < tabs.Length; i++) { if (GUILayout.Button(tabs[i], adminTab == i ? primary : secondary, GUILayout.Height(30), GUILayout.Width(120))) adminTab = i; GUILayout.Space(6); } GUILayout.EndHorizontal(); GUILayout.Space(10); if (adminTab == 1) DrawAdminTeleport(mp); else if (adminTab == 2) DrawAdminItems(mp); else if (adminTab == 3) DrawAdminBans(mp); else if (adminTab == 4) DrawAdminWorld(mp); else DrawAdminPlayers(mp); GUILayout.EndArea(); } Vector2 teleportScroll; 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 = 3; for (int i = 0; i < Cheats.Places.Length; i += columns) { GUILayout.BeginHorizontal(); for (int c = 0; c < columns; c++) { if (i + c < Cheats.Places.Length) { string[] place = Cheats.Places[i + c]; if (GUILayout.Button(place[0].ToUpperInvariant(), secondary, GUILayout.Height(38))) mp.TeleportToPlace(place[0], place[1]); } else GUILayout.Label("", GUILayout.Height(38)); if (c < columns - 1) GUILayout.Space(6); } GUILayout.EndHorizontal(); GUILayout.Space(6); } GUILayout.EndScrollView(); } void DrawAdminPlayers(MultiplayerManager mp) { // Cheats für den eigenen Spieler (nur im Spiel) if (mp.CanCheat) { GUILayout.BeginHorizontal(); if (GUILayout.Button("+1.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(1000f); GUILayout.Space(6); if (GUILayout.Button("+10.000 MK", primary, GUILayout.Height(30))) mp.CheatMoney(10000f); GUILayout.Space(6); if (GUILayout.Button("REFILL NEEDS", primary, GUILayout.Height(30))) mp.CheatNeeds(); 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 teleports you to a player, BRING teleports a player to you.", small); GUILayout.Space(8); } GUILayout.Label("REASON FOR KICK / BAN (optional)", caption); adminReason = GUILayout.TextField(adminReason, 100, field, GUILayout.Height(32)); 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) { GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(44)); GUILayout.BeginVertical(); bool you = p.Id == mp.LocalId; GUILayout.Label(p.Name + (you ? " (you)" : "") + (p.IsAdmin ? " (admin)" : ""), chatName); GUILayout.Label(string.IsNullOrEmpty(p.SteamId) ? "id " + p.Id : p.SteamId.StartsWith("name:") ? "no Steam" : "SteamID " + p.SteamId, small); GUILayout.EndVertical(); GUILayout.FlexibleSpace(); bool self = p.Id == mp.LocalId; if (mp.CanCheat) { GUI.enabled = !self; if (GUILayout.Button("GOTO", secondary, GUILayout.Height(30), GUILayout.Width(70))) mp.GotoPlayer(p.Id); GUILayout.Space(6); if (GUILayout.Button("BRING", secondary, GUILayout.Height(30), GUILayout.Width(70))) mp.BringPlayer(p.Id); GUILayout.Space(12); } if (mp.IsDedicated) { // Admin geben/nehmen (braucht Steam; der Server meldet, wenn es nicht geht) GUI.enabled = !self; if (GUILayout.Button(p.IsAdmin ? "REMOVE ADMIN" : "MAKE ADMIN", secondary, GUILayout.Height(30), GUILayout.Width(120))) mp.SetAdmin(p.Id, !p.IsAdmin); GUILayout.Space(12); } GUI.enabled = !self && !p.IsAdmin; 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(70))) { if (confirming) { mp.AdminAction(true, p.Id, adminReason); confirmBanId = -1; } else confirmBanId = p.Id; } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.Space(4); } } GUILayout.EndScrollView(); } 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(); } /// Welt zurücksetzen, Backups anlegen und wiederherstellen (dedizierter Server). 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); GUILayout.BeginHorizontal(); if (confirmWorld == "reset") { if (GUILayout.Button("REALLY RESET?", danger, GUILayout.Height(32), GUILayout.Width(170))) { mp.WorldAction(5, null); confirmWorld = null; } GUILayout.Space(6); if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(32), GUILayout.Width(100))) confirmWorld = null; } else if (GUILayout.Button("RESET WORLD", danger, GUILayout.Height(32), GUILayout.Width(170))) confirmWorld = "reset"; GUILayout.Space(10); if (GUILayout.Button("BACKUP NOW", primary, GUILayout.Height(32), GUILayout.Width(150))) mp.WorldAction(6, null); GUILayout.Space(6); if (confirmWorld == "point") { if (GUILayout.Button("REALLY?", danger, GUILayout.Height(32), GUILayout.Width(100))) { mp.WorldAction(8, null); confirmWorld = null; } GUILayout.Space(4); if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(32), GUILayout.Width(90))) confirmWorld = null; } else if (GUILayout.Button("SET RESET POINT", secondary, GUILayout.Height(32), GUILayout.Width(170))) confirmWorld = "point"; GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); 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)", small); 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) { GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(40)); GUILayout.Label(PrettyBackup(name), chatName); GUILayout.FlexibleSpace(); if (confirmWorld == name) { if (GUILayout.Button("REALLY?", danger, GUILayout.Height(28), GUILayout.Width(100))) { mp.WorldAction(7, name); confirmWorld = null; } GUILayout.Space(4); if (GUILayout.Button("CANCEL", secondary, GUILayout.Height(28), GUILayout.Width(90))) confirmWorld = null; } else if (GUILayout.Button("RESTORE", primary, GUILayout.Height(28), GUILayout.Width(100))) confirmWorld = name; GUILayout.EndHorizontal(); GUILayout.Space(4); } } GUILayout.EndScrollView(); } /// "20260927-104626" -> "27.09.2026 10:46:26". 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 { 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 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; } } }