0
0
Fork 0
mirror of https://github.com/Sanae6/SmoOnlineServer.git synced 2024-11-22 03:05:16 +00:00

fix: synchronization issues

- Send empty `TagPacket` and `CapturePacket` on new connections, to reset old data back that other players might still have in their puppet from an
earlier connection.
- Cache and send `CostumePacket`, `CapturePacket`, `TagPacket`, `GamePacket` and `PlayerPacket` to (re-)connecting players.
- Clear Metadata cache for existing clients that connect fresh (after a game restart).
This commit is contained in:
Robin C. Ladiges 2023-06-28 17:12:49 +02:00 committed by Sanae
parent 1e9d334d6f
commit 86c79177fd
3 changed files with 127 additions and 40 deletions

View file

@ -76,6 +76,17 @@ public class Client : IDisposable {
await Socket!.SendAsync(data[..(Constants.HeaderSize + header.PacketSize)], SocketFlags.None); await Socket!.SendAsync(data[..(Constants.HeaderSize + header.PacketSize)], SocketFlags.None);
} }
public void CleanMetadataOnNewConnection() {
object? tmp;
Metadata.TryRemove("time", out tmp);
Metadata.TryRemove("seeking", out tmp);
Metadata.TryRemove("lastCostumePacket", out tmp);
Metadata.TryRemove("lastCapturePacket", out tmp);
Metadata.TryRemove("lastTagPacket", out tmp);
Metadata.TryRemove("lastGamePacket", out tmp);
Metadata.TryRemove("lastPlayerPacket", out tmp);
}
public static bool operator ==(Client? left, Client? right) { public static bool operator ==(Client? left, Client? right) {
return left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id; return left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id;
} }

View file

