MSCO 2.10.2: passenger only while moving, drain plug sync, admin VEHICLE tab, profile layout

- Passenger: auto ride-along only when the vehicle moves (speed from its motion, also for
  kinematic remote cars) - no more getting stuck while working in the engine bay or re-captured
- Bolts with a function (drain plug: stage 0-5 = oil leak on) are synced as stage and applied
  through the bolt's own stage logic; world authority sends its state once after joining.
  Fixes endless oil leaking when players had the drain plug in different states
- Admin menu VEHICLE tab: fluids with FILL/EMPTY, all Database parts with wear/damage,
  REPAIR/BREAK per part, REPAIR ALL, filter
- Server profile: info and players as two equal cards, no wrapping
- fsmdump also lists FSM variables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 12:02:21 +02:00
co-authored by Claude Opus 5.5
parent adc6eff9e8
commit 7b4a89d50f
11 changed files with 462 additions and 41 deletions
+71 -26
View File
@@ -599,8 +599,8 @@ namespace MSCO
GUILayout.BeginVertical();
GUILayout.Label(s.Name, titleStyle);
GUILayout.BeginHorizontal();
Rect badgeRow = GUILayoutUtility.GetRect(S(10), S(20), GUILayout.ExpandWidth(true));
float bx = badgeRow.x;
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);
@@ -651,39 +651,84 @@ namespace MSCO
GUILayout.Space(S(10));
}
// Infos links, Spielerliste rechts
// Zwei Karten gleicher Höhe: Infos links, Spielerliste rechts
bool versionOk = MSCO.Compatible(s.Version);
GUILayout.BeginHorizontal();
GUILayout.BeginVertical(GUILayout.Width(S(250)));
InfoLine("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle);
InfoLine("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle);
InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle);
InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle);
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle);
if (s.Mods.Count > 0) InfoLine("MODS", string.Join(", ", s.Mods.ToArray()), textStyle);
GUILayout.EndVertical();
GUILayout.Space(S(16));
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);
GUILayout.BeginVertical(rowBox, GUILayout.ExpandWidth(true), GUILayout.MinHeight(S(150)));
GUILayout.Space(S(6));
GUILayout.Label("PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle);
profileScroll = GUILayout.BeginScrollView(profileScroll, GUILayout.Height(ListHeight(S(330f), S(130f))));
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)
GUILayout.Label("Nobody online right now.", textStyle);
{
GUI.Label(listArea, "Nobody online right now.", profileEmptyStyle);
}
else
foreach (string name in s.PlayerNames)
{
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++)
{
Rect line = GUILayoutUtility.GetRect(S(10), S(24), GUILayout.ExpandWidth(true));
GUI.DrawTexture(new Rect(line.x + S(2), line.y + S(8), S(8), S(8)), texOnline);
GUI.Label(new Rect(line.x + S(18), line.y, line.width - S(18), line.height), name, rowNameStyle);
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);
}
GUILayout.EndScrollView();
GUILayout.EndVertical();
GUILayout.EndHorizontal();
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)
{