Passenger/driver sit still in moving cars: seat pose sent relative to the vehicle (backward-compatible), no backward pull; remote cars/traffic extrapolate in physics time and interpolate; UI polish (shadows, borders, gradient buttons, server list hover, fill bars, colored ping)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nico Meister
2026-09-28 20:52:56 +02:00
co-authored by Claude Opus 5.5
parent 33a415139b
commit ce2fbdfc10
11 changed files with 274 additions and 24 deletions
+46 -9
View File
@@ -13,7 +13,7 @@ namespace MSCO
enum Page { P2P, ServerBrowser, Favorites }
const float Width = 760f;
const float Height = 460f;
const float Height = 500f;
const float SidebarWidth = 190f;
const float StatusHeight = 36f;
@@ -51,6 +51,8 @@ namespace MSCO
bool stylesReady;
Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow;
Texture2D texDivider, texGold, texBarBack, texBarFill, texBarFull;
GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle;
List<ServerListEntry> shownAll, shownServers; // Stand des letzten Layout-Durchgangs
string layoutError;
GUIStyle dangerStyle;
@@ -87,6 +89,7 @@ namespace MSCO
var window = new Rect((Screen.width - Width) / 2f, (Screen.height - Height) / 2f, Width, Height);
ScreenRect = window;
UiTheme.DrawShadow(window);
GUI.Box(window, GUIContent.none, windowBox);
DrawSidebar(new Rect(window.x, window.y, SidebarWidth, Height - StatusHeight));
@@ -120,6 +123,7 @@ namespace MSCO
}
GUI.Label(new Rect(textX, r.y + 24, r.xMax - textX, 40), "MSCO", logoStyle);
GUI.Label(new Rect(r.x + 22, r.y + 70, r.width - 30, 20), "MY SUMMER CAR ONLINE", taglineStyle);
GUI.DrawTexture(new Rect(r.x + 20, r.y + 98, r.width - 40, 1), texDivider);
float y = r.y + 110;
if (NavButton(new Rect(r.x, y, r.width, 42), "P2P", page == Page.P2P)) page = Page.P2P;
@@ -234,7 +238,7 @@ namespace MSCO
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.76f, header.y, 50, header.height), "PING", headerCellStyle);
serverScroll = GUILayout.BeginScrollView(serverScroll, GUILayout.Height(140));
serverScroll = GUILayout.BeginScrollView(serverScroll, GUILayout.Height(180));
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);
@@ -242,7 +246,10 @@ namespace MSCO
{
Rect row = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
// Hervorgehobene Server (Keymaster-Admin) stehen oben - der Master liefert sie zuerst
GUI.Box(new Rect(row.x, row.y + 2, row.width, row.height - 4), GUIContent.none, s.Featured ? featuredRowBox : rowBox);
var rowRect = new Rect(row.x, row.y + 2, row.width, row.height - 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 + 8, 3, row.height - 16), texGold);
// 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);
@@ -266,9 +273,14 @@ namespace MSCO
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);
GUI.Label(new Rect(row.x + row.width * 0.5f, row.y, 70, row.height), s.Players + "/" + s.MaxPlayers, rowNameStyle);
// Spielerzahl mit Füllbalken (wie auf der Website)
float px = row.x + row.width * 0.5f;
GUI.Label(new Rect(px, row.y - 3, 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 + 24, 48, 3), texBarBack);
GUI.DrawTexture(new Rect(px, row.y + 24, Mathf.Max(2f, 48 * fill), 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, 50, row.height), s.Ping < 0 ? "..." : s.Ping + " ms", rowNameStyle);
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.xMax - 30, row.y, 20, row.height), "›", headerCellStyle); // Hinweis: Profil öffnen
}
GUILayout.EndScrollView();
@@ -354,7 +366,7 @@ namespace MSCO
GUILayout.Space(8);
List<Favorites.Favorite> favs = Favorites.All;
favoritesScroll = GUILayout.BeginScrollView(favoritesScroll, GUILayout.Height(220));
favoritesScroll = GUILayout.BeginScrollView(favoritesScroll, GUILayout.Height(250));
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;
@@ -562,7 +574,7 @@ namespace MSCO
InfoLine("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle);
InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle);
InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle);
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", rowNameStyle);
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle);
GUILayout.EndVertical();
GUILayout.Space(16);
@@ -617,6 +629,13 @@ namespace MSCO
GUILayout.Space(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;
@@ -698,6 +717,7 @@ namespace MSCO
var r = new Rect((Screen.width - PromptWidth) / 2f, (Screen.height - PromptHeight) / 2f, PromptWidth, PromptHeight);
ScreenRect = new Rect(0, 0, Screen.width, Screen.height); // Spielmenü dahinter komplett sperren
UiTheme.DrawShadow(r);
GUI.Box(r, GUIContent.none, windowBox);
string title = reconnecting
? ((mp.ReconnectMessage ?? "").StartsWith("Switching") ? "SWITCHING SERVER" : "RECONNECTING")
@@ -783,6 +803,7 @@ namespace MSCO
{
var r = new Rect((Screen.width - 220) / 2f, 0, 220, 30);
ScreenRect = r;
UiTheme.DrawShadow(r);
GUI.Box(r, GUIContent.none, sidebarBox);
GUI.Box(new Rect(r.x + 10, r.yMax - 3, r.width - 20, 3), GUIContent.none, accentBox);
Texture2D dot = mp.IsFullyConnected ? texOnline : mp.IsInSession || mp.IsReconnecting ? texPending : texMuted;
@@ -809,9 +830,16 @@ namespace MSCO
texPending = UiTheme.Rounded(Pending, 4);
texMuted = UiTheme.Rounded(Muted, 4);
texRow = Solid(Field);
windowBox = UiTheme.Panel9(Background, 10);
sidebarBox = UiTheme.Panel9(Sidebar, 10);
// Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt)
windowBox = UiTheme.Card(Background, 10);
sidebarBox = UiTheme.Card(Sidebar, 10);
rowBox = UiTheme.Panel9(Field, UiTheme.Radius);
rowHoverBox = UiTheme.Card(FieldFocus, UiTheme.Radius);
texDivider = Solid(new Color(1f, 1f, 1f, 0.07f));
texGold = Solid(Gold);
texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f));
texBarFill = Solid(Online);
texBarFull = Solid(UiTheme.Danger);
accentBox = UiTheme.Panel9(Accent, 1);
pillStyle = UiTheme.Panel9(new Color(Accent.r, Accent.g, Accent.b, 0.18f), 4);
@@ -846,6 +874,12 @@ namespace MSCO
headerCellStyle.alignment = TextAnchor.MiddleLeft;
rowNameStyle = Label(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(11, Accent, FontStyle.Bold);
rowTagStyle.alignment = TextAnchor.MiddleRight;
badgeStyle = new GUIStyle(rowTagStyle);
@@ -859,6 +893,7 @@ namespace MSCO
toggleStyle.fontStyle = FontStyle.Bold;
navStyle = Button(Sidebar, Secondary, Muted, 14, FontStyle.Normal);
navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau)
navStyle.alignment = TextAnchor.MiddleLeft;
navStyle.padding = new RectOffset(26, 10, 0, 0);
navActiveStyle = Button(Secondary, Secondary, Color.white, 14, FontStyle.Bold);
@@ -866,9 +901,11 @@ namespace MSCO
navActiveStyle.padding = new RectOffset(26, 10, 0, 0);
primaryStyle = Button(Accent, AccentHover, Color.white, 14, FontStyle.Bold);
UiTheme.ApplyPrimaryGradient(primaryStyle);
dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 13, FontStyle.Bold);
secondaryStyle = Button(Secondary, SecondaryHover, Color.white, 14, FontStyle.Bold);
tabStyle = Button(Sidebar, Secondary, Muted, 11, FontStyle.Bold);
tabStyle.hover.textColor = Color.white;
fieldStyle = UiTheme.Field(14);