1310 lines
72 KiB
C#
1310 lines
72 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace MSCO
|
||
{
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
public class LobbyUI
|
||
{
|
||
enum Page { P2P, ServerBrowser, Favorites, Mods }
|
||
|
||
// 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)); }
|
||
|
||
/// <summary>Höhe für eine Liste: was nach "reserved" für Überschriften/Knöpfe übrig bleibt.</summary>
|
||
float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); }
|
||
|
||
|
||
static readonly Color Background = Hex(0x11, 0x11, 0x14);
|
||
static readonly Color Sidebar = Hex(0x0c, 0x0c, 0x0f);
|
||
static readonly Color Field = Hex(0x1b, 0x1b, 0x20);
|
||
static readonly Color FieldFocus = Hex(0x24, 0x24, 0x2a);
|
||
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, showVanilla = true, showModded = true;
|
||
readonly Dictionary<string, string> nicknameEdits = new Dictionary<string, string>(); // 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, texGlow;
|
||
GUIStyle scrollbarStyle, scrollThumbStyle;
|
||
GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle;
|
||
List<ServerListEntry> shownAll, shownServers; // Stand des letzten Layout-Durchgangs
|
||
string layoutError;
|
||
GUIStyle dangerStyle;
|
||
GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle;
|
||
GUIStyle featuredRowBox, pillFeaturedStyle, pillDangerStyle, newsTitleStyle;
|
||
// Ankündigungen/Wartung vom Keymaster - nur im Layout-Durchgang übernehmen (Liste kann sich im Hintergrund ändern)
|
||
List<Announcement> shownNews;
|
||
bool shownMaintenance;
|
||
string shownMaintenanceText;
|
||
readonly HashSet<string> dismissedNews = new HashSet<string>();
|
||
GUIStyle betaPillStyle, betaNoteStyle;
|
||
GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle,
|
||
fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle,
|
||
rowNameStyle, rowTagStyle, badgeStyle, toggleStyle, placeholderStyle;
|
||
|
||
/// <summary>Fläche, die das Fenster gerade belegt (GUI-Koordinaten, oben = 0).</summary>
|
||
public Rect ScreenRect { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public bool Minimized { get; set; }
|
||
|
||
/// <summary>Aufgeklappt füllt der Launcher den ganzen Bildschirm - das Spielmenü dahinter ist dann gesperrt.</summary>
|
||
public bool IsOpen { get { return !Minimized; } }
|
||
|
||
/// <summary>Ist die Maus gerade über dem Fenster bzw. der eingeklappten Leiste?</summary>
|
||
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 && mp.PendingMods != null) 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; }
|
||
if (mp.PendingMods != null && mp.IsInSession) { DrawModsPrompt(mp); return; }
|
||
var window = new Rect(0, 0, vw, vh);
|
||
GUI.Box(window, GUIContent.none, windowBox);
|
||
float glowWidth = Mathf.Min(vw * 0.9f, S(1300));
|
||
GUI.DrawTexture(new Rect(S(SidebarWidth) + (vw - S(SidebarWidth) - glowWidth) / 2f, -S(60), glowWidth, S(420)), texGlow);
|
||
|
||
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;
|
||
GUIStyle oldBar = GUI.skin.verticalScrollbar, oldThumb = GUI.skin.verticalScrollbarThumb;
|
||
GUI.skin.verticalScrollbar = scrollbarStyle;
|
||
GUI.skin.verticalScrollbarThumb = scrollThumbStyle;
|
||
GUILayout.BeginArea(content);
|
||
if (!versionWarning && !pendingJoin && !mp.IsInSession) DrawAnnouncements();
|
||
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 if (page == Page.Mods) DrawMods();
|
||
else DrawServerBrowser(mp);
|
||
GUILayout.EndArea();
|
||
GUI.skin.verticalScrollbar = oldBar;
|
||
GUI.skin.verticalScrollbarThumb = oldThumb;
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>Eingeklappt: kleine Leiste oben in der Mitte mit Verbindungsstand - Klick öffnet den Launcher wieder.</summary>
|
||
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);
|
||
// Beta-Abzeichen direkt hinter dem Schriftzug
|
||
float logoWidth = logoStyle.CalcSize(new GUIContent("MSCO")).x;
|
||
GUI.Box(new Rect(textX + logoWidth + S(6), r.y + S(36), S(40), S(18)), "BETA", betaPillStyle);
|
||
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;
|
||
y += S(44);
|
||
int modCount = ModLoader.All.Count;
|
||
if (NavButton(new Rect(r.x, y, r.width, S(42)), "Mods" + (modCount > 0 ? " (" + modCount + ")" : ""), page == Page.Mods)) page = Page.Mods;
|
||
y += S(54);
|
||
GUI.Label(new Rect(r.x + S(22), y, r.width - S(40), S(64)),
|
||
"MSCO is still in beta - bugs can happen. If something goes wrong, please tell us.", betaNoteStyle);
|
||
|
||
// 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)
|
||
{
|
||
var item = new Rect(r.x + S(12), r.y, r.width - S(24), r.height);
|
||
bool clicked = GUI.Button(item, text, active ? navActiveStyle : navStyle);
|
||
if (active) GUI.Box(new Rect(item.x + S(8), item.y + S(11), S(3), item.height - S(22)), GUIContent.none, accentBox); // Akzentbalken
|
||
return clicked;
|
||
}
|
||
|
||
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<ServerListEntry> 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.Space(S(12));
|
||
showVanilla = Chip(showVanilla, "VANILLA");
|
||
GUILayout.Space(S(6));
|
||
showModded = Chip(showModded, "MODDED");
|
||
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) &&
|
||
(s.Modded ? showModded : showVanilla) &&
|
||
(search.Length == 0 || (s.Name ?? "").ToLower().Contains(search) || (s.Owner ?? "").ToLower().Contains(search) ||
|
||
(s.Tags != null && s.Tags.Exists(t => (t ?? "").Contains(search)))));
|
||
List<ServerListEntry> servers = shownServers;
|
||
string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError;
|
||
|
||
// Tabellenkopf
|
||
GUILayout.Space(S(6));
|
||
Rect header = GUILayoutUtility.GetRect(S(10), S(26), GUILayout.ExpandWidth(true));
|
||
GUI.DrawTexture(new Rect(header.x, header.yMax - S(1), header.width, S(1)), texDivider);
|
||
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.Modded) nameX += Pill(nameX, row, s.Mods.Count > 0 ? "MODS " + s.Mods.Count : "MODDED", pillStyle);
|
||
if (s.IsLocal)
|
||
{
|
||
// Server auf diesem PC (localhost)
|
||
nameX += Pill(nameX, row, "LOCAL", pillStyle);
|
||
}
|
||
if (s.Outdated) nameX += Pill(nameX, row, "OUTDATED", pillDangerStyle); // fehlende Sicherheitsupdates
|
||
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.Outdated && s.Players < s.MaxPlayers && (!s.RequireSteam || mp.SteamAvailable);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Oben im MSCO-Menü: Wartungshinweis und Ankündigungen der MSCO-Admins (Keymaster → Wartung & News).
|
||
/// Ankündigungen lassen sich mit × für diese Sitzung ausblenden.
|
||
/// </summary>
|
||
void DrawAnnouncements()
|
||
{
|
||
if (shownNews == null || Event.current.type == EventType.Layout)
|
||
{
|
||
shownNews = SiteStatus.Announcements.FindAll(a => !dismissedNews.Contains(a.Id));
|
||
if (shownNews.Count > 3) shownNews.RemoveRange(3, shownNews.Count - 3);
|
||
shownMaintenance = SiteStatus.Maintenance;
|
||
shownMaintenanceText = SiteStatus.MaintenanceMessage;
|
||
}
|
||
if (shownMaintenance)
|
||
NewsBox("MAINTENANCE", pillFeaturedStyle, "MSCO is in maintenance right now",
|
||
string.IsNullOrEmpty(shownMaintenanceText) ? "Some things may not work for a while." : shownMaintenanceText, null);
|
||
foreach (Announcement a in shownNews)
|
||
NewsBox(a.Level == "critical" ? "IMPORTANT" : a.Level == "warning" ? "NOTICE" : "INFO",
|
||
a.Level == "critical" ? pillDangerStyle : a.Level == "warning" ? pillFeaturedStyle : pillMutedStyle, a.Title, a.Text, a.Id);
|
||
if (shownMaintenance || shownNews.Count > 0) GUILayout.Space(S(10));
|
||
}
|
||
|
||
void NewsBox(string label, GUIStyle pill, string title, string text, string id)
|
||
{
|
||
GUILayout.BeginVertical(rowBox);
|
||
GUILayout.BeginHorizontal();
|
||
GUILayout.Label(label, pill, GUILayout.Height(S(20)), GUILayout.ExpandWidth(false));
|
||
GUILayout.Space(S(8));
|
||
GUILayout.Label(title, newsTitleStyle, GUILayout.ExpandWidth(true));
|
||
if (id != null && GUILayout.Button("×", tabStyle, GUILayout.Width(S(28)), GUILayout.Height(S(22)))) dismissedNews.Add(id);
|
||
GUILayout.EndHorizontal();
|
||
if (!string.IsNullOrEmpty(text)) GUILayout.Label(text, textStyle);
|
||
GUILayout.EndVertical();
|
||
GUILayout.Space(S(6));
|
||
}
|
||
|
||
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
|
||
|
||
/// <summary>Herz: gefüllt = Favorit. Klick merkt den Server (Kurzname aus dem Namen) bzw. entfernt ihn.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>Tab "Favorites": Kurznamen setzen, beitreten, entfernen, per Adresse hinzufügen.</summary>
|
||
void DrawFavorites(MultiplayerManager mp)
|
||
{
|
||
EnsureHearts();
|
||
GUILayout.Label("Favorites", titleStyle);
|
||
GUILayout.Label("Give each server a nickname - switch in the game with /switch <nickname> in the chat or F7 console.", textStyle);
|
||
GUILayout.Space(S(8));
|
||
|
||
List<Favorites.Favorite> 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);
|
||
}
|
||
|
||
/// <summary>Herz als Textur (gefüllt oder nur Umriss), weich geglättet.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Server-Profil: wem der Server gehört, Version, Spieler, Laufzeit ...</summary>
|
||
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(22), GUILayout.ExpandWidth(true));
|
||
float bx = badgeRow.x + titleStyle.padding.left;
|
||
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 (s.Outdated) bx += Pill(bx, badgeRow, "OUTDATED", pillDangerStyle);
|
||
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.Outdated ? "UPDATE REQUIRED" : 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 (s.Outdated)
|
||
GUILayout.Label("This server is missing MSCO security updates" + (string.IsNullOrEmpty(s.ServerVersion) ? "" : " (server " + s.ServerVersion + ")") +
|
||
" - you can join again once its owner has updated it.", textStyle);
|
||
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));
|
||
}
|
||
|
||
// Zwei Karten gleicher Höhe: Infos links, Spielerliste rechts
|
||
bool versionOk = MSCO.Compatible(s.Version);
|
||
float cardHeight = Mathf.Clamp(contentHeight - S(250), S(240), S(380));
|
||
Rect area = GUILayoutUtility.GetRect(S(10), cardHeight, GUILayout.ExpandWidth(true));
|
||
float gap = S(16), leftWidth = Mathf.Round(area.width * 0.44f);
|
||
var info = new Rect(area.x, area.y, leftWidth, area.height);
|
||
var list = new Rect(area.x + leftWidth + gap, area.y, area.width - leftWidth - gap, area.height);
|
||
GUI.Box(info, GUIContent.none, rowBox);
|
||
GUI.Box(list, GUIContent.none, rowBox);
|
||
|
||
EnsureProfileStyles();
|
||
float x = info.x + S(20), w = info.width - S(40), y = info.y + S(16);
|
||
GUI.Label(new Rect(x, y, w, S(18)), "SERVER INFO", captionStyle);
|
||
y += S(28);
|
||
y = ProfileRow(x, y, w, "HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, profileValueStyle);
|
||
y = ProfileRow(x, y, w, "VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? profileValueStyle : badgeStyle);
|
||
y = ProfileRow(x, y, w, "STEAM", s.RequireSteam ? "Required" : "Not required", profileValueStyle);
|
||
y = ProfileRow(x, y, w, "ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", profileValueStyle);
|
||
y = ProfileRow(x, y, w, "PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
|
||
y = ProfileRow(x, y, w, "ADDRESS", s.Address + ":" + s.Port, profileValueStyle);
|
||
if (s.Mods.Count > 0) ProfileRow(x, y, w, "MODS", string.Join(", ", s.Mods.ToArray()), profileWrapStyle);
|
||
|
||
// Spieler
|
||
float lx = list.x + S(20), lw = list.width - S(40), ly = list.y + S(16);
|
||
GUI.Label(new Rect(lx, ly, lw, S(18)), "PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle);
|
||
ly += S(30);
|
||
var listArea = new Rect(lx, ly, lw, list.yMax - ly - S(14));
|
||
if (s.PlayerNames == null || s.PlayerNames.Count == 0)
|
||
{
|
||
GUI.Label(listArea, "Nobody online right now.", profileEmptyStyle);
|
||
}
|
||
else
|
||
{
|
||
float rowH = S(32);
|
||
var inner = new Rect(0, 0, lw - S(14), rowH * s.PlayerNames.Count);
|
||
profileScroll = GUI.BeginScrollView(listArea, profileScroll, inner);
|
||
for (int i = 0; i < s.PlayerNames.Count; i++)
|
||
{
|
||
var line = new Rect(0, i * rowH, inner.width, rowH);
|
||
if (i > 0) GUI.DrawTexture(new Rect(line.x, line.y, line.width, S(1)), texDivider);
|
||
GUI.DrawTexture(new Rect(line.x + S(2), line.y + (rowH - S(8)) / 2f, S(8), S(8)), texOnline);
|
||
GUI.Label(new Rect(line.x + S(20), line.y, line.width - S(20), rowH), s.PlayerNames[i], profileValueStyle);
|
||
}
|
||
GUI.EndScrollView();
|
||
}
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
|
||
GUIStyle profileValueStyle, profileWrapStyle, profileCaptionStyle, profileEmptyStyle;
|
||
|
||
void EnsureProfileStyles()
|
||
{
|
||
if (profileValueStyle != null && profileValueStyle.fontSize == P(15)) return;
|
||
profileValueStyle = new GUIStyle(rowNameStyle) { fontSize = P(15), wordWrap = false, clipping = TextClipping.Clip, alignment = TextAnchor.MiddleLeft };
|
||
profileWrapStyle = new GUIStyle(profileValueStyle) { wordWrap = true, fontSize = P(13), alignment = TextAnchor.UpperLeft };
|
||
profileWrapStyle.normal.textColor = Muted;
|
||
profileCaptionStyle = new GUIStyle(captionStyle) { alignment = TextAnchor.MiddleLeft };
|
||
profileCaptionStyle.margin = new RectOffset();
|
||
profileEmptyStyle = new GUIStyle(textStyle) { alignment = TextAnchor.MiddleCenter };
|
||
}
|
||
|
||
/// <summary>Info-Zeile der Server-Karte: Bezeichnung links, Wert daneben, feine Linie darunter. Gibt das nächste y zurück.</summary>
|
||
float ProfileRow(float x, float y, float w, string caption, string value, GUIStyle valueStyle)
|
||
{
|
||
const float captionWidth = 120f;
|
||
float valueWidth = w - S(captionWidth);
|
||
float h = S(36);
|
||
if (valueStyle.wordWrap) h = Mathf.Max(h, valueStyle.CalcHeight(new GUIContent(value), valueWidth) + S(16));
|
||
GUI.Label(new Rect(x, y, S(captionWidth), S(36)), caption, profileCaptionStyle);
|
||
var valueRect = valueStyle.wordWrap ? new Rect(x + S(captionWidth), y + S(9), valueWidth, h - S(9)) : new Rect(x + S(captionWidth), y, valueWidth, S(36));
|
||
GUIStyle style = valueStyle;
|
||
if (!valueStyle.wordWrap && valueStyle.alignment != TextAnchor.MiddleLeft)
|
||
style = new GUIStyle(valueStyle) { alignment = TextAnchor.MiddleLeft };
|
||
GUI.Label(valueRect, value, style);
|
||
GUI.DrawTexture(new Rect(x, y + h, w, S(1)), texDivider);
|
||
return y + h + S(1);
|
||
}
|
||
|
||
/// <summary>Eine Info-Zeile: Bezeichnung links (grau), Wert rechts.</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>Kleines abgerundetes Abzeichen, so breit wie sein Text. Gibt die belegte Breite zurück.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>Filter-Chip (an = orange umrandet), ersetzt die Unity-Häkchen.</summary>
|
||
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));
|
||
}
|
||
|
||
/// <summary>Ping farbig: grün gut, gelb geht so, rot schlecht.</summary>
|
||
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";
|
||
}
|
||
|
||
/// <summary>Server hat eine andere Version: warnen, Verbinden nur auf Wunsch.</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>Nachfrage nach einem JOIN-Link der Website (keine Webseite soll einen ungefragt verbinden).</summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Mods
|
||
|
||
Vector2 modsScroll;
|
||
|
||
/// <summary>Server verlangt Mods, die hier fehlen: zeigen, was geladen würde, und fragen.</summary>
|
||
void DrawModsPrompt(MultiplayerManager mp)
|
||
{
|
||
if (texDim == null) texDim = Solid(new Color(0f, 0f, 0f, 0.6f));
|
||
GUI.DrawTexture(new Rect(0, 0, vw, vh), texDim);
|
||
List<MultiplayerManager.ServerModInfo> list = mp.PendingMods;
|
||
float height = S(250) + S(46) * Mathf.Min(list.Count, 6);
|
||
var r = new Rect(Mathf.Round((vw - S(620)) / 2f), Mathf.Round((vh - height) / 2f), S(620), height);
|
||
UiTheme.DrawShadow(r);
|
||
GUI.Box(r, GUIContent.none, windowBox);
|
||
GUI.DrawTexture(new Rect(r.x, r.y, r.width, S(4)), texAccent);
|
||
|
||
float x = r.x + S(32), w = r.width - S(64), y = r.y + S(26);
|
||
GUI.Label(new Rect(x, y, w, S(34)), "THIS SERVER USES MODS", titleStyle);
|
||
y += S(44);
|
||
GUI.Label(new Rect(x, y, w, S(44)), "To play here you need these mods. Mods are programs - they run on your PC. " +
|
||
"Only install them from servers you trust.", textStyle);
|
||
y += S(52);
|
||
for (int i = 0; i < list.Count && i < 6; i++)
|
||
{
|
||
MultiplayerManager.ServerModInfo m = list[i];
|
||
var row = new Rect(x, y, w, S(40));
|
||
GUI.Box(row, GUIContent.none, rowBox);
|
||
GUI.Label(new Rect(row.x + S(14), row.y, w * 0.55f, row.height), m.Name + (string.IsNullOrEmpty(m.Version) ? "" : " " + m.Version), rowNameStyle);
|
||
GUI.Label(new Rect(row.x + w * 0.55f, row.y, w * 0.45f - S(14), row.height),
|
||
(m.Size / 1024) + " KB · SHA-256 " + (m.Sha256.Length > 12 ? m.Sha256.Substring(0, 12) + "…" : m.Sha256), headerCellStyle);
|
||
y += S(46);
|
||
}
|
||
if (list.Count > 6) GUI.Label(new Rect(x, y - S(6), w, S(20)), "+ " + (list.Count - 6) + " more", textStyle);
|
||
|
||
float by = r.yMax - S(28) - S(42);
|
||
if (GUI.Button(new Rect(r.xMax - S(32) - S(220), by, S(220), S(42)), "INSTALL & JOIN", primaryStyle)) mp.AcceptServerMods();
|
||
if (GUI.Button(new Rect(r.xMax - S(32) - S(220) - S(12) - S(140), by, S(140), S(42)), "CANCEL", secondaryStyle)) mp.DeclineServerMods();
|
||
}
|
||
|
||
/// <summary>Seite "Mods": geladene MSCO-Mods, Ordner öffnen.</summary>
|
||
void DrawMods()
|
||
{
|
||
GUILayout.BeginHorizontal();
|
||
GUILayout.Label("Mods", titleStyle, GUILayout.ExpandWidth(true));
|
||
if (GUILayout.Button("OPEN FOLDER", tabStyle, GUILayout.Height(S(28)), GUILayout.Width(S(130))))
|
||
{
|
||
try
|
||
{
|
||
System.IO.Directory.CreateDirectory(ModLoader.Folder);
|
||
System.Diagnostics.Process.Start("explorer.exe", "\"" + ModLoader.Folder + "\"");
|
||
}
|
||
catch { }
|
||
}
|
||
GUILayout.EndHorizontal();
|
||
GUILayout.Label("Put MSCO mods (.dll) into MSCO\\Mods in the game folder and restart the game - or manage them in the MSCO Launcher. " +
|
||
"Mods from a server are downloaded when you join it (you are asked first) and only run on that server.", textStyle);
|
||
GUILayout.Space(S(12));
|
||
|
||
modsScroll = GUILayout.BeginScrollView(modsScroll, GUILayout.Height(ListHeight(S(150f), S(140f))));
|
||
if (ModLoader.All.Count == 0) GUILayout.Label("No mods installed.", textStyle);
|
||
foreach (ModLoader.LoadedMod m in ModLoader.All)
|
||
{
|
||
Rect row = GUILayoutUtility.GetRect(S(10), S(52), GUILayout.ExpandWidth(true));
|
||
var box = new Rect(row.x, row.y + S(3), row.width, row.height - S(6));
|
||
GUI.Box(box, GUIContent.none, rowBox);
|
||
GUI.DrawTexture(new Rect(box.x + S(14), box.y + (box.height - S(8)) / 2f, S(8), S(8)),
|
||
m.Error != null ? texPending : m.Active ? texOnline : texMuted);
|
||
string name = "?", version = "", author = "";
|
||
try { name = m.Mod.Name; version = m.Mod.Version; author = m.Mod.Author; } catch { }
|
||
float nx = box.x + S(32);
|
||
if (m.FromServer) nx += Pill(nx, new Rect(box.x, box.y, box.width, box.height), "SERVER", pillMutedStyle);
|
||
GUI.Label(new Rect(nx, box.y, box.width * 0.5f, box.height), name + " " + version, rowNameStyle);
|
||
string state = m.Error != null ? "turned off: " + m.Error : m.Active ? "active" : m.FromServer ? "only on its server" : "off";
|
||
GUI.Label(new Rect(box.x + box.width * 0.55f, box.y, box.width * 0.45f - S(14), box.height),
|
||
(string.IsNullOrEmpty(author) ? "" : "by " + author + " · ") + state, headerCellStyle);
|
||
}
|
||
GUILayout.EndScrollView();
|
||
}
|
||
|
||
bool newGameConfirm;
|
||
|
||
/// <summary>Host: eigenen Spielstand weiterspielen oder ein neues Spiel anlegen (mit Sicherheitsabfrage).</summary>
|
||
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.Card(Field, P(10));
|
||
rowHoverBox = new GUIStyle(rowBox);
|
||
rowHoverBox.normal.background = UiTheme.RoundedEx(FieldFocus, FieldFocus, new Color(Accent.r, Accent.g, Accent.b, 0.45f), true, P(10), P(10) * 2 + 4);
|
||
texGlow = Glow(new Color(Accent.r, Accent.g, Accent.b, 0.16f));
|
||
scrollbarStyle = new GUIStyle();
|
||
scrollbarStyle.fixedWidth = S(8);
|
||
scrollbarStyle.margin = new RectOffset(P(6), 0, 0, 0);
|
||
scrollThumbStyle = UiTheme.Panel9(new Color(1f, 1f, 1f, 0.14f), P(3));
|
||
scrollThumbStyle.fixedWidth = S(6);
|
||
scrollThumbStyle.hover.background = UiTheme.Rounded(new Color(1f, 1f, 1f, 0.24f), P(3));
|
||
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);
|
||
pillDangerStyle = new GUIStyle(pillStyle);
|
||
pillDangerStyle.normal.background = UiTheme.Rounded(new Color(UiTheme.Danger.r, UiTheme.Danger.g, UiTheme.Danger.b, 0.25f), P(4));
|
||
pillDangerStyle.normal.textColor = UiTheme.DangerHover;
|
||
newsTitleStyle = new GUIStyle(rowNameStyle) { wordWrap = true };
|
||
featuredRowBox = UiTheme.Card(Color.Lerp(Field, Gold, 0.14f), P(10));
|
||
|
||
chipOnStyle = Button(new Color(Accent.r, Accent.g, Accent.b, 0.2f), new Color(Accent.r, Accent.g, Accent.b, 0.3f), AccentHover, P(11), FontStyle.Bold, P(11), new Color(Accent.r, Accent.g, Accent.b, 0.55f));
|
||
chipOffStyle = Button(Field, FieldFocus, Muted, P(11), FontStyle.Bold, P(11), UiTheme.Border);
|
||
chipOffStyle.hover.textColor = Color.white;
|
||
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);
|
||
betaPillStyle = UiTheme.Panel9(UiTheme.Accent, 4);
|
||
if (UiTheme.DynamicFont != null) betaPillStyle.font = UiTheme.DynamicFont;
|
||
betaPillStyle.normal.textColor = new Color(0.1f, 0.07f, 0.02f);
|
||
betaPillStyle.fontSize = P(10);
|
||
betaPillStyle.fontStyle = FontStyle.Bold;
|
||
betaPillStyle.alignment = TextAnchor.MiddleCenter;
|
||
betaNoteStyle = Label(P(11), Muted, FontStyle.Normal);
|
||
betaNoteStyle.wordWrap = true;
|
||
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, new Color(1f, 1f, 1f, 0.05f), Muted, P(14), FontStyle.Normal, P(10));
|
||
navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau)
|
||
navStyle.alignment = TextAnchor.MiddleLeft;
|
||
navStyle.padding = new RectOffset(P(22), P(10), P(0), P(0));
|
||
navActiveStyle = Button(Color.Lerp(Sidebar, Accent, 0.12f), Color.Lerp(Sidebar, Accent, 0.16f), Color.white, P(14), FontStyle.Bold, P(10), new Color(Accent.r, Accent.g, Accent.b, 0.25f));
|
||
navActiveStyle.alignment = TextAnchor.MiddleLeft;
|
||
navActiveStyle.padding = new RectOffset(P(22), P(10), P(0), P(0));
|
||
|
||
primaryStyle = Button(Accent, AccentHover, Color.white, P(14), FontStyle.Bold, P(10));
|
||
primaryStyle.normal.background = primaryStyle.focused.background = UiTheme.RoundedEx(AccentHover, Accent, Color.clear, false, P(10));
|
||
primaryStyle.hover.background = primaryStyle.active.background = primaryStyle.onNormal.background =
|
||
UiTheme.RoundedEx(Color.Lerp(AccentHover, Color.white, 0.12f), AccentHover, Color.clear, false, P(10));
|
||
dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, P(13), FontStyle.Bold, P(10));
|
||
secondaryStyle = Button(Hex(0x22, 0x22, 0x28), Hex(0x2a, 0x2a, 0x31), Color.white, P(14), FontStyle.Bold, P(10), UiTheme.Border);
|
||
tabStyle = Button(Hex(0x1b, 0x1b, 0x20), Hex(0x24, 0x24, 0x2a), Muted, P(11), FontStyle.Bold, P(10), UiTheme.Border);
|
||
tabStyle.hover.textColor = Color.white;
|
||
|
||
fieldStyle = UiTheme.Field(P(14));
|
||
fieldStyle.normal.background = UiTheme.RoundedEx(Hex(0x16, 0x16, 0x1a), Hex(0x16, 0x16, 0x1a), UiTheme.Border, true, P(10), P(10) * 2 + 4);
|
||
fieldStyle.hover.background = UiTheme.RoundedEx(Hex(0x19, 0x19, 0x1e), Hex(0x19, 0x19, 0x1e), new Color(1f, 1f, 1f, 0.14f), true, P(10), P(10) * 2 + 4);
|
||
fieldStyle.focused.background = UiTheme.RoundedEx(Hex(0x19, 0x19, 0x1e), Hex(0x19, 0x19, 0x1e), Accent, true, P(10), P(10) * 2 + 4);
|
||
fieldStyle.border = new RectOffset(P(10) + 2, P(10) + 2, P(10) + 2, P(10) + 2);
|
||
|
||
stylesReady = true;
|
||
}
|
||
|
||
static GUIStyle Label(int size, Color color, FontStyle font)
|
||
{
|
||
var s = new GUIStyle(GUI.skin.label);
|
||
if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont;
|
||
s.fontSize = size;
|
||
s.fontStyle = font;
|
||
s.normal.textColor = color;
|
||
return s;
|
||
}
|
||
|
||
/// <summary>Knopf mit runden Ecken (9-Slice, siehe UiTheme), optional mit feinem Rand wie .btn auf der Website.</summary>
|
||
static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font, int radius = UiTheme.Radius, Color border = default(Color))
|
||
{
|
||
var s = new GUIStyle(GUI.skin.button);
|
||
if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont;
|
||
bool withBorder = border.a > 0f;
|
||
int h = radius * 2 + 4;
|
||
Texture2D n = UiTheme.RoundedEx(normal, normal, border, withBorder, radius, h);
|
||
Texture2D o = UiTheme.RoundedEx(hover, hover, withBorder ? new Color(border.r, border.g, border.b, Mathf.Min(1f, border.a * 1.8f)) : border, withBorder, radius, h);
|
||
s.normal.background = n;
|
||
s.focused.background = n;
|
||
s.hover.background = o;
|
||
s.active.background = o;
|
||
s.onNormal.background = o;
|
||
s.normal.textColor = s.hover.textColor = s.active.textColor = s.focused.textColor = s.onNormal.textColor = text;
|
||
s.border = new RectOffset(radius + 2, radius + 2, radius + 2, radius + 2);
|
||
s.fontSize = size;
|
||
s.fontStyle = font;
|
||
return s;
|
||
}
|
||
|
||
/// <summary>Weicher runder Schein (Mitte deckend, nach außen durchsichtig) - für den orangen Schimmer oben.</summary>
|
||
static Texture2D Glow(Color color)
|
||
{
|
||
const int w = 128, h = 64;
|
||
var tex = new Texture2D(w, h, TextureFormat.ARGB32, false);
|
||
tex.wrapMode = TextureWrapMode.Clamp;
|
||
tex.filterMode = FilterMode.Bilinear;
|
||
for (int y = 0; y < h; y++)
|
||
for (int x = 0; x < w; x++)
|
||
{
|
||
float dx = (x + 0.5f) / w * 2f - 1f, dy = (y + 0.5f) / h * 2f - 1f;
|
||
float d = Mathf.Clamp01(Mathf.Sqrt(dx * dx + dy * dy));
|
||
float a = 1f - d;
|
||
tex.SetPixel(x, y, new Color(color.r, color.g, color.b, color.a * a * a));
|
||
}
|
||
tex.Apply();
|
||
tex.hideFlags = HideFlags.HideAndDontSave;
|
||
return tex;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|