diff --git a/GameServer.cs b/GameServer.cs index 3c3cc73..dd1d65c 100644 --- a/GameServer.cs +++ b/GameServer.cs @@ -30,6 +30,12 @@ public sealed class GameServer /// SteamID von Steam bestätigt (Ticket per Web API geprüft). Nur dann gibt es Admin-Rechte. public bool SteamVerified; public bool IsAdmin; + /// + /// Ab wann die Position des Spielers gespeichert wird: erst nachdem er seinen Weltzustand (mit seiner letzten + /// Position) bekommen und sich dorthin gesetzt hat - sonst überschreibt der Startpunkt des Spielstands + /// (Toilette) die gespeicherte Position, bevor er sie zurückbekommt. + /// + public DateTime TrackPositionFrom = DateTime.MaxValue; public Task Verification; } @@ -181,6 +187,7 @@ public sealed class GameServer Log($"{c.Name} left ({c.Conn.CloseReason})"); Broadcast(MsgType.PlayerLeft, Wire.Build(w => w.Write(c.Id))); HeartbeatSoon(); + if (world.Dirty) SaveWorld(); // letzte Position gleich sichern, nicht erst beim nächsten Autosave } } @@ -268,6 +275,7 @@ public sealed class GameServer case MsgType.Ready: if (c.State != ClientState.Joined) return; + c.TrackPositionFrom = DateTime.UtcNow.AddSeconds(8); c.Conn.Send(MsgType.WorldSnapshot, world.BuildSnapshot(c.SteamId)); // Letzter Stand von Uhrzeit und Wetter, damit der Neue nicht bis zum nächsten Update warten muss if (lastWorldTime != null) c.Conn.Send(MsgType.WorldTime, Wire.WithSender(ServerId, lastWorldTime)); @@ -325,7 +333,9 @@ public sealed class GameServer case MsgType.ObjStates: world.ApplyObjStates(m.Payload); break; case MsgType.ObjRelease: world.ApplyObjRelease(m.Payload); break; case MsgType.FsmEvent: world.AddFsmEvent(m.Payload); break; - case MsgType.Player: world.ApplyPlayer(c.SteamId, m.Payload); break; + case MsgType.Player: + if (DateTime.UtcNow >= c.TrackPositionFrom) world.ApplyPlayer(c.SteamId, m.Payload); + break; case MsgType.MoneyDelta: world.ApplyMoney(m.Payload); break; case MsgType.DoorState: world.ApplyDoor(m.Payload); break; case MsgType.VarSync: world.ApplyVars(m.Payload); break; diff --git a/Tests/Program.cs b/Tests/Program.cs index a3f9266..c357024 100644 --- a/Tests/Program.cs +++ b/Tests/Program.cs @@ -115,6 +115,26 @@ try alice.Send(MsgType.Chat, Wire.Build(w => w.Write(new string('x', 5000)))); var longChat = await bob.WaitFor(MsgType.Chat, 800); Check(longChat == null, "Huge chat messages are not relayed"); + + Console.WriteLine("== Last position (spawn)"); + var pos1 = await TestClient.Join("Walker", 0, null); + pos1.Send(MsgType.Player, PlayerPos(-15.6f, -0.3f, 7f)); // Startpunkt des Spielstands (Toilette) - vor Ready + pos1.Send(MsgType.Ready, Array.Empty()); + Check(Snapshot.Parse(await pos1.WaitFor(MsgType.WorldSnapshot)).Spawn == null, "Position before joining is not stored (no spawn yet)"); + pos1.Send(MsgType.Player, PlayerPos(-15.6f, -0.3f, 7f)); // direkt nach Ready: noch nicht übernehmen + await Task.Delay(8500); + pos1.Send(MsgType.Player, PlayerPos(250f, 3f, -120f)); // echte Position nach dem Umsetzen + await Task.Delay(300); + pos1.Send(MsgType.Player, PlayerPos(2.7e9f, 9.4e10f, -8.5e8f)); // weggeschleudert - darf nicht gespeichert werden + await Task.Delay(300); + pos1.Close(); + await Task.Delay(500); + var pos2 = await TestClient.Join("Walker", 0, null); + pos2.Send(MsgType.Ready, Array.Empty()); + var spawn = Snapshot.Parse(await pos2.WaitFor(MsgType.WorldSnapshot)).Spawn; + Check(spawn != null && Math.Abs(spawn[0] - 250f) < 0.01f && Math.Abs(spawn[2] + 120f) < 0.01f, + "Rejoining returns the last real position, not the save's start point or a flung-away one: " + (spawn == null ? "none" : string.Join(", ", spawn))); + pos2.Close(); } catch (Exception e) { @@ -128,6 +148,7 @@ Thread.Sleep(300); try { Directory.Delete(dir, true); } catch { } return failed == 0 ? 0 : 1; +static byte[] PlayerPos(float x, float y, float z) => Wire.Build(w => { w.Write(x); w.Write(y); w.Write(z); w.Write(0f); }); static string ReadString(byte[] p) => new BinaryReader(new MemoryStream(p)).ReadString(); static string ReadStringAfterSender(byte[] p) { var r = new BinaryReader(new MemoryStream(p)); r.ReadInt32(); return r.ReadString(); } @@ -147,6 +168,7 @@ sealed class Snapshot public bool Ok; public string Info; public HashSet ObjectIds = new(); + public float[] Spawn; public int Vars = -1; public static Snapshot Parse(byte[] data) @@ -156,7 +178,7 @@ sealed class Snapshot try { var r = new BinaryReader(new MemoryStream(data)); - if (r.ReadBoolean()) { r.ReadSingle(); r.ReadSingle(); r.ReadSingle(); } + if (r.ReadBoolean()) s.Spawn = new[] { r.ReadSingle(), r.ReadSingle(), r.ReadSingle() }; int objects = r.ReadInt32(); for (int i = 0; i < objects; i++) { s.ObjectIds.Add(r.ReadInt32()); for (int k = 0; k < 7; k++) r.ReadSingle(); } int events = r.ReadInt32(); diff --git a/WorldState.cs b/WorldState.cs index c281827..895e130 100644 --- a/WorldState.cs +++ b/WorldState.cs @@ -157,10 +157,10 @@ public sealed class WorldState public void ApplyPlayer(string steamId, byte[] payload) { if (string.IsNullOrEmpty(steamId) || payload.Length < 12) return; - PlayerPositions[steamId] = new[] - { - BitConverter.ToSingle(payload, 0), BitConverter.ToSingle(payload, 4), BitConverter.ToSingle(payload, 8) - }; + var pos = new[] { BitConverter.ToSingle(payload, 0), BitConverter.ToSingle(payload, 4), BitConverter.ToSingle(payload, 8) }; + // Unsinnige Positionen (z.B. mit einem explodierten Auto weggeschleudert) nicht speichern + if (!IsValidPose(pos, new float[] { 0, 0, 0, 1 })) return; + PlayerPositions[steamId] = pos; Dirty = true; }