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:
@@ -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**
|
||||
|
||||
+11
@@ -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();
|
||||
|
||||
+71
-26
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
public const string ModVersion = "2.10.1";
|
||||
public const string ModVersion = "2.10.2";
|
||||
/// <summary>
|
||||
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
|
||||
/// auch GameServer.ModVersion im Server.
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
<Compile Include="MiniJson.cs" />
|
||||
<Compile Include="PerfStats.cs" />
|
||||
<Compile Include="FsmDump.cs" />
|
||||
<Compile Include="VehicleAdmin.cs" />
|
||||
<Compile Include="ModApi.cs" />
|
||||
<Compile Include="ModLoader.cs" />
|
||||
<Compile Include="MenuBranding.cs" />
|
||||
|
||||
@@ -569,6 +569,12 @@ namespace MSCO
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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)
|
||||
|
||||
+110
-6
@@ -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 = "";
|
||||
|
||||
/// <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)
|
||||
{
|
||||
|
||||
+34
-3
@@ -4,8 +4,9 @@ using UnityEngine;
|
||||
namespace MSCO
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
@@ -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<Rigidbody, Vector3> lastVehiclePos = new Dictionary<Rigidbody, Vector3>();
|
||||
readonly Dictionary<Rigidbody, float> vehicleSpeed = new Dictionary<Rigidbody, float>();
|
||||
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)";
|
||||
}
|
||||
|
||||
/// <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>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
+137
@@ -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
@@ -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<int, Var> vars = new Dictionary<int, Var>();
|
||||
@@ -103,7 +107,7 @@ namespace MSCO
|
||||
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 (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");
|
||||
}
|
||||
|
||||
/// <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 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
|
||||
|
||||
Reference in New Issue
Block a user