diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3cbd97b..dcf5694 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# 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
**Türen**
diff --git a/FsmDump.cs b/FsmDump.cs
index c9a82bd..1531a2e 100644
--- a/FsmDump.cs
+++ b/FsmDump.cs
@@ -55,6 +55,17 @@ namespace MSCO
{
sb.Append("==== ").Append(path).Append(" | ").Append(fsm.FsmName).Append(" | active ").Append(fsm.gameObject.activeInHierarchy)
.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)
foreach (FsmTransition t in fsm.FsmGlobalTransitions)
sb.Append(" GLOBAL ").Append(t.EventName).Append(" -> ").Append(t.ToState).AppendLine();
diff --git a/LobbyUI.cs b/LobbyUI.cs
index 1fcd966..d6e8366 100644
--- a/LobbyUI.cs
+++ b/LobbyUI.cs
@@ -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 };
+ }
+
+ /// Info-Zeile der Server-Karte: Bezeichnung links, Wert daneben, feine Linie darunter. Gibt das nächste y zurück.
+ 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);
+ }
+
/// Eine Info-Zeile: Bezeichnung links (grau), Wert rechts.
void InfoLine(string caption, string value, GUIStyle valueStyle)
{
diff --git a/MSCO.cs b/MSCO.cs
index c1f89f7..319e908 100644
--- a/MSCO.cs
+++ b/MSCO.cs
@@ -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
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
///
- public const string ModVersion = "2.10.1";
+ public const string ModVersion = "2.10.2";
///
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server.
diff --git a/MSCO.csproj b/MSCO.csproj
index 80d6d5d..24c34cb 100644
--- a/MSCO.csproj
+++ b/MSCO.csproj
@@ -96,6 +96,7 @@
+
diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs
index 3d68a63..4399ba9 100644
--- a/MultiplayerManager.cs
+++ b/MultiplayerManager.cs
@@ -569,6 +569,12 @@ namespace MSCO
}
/// Cheats für alle sichtbar im Chat melden.
+ /// Admin-Menü (Reiter VEHICLE): Änderung am Fahrzeug im Chat melden.
+ public void VehicleCheat(string what)
+ {
+ if (CanCheat) AnnounceCheat(what);
+ }
+
void AnnounceCheat(string what)
{
string text = OwnName() + " " + what + " (admin)";
@@ -2162,7 +2168,7 @@ namespace MSCO
// Gesamtzustand der Fahrzeuge (Tank, Öl, Verschleiß ...)
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); }
PartEnd("vehicle state sync");
// Unfallschäden (verbeulte Karosserie)
diff --git a/Overlays.cs b/Overlays.cs
index b087193..8095b05 100644
--- a/Overlays.cs
+++ b/Overlays.cs
@@ -293,24 +293,128 @@ namespace MSCO
// Reiter
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++)
{
- 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.EndHorizontal();
GUILayout.Space(10);
if (adminTab == 1) DrawAdminTeleport(mp);
- else if (adminTab == 2) DrawAdminItems(mp);
- else if (adminTab == 3) DrawAdminBans(mp);
- else if (adminTab == 4) DrawAdminWorld(mp);
+ else if (adminTab == 2) DrawAdminVehicle(mp);
+ else if (adminTab == 3) DrawAdminItems(mp);
+ else if (adminTab == 4) DrawAdminBans(mp);
+ else if (adminTab == 5) DrawAdminWorld(mp);
else DrawAdminPlayers(mp);
GUILayout.EndArea();
}
- Vector2 teleportScroll;
+ Vector2 teleportScroll, vehicleScroll;
+ Texture2D texBarBack, texBarFill, texBarLow;
+ string partFilter = "";
+
+ /// Reiter VEHICLE: Füllstände und Teile des Satsuma reparieren/zerstören (zum Testen).
+ 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 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)
{
diff --git a/Passenger.cs b/Passenger.cs
index 03da5ca..59ea29a 100644
--- a/Passenger.cs
+++ b/Passenger.cs
@@ -4,8 +4,9 @@ using UnityEngine;
namespace MSCO
{
///
- /// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten und
- /// fährt mit - egal ob schon jemand fährt oder das Auto noch steht. Aussteigen mit Leertaste oder der Beifahrer-Taste.
+ /// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten,
+ /// 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).
/// Während der Fahrt wird der Spieler jedes Frame an seine Stelle im Fahrzeug gesetzt und dreht mit; umschauen geht.
///
@@ -15,7 +16,8 @@ namespace MSCO
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
// 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 AutoCheckInterval = 0.15f;
const float CenterSeatThreshold = 0.2f;
@@ -40,6 +42,10 @@ namespace MSCO
const float AfterDrivingPause = 4f; // nach dem Fahren (Ein-/Aussteigen am Fahrersitz) nicht automatisch
SyncedBody dwellVehicle;
bool autoArmed = true;
+ // Geschwindigkeit je Fahrzeug aus der Bewegung (Fahrzeuge anderer sind kinematisch - deren velocity ist 0)
+ readonly Dictionary lastVehiclePos = new Dictionary();
+ readonly Dictionary vehicleSpeed = new Dictionary();
+ float lastSpeedSample = -1f;
Vector3 lastExitSeat;
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 (Time.time < nextAutoCheck) return null;
nextAutoCheck = Time.time + AutoCheckInterval;
+ SampleSpeeds(objects);
Vector3 head = cam.transform.position;
// 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);
+ if (found != null && SpeedOf(found) < MovingSpeed) found = null; // steht: frei bewegen (z.B. schrauben)
if (found == null || found != dwellVehicle)
{
dwellVehicle = found;
@@ -124,6 +132,26 @@ namespace MSCO
return "Riding along (Space or the passenger key to get out)";
}
+ /// Geschwindigkeit aller Fahrzeuge in der Nähe aus ihrer Bewegung seit der letzten Messung.
+ 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;
+ }
+
/// Fahrzeug, in dessen Umriss der Kopf gerade ist (null = in keinem).
SyncedBody VehicleAround(ObjectSync objects, Vector3 head)
{
@@ -280,6 +308,9 @@ namespace MSCO
dwellStart = -1f;
driverHeads.Clear();
innerBounds.Clear();
+ lastVehiclePos.Clear();
+ vehicleSpeed.Clear();
+ lastSpeedSample = -1f;
SetMovementEnabled(null, true);
}
diff --git a/README.md b/README.md
index 582c9e4..43c13ea 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,8 @@ einem dedizierten Server aus dem Server-Browser. MSCO läuft **eigenständig, oh
- **T** – Chat
- **F7** – MSCO-Konsole mit allen Befehlen
- **/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
- **P** / **Leertaste** – aussteigen; **P** neben einem Auto – auf den Beifahrersitz
- Tasten ändern mit `bind` in der F7-Konsole (z.B. `bind passenger O`)
diff --git a/VehicleAdmin.cs b/VehicleAdmin.cs
new file mode 100644
index 0000000..302b228
--- /dev/null
+++ b/VehicleAdmin.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using HutongGames.PlayMaker;
+using UnityEngine;
+
+namespace MSCO
+{
+ ///
+ /// 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.
+ ///
+ 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 fluids = new List();
+ static readonly List parts = new List();
+ static float scannedAt = -100f;
+
+ public static List Fluids { get { Refresh(false); return fluids; } }
+ public static List 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" },
+ };
+
+ /// Database neu einlesen (höchstens alle 5 s, außer erzwungen).
+ 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();
+ foreach (PlayMakerFSM fsm in Resources.FindObjectsOfTypeAll())
+ {
+ 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 });
+ }
+ }
+
+ /// "RadiatorHose1" -> "Radiator Hose 1", "Drumbrake_RR" -> "Drumbrake RR".
+ 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;
+ }
+ }
+}
diff --git a/VehicleStateSync.cs b/VehicleStateSync.cs
index 397b032..fb33e72 100644
--- a/VehicleStateSync.cs
+++ b/VehicleStateSync.cs
@@ -60,6 +60,10 @@ namespace MSCO
public float LastF;
public int LastI;
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 vars = new Dictionary();
@@ -103,7 +107,7 @@ namespace MSCO
nextScan = -1f; // erster Suchlauf verteilt, siehe Update
}
- public void Update(ObjectSync objects, int localId, Action send)
+ public void Update(ObjectSync objects, int localId, Action send, bool worldAuthority = false)
{
if (nextScan < 0f) nextScan = Time.time + 3.1f; // nicht alle Sync-Teile im selben Frame suchen lassen
if (Time.time >= nextScan)
@@ -124,7 +128,15 @@ namespace MSCO
// 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
- 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;
Remember(v);
outgoing.Add(v);
@@ -186,6 +198,7 @@ namespace MSCO
if (!IsTracked(root)) { Forget(fsm); continue; }
scanned[fsm] = fsm.transform.parent;
Forget(fsm);
+ if (RegisterFunctionalBolt(fsm, root)) continue;
// 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")
if (TouchesTransform(fsm)) continue;
@@ -205,6 +218,54 @@ namespace MSCO
if (vars.Count != before) MscoLog.Info("[MSCO] Vehicle state: " + vars.Count + " values synced");
}
+ ///
+ /// 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).
+ ///
+ 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 keys;
+ if (!keysByFsm.TryGetValue(fsm, out keys)) keysByFsm[fsm] = keys = new List();
+ 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 bool TouchesTransform(PlayMakerFSM fsm)
@@ -369,7 +430,18 @@ namespace MSCO
if (float.IsNaN(f) || float.IsInfinity(f)) return; // nie ungültige Werte übernehmen
SetFloat(v, f);
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;
}
Remember(v); // nicht als eigene Änderung zurückschicken