Sync vehicle controls (ignition, engine, lights ...), stop forced starter sound, auto-reconnect after server restart or lost connection
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+7
-2
@@ -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
|
||||
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using HutongGames.PlayMaker;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MSCO
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<int, Control> controls = new Dictionary<int, Control>();
|
||||
readonly HashSet<PlayMakerFSM> scanned = new HashSet<PlayMakerFSM>();
|
||||
readonly List<Control> outgoing = new List<Control>();
|
||||
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<MsgType, byte[]> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@
|
||||
<Compile Include="WorldVarSync.cs" />
|
||||
<Compile Include="TrafficSync.cs" />
|
||||
<Compile Include="HingeSync.cs" />
|
||||
<Compile Include="ControlSync.cs" />
|
||||
<Compile Include="MpCommand.cs" />
|
||||
<Compile Include="Commands.cs" />
|
||||
<Compile Include="MscoLog.cs" />
|
||||
|
||||
+24
-4
@@ -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<string> bannedNames = new HashSet<string>(); // 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); }
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+28
-1
@@ -46,6 +46,7 @@ namespace MSCO
|
||||
class Snapshot
|
||||
{
|
||||
public float ReceivedAt;
|
||||
public bool Released; // nach dem Timeout einmal freigegeben
|
||||
public readonly Dictionary<int, bool> Lights = new Dictionary<int, bool>();
|
||||
public readonly Dictionary<int, AudioState> Audio = new Dictionary<int, AudioState>();
|
||||
public readonly Dictionary<int, Quaternion> Dials = new Dictionary<int, Quaternion>();
|
||||
@@ -161,11 +162,17 @@ namespace MSCO
|
||||
{
|
||||
foreach (KeyValuePair<int, Snapshot> 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<int, bool> l in kv.Value.Lights)
|
||||
{
|
||||
@@ -192,6 +199,26 @@ namespace MSCO
|
||||
}
|
||||
}
|
||||
|
||||
static void Release(Snapshot s, Components c)
|
||||
{
|
||||
s.Released = true;
|
||||
foreach (KeyValuePair<int, AudioState> 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<int, bool> p in s.Particles)
|
||||
{
|
||||
ParticleSystem ps;
|
||||
if (p.Value && c.Particles.TryGetValue(p.Key, out ps) && ps != null && ps.isPlaying) ps.Stop();
|
||||
}
|
||||
foreach (KeyValuePair<int, bool> 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)
|
||||
|
||||
Reference in New Issue
Block a user