Crisp launcher (sizes and fonts scaled in screen pixels instead of a GUI matrix), fps command and display, fewer hitches (staggered scans, shared rigidbody scan, crash-damage mesh reads only after impacts, rarer vehicle fx lookups, cached display prefs)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:26:43 +02:00
co-authored by Claude Opus 5.5
parent 0b55d270e5
commit 7d341126e8
13 changed files with 381 additions and 231 deletions
+7
View File
@@ -26,6 +26,13 @@
Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d --build`). Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d --build`).
**Scharfe Oberfläche, FPS, Leistung**
- Launcher wird in voller Auflösung gezeichnet (Schrift und Kanten scharf statt hochgezogen/verpixelt)
- `fps` in der F7-Konsole: FPS-Anzeige oben rechts (grün/gelb/rot), Konsole zeigt auch das längste Bild (Ruckler)
- Weniger Ruckler: Sync-Teile suchen nach dem Laden nicht mehr alle im selben Frame, gemeinsame Suche nach Rigidbodys,
Unfallschäden werden nur noch nach einem Aufprall geprüft (vorher jede Sekunde große Mesh-Kopien), Fahrzeug-Effekte
schlagen ihre Bauteile seltener nach, Ping-/FPS-Anzeige ohne Registry-Zugriff pro Bild
**Launcher im Vollbild** **Launcher im Vollbild**
- Der MSCO-Launcher füllt das ganze Hauptmenü und **skaliert automatisch** mit der Auflösung; Listen wachsen mit dem - Der MSCO-Launcher füllt das ganze Hauptmenü und **skaliert automatisch** mit der Auflösung; Listen wachsen mit dem
Platz (keine abgeschnittenen Knöpfe mehr). Das Spielmenü dahinter (Logo, Farbauswahl, MP3 …) ist komplett verdeckt Platz (keine abgeschnittenen Knöpfe mehr). Das Spielmenü dahinter (Logo, Farbauswahl, MP3 …) ist komplett verdeckt
+7
View File
@@ -18,6 +18,7 @@ namespace MSCO
"host [port] / join <ip> [port] - direct LAN game (no Steam)\n" + "host [port] / join <ip> [port] - direct LAN game (no Steam)\n" +
"save - save the game (host / singleplayer)\n" + "save - save the game (host / singleplayer)\n" +
"ping [on|off] - show your ping to the server (toggle)\n" + "ping [on|off] - show your ping to the server (toggle)\n" +
"fps [on|off] - show the frames per second (toggle)\n" +
"/admin - admin menu (kick / ban / teleport / cheats)\n" + "/admin - admin menu (kick / ban / teleport / cheats)\n" +
"vehicles | rig - debug info about vehicles / player models\n" + "vehicles | rig - debug info about vehicles / player models\n" +
"debug - toggle sync debug output\n" + "debug - toggle sync debug output\n" +
@@ -112,6 +113,12 @@ namespace MSCO
else mp.ShowPing = !mp.ShowPing; else mp.ShowPing = !mp.ShowPing;
print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")"); print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")");
break; break;
case "fps":
// "fps" schaltet die Anzeige um, "fps on/off" setzt sie
if (args.Length > 1) mp.ShowFps = args[1].ToLower() == "on" || args[1] == "1";
else mp.ShowFps = !mp.ShowFps;
print("FPS: " + mp.FpsText + " (display " + (mp.ShowFps ? "on" : "off") + ")");
break;
case "admin": case "admin":
case "/admin": case "/admin":
mp.OpenAdminMenu(); mp.OpenAdminMenu();
+2 -1
View File
@@ -60,11 +60,12 @@ namespace MSCO
{ {
controls.Clear(); controls.Clear();
scanned.Clear(); scanned.Clear();
nextScan = 0f; nextScan = -1f; // erster Suchlauf verteilt, siehe Update
} }
public void Update(ObjectSync objects, int localId, Vector3 playerPos, Action<MsgType, byte[]> send) public void Update(ObjectSync objects, int localId, Vector3 playerPos, Action<MsgType, byte[]> send)
{ {
if (nextScan < 0f) nextScan = Time.time + 2.4f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan) if (Time.time >= nextScan)
{ {
nextScan = Time.time + ScanInterval; nextScan = Time.time + ScanInterval;
+34 -8
View File
@@ -14,8 +14,10 @@ namespace MSCO
public class DamageSync public class DamageSync
{ {
const float ScanInterval = 10f; const float ScanInterval = 10f;
const float CheckInterval = 1f; // Fahrzeuge, die wir fahren const float CheckInterval = 0.5f; // so oft nach einem Aufprall prüfen
const float FreeCheckInterval = 5f; // geparkte Fahrzeuge (Unfall nur durch Anstoßen) seltener const float ImpactWindow = 4f; // so lange nach einem Aufprall prüfen (Verformung läuft ein paar Frames)
const float ImpactDeltaV = 2.5f; // plötzliche Geschwindigkeitsänderung (m/s pro Frame) = Aufprall
const float SafetyCheckInterval = 30f; // sonst nur selten (z.B. Reparatur in der Werkstatt)
const float MinMove = 0.002f; const float MinMove = 0.002f;
const int MaxPerMessage = 4000; const int MaxPerMessage = 4000;
@@ -26,13 +28,18 @@ namespace MSCO
public int Key; public int Key;
public string Root; public string Root;
public MeshFilter Filter; public MeshFilter Filter;
public Rigidbody Body; // Fahrzeug, zu dem das Mesh gehört
public Vector3[] Known; // zuletzt gesendet/empfangen public Vector3[] Known; // zuletzt gesendet/empfangen
} }
readonly Dictionary<int, DamageMesh> meshes = new Dictionary<int, DamageMesh>(); readonly Dictionary<int, DamageMesh> meshes = new Dictionary<int, DamageMesh>();
readonly HashSet<MeshFilter> seen = new HashSet<MeshFilter>(); readonly HashSet<MeshFilter> seen = new HashSet<MeshFilter>();
readonly List<int> changed = new List<int>(); readonly List<int> changed = new List<int>();
float nextScan, nextCheck, nextFreeCheck; float nextScan, nextCheck, nextSafetyCheck;
// Mesh-Daten lesen ist teuer (große Arrays, Speicher-Müll) - nur nach einem Aufprall
readonly Dictionary<Rigidbody, Vector3> lastVelocity = new Dictionary<Rigidbody, Vector3>();
readonly Dictionary<Rigidbody, float> lastImpact = new Dictionary<Rigidbody, float>();
readonly List<Rigidbody> bodies = new List<Rigidbody>();
static List<Type> types; static List<Type> types;
public int Count { get { return meshes.Count; } } public int Count { get { return meshes.Count; } }
@@ -41,27 +48,44 @@ namespace MSCO
{ {
meshes.Clear(); meshes.Clear();
seen.Clear(); seen.Clear();
nextScan = 0f; lastVelocity.Clear();
lastImpact.Clear();
bodies.Clear();
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
} }
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send) public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send)
{ {
if (nextScan < 0f) nextScan = Time.time + 1.0f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan) if (Time.time >= nextScan)
{ {
nextScan = Time.time + ScanInterval; nextScan = Time.time + ScanInterval;
Scan(); Scan();
} }
// Aufprall erkennen: Geschwindigkeit ändert sich schlagartig (billig, jedes Frame)
for (int i = 0; i < bodies.Count; i++)
{
Rigidbody rb = bodies[i];
if (rb == null || rb.isKinematic) continue;
Vector3 v = rb.velocity, before;
if (lastVelocity.TryGetValue(rb, out before) && (v - before).sqrMagnitude > ImpactDeltaV * ImpactDeltaV)
lastImpact[rb] = Time.time;
lastVelocity[rb] = v;
}
if (Time.time < nextCheck) return; if (Time.time < nextCheck) return;
nextCheck = Time.time + CheckInterval; nextCheck = Time.time + CheckInterval;
bool checkFree = Time.time >= nextFreeCheck; bool safety = Time.time >= nextSafetyCheck;
if (checkFree) nextFreeCheck = Time.time + FreeCheckInterval; if (safety) nextSafetyCheck = Time.time + SafetyCheckInterval;
foreach (DamageMesh m in meshes.Values) foreach (DamageMesh m in meshes.Values)
{ {
if (m.Filter == null) continue; if (m.Filter == null) continue;
int owner = objects.VehicleOwner(m.Root); int owner = objects.VehicleOwner(m.Root);
if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt die Schäden if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt die Schäden
if (owner == SyncedBody.NoOwner && !checkFree) continue; float impact;
bool recentImpact = m.Body != null && lastImpact.TryGetValue(m.Body, out impact) && Time.time - impact < ImpactWindow;
if (!recentImpact && !safety) continue;
Mesh mesh = m.Filter.sharedMesh; Mesh mesh = m.Filter.sharedMesh;
if (mesh == null) continue; if (mesh == null) continue;
@@ -164,7 +188,9 @@ namespace MSCO
if (mesh == null || mesh.vertexCount == 0 || mesh.vertexCount > 65000) return; if (mesh == null || mesh.vertexCount == 0 || mesh.vertexCount > 65000) return;
int key = Hash(FsmSync.PathOf(mf.transform)); int key = Hash(FsmSync.PathOf(mf.transform));
if (meshes.ContainsKey(key)) return; // nicht eindeutig if (meshes.ContainsKey(key)) return; // nicht eindeutig
meshes[key] = new DamageMesh { Key = key, Root = mf.transform.root.name, Filter = mf, Known = mesh.vertices }; Rigidbody body = mf.transform.root.GetComponent<Rigidbody>();
meshes[key] = new DamageMesh { Key = key, Root = mf.transform.root.name, Filter = mf, Body = body, Known = mesh.vertices };
if (body != null && !bodies.Contains(body)) bodies.Add(body);
} }
static bool IsVehicle(string root) static bool IsVehicle(string root)
+2 -1
View File
@@ -50,11 +50,12 @@ namespace MSCO
foreach (Hinge h in hinges.Values) StopDriving(h); foreach (Hinge h in hinges.Values) StopDriving(h);
hinges.Clear(); hinges.Clear();
seen.Clear(); seen.Clear();
nextScan = 0f; nextScan = -1f; // erster Suchlauf verteilt, siehe Update
} }
public void Update(ObjectSync objects, int localId, Vector3 playerPos, float dt, Action<MsgType, byte[]> send) public void Update(ObjectSync objects, int localId, Vector3 playerPos, float dt, Action<MsgType, byte[]> send)
{ {
if (nextScan < 0f) nextScan = Time.time + 1.7f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan) if (Time.time >= nextScan)
{ {
nextScan = Time.time + ScanInterval; nextScan = Time.time + ScanInterval;
+215 -210
View File
@@ -19,9 +19,16 @@ namespace MSCO
const float SidebarWidth = 240f; const float SidebarWidth = 240f;
const float StatusHeight = 40f; const float StatusHeight = 40f;
const float MaxContentWidth = 1100f; const float MaxContentWidth = 1100f;
float vw, vh, uiScale = 1f; // virtuelle Bildschirmgröße (Einheiten vor der Skalierung) 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 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> /// <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); } float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); }
@@ -84,15 +91,12 @@ namespace MSCO
public void Draw(MultiplayerManager mp) public void Draw(MultiplayerManager mp)
{ {
EnsureStyles();
uiScale = Mathf.Clamp(Mathf.Min(Screen.height / RefHeight, Screen.width / RefWidth), MinScale, MaxScale); uiScale = Mathf.Clamp(Mathf.Min(Screen.height / RefHeight, Screen.width / RefWidth), MinScale, MaxScale);
vw = Screen.width / uiScale; EnsureStyles(); // baut die Stile neu, wenn sich die Skalierung ändert (Auflösung gewechselt)
vh = Screen.height / uiScale; vw = Screen.width;
vh = Screen.height;
ScreenRect = new Rect(0, 0, Screen.width, Screen.height); ScreenRect = new Rect(0, 0, Screen.width, Screen.height);
Matrix4x4 previous = GUI.matrix; DrawScaled(mp);
GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(uiScale, uiScale, 1f));
try { DrawScaled(mp); }
finally { GUI.matrix = previous; }
} }
void DrawScaled(MultiplayerManager mp) void DrawScaled(MultiplayerManager mp)
@@ -109,12 +113,12 @@ namespace MSCO
var window = new Rect(0, 0, vw, vh); var window = new Rect(0, 0, vw, vh);
GUI.Box(window, GUIContent.none, windowBox); GUI.Box(window, GUIContent.none, windowBox);
DrawSidebar(new Rect(0, 0, SidebarWidth, vh - StatusHeight)); DrawSidebar(new Rect(0, 0, S(SidebarWidth), vh - S(StatusHeight)));
// Inhalt mittig im freien Bereich, nicht beliebig breit (lesbar bleiben) // Inhalt mittig im freien Bereich, nicht beliebig breit (lesbar bleiben)
float free = vw - SidebarWidth; float free = vw - S(SidebarWidth);
float width = Mathf.Min(MaxContentWidth, free - 96f); float width = Mathf.Min(S(MaxContentWidth), free - S(96f));
var content = new Rect(SidebarWidth + (free - width) / 2f, 40f, width, vh - StatusHeight - 72f); var content = new Rect(S(SidebarWidth) + Mathf.Round((free - width) / 2f), S(40f), width, vh - S(StatusHeight) - S(72f));
contentHeight = content.height; contentHeight = content.height;
GUILayout.BeginArea(content); GUILayout.BeginArea(content);
if (versionWarning) DrawVersionWarning(mp); if (versionWarning) DrawVersionWarning(mp);
@@ -125,7 +129,7 @@ namespace MSCO
else DrawServerBrowser(mp); else DrawServerBrowser(mp);
GUILayout.EndArea(); GUILayout.EndArea();
DrawStatusBar(mp, new Rect(0, vh - StatusHeight, vw, StatusHeight)); DrawStatusBar(mp, new Rect(0, vh - S(StatusHeight), vw, S(StatusHeight)));
} }
// ---------------------------------------------------------------- Bereiche // ---------------------------------------------------------------- Bereiche
@@ -135,38 +139,38 @@ namespace MSCO
GUI.Box(r, GUIContent.none, sidebarBox); GUI.Box(r, GUIContent.none, sidebarBox);
// Logo + Schriftzug // Logo + Schriftzug
Texture2D logo = Branding.LogoTexture; Texture2D logo = Branding.LogoTexture;
float textX = r.x + 22; float textX = r.x + S(22);
if (logo != null) if (logo != null)
{ {
GUI.DrawTexture(new Rect(r.x + 20, r.y + 20, 44, 44), logo); GUI.DrawTexture(new Rect(r.x + S(20), r.y + S(20), S(44), S(44)), logo);
textX = r.x + 72; textX = r.x + S(72);
} }
GUI.Label(new Rect(textX, r.y + 24, r.xMax - textX, 40), "MSCO", logoStyle); GUI.Label(new Rect(textX, r.y + S(24), r.xMax - textX, S(40)), "MSCO", logoStyle);
GUI.Label(new Rect(r.x + 22, r.y + 70, r.width - 30, 20), "MY SUMMER CAR ONLINE", taglineStyle); 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 + 20, r.y + 98, r.width - 40, 1), texDivider); GUI.DrawTexture(new Rect(r.x + S(20), r.y + S(98), r.width - S(40), S(1)), texDivider);
float y = r.y + 110; float y = r.y + S(110);
if (NavButton(new Rect(r.x, y, r.width, 42), "P2P", page == Page.P2P)) page = Page.P2P; if (NavButton(new Rect(r.x, y, r.width, S(42)), "P2P", page == Page.P2P)) page = Page.P2P;
y += 44; y += S(44);
if (NavButton(new Rect(r.x, y, r.width, 42), "Server Browser", page == Page.ServerBrowser)) page = Page.ServerBrowser; if (NavButton(new Rect(r.x, y, r.width, S(42)), "Server Browser", page == Page.ServerBrowser)) page = Page.ServerBrowser;
y += 44; y += S(44);
int favCount = Favorites.All.Count; int favCount = Favorites.All.Count;
if (NavButton(new Rect(r.x, y, r.width, 42), "Favorites" + (favCount > 0 ? " (" + favCount + ")" : ""), page == Page.Favorites)) page = Page.Favorites; 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) // Eigenes Hauptmenü: Spiel beenden (die Knöpfe des Spiel-Menüs sind ausgeblendet)
if (quitConfirm) if (quitConfirm)
{ {
if (GUI.Button(new Rect(r.x + 16, r.yMax - 118, r.width - 32, 36), "QUIT GAME?", dangerStyle)) Application.Quit(); 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 + 16, r.yMax - 76, r.width - 32, 30), "CANCEL", tabStyle)) quitConfirm = false; 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 + 16, r.yMax - 76, r.width - 32, 36), "QUIT", tabStyle)) quitConfirm = true; 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 + 24, r.yMax - 30, r.width - 48, 20), "v" + MSCO.ModVersion, taglineStyle); 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) bool NavButton(Rect r, string text, bool active)
{ {
if (active) GUI.Box(new Rect(r.x + 4, r.y + 8, 4, r.height - 16), GUIContent.none, accentBox); // Akzentbalken links 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); return GUI.Button(r, text, active ? navActiveStyle : navStyle);
} }
@@ -175,13 +179,13 @@ namespace MSCO
GUILayout.Label("P2P", titleStyle); GUILayout.Label("P2P", titleStyle);
GUILayout.Label("Host a game and invite up to " + (MultiplayerManager.MaxPlayers - 1) + 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); " friends through Steam - continue your own save or start a new game.", textStyle);
GUILayout.Space(18); GUILayout.Space(S(18));
if (mp.SteamAvailable) if (mp.SteamAvailable)
{ {
if (GUILayout.Button("HOST GAME", primaryStyle, GUILayout.Height(46), GUILayout.Width(250))) if (GUILayout.Button("HOST GAME", primaryStyle, GUILayout.Height(S(46)), GUILayout.Width(S(250))))
mp.HostSteam(); mp.HostSteam();
GUILayout.Space(14); GUILayout.Space(S(14));
GUILayout.Label("To join a friend: accept their invite in Steam, or right-click them in your " + 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); "Steam friends list and choose \"Join Game\".", textStyle);
} }
@@ -219,26 +223,26 @@ namespace MSCO
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
// Titel darf nicht vom REFRESH-Button zusammengedrückt werden (sonst Zeilenumbruch) // Titel darf nicht vom REFRESH-Button zusammengedrückt werden (sonst Zeilenumbruch)
GUILayout.Label("Server Browser", titleStyle, GUILayout.ExpandWidth(true)); GUILayout.Label("Server Browser", titleStyle, GUILayout.ExpandWidth(true));
if (GUILayout.Button(browser.Loading ? "LOADING..." : "REFRESH", tabStyle, GUILayout.Height(28), GUILayout.Width(110))) if (GUILayout.Button(browser.Loading ? "LOADING..." : "REFRESH", tabStyle, GUILayout.Height(S(28)), GUILayout.Width(S(110))))
browser.Refresh(mp); browser.Refresh(mp);
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
// Suche + Filter // Suche + Filter
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
searchInput = GUILayout.TextField(searchInput, 40, fieldStyle, GUILayout.Height(26), GUILayout.Width(170)); searchInput = GUILayout.TextField(searchInput, 40, fieldStyle, GUILayout.Height(S(26)), GUILayout.Width(S(170)));
if (string.IsNullOrEmpty(searchInput)) if (string.IsNullOrEmpty(searchInput))
{ {
Rect field = GUILayoutUtility.GetLastRect(); Rect field = GUILayoutUtility.GetLastRect();
GUI.Label(new Rect(field.x + 10, field.y, field.width, field.height), "Search...", placeholderStyle); GUI.Label(new Rect(field.x + S(10), field.y, field.width, field.height), "Search...", placeholderStyle);
} }
GUILayout.Space(12); GUILayout.Space(S(12));
showSteam = Chip(showSteam, "STEAM"); showSteam = Chip(showSteam, "STEAM");
GUILayout.Space(6); GUILayout.Space(S(6));
showNonSteam = Chip(showNonSteam, "NON-STEAM"); showNonSteam = Chip(showNonSteam, "NON-STEAM");
GUILayout.Space(6); GUILayout.Space(S(6));
hideFull = Chip(hideFull, "HIDE FULL"); hideFull = Chip(hideFull, "HIDE FULL");
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.Space(6); GUILayout.Space(S(6));
string search = (searchInput ?? "").Trim().ToLower(); string search = (searchInput ?? "").Trim().ToLower();
if (shownServers == null || Event.current.type == EventType.Layout) if (shownServers == null || Event.current.type == EventType.Layout)
@@ -251,35 +255,35 @@ namespace MSCO
string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError; string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError;
// Tabellenkopf // Tabellenkopf
Rect header = GUILayoutUtility.GetRect(10, 26, GUILayout.ExpandWidth(true)); Rect header = GUILayoutUtility.GetRect(S(10), S(26), GUILayout.ExpandWidth(true));
GUI.Box(header, GUIContent.none, rowBox); GUI.Box(header, GUIContent.none, rowBox);
GUI.Label(new Rect(header.x + 12, header.y, header.width * 0.48f, header.height), "SERVER NAME", headerCellStyle); 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, 70, header.height), "PLAYERS", 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, 70, header.height), "TYPE", 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, 50, header.height), "PING", 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(230f, 140f))); serverScroll = GUILayout.BeginScrollView(serverScroll, GUILayout.Height(ListHeight(S(230f), S(140f))));
if (shownError != null) GUILayout.Label(shownError, textStyle); if (shownError != null) GUILayout.Label(shownError, textStyle);
if (servers.Count == 0) if (servers.Count == 0)
GUILayout.Label(browser.Loading ? "Loading servers..." : all.Count > 0 ? "No servers match your filters." : "No servers online right now.", textStyle); 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) foreach (ServerListEntry s in servers)
{ {
Rect row = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true)); Rect row = GUILayoutUtility.GetRect(S(10), S(34), GUILayout.ExpandWidth(true));
// Hervorgehobene Server (Keymaster-Admin) stehen oben - der Master liefert sie zuerst // Hervorgehobene Server (Keymaster-Admin) stehen oben - der Master liefert sie zuerst
var rowRect = new Rect(row.x, row.y + 2, row.width, row.height - 4); var rowRect = new Rect(row.x, row.y + S(2), row.width, row.height - S(4));
bool hover = rowRect.Contains(Event.current.mousePosition); bool hover = rowRect.Contains(Event.current.mousePosition);
GUI.Box(rowRect, GUIContent.none, s.Featured ? featuredRowBox : hover ? rowHoverBox : rowBox); GUI.Box(rowRect, GUIContent.none, s.Featured ? featuredRowBox : hover ? rowHoverBox : rowBox);
if (s.Featured) GUI.DrawTexture(new Rect(row.x, row.y + 8, 3, row.height - 16), texGold); 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) // Herz: als Favorit merken / entfernen (vor dem Zeilen-Knopf, damit es den Klick bekommt)
Heart(new Rect(row.xMax - 58, row.y + 7, 20, 20), s); 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) // 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)) if (GUI.Button(new Rect(row.x, row.y, row.width, row.height), GUIContent.none, GUIStyle.none))
selectedServer = s.Address + ":" + s.Port; selectedServer = s.Address + ":" + s.Port;
GUI.DrawTexture(new Rect(row.x + 10, row.y + 13, 8, 8), s.Ping < 0 ? texPending : texOnline); GUI.DrawTexture(new Rect(row.x + S(10), row.y + S(13), S(8), S(8)), s.Ping < 0 ? texPending : texOnline);
float nameX = row.x + 26; float nameX = row.x + S(26);
Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port); Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port);
if (s.Featured) nameX += Pill(nameX, row, "FEATURED", pillFeaturedStyle); if (s.Featured) nameX += Pill(nameX, row, "FEATURED", pillFeaturedStyle);
if (fav != null) nameX += Pill(nameX, row, fav.Nickname, pillMutedStyle); if (fav != null) nameX += Pill(nameX, row, fav.Nickname, pillMutedStyle);
@@ -295,13 +299,13 @@ namespace MSCO
GUI.Label(new Rect(nameX, row.y, row.x + row.width * 0.49f - nameX, row.height), s.Name, rowNameStyle); 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) // Spielerzahl mit Füllbalken (wie auf der Website)
float px = row.x + row.width * 0.5f; float px = row.x + row.width * 0.5f;
GUI.Label(new Rect(px, row.y - 3, 70, row.height), s.Players + "/" + s.MaxPlayers, rowNameStyle); 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; float fill = s.MaxPlayers > 0 ? Mathf.Clamp01((float)s.Players / s.MaxPlayers) : 0f;
GUI.DrawTexture(new Rect(px, row.y + 24, 48, 3), texBarBack); GUI.DrawTexture(new Rect(px, row.y + S(24), S(48), S(3)), texBarBack);
GUI.DrawTexture(new Rect(px, row.y + 24, Mathf.Max(2f, 48 * fill), 3), fill >= 1f ? texBarFull : texBarFill); 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); 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, 60, row.height), s.Ping < 0 ? "..." : s.Ping + " ms", PingStyle(s.Ping)); 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 - 30, row.y, 20, row.height), "›", headerCellStyle); // Hinweis: Profil öffnen GUI.Label(new Rect(row.xMax - S(30), row.y, S(20), row.height), "›", headerCellStyle); // Hinweis: Profil öffnen
} }
GUILayout.EndScrollView(); GUILayout.EndScrollView();
@@ -310,18 +314,18 @@ namespace MSCO
if (!mp.SteamAvailable) if (!mp.SteamAvailable)
{ {
// Ohne Steam: eigener Name (nur für Non-Steam-Server) // Ohne Steam: eigener Name (nur für Non-Steam-Server)
GUILayout.BeginVertical(GUILayout.Width(150)); GUILayout.BeginVertical(GUILayout.Width(S(150)));
GUILayout.Label("YOUR NAME", captionStyle); GUILayout.Label("YOUR NAME", captionStyle);
DrawNameField(mp, 150); DrawNameField(mp, 150);
GUILayout.EndVertical(); GUILayout.EndVertical();
GUILayout.Space(10); GUILayout.Space(S(10));
} }
GUILayout.BeginVertical(); GUILayout.BeginVertical();
GUILayout.Label("DIRECT CONNECT", captionStyle); GUILayout.Label("DIRECT CONNECT", captionStyle);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
directInput = GUILayout.TextField(directInput, fieldStyle, GUILayout.Height(34)); directInput = GUILayout.TextField(directInput, fieldStyle, GUILayout.Height(S(34)));
GUILayout.Space(10); GUILayout.Space(S(10));
if (GUILayout.Button("CONNECT", primaryStyle, GUILayout.Height(34), GUILayout.Width(110))) if (GUILayout.Button("CONNECT", primaryStyle, GUILayout.Height(S(34)), GUILayout.Width(S(110))))
{ {
string host; string host;
int port; int port;
@@ -344,7 +348,7 @@ namespace MSCO
void DrawNameField(MultiplayerManager mp, float width) void DrawNameField(MultiplayerManager mp, float width)
{ {
if (nameInput == null) nameInput = mp.PlayerName; if (nameInput == null) nameInput = mp.PlayerName;
string edited = GUILayout.TextField(nameInput, 24, fieldStyle, GUILayout.Height(34), GUILayout.Width(width)); string edited = GUILayout.TextField(nameInput, 24, fieldStyle, GUILayout.Height(S(34)), GUILayout.Width(width));
if (edited != nameInput) if (edited != nameInput)
{ {
nameInput = edited; nameInput = edited;
@@ -383,40 +387,40 @@ namespace MSCO
EnsureHearts(); EnsureHearts();
GUILayout.Label("Favorites", titleStyle); 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.Label("Give each server a nickname - switch in the game with /switch <nickname> in the chat or F7 console.", textStyle);
GUILayout.Space(8); GUILayout.Space(S(8));
List<Favorites.Favorite> favs = Favorites.All; List<Favorites.Favorite> favs = Favorites.All;
favoritesScroll = GUILayout.BeginScrollView(favoritesScroll, GUILayout.Height(ListHeight(190f, 160f))); favoritesScroll = GUILayout.BeginScrollView(favoritesScroll, GUILayout.Height(ListHeight(S(190f), S(160f))));
if (favs.Count == 0) 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); 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; Favorites.Favorite remove = null;
foreach (Favorites.Favorite f in favs) 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)); 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(44)); GUILayout.BeginHorizontal(rowBox, GUILayout.Height(S(44)));
GUILayout.Space(6); GUILayout.Space(S(6));
// Herz (entfernen) // Herz (entfernen)
Rect heart = GUILayoutUtility.GetRect(22, 22, GUILayout.Width(22), GUILayout.Height(40)); Rect heart = GUILayoutUtility.GetRect(S(22), S(22), GUILayout.Width(S(22)), GUILayout.Height(S(40)));
Rect heartIcon = new Rect(heart.x, heart.y + 10, 20, 20); Rect heartIcon = new Rect(heart.x, heart.y + S(10), S(20), S(20));
GUI.DrawTexture(heartIcon, texHeartOn); GUI.DrawTexture(heartIcon, texHeartOn);
if (GUI.Button(heartIcon, GUIContent.none, GUIStyle.none)) remove = f; if (GUI.Button(heartIcon, GUIContent.none, GUIStyle.none)) remove = f;
GUILayout.Space(8); GUILayout.Space(S(8));
// Kurzname bearbeiten // Kurzname bearbeiten
string edit; string edit;
if (!nicknameEdits.TryGetValue(f.Address, out edit)) edit = f.Nickname; if (!nicknameEdits.TryGetValue(f.Address, out edit)) edit = f.Nickname;
GUILayout.BeginVertical(GUILayout.Width(150)); GUILayout.BeginVertical(GUILayout.Width(S(150)));
GUILayout.Space(7); GUILayout.Space(S(7));
edit = GUILayout.TextField(edit, 20, fieldStyle, GUILayout.Height(28), GUILayout.Width(150)); edit = GUILayout.TextField(edit, 20, fieldStyle, GUILayout.Height(S(28)), GUILayout.Width(S(150)));
GUILayout.EndVertical(); GUILayout.EndVertical();
nicknameEdits[f.Address] = edit; nicknameEdits[f.Address] = edit;
GUILayout.Space(6); GUILayout.Space(S(6));
bool changed = Favorites.CleanNickname(edit) != f.Nickname; bool changed = Favorites.CleanNickname(edit) != f.Nickname;
GUILayout.BeginVertical(GUILayout.Width(64)); GUILayout.BeginVertical(GUILayout.Width(S(64)));
GUILayout.Space(7); GUILayout.Space(S(7));
GUI.enabled = changed; GUI.enabled = changed;
if (GUILayout.Button("SAVE", tabStyle, GUILayout.Height(28), GUILayout.Width(64))) if (GUILayout.Button("SAVE", tabStyle, GUILayout.Height(S(28)), GUILayout.Width(S(64))))
{ {
string error = Favorites.Set(edit, f.Host, f.Port, f.ServerName); string error = Favorites.Set(edit, f.Host, f.Port, f.ServerName);
if (error != null) MscoLog.Warning(error); if (error != null) MscoLog.Warning(error);
@@ -424,24 +428,24 @@ namespace MSCO
} }
GUI.enabled = true; GUI.enabled = true;
GUILayout.EndVertical(); GUILayout.EndVertical();
GUILayout.Space(10); GUILayout.Space(S(10));
// Server + Status // Server + Status
GUILayout.BeginVertical(); GUILayout.BeginVertical();
GUILayout.Space(4); GUILayout.Space(S(4));
GUILayout.Label(string.IsNullOrEmpty(f.ServerName) ? f.Address : f.ServerName, rowNameStyle); GUILayout.Label(string.IsNullOrEmpty(f.ServerName) ? f.Address : f.ServerName, rowNameStyle);
GUILayout.Label(live == null ? f.Address + " · not listed / offline" GUILayout.Label(live == null ? f.Address + " · not listed / offline"
: f.Address + " · " + live.Players + "/" + live.MaxPlayers + " · " + (live.Ping < 0 ? "..." : live.Ping + " ms"), textStyle); : f.Address + " · " + live.Players + "/" + live.MaxPlayers + " · " + (live.Ping < 0 ? "..." : live.Ping + " ms"), textStyle);
GUILayout.EndVertical(); GUILayout.EndVertical();
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
GUILayout.BeginVertical(GUILayout.Width(90)); GUILayout.BeginVertical(GUILayout.Width(S(90)));
GUILayout.Space(7); GUILayout.Space(S(7));
if (GUILayout.Button("JOIN", primaryStyle, GUILayout.Height(30), GUILayout.Width(90))) mp.SwitchServer(f.Nickname); if (GUILayout.Button("JOIN", primaryStyle, GUILayout.Height(S(30)), GUILayout.Width(S(90)))) mp.SwitchServer(f.Nickname);
GUILayout.EndVertical(); GUILayout.EndVertical();
GUILayout.Space(6); GUILayout.Space(S(6));
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.Space(4); GUILayout.Space(S(4));
} }
if (remove != null) { Favorites.Remove(remove); nicknameEdits.Remove(remove.Address); } if (remove != null) { Favorites.Remove(remove); nicknameEdits.Remove(remove.Address); }
GUILayout.EndScrollView(); GUILayout.EndScrollView();
@@ -449,14 +453,14 @@ namespace MSCO
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
GUILayout.Label("ADD BY ADDRESS", captionStyle); GUILayout.Label("ADD BY ADDRESS", captionStyle);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
favoriteAddInput = GUILayout.TextField(favoriteAddInput ?? "", 60, fieldStyle, GUILayout.Height(34)); favoriteAddInput = GUILayout.TextField(favoriteAddInput ?? "", 60, fieldStyle, GUILayout.Height(S(34)));
if (string.IsNullOrEmpty(favoriteAddInput)) if (string.IsNullOrEmpty(favoriteAddInput))
{ {
Rect field = GUILayoutUtility.GetLastRect(); Rect field = GUILayoutUtility.GetLastRect();
GUI.Label(new Rect(field.x + 10, field.y, field.width, field.height), "host:port", placeholderStyle); GUI.Label(new Rect(field.x + S(10), field.y, field.width, field.height), "host:port", placeholderStyle);
} }
GUILayout.Space(10); GUILayout.Space(S(10));
if (GUILayout.Button("ADD", secondaryStyle, GUILayout.Height(34), GUILayout.Width(110))) if (GUILayout.Button("ADD", secondaryStyle, GUILayout.Height(S(34)), GUILayout.Width(S(110))))
{ {
string host; string host;
int port; int port;
@@ -517,12 +521,12 @@ namespace MSCO
/// <summary>Server-Profil: wem der Server gehört, Version, Spieler, Laufzeit ...</summary> /// <summary>Server-Profil: wem der Server gehört, Version, Spieler, Laufzeit ...</summary>
void DrawServerProfile(MultiplayerManager mp, ServerListEntry s) void DrawServerProfile(MultiplayerManager mp, ServerListEntry s)
{ {
if (GUILayout.Button("◂ BACK", tabStyle, GUILayout.Height(26), GUILayout.Width(90))) if (GUILayout.Button("◂ BACK", tabStyle, GUILayout.Height(S(26)), GUILayout.Width(S(90))))
{ {
selectedServer = null; selectedServer = null;
return; return;
} }
GUILayout.Space(8); GUILayout.Space(S(8));
if (s == null) if (s == null)
{ {
@@ -536,7 +540,7 @@ namespace MSCO
GUILayout.BeginVertical(); GUILayout.BeginVertical();
GUILayout.Label(s.Name, titleStyle); GUILayout.Label(s.Name, titleStyle);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
Rect badgeRow = GUILayoutUtility.GetRect(10, 20, GUILayout.ExpandWidth(true)); Rect badgeRow = GUILayoutUtility.GetRect(S(10), S(20), GUILayout.ExpandWidth(true));
float bx = badgeRow.x; float bx = badgeRow.x;
if (s.Featured) bx += Pill(bx, badgeRow, "FEATURED", pillFeaturedStyle); if (s.Featured) bx += Pill(bx, badgeRow, "FEATURED", pillFeaturedStyle);
bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle); bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle);
@@ -547,49 +551,49 @@ namespace MSCO
if (!string.IsNullOrEmpty(tag) && tag != "featured") bx += Pill(bx, badgeRow, tag.ToUpper(), pillMutedStyle); if (!string.IsNullOrEmpty(tag) && tag != "featured") bx += Pill(bx, badgeRow, tag.ToUpper(), pillMutedStyle);
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.EndVertical(); GUILayout.EndVertical();
GUILayout.Space(12); GUILayout.Space(S(12));
Rect heartSlot = GUILayoutUtility.GetRect(44, 44, GUILayout.Width(44), GUILayout.Height(44)); Rect heartSlot = GUILayoutUtility.GetRect(S(44), S(44), GUILayout.Width(S(44)), GUILayout.Height(S(44)));
GUI.Box(heartSlot, GUIContent.none, rowBox); GUI.Box(heartSlot, GUIContent.none, rowBox);
Heart(new Rect(heartSlot.x + 11, heartSlot.y + 11, 22, 22), s); Heart(new Rect(heartSlot.x + S(11), heartSlot.y + S(11), S(22), S(22)), s);
GUILayout.Space(8); GUILayout.Space(S(8));
GUI.enabled = CanJoin(mp, s); GUI.enabled = CanJoin(mp, s);
if (GUILayout.Button(s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(44), GUILayout.Width(180))) 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); mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers);
GUI.enabled = true; GUI.enabled = true;
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle); if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle);
GUILayout.Space(10); GUILayout.Space(S(10));
// Ohne Steam: Name wählen (sauber in einem eigenen Kasten) // Ohne Steam: Name wählen (sauber in einem eigenen Kasten)
if (s.RequireSteam && !mp.SteamAvailable) if (s.RequireSteam && !mp.SteamAvailable)
{ {
GUILayout.BeginHorizontal(rowBox, GUILayout.Height(40)); GUILayout.BeginHorizontal(rowBox, GUILayout.Height(S(40)));
GUILayout.Label("This server requires Steam - start the game through Steam to join.", textStyle); GUILayout.Label("This server requires Steam - start the game through Steam to join.", textStyle);
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.Space(10); GUILayout.Space(S(10));
} }
else if (!mp.SteamAvailable) else if (!mp.SteamAvailable)
{ {
GUILayout.BeginHorizontal(rowBox, GUILayout.Height(44)); GUILayout.BeginHorizontal(rowBox, GUILayout.Height(S(44)));
GUILayout.Space(6); GUILayout.Space(S(6));
GUILayout.BeginVertical(GUILayout.Width(110)); GUILayout.BeginVertical(GUILayout.Width(S(110)));
GUILayout.Space(4); GUILayout.Space(S(4));
GUILayout.Label("PLAYING AS", captionStyle); GUILayout.Label("PLAYING AS", captionStyle);
GUILayout.Label("no Steam", textStyle); GUILayout.Label("no Steam", textStyle);
GUILayout.EndVertical(); GUILayout.EndVertical();
GUILayout.BeginVertical(); GUILayout.BeginVertical();
GUILayout.Space(5); GUILayout.Space(S(5));
DrawNameField(mp, 220); DrawNameField(mp, 220);
GUILayout.EndVertical(); GUILayout.EndVertical();
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.Space(10); GUILayout.Space(S(10));
} }
// Infos links, Spielerliste rechts // Infos links, Spielerliste rechts
bool versionOk = s.Version == MSCO.ModVersion; bool versionOk = s.Version == MSCO.ModVersion;
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
GUILayout.BeginVertical(GUILayout.Width(250)); GUILayout.BeginVertical(GUILayout.Width(S(250)));
InfoLine("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle); 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("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle);
InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle); InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle);
@@ -597,20 +601,20 @@ namespace MSCO
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping)); InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle); InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle);
GUILayout.EndVertical(); GUILayout.EndVertical();
GUILayout.Space(16); GUILayout.Space(S(16));
GUILayout.BeginVertical(rowBox, GUILayout.ExpandWidth(true), GUILayout.MinHeight(150)); GUILayout.BeginVertical(rowBox, GUILayout.ExpandWidth(true), GUILayout.MinHeight(S(150)));
GUILayout.Space(6); GUILayout.Space(S(6));
GUILayout.Label("PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle); GUILayout.Label("PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle);
profileScroll = GUILayout.BeginScrollView(profileScroll, GUILayout.Height(ListHeight(330f, 130f))); profileScroll = GUILayout.BeginScrollView(profileScroll, GUILayout.Height(ListHeight(S(330f), S(130f))));
if (s.PlayerNames == null || s.PlayerNames.Count == 0) if (s.PlayerNames == null || s.PlayerNames.Count == 0)
GUILayout.Label("Nobody online right now.", textStyle); GUILayout.Label("Nobody online right now.", textStyle);
else else
foreach (string name in s.PlayerNames) foreach (string name in s.PlayerNames)
{ {
Rect line = GUILayoutUtility.GetRect(10, 24, GUILayout.ExpandWidth(true)); Rect line = GUILayoutUtility.GetRect(S(10), S(24), GUILayout.ExpandWidth(true));
GUI.DrawTexture(new Rect(line.x + 2, line.y + 8, 8, 8), texOnline); GUI.DrawTexture(new Rect(line.x + S(2), line.y + S(8), S(8), S(8)), texOnline);
GUI.Label(new Rect(line.x + 18, line.y, line.width - 18, line.height), name, rowNameStyle); GUI.Label(new Rect(line.x + S(18), line.y, line.width - S(18), line.height), name, rowNameStyle);
} }
GUILayout.EndScrollView(); GUILayout.EndScrollView();
GUILayout.EndVertical(); GUILayout.EndVertical();
@@ -621,8 +625,8 @@ namespace MSCO
/// <summary>Eine Info-Zeile: Bezeichnung links (grau), Wert rechts.</summary> /// <summary>Eine Info-Zeile: Bezeichnung links (grau), Wert rechts.</summary>
void InfoLine(string caption, string value, GUIStyle valueStyle) void InfoLine(string caption, string value, GUIStyle valueStyle)
{ {
GUILayout.BeginHorizontal(GUILayout.Height(26)); GUILayout.BeginHorizontal(GUILayout.Height(S(26)));
GUILayout.Label(caption, captionStyle, GUILayout.Width(100)); GUILayout.Label(caption, captionStyle, GUILayout.Width(S(100)));
GUILayout.Label(value, valueStyle); GUILayout.Label(value, valueStyle);
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
} }
@@ -631,14 +635,14 @@ namespace MSCO
static float Pill(float x, Rect row, string text, GUIStyle style) static float Pill(float x, Rect row, string text, GUIStyle style)
{ {
Vector2 size = style.CalcSize(new GUIContent(text)); Vector2 size = style.CalcSize(new GUIContent(text));
GUI.Label(new Rect(x, row.y + (row.height - 18) / 2f, size.x, 18), text, style); GUI.Label(new Rect(x, row.y + (row.height - S(18)) / 2f, size.x, S(18)), text, style);
return size.x + 8; return size.x + S(8);
} }
/// <summary>Filter-Chip (an = orange umrandet), ersetzt die Unity-Häkchen.</summary> /// <summary>Filter-Chip (an = orange umrandet), ersetzt die Unity-Häkchen.</summary>
bool Chip(bool on, string text) bool Chip(bool on, string text)
{ {
if (GUILayout.Button(text, on ? chipOnStyle : chipOffStyle, GUILayout.Height(26), GUILayout.ExpandWidth(false))) on = !on; if (GUILayout.Button(text, on ? chipOnStyle : chipOffStyle, GUILayout.Height(S(26)), GUILayout.ExpandWidth(false))) on = !on;
return on; return on;
} }
@@ -646,7 +650,7 @@ namespace MSCO
{ {
GUILayout.Label(caption, captionStyle); GUILayout.Label(caption, captionStyle);
GUILayout.Label(value, valueStyle); GUILayout.Label(value, valueStyle);
GUILayout.Space(2); GUILayout.Space(S(2));
} }
/// <summary>Ping farbig: grün gut, gelb geht so, rot schlecht.</summary> /// <summary>Ping farbig: grün gut, gelb geht so, rot schlecht.</summary>
@@ -671,20 +675,20 @@ namespace MSCO
GUILayout.Label("Version mismatch", titleStyle); GUILayout.Label("Version mismatch", titleStyle);
GUILayout.Label("This server runs a different MSCO version than you. You can still join, but things may not " + 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); "sync correctly (players, items, cars). For the best experience use the same version.", textStyle);
GUILayout.Space(16); GUILayout.Space(S(16));
GUILayout.Label("SERVER", captionStyle); GUILayout.Label("SERVER", captionStyle);
GUILayout.Label(w.Name + " (" + w.Host + ":" + w.Port + ")", rowNameStyle); GUILayout.Label(w.Name + " (" + w.Host + ":" + w.Port + ")", rowNameStyle);
GUILayout.Space(8); GUILayout.Space(S(8));
GUILayout.Label("VERSIONS", captionStyle); GUILayout.Label("VERSIONS", captionStyle);
GUILayout.Label("Server v" + w.ServerVersion + " - You v" + MSCO.ModVersion, rowTagStyle); GUILayout.Label("Server v" + w.ServerVersion + " - You v" + MSCO.ModVersion, rowTagStyle);
GUILayout.Space(22); GUILayout.Space(S(22));
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
if (GUILayout.Button("JOIN ANYWAY", primaryStyle, GUILayout.Height(42), GUILayout.Width(170))) if (GUILayout.Button("JOIN ANYWAY", primaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(170))))
mp.ConfirmVersionWarning(); mp.ConfirmVersionWarning();
GUILayout.Space(10); GUILayout.Space(S(10));
if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(42), GUILayout.Width(170))) if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(170))))
mp.CancelVersionWarning(); mp.CancelVersionWarning();
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
} }
@@ -694,22 +698,22 @@ namespace MSCO
{ {
GUILayout.Label("Join server?", titleStyle); GUILayout.Label("Join server?", titleStyle);
GUILayout.Label("You opened a join link from the server list.", textStyle); GUILayout.Label("You opened a join link from the server list.", textStyle);
GUILayout.Space(18); GUILayout.Space(S(18));
GUILayout.Label("SERVER ADDRESS", captionStyle); GUILayout.Label("SERVER ADDRESS", captionStyle);
GUILayout.Label(mp.PendingJoinHost + ":" + mp.PendingJoinPort, rowNameStyle); GUILayout.Label(mp.PendingJoinHost + ":" + mp.PendingJoinPort, rowNameStyle);
GUILayout.Space(24); GUILayout.Space(S(24));
if (!mp.SteamAvailable) if (!mp.SteamAvailable)
GUILayout.Label("Steam is required to join servers - start the game through Steam.", textStyle); GUILayout.Label("Steam is required to join servers - start the game through Steam.", textStyle);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
GUI.enabled = mp.SteamAvailable; GUI.enabled = mp.SteamAvailable;
if (GUILayout.Button("JOIN", primaryStyle, GUILayout.Height(42), GUILayout.Width(170))) if (GUILayout.Button("JOIN", primaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(170))))
mp.ConfirmPendingJoin(); mp.ConfirmPendingJoin();
GUI.enabled = true; GUI.enabled = true;
GUILayout.Space(10); GUILayout.Space(S(10));
if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(42), GUILayout.Width(170))) if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(170))))
mp.CancelPendingJoin(); mp.CancelPendingJoin();
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
} }
@@ -727,43 +731,43 @@ namespace MSCO
{ {
if (texDim == null) texDim = Solid(new Color(0f, 0f, 0f, 0.6f)); if (texDim == null) texDim = Solid(new Color(0f, 0f, 0f, 0.6f));
if (texDanger == null) texDanger = Solid(Hex(0xe5, 0x48, 0x4d)); if (texDanger == null) texDanger = Solid(Hex(0xe5, 0x48, 0x4d));
if (promptTextStyle == null) if (promptTextStyle == null || promptTextStyle.fontSize != P(15))
{ {
promptTextStyle = new GUIStyle(rowNameStyle); promptTextStyle = new GUIStyle(rowNameStyle);
promptTextStyle.wordWrap = true; promptTextStyle.wordWrap = true;
promptTextStyle.fontSize = 15; promptTextStyle.fontSize = P(15);
} }
GUI.DrawTexture(new Rect(0, 0, vw, vh), texDim); GUI.DrawTexture(new Rect(0, 0, vw, vh), texDim);
var r = new Rect((vw - PromptWidth) / 2f, (vh - PromptHeight) / 2f, PromptWidth, PromptHeight); var r = new Rect(Mathf.Round((vw - S(PromptWidth)) / 2f), Mathf.Round((vh - S(PromptHeight)) / 2f), S(PromptWidth), S(PromptHeight));
UiTheme.DrawShadow(r); UiTheme.DrawShadow(r);
GUI.Box(r, GUIContent.none, windowBox); GUI.Box(r, GUIContent.none, windowBox);
string title = reconnecting string title = reconnecting
? ((mp.ReconnectMessage ?? "").StartsWith("Switching") ? "SWITCHING SERVER" : "RECONNECTING") ? ((mp.ReconnectMessage ?? "").StartsWith("Switching") ? "SWITCHING SERVER" : "RECONNECTING")
: mp.DisconnectTitle; : mp.DisconnectTitle;
bool bad = !reconnecting && title != "DISCONNECTED"; bool bad = !reconnecting && title != "DISCONNECTED";
GUI.DrawTexture(new Rect(r.x, r.y, r.width, 4), bad ? texDanger : texAccent); GUI.DrawTexture(new Rect(r.x, r.y, r.width, S(4)), bad ? texDanger : texAccent);
float x = r.x + 32, w = r.width - 64, y = r.y + 28; float x = r.x + S(32), w = r.width - S(64), y = r.y + S(28);
GUI.Label(new Rect(x, y, w, 34), title, titleStyle); GUI.Label(new Rect(x, y, w, S(34)), title, titleStyle);
y += 50; y += S(50);
GUI.Label(new Rect(x, y, w, 20), "REASON", captionStyle); GUI.Label(new Rect(x, y, w, S(20)), "REASON", captionStyle);
if (reconnecting) if (reconnecting)
{ {
GUI.Label(new Rect(x, y + 22, w, 44), mp.ReconnectMessage ?? "", promptTextStyle); GUI.Label(new Rect(x, y + S(22), w, S(44)), mp.ReconnectMessage ?? "", promptTextStyle);
string attempt = mp.ReconnectAttempt == 0 string attempt = mp.ReconnectAttempt == 0
? "Connecting again in a moment..." ? "Connecting again in a moment..."
: "Attempt " + mp.ReconnectAttempt + " of " + mp.ReconnectMaxAttempts + : "Attempt " + mp.ReconnectAttempt + " of " + mp.ReconnectMaxAttempts +
(string.IsNullOrEmpty(mp.Status) ? "" : " - " + mp.Status); (string.IsNullOrEmpty(mp.Status) ? "" : " - " + mp.Status);
GUI.Label(new Rect(x, y + 74, w, 40), attempt, textStyle); GUI.Label(new Rect(x, y + S(74), w, S(40)), attempt, textStyle);
if (GUI.Button(new Rect(r.xMax - 32 - 170, r.yMax - 28 - 42, 170, 42), "CANCEL", secondaryStyle)) mp.CancelReconnect(); 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; return;
} }
GUI.Label(new Rect(x, y + 22, w, 60), mp.DisconnectNotice ?? "", promptTextStyle); GUI.Label(new Rect(x, y + S(22), w, S(60)), mp.DisconnectNotice ?? "", promptTextStyle);
GUI.Label(new Rect(x, y + 88, w, 20), "Your own save game has been restored.", textStyle); 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 - 32 - 170, r.yMax - 28 - 42, 170, 42), "OK", primaryStyle)) mp.ClearDisconnectNotice(); 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; bool newGameConfirm;
@@ -775,38 +779,38 @@ namespace MSCO
if (newGameConfirm) if (newGameConfirm)
{ {
GUILayout.BeginVertical(rowBox); GUILayout.BeginVertical(rowBox);
GUILayout.Space(6); GUILayout.Space(S(6));
GUILayout.Label("START A NEW GAME?", captionStyle); GUILayout.Label("START A NEW GAME?", captionStyle);
GUILayout.Label(saved != null GUILayout.Label(saved != null
? "Your current save game is kept as a backup in MSCO_Saves - everyone starts in a fresh world." ? "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); : "Everyone starts in a fresh world.", textStyle);
GUILayout.Space(6); GUILayout.Space(S(6));
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
if (GUILayout.Button("YES, NEW GAME", dangerStyle, GUILayout.Height(38), GUILayout.Width(170))) if (GUILayout.Button("YES, NEW GAME", dangerStyle, GUILayout.Height(S(38)), GUILayout.Width(S(170))))
{ {
newGameConfirm = false; newGameConfirm = false;
mp.StartHostNewGame(); mp.StartHostNewGame();
} }
GUILayout.Space(8); GUILayout.Space(S(8));
if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(38), GUILayout.Width(110))) newGameConfirm = false; if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(S(38)), GUILayout.Width(S(110)))) newGameConfirm = false;
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.Space(6); GUILayout.Space(S(6));
GUILayout.EndVertical(); GUILayout.EndVertical();
return; return;
} }
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
GUI.enabled = saved != null; GUI.enabled = saved != null;
GUILayout.BeginVertical(GUILayout.Width(190)); GUILayout.BeginVertical(GUILayout.Width(S(190)));
if (GUILayout.Button("CONTINUE", primaryStyle, GUILayout.Height(42), GUILayout.Width(190))) if (GUILayout.Button("CONTINUE", primaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(190))))
mp.StartHostGame(); mp.StartHostGame();
GUILayout.Label(saved != null ? "saved " + saved.Value.ToString("dd.MM.yyyy HH:mm") : "no save game yet", captionStyle); GUILayout.Label(saved != null ? "saved " + saved.Value.ToString("dd.MM.yyyy HH:mm") : "no save game yet", captionStyle);
GUILayout.EndVertical(); GUILayout.EndVertical();
GUI.enabled = true; GUI.enabled = true;
GUILayout.Space(10); GUILayout.Space(S(10));
GUILayout.BeginVertical(GUILayout.Width(190)); GUILayout.BeginVertical(GUILayout.Width(S(190)));
if (GUILayout.Button("NEW GAME", secondaryStyle, GUILayout.Height(42), GUILayout.Width(190))) if (GUILayout.Button("NEW GAME", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(190))))
{ {
if (saved != null) newGameConfirm = true; if (saved != null) newGameConfirm = true;
else mp.StartHostNewGame(); else mp.StartHostNewGame();
@@ -825,39 +829,39 @@ namespace MSCO
: mp.IsDedicated || mp.IsConnectingToServer : mp.IsDedicated || mp.IsConnectingToServer
? "Connected to the server - the world is loading" + (string.IsNullOrEmpty(mp.Status) ? "." : ": " + mp.Status) ? "Connected to the server - the world is loading" + (string.IsNullOrEmpty(mp.Status) ? "." : ": " + mp.Status)
: "Waiting for the host to start the game.", textStyle); : "Waiting for the host to start the game.", textStyle);
GUILayout.Space(16); GUILayout.Space(S(16));
GUILayout.Label("PLAYERS " + mp.Players.Count + "/" + mp.SessionMaxPlayers, captionStyle); GUILayout.Label("PLAYERS " + mp.Players.Count + "/" + mp.SessionMaxPlayers, captionStyle);
playerScroll = GUILayout.BeginScrollView(playerScroll, GUILayout.Height(ListHeight(mp.IsHost ? 330f : 170f, 120f))); playerScroll = GUILayout.BeginScrollView(playerScroll, GUILayout.Height(ListHeight(mp.IsHost ? S(330f) : S(170f), S(120f))));
foreach (PlayerInfo p in mp.Players) foreach (PlayerInfo p in mp.Players)
{ {
Rect row = GUILayoutUtility.GetRect(10, 32, GUILayout.ExpandWidth(true)); Rect row = GUILayoutUtility.GetRect(S(10), S(32), GUILayout.ExpandWidth(true));
GUI.Box(new Rect(row.x, row.y, row.width, row.height - 4), GUIContent.none, rowBox); 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 // Eigener Punkt gelb, solange man noch lädt/synchronisiert; grün, wenn man wirklich im Spiel ist
GUI.DrawTexture(new Rect(row.x + 12, row.y + 11, 8, 8), p.IsLocal && !mp.IsFullyConnected ? texPending : texOnline); 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 + 30, row.y, row.width - 150, row.height - 2), p.Name, rowNameStyle); 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" : ""; string tag = p.Id == NetSession.HostId ? "HOST" : "";
if (p.IsLocal) tag = tag.Length > 0 ? tag + " · YOU" : "YOU"; if (p.IsLocal) tag = tag.Length > 0 ? tag + " · YOU" : "YOU";
GUI.Label(new Rect(row.xMax - 120, row.y, 110, row.height - 2), tag, rowTagStyle); GUI.Label(new Rect(row.xMax - S(120), row.y, S(110), row.height - S(2)), tag, rowTagStyle);
} }
GUILayout.EndScrollView(); GUILayout.EndScrollView();
GUILayout.Space(14); GUILayout.Space(S(14));
if (mp.IsHost) if (mp.IsHost)
{ {
DrawHostStart(mp); DrawHostStart(mp);
GUILayout.Space(10); GUILayout.Space(S(10));
} }
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
if (mp.IsHost) if (mp.IsHost)
{ {
if (mp.IsSteamSession && mp.Players.Count < MultiplayerManager.MaxPlayers && if (mp.IsSteamSession && mp.Players.Count < MultiplayerManager.MaxPlayers &&
GUILayout.Button("INVITE FRIENDS", secondaryStyle, GUILayout.Height(42), GUILayout.Width(150))) GUILayout.Button("INVITE FRIENDS", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(150))))
mp.InviteFriends(); mp.InviteFriends();
GUILayout.Space(10); GUILayout.Space(S(10));
} }
if (GUILayout.Button("DISCONNECT", secondaryStyle, GUILayout.Height(42), GUILayout.Width(150))) if (GUILayout.Button("DISCONNECT", secondaryStyle, GUILayout.Height(S(42)), GUILayout.Width(S(150))))
mp.Disconnect(); mp.Disconnect();
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
} }
@@ -866,8 +870,8 @@ namespace MSCO
{ {
GUI.Box(r, GUIContent.none, sidebarBox); GUI.Box(r, GUIContent.none, sidebarBox);
Texture2D dot = mp.IsFullyConnected ? texOnline : mp.IsInSession || mp.IsReconnecting ? texPending : texMuted; Texture2D dot = mp.IsFullyConnected ? texOnline : mp.IsInSession || mp.IsReconnecting ? texPending : texMuted;
GUI.DrawTexture(new Rect(r.x + 18, r.y + (r.height - 8) / 2f, 8, 8), dot); 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 + 36, r.y, r.width - 72, r.height), mp.Status, statusStyle); GUI.Label(new Rect(r.x + S(36), r.y, r.width - S(72), r.height), mp.Status, statusStyle);
} }
@@ -875,61 +879,62 @@ namespace MSCO
void EnsureStyles() void EnsureStyles()
{ {
if (stylesReady && texBackground != null) return; if (stylesReady && texBackground != null && Mathf.Abs(stylesScale - uiScale) < 0.01f) return;
stylesScale = uiScale;
// PlayerPrefs erst hier lesen (nicht im Konstruktor erlaubt) // PlayerPrefs erst hier lesen (nicht im Konstruktor erlaubt)
directInput = PlayerPrefs.GetString(PrefDirect, ""); if (directInput == null) directInput = PlayerPrefs.GetString(PrefDirect, "");
texBackground = Solid(Background); texBackground = Solid(Background);
texSidebar = Solid(Sidebar); texSidebar = Solid(Sidebar);
texAccent = Solid(Accent); texAccent = Solid(Accent);
texOnline = UiTheme.Rounded(Online, 4); // runde Status-Punkte texOnline = UiTheme.Rounded(Online, P(4)); // runde Status-Punkte
texPending = UiTheme.Rounded(Pending, 4); texPending = UiTheme.Rounded(Pending, P(4));
texMuted = UiTheme.Rounded(Muted, 4); texMuted = UiTheme.Rounded(Muted, P(4));
texRow = Solid(Field); texRow = Solid(Field);
// Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt) // Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt)
windowBox = UiTheme.Card(Background, 10); windowBox = UiTheme.Card(Background, P(10));
sidebarBox = UiTheme.Card(Sidebar, 10); sidebarBox = UiTheme.Card(Sidebar, P(10));
rowBox = UiTheme.Panel9(Field, UiTheme.Radius); rowBox = UiTheme.Panel9(Field, P(UiTheme.Radius));
rowHoverBox = UiTheme.Card(FieldFocus, UiTheme.Radius); rowHoverBox = UiTheme.Card(FieldFocus, P(UiTheme.Radius));
texDivider = Solid(new Color(1f, 1f, 1f, 0.07f)); texDivider = Solid(new Color(1f, 1f, 1f, 0.07f));
texGold = Solid(Gold); texGold = Solid(Gold);
texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f)); texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f));
texBarFill = Solid(Online); texBarFill = Solid(Online);
texBarFull = Solid(UiTheme.Danger); texBarFull = Solid(UiTheme.Danger);
accentBox = UiTheme.Panel9(Accent, 1); accentBox = UiTheme.Panel9(Accent, P(1));
pillStyle = UiTheme.Panel9(new Color(Accent.r, Accent.g, Accent.b, 0.18f), 4); pillStyle = UiTheme.Panel9(new Color(Accent.r, Accent.g, Accent.b, 0.18f), P(4));
pillStyle.normal.textColor = Accent; pillStyle.normal.textColor = Accent;
pillStyle.fontSize = 10; pillStyle.fontSize = P(10);
pillStyle.fontStyle = FontStyle.Bold; pillStyle.fontStyle = FontStyle.Bold;
pillStyle.alignment = TextAnchor.MiddleCenter; pillStyle.alignment = TextAnchor.MiddleCenter;
pillStyle.padding = new RectOffset(7, 7, 0, 0); pillStyle.padding = new RectOffset(P(7), P(7), P(0), P(0));
pillStyle.wordWrap = false; pillStyle.wordWrap = false;
pillMutedStyle = new GUIStyle(pillStyle); pillMutedStyle = new GUIStyle(pillStyle);
pillMutedStyle.normal.background = UiTheme.Rounded(new Color(1f, 1f, 1f, 0.08f), 4); pillMutedStyle.normal.background = UiTheme.Rounded(new Color(1f, 1f, 1f, 0.08f), P(4));
pillMutedStyle.normal.textColor = Muted; pillMutedStyle.normal.textColor = Muted;
pillFeaturedStyle = new GUIStyle(pillStyle); pillFeaturedStyle = new GUIStyle(pillStyle);
pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, 4); pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4));
pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06); pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06);
featuredRowBox = UiTheme.Panel9(Color.Lerp(Field, Gold, 0.16f), UiTheme.Radius); 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, 11, FontStyle.Bold); 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, 11, FontStyle.Bold); chipOffStyle = Button(Field, FieldFocus, Muted, P(11), FontStyle.Bold);
chipOnStyle.padding = chipOffStyle.padding = new RectOffset(12, 12, 0, 0); chipOnStyle.padding = chipOffStyle.padding = new RectOffset(P(12), P(12), P(0), P(0));
logoStyle = Label(28, Color.white, FontStyle.Bold); logoStyle = Label(P(28), Color.white, FontStyle.Bold);
taglineStyle = Label(10, Muted, FontStyle.Bold); taglineStyle = Label(P(10), Muted, FontStyle.Bold);
titleStyle = Label(24, Color.white, FontStyle.Bold); titleStyle = Label(P(24), Color.white, FontStyle.Bold);
textStyle = Label(13, Muted, FontStyle.Normal); textStyle = Label(P(13), Muted, FontStyle.Normal);
textStyle.wordWrap = true; textStyle.wordWrap = true;
captionStyle = Label(11, Muted, FontStyle.Bold); captionStyle = Label(P(11), Muted, FontStyle.Bold);
captionStyle.margin.bottom = 6; captionStyle.margin.bottom = P(6);
statusStyle = Label(12, Color.white, FontStyle.Normal); statusStyle = Label(P(12), Color.white, FontStyle.Normal);
statusStyle.alignment = TextAnchor.MiddleLeft; statusStyle.alignment = TextAnchor.MiddleLeft;
headerCellStyle = Label(11, Muted, FontStyle.Bold); headerCellStyle = Label(P(11), Muted, FontStyle.Bold);
headerCellStyle.alignment = TextAnchor.MiddleLeft; headerCellStyle.alignment = TextAnchor.MiddleLeft;
rowNameStyle = Label(14, Color.white, FontStyle.Normal); rowNameStyle = Label(P(14), Color.white, FontStyle.Normal);
rowNameStyle.alignment = TextAnchor.MiddleLeft; rowNameStyle.alignment = TextAnchor.MiddleLeft;
pingGoodStyle = new GUIStyle(rowNameStyle); pingGoodStyle = new GUIStyle(rowNameStyle);
pingGoodStyle.normal.textColor = Online; pingGoodStyle.normal.textColor = Online;
@@ -937,34 +942,34 @@ namespace MSCO
pingMidStyle.normal.textColor = Pending; pingMidStyle.normal.textColor = Pending;
pingBadStyle = new GUIStyle(rowNameStyle); pingBadStyle = new GUIStyle(rowNameStyle);
pingBadStyle.normal.textColor = UiTheme.DangerHover; pingBadStyle.normal.textColor = UiTheme.DangerHover;
rowTagStyle = Label(11, Accent, FontStyle.Bold); rowTagStyle = Label(P(11), Accent, FontStyle.Bold);
rowTagStyle.alignment = TextAnchor.MiddleRight; rowTagStyle.alignment = TextAnchor.MiddleRight;
badgeStyle = new GUIStyle(rowTagStyle); badgeStyle = new GUIStyle(rowTagStyle);
badgeStyle.alignment = TextAnchor.MiddleLeft; badgeStyle.alignment = TextAnchor.MiddleLeft;
badgeStyle.padding = new RectOffset(6, 6, 2, 2); badgeStyle.padding = new RectOffset(P(6), P(6), P(2), P(2));
placeholderStyle = Label(13, Muted, FontStyle.Italic); placeholderStyle = Label(P(13), Muted, FontStyle.Italic);
placeholderStyle.alignment = TextAnchor.MiddleLeft; placeholderStyle.alignment = TextAnchor.MiddleLeft;
toggleStyle = new GUIStyle(GUI.skin.toggle); toggleStyle = new GUIStyle(GUI.skin.toggle);
toggleStyle.normal.textColor = toggleStyle.onNormal.textColor = toggleStyle.hover.textColor = toggleStyle.onHover.textColor = Color.white; toggleStyle.normal.textColor = toggleStyle.onNormal.textColor = toggleStyle.hover.textColor = toggleStyle.onHover.textColor = Color.white;
toggleStyle.fontSize = 11; toggleStyle.fontSize = P(11);
toggleStyle.fontStyle = FontStyle.Bold; toggleStyle.fontStyle = FontStyle.Bold;
navStyle = Button(Sidebar, Secondary, Muted, 14, FontStyle.Normal); navStyle = Button(Sidebar, Secondary, Muted, P(14), FontStyle.Normal);
navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau) navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau)
navStyle.alignment = TextAnchor.MiddleLeft; navStyle.alignment = TextAnchor.MiddleLeft;
navStyle.padding = new RectOffset(26, 10, 0, 0); navStyle.padding = new RectOffset(P(26), P(10), P(0), P(0));
navActiveStyle = Button(Secondary, Secondary, Color.white, 14, FontStyle.Bold); navActiveStyle = Button(Secondary, Secondary, Color.white, P(14), FontStyle.Bold);
navActiveStyle.alignment = TextAnchor.MiddleLeft; navActiveStyle.alignment = TextAnchor.MiddleLeft;
navActiveStyle.padding = new RectOffset(26, 10, 0, 0); navActiveStyle.padding = new RectOffset(P(26), P(10), P(0), P(0));
primaryStyle = Button(Accent, AccentHover, Color.white, 14, FontStyle.Bold); primaryStyle = Button(Accent, AccentHover, Color.white, P(14), FontStyle.Bold);
UiTheme.ApplyPrimaryGradient(primaryStyle); UiTheme.ApplyPrimaryGradient(primaryStyle);
dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 13, FontStyle.Bold); dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, P(13), FontStyle.Bold);
secondaryStyle = Button(Secondary, SecondaryHover, Color.white, 14, FontStyle.Bold); secondaryStyle = Button(Secondary, SecondaryHover, Color.white, P(14), FontStyle.Bold);
tabStyle = Button(Sidebar, Secondary, Muted, 11, FontStyle.Bold); tabStyle = Button(Sidebar, Secondary, Muted, P(11), FontStyle.Bold);
tabStyle.hover.textColor = Color.white; tabStyle.hover.textColor = Color.white;
fieldStyle = UiTheme.Field(14); fieldStyle = UiTheme.Field(P(14));
stylesReady = true; stylesReady = true;
} }
+83 -4
View File
@@ -279,8 +279,84 @@ namespace MSCO
/// <summary>Ping-Anzeige oben rechts (Konsole: "ping"), bleibt gespeichert.</summary> /// <summary>Ping-Anzeige oben rechts (Konsole: "ping"), bleibt gespeichert.</summary>
public bool ShowPing public bool ShowPing
{ {
get { return PlayerPrefs.GetInt(PrefShowPing, 0) == 1; } get
set { PlayerPrefs.SetInt(PrefShowPing, value ? 1 : 0); } {
if (showPing < 0) showPing = PlayerPrefs.GetInt(PrefShowPing, 0); // einmal lesen, nicht bei jedem Zeichnen
return showPing == 1;
}
set
{
showPing = value ? 1 : 0;
PlayerPrefs.SetInt(PrefShowPing, showPing);
}
}
int showPing = -1, showFps = -1;
// ---------------------------------------------------------------- FPS-Anzeige (Konsole: "fps")
const string PrefShowFps = "MSCO_ShowFps";
float fpsTimer, fpsValue = -1f, frameMsWorst;
int fpsFrames;
float frameMsWorstWindow;
/// <summary>FPS-Anzeige oben rechts, bleibt gespeichert.</summary>
public bool ShowFps
{
get
{
if (showFps < 0) showFps = PlayerPrefs.GetInt(PrefShowFps, 0);
return showFps == 1;
}
set
{
showFps = value ? 1 : 0;
PlayerPrefs.SetInt(PrefShowFps, showFps);
}
}
/// <summary>Bilder pro Sekunde (Mittel über eine halbe Sekunde) und längstes Bild darin (Ruckler).</summary>
public string FpsText
{
get
{
if (fpsValue < 0f) return "measuring...";
return Mathf.RoundToInt(fpsValue) + " FPS (" + (1000f / Mathf.Max(1f, fpsValue)).ToString("0.0") + " ms, worst " +
frameMsWorst.ToString("0") + " ms)";
}
}
void MeasureFps()
{
float dt = Time.unscaledDeltaTime;
fpsFrames++;
fpsTimer += dt;
frameMsWorstWindow = Mathf.Max(frameMsWorstWindow, dt * 1000f);
if (fpsTimer < 0.5f) return;
fpsValue = fpsFrames / fpsTimer;
frameMsWorst = frameMsWorstWindow;
fpsFrames = 0;
fpsTimer = 0f;
frameMsWorstWindow = 0f;
}
GUIStyle fpsStyle;
/// <summary>Oben rechts, links neben dem Ping.</summary>
void DrawFps(float rightEdge)
{
if (!ShowFps) return;
if (fpsStyle == null)
{
fpsStyle = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.85f), 6);
fpsStyle.fontSize = 12;
fpsStyle.fontStyle = FontStyle.Bold;
fpsStyle.alignment = TextAnchor.MiddleCenter;
fpsStyle.padding = new RectOffset(10, 10, 0, 0);
}
// grün ab 50, gelb ab 30, sonst rot
fpsStyle.normal.textColor = fpsValue < 0f ? UiTheme.Muted : fpsValue >= 50f ? UiTheme.Online : fpsValue >= 30f ? UiTheme.Pending : UiTheme.Danger;
var content = new GUIContent(fpsValue < 0f ? "FPS ..." : "FPS " + Mathf.RoundToInt(fpsValue));
float width = fpsStyle.CalcSize(content).x;
GUI.Box(new Rect(rightEdge - width, 8, width, 24), content, fpsStyle);
} }
/// <summary>Text für Anzeige und Konsole.</summary> /// <summary>Text für Anzeige und Konsole.</summary>
@@ -311,7 +387,9 @@ namespace MSCO
var content = new GUIContent("PING " + PingText); var content = new GUIContent("PING " + PingText);
float width = pingStyle.CalcSize(content).x; float width = pingStyle.CalcSize(content).x;
GUI.Box(new Rect(Screen.width - width - 12, 8, width, 24), content, pingStyle); GUI.Box(new Rect(Screen.width - width - 12, 8, width, 24), content, pingStyle);
pingWidth = width + 8;
} }
float pingWidth; // Platz, den die Ping-Anzeige belegt (FPS steht links daneben)
// ---------------------------------------------------------------- Cheats (Admin-Menü) // ---------------------------------------------------------------- Cheats (Admin-Menü)
@@ -871,6 +949,7 @@ namespace MSCO
void Update() void Update()
{ {
MeasureFps();
// Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog) // Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog)
// Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht; // Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht;
// erst nach MINIMIZE wieder. Über der eingeklappten Leiste ebenfalls nicht (sonst öffnet sich z.B. der Changelog). // erst nach MINIMIZE wieder. Über der eingeklappten Leiste ebenfalls nicht (sonst öffnet sich z.B. der Changelog).
@@ -2179,7 +2258,7 @@ namespace MSCO
if (MenuHelper.InMenu) if (MenuHelper.InMenu)
{ {
lobby.Draw(this); lobby.Draw(this);
DrawPing(); toasts.Draw(); pingWidth = 0f; DrawPing(); DrawFps(Screen.width - 12 - pingWidth); toasts.Draw();
overlays.Draw(this, false); overlays.Draw(this, false);
return; return;
} }
@@ -2196,7 +2275,7 @@ namespace MSCO
} }
if (session != null) DrawHud(); if (session != null) DrawHud();
DrawPing(); toasts.Draw(); pingWidth = 0f; DrawPing(); DrawFps(Screen.width - 12 - pingWidth); toasts.Draw();
overlays.Draw(this, session != null); overlays.Draw(this, session != null);
} }
+2 -2
View File
@@ -900,9 +900,9 @@ namespace MSCO
var candidates = new List<KeyValuePair<Rigidbody, bool>>(); var candidates = new List<KeyValuePair<Rigidbody, bool>>();
var rootsByName = new Dictionary<string, List<Transform>>(); var rootsByName = new Dictionary<string, List<Transform>>();
foreach (UnityEngine.Object o in UnityEngine.Object.FindObjectsOfType(typeof(Rigidbody))) foreach (Rigidbody rb in SceneCache.Rigidbodies(1f))
{ {
var rb = (Rigidbody)o; if (rb == null) continue;
Transform root = rb.transform.root; Transform root = rb.transform.root;
List<Transform> roots; List<Transform> roots;
if (!rootsByName.TryGetValue(root.name, out roots)) rootsByName[root.name] = roots = new List<Transform>(); if (!rootsByName.TryGetValue(root.name, out roots)) rootsByName[root.name] = roots = new List<Transform>();
+21
View File
@@ -14,6 +14,27 @@ namespace MSCO
static float builtAt = -1000f; static float builtAt = -1000f;
static int builtForLevel = -1; static int builtForLevel = -1;
static Rigidbody[] bodies = new Rigidbody[0];
static float bodiesAt = -1000f;
static int bodiesForLevel = -1;
/// <summary>
/// Alle Rigidbodies; höchstens maxAge Sekunden alt (Objekt-Sync und Verkehr teilen sich die teure Suche).
/// Einträge können inzwischen zerstört sein (== null prüfen).
/// </summary>
public static Rigidbody[] Rigidbodies(float maxAge)
{
if (Time.realtimeSinceStartup - bodiesAt > maxAge || bodiesForLevel != Application.loadedLevel)
{
Object[] found = Object.FindObjectsOfType(typeof(Rigidbody));
bodies = new Rigidbody[found.Length];
for (int i = 0; i < found.Length; i++) bodies[i] = (Rigidbody)found[i];
bodiesAt = Time.realtimeSinceStartup;
bodiesForLevel = Application.loadedLevel;
}
return bodies;
}
/// <summary>Alle FSMs; höchstens maxAge Sekunden alt. Einträge können inzwischen zerstört sein (== null prüfen).</summary> /// <summary>Alle FSMs; höchstens maxAge Sekunden alt. Einträge können inzwischen zerstört sein (== null prüfen).</summary>
public static PlayMakerFSM[] Fsms(float maxAge) public static PlayMakerFSM[] Fsms(float maxAge)
{ {
+3 -2
View File
@@ -42,11 +42,12 @@ namespace MSCO
foreach (Car c in cars.Values) StopFollowing(c); foreach (Car c in cars.Values) StopFollowing(c);
cars.Clear(); cars.Clear();
seen.Clear(); seen.Clear();
nextScan = 0f; nextScan = -1f; // erster Suchlauf verteilt, siehe Update
} }
public void Update(bool authority, float dt, Action<MsgType, byte[]> send) public void Update(bool authority, float dt, Action<MsgType, byte[]> send)
{ {
if (nextScan < 0f) nextScan = Time.time + 4.5f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan) if (Time.time >= nextScan)
{ {
nextScan = Time.time + ScanInterval; nextScan = Time.time + ScanInterval;
@@ -136,7 +137,7 @@ namespace MSCO
void Scan() void Scan()
{ {
int before = cars.Count; int before = cars.Count;
foreach (Rigidbody rb in UnityEngine.Object.FindObjectsOfType<Rigidbody>()) foreach (Rigidbody rb in SceneCache.Rigidbodies(5f)) // teilt sich die Suche mit dem Objekt-Sync
{ {
if (rb == null || !seen.Add(rb)) continue; if (rb == null || !seen.Add(rb)) continue;
if (Array.IndexOf(Roots, rb.transform.root.name) < 0) continue; if (Array.IndexOf(Roots, rb.transform.root.name) < 0) continue;
+1 -1
View File
@@ -23,7 +23,7 @@ namespace MSCO
string n = name.ToLower(); string n = name.ToLower();
return n.Contains("needle") || n.Contains("steeringwheel"); return n.Contains("needle") || n.Contains("steeringwheel");
} }
const float CacheRefresh = 5f; // Teile können an-/abgebaut werden const float CacheRefresh = 15f; // Teile können an-/abgebaut werden (Nachschlagen ist teuer: ganzes Fahrzeug)
const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen
struct AudioState struct AudioState
+2 -1
View File
@@ -58,11 +58,12 @@ namespace MSCO
vars.Clear(); vars.Clear();
pending.Clear(); pending.Clear();
scanned.Clear(); scanned.Clear();
nextScan = 0f; nextScan = -1f; // erster Suchlauf verteilt, siehe Update
} }
public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send) public void Update(ObjectSync objects, int localId, Action<MsgType, byte[]> send)
{ {
if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan) if (Time.time >= nextScan)
{ {
nextScan = Time.time + ScanInterval; nextScan = Time.time + ScanInterval;
+2 -1
View File
@@ -49,11 +49,12 @@ namespace MSCO
{ {
vars.Clear(); vars.Clear();
scanned.Clear(); scanned.Clear();
nextScan = 0f; nextScan = -1f; // erster Suchlauf verteilt, siehe Update
} }
public void Update(Action<MsgType, byte[]> send) public void Update(Action<MsgType, byte[]> send)
{ {
if (nextScan < 0f) nextScan = Time.time + 3.8f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan) if (Time.time >= nextScan)
{ {
nextScan = Time.time + ScanInterval; nextScan = Time.time + ScanInterval;