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
+13
View File
@@ -1,5 +1,18 @@
# Changelog # Changelog
## 2.10.2
- **Beifahrer**: automatisch festgehalten wird man nur noch, wenn das Fahrzeug fährt (egal wer fährt) – am stehenden
Auto kann man sich frei bewegen und schrauben (vorher hing man fest, sobald man sich in den Motorraum beugte, und
wurde nach dem Aussteigen gleich wieder eingefangen)
- **Öl lief endlos aus**: die Ablassschraube war bei Mitspielern unterschiedlich weit eingedreht (offen = Leck). Schrauben
mit Funktion (Ablassschraube) werden jetzt als Stufe übertragen und über die Logik des Spiels angewendet; beim Beitreten
gibt die Welt-Autorität ihren Stand vor
- **Admin-Menü, Reiter VEHICLE**: Füllstände (Benzin, Öl, Brems-/Kupplungsflüssigkeit, Kühlwasser) mit FILL/EMPTY, alle
Teile mit Verschleiß und Zustand – einzeln REPAIR/BREAK, REPAIR ALL, Filter (zum Testen; im Chat angezeigt)
- **Server-Profil** im Server-Browser aufgeräumt: Infos und Spieler als zwei gleich hohe Karten, nichts bricht mehr um
- Diagnose `fsmdump` zeigt jetzt auch die Variablen der FSMs
## 2.10.1 ## 2.10.1
**Türen** **Türen**
+11
View File
@@ -55,6 +55,17 @@ namespace MSCO
{ {
sb.Append("==== ").Append(path).Append(" | ").Append(fsm.FsmName).Append(" | active ").Append(fsm.gameObject.activeInHierarchy) sb.Append("==== ").Append(path).Append(" | ").Append(fsm.FsmName).Append(" | active ").Append(fsm.gameObject.activeInHierarchy)
.Append(" | state ").Append(fsm.ActiveStateName).AppendLine(); .Append(" | state ").Append(fsm.ActiveStateName).AppendLine();
try
{
FsmVariables fv = fsm.FsmVariables;
var vb = new StringBuilder();
foreach (FsmFloat f in fv.FloatVariables) vb.Append(" ").Append(f.Name).Append("=").Append(f.Value.ToString("0.###"));
foreach (FsmInt n in fv.IntVariables) vb.Append(" ").Append(n.Name).Append("=").Append(n.Value);
foreach (FsmBool b in fv.BoolVariables) vb.Append(" ").Append(b.Name).Append("=").Append(b.Value);
foreach (FsmString t in fv.StringVariables) vb.Append(" ").Append(t.Name).Append("=\"").Append(t.Value).Append("\"");
if (vb.Length > 0) sb.Append(" VARS").Append(vb).AppendLine();
}
catch { }
if (fsm.FsmGlobalTransitions != null) if (fsm.FsmGlobalTransitions != null)
foreach (FsmTransition t in fsm.FsmGlobalTransitions) foreach (FsmTransition t in fsm.FsmGlobalTransitions)
sb.Append(" GLOBAL ").Append(t.EventName).Append(" -> ").Append(t.ToState).AppendLine(); sb.Append(" GLOBAL ").Append(t.EventName).Append(" -> ").Append(t.ToState).AppendLine();
+71 -26
View File
@@ -599,8 +599,8 @@ namespace MSCO
GUILayout.BeginVertical(); GUILayout.BeginVertical();
GUILayout.Label(s.Name, titleStyle); GUILayout.Label(s.Name, titleStyle);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
Rect badgeRow = GUILayoutUtility.GetRect(S(10), S(20), GUILayout.ExpandWidth(true)); Rect badgeRow = GUILayoutUtility.GetRect(S(10), S(22), GUILayout.ExpandWidth(true));
float bx = badgeRow.x; float bx = badgeRow.x + titleStyle.padding.left;
if (s.Featured) bx += Pill(bx, badgeRow, "FEATURED", pillFeaturedStyle); if (s.Featured) bx += Pill(bx, badgeRow, "FEATURED", pillFeaturedStyle);
bool official = s.Tags != null && s.Tags.Contains("official"); // vergibt nur ein MSCO-Admin bool official = s.Tags != null && s.Tags.Contains("official"); // vergibt nur ein MSCO-Admin
if (official) bx += Pill(bx, badgeRow, "OFFICIAL", pillStyle); if (official) bx += Pill(bx, badgeRow, "OFFICIAL", pillStyle);
@@ -651,39 +651,84 @@ namespace MSCO
GUILayout.Space(S(10)); GUILayout.Space(S(10));
} }
// Infos links, Spielerliste rechts // Zwei Karten gleicher Höhe: Infos links, Spielerliste rechts
bool versionOk = MSCO.Compatible(s.Version); bool versionOk = MSCO.Compatible(s.Version);
GUILayout.BeginHorizontal(); float cardHeight = Mathf.Clamp(contentHeight - S(250), S(240), S(380));
GUILayout.BeginVertical(GUILayout.Width(S(250))); Rect area = GUILayoutUtility.GetRect(S(10), cardHeight, GUILayout.ExpandWidth(true));
InfoLine("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle); float gap = S(16), leftWidth = Mathf.Round(area.width * 0.44f);
InfoLine("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle); var info = new Rect(area.x, area.y, leftWidth, area.height);
InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle); var list = new Rect(area.x + leftWidth + gap, area.y, area.width - leftWidth - gap, area.height);
InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle); GUI.Box(info, GUIContent.none, rowBox);
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping)); GUI.Box(list, GUIContent.none, rowBox);
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));
GUILayout.BeginVertical(rowBox, GUILayout.ExpandWidth(true), GUILayout.MinHeight(S(150))); EnsureProfileStyles();
GUILayout.Space(S(6)); float x = info.x + S(20), w = info.width - S(40), y = info.y + S(16);
GUILayout.Label("PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle); GUI.Label(new Rect(x, y, w, S(18)), "SERVER INFO", captionStyle);
profileScroll = GUILayout.BeginScrollView(profileScroll, GUILayout.Height(ListHeight(S(330f), S(130f)))); 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) if (s.PlayerNames == null || s.PlayerNames.Count == 0)
GUILayout.Label("Nobody online right now.", textStyle); {
GUI.Label(listArea, "Nobody online right now.", profileEmptyStyle);
}
else 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)); var line = new Rect(0, i * rowH, inner.width, rowH);
GUI.DrawTexture(new Rect(line.x + S(2), line.y + S(8), S(8), S(8)), texOnline); if (i > 0) GUI.DrawTexture(new Rect(line.x, line.y, line.width, S(1)), texDivider);
GUI.Label(new Rect(line.x + S(18), line.y, line.width - S(18), line.height), name, rowNameStyle); 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(); GUI.EndScrollView();
GUILayout.EndVertical(); }
GUILayout.EndHorizontal();
GUILayout.FlexibleSpace(); 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> /// <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)
{ {
+1 -1
View File
@@ -11,7 +11,7 @@ namespace MSCO
/// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit /// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel. /// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
/// </summary> /// </summary>
public const string ModVersion = "2.10.1"; public const string ModVersion = "2.10.2";
/// <summary> /// <summary>
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server. /// auch GameServer.ModVersion im Server.
+1
View File
@@ -96,6 +96,7 @@
<Compile Include="MiniJson.cs" /> <Compile Include="MiniJson.cs" />
<Compile Include="PerfStats.cs" /> <Compile Include="PerfStats.cs" />
<Compile Include="FsmDump.cs" /> <Compile Include="FsmDump.cs" />
<Compile Include="VehicleAdmin.cs" />
<Compile Include="ModApi.cs" /> <Compile Include="ModApi.cs" />
<Compile Include="ModLoader.cs" /> <Compile Include="ModLoader.cs" />
<Compile Include="MenuBranding.cs" /> <Compile Include="MenuBranding.cs" />
+7 -1
View File
@@ -569,6 +569,12 @@ namespace MSCO
} }
/// <summary>Cheats für alle sichtbar im Chat melden.</summary> /// <summary>Cheats für alle sichtbar im Chat melden.</summary>
/// <summary>Admin-Menü (Reiter VEHICLE): Änderung am Fahrzeug im Chat melden.</summary>
public void VehicleCheat(string what)
{
if (CanCheat) AnnounceCheat(what);
}
void AnnounceCheat(string what) void AnnounceCheat(string what)
{ {
string text = OwnName() + " " + what + " (admin)"; string text = OwnName() + " " + what + " (admin)";
@@ -2162,7 +2168,7 @@ namespace MSCO
// Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...) // Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...)
PartStart(); PartStart();
try { if (objects.ScanComplete) vehicleState.Update(objects, localId, SendGame); } try { if (objects.ScanComplete) vehicleState.Update(objects, localId, SendGame, IsWorldAuthority); }
catch (System.Exception e) { ReportError("vehicle state sync", e); } catch (System.Exception e) { ReportError("vehicle state sync", e); }
PartEnd("vehicle state sync"); PartEnd("vehicle state sync");
// Unfallschäden (verbeulte Karosserie) // Unfallschäden (verbeulte Karosserie)
+110 -6
View File
@@ -293,24 +293,128 @@ namespace MSCO
// Reiter // Reiter
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
string[] tabs = { "PLAYERS", "TELEPORT", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" }; string[] tabs = { "PLAYERS", "TELEPORT", "VEHICLE", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" };
for (int i = 0; i < tabs.Length; i++) for (int i = 0; i < tabs.Length; i++)
{ {
if (GUILayout.Button(tabs[i], adminTab == i ? primary : secondary, GUILayout.Height(30), GUILayout.Width(120))) adminTab = i; if (GUILayout.Button(tabs[i], adminTab == i ? primary : secondary, GUILayout.Height(30), GUILayout.Width(104))) adminTab = i;
GUILayout.Space(6); GUILayout.Space(6);
} }
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.Space(10); GUILayout.Space(10);
if (adminTab == 1) DrawAdminTeleport(mp); if (adminTab == 1) DrawAdminTeleport(mp);
else if (adminTab == 2) DrawAdminItems(mp); else if (adminTab == 2) DrawAdminVehicle(mp);
else if (adminTab == 3) DrawAdminBans(mp); else if (adminTab == 3) DrawAdminItems(mp);
else if (adminTab == 4) DrawAdminWorld(mp); else if (adminTab == 4) DrawAdminBans(mp);
else if (adminTab == 5) DrawAdminWorld(mp);
else DrawAdminPlayers(mp); else DrawAdminPlayers(mp);
GUILayout.EndArea(); GUILayout.EndArea();
} }
Vector2 teleportScroll; Vector2 teleportScroll, vehicleScroll;
Texture2D texBarBack, texBarFill, texBarLow;
string partFilter = "";
/// <summary>Reiter VEHICLE: Füllstände und Teile des Satsuma reparieren/zerstören (zum Testen).</summary>
void DrawAdminVehicle(MultiplayerManager mp)
{
if (!mp.CanCheat)
{
GUILayout.Label("Only works in the game.", small);
return;
}
if (texBarBack == null)
{
texBarBack = UiTheme.Solid(new Color(1f, 1f, 1f, 0.1f));
texBarFill = UiTheme.Solid(UiTheme.Online);
texBarLow = UiTheme.Solid(UiTheme.Danger);
}
GUILayout.BeginHorizontal();
if (GUILayout.Button("REPAIR ALL", primary, GUILayout.Height(30), GUILayout.Width(130)))
{
int n = VehicleAdmin.RepairAll();
Log(n + " part(s) repaired");
if (n > 0) mp.VehicleCheat("repaired the Satsuma (" + n + " parts)");
}
GUILayout.Space(6);
if (GUILayout.Button("REFRESH", secondary, GUILayout.Height(30), GUILayout.Width(100))) VehicleAdmin.Refresh(true);
GUILayout.Space(12);
GUILayout.Label("Filter", caption, GUILayout.Width(44));
partFilter = GUILayout.TextField(partFilter ?? "", 30, field, GUILayout.Height(30));
GUILayout.EndHorizontal();
GUILayout.Space(8);
vehicleScroll = GUILayout.BeginScrollView(vehicleScroll);
GUILayout.Label("FLUIDS", caption);
List<VehicleAdmin.Fluid> fluids = VehicleAdmin.Fluids;
if (fluids.Count == 0) GUILayout.Label("No fluids found (only works in the game).", small);
foreach (VehicleAdmin.Fluid f in fluids)
{
GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(36));
GUILayout.Label(f.Label, chatName, GUILayout.Width(170));
Rect bar = GUILayoutUtility.GetRect(120, 36, GUILayout.ExpandWidth(true));
float frac = f.Max > 0f ? Mathf.Clamp01(f.Value.Value / f.Max) : 0f;
var back = new Rect(bar.x, bar.y + 15, bar.width, 6);
GUI.DrawTexture(back, texBarBack);
GUI.DrawTexture(new Rect(back.x, back.y, Mathf.Max(2f, back.width * frac), back.height), frac < 0.25f ? texBarLow : texBarFill);
GUILayout.Label(f.Value.Value.ToString("0.00") + " / " + f.Max.ToString("0.#"), small, GUILayout.Width(90));
if (GUILayout.Button("FILL", secondary, GUILayout.Height(26), GUILayout.Width(64)))
{
VehicleAdmin.SetFluid(f, true);
mp.VehicleCheat("filled up " + f.Label.ToLower());
}
GUILayout.Space(4);
if (GUILayout.Button("EMPTY", danger, GUILayout.Height(26), GUILayout.Width(70)))
{
VehicleAdmin.SetFluid(f, false);
mp.VehicleCheat("emptied " + f.Label.ToLower());
}
GUILayout.EndHorizontal();
GUILayout.Space(3);
}
GUILayout.Space(10);
GUILayout.Label("PARTS (wear: 100 = new)", caption);
string filter = (partFilter ?? "").Trim().ToLowerInvariant();
int shown = 0;
foreach (VehicleAdmin.Part p in VehicleAdmin.Parts)
{
if (filter.Length > 0 && !(p.Name + " " + p.Category).ToLowerInvariant().Contains(filter)) continue;
shown++;
bool installed = p.Installed == null || p.Installed.Value;
bool damaged = p.Damaged != null && p.Damaged.Value;
GUILayout.BeginHorizontal(rowStyle, GUILayout.Height(36));
GUILayout.BeginVertical(GUILayout.Width(220));
GUILayout.Label(p.Name, chatName);
GUILayout.Label(p.Category + (installed ? "" : " · not installed"), small);
GUILayout.EndVertical();
Rect bar = GUILayoutUtility.GetRect(100, 36, GUILayout.ExpandWidth(true));
if (p.Wear != null)
{
float frac = Mathf.Clamp01(p.Wear.Value / 100f);
var back = new Rect(bar.x, bar.y + 15, bar.width, 6);
GUI.DrawTexture(back, texBarBack);
GUI.DrawTexture(new Rect(back.x, back.y, Mathf.Max(2f, back.width * frac), back.height), frac < 0.3f ? texBarLow : texBarFill);
}
GUILayout.Label(damaged ? "DAMAGED" : p.Wear != null ? p.Wear.Value.ToString("0") + " %" : "OK", damaged ? danger : small, GUILayout.Width(84));
if (GUILayout.Button("REPAIR", secondary, GUILayout.Height(26), GUILayout.Width(76)))
{
VehicleAdmin.Repair(p);
mp.VehicleCheat("repaired " + p.Name);
}
GUILayout.Space(4);
if (GUILayout.Button("BREAK", danger, GUILayout.Height(26), GUILayout.Width(70)))
{
VehicleAdmin.Break(p);
mp.VehicleCheat("broke " + p.Name + " (testing)");
}
GUILayout.EndHorizontal();
GUILayout.Space(3);
}
if (shown == 0) GUILayout.Label(VehicleAdmin.Parts.Count == 0 ? "No parts found (only works in the game)." : "No part matches the filter.", small);
GUILayout.EndScrollView();
}
void DrawAdminTeleport(MultiplayerManager mp) void DrawAdminTeleport(MultiplayerManager mp)
{ {
+34 -3
View File
@@ -4,8 +4,9 @@ using UnityEngine;
namespace MSCO namespace MSCO
{ {
/// <summary> /// <summary>
/// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten und /// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten,
/// fährt mit - egal ob schon jemand fährt oder das Auto noch steht. Aussteigen mit Leertaste oder der Beifahrer-Taste. /// sobald das Fahrzeug FÄHRT (egal wer fährt). Am stehenden Auto wird niemand automatisch festgehalten - sonst hing
/// man fest, sobald man sich zum Schrauben in den Motorraum beugte. Aussteigen mit Leertaste oder der Beifahrer-Taste.
/// Steht man per Taste neben dem Auto, kommt man auf den Beifahrersitz (Fahrerposition des Spiels, gespiegelt). /// Steht man per Taste neben dem Auto, kommt man auf den Beifahrersitz (Fahrerposition des Spiels, gespiegelt).
/// Während der Fahrt wird der Spieler jedes Frame an seine Stelle im Fahrzeug gesetzt und dreht mit; umschauen geht. /// Während der Fahrt wird der Spieler jedes Frame an seine Stelle im Fahrzeug gesetzt und dreht mit; umschauen geht.
/// </summary> /// </summary>
@@ -15,7 +16,8 @@ namespace MSCO
const float AutoEnterRange = 1.0f; // automatisch: Kopf so nah am Beifahrersitz ... const float AutoEnterRange = 1.0f; // automatisch: Kopf so nah am Beifahrersitz ...
const float AutoEnterSide = 0.6f; // ... und seitlich höchstens so weit außerhalb des Sitzes - an der const float AutoEnterSide = 0.6f; // ... und seitlich höchstens so weit außerhalb des Sitzes - an der
// geschlossenen Tür steht man weiter draußen, nur in der offenen Tür so nah // geschlossenen Tür steht man weiter draußen, nur in der offenen Tür so nah
const float AutoEnterDwell = 0.35f; // ... so lange (nicht beim bloßen Vorbeilaufen) const float AutoEnterDwell = 0.15f; // ... so lange
const float MovingSpeed = 1.2f; // m/s: ab hier "fährt" das Fahrzeug (dann wird man festgehalten)
const float AutoRearmRange = 2f; // nach dem Aussteigen erst wieder, wenn man so weit weg war const float AutoRearmRange = 2f; // nach dem Aussteigen erst wieder, wenn man so weit weg war
const float AutoCheckInterval = 0.15f; const float AutoCheckInterval = 0.15f;
const float CenterSeatThreshold = 0.2f; const float CenterSeatThreshold = 0.2f;
@@ -40,6 +42,10 @@ namespace MSCO
const float AfterDrivingPause = 4f; // nach dem Fahren (Ein-/Aussteigen am Fahrersitz) nicht automatisch const float AfterDrivingPause = 4f; // nach dem Fahren (Ein-/Aussteigen am Fahrersitz) nicht automatisch
SyncedBody dwellVehicle; SyncedBody dwellVehicle;
bool autoArmed = true; bool autoArmed = true;
// Geschwindigkeit je Fahrzeug aus der Bewegung (Fahrzeuge anderer sind kinematisch - deren velocity ist 0)
readonly Dictionary<Rigidbody, Vector3> lastVehiclePos = new Dictionary<Rigidbody, Vector3>();
readonly Dictionary<Rigidbody, float> vehicleSpeed = new Dictionary<Rigidbody, float>();
float lastSpeedSample = -1f;
Vector3 lastExitSeat; Vector3 lastExitSeat;
public bool IsRiding { get { return vehicle != null; } } public bool IsRiding { get { return vehicle != null; } }
@@ -101,6 +107,7 @@ namespace MSCO
if ((cc != null && !cc.enabled) || player.parent != null) { lastDriving = Time.time; dwellStart = -1f; return null; } if ((cc != null && !cc.enabled) || player.parent != null) { lastDriving = Time.time; dwellStart = -1f; return null; }
if (Time.time < nextAutoCheck) return null; if (Time.time < nextAutoCheck) return null;
nextAutoCheck = Time.time + AutoCheckInterval; nextAutoCheck = Time.time + AutoCheckInterval;
SampleSpeeds(objects);
Vector3 head = cam.transform.position; Vector3 head = cam.transform.position;
// Nach dem Aussteigen erst einmal aus dem Fahrzeug heraus, sonst säße man sofort wieder drin // Nach dem Aussteigen erst einmal aus dem Fahrzeug heraus, sonst säße man sofort wieder drin
@@ -111,6 +118,7 @@ namespace MSCO
} }
SyncedBody found = VehicleAround(objects, head); SyncedBody found = VehicleAround(objects, head);
if (found != null && SpeedOf(found) < MovingSpeed) found = null; // steht: frei bewegen (z.B. schrauben)
if (found == null || found != dwellVehicle) if (found == null || found != dwellVehicle)
{ {
dwellVehicle = found; dwellVehicle = found;
@@ -124,6 +132,26 @@ namespace MSCO
return "Riding along (Space or the passenger key to get out)"; return "Riding along (Space or the passenger key to get out)";
} }
/// <summary>Geschwindigkeit aller Fahrzeuge in der Nähe aus ihrer Bewegung seit der letzten Messung.</summary>
void SampleSpeeds(ObjectSync objects)
{
float now = Time.time, dt = lastSpeedSample < 0f ? 0f : now - lastSpeedSample;
lastSpeedSample = now;
foreach (SyncedBody b in objects.AllVehicles())
{
if (b.Rb == null) continue;
Vector3 pos = b.Rb.position, last;
if (dt > 0.01f && lastVehiclePos.TryGetValue(b.Rb, out last)) vehicleSpeed[b.Rb] = (pos - last).magnitude / dt;
lastVehiclePos[b.Rb] = pos;
}
}
float SpeedOf(SyncedBody b)
{
float v;
return b.Rb != null && vehicleSpeed.TryGetValue(b.Rb, out v) ? v : 0f;
}
/// <summary>Fahrzeug, in dessen Umriss der Kopf gerade ist (null = in keinem).</summary> /// <summary>Fahrzeug, in dessen Umriss der Kopf gerade ist (null = in keinem).</summary>
SyncedBody VehicleAround(ObjectSync objects, Vector3 head) SyncedBody VehicleAround(ObjectSync objects, Vector3 head)
{ {
@@ -280,6 +308,9 @@ namespace MSCO
dwellStart = -1f; dwellStart = -1f;
driverHeads.Clear(); driverHeads.Clear();
innerBounds.Clear(); innerBounds.Clear();
lastVehiclePos.Clear();
vehicleSpeed.Clear();
lastSpeedSample = -1f;
SetMovementEnabled(null, true); SetMovementEnabled(null, true);
} }
+2 -1
View File
@@ -47,7 +47,8 @@ einem dedizierten Server aus dem Server-Browser. MSCO läuft **eigenständig, oh
- **T** – Chat - **T** – Chat
- **F7** – MSCO-Konsole mit allen Befehlen - **F7** – MSCO-Konsole mit allen Befehlen
- **/admin** – Admin-Menü für Server-Admins bzw. den P2P-Host: Spieler kicken/bannen, **Admin geben/nehmen**, - **/admin** – Admin-Menü für Server-Admins bzw. den P2P-Host: Spieler kicken/bannen, **Admin geben/nehmen**,
GOTO/BRING (teleportieren), **TELEPORT** zu Orten (Haus, Laden, Werkstatt, Prüfstelle, Peräjärvi …), Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen), Items spawnen, GOTO/BRING (teleportieren), **TELEPORT** zu Orten (Haus, Laden, Werkstatt, Prüfstelle, Peräjärvi …), **VEHICLE**
(Füllstände auffüllen/leeren, Teile reparieren/zerstören – zum Testen), Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen), Items spawnen,
Welt zurücksetzen/Backups – Cheats werden im Chat angezeigt Welt zurücksetzen/Backups – Cheats werden im Chat angezeigt
- **P** / **Leertaste** – aussteigen; **P** neben einem Auto – auf den Beifahrersitz - **P** / **Leertaste** – aussteigen; **P** neben einem Auto – auf den Beifahrersitz
- Tasten ändern mit `bind` in der F7-Konsole (z.B. `bind passenger O`) - Tasten ändern mit `bind` in der F7-Konsole (z.B. `bind passenger O`)
+137
View File
@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// Admin-Menü, Reiter VEHICLE: Füllstände und Teile des Satsuma aus der "Database" des Spiels anzeigen und zum Testen
/// reparieren/zerstören. Jedes Teil hat dort ein Objekt mit der FSM "Data" (Installed, Bolted, Damaged, teils Wear);
/// die Füllstände stehen in Tank, Ölwanne, Brems- und Kupplungszylinder. Die Werte überträgt VehicleStateSync an alle.
/// </summary>
public static class VehicleAdmin
{
public sealed class Fluid
{
public string Label;
public FsmFloat Value;
public float Max;
}
public sealed class Part
{
public string Name, Category;
public FsmBool Damaged, Installed;
public FsmFloat Wear;
}
static readonly List<Fluid> fluids = new List<Fluid>();
static readonly List<Part> parts = new List<Part>();
static float scannedAt = -100f;
public static List<Fluid> Fluids { get { Refresh(false); return fluids; } }
public static List<Part> Parts { get { Refresh(false); return parts; } }
// Füllstand: Teil in der Database, Variable, Anzeige, Maximum (Tank: aus MaxCapacity)
static readonly string[][] FluidDefs =
{
new[] { "FuelTank", "FuelLevel", "Fuel", "36" },
new[] { "Oilpan", "Oil", "Engine oil", "3" },
new[] { "BrakeMasterCylinder", "BrakeFluidF", "Brake fluid (front)", "1" },
new[] { "BrakeMasterCylinder", "BrakeFluidR", "Brake fluid (rear)", "1" },
new[] { "ClutchMasterCylinder", "ClutchFluid", "Clutch fluid", "0.5" },
new[] { "Radiator", "Water", "Coolant", "6" },
};
/// <summary>Database neu einlesen (höchstens alle 5 s, außer erzwungen).</summary>
public static void Refresh(bool force)
{
if (!force && Time.realtimeSinceStartup - scannedAt < 5f) return;
scannedAt = Time.realtimeSinceStartup;
fluids.Clear();
parts.Clear();
if (!MenuHelper.InGame) return;
var byName = new Dictionary<string, PlayMakerFSM>();
foreach (PlayMakerFSM fsm in Resources.FindObjectsOfTypeAll<PlayMakerFSM>())
{
if (fsm == null || fsm.FsmName != "Data" || fsm.gameObject.hideFlags != HideFlags.None) continue;
Transform t = fsm.transform;
if (t.root.name != "Database" || t.parent == null || t.parent.parent != t.root) continue;
string category = t.parent.name;
if (category == "DatabaseOrders" || category == "PlayerDatabase") continue; // Bestellungen/Spieler: keine Fahrzeugteile
byName[t.name] = fsm;
FsmBool installed = fsm.FsmVariables.FindFsmBool("Installed");
FsmBool damaged = fsm.FsmVariables.FindFsmBool("Damaged");
FsmFloat wear = fsm.FsmVariables.FindFsmFloat("Wear");
if (damaged == null && wear == null) continue;
parts.Add(new Part
{
Name = Pretty(t.name),
Category = category.Replace("Database", ""),
Damaged = damaged, Installed = installed, Wear = wear,
});
}
parts.Sort((a, b) => string.Compare(a.Category + a.Name, b.Category + b.Name, StringComparison.Ordinal));
foreach (string[] def in FluidDefs)
{
PlayMakerFSM fsm;
if (!byName.TryGetValue(def[0], out fsm)) continue;
FsmFloat value = fsm.FsmVariables.FindFsmFloat(def[1]);
if (value == null) continue;
float max = float.Parse(def[3], System.Globalization.CultureInfo.InvariantCulture);
FsmFloat capacity = fsm.FsmVariables.FindFsmFloat("MaxCapacity");
if (def[0] == "FuelTank" && capacity != null && capacity.Value > 0f) max = capacity.Value;
fluids.Add(new Fluid { Label = def[2], Value = value, Max = max });
}
}
/// <summary>"RadiatorHose1" -> "Radiator Hose 1", "Drumbrake_RR" -> "Drumbrake RR".</summary>
static string Pretty(string name)
{
var sb = new System.Text.StringBuilder();
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if (c == '_') { sb.Append(' '); continue; }
if (i > 0 && ((char.IsUpper(c) && char.IsLower(name[i - 1])) || (char.IsDigit(c) && !char.IsDigit(name[i - 1])))) sb.Append(' ');
sb.Append(c);
}
return sb.ToString();
}
public static void SetFluid(Fluid f, bool full)
{
if (f.Value != null) f.Value.Value = full ? f.Max : 0f;
}
public static void Repair(Part p)
{
if (p.Damaged != null) p.Damaged.Value = false;
if (p.Wear != null) p.Wear.Value = 100f;
}
public static void Break(Part p)
{
if (p.Damaged != null) p.Damaged.Value = true;
if (p.Wear != null) p.Wear.Value = 0f;
}
public static int RepairAll()
{
Refresh(true);
int n = 0;
foreach (Part p in parts)
{
if (p.Installed != null && !p.Installed.Value) continue;
bool needs = (p.Damaged != null && p.Damaged.Value) || (p.Wear != null && p.Wear.Value < 100f);
if (!needs) continue;
Repair(p);
n++;
}
return n;
}
}
}
+75 -3
View File
@@ -60,6 +60,10 @@ namespace MSCO
public float LastF; public float LastF;
public int LastI; public int LastI;
public bool LastB; public bool LastB;
// Schraube mit Funktion (z.B. Ablassschraube der Ölwanne: offen = Öl läuft aus): die Stufe wird als Zustand
// übertragen und über die Stufen-Logik der Schraube angewendet (Position, Dichtung, Zähler des Teils)
public FsmState BoltDispatch;
public bool InitialSent;
} }
readonly Dictionary<int, Var> vars = new Dictionary<int, Var>(); readonly Dictionary<int, Var> vars = new Dictionary<int, Var>();
@@ -103,7 +107,7 @@ namespace MSCO
nextScan = -1f; // erster Suchlauf verteilt, siehe Update 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, bool worldAuthority = false)
{ {
if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen 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)
@@ -124,7 +128,15 @@ namespace MSCO
// Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) - // Eigenes Fahrzeug: jede Änderung. Freies Fahrzeug: nur deutliche Sprünge (Öl nachfüllen, tanken ...) -
// langsames Abdriften (z.B. Abkühlen) rechnet jeder selbst, sonst überschreiben sich alle gegenseitig // langsames Abdriften (z.B. Abkühlen) rechnet jeder selbst, sonst überschreiben sich alle gegenseitig
bool big = owner == SyncedBody.NoOwner; bool big = owner == SyncedBody.NoOwner && v.BoltDispatch == null;
if (v.BoltDispatch != null && worldAuthority && !v.InitialSent)
{
// Einmal den eigenen Stand vorgeben, damit alle gleich sind (vorher auseinandergelaufene Schrauben)
v.InitialSent = true;
Remember(v);
outgoing.Add(v);
continue;
}
if (!Changed(v, big)) continue; if (!Changed(v, big)) continue;
Remember(v); Remember(v);
outgoing.Add(v); outgoing.Add(v);
@@ -186,6 +198,7 @@ namespace MSCO
if (!IsTracked(root)) { Forget(fsm); continue; } if (!IsTracked(root)) { Forget(fsm); continue; }
scanned[fsm] = fsm.transform.parent; scanned[fsm] = fsm.transform.parent;
Forget(fsm); Forget(fsm);
if (RegisterFunctionalBolt(fsm, root)) continue;
// FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert // FSMs, die Größen/Positionen setzen (z.B. Füllstands-Anzeigen), NICHT anfassen: ein übertragener Wert
// kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed") // kann dort eine Größe 0 ergeben - Unity stürzt dann in der Physik ab ("cleaning the mesh failed")
if (TouchesTransform(fsm)) continue; if (TouchesTransform(fsm)) continue;
@@ -205,6 +218,54 @@ namespace MSCO
if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced"); if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced");
} }
/// <summary>
/// Schrauben, deren Stufen etwas ein-/ausschalten (ActivateGameObject - z.B. die Ablassschraube: Stufe 0-5 = das
/// Leck "Oil" ist an). Sonst laufen Schrauben über FsmSync (Ereignisse): ging eins verloren, war die Ablassschraube
/// bei einem offen und beim anderen zu - bei ihm lief dann endlos Öl aus (der Ölstand kam ja ständig nach).
/// </summary>
bool RegisterFunctionalBolt(PlayMakerFSM fsm, string root)
{
if (fsm.FsmName != "Screw") return false;
FsmInt stage = fsm.FsmVariables.FindFsmInt("Stage");
if (stage == null) return false;
FsmState dispatch = fsm.Fsm.GetState("Stages 2") ?? fsm.Fsm.GetState("Stages");
if (dispatch == null) return false;
bool switches = false;
try
{
foreach (FsmState state in fsm.FsmStates)
{
if (!state.ActionsLoaded) state.LoadActions();
if (state.Actions == null) continue;
foreach (FsmStateAction a in state.Actions)
if (a != null && a.GetType().Name == "ActivateGameObject") { switches = true; break; }
if (switches) break;
}
}
catch { return false; }
if (!switches) return false;
string name = FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName + "|Stage";
int key = Hash(name);
Var existing;
if (vars.TryGetValue(key, out existing) && existing.Variable == stage) return true;
var v = new Var { Key = key, Name = name, Root = root, Variable = stage, Type = 2, Fsm = fsm, BoltDispatch = dispatch };
Remember(v);
vars[key] = v;
List<int> keys;
if (!keysByFsm.TryGetValue(fsm, out keys)) keysByFsm[fsm] = keys = new List<int>();
keys.Add(key);
byte[] stored;
if (pending.TryGetValue(key, out stored))
{
pending.Remove(key);
byte[] value = new byte[stored.Length - 1];
Array.Copy(stored, 1, value, 0, value.Length);
Apply(v, stored[0], value);
}
return true;
}
static readonly string[] TransformActionWords = { "Scale", "Position", "Rotate", "Rotation", "Translate", "Parent" }; static readonly string[] TransformActionWords = { "Scale", "Position", "Rotate", "Rotation", "Translate", "Parent" };
static bool TouchesTransform(PlayMakerFSM fsm) static bool TouchesTransform(PlayMakerFSM fsm)
@@ -369,7 +430,18 @@ namespace MSCO
if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen
SetFloat(v, f); SetFloat(v, f);
break; break;
case 2: ((FsmInt)v.Variable).Value = BitConverter.ToInt32(value, 0); break; case 2:
int n = BitConverter.ToInt32(value, 0);
if (v.BoltDispatch != null)
{
if (n < 0 || n > 8 || ((FsmInt)v.Variable).Value == n) { Remember(v); return; }
((FsmInt)v.Variable).Value = n;
if (v.Fsm != null && v.Fsm.gameObject.activeInHierarchy) FsmSync.SwitchStateSilently(v.Fsm, v.BoltDispatch);
if (FsmSync.Debug) MscoLog.Info("[MSCO] << BOLT " + v.Name + " -> " + n);
break;
}
((FsmInt)v.Variable).Value = n;
break;
default: ((FsmBool)v.Variable).Value = value[0] != 0; break; default: ((FsmBool)v.Variable).Value = value[0] != 0; break;
} }
Remember(v); // nicht als eigene Änderung zurückschicken Remember(v); // nicht als eigene Änderung zurückschicken