@ -72,13 +72,6 @@ server.ClientJoined += (c, _) => {
c.Metadata["scenario"] = (byte?) 0; c.Metadata["scenario"] = (byte?) 0;
c.Metadata["2d"] = false; c.Metadata["2d"] = false;
c.Metadata["speedrun"] = false; c.Metadata["speedrun"] = false;
foreach (Client client in server.ClientsConnected) {
try {
c.Send((GamePacket) client.Metadata["lastGamePacket"]!, client).Wait();
} catch {
// lol who gives a fuck
}
}
}; };
async Task ClientSyncShineBag(Client client) { async Task ClientSyncShineBag(Client client) {
@ -115,13 +108,36 @@ timer.Start();
float MarioSize(bool is2d) => is2d ? 180 : 160; float MarioSize(bool is2d) => is2d ? 180 : 160;
void flipPlayer(Client c, ref PlayerPacket pp) {
pp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!);
pp.Rotation *= (
Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI))
* Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI))
);
};
void logError(Task x) {
if (x.Exception != null) {
consoleLogger.Error(x.Exception.ToString());
}
};
server.PacketHandler = (c, p) => { server.PacketHandler = (c, p) => {
switch (p) { switch (p) {
case GamePacket gamePacket: { case GamePacket gamePacket: {
c.Logger.Info($"Got game packet {gamePacket.Stage}->{gamePacket.ScenarioNum}"); c.Logger.Info($"Got game packet {gamePacket.Stage}->{gamePacket.ScenarioNum}");
// reset lastPlayerPacket on stage changes
object? old = null;
c.Metadata.TryGetValue("lastGamePacket", out old);
if (old != null && ((GamePacket) old).Stage != gamePacket.Stage) {
c.Metadata["lastPlayerPacket"] = null;
}
c.Metadata["scenario"] = gamePacket.ScenarioNum; c.Metadata["scenario"] = gamePacket.ScenarioNum;
c.Metadata["2d"] = gamePacket.Is2d; c.Metadata["2d"] = gamePacket.Is2d;
c.Metadata["lastGamePacket"] = gamePacket; c.Metadata["lastGamePacket"] = gamePacket;
switch (gamePacket.Stage) { switch (gamePacket.Stage) {
case "CapWorldHomeStage" when gamePacket.ScenarioNum == 0: case "CapWorldHomeStage" when gamePacket.ScenarioNum == 0:
c.Metadata["speedrun"] = true; c.Metadata["speedrun"] = true;
@ -145,8 +161,7 @@ server.PacketHandler = (c, p) => {
server.BroadcastReplace(gamePacket, c, (from, to, gp) => { server.BroadcastReplace(gamePacket, c, (from, to, gp) => {
gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200; gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200;
#pragma warning disable CS4014 #pragma warning disable CS4014
to.Send(gp, from) to.Send(gp, from).ContinueWith(logError);
.ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014 #pragma warning restore CS4014
}); });
return false; return false;
@ -156,14 +171,23 @@ server.PacketHandler = (c, p) => {
} }
case TagPacket tagPacket: { case TagPacket tagPacket: {
// c.Logger.Info($"Got tag packet: {tagPacket.IsIt}");
c.Metadata["lastTagPacket"] = tagPacket;
if ((tagPacket.UpdateType & TagPacket.TagUpdate.State) != 0) c.Metadata["seeking"] = tagPacket.IsIt; if ((tagPacket.UpdateType & TagPacket.TagUpdate.State) != 0) c.Metadata["seeking"] = tagPacket.IsIt;
if ((tagPacket.UpdateType & TagPacket.TagUpdate.Time) != 0) if ((tagPacket.UpdateType & TagPacket.TagUpdate.Time) != 0)
c.Metadata["time"] = new Time(tagPacket.Minutes, tagPacket.Seconds, DateTime.Now); c.Metadata["time"] = new Time(tagPacket.Minutes, tagPacket.Seconds, DateTime.Now);
break; break;
} }
case CapturePacket capturePacket: {
// c.Logger.Info($"Got capture packet: {capturePacket.ModelName}");
c.Metadata["lastCapturePacket"] = capturePacket;
break;
}
case CostumePacket costumePacket: case CostumePacket costumePacket:
c.Logger.Info($"Got costume packet: {costumePacket.BodyName}, {costumePacket.CapName}"); c.Logger.Info($"Got costume packet: {costumePacket.BodyName}, {costumePacket.CapName}");
c.Metadata["lastCostumePacket"] = costumePacket;
c.CurrentCostume = costumePacket; c.CurrentCostume = costumePacket;
#pragma warning disable CS4014 #pragma warning disable CS4014
ClientSyncShineBag(c); //no point logging since entire def has try/catch ClientSyncShineBag(c); //no point logging since entire def has try/catch
@ -183,34 +207,36 @@ server.PacketHandler = (c, p) => {
break; break;
} }
case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled case PlayerPacket playerPacket: {
c.Metadata["lastPlayerPacket"] = playerPacket;
// flip for all
if ( Settings.Instance.Flip.Enabled
&& Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others
&& Settings.Instance.Flip.Players.Contains(c.Id): { && Settings.Instance.Flip.Players.Contains(c.Id)
playerPacket.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!); ) {
playerPacket.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) flipPlayer(c, ref playerPacket);
* Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
#pragma warning disable CS4014 #pragma warning disable CS4014
server.Broadcast(playerPacket, c) server.Broadcast(playerPacket, c).ContinueWith(logError);
.ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014 #pragma warning restore CS4014
return false; return false;
} }
case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled // flip only for specific clients
if ( Settings.Instance.Flip.Enabled
&& Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self
&& !Settings.Instance.Flip.Players.Contains(c.Id): { && !Settings.Instance.Flip.Players.Contains(c.Id)
) {
server.BroadcastReplace(playerPacket, c, (from, to, sp) => { server.BroadcastReplace(playerPacket, c, (from, to, sp) => {
if (Settings.Instance.Flip.Players.Contains(to.Id)) { if (Settings.Instance.Flip.Players.Contains(to.Id)) {
sp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!); flipPlayer(c, ref sp);
sp.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI))
* Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
} }
#pragma warning disable CS4014 #pragma warning disable CS4014
to.Send(sp, from) to.Send(sp, from).ContinueWith(logError);
.ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014 #pragma warning restore CS4014
}); });
return false; return false;
} }
break;
}
} }
return true; // Broadcast packet to all other clients return true; // Broadcast packet to all other clients
@ -651,7 +677,7 @@ Task.Run(() => {
} }
} }
} }
}).ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } }); }).ContinueWith(logError);
#pragma warning restore CS4014 #pragma warning restore CS4014
await server.Listen(cts.Token); await server.Listen(cts.Token);

View file

