diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9ce25f..15cce75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d
- **Räder** drehen sich bei Autos, die ein anderer Spieler fährt
- **Unfallschäden**: verbeulte Karosserie wird an alle übertragen
- **Jobs, Aufträge und Laden**: Fortschritt (Auftrag angenommen, erledigt, bestellt ...) wird synchronisiert
+- **Zündung, Motor, Licht, Choke, Blinker, Wischer**: die Schalterstellungen werden übertragen – beim anderen laufen
+ Motor und Scheinwerfer dadurch wirklich (nicht nur das Geräusch)
- **Autotüren, Motorhaube, Kofferraum**: stehen bei allen im selben Winkel offen
- **Verkehr**: NPC-Autos fahren bei allen an derselben Stelle (der erste Spieler simuliert, die anderen folgen)
- `vehicles` in der F7-Konsole zeigt, wie viele Karosserie-Meshes, Job-Werte und Verkehrsautos synchronisiert werden
@@ -23,10 +25,13 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d
**Trennen & Neu-Verbinden**
- Gekickt, gebannt, Server beendet, Verbindung verloren: eigenes Fenster in der Bildmitte mit **Grund** und OK
(im Spiel wird der Grund vorher 2,5 s groß angezeigt)
-- `reset-world` / `reload-save`: Fenster „Reconnecting“ – verbindet automatisch neu, bis zu 5 Versuche (falls der
- Server gerade neu startet), mit CANCEL-Knopf
+- `reset-world` / `reload-save` / **Server-Neustart** (z.B. Update per `docker compose up`) / **Verbindung verloren**:
+ Fenster „Reconnecting“ – verbindet automatisch neu, bis zu 10 Versuche (ca. 1 Minute), mit CANCEL-Knopf.
+ Nach Kick, Ban oder eigenem Trennen wird nicht neu verbunden
**Behoben**
+- Beim anderen Spieler „orgelte“ der Anlasser endlos: erzwungene Geräusche/Lichter werden losgelassen, sobald keine
+ Daten mehr vom Fahrer kommen
- Beim Neuladen der Welt wurden herumliegende Objekte als „verbraucht“ gemeldet und bei allen gelöscht. Jetzt werden
nur einzelne echte Items gemeldet, Fahrzeuge/Teile nie gelöscht, und der Server nimmt höchstens 20 pro Minute an
diff --git a/ControlSync.cs b/ControlSync.cs
new file mode 100644
index 0000000..97334df
--- /dev/null
+++ b/ControlSync.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using HutongGames.PlayMaker;
+using UnityEngine;
+
+namespace MSCO
+{
+ ///
+ /// Bedienelemente der Fahrzeuge: Zündschloss, Anlasser, Motor, Lichtschalter, Choke, Blinker, Scheibenwischer ...
+ /// Das Spiel schaltet damit Motor und Licht über PlayMaker-FSMs (Zustände wie "Off" / "Lights" / "Running").
+ /// Übertragen wird der aktive Zustand; beim anderen Spieler wird dieselbe FSM in denselben Zustand geschaltet,
+ /// sodass sein Spiel Motor, Geräusch, Scheinwerfer und Armaturen selbst richtig anschaltet.
+ /// Wer sendet: der Fahrer; ist das Fahrzeug frei, wer daneben steht bzw. drin sitzt (Schalter betätigt).
+ /// FSMs, die ständig den Zustand wechseln (interne Schleifen), werden nach kurzer Zeit nicht mehr übertragen.
+ ///
+ public class ControlSync
+ {
+ // Objekt- oder FSM-Name enthält eines davon
+ static readonly string[] Keywords =
+ {
+ "ignition", "starter", "engine", "light", "choke", "wiper", "hazard", "indicator", "turnsignal", "blinker",
+ "horn", "heater", "fan", "radio", "key", "switch", "knob", "button"
+ };
+ // Montage/Speichern/Schrauben - läuft über den Objekt- bzw. FSM-Sync, hier nicht anfassen
+ static readonly string[] Excluded =
+ {
+ "removal", "assembly", "assemble", "bolt", "screw", "data", "save", "load", "paint", "detach", "attach",
+ "install", "trigger", "tighten", "wear", "damage", "oil", "coolant", "fuel", "inspection"
+ };
+
+ const float ScanInterval = 10f;
+ const float CheckInterval = 0.1f;
+ const float NearRange = 5f;
+ const int FlipLimit = 12; // so viele Zustandswechsel ...
+ const float FlipWindow = 10f; // ... in dieser Zeit = interne Schleife, nicht übertragen
+ const int MaxPerMessage = 100;
+
+ class Control
+ {
+ public int Key;
+ public string Root;
+ public PlayMakerFSM Fsm;
+ public string LastState;
+ public float WindowStart;
+ public int Flips;
+ public bool Muted;
+ }
+
+ readonly Dictionary controls = new Dictionary();
+ readonly HashSet scanned = new HashSet();
+ readonly List outgoing = new List();
+ float nextScan, nextCheck;
+
+ public int Count { get { return controls.Count; } }
+ public int MutedCount { get { int n = 0; foreach (Control c in controls.Values) if (c.Muted) n++; return n; } }
+
+ public void Reset()
+ {
+ controls.Clear();
+ scanned.Clear();
+ nextScan = 0f;
+ }
+
+ public void Update(ObjectSync objects, int localId, Vector3 playerPos, Action send)
+ {
+ if (Time.time >= nextScan)
+ {
+ nextScan = Time.time + ScanInterval;
+ Scan();
+ }
+ if (Time.time < nextCheck) return;
+ nextCheck = Time.time + CheckInterval;
+
+ outgoing.Clear();
+ foreach (Control c in controls.Values)
+ {
+ if (c.Fsm == null || c.Muted) continue;
+ string state = c.Fsm.ActiveStateName;
+ if (state == c.LastState || string.IsNullOrEmpty(state)) continue;
+ c.LastState = state;
+
+ if (Time.time - c.WindowStart > FlipWindow) { c.WindowStart = Time.time; c.Flips = 0; }
+ if (++c.Flips > FlipLimit)
+ {
+ c.Muted = true;
+ MscoLog.Info("[MSCO] Control not synced (changes all the time): " + c.Fsm.gameObject.name + " [" + c.Fsm.FsmName + "]");
+ continue;
+ }
+
+ int owner = objects.VehicleOwner(c.Root);
+ if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer gibt vor
+ if (owner == SyncedBody.NoOwner && (c.Fsm.transform.position - playerPos).sqrMagnitude > NearRange * NearRange) continue;
+ outgoing.Add(c);
+ }
+
+ for (int start = 0; start < outgoing.Count; start += MaxPerMessage)
+ {
+ int from = start, count = Math.Min(MaxPerMessage, outgoing.Count - start);
+ send(MsgType.VehicleControls, NetSession.Build(w =>
+ {
+ w.Write(count);
+ for (int i = from; i < from + count; i++) { w.Write(outgoing[i].Key); w.Write(outgoing[i].LastState); }
+ }));
+ }
+ }
+
+ public void Handle(BinaryReader r, ObjectSync objects, int localId)
+ {
+ int count = r.ReadInt32();
+ for (int i = 0; i < count; i++)
+ {
+ int key = r.ReadInt32();
+ string state = r.ReadString();
+ Control c;
+ if (!controls.TryGetValue(key, out c) || c.Fsm == null) { nextScan = 0f; continue; }
+ if (objects.VehicleOwner(c.Root) == localId) continue; // ich fahre: meine Schalter gelten
+ if (c.Fsm.ActiveStateName == state) { c.LastState = state; continue; }
+ FsmState target = c.Fsm.Fsm.GetState(state);
+ if (target == null) continue;
+ c.LastState = state; // nicht als eigene Änderung zurückschicken
+ FsmSync.SwitchStateSilently(c.Fsm, target);
+ c.LastState = c.Fsm.ActiveStateName; // FSM kann sofort weiterschalten (z.B. "Start" -> "Running")
+ }
+ }
+
+ void Scan()
+ {
+ int before = controls.Count;
+ foreach (PlayMakerFSM fsm in SceneCache.Fsms(5f))
+ {
+ if (fsm == null || !scanned.Add(fsm)) continue;
+ string root = fsm.transform.root.name;
+ if (!IsVehicle(root) || !Wanted(fsm.gameObject.name, fsm.FsmName)) continue;
+ int key = Hash(FsmSync.PathOf(fsm.transform) + "|" + fsm.FsmName);
+ if (controls.ContainsKey(key)) continue;
+ controls[key] = new Control { Key = key, Root = root, Fsm = fsm, LastState = fsm.ActiveStateName, WindowStart = Time.time };
+ }
+ if (controls.Count != before) MscoLog.Info("[MSCO] Vehicle controls (ignition, lights, engine ...): " + controls.Count + " synced");
+ }
+
+ static bool Wanted(string objectName, string fsmName)
+ {
+ string o = objectName.ToLower(), f = fsmName.ToLower();
+ foreach (string x in Excluded) if (o.Contains(x) || f.Contains(x)) return false;
+ foreach (string k in Keywords) if (o.Contains(k) || f.Contains(k)) return true;
+ return false;
+ }
+
+ static bool IsVehicle(string root)
+ {
+ foreach (string v in ObjectSync.VehicleRoots) if (root.StartsWith(v)) return true;
+ return false;
+ }
+
+ static int Hash(string s)
+ {
+ unchecked
+ {
+ uint h = 2166136261;
+ foreach (byte c in Encoding.UTF8.GetBytes(s)) { h ^= c; h *= 16777619; }
+ return (int)h;
+ }
+ }
+ }
+}
diff --git a/MSCO.csproj b/MSCO.csproj
index 30c8d7d..e121b68 100644
--- a/MSCO.csproj
+++ b/MSCO.csproj
@@ -78,6 +78,7 @@
+
diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs
index 61dc8d9..0895c22 100644
--- a/MultiplayerManager.cs
+++ b/MultiplayerManager.cs
@@ -64,6 +64,7 @@ namespace MSCO
readonly WorldVarSync worldVars = new WorldVarSync();
readonly TrafficSync traffic = new TrafficSync();
readonly HingeSync hinges = new HingeSync();
+ readonly ControlSync controls = new ControlSync();
readonly Overlays overlays = new Overlays();
bool isAdmin; // Server hat uns als Admin gemeldet
readonly HashSet bannedNames = new HashSet(); // P2P-Host: für diese Sitzung gebannt
@@ -209,6 +210,7 @@ namespace MSCO
lines.Add("synced vehicle values (fuel, oil, bolts, wear ...): " + vehicleState.Count);
lines.Add("synced body meshes (crash damage): " + damageSync.Count);
lines.Add("synced car doors/hoods/trunks: " + hinges.Count);
+ lines.Add("synced vehicle controls (ignition, engine, lights ...): " + controls.Count + " (" + controls.MutedCount + " muted)");
lines.Add("synced job/shop values: " + worldVars.Count);
lines.Add("synced traffic cars: " + traffic.Count + (IsWorldAuthority ? " (simulated here)" : " (following)"));
return lines;
@@ -607,7 +609,7 @@ namespace MSCO
float centerMessageUntil;
float reconnectAt;
int reconnectAttempts;
- const int MaxReconnectAttempts = 5;
+ const int MaxReconnectAttempts = 10; // ~1 Minute: reicht für einen Server-Neustart
const float ReconnectRetrySeconds = 6f;
void ConnectToServer(string host, int port, string displayName, int maxPlayers)
@@ -848,6 +850,8 @@ namespace MSCO
{
reconnectAttempts++;
reconnectAt = Time.realtimeSinceStartup + ReconnectRetrySeconds;
+ MscoLog.Info("[MSCO] Reconnecting to " + lastServerHost + ":" + lastServerPort +
+ " (attempt " + reconnectAttempts + "/" + MaxReconnectAttempts + ")");
JoinServer(lastServerHost, lastServerPort, lastServerName, lastServerMax);
}
}
@@ -990,6 +994,15 @@ namespace MSCO
void OnSessionEnded(string reason)
{
+ // Verbindung zum Server unerwartet weg (Absturz, Timeout, Netz) - nicht gekickt/gebannt, nicht selbst
+ // getrennt: automatisch neu verbinden, als hätte der Server einen Neustart angekündigt
+ if (reconnectMessage == null && serverReason == null && reason != "Disconnected" && dedicated && handshakeDone
+ && lastServerHost != null)
+ {
+ reconnectMessage = "Lost the connection to the server - reconnecting...";
+ reconnectAttempts = 0;
+ MscoLog.Warning("[MSCO] Connection lost (" + reason + ") - will reconnect");
+ }
// Hinweis fürs Hauptmenü - außer man hat selbst auf DISCONNECT gedrückt oder verbindet gleich neu
if (reconnectMessage != null)
{
@@ -1022,7 +1035,7 @@ namespace MSCO
worldSync.Reset();
moneySync.Reset();
doorSync.Reset();
- vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); hinges.Reset();
+ vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); hinges.Reset(); controls.Reset();
isAdmin = false;
if (overlays.AdminOpen || overlays.ChatOpen) overlays.CloseAll();
FsmSync.Enabled = false;
@@ -1051,7 +1064,8 @@ namespace MSCO
|| t == MsgType.ObjRelease || t == MsgType.FsmEvent || t == MsgType.VehicleFx || t == MsgType.WorldTime || t == MsgType.MoneyDelta || t == MsgType.DoorState
|| t == MsgType.Chat || t == MsgType.VarSync || t == MsgType.SpawnItem
|| t == MsgType.ItemAppeared || t == MsgType.ObjDestroy || t == MsgType.VehicleDamage
- || t == MsgType.TrafficStates || t == MsgType.WorldVars || t == MsgType.HingeStates;
+ || t == MsgType.TrafficStates || t == MsgType.WorldVars || t == MsgType.HingeStates
+ || t == MsgType.VehicleControls;
}
void OnMessage(int peerId, Message m)
@@ -1366,6 +1380,9 @@ namespace MSCO
case MsgType.HingeStates:
hinges.Handle(r);
break;
+ case MsgType.VehicleControls:
+ controls.Handle(r, objects, localId);
+ break;
case MsgType.TrafficStates:
traffic.Handle(r, IsWorldAuthority);
break;
@@ -1486,7 +1503,7 @@ namespace MSCO
passenger.Reset();
localPlayer = null;
objects.OnGameLoaded();
- vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); hinges.Reset();
+ vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); hinges.Reset(); controls.Reset();
vehicleFx.Reset(); wheelSpin.Reset();
worldSync.Reset();
moneySync.Reset();
@@ -1610,6 +1627,9 @@ namespace MSCO
// Unfallschäden (verbeulte Karosserie)
try { if (objects.ScanComplete) damageSync.Update(objects, localId, SendGame); }
catch (System.Exception e) { ReportError("crash damage sync", e); }
+ // Zündung, Motor, Licht, Choke ... der Fahrzeuge
+ try { if (objects.ScanComplete && localPlayer != null) controls.Update(objects, localId, localPlayer.position, SendGame); }
+ catch (System.Exception e) { ReportError("vehicle controls sync", e); }
// Türen, Motorhaube, Kofferraum der Fahrzeuge
try { if (objects.ScanComplete && localPlayer != null) hinges.Update(objects, localId, localPlayer.position, Time.deltaTime, SendGame); }
catch (System.Exception e) { ReportError("car door sync", e); }
diff --git a/NetSession.cs b/NetSession.cs
index 5a2560c..19ec529 100644
--- a/NetSession.cs
+++ b/NetSession.cs
@@ -50,7 +50,8 @@ namespace MSCO
VehicleDamage = 39, // Unfallschaden: int Mesh-Schlüssel, int Eckpunkte gesamt, int n, je [int Index, 3 float]
TrafficStates = 40, // Verkehr (nur Welt-Autorität): int n, je [int Schlüssel, 3 float Pos, 4 float Rot, 3 float Geschw.]
WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ (2 int, 3 bool), Wert]
- HingeStates = 42 // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, 3 float Pos, 4 float Rot relativ zum Auto]
+ HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, 3 float Pos, 4 float Rot relativ zum Auto]
+ VehicleControls = 43 // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
}
public struct Message
diff --git a/VehicleFx.cs b/VehicleFx.cs
index e6ea003..ec9030b 100644
--- a/VehicleFx.cs
+++ b/VehicleFx.cs
@@ -46,6 +46,7 @@ namespace MSCO
class Snapshot
{
public float ReceivedAt;
+ public bool Released; // nach dem Timeout einmal freigegeben
public readonly Dictionary Lights = new Dictionary();
public readonly Dictionary Audio = new Dictionary();
public readonly Dictionary Dials = new Dictionary();
@@ -161,11 +162,17 @@ namespace MSCO
{
foreach (KeyValuePair kv in incoming)
{
- if (Time.time - kv.Value.ReceivedAt > ApplyTimeout) continue;
SyncedBody b = objects.Get(kv.Key);
if (b == null || objects.IsLocalBody(b)) continue; // eigene Fahrzeuge simulieren wir selbst
Components c = GetComponents(b);
if (c == null) continue;
+ if (Time.time - kv.Value.ReceivedAt > ApplyTimeout)
+ {
+ // Keine Daten mehr (Fahrer ausgestiegen o.ä.): einmal alles loslassen, was wir erzwungen haben -
+ // sonst läuft z.B. das Anlasser-Geräusch endlos weiter ("orgelt")
+ if (!kv.Value.Released) Release(kv.Value, c);
+ continue;
+ }
foreach (KeyValuePair l in kv.Value.Lights)
{
@@ -192,6 +199,26 @@ namespace MSCO
}
}
+ static void Release(Snapshot s, Components c)
+ {
+ s.Released = true;
+ foreach (KeyValuePair a in s.Audio)
+ {
+ AudioSource src;
+ if (a.Value.Playing && c.Audio.TryGetValue(a.Key, out src) && src != null && src.isPlaying) src.Stop();
+ }
+ foreach (KeyValuePair p in s.Particles)
+ {
+ ParticleSystem ps;
+ if (p.Value && c.Particles.TryGetValue(p.Key, out ps) && ps != null && ps.isPlaying) ps.Stop();
+ }
+ foreach (KeyValuePair l in s.Lights)
+ {
+ Light light;
+ if (l.Value && c.Lights.TryGetValue(l.Key, out light) && light != null) light.enabled = false;
+ }
+ }
+
static void SetLight(Light light, bool on, Transform root)
{
if (!on)