Server gets the current save game from a player in the game (on join and every few minutes) instead of replaying actions; vehicle fire/smoke synced, also for parked cars
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,11 @@ Server und Mod müssen beide 2.7 sein (Server: `git pull && docker compose up -d
|
||||
- Admin-ITEMS: Werkzeuge und Laden-Artikel (Ratschenkasten, Batterie, Motoröl, Kühlmittel, Bremsflüssigkeit ...)
|
||||
- Nach dem Trennen (Kick, Ban, Shutdown, Timeout ...) zeigt das Hauptmenü den Grund
|
||||
- `reset-world` / `reload-save` auf dem Server: alle Spieler bekommen eine Meldung und verbinden sich automatisch neu
|
||||
- **Server holt sich den aktuellen Spielstand vom Spiel** (beim Beitreten und alle 5 Minuten): neue Spieler bekommen
|
||||
exakt die aktuelle Welt – Schrauben fest, Teile dran – statt nachgespielter Aktionen; Backups der letzten 5 Stände
|
||||
- Feuer, Rauch, Dampf und Funken an Fahrzeugen werden synchronisiert, auch bei geparkten Autos
|
||||
- Kick-, Ban-, Reset- und Shutdown-Meldungen kommen zuverlässig an (Server trennt sauber)
|
||||
- Status-Punkt gelb, bis man wirklich verbunden ist
|
||||
|
||||
**Behoben**
|
||||
- Auto fiel beim Fahren eines anderen Spielers auseinander (angebaute Teile rissen an ihren Gelenken ab)
|
||||
|
||||
+46
-1
@@ -1134,6 +1134,11 @@ namespace MSCO
|
||||
break;
|
||||
}
|
||||
|
||||
case MsgType.SaveRequest:
|
||||
// Server möchte den aktuellen Spielstand (für neue Spieler / zum Sichern)
|
||||
StartCoroutine(UploadSave(r.ReadInt32()));
|
||||
break;
|
||||
|
||||
case MsgType.Reconnect:
|
||||
{
|
||||
// Server hat die Welt zurückgesetzt o.ä.: Meldung zeigen, ins Menü, dann automatisch neu verbinden
|
||||
@@ -1445,7 +1450,7 @@ namespace MSCO
|
||||
}
|
||||
try { objects.Update(localPlayer, Time.deltaTime); }
|
||||
catch (System.Exception e) { ReportError("object sync", e); }
|
||||
try { vehicleFx.Update(objects, SendGame, Time.deltaTime); }
|
||||
try { vehicleFx.Update(objects, SendGame, Time.deltaTime, IsWorldAuthority); }
|
||||
catch (System.Exception e) { ReportError("vehicle lights/sound", e); }
|
||||
try { worldSync.Update(IsWorldAuthority, SendGame, Time.unscaledDeltaTime); }
|
||||
catch (System.Exception e) { ReportError("time/weather sync", e); }
|
||||
@@ -1627,6 +1632,46 @@ namespace MSCO
|
||||
public Quaternion Rotation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Im Spiel speichern (wie auf der Toilette) und den Spielstand an den Server schicken. So bekommen neue Spieler
|
||||
/// genau die aktuelle Welt - Schrauben, Teile, alles - statt eines alten Stands mit nachgespielten Aktionen.
|
||||
/// Der eigene Spielstand ist davon nicht betroffen: er liegt gesichert im Backup und kommt im Hauptmenü zurück.
|
||||
/// </summary>
|
||||
System.Collections.IEnumerator UploadSave(int requestId)
|
||||
{
|
||||
if (!MenuHelper.InGame || !dedicated || session == null) yield break;
|
||||
PlayMakerFSM.BroadcastEvent("SAVEGAME");
|
||||
float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames
|
||||
while (Time.realtimeSinceStartup < until) yield return null;
|
||||
if (session == null) yield break;
|
||||
|
||||
List<KeyValuePair<string, byte[]>> files;
|
||||
try { files = SaveSync.ReadLocalSave(); }
|
||||
catch (System.Exception e) { MscoLog.Warning("Could not read the save game for the server: " + e.Message); yield break; }
|
||||
|
||||
const int Chunk = 256 * 1024; // unter der Paketgrenze des Servers
|
||||
foreach (KeyValuePair<string, byte[]> f in files)
|
||||
{
|
||||
for (int offset = 0; offset < f.Value.Length || offset == 0; offset += Chunk)
|
||||
{
|
||||
if (session == null) yield break;
|
||||
int start = offset, length = Mathf.Min(Chunk, f.Value.Length - offset);
|
||||
string name = f.Key;
|
||||
byte[] data = f.Value;
|
||||
session.SendTo(NetSession.HostId, MsgType.SaveUploadPart, NetSession.Build(w =>
|
||||
{
|
||||
w.Write(requestId); w.Write(name); w.Write(data.Length); w.Write(start); w.Write(length);
|
||||
w.Write(data, start, length);
|
||||
}));
|
||||
yield return null;
|
||||
if (data.Length == 0) break;
|
||||
}
|
||||
}
|
||||
int count = files.Count;
|
||||
session.SendTo(NetSession.HostId, MsgType.SaveUploadDone, NetSession.Build(w => { w.Write(requestId); w.Write(count); }));
|
||||
MscoLog.Info("Sent the current save game to the server (" + count + " files)");
|
||||
}
|
||||
|
||||
/// <summary>Sobald die erste Position des Hosts da ist: seitlich neben ihn stellen.</summary>
|
||||
void TryTeleportToHost()
|
||||
{
|
||||
|
||||
+4
-1
@@ -41,7 +41,10 @@ namespace MSCO
|
||||
Teleport = 30, // Admin holt einen Spieler zu sich: 3 float Position
|
||||
Pong = 31, // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
|
||||
SpawnItem = 32, // Admin spawnt ein Item: string Item-Name, 3 float Position, 4 float Drehung
|
||||
Reconnect = 33 // Server -> Client: string Meldung; ins Menü und neu verbinden (z.B. Welt zurückgesetzt)
|
||||
Reconnect = 33, // Server -> Client: string Meldung; ins Menü und neu verbinden (z.B. Welt zurückgesetzt)
|
||||
SaveRequest = 34, // Server -> Client: int Anfrage; im Spiel speichern und den Spielstand hochladen
|
||||
SaveUploadPart = 35, // Client -> Server: int Anfrage, string Datei, int Gesamtgröße, int Offset, int Länge, bytes
|
||||
SaveUploadDone = 36 // Client -> Server: int Anfrage, int Anzahl Dateien
|
||||
}
|
||||
|
||||
public struct Message
|
||||
|
||||
@@ -168,6 +168,17 @@ namespace MSCO
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Fahrzeuge, die gerade niemand besitzt (geparkt).</summary>
|
||||
public List<SyncedBody> FreeVehicles()
|
||||
{
|
||||
var list = new List<SyncedBody>();
|
||||
foreach (SyncedBody b in byId.Values)
|
||||
if (b.IsVehicle && b.OwnerId == SyncedBody.NoOwner && b.Rb != null) list.Add(b);
|
||||
return list;
|
||||
}
|
||||
|
||||
public bool IsLocalBody(SyncedBody b) { return IsLocal(b); }
|
||||
|
||||
/// <summary>Fahrzeuge, die der lokale Spieler gerade besitzt (fährt/schiebt).</summary>
|
||||
public List<SyncedBody> LocalVehicles()
|
||||
{
|
||||
|
||||
+44
-3
@@ -40,6 +40,7 @@ namespace MSCO
|
||||
public readonly Dictionary<int, Light> Lights = new Dictionary<int, Light>();
|
||||
public readonly Dictionary<int, AudioSource> Audio = new Dictionary<int, AudioSource>();
|
||||
public readonly Dictionary<int, Transform> Dials = new Dictionary<int, Transform>();
|
||||
public readonly Dictionary<int, ParticleSystem> Particles = new Dictionary<int, ParticleSystem>();
|
||||
}
|
||||
|
||||
class Snapshot
|
||||
@@ -48,6 +49,7 @@ namespace MSCO
|
||||
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>();
|
||||
public readonly Dictionary<int, bool> Particles = new Dictionary<int, bool>();
|
||||
}
|
||||
|
||||
readonly Dictionary<int, Components> components = new Dictionary<int, Components>();
|
||||
@@ -62,13 +64,17 @@ namespace MSCO
|
||||
|
||||
// ---------------------------------------------------------------- Senden
|
||||
|
||||
public void Update(ObjectSync objects, Action<MsgType, byte[]> send, float dt)
|
||||
public void Update(ObjectSync objects, Action<MsgType, byte[]> send, float dt, bool worldAuthority)
|
||||
{
|
||||
sendTimer += dt;
|
||||
if (sendTimer < SendInterval) return;
|
||||
sendTimer = 0f;
|
||||
|
||||
foreach (SyncedBody b in objects.LocalVehicles())
|
||||
// Eigene Fahrzeuge (fahren/schieben) und - wenn wir die Welt vorgeben - auch freie, geparkte Fahrzeuge:
|
||||
// sonst würde z.B. ein brennendes, abgestelltes Auto nur bei einem Spieler brennen
|
||||
List<SyncedBody> vehicles = objects.LocalVehicles();
|
||||
if (worldAuthority) vehicles.AddRange(objects.FreeVehicles());
|
||||
foreach (SyncedBody b in vehicles)
|
||||
{
|
||||
Components c = GetComponents(b);
|
||||
if (c == null) continue;
|
||||
@@ -104,6 +110,16 @@ namespace MSCO
|
||||
w.Write(d.Key);
|
||||
w.Write(q.x); w.Write(q.y); w.Write(q.z); w.Write(q.w);
|
||||
}
|
||||
// Partikel-Effekte (Feuer, Rauch, Dampf, Funken): läuft / läuft nicht
|
||||
int particleCount = 0;
|
||||
foreach (ParticleSystem ps in c.Particles.Values) if (ps != null) particleCount++;
|
||||
w.Write(particleCount);
|
||||
foreach (KeyValuePair<int, ParticleSystem> ps in c.Particles)
|
||||
{
|
||||
if (ps.Value == null) continue;
|
||||
w.Write(ps.Key);
|
||||
w.Write(ps.Value.gameObject.activeInHierarchy && ps.Value.isPlaying);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -132,6 +148,11 @@ namespace MSCO
|
||||
if (!float.IsNaN(q.x) && !float.IsNaN(q.w)) s.Dials[key] = q;
|
||||
}
|
||||
}
|
||||
if (r.BaseStream.Length - r.BaseStream.Position >= 4)
|
||||
{
|
||||
int particles = r.ReadInt32();
|
||||
for (int i = 0; i < particles; i++) s.Particles[r.ReadInt32()] = r.ReadBoolean();
|
||||
}
|
||||
incoming[id] = s;
|
||||
}
|
||||
|
||||
@@ -142,7 +163,7 @@ namespace MSCO
|
||||
{
|
||||
if (Time.time - kv.Value.ReceivedAt > ApplyTimeout) continue;
|
||||
SyncedBody b = objects.Get(kv.Key);
|
||||
if (b == null || !objects.IsRemoteOwnedBody(b)) continue;
|
||||
if (b == null || objects.IsLocalBody(b)) continue; // eigene Fahrzeuge simulieren wir selbst
|
||||
Components c = GetComponents(b);
|
||||
if (c == null) continue;
|
||||
|
||||
@@ -156,6 +177,11 @@ namespace MSCO
|
||||
AudioSource src;
|
||||
if (c.Audio.TryGetValue(a.Key, out src) && src != null) SetAudio(src, a.Value);
|
||||
}
|
||||
foreach (KeyValuePair<int, bool> p in kv.Value.Particles)
|
||||
{
|
||||
ParticleSystem ps;
|
||||
if (c.Particles.TryGetValue(p.Key, out ps) && ps != null) SetParticles(ps, p.Value, c.Root);
|
||||
}
|
||||
float t = Mathf.Clamp01(Time.deltaTime * DialSmoothing);
|
||||
foreach (KeyValuePair<int, Quaternion> d in kv.Value.Dials)
|
||||
{
|
||||
@@ -179,6 +205,18 @@ namespace MSCO
|
||||
if (!t.gameObject.activeSelf) t.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
static void SetParticles(ParticleSystem ps, bool playing, Transform root)
|
||||
{
|
||||
if (playing)
|
||||
{
|
||||
// Effekte hängen oft an einem abgeschalteten Objekt (z.B. "Fire"), das die Spiel-Logik aktiviert
|
||||
for (Transform t = ps.transform; t != null && t != root; t = t.parent)
|
||||
if (!t.gameObject.activeSelf) t.gameObject.SetActive(true);
|
||||
if (!ps.isPlaying) ps.Play();
|
||||
}
|
||||
else if (ps.isPlaying) ps.Stop();
|
||||
}
|
||||
|
||||
static void SetAudio(AudioSource src, AudioState state)
|
||||
{
|
||||
if (!src.gameObject.activeInHierarchy) return;
|
||||
@@ -206,6 +244,9 @@ namespace MSCO
|
||||
seen.Clear();
|
||||
foreach (Transform t in b.Rb.GetComponentsInChildren<Transform>(true))
|
||||
if (IsDial(t.name)) c.Dials[Key(t, c.Root, seen)] = t;
|
||||
seen.Clear();
|
||||
foreach (ParticleSystem ps in b.Rb.GetComponentsInChildren<ParticleSystem>(true))
|
||||
c.Particles[Key(ps.transform, c.Root, seen)] = ps;
|
||||
components[b.Id] = c;
|
||||
return c;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user