@ -174,6 +174,8 @@ public class Server {
ConnectPacket connect = new ConnectPacket(); ConnectPacket connect = new ConnectPacket();
connect.Deserialize(memory.Memory.Span[packetRange]); connect.Deserialize(memory.Memory.Span[packetRange]);
bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection;
lock (Clients) { lock (Clients) {
if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) { if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) {
client.Logger.Error($"Turned away as server is at max clients"); client.Logger.Error($"Turned away as server is at max clients");
@ -218,25 +220,33 @@ public class Server {
// done disconnecting and removing stale clients with the same id // done disconnecting and removing stale clients with the same id
ClientJoined?.Invoke(client, connect); ClientJoined?.Invoke(client, connect);
// a new connection, not a reconnect, for an existing client
} else if (wasFirst) {
client.CleanMetadataOnNewConnection();
} }
} }
// for all other clients that are already connected
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null); List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null);
await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => {
IMemoryOwner<byte> tempBuffer = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size)); IMemoryOwner<byte> tempBuffer = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size));
// make the other client known to the (new) client
PacketHeader connectHeader = new PacketHeader { PacketHeader connectHeader = new PacketHeader {
Id = other.Id, Id = other.Id,
Type = PacketType.Connect, Type = PacketType.Connect,
PacketSize = connect.Size PacketSize = connect.Size,
}; };
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]); connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
ConnectPacket connectPacket = new ConnectPacket { ConnectPacket connectPacket = new ConnectPacket {
ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is
MaxPlayers = Settings.Instance.Server.MaxPlayers, MaxPlayers = Settings.Instance.Server.MaxPlayers,
ClientName = other.Name ClientName = other.Name,
}; };
connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]); connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]);
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null); await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null);
// tell the (new) client what costume the other client has
if (other.CurrentCostume.HasValue) { if (other.CurrentCostume.HasValue) {
connectHeader.Type = PacketType.Costume; connectHeader.Type = PacketType.Costume;
connectHeader.PacketSize = other.CurrentCostume.Value.Size; connectHeader.PacketSize = other.CurrentCostume.Value.Size;
@ -246,9 +256,17 @@ public class Server {
} }
tempBuffer.Dispose(); tempBuffer.Dispose();
// make the other client reset their puppet cache for this client, if it is a new connection (after restart)
if (wasFirst) {
await SendEmptyPackets(client, other);
}
}); });
Logger.Info($"Client {client.Name} ({client.Id}/{remote}) connected."); Logger.Info($"Client {client.Name} ({client.Id}/{remote}) connected.");
// send missing or outdated packets from others to the new client
await ResendPackets(client);
} else if (header.Id != client.Id && client.Id != Guid.Empty) { } else if (header.Id != client.Id && client.Id != Guid.Empty) {
throw new Exception($"Client {client.Name} sent packet with invalid client id {header.Id} instead of {client.Id}"); throw new Exception($"Client {client.Name} sent packet with invalid client id {header.Id} instead of {client.Id}");
} }
@ -310,6 +328,38 @@ public class Server {
#pragma warning restore CS4014 #pragma warning restore CS4014
} }
private async Task ResendPackets(Client client) {
async Task trySend<T>(Client other, string packetType) where T : struct, IPacket {
if (! other.Metadata.ContainsKey(packetType)) { return; }
try {
await client.Send((T) other.Metadata[packetType]!, other);
}
catch {
// lol who gives a fuck
}
};
await Parallel.ForEachAsync(this.ClientsConnected, async (other, _) => {
if (client.Id == other.Id) { return; }
await trySend<CostumePacket>(other, "lastCostumePacket");
await trySend<CapturePacket>(other, "lastCapturePacket");
await trySend<TagPacket>(other, "lastTagPacket");
await trySend<GamePacket>(other, "lastGamePacket");
await trySend<PlayerPacket>(other, "lastPlayerPacket");
});
}
private async Task SendEmptyPackets(Client client, Client other) {
await other.Send(new TagPacket {
UpdateType = TagPacket.TagUpdate.State | TagPacket.TagUpdate.Time,
IsIt = false,
Seconds = 0,
Minutes = 0,
}, client);
await other.Send(new CapturePacket {
ModelName = "",
}, client);
}
private static PacketHeader GetHeader(Span<byte> data) { private static PacketHeader GetHeader(Span<byte> data) {
//no need to error check, the client will disconnect when the packet is invalid :) //no need to error check, the client will disconnect when the packet is invalid :)
PacketHeader header = new PacketHeader(); PacketHeader header = new PacketHeader();