using System.Collections.Generic; using UnityEngine; namespace MSCO { /// /// Multiplayer-Launcher im Hauptmenü (dunkles Design mit Seitenleiste, Statusleiste /// und Minimieren). Seiten: P2P (Spieler hostet selbst) und Server Browser (Platzhalter, /// dort kommt später auch Direct Connect zu dedizierten Servern hin). /// public class LobbyUI { enum Page { P2P, ServerBrowser, Favorites } // Vollbild, automatisch skaliert: gebaut für RefHeight x RefWidth, vergrößert/verkleinert passend zur Auflösung const float RefHeight = 760f; const float RefWidth = 1180f; const float MinScale = 0.8f, MaxScale = 1.75f; const float SidebarWidth = 240f; const float StatusHeight = 40f; const float MaxContentWidth = 1100f; float vw, vh; // Bildschirmgröße in Pixeln static float uiScale = 1f; // Skalierung passend zur Auflösung float stylesScale = -1f; // für diese Skalierung wurden die Stile gebaut float contentHeight = 400f; // Höhe des Inhaltsbereichs - Listen wachsen damit // Maße (S) und Schriftgrößen/Abstände (P) werden direkt in Bildschirmpixeln berechnet - Schrift und Kanten // werden so in voller Auflösung gezeichnet (scharf), statt ein kleines Bild hochzuziehen static float S(float v) { return Mathf.Round(v * uiScale); } static int P(int v) { return Mathf.Max(1, Mathf.RoundToInt(v * uiScale)); } /// Höhe für eine Liste: was nach "reserved" für Überschriften/Knöpfe übrig bleibt. float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); } static readonly Color Background = Hex(0x17, 0x17, 0x1b); static readonly Color Sidebar = Hex(0x0f, 0x0f, 0x12); static readonly Color Field = Hex(0x24, 0x24, 0x2a); static readonly Color FieldFocus = Hex(0x2e, 0x2e, 0x36); static readonly Color Accent = Hex(0xf2, 0x6b, 0x1d); static readonly Color AccentHover = Hex(0xff, 0x82, 0x3a); static readonly Color Secondary = Hex(0x2c, 0x2c, 0x33); static readonly Color SecondaryHover = Hex(0x3a, 0x3a, 0x43); static readonly Color Muted = Hex(0x8e, 0x8e, 0x99); static readonly Color Online = Hex(0x3d, 0xd6, 0x8c); static readonly Color Pending = Hex(0xf2, 0xc2, 0x1d); static readonly Color Gold = Hex(0xff, 0xb4, 0x5a); // hervorgehobene (featured) Server Page page = Page.P2P; Vector2 profileScroll; bool quitConfirm; Vector2 playerScroll; Vector2 serverScroll; bool browserLoaded; string selectedServer; // "adresse:port" des geöffneten Server-Profils string searchInput = ""; bool showSteam = true, showNonSteam = true, hideFull; readonly Dictionary nicknameEdits = new Dictionary(); // Favoriten-Tab: Adresse -> bearbeiteter Kurzname string favoriteAddInput = ""; Vector2 favoritesScroll; Texture2D texHeartOn, texHeartOff; string nameInput; string directInput; const string PrefDirect = "MSCO_LastServer"; bool stylesReady; Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow; Texture2D texDivider, texGold, texBarBack, texBarFill, texBarFull; GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle; List shownAll, shownServers; // Stand des letzten Layout-Durchgangs string layoutError; GUIStyle dangerStyle; GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle; GUIStyle featuredRowBox, pillFeaturedStyle; GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle, fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle, rowNameStyle, rowTagStyle, badgeStyle, toggleStyle, placeholderStyle; /// Fläche, die das Fenster gerade belegt (GUI-Koordinaten, oben = 0). public Rect ScreenRect { get; private set; } /// /// Eingeklappt: nur eine kleine MSCO-Leiste oben, das Startmenü des Spiels (Optionen, Farbe, Musik ...) ist wieder /// sichtbar und bedienbar. Bei Meldungen (getrennt, neu verbinden, beitreten ...) klappt der Launcher von selbst auf. /// public bool Minimized { get; set; } /// Aufgeklappt füllt der Launcher den ganzen Bildschirm - das Spielmenü dahinter ist dann gesperrt. public bool IsOpen { get { return !Minimized; } } /// Ist die Maus gerade über dem Fenster bzw. der eingeklappten Leiste? public bool ContainsMouse() { Vector3 m = Input.mousePosition; return ScreenRect.Contains(new Vector2(m.x, Screen.height - m.y)); } public void Draw(MultiplayerManager mp) { uiScale = Mathf.Clamp(Mathf.Min(Screen.height / RefHeight, Screen.width / RefWidth), MinScale, MaxScale); EnsureStyles(); // baut die Stile neu, wenn sich die Skalierung ändert (Auflösung gewechselt) vw = Screen.width; vh = Screen.height; // Wichtige Meldungen nie hinter der eingeklappten Leiste verstecken if (Minimized && !mp.IsInSession && (mp.DisconnectNotice != null || mp.PendingJoinHost != null || mp.PendingVersionWarning != null)) Minimized = false; if (Minimized && mp.IsReconnecting) Minimized = false; if (Minimized) { DrawMinimized(mp); return; } ScreenRect = new Rect(0, 0, Screen.width, Screen.height); DrawScaled(mp); } void DrawScaled(MultiplayerManager mp) { // Undurchsichtiger Hintergrund über das ganze Spielmenü (Logo, Farbauswahl, Knöpfe ... sind dahinter verborgen) if (texScreen == null) texScreen = Solid(Hex(0x0d, 0x0d, 0x10)); GUI.DrawTexture(new Rect(0, 0, vw, vh), texScreen); bool pendingJoin = mp.PendingJoinHost != null && !mp.IsInSession; bool versionWarning = mp.PendingVersionWarning != null && !mp.IsInSession; bool disconnected = mp.DisconnectNotice != null && !mp.IsInSession; // Getrennt / Neu-Verbinden: eigenes Fenster in der Bildmitte, alles andere gesperrt if (disconnected) { DrawPrompt(mp, false); return; } if (mp.IsReconnecting) { DrawPrompt(mp, true); return; } var window = new Rect(0, 0, vw, vh); GUI.Box(window, GUIContent.none, windowBox); DrawSidebar(new Rect(0, 0, S(SidebarWidth), vh - S(StatusHeight))); // Inhalt mittig im freien Bereich, nicht beliebig breit (lesbar bleiben) float free = vw - S(SidebarWidth); float width = Mathf.Min(S(MaxContentWidth), free - S(96f)); var content = new Rect(S(SidebarWidth) + Mathf.Round((free - width) / 2f), S(40f), width, vh - S(StatusHeight) - S(72f)); contentHeight = content.height; GUILayout.BeginArea(content); if (versionWarning) DrawVersionWarning(mp); else if (pendingJoin) DrawJoinConfirm(mp); else if (mp.IsInSession) DrawSession(mp); else if (page == Page.P2P) DrawP2P(mp); else if (page == Page.Favorites) DrawFavorites(mp); else DrawServerBrowser(mp); GUILayout.EndArea(); DrawStatusBar(mp, new Rect(0, vh - S(StatusHeight), vw, S(StatusHeight))); // Einklappen (oben rechts): das Startmenü des Spiels wird wieder sichtbar var minimize = new Rect(vw - S(54), S(12), S(40), S(32)); if (GUI.Button(minimize, new GUIContent("–", "Minimize - show the game's start menu"), secondaryStyle)) Minimized = true; } /// Eingeklappt: kleine Leiste oben in der Mitte mit Verbindungsstand - Klick öffnet den Launcher wieder. void DrawMinimized(MultiplayerManager mp) { float w = S(300), h = S(40); var bar = new Rect(Mathf.Round((vw - w) / 2f), S(10), w, h); ScreenRect = bar; GUI.Box(bar, GUIContent.none, sidebarBox); Texture2D logo = Branding.LogoTexture; float x = bar.x + S(10); if (logo != null) { GUI.DrawTexture(new Rect(x, bar.y + S(6), S(28), S(28)), logo); x += S(36); } Texture2D dot = mp.IsFullyConnected ? texOnline : mp.IsInSession || mp.IsReconnecting ? texPending : texMuted; GUI.DrawTexture(new Rect(x, bar.y + (h - S(8)) / 2f, S(8), S(8)), dot); string text = mp.IsInSession ? "MSCO · " + mp.Players.Count + " player(s) · open" : "MSCO · open multiplayer"; GUI.Label(new Rect(x + S(16), bar.y, bar.xMax - x - S(24), h), text, statusStyle); if (GUI.Button(bar, new GUIContent("", "Open the MSCO launcher"), GUIStyle.none)) Minimized = false; } // ---------------------------------------------------------------- Bereiche void DrawSidebar(Rect r) { GUI.Box(r, GUIContent.none, sidebarBox); // Logo + Schriftzug Texture2D logo = Branding.LogoTexture; float textX = r.x + S(22); if (logo != null) { GUI.DrawTexture(new Rect(r.x + S(20), r.y + S(20), S(44), S(44)), logo); textX = r.x + S(72); } GUI.Label(new Rect(textX, r.y + S(24), r.xMax - textX, S(40)), "MSCO", logoStyle); GUI.Label(new Rect(r.x + S(22), r.y + S(70), r.width - S(30), S(20)), "MY SUMMER CAR ONLINE", taglineStyle); GUI.DrawTexture(new Rect(r.x + S(20), r.y + S(98), r.width - S(40), S(1)), texDivider); float y = r.y + S(110); if (NavButton(new Rect(r.x, y, r.width, S(42)), "P2P", page == Page.P2P)) page = Page.P2P; y += S(44); if (NavButton(new Rect(r.x, y, r.width, S(42)), "Server Browser", page == Page.ServerBrowser)) page = Page.ServerBrowser; y += S(44); int favCount = Favorites.All.Count; if (NavButton(new Rect(r.x, y, r.width, S(42)), "Favorites" + (favCount > 0 ? " (" + favCount + ")" : ""), page == Page.Favorites)) page = Page.Favorites; // Eigenes Hauptmenü: Spiel beenden (die Knöpfe des Spiel-Menüs sind ausgeblendet) if (quitConfirm) { if (GUI.Button(new Rect(r.x + S(16), r.yMax - S(118), r.width - S(32), S(36)), "QUIT GAME?", dangerStyle)) Application.Quit(); if (GUI.Button(new Rect(r.x + S(16), r.yMax - S(76), r.width - S(32), S(30)), "CANCEL", tabStyle)) quitConfirm = false; } else if (GUI.Button(new Rect(r.x + S(16), r.yMax - S(76), r.width - S(32), S(36)), "QUIT", tabStyle)) quitConfirm = true; GUI.Label(new Rect(r.x + S(24), r.yMax - S(30), r.width - S(48), S(20)), "v" + MSCO.ModVersion, taglineStyle); } bool NavButton(Rect r, string text, bool active) { if (active) GUI.Box(new Rect(r.x + S(4), r.y + S(8), S(4), r.height - S(16)), GUIContent.none, accentBox); // Akzentbalken links return GUI.Button(r, text, active ? navActiveStyle : navStyle); } void DrawP2P(MultiplayerManager mp) { GUILayout.Label("P2P", titleStyle); GUILayout.Label("Host a game and invite up to " + (MultiplayerManager.MaxPlayers - 1) + " friends through Steam - continue your own save or start a new game.", textStyle); GUILayout.Space(S(18)); if (mp.SteamAvailable) { if (GUILayout.Button("HOST GAME", primaryStyle, GUILayout.Height(S(46)), GUILayout.Width(S(250)))) mp.HostSteam(); GUILayout.Space(S(14)); GUILayout.Label("To join a friend: accept their invite in Steam, or right-click them in your " + "Steam friends list and choose \"Join Game\".", textStyle); } else { GUILayout.Label("Steam is not running - start the game through Steam to host or join.", textStyle); } } void DrawServerBrowser(MultiplayerManager mp) { ServerBrowser browser = mp.Browser; // Beim ersten Öffnen laden, danach automatisch alle 15 s (Spielerzahl, Ping, neue Server) if (!browserLoaded || (!browser.Loading && Time.realtimeSinceStartup - browser.LastRefresh > ServerBrowser.AutoRefreshSeconds)) { browserLoaded = true; browser.Refresh(mp); } // Server, die auf die direkte Abfrage nicht antworten (z.B. gerade geschlossen), gar nicht zeigen // Unity-GUI läuft in mehreren Durchgängen (Layout, Klick, Zeichnen). Die Liste nur im Layout-Durchgang // übernehmen - ändert sie sich dazwischen (Aktualisierung im Hintergrund), bricht das Layout sonst ab if (shownAll == null || Event.current.type == EventType.Layout) shownAll = browser.Servers.FindAll(s => !s.Offline); List all = shownAll; // Server-Profil (Klick auf einen Server) if (selectedServer != null) { ServerListEntry selected = all.Find(s => s.Address + ":" + s.Port == selectedServer); DrawServerProfile(mp, selected); return; } GUILayout.BeginHorizontal(); // Titel darf nicht vom REFRESH-Button zusammengedrückt werden (sonst Zeilenumbruch) GUILayout.Label("Server Browser", titleStyle, GUILayout.ExpandWidth(true)); if (GUILayout.Button(browser.Loading ? "LOADING..." : "REFRESH", tabStyle, GUILayout.Height(S(28)), GUILayout.Width(S(110)))) browser.Refresh(mp); GUILayout.EndHorizontal(); // Suche + Filter GUILayout.BeginHorizontal(); searchInput = GUILayout.TextField(searchInput, 40, fieldStyle, GUILayout.Height(S(26)), GUILayout.Width(S(170))); if (string.IsNullOrEmpty(searchInput)) { Rect field = GUILayoutUtility.GetLastRect(); GUI.Label(new Rect(field.x + S(10), field.y, field.width, field.height), "Search...", placeholderStyle); } GUILayout.Space(S(12)); showSteam = Chip(showSteam, "STEAM"); GUILayout.Space(S(6)); showNonSteam = Chip(showNonSteam, "NON-STEAM"); GUILayout.Space(S(6)); hideFull = Chip(hideFull, "HIDE FULL"); GUILayout.EndHorizontal(); GUILayout.Space(S(6)); string search = (searchInput ?? "").Trim().ToLower(); if (shownServers == null || Event.current.type == EventType.Layout) shownServers = all.FindAll(s => (s.RequireSteam ? showSteam : showNonSteam) && (!hideFull || s.Players < s.MaxPlayers) && (search.Length == 0 || (s.Name ?? "").ToLower().Contains(search) || (s.Owner ?? "").ToLower().Contains(search) || (s.Tags != null && s.Tags.Exists(t => (t ?? "").Contains(search))))); List servers = shownServers; string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError; // Tabellenkopf Rect header = GUILayoutUtility.GetRect(S(10), S(26), GUILayout.ExpandWidth(true)); GUI.Box(header, GUIContent.none, rowBox); GUI.Label(new Rect(header.x + S(12), header.y, header.width * 0.48f, header.height), "SERVER NAME", headerCellStyle); GUI.Label(new Rect(header.x + header.width * 0.5f, header.y, S(70), header.height), "PLAYERS", headerCellStyle); GUI.Label(new Rect(header.x + header.width * 0.62f, header.y, S(70), header.height), "TYPE", headerCellStyle); GUI.Label(new Rect(header.x + header.width * 0.76f, header.y, S(50), header.height), "PING", headerCellStyle); serverScroll = GUILayout.BeginScrollView(serverScroll, GUILayout.Height(ListHeight(S(230f), S(140f)))); if (shownError != null) GUILayout.Label(shownError, textStyle); if (servers.Count == 0) GUILayout.Label(browser.Loading ? "Loading servers..." : all.Count > 0 ? "No servers match your filters." : "No servers online right now.", textStyle); foreach (ServerListEntry s in servers) { Rect row = GUILayoutUtility.GetRect(S(10), S(34), GUILayout.ExpandWidth(true)); // Hervorgehobene Server (Keymaster-Admin) stehen oben - der Master liefert sie zuerst var rowRect = new Rect(row.x, row.y + S(2), row.width, row.height - S(4)); bool hover = rowRect.Contains(Event.current.mousePosition); GUI.Box(rowRect, GUIContent.none, s.Featured ? featuredRowBox : hover ? rowHoverBox : rowBox); if (s.Featured) GUI.DrawTexture(new Rect(row.x, row.y + S(8), S(3), row.height - S(16)), texGold); // Herz: als Favorit merken / entfernen (vor dem Zeilen-Knopf, damit es den Klick bekommt) Heart(new Rect(row.xMax - S(58), row.y + S(7), S(20), S(20)), s); // 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 + S(10), row.y + S(13), S(8), S(8)), s.Ping < 0 ? texPending : texOnline); float nameX = row.x + S(26); Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port); if (s.Featured) nameX += Pill(nameX, row, "FEATURED", pillFeaturedStyle); if (fav != null) nameX += Pill(nameX, row, fav.Nickname, pillMutedStyle); if (s.IsLocal) { // Server auf diesem PC (localhost) nameX += Pill(nameX, row, "LOCAL", pillStyle); } if (!string.IsNullOrEmpty(s.Version) && !MSCO.Compatible(s.Version)) { nameX += Pill(nameX, row, "v" + s.Version, pillStyle); // abweichende Version } GUI.Label(new Rect(nameX, row.y, row.x + row.width * 0.49f - nameX, row.height), s.Name, rowNameStyle); // Spielerzahl mit Füllbalken (wie auf der Website) float px = row.x + row.width * 0.5f; GUI.Label(new Rect(px, row.y - S(3), S(70), row.height), s.Players + "/" + s.MaxPlayers, rowNameStyle); float fill = s.MaxPlayers > 0 ? Mathf.Clamp01((float)s.Players / s.MaxPlayers) : 0f; GUI.DrawTexture(new Rect(px, row.y + S(24), S(48), S(3)), texBarBack); GUI.DrawTexture(new Rect(px, row.y + S(24), Mathf.Max(S(2f), S(48) * fill), S(3)), fill >= 1f ? texBarFull : texBarFill); 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, S(60), row.height), s.Ping < 0 ? "..." : s.Ping + " ms", PingStyle(s.Ping)); GUI.Label(new Rect(row.xMax - S(30), row.y, S(20), row.height), "›", headerCellStyle); // Hinweis: Profil öffnen } GUILayout.EndScrollView(); GUILayout.FlexibleSpace(); GUILayout.BeginHorizontal(); if (!mp.SteamAvailable) { // Ohne Steam: eigener Name (nur für Non-Steam-Server) GUILayout.BeginVertical(GUILayout.Width(S(150))); GUILayout.Label("YOUR NAME", captionStyle); DrawNameField(mp, 150); GUILayout.EndVertical(); GUILayout.Space(S(10)); } GUILayout.BeginVertical(); GUILayout.Label("DIRECT CONNECT", captionStyle); GUILayout.BeginHorizontal(); directInput = GUILayout.TextField(directInput, fieldStyle, GUILayout.Height(S(34))); GUILayout.Space(S(10)); if (GUILayout.Button("CONNECT", primaryStyle, GUILayout.Height(S(34)), GUILayout.Width(S(110)))) { string host; int port; if (ServerBrowser.TryParseAddress(directInput, out host, out port)) { PlayerPrefs.SetString(PrefDirect, directInput.Trim()); mp.JoinServer(host, port); } } GUILayout.EndHorizontal(); GUILayout.EndVertical(); GUILayout.EndHorizontal(); } static bool CanJoin(MultiplayerManager mp, ServerListEntry s) { return s.Players < s.MaxPlayers && (!s.RequireSteam || mp.SteamAvailable); } void DrawNameField(MultiplayerManager mp, float width) { if (nameInput == null) nameInput = mp.PlayerName; string edited = GUILayout.TextField(nameInput, 24, fieldStyle, GUILayout.Height(S(34)), GUILayout.Width(width)); if (edited != nameInput) { nameInput = edited; if (edited.Trim().Length > 0) mp.PlayerName = edited.Trim(); } } // ---------------------------------------------------------------- Favoriten /// Herz: gefüllt = Favorit. Klick merkt den Server (Kurzname aus dem Namen) bzw. entfernt ihn. void Heart(Rect r, ServerListEntry s) { EnsureHearts(); Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port); bool hover = r.Contains(Event.current.mousePosition); GUI.DrawTexture(r, fav != null || hover ? texHeartOn : texHeartOff); if (GUI.Button(r, GUIContent.none, GUIStyle.none)) { if (fav != null) Favorites.Remove(fav); else AddFavorite(s.Name, s.Address, s.Port); } } static void AddFavorite(string serverName, string host, int port) { // Kurzname aus dem Servernamen; ist er schon vergeben, eine Zahl anhängen string baseNick = Favorites.SuggestNickname(serverName), nick = baseNick; for (int i = 2; Favorites.Find(nick) != null && i < 100; i++) nick = baseNick + i; string error = Favorites.Set(nick, host, port, serverName); if (error != null) MscoLog.Warning(error); } /// Tab "Favorites": Kurznamen setzen, beitreten, entfernen, per Adresse hinzufügen. void DrawFavorites(MultiplayerManager mp) { EnsureHearts(); GUILayout.Label("Favorites", titleStyle); GUILayout.Label("Give each server a nickname - switch in the game with /switch in the chat or F7 console.", textStyle); GUILayout.Space(S(8)); List favs = Favorites.All; favoritesScroll = GUILayout.BeginScrollView(favoritesScroll, GUILayout.Height(ListHeight(S(190f), S(160f)))); if (favs.Count == 0) GUILayout.Label("No favorites yet. Click the heart next to a server in the Server Browser, or add one by address below.", textStyle); Favorites.Favorite remove = null; foreach (Favorites.Favorite f in favs) { ServerListEntry live = mp.Browser.Servers.Find(x => x.Port == f.Port && string.Equals(x.Address, f.Host, System.StringComparison.OrdinalIgnoreCase)); GUILayout.BeginHorizontal(rowBox, GUILayout.Height(S(44))); GUILayout.Space(S(6)); // Herz (entfernen) Rect heart = GUILayoutUtility.GetRect(S(22), S(22), GUILayout.Width(S(22)), GUILayout.Height(S(40))); Rect heartIcon = new Rect(heart.x, heart.y + S(10), S(20), S(20)); GUI.DrawTexture(heartIcon, texHeartOn); if (GUI.Button(heartIcon, GUIContent.none, GUIStyle.none)) remove = f; GUILayout.Space(S(8)); // Kurzname bearbeiten string edit; if (!nicknameEdits.TryGetValue(f.Address, out edit)) edit = f.Nickname; GUILayout.BeginVertical(GUILayout.Width(S(150))); GUILayout.Space(S(7)); edit = GUILayout.TextField(edit, 20, fieldStyle, GUILayout.Height(S(28)), GUILayout.Width(S(150))); GUILayout.EndVertical(); nicknameEdits[f.Address] = edit; GUILayout.Space(S(6)); bool changed = Favorites.CleanNickname(edit) != f.Nickname; GUILayout.BeginVertical(GUILayout.Width(S(64))); GUILayout.Space(S(7)); GUI.enabled = changed; if (GUILayout.Button("SAVE", tabStyle, GUILayout.Height(S(28)), GUILayout.Width(S(64)))) { string error = Favorites.Set(edit, f.Host, f.Port, f.ServerName); if (error != null) MscoLog.Warning(error); nicknameEdits.Remove(f.Address); } GUI.enabled = true; GUILayout.EndVertical(); GUILayout.Space(S(10)); // Server + Status GUILayout.BeginVertical(); GUILayout.Space(S(4)); GUILayout.Label(string.IsNullOrEmpty(f.ServerName) ? f.Address : f.ServerName, rowNameStyle); GUILayout.Label(live == null ? f.Address + " · not listed / offline" : f.Address + " · " + live.Players + "/" + live.MaxPlayers + " · " + (live.Ping < 0 ? "..." : live.Ping + " ms"), textStyle); GUILayout.EndVertical(); GUILayout.FlexibleSpace(); GUILayout.BeginVertical(GUILayout.Width(S(90))); GUILayout.Space(S(7)); if (GUILayout.Button("JOIN", primaryStyle, GUILayout.Height(S(30)), GUILayout.Width(S(90)))) mp.SwitchServer(f.Nickname); GUILayout.EndVertical(); GUILayout.Space(S(6)); GUILayout.EndHorizontal(); GUILayout.Space(S(4)); } if (remove != null) { Favorites.Remove(remove); nicknameEdits.Remove(remove.Address); } GUILayout.EndScrollView(); GUILayout.FlexibleSpace(); GUILayout.Label("ADD BY ADDRESS", captionStyle); GUILayout.BeginHorizontal(); favoriteAddInput = GUILayout.TextField(favoriteAddInput ?? "", 60, fieldStyle, GUILayout.Height(S(34))); if (string.IsNullOrEmpty(favoriteAddInput)) { Rect field = GUILayoutUtility.GetLastRect(); GUI.Label(new Rect(field.x + S(10), field.y, field.width, field.height), "host:port", placeholderStyle); } GUILayout.Space(S(10)); if (GUILayout.Button("ADD", secondaryStyle, GUILayout.Height(S(34)), GUILayout.Width(S(110)))) { string host; int port; if (ServerBrowser.TryParseAddress(favoriteAddInput, out host, out port)) { ServerListEntry live = mp.Browser.Servers.Find(x => x.Port == port && x.Address == host); AddFavorite(live != null ? live.Name : host, host, port); favoriteAddInput = ""; } else MscoLog.Warning("Enter an address like 1.2.3.4:7777"); } GUILayout.EndHorizontal(); } void EnsureHearts() { if (texHeartOn != null && texHeartOff != null) return; texHeartOn = HeartTexture(new Color(0.93f, 0.28f, 0.33f), true); texHeartOff = HeartTexture(new Color(1f, 1f, 1f, 0.35f), false); } /// Herz als Textur (gefüllt oder nur Umriss), weich geglättet. static Texture2D HeartTexture(Color color, bool filled) { const int size = 48; var tex = new Texture2D(size, size, TextureFormat.ARGB32, false); for (int py = 0; py < size; py++) for (int px = 0; px < size; px++) { // Herz-Kurve (x² + y² - 1)³ - x² y³ <= 0, mehrfach abgetastet für glatte Kanten int inside = 0, samples = 0; for (int sy = 0; sy < 3; sy++) for (int sx = 0; sx < 3; sx++) { float x = ((px + (sx + 0.5f) / 3f) / size - 0.5f) * 2.6f; float y = ((py + (sy + 0.5f) / 3f) / size - 0.55f) * 2.6f; float a = x * x + y * y - 1f; bool inHeart = a * a * a - x * x * y * y * y <= 0f; if (!filled && inHeart) { // Umriss: innen ein kleineres Herz aussparen float x2 = x * 1.28f, y2 = (y + 0.08f) * 1.28f; float b = x2 * x2 + y2 * y2 - 1f; if (b * b * b - x2 * x2 * y2 * y2 * y2 <= 0f) inHeart = false; } if (inHeart) inside++; samples++; } tex.SetPixel(px, py, new Color(color.r, color.g, color.b, color.a * inside / samples)); } tex.filterMode = FilterMode.Bilinear; tex.wrapMode = TextureWrapMode.Clamp; tex.Apply(); tex.hideFlags = HideFlags.HideAndDontSave; return tex; } /// Server-Profil: wem der Server gehört, Version, Spieler, Laufzeit ... void DrawServerProfile(MultiplayerManager mp, ServerListEntry s) { if (GUILayout.Button("◂ BACK", tabStyle, GUILayout.Height(S(26)), GUILayout.Width(S(90)))) { selectedServer = null; return; } GUILayout.Space(S(8)); if (s == null) { GUILayout.Label("Server offline", titleStyle); GUILayout.Label("This server does not respond anymore.", textStyle); return; } // Kopf: Name + Abzeichen links, JOIN rechts - immer sichtbar GUILayout.BeginHorizontal(); GUILayout.BeginVertical(); GUILayout.Label(s.Name, titleStyle); GUILayout.BeginHorizontal(); Rect badgeRow = GUILayoutUtility.GetRect(S(10), S(20), GUILayout.ExpandWidth(true)); float bx = badgeRow.x; if (s.Featured) bx += Pill(bx, badgeRow, "FEATURED", pillFeaturedStyle); bool official = s.Tags != null && s.Tags.Contains("official"); // vergibt nur ein MSCO-Admin if (official) bx += Pill(bx, badgeRow, "OFFICIAL", pillStyle); bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle); if (s.IsLocal) bx += Pill(bx, badgeRow, "LOCAL", pillStyle); if (!MSCO.Compatible(s.Version)) bx += Pill(bx, badgeRow, "v" + s.Version, pillStyle); if (s.Tags != null) foreach (string tag in s.Tags) if (!string.IsNullOrEmpty(tag) && tag != "featured" && tag != "official") bx += Pill(bx, badgeRow, tag.ToUpper(), pillMutedStyle); GUILayout.EndHorizontal(); GUILayout.EndVertical(); GUILayout.Space(S(12)); Rect heartSlot = GUILayoutUtility.GetRect(S(44), S(44), GUILayout.Width(S(44)), GUILayout.Height(S(44))); GUI.Box(heartSlot, GUIContent.none, rowBox); Heart(new Rect(heartSlot.x + S(11), heartSlot.y + S(11), S(22), S(22)), s); GUILayout.Space(S(8)); GUI.enabled = CanJoin(mp, s); if (GUILayout.Button(s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(S(44)), GUILayout.Width(S(180)))) mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers); GUI.enabled = true; GUILayout.EndHorizontal(); if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle); GUILayout.Space(S(10)); // Ohne Steam: Name wählen (sauber in einem eigenen Kasten) if (s.RequireSteam && !mp.SteamAvailable) { GUILayout.BeginHorizontal(rowBox, GUILayout.Height(S(40))); GUILayout.Label("This server requires Steam - start the game through Steam to join.", textStyle); GUILayout.EndHorizontal(); GUILayout.Space(S(10)); } else if (!mp.SteamAvailable) { GUILayout.BeginHorizontal(rowBox, GUILayout.Height(S(44))); GUILayout.Space(S(6)); GUILayout.BeginVertical(GUILayout.Width(S(110))); GUILayout.Space(S(4)); GUILayout.Label("PLAYING AS", captionStyle); GUILayout.Label("no Steam", textStyle); GUILayout.EndVertical(); GUILayout.BeginVertical(); GUILayout.Space(S(5)); DrawNameField(mp, 220); GUILayout.EndVertical(); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); GUILayout.Space(S(10)); } // Infos links, Spielerliste rechts bool versionOk = MSCO.Compatible(s.Version); GUILayout.BeginHorizontal(); GUILayout.BeginVertical(GUILayout.Width(S(250))); InfoLine("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle); InfoLine("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle); InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle); InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle); InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping)); InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle); GUILayout.EndVertical(); GUILayout.Space(S(16)); GUILayout.BeginVertical(rowBox, GUILayout.ExpandWidth(true), GUILayout.MinHeight(S(150))); GUILayout.Space(S(6)); GUILayout.Label("PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle); profileScroll = GUILayout.BeginScrollView(profileScroll, GUILayout.Height(ListHeight(S(330f), S(130f)))); if (s.PlayerNames == null || s.PlayerNames.Count == 0) GUILayout.Label("Nobody online right now.", textStyle); else foreach (string name in s.PlayerNames) { Rect line = GUILayoutUtility.GetRect(S(10), S(24), GUILayout.ExpandWidth(true)); GUI.DrawTexture(new Rect(line.x + S(2), line.y + S(8), S(8), S(8)), texOnline); GUI.Label(new Rect(line.x + S(18), line.y, line.width - S(18), line.height), name, rowNameStyle); } GUILayout.EndScrollView(); GUILayout.EndVertical(); GUILayout.EndHorizontal(); GUILayout.FlexibleSpace(); } /// Eine Info-Zeile: Bezeichnung links (grau), Wert rechts. void InfoLine(string caption, string value, GUIStyle valueStyle) { GUILayout.BeginHorizontal(GUILayout.Height(S(26))); GUILayout.Label(caption, captionStyle, GUILayout.Width(S(100))); GUILayout.Label(value, valueStyle); GUILayout.EndHorizontal(); } /// Kleines abgerundetes Abzeichen, so breit wie sein Text. Gibt die belegte Breite zurück. static float Pill(float x, Rect row, string text, GUIStyle style) { Vector2 size = style.CalcSize(new GUIContent(text)); GUI.Label(new Rect(x, row.y + (row.height - S(18)) / 2f, size.x, S(18)), text, style); return size.x + S(8); } /// Filter-Chip (an = orange umrandet), ersetzt die Unity-Häkchen. bool Chip(bool on, string text) { if (GUILayout.Button(text, on ? chipOnStyle : chipOffStyle, GUILayout.Height(S(26)), GUILayout.ExpandWidth(false))) on = !on; return on; } void InfoRow(string caption, string value, GUIStyle valueStyle) { GUILayout.Label(caption, captionStyle); GUILayout.Label(value, valueStyle); GUILayout.Space(S(2)); } /// Ping farbig: grün gut, gelb geht so, rot schlecht. GUIStyle PingStyle(int ping) { if (ping < 0) return rowNameStyle; return ping < 80 ? pingGoodStyle : ping < 160 ? pingMidStyle : pingBadStyle; } static string FormatUptime(int seconds) { int days = seconds / 86400, hours = seconds / 3600 % 24, minutes = seconds / 60 % 60; if (days > 0) return days + "d " + hours + "h"; if (hours > 0) return hours + "h " + minutes + "m"; return Mathf.Max(1, minutes) + " min"; } /// Server hat eine andere Version: warnen, Verbinden nur auf Wunsch. void DrawVersionWarning(MultiplayerManager mp) { MultiplayerManager.VersionWarning w = mp.PendingVersionWarning; GUILayout.Label("Version mismatch", titleStyle); GUILayout.Label("This server runs a different MSCO version than you. You can still join, but things may not " + "sync correctly (players, items, cars). For the best experience use the same version.", textStyle); GUILayout.Space(S(16)); GUILayout.Label("SERVER", captionStyle); GUILayout.Label(w.Name + " (" + w.Host + ":" + w.Port + ")", rowNameStyle); GUILayout.Space(S(8)); GUILayout.Label("VERSIONS", captionStyle); GUILayout.Label("Server v" + w.ServerVersion + " - You v" + MSCO.ModVersion, rowTagStyle); GUILayout.Space(S(22)); GUILayout.BeginHorizontal(); if (GUILayout.Button("JOIN ANYWAY", primaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(170)))) mp.ConfirmVersionWarning(); GUILayout.Space(S(10)); if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(170)))) mp.CancelVersionWarning(); GUILayout.EndHorizontal(); } /// Nachfrage nach einem JOIN-Link der Website (keine Webseite soll einen ungefragt verbinden). void DrawJoinConfirm(MultiplayerManager mp) { GUILayout.Label("Join server?", titleStyle); GUILayout.Label("You opened a join link from the server list.", textStyle); GUILayout.Space(S(18)); GUILayout.Label("SERVER ADDRESS", captionStyle); GUILayout.Label(mp.PendingJoinHost + ":" + mp.PendingJoinPort, rowNameStyle); GUILayout.Space(S(24)); if (!mp.SteamAvailable) GUILayout.Label("Steam is required to join servers - start the game through Steam.", textStyle); GUILayout.BeginHorizontal(); GUI.enabled = mp.SteamAvailable; if (GUILayout.Button("JOIN", primaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(170)))) mp.ConfirmPendingJoin(); GUI.enabled = true; GUILayout.Space(S(10)); if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(170)))) mp.CancelPendingJoin(); GUILayout.EndHorizontal(); } const float PromptWidth = 540f; const float PromptHeight = 270f; Texture2D texDim, texDanger, texScreen; GUIStyle promptTextStyle; /// /// Fenster in der Bildmitte über abgedunkeltem Hintergrund: nach dem Trennen (Kick, Ban, Shutdown, Timeout ...) /// mit Grund und OK, bzw. beim automatischen Neu-Verbinden nach reset-world mit Versuch-Anzeige und CANCEL. /// void DrawPrompt(MultiplayerManager mp, bool reconnecting) { if (texDim == null) texDim = Solid(new Color(0f, 0f, 0f, 0.6f)); if (texDanger == null) texDanger = Solid(Hex(0xe5, 0x48, 0x4d)); if (promptTextStyle == null || promptTextStyle.fontSize != P(15)) { promptTextStyle = new GUIStyle(rowNameStyle); promptTextStyle.wordWrap = true; promptTextStyle.fontSize = P(15); } GUI.DrawTexture(new Rect(0, 0, vw, vh), texDim); var r = new Rect(Mathf.Round((vw - S(PromptWidth)) / 2f), Mathf.Round((vh - S(PromptHeight)) / 2f), S(PromptWidth), S(PromptHeight)); UiTheme.DrawShadow(r); GUI.Box(r, GUIContent.none, windowBox); string title = reconnecting ? ((mp.ReconnectMessage ?? "").StartsWith("Switching") ? "SWITCHING SERVER" : "RECONNECTING") : mp.DisconnectTitle; bool bad = !reconnecting && title != "DISCONNECTED"; GUI.DrawTexture(new Rect(r.x, r.y, r.width, S(4)), bad ? texDanger : texAccent); float x = r.x + S(32), w = r.width - S(64), y = r.y + S(28); GUI.Label(new Rect(x, y, w, S(34)), title, titleStyle); y += S(50); GUI.Label(new Rect(x, y, w, S(20)), "REASON", captionStyle); if (reconnecting) { GUI.Label(new Rect(x, y + S(22), w, S(44)), mp.ReconnectMessage ?? "", promptTextStyle); string attempt = mp.ReconnectAttempt == 0 ? "Connecting again in a moment..." : "Attempt " + mp.ReconnectAttempt + " of " + mp.ReconnectMaxAttempts + (string.IsNullOrEmpty(mp.Status) ? "" : " - " + mp.Status); GUI.Label(new Rect(x, y + S(74), w, S(40)), attempt, textStyle); if (GUI.Button(new Rect(r.xMax - S(32) - S(170), r.yMax - S(28) - S(42), S(170), S(42)), "CANCEL", secondaryStyle)) mp.CancelReconnect(); return; } GUI.Label(new Rect(x, y + S(22), w, S(60)), mp.DisconnectNotice ?? "", promptTextStyle); GUI.Label(new Rect(x, y + S(88), w, S(20)), "Your own save game has been restored.", textStyle); if (GUI.Button(new Rect(r.xMax - S(32) - S(170), r.yMax - S(28) - S(42), S(170), S(42)), "OK", primaryStyle)) mp.ClearDisconnectNotice(); } bool newGameConfirm; /// Host: eigenen Spielstand weiterspielen oder ein neues Spiel anlegen (mit Sicherheitsabfrage). void DrawHostStart(MultiplayerManager mp) { System.DateTime? saved = SaveSync.LocalSaveTime; if (newGameConfirm) { GUILayout.BeginVertical(rowBox); GUILayout.Space(S(6)); GUILayout.Label("START A NEW GAME?", captionStyle); GUILayout.Label(saved != null ? "Your current save game is kept as a backup in MSCO_Saves - everyone starts in a fresh world." : "Everyone starts in a fresh world.", textStyle); GUILayout.Space(S(6)); GUILayout.BeginHorizontal(); if (GUILayout.Button("YES, NEW GAME", dangerStyle, GUILayout.Height(S(38)), GUILayout.Width(S(170)))) { newGameConfirm = false; mp.StartHostNewGame(); } GUILayout.Space(S(8)); if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(S(38)), GUILayout.Width(S(110)))) newGameConfirm = false; GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); GUILayout.Space(S(6)); GUILayout.EndVertical(); return; } GUILayout.BeginHorizontal(); GUI.enabled = saved != null; GUILayout.BeginVertical(GUILayout.Width(S(190))); if (GUILayout.Button("CONTINUE", primaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(190)))) mp.StartHostGame(); GUILayout.Label(saved != null ? "saved " + saved.Value.ToString("dd.MM.yyyy HH:mm") : "no save game yet", captionStyle); GUILayout.EndVertical(); GUI.enabled = true; GUILayout.Space(S(10)); GUILayout.BeginVertical(GUILayout.Width(S(190))); if (GUILayout.Button("NEW GAME", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(190)))) { if (saved != null) newGameConfirm = true; else mp.StartHostNewGame(); } GUILayout.Label("fresh world for everyone", captionStyle); GUILayout.EndVertical(); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); } void DrawSession(MultiplayerManager mp) { GUILayout.Label(mp.IsHost ? "Hosting" : "Connected", titleStyle); GUILayout.Label(mp.IsHost ? "Your game is open. Continue your save or start a new game - friends can also join later." : mp.IsDedicated || mp.IsConnectingToServer ? "Connected to the server - the world is loading" + (string.IsNullOrEmpty(mp.Status) ? "." : ": " + mp.Status) : "Waiting for the host to start the game.", textStyle); GUILayout.Space(S(16)); GUILayout.Label("PLAYERS " + mp.Players.Count + "/" + mp.SessionMaxPlayers, captionStyle); playerScroll = GUILayout.BeginScrollView(playerScroll, GUILayout.Height(ListHeight(mp.IsHost ? S(330f) : S(170f), S(120f)))); foreach (PlayerInfo p in mp.Players) { Rect row = GUILayoutUtility.GetRect(S(10), S(32), GUILayout.ExpandWidth(true)); GUI.Box(new Rect(row.x, row.y, row.width, row.height - S(4)), GUIContent.none, rowBox); // Eigener Punkt gelb, solange man noch lädt/synchronisiert; grün, wenn man wirklich im Spiel ist GUI.DrawTexture(new Rect(row.x + S(12), row.y + S(11), S(8), S(8)), p.IsLocal && !mp.IsFullyConnected ? texPending : texOnline); GUI.Label(new Rect(row.x + S(30), row.y, row.width - S(150), row.height - S(2)), p.Name, rowNameStyle); string tag = p.Id == NetSession.HostId ? "HOST" : ""; if (p.IsLocal) tag = tag.Length > 0 ? tag + " · YOU" : "YOU"; GUI.Label(new Rect(row.xMax - S(120), row.y, S(110), row.height - S(2)), tag, rowTagStyle); } GUILayout.EndScrollView(); GUILayout.Space(S(14)); if (mp.IsHost) { DrawHostStart(mp); GUILayout.Space(S(10)); } GUILayout.BeginHorizontal(); if (mp.IsHost) { if (mp.IsSteamSession && mp.Players.Count < MultiplayerManager.MaxPlayers && GUILayout.Button("INVITE FRIENDS", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(150)))) mp.InviteFriends(); GUILayout.Space(S(10)); } if (GUILayout.Button("DISCONNECT", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(150)))) mp.Disconnect(); GUILayout.EndHorizontal(); } void DrawStatusBar(MultiplayerManager mp, Rect r) { GUI.Box(r, GUIContent.none, sidebarBox); Texture2D dot = mp.IsFullyConnected ? texOnline : mp.IsInSession || mp.IsReconnecting ? texPending : texMuted; GUI.DrawTexture(new Rect(r.x + S(18), r.y + (r.height - S(8)) / 2f, S(8), S(8)), dot); GUI.Label(new Rect(r.x + S(36), r.y, r.width - S(72), r.height), mp.Status, statusStyle); } // ---------------------------------------------------------------- Hilfen void EnsureStyles() { if (stylesReady && texBackground != null && Mathf.Abs(stylesScale - uiScale) < 0.01f) return; stylesScale = uiScale; // PlayerPrefs erst hier lesen (nicht im Konstruktor erlaubt) if (directInput == null) directInput = PlayerPrefs.GetString(PrefDirect, ""); texBackground = Solid(Background); texSidebar = Solid(Sidebar); texAccent = Solid(Accent); texOnline = UiTheme.Rounded(Online, P(4)); // runde Status-Punkte texPending = UiTheme.Rounded(Pending, P(4)); texMuted = UiTheme.Rounded(Muted, P(4)); texRow = Solid(Field); // Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt) windowBox = UiTheme.Card(Background, P(10)); sidebarBox = UiTheme.Card(Sidebar, P(10)); rowBox = UiTheme.Panel9(Field, P(UiTheme.Radius)); rowHoverBox = UiTheme.Card(FieldFocus, P(UiTheme.Radius)); texDivider = Solid(new Color(1f, 1f, 1f, 0.07f)); texGold = Solid(Gold); texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f)); texBarFill = Solid(Online); texBarFull = Solid(UiTheme.Danger); accentBox = UiTheme.Panel9(Accent, P(1)); pillStyle = UiTheme.Panel9(new Color(Accent.r, Accent.g, Accent.b, 0.18f), P(4)); pillStyle.normal.textColor = Accent; pillStyle.fontSize = P(10); pillStyle.fontStyle = FontStyle.Bold; pillStyle.alignment = TextAnchor.MiddleCenter; pillStyle.padding = new RectOffset(P(7), P(7), P(0), P(0)); pillStyle.wordWrap = false; pillMutedStyle = new GUIStyle(pillStyle); pillMutedStyle.normal.background = UiTheme.Rounded(new Color(1f, 1f, 1f, 0.08f), P(4)); pillMutedStyle.normal.textColor = Muted; pillFeaturedStyle = new GUIStyle(pillStyle); pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4)); pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06); featuredRowBox = UiTheme.Panel9(Color.Lerp(Field, Gold, 0.16f), P(UiTheme.Radius)); chipOnStyle = Button(new Color(Accent.r, Accent.g, Accent.b, 0.22f), new Color(Accent.r, Accent.g, Accent.b, 0.32f), AccentHover, P(11), FontStyle.Bold); chipOffStyle = Button(Field, FieldFocus, Muted, P(11), FontStyle.Bold); chipOnStyle.padding = chipOffStyle.padding = new RectOffset(P(12), P(12), P(0), P(0)); logoStyle = Label(P(28), Color.white, FontStyle.Bold); taglineStyle = Label(P(10), Muted, FontStyle.Bold); titleStyle = Label(P(24), Color.white, FontStyle.Bold); textStyle = Label(P(13), Muted, FontStyle.Normal); textStyle.wordWrap = true; captionStyle = Label(P(11), Muted, FontStyle.Bold); captionStyle.margin.bottom = P(6); statusStyle = Label(P(12), Color.white, FontStyle.Normal); statusStyle.alignment = TextAnchor.MiddleLeft; headerCellStyle = Label(P(11), Muted, FontStyle.Bold); headerCellStyle.alignment = TextAnchor.MiddleLeft; rowNameStyle = Label(P(14), Color.white, FontStyle.Normal); rowNameStyle.alignment = TextAnchor.MiddleLeft; pingGoodStyle = new GUIStyle(rowNameStyle); pingGoodStyle.normal.textColor = Online; pingMidStyle = new GUIStyle(rowNameStyle); pingMidStyle.normal.textColor = Pending; pingBadStyle = new GUIStyle(rowNameStyle); pingBadStyle.normal.textColor = UiTheme.DangerHover; rowTagStyle = Label(P(11), Accent, FontStyle.Bold); rowTagStyle.alignment = TextAnchor.MiddleRight; badgeStyle = new GUIStyle(rowTagStyle); badgeStyle.alignment = TextAnchor.MiddleLeft; badgeStyle.padding = new RectOffset(P(6), P(6), P(2), P(2)); placeholderStyle = Label(P(13), Muted, FontStyle.Italic); placeholderStyle.alignment = TextAnchor.MiddleLeft; toggleStyle = new GUIStyle(GUI.skin.toggle); toggleStyle.normal.textColor = toggleStyle.onNormal.textColor = toggleStyle.hover.textColor = toggleStyle.onHover.textColor = Color.white; toggleStyle.fontSize = P(11); toggleStyle.fontStyle = FontStyle.Bold; navStyle = Button(Sidebar, Secondary, Muted, P(14), FontStyle.Normal); navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau) navStyle.alignment = TextAnchor.MiddleLeft; navStyle.padding = new RectOffset(P(26), P(10), P(0), P(0)); navActiveStyle = Button(Secondary, Secondary, Color.white, P(14), FontStyle.Bold); navActiveStyle.alignment = TextAnchor.MiddleLeft; navActiveStyle.padding = new RectOffset(P(26), P(10), P(0), P(0)); primaryStyle = Button(Accent, AccentHover, Color.white, P(14), FontStyle.Bold); UiTheme.ApplyPrimaryGradient(primaryStyle); dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, P(13), FontStyle.Bold); secondaryStyle = Button(Secondary, SecondaryHover, Color.white, P(14), FontStyle.Bold); tabStyle = Button(Sidebar, Secondary, Muted, P(11), FontStyle.Bold); tabStyle.hover.textColor = Color.white; fieldStyle = UiTheme.Field(P(14)); stylesReady = true; } static GUIStyle Label(int size, Color color, FontStyle font) { var s = new GUIStyle(GUI.skin.label); s.fontSize = size; s.fontStyle = font; s.normal.textColor = color; return s; } static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font) { var s = new GUIStyle(GUI.skin.button); // abgerundete Ecken (9-Slice), siehe UiTheme s.normal.background = UiTheme.Rounded(normal); s.hover.background = UiTheme.Rounded(hover); s.active.background = UiTheme.Rounded(hover); s.focused.background = UiTheme.Rounded(normal); s.normal.textColor = s.hover.textColor = s.active.textColor = s.focused.textColor = text; s.border = new RectOffset(UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2); s.fontSize = size; s.fontStyle = font; return s; } static Texture2D Solid(Color color) { var tex = new Texture2D(1, 1); tex.SetPixel(0, 0, color); tex.Apply(); tex.hideFlags = HideFlags.HideAndDontSave; return tex; } static Color Hex(int r, int g, int b) { return new Color(r / 255f, g / 255f, b / 255f, 1f); } } }