From 2f64c310392beef5556174cd4e9ae369c6c64d36 Mon Sep 17 00:00:00 2001 From: Enn3Developer Date: Wed, 12 Mar 2025 12:29:37 +0100 Subject: [PATCH 1/8] server implementation --- OpenPolytopia.Server/Exceptions.cs | 13 + OpenPolytopia.Server/GameServer.cs | 104 ------- OpenPolytopia.Server/Module.cs | 279 ++++++++++++++++++ .../OpenPolytopia.Server.csproj | 3 +- OpenPolytopia.Server/Program.cs | 9 - .../ReducerContextExtensions.cs | 41 +++ OpenPolytopia.sln | 14 +- OpenPolytopia.sln.DotSettings.user | 3 +- 8 files changed, 343 insertions(+), 123 deletions(-) create mode 100644 OpenPolytopia.Server/Exceptions.cs delete mode 100644 OpenPolytopia.Server/GameServer.cs create mode 100644 OpenPolytopia.Server/Module.cs delete mode 100644 OpenPolytopia.Server/Program.cs create mode 100644 OpenPolytopia.Server/ReducerContextExtensions.cs diff --git a/OpenPolytopia.Server/Exceptions.cs b/OpenPolytopia.Server/Exceptions.cs new file mode 100644 index 00000000..0f9cf522 --- /dev/null +++ b/OpenPolytopia.Server/Exceptions.cs @@ -0,0 +1,13 @@ +namespace OpenPolytopia.Server; + +public class UserNotRegisteredException() : Exception("User not registered"); + +public class LobbyNotFoundException() : Exception("Lobby not found"); + +public class LobbyAlreadyStartedException() : Exception("Lobby has already started a game"); + +public class AlreadyJoinedLobbyException() : Exception("You already joined this lobby"); + +public class LobbyFullException() : Exception("This lobby is full"); + +public class NotInLobbyException() : Exception("You must join a lobby first before leaving it"); diff --git a/OpenPolytopia.Server/GameServer.cs b/OpenPolytopia.Server/GameServer.cs deleted file mode 100644 index fd92be2d..00000000 --- a/OpenPolytopia.Server/GameServer.cs +++ /dev/null @@ -1,104 +0,0 @@ -namespace OpenPolytopia.Server; - -using System.Collections.Concurrent; -using System.Net.Sockets; -using Common; -using Common.Network; -using Common.Network.Packets; - -public class GameServer : IDisposable { - private readonly ServerConnection _server; - private readonly LobbyManager _lobbyManager = new(); - private readonly ConcurrentDictionary _playerNames = new(); - - public GameServer(int port) { - _server = new ServerConnection(port); - PacketRegistrar.RegisterAllPackets(); - _server.OnPacketReceived += ManagePacketAsync; - } - - private async Task ManagePacketAsync(uint id, IPacket packet, NetworkStream stream, List bytes) { - switch (packet) { - // Handshake packet - // Respond with the result of the check - case HandshakePacket handshakePacket: - await stream.WritePacketAsync(new HandshakeResponsePacket { Ok = handshakePacket.Version == "0.1.0" }, bytes); - break; - // Register the username - case RegisterUserPacket registerUserPacket: - foreach (var keyValue in _playerNames) { - if (keyValue.Value == registerUserPacket.Name) { - _playerNames.TryRemove(keyValue.Key, out _); - } - } - - _playerNames[id] = registerUserPacket.Name; - await stream.WritePacketAsync(new RegisterUserResponsePacket { Ok = true }, bytes); - break; - // Add a player to a lobby - // Respond with the result of the operation - case LobbyConnectPacket lobbyConnectPacket: - var ok = _lobbyManager.AddPlayer(lobbyConnectPacket.Id, _playerNames[id]); - if (ok) { - Broadcast(new LobbyUpdatePacket { Lobby = _lobbyManager[id]! }); - } - - await stream.WritePacketAsync(new LobbyConnectResponsePacket { Ok = ok }, bytes); - break; - // Create a new lobby - case CreateLobbyPacket createLobbyPacket: - var lobby = _lobbyManager.NewLobby(createLobbyPacket.MaxPlayers); - Broadcast(new LobbyUpdatePacket { Lobby = lobby }); - await stream.WritePacketAsync(new CreateLobbyResponsePacket { Ok = true, Id = lobby.Id }, bytes); - break; - // Respond with the lobbies currently available - case GetLobbiesPacket: - await stream.WritePacketAsync(new GetLobbiesResponsePacket { Lobbies = _lobbyManager.Lobbies }, bytes); - break; - // Disconnect from a lobby - case LobbyDisconnectPacket lobbyDisconnectPacket: - _lobbyManager.RemovePlayer(lobbyDisconnectPacket.Id, _playerNames[id]); - if (_lobbyManager[lobbyDisconnectPacket.Id] != null) { - var lobbyToDelete = _lobbyManager[lobbyDisconnectPacket.Id]!; - if (lobbyToDelete.Players.Count == 0) { - _lobbyManager.Lobbies.Remove(lobbyToDelete); - Broadcast(new LobbyDeletedPacket { Id = lobbyDisconnectPacket.Id }); - } - } - - await stream.WritePacketAsync(new LobbyDisconnectResponsePacket { Ok = true }, bytes); - break; - } - - return false; - } - - private void Broadcast(IPacket packet) { - foreach (var clientId in _playerNames.Keys) { - _server.Channels[clientId].Enqueue(packet); - } - } - - private void BroadcastTo(uint[] ids, IPacket packet) { - foreach (var id in ids) { - _server.Channels[id].Enqueue(packet); - } - } - - public async Task Run() { - _server.Start(); - - var run = true; - while (run) { - await _server.ListenAsync(); - _server.Update(); - } - - _server.Stop(); - } - - public void Dispose() { - _server.Dispose(); - GC.SuppressFinalize(this); - } -} diff --git a/OpenPolytopia.Server/Module.cs b/OpenPolytopia.Server/Module.cs new file mode 100644 index 00000000..290d88cb --- /dev/null +++ b/OpenPolytopia.Server/Module.cs @@ -0,0 +1,279 @@ +namespace OpenPolytopia.Server; + +using SpacetimeDB; + +public static partial class Module { + /// + /// Defines a player + /// + [Table(Name = "Player", Public = true)] + public partial class Player { + [PrimaryKey] public Identity Id; + public required string Name; + public bool Online; + } + + /// + /// Defines a lobby where players can join and start a game + /// + [Table(Name = "Lobby", Public = true)] + public partial class Lobby { + [PrimaryKey] [AutoInc] public ulong Id; + public uint MaxPlayers; + public uint Players; + public uint Ready; + public bool Started; + public bool Starting; + } + + /// + /// Defines a player who joined a lobby + /// + [Table(Name = "LobbyPlayer", Public = true)] + [Index.BTree(Name = "LobbyAndPlayer", Columns = [nameof(LobbyId), nameof(PlayerId)])] + public partial class LobbyPlayer { + [PrimaryKey] [AutoInc] public ulong Id; + public ulong LobbyId; + public Identity PlayerId; + public uint Tribe; + } + + [Reducer(ReducerKind.ClientConnected)] + public static void ClientConnected(ReducerContext ctx) { + // get the player + var player = ctx.FindPlayer(); + + // check if he exists in the database + if (player == null) { + throw new UserNotRegisteredException(); + } + + // set the online status + player.Online = true; + + // update the database + ctx.Db.Player.Id.Update(player); + } + + [Reducer(ReducerKind.ClientDisconnected)] + public static void ClientDisconnected(ReducerContext ctx) { + // get the player + var player = ctx.FindPlayer(); + + // check if he exists in the database + if (player == null) { + throw new UserNotRegisteredException(); + } + + // set the online status + player.Online = false; + + // update the database + ctx.Db.Player.Id.Update(player); + } + + [Reducer] + public static void SetName(ReducerContext ctx, string name) { + // get the player + var player = ctx.FindPlayer(); + + // check if player exists + if (player != null) { + // set the name + player.Name = name; + + // and update him + ctx.Db.Player.Id.Update(player); + } + else { + // create a new player; we set the online status to true because to make changes to the name you need to be online + player = new Player { Id = ctx.Sender, Name = name, Online = true, }; + + // and add him + ctx.Db.Player.Insert(player); + } + } + + [Reducer] + public static void CreateLobby(ReducerContext ctx, uint maxPlayers, uint tribe) { + // get the player + var player = ctx.FindPlayer(); + + // check if the player exists + if (player == null) { + throw new UserNotRegisteredException(); + } + + // create the lobby + var lobby = ctx.CreateLobby(maxPlayers); + + // set the players in lobby to 1 + lobby.Players++; + + // create the lobby player + ctx.CreateLobbyPlayer(lobby.Id, tribe); + } + + [Reducer] + public static void JoinLobby(ReducerContext ctx, ulong lobbyId, uint tribe) { + // get the player + var player = ctx.FindPlayer(); + + // check if the player exists + if (player == null) { + throw new UserNotRegisteredException(); + } + + // get the lobby + var lobby = ctx.FindLobby(lobbyId); + + // check if lobby exists + if (lobby == null) { + throw new LobbyNotFoundException(); + } + + // check if lobby is still waiting for more players + if (lobby.Starting || lobby.Started) { + throw new LobbyAlreadyStartedException(); + } + + // check if the lobby is full + if (lobby.Players >= lobby.MaxPlayers) { + throw new LobbyFullException(); + } + + // get all players in the lobby + var lobbyPlayers = ctx.FilterLobbyPlayer(lobbyId); + + // check if player is in lobby + if (lobbyPlayers.Any(lobbyPlayer => lobbyPlayer.PlayerId == ctx.Sender)) { + throw new AlreadyJoinedLobbyException(); + } + + // increment the players amount + lobby.Players++; + + // update the lobby + ctx.UpdateLobby(lobby); + + // add the player to the lobby + ctx.CreateLobbyPlayer(lobbyId, tribe); + } + + [Reducer] + public static void LeaveLobby(ReducerContext ctx, ulong lobbyId) { + // get the player + var player = ctx.FindPlayer(); + + // check if the player exists + if (player == null) { + throw new UserNotRegisteredException(); + } + + // get the lobby + var lobby = ctx.FindLobby(lobbyId); + + // check if lobby exists + if (lobby == null) { + throw new LobbyNotFoundException(); + } + + // check if lobby is still waiting for more players + if (lobby.Starting || lobby.Started) { + throw new LobbyAlreadyStartedException(); + } + + // get all players in the lobby + var lobbyPlayer = ctx.FilterLobbyPlayer(lobbyId).FirstOrDefault(lobbyPlayer => lobbyPlayer.PlayerId == ctx.Sender); + + // check if the player is in the lobby + if (lobbyPlayer == null) { + throw new NotInLobbyException(); + } + + // remove the player from the lobby + ctx.RemoveLobbyPlayer(lobbyPlayer); + } + + [Reducer] + public static void AddReady(ReducerContext ctx, ulong lobbyId) { + // get the player + var player = ctx.FindPlayer(); + + // check if the player exists + if (player == null) { + throw new UserNotRegisteredException(); + } + + // get the lobby + var lobby = ctx.FindLobby(lobbyId); + + // check if lobby exists + if (lobby == null) { + throw new LobbyNotFoundException(); + } + + // check if lobby is still waiting for more players + if (lobby.Starting || lobby.Started) { + throw new LobbyAlreadyStartedException(); + } + + // get all players in the lobby + var lobbyPlayer = ctx.FilterLobbyPlayer(lobbyId).FirstOrDefault(lobbyPlayer => lobbyPlayer.PlayerId == ctx.Sender); + + // check if the player is in the lobby + if (lobbyPlayer == null) { + throw new NotInLobbyException(); + } + + // increment the number of players ready + lobby.Ready++; + + // check if all players are ready + if (lobby.Ready == lobby.Players) { + // start the game + lobby.Starting = true; + } + + // update the lobby + ctx.UpdateLobby(lobby); + } + + [Reducer] + public static void RemoveReady(ReducerContext ctx, ulong lobbyId) { + // get the player + var player = ctx.FindPlayer(); + + // check if the player exists + if (player == null) { + throw new UserNotRegisteredException(); + } + + // get the lobby + var lobby = ctx.FindLobby(lobbyId); + + // check if lobby exists + if (lobby == null) { + throw new LobbyNotFoundException(); + } + + // check if lobby is still waiting for more players + if (lobby.Starting || lobby.Started) { + throw new LobbyAlreadyStartedException(); + } + + // get all players in the lobby + var lobbyPlayer = ctx.FilterLobbyPlayer(lobbyId).FirstOrDefault(lobbyPlayer => lobbyPlayer.PlayerId == ctx.Sender); + + // check if the player is in the lobby + if (lobbyPlayer == null) { + throw new NotInLobbyException(); + } + + // decrement the number of players ready + lobby.Ready--; + + // update the lobby + ctx.UpdateLobby(lobby); + } +} diff --git a/OpenPolytopia.Server/OpenPolytopia.Server.csproj b/OpenPolytopia.Server/OpenPolytopia.Server.csproj index 9d452459..d78de9a3 100644 --- a/OpenPolytopia.Server/OpenPolytopia.Server.csproj +++ b/OpenPolytopia.Server/OpenPolytopia.Server.csproj @@ -1,7 +1,6 @@  - Exe net9.0 latestmajor enable @@ -9,7 +8,7 @@ - + diff --git a/OpenPolytopia.Server/Program.cs b/OpenPolytopia.Server/Program.cs deleted file mode 100644 index dfdda698..00000000 --- a/OpenPolytopia.Server/Program.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace OpenPolytopia.Server; - -internal static class Program { - private static async Task Main(string[] args) { - Console.WriteLine("Server starting"); - var gameServer = new GameServer(6969); - await gameServer.Run(); - } -} diff --git a/OpenPolytopia.Server/ReducerContextExtensions.cs b/OpenPolytopia.Server/ReducerContextExtensions.cs new file mode 100644 index 00000000..6620f95d --- /dev/null +++ b/OpenPolytopia.Server/ReducerContextExtensions.cs @@ -0,0 +1,41 @@ +namespace OpenPolytopia.Server; + +using System.Runtime.CompilerServices; +using SpacetimeDB; + +public static class ReducerContextExtensions { + public static Module.Player? FindPlayer(this ReducerContext ctx, Identity? id = null) => + ctx.Db.Player.Id.Find(id ?? ctx.Sender); + + public static Module.Lobby? FindLobby(this ReducerContext ctx, ulong id) => ctx.Db.Lobby.Id.Find(id); + + public static Module.LobbyPlayer? FindLobbyPlayer(this ReducerContext ctx, ulong id) => + ctx.Db.LobbyPlayer.Id.Find(id); + + public static IEnumerable FilterLobbyPlayer(this ReducerContext ctx, ulong lobbyId, + Identity? playerId = null) => + playerId == null + ? ctx.Db.LobbyPlayer.LobbyAndPlayer.Filter(lobbyId) + : ctx.Db.LobbyPlayer.LobbyAndPlayer.Filter((lobbyId, playerId.Value)); + + public static Module.Lobby UpdateLobby(this ReducerContext ctx, Module.Lobby lobby) => ctx.Db.Lobby.Id.Update(lobby); + + public static Module.Lobby CreateLobby(this ReducerContext ctx, uint maxPlayers) => + ctx.Db.Lobby.Insert(new Module.Lobby { + Id = 0, + MaxPlayers = maxPlayers, + Started = false, + Starting = false, + Players = 0, + Ready = 0 + }); + + public static Module.LobbyPlayer CreateLobbyPlayer(this ReducerContext ctx, ulong lobbyId, uint tribe, + Identity? id = null) => + ctx.Db.LobbyPlayer.Insert(new Module.LobbyPlayer { + Id = 0, LobbyId = lobbyId, Tribe = tribe, PlayerId = id ?? ctx.Sender + }); + + public static void RemoveLobbyPlayer(this ReducerContext ctx, Module.LobbyPlayer lobbyPlayer) => + ctx.Db.LobbyPlayer.Id.Delete(lobbyPlayer.Id); +} diff --git a/OpenPolytopia.sln b/OpenPolytopia.sln index 520efac7..86427fbd 100644 --- a/OpenPolytopia.sln +++ b/OpenPolytopia.sln @@ -6,7 +6,7 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpLibrary", "FSharpLibr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenPolytopia.Common", "OpenPolytopia.Common\OpenPolytopia.Common.csproj", "{07BAA3DA-9489-4A3A-85AD-92C16108241D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenPolytopia.Server", "OpenPolytopia.Server\OpenPolytopia.Server.csproj", "{8AEBDE70-EA54-4927-8DD1-3E4E14AA0E19}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenPolytopia.Server", "OpenPolytopia.Server\OpenPolytopia.Server.csproj", "{0B313502-8039-43DA-AC92-4B8146E88349}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,11 +33,11 @@ Global {07BAA3DA-9489-4A3A-85AD-92C16108241D}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU {07BAA3DA-9489-4A3A-85AD-92C16108241D}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU {07BAA3DA-9489-4A3A-85AD-92C16108241D}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU - {8AEBDE70-EA54-4927-8DD1-3E4E14AA0E19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8AEBDE70-EA54-4927-8DD1-3E4E14AA0E19}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8AEBDE70-EA54-4927-8DD1-3E4E14AA0E19}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU - {8AEBDE70-EA54-4927-8DD1-3E4E14AA0E19}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU - {8AEBDE70-EA54-4927-8DD1-3E4E14AA0E19}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU - {8AEBDE70-EA54-4927-8DD1-3E4E14AA0E19}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU + {0B313502-8039-43DA-AC92-4B8146E88349}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B313502-8039-43DA-AC92-4B8146E88349}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B313502-8039-43DA-AC92-4B8146E88349}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU + {0B313502-8039-43DA-AC92-4B8146E88349}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU + {0B313502-8039-43DA-AC92-4B8146E88349}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU + {0B313502-8039-43DA-AC92-4B8146E88349}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection EndGlobal diff --git a/OpenPolytopia.sln.DotSettings.user b/OpenPolytopia.sln.DotSettings.user index 01ea4ef4..2fccb4b3 100644 --- a/OpenPolytopia.sln.DotSettings.user +++ b/OpenPolytopia.sln.DotSettings.user @@ -5,4 +5,5 @@ ForceIncluded ForceIncluded ForceIncluded - ForceIncluded \ No newline at end of file + ForceIncluded + \ No newline at end of file From 11b5ff877322b27a8847fbdd50ebba975b04c70e Mon Sep 17 00:00:00 2001 From: Enn3Developer Date: Wed, 12 Mar 2025 12:57:18 +0100 Subject: [PATCH 2/8] added start lobby --- OpenPolytopia.Server/Exceptions.cs | 2 + OpenPolytopia.Server/Module.cs | 83 +++++++++++-------- .../ReducerContextExtensions.cs | 9 +- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/OpenPolytopia.Server/Exceptions.cs b/OpenPolytopia.Server/Exceptions.cs index 0f9cf522..7504f86c 100644 --- a/OpenPolytopia.Server/Exceptions.cs +++ b/OpenPolytopia.Server/Exceptions.cs @@ -11,3 +11,5 @@ public class AlreadyJoinedLobbyException() : Exception("You already joined this public class LobbyFullException() : Exception("This lobby is full"); public class NotInLobbyException() : Exception("You must join a lobby first before leaving it"); + +public class ReducerNoPermissionException() : Exception("You don't have permission to run this reducer"); diff --git a/OpenPolytopia.Server/Module.cs b/OpenPolytopia.Server/Module.cs index 290d88cb..81c8e7a3 100644 --- a/OpenPolytopia.Server/Module.cs +++ b/OpenPolytopia.Server/Module.cs @@ -38,6 +38,19 @@ public partial class LobbyPlayer { public uint Tribe; } + [Table(Name = "ScheduledStartLobby", Scheduled = nameof(StartLobby), ScheduledAt = nameof(ScheduleAt))] + public partial class ScheduledStartLobby { + [PrimaryKey] [AutoInc] public ulong Id; + public required ScheduleAt ScheduleAt; + } + + [Reducer(ReducerKind.Init)] + public static void Init(ReducerContext ctx) => + // add scheduled start lobby to run every 5 seconds + ctx.Db.ScheduledStartLobby.Insert(new ScheduledStartLobby { + Id = 0, ScheduleAt = new ScheduleAt.Interval(new TimeDuration(5_000_000)) + }); + [Reducer(ReducerKind.ClientConnected)] public static void ClientConnected(ReducerContext ctx) { // get the player @@ -72,6 +85,31 @@ public static void ClientDisconnected(ReducerContext ctx) { ctx.Db.Player.Id.Update(player); } + [Reducer] + public static void StartLobby(ReducerContext ctx, ScheduledStartLobby startLobby) { + // check if the reducer was called from the server and not from a client + if (ctx.Sender != ctx.Identity) { + throw new ReducerNoPermissionException(); + } + + // for each lobby that is starting + foreach (var lobby in ctx.Db.Lobby.Iter()) { + if (!lobby.Starting) { + continue; + } + + // TODO: world generation + // TODO: initialize game data + // TODO: add players to the game + + // remove all players from the lobby + ctx.FilterRemoveLobbyPlayer(lobby); + + // remove the lobby + ctx.RemoveLobby(lobby); + } + } + [Reducer] public static void SetName(ReducerContext ctx, string name) { // get the player @@ -142,11 +180,8 @@ public static void JoinLobby(ReducerContext ctx, ulong lobbyId, uint tribe) { throw new LobbyFullException(); } - // get all players in the lobby - var lobbyPlayers = ctx.FilterLobbyPlayer(lobbyId); - // check if player is in lobby - if (lobbyPlayers.Any(lobbyPlayer => lobbyPlayer.PlayerId == ctx.Sender)) { + if (ctx.FilterLobbyPlayer(lobbyId).Any(lobbyPlayer => lobbyPlayer.PlayerId == ctx.Sender)) { throw new AlreadyJoinedLobbyException(); } @@ -162,14 +197,6 @@ public static void JoinLobby(ReducerContext ctx, ulong lobbyId, uint tribe) { [Reducer] public static void LeaveLobby(ReducerContext ctx, ulong lobbyId) { - // get the player - var player = ctx.FindPlayer(); - - // check if the player exists - if (player == null) { - throw new UserNotRegisteredException(); - } - // get the lobby var lobby = ctx.FindLobby(lobbyId); @@ -191,20 +218,18 @@ public static void LeaveLobby(ReducerContext ctx, ulong lobbyId) { throw new NotInLobbyException(); } + // decrement the players amount + lobby.Players--; + + // update lobby + ctx.UpdateLobby(lobby); + // remove the player from the lobby ctx.RemoveLobbyPlayer(lobbyPlayer); } [Reducer] public static void AddReady(ReducerContext ctx, ulong lobbyId) { - // get the player - var player = ctx.FindPlayer(); - - // check if the player exists - if (player == null) { - throw new UserNotRegisteredException(); - } - // get the lobby var lobby = ctx.FindLobby(lobbyId); @@ -218,11 +243,8 @@ public static void AddReady(ReducerContext ctx, ulong lobbyId) { throw new LobbyAlreadyStartedException(); } - // get all players in the lobby - var lobbyPlayer = ctx.FilterLobbyPlayer(lobbyId).FirstOrDefault(lobbyPlayer => lobbyPlayer.PlayerId == ctx.Sender); - // check if the player is in the lobby - if (lobbyPlayer == null) { + if (ctx.FilterLobbyPlayer(lobbyId).All(lobbyPlayer => lobbyPlayer.PlayerId != ctx.Sender)) { throw new NotInLobbyException(); } @@ -241,14 +263,6 @@ public static void AddReady(ReducerContext ctx, ulong lobbyId) { [Reducer] public static void RemoveReady(ReducerContext ctx, ulong lobbyId) { - // get the player - var player = ctx.FindPlayer(); - - // check if the player exists - if (player == null) { - throw new UserNotRegisteredException(); - } - // get the lobby var lobby = ctx.FindLobby(lobbyId); @@ -262,11 +276,8 @@ public static void RemoveReady(ReducerContext ctx, ulong lobbyId) { throw new LobbyAlreadyStartedException(); } - // get all players in the lobby - var lobbyPlayer = ctx.FilterLobbyPlayer(lobbyId).FirstOrDefault(lobbyPlayer => lobbyPlayer.PlayerId == ctx.Sender); - // check if the player is in the lobby - if (lobbyPlayer == null) { + if (ctx.FilterLobbyPlayer(lobbyId).All(lobbyPlayer => lobbyPlayer.PlayerId != ctx.Sender)) { throw new NotInLobbyException(); } diff --git a/OpenPolytopia.Server/ReducerContextExtensions.cs b/OpenPolytopia.Server/ReducerContextExtensions.cs index 6620f95d..6a3cf7d6 100644 --- a/OpenPolytopia.Server/ReducerContextExtensions.cs +++ b/OpenPolytopia.Server/ReducerContextExtensions.cs @@ -18,8 +18,6 @@ public static class ReducerContextExtensions { ? ctx.Db.LobbyPlayer.LobbyAndPlayer.Filter(lobbyId) : ctx.Db.LobbyPlayer.LobbyAndPlayer.Filter((lobbyId, playerId.Value)); - public static Module.Lobby UpdateLobby(this ReducerContext ctx, Module.Lobby lobby) => ctx.Db.Lobby.Id.Update(lobby); - public static Module.Lobby CreateLobby(this ReducerContext ctx, uint maxPlayers) => ctx.Db.Lobby.Insert(new Module.Lobby { Id = 0, @@ -30,6 +28,10 @@ public static Module.Lobby CreateLobby(this ReducerContext ctx, uint maxPlayers) Ready = 0 }); + public static Module.Lobby UpdateLobby(this ReducerContext ctx, Module.Lobby lobby) => ctx.Db.Lobby.Id.Update(lobby); + + public static void RemoveLobby(this ReducerContext ctx, Module.Lobby lobby) => ctx.Db.Lobby.Id.Delete(lobby.Id); + public static Module.LobbyPlayer CreateLobbyPlayer(this ReducerContext ctx, ulong lobbyId, uint tribe, Identity? id = null) => ctx.Db.LobbyPlayer.Insert(new Module.LobbyPlayer { @@ -38,4 +40,7 @@ public static Module.LobbyPlayer CreateLobbyPlayer(this ReducerContext ctx, ulon public static void RemoveLobbyPlayer(this ReducerContext ctx, Module.LobbyPlayer lobbyPlayer) => ctx.Db.LobbyPlayer.Id.Delete(lobbyPlayer.Id); + + public static void FilterRemoveLobbyPlayer(this ReducerContext ctx, Module.Lobby lobby) => + ctx.Db.LobbyPlayer.LobbyAndPlayer.Delete(lobby.Id); } From 8d40cadd125940747c6ee9cbce5a850420d59524 Mon Sep 17 00:00:00 2001 From: Enn3Developer Date: Wed, 12 Mar 2025 13:49:53 +0100 Subject: [PATCH 3/8] fixed everything to make spacetime generate bindings for the client --- FSharpLibrary/FSharpLibrary.fsproj | 2 +- .../OpenPolytopia.Common.csproj | 2 +- OpenPolytopia.sln | 2 +- OpenPolytopia.sln.DotSettings.user | 2 + OpenPolytopia/OpenPolytopia.csproj | 4 +- .../src/ModuleBindings/Reducers/AddReady.g.cs | 53 +++ .../Reducers/ClientConnected.g.cs | 33 ++ .../Reducers/ClientDisconnected.g.cs | 33 ++ .../ModuleBindings/Reducers/CreateLobby.g.cs | 60 +++ .../ModuleBindings/Reducers/JoinLobby.g.cs | 60 +++ .../ModuleBindings/Reducers/LeaveLobby.g.cs | 53 +++ .../ModuleBindings/Reducers/RemoveReady.g.cs | 53 +++ .../src/ModuleBindings/Reducers/SetName.g.cs | 54 +++ .../ModuleBindings/Reducers/StartLobby.g.cs | 54 +++ .../src/ModuleBindings/SpacetimeDBClient.g.cs | 447 ++++++++++++++++++ .../src/ModuleBindings/Tables/Lobby.g.cs | 34 ++ .../ModuleBindings/Tables/LobbyPlayer.g.cs | 43 ++ .../src/ModuleBindings/Tables/Player.g.cs | 34 ++ .../Tables/StartLobbySchedule.g.cs | 34 ++ .../src/ModuleBindings/Types/Lobby.g.cs | 46 ++ .../src/ModuleBindings/Types/LobbyPlayer.g.cs | 38 ++ .../src/ModuleBindings/Types/Player.g.cs | 35 ++ .../Types/StartLobbySchedule.g.cs | 31 ++ .../Exceptions.cs | 2 + .../Module.cs | 15 +- .../ReducerContextExtensions.cs | 4 +- .../StdbModule.csproj | 4 +- global.json | 9 - 28 files changed, 1218 insertions(+), 23 deletions(-) create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/AddReady.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/ClientConnected.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/ClientDisconnected.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/CreateLobby.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/JoinLobby.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/LeaveLobby.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/RemoveReady.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/SetName.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Reducers/StartLobby.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/SpacetimeDBClient.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Tables/Lobby.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Tables/LobbyPlayer.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Tables/Player.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Tables/StartLobbySchedule.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Types/Lobby.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Types/LobbyPlayer.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Types/Player.g.cs create mode 100644 OpenPolytopia/src/ModuleBindings/Types/StartLobbySchedule.g.cs rename {OpenPolytopia.Server => StdbModule}/Exceptions.cs (97%) rename {OpenPolytopia.Server => StdbModule}/Module.cs (94%) rename {OpenPolytopia.Server => StdbModule}/ReducerContextExtensions.cs (95%) rename OpenPolytopia.Server/OpenPolytopia.Server.csproj => StdbModule/StdbModule.csproj (69%) delete mode 100644 global.json diff --git a/FSharpLibrary/FSharpLibrary.fsproj b/FSharpLibrary/FSharpLibrary.fsproj index 8cebf2ca..a069b46f 100644 --- a/FSharpLibrary/FSharpLibrary.fsproj +++ b/FSharpLibrary/FSharpLibrary.fsproj @@ -1,7 +1,7 @@  - net9.0 + net8.0 true diff --git a/OpenPolytopia.Common/OpenPolytopia.Common.csproj b/OpenPolytopia.Common/OpenPolytopia.Common.csproj index a77db6ad..1e6f2ab2 100644 --- a/OpenPolytopia.Common/OpenPolytopia.Common.csproj +++ b/OpenPolytopia.Common/OpenPolytopia.Common.csproj @@ -1,7 +1,7 @@ - net9.0 + net8.0 enable enable diff --git a/OpenPolytopia.sln b/OpenPolytopia.sln index 86427fbd..268b3f11 100644 --- a/OpenPolytopia.sln +++ b/OpenPolytopia.sln @@ -6,7 +6,7 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpLibrary", "FSharpLibr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenPolytopia.Common", "OpenPolytopia.Common\OpenPolytopia.Common.csproj", "{07BAA3DA-9489-4A3A-85AD-92C16108241D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenPolytopia.Server", "OpenPolytopia.Server\OpenPolytopia.Server.csproj", "{0B313502-8039-43DA-AC92-4B8146E88349}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StdbModule", "StdbModule\StdbModule.csproj", "{0B313502-8039-43DA-AC92-4B8146E88349}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/OpenPolytopia.sln.DotSettings.user b/OpenPolytopia.sln.DotSettings.user index 2fccb4b3..4b57c481 100644 --- a/OpenPolytopia.sln.DotSettings.user +++ b/OpenPolytopia.sln.DotSettings.user @@ -6,4 +6,6 @@ ForceIncluded ForceIncluded ForceIncluded + + \ No newline at end of file diff --git a/OpenPolytopia/OpenPolytopia.csproj b/OpenPolytopia/OpenPolytopia.csproj index 07e80cfc..ffa23343 100644 --- a/OpenPolytopia/OpenPolytopia.csproj +++ b/OpenPolytopia/OpenPolytopia.csproj @@ -1,6 +1,5 @@ - net9.0 true latest enable @@ -27,6 +26,7 @@ $(DefaultItemExcludes);test/**/* + net8.0 @@ -46,6 +46,8 @@ + + diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/AddReady.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/AddReady.g.cs new file mode 100644 index 00000000..c300390f --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/AddReady.g.cs @@ -0,0 +1,53 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void AddReadyHandler(ReducerEventContext ctx, ulong lobbyId); + public event AddReadyHandler? OnAddReady; + + public void AddReady(ulong lobbyId) { + conn.InternalCallReducer(new Reducer.AddReady(lobbyId), this.SetCallReducerFlags.AddReadyFlags); + } + + public bool InvokeAddReady(ReducerEventContext ctx, Reducer.AddReady args) { + if (OnAddReady == null) + return false; + OnAddReady( + ctx, + args.LobbyId + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class AddReady : Reducer, IReducerArgs { + [DataMember(Name = "lobbyId")] + public ulong LobbyId; + + public AddReady(ulong LobbyId) { + this.LobbyId = LobbyId; + } + + public AddReady() { + } + + string IReducerArgs.ReducerName => "AddReady"; + } + } + + public sealed partial class SetReducerFlags { + internal CallReducerFlags AddReadyFlags; + public void AddReady(CallReducerFlags flags) => AddReadyFlags = flags; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/ClientConnected.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/ClientConnected.g.cs new file mode 100644 index 00000000..d2c4ae7d --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/ClientConnected.g.cs @@ -0,0 +1,33 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void ClientConnectedHandler(ReducerEventContext ctx); + public event ClientConnectedHandler? OnClientConnected; + + public bool InvokeClientConnected(ReducerEventContext ctx, Reducer.ClientConnected args) { + if (OnClientConnected == null) + return false; + OnClientConnected( + ctx + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class ClientConnected : Reducer, IReducerArgs { + string IReducerArgs.ReducerName => "ClientConnected"; + } + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/ClientDisconnected.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/ClientDisconnected.g.cs new file mode 100644 index 00000000..ec471f44 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/ClientDisconnected.g.cs @@ -0,0 +1,33 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void ClientDisconnectedHandler(ReducerEventContext ctx); + public event ClientDisconnectedHandler? OnClientDisconnected; + + public bool InvokeClientDisconnected(ReducerEventContext ctx, Reducer.ClientDisconnected args) { + if (OnClientDisconnected == null) + return false; + OnClientDisconnected( + ctx + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class ClientDisconnected : Reducer, IReducerArgs { + string IReducerArgs.ReducerName => "ClientDisconnected"; + } + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/CreateLobby.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/CreateLobby.g.cs new file mode 100644 index 00000000..99a9554d --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/CreateLobby.g.cs @@ -0,0 +1,60 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void CreateLobbyHandler(ReducerEventContext ctx, uint maxPlayers, uint tribe); + public event CreateLobbyHandler? OnCreateLobby; + + public void CreateLobby(uint maxPlayers, uint tribe) { + conn.InternalCallReducer(new Reducer.CreateLobby(maxPlayers, tribe), this.SetCallReducerFlags.CreateLobbyFlags); + } + + public bool InvokeCreateLobby(ReducerEventContext ctx, Reducer.CreateLobby args) { + if (OnCreateLobby == null) + return false; + OnCreateLobby( + ctx, + args.MaxPlayers, + args.Tribe + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class CreateLobby : Reducer, IReducerArgs { + [DataMember(Name = "maxPlayers")] + public uint MaxPlayers; + [DataMember(Name = "tribe")] + public uint Tribe; + + public CreateLobby( + uint MaxPlayers, + uint Tribe + ) { + this.MaxPlayers = MaxPlayers; + this.Tribe = Tribe; + } + + public CreateLobby() { + } + + string IReducerArgs.ReducerName => "CreateLobby"; + } + } + + public sealed partial class SetReducerFlags { + internal CallReducerFlags CreateLobbyFlags; + public void CreateLobby(CallReducerFlags flags) => CreateLobbyFlags = flags; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/JoinLobby.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/JoinLobby.g.cs new file mode 100644 index 00000000..edb8df5b --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/JoinLobby.g.cs @@ -0,0 +1,60 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void JoinLobbyHandler(ReducerEventContext ctx, ulong lobbyId, uint tribe); + public event JoinLobbyHandler? OnJoinLobby; + + public void JoinLobby(ulong lobbyId, uint tribe) { + conn.InternalCallReducer(new Reducer.JoinLobby(lobbyId, tribe), this.SetCallReducerFlags.JoinLobbyFlags); + } + + public bool InvokeJoinLobby(ReducerEventContext ctx, Reducer.JoinLobby args) { + if (OnJoinLobby == null) + return false; + OnJoinLobby( + ctx, + args.LobbyId, + args.Tribe + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class JoinLobby : Reducer, IReducerArgs { + [DataMember(Name = "lobbyId")] + public ulong LobbyId; + [DataMember(Name = "tribe")] + public uint Tribe; + + public JoinLobby( + ulong LobbyId, + uint Tribe + ) { + this.LobbyId = LobbyId; + this.Tribe = Tribe; + } + + public JoinLobby() { + } + + string IReducerArgs.ReducerName => "JoinLobby"; + } + } + + public sealed partial class SetReducerFlags { + internal CallReducerFlags JoinLobbyFlags; + public void JoinLobby(CallReducerFlags flags) => JoinLobbyFlags = flags; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/LeaveLobby.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/LeaveLobby.g.cs new file mode 100644 index 00000000..4cdaf884 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/LeaveLobby.g.cs @@ -0,0 +1,53 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void LeaveLobbyHandler(ReducerEventContext ctx, ulong lobbyId); + public event LeaveLobbyHandler? OnLeaveLobby; + + public void LeaveLobby(ulong lobbyId) { + conn.InternalCallReducer(new Reducer.LeaveLobby(lobbyId), this.SetCallReducerFlags.LeaveLobbyFlags); + } + + public bool InvokeLeaveLobby(ReducerEventContext ctx, Reducer.LeaveLobby args) { + if (OnLeaveLobby == null) + return false; + OnLeaveLobby( + ctx, + args.LobbyId + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class LeaveLobby : Reducer, IReducerArgs { + [DataMember(Name = "lobbyId")] + public ulong LobbyId; + + public LeaveLobby(ulong LobbyId) { + this.LobbyId = LobbyId; + } + + public LeaveLobby() { + } + + string IReducerArgs.ReducerName => "LeaveLobby"; + } + } + + public sealed partial class SetReducerFlags { + internal CallReducerFlags LeaveLobbyFlags; + public void LeaveLobby(CallReducerFlags flags) => LeaveLobbyFlags = flags; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/RemoveReady.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/RemoveReady.g.cs new file mode 100644 index 00000000..81de41cc --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/RemoveReady.g.cs @@ -0,0 +1,53 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void RemoveReadyHandler(ReducerEventContext ctx, ulong lobbyId); + public event RemoveReadyHandler? OnRemoveReady; + + public void RemoveReady(ulong lobbyId) { + conn.InternalCallReducer(new Reducer.RemoveReady(lobbyId), this.SetCallReducerFlags.RemoveReadyFlags); + } + + public bool InvokeRemoveReady(ReducerEventContext ctx, Reducer.RemoveReady args) { + if (OnRemoveReady == null) + return false; + OnRemoveReady( + ctx, + args.LobbyId + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class RemoveReady : Reducer, IReducerArgs { + [DataMember(Name = "lobbyId")] + public ulong LobbyId; + + public RemoveReady(ulong LobbyId) { + this.LobbyId = LobbyId; + } + + public RemoveReady() { + } + + string IReducerArgs.ReducerName => "RemoveReady"; + } + } + + public sealed partial class SetReducerFlags { + internal CallReducerFlags RemoveReadyFlags; + public void RemoveReady(CallReducerFlags flags) => RemoveReadyFlags = flags; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/SetName.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/SetName.g.cs new file mode 100644 index 00000000..3971e0a5 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/SetName.g.cs @@ -0,0 +1,54 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void SetNameHandler(ReducerEventContext ctx, string name); + public event SetNameHandler? OnSetName; + + public void SetName(string name) { + conn.InternalCallReducer(new Reducer.SetName(name), this.SetCallReducerFlags.SetNameFlags); + } + + public bool InvokeSetName(ReducerEventContext ctx, Reducer.SetName args) { + if (OnSetName == null) + return false; + OnSetName( + ctx, + args.Name + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class SetName : Reducer, IReducerArgs { + [DataMember(Name = "name")] + public string Name; + + public SetName(string Name) { + this.Name = Name; + } + + public SetName() { + this.Name = ""; + } + + string IReducerArgs.ReducerName => "SetName"; + } + } + + public sealed partial class SetReducerFlags { + internal CallReducerFlags SetNameFlags; + public void SetName(CallReducerFlags flags) => SetNameFlags = flags; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Reducers/StartLobby.g.cs b/OpenPolytopia/src/ModuleBindings/Reducers/StartLobby.g.cs new file mode 100644 index 00000000..77eebd0c --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Reducers/StartLobby.g.cs @@ -0,0 +1,54 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + public delegate void StartLobbyHandler(ReducerEventContext ctx, StartLobbySchedule schedule); + public event StartLobbyHandler? OnStartLobby; + + public void StartLobby(StartLobbySchedule schedule) { + conn.InternalCallReducer(new Reducer.StartLobby(schedule), this.SetCallReducerFlags.StartLobbyFlags); + } + + public bool InvokeStartLobby(ReducerEventContext ctx, Reducer.StartLobby args) { + if (OnStartLobby == null) + return false; + OnStartLobby( + ctx, + args.Schedule + ); + return true; + } + } + + public abstract partial class Reducer { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class StartLobby : Reducer, IReducerArgs { + [DataMember(Name = "schedule")] + public StartLobbySchedule Schedule; + + public StartLobby(StartLobbySchedule Schedule) { + this.Schedule = Schedule; + } + + public StartLobby() { + this.Schedule = new(); + } + + string IReducerArgs.ReducerName => "StartLobby"; + } + } + + public sealed partial class SetReducerFlags { + internal CallReducerFlags StartLobbyFlags; + public void StartLobby(CallReducerFlags flags) => StartLobbyFlags = flags; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/SpacetimeDBClient.g.cs b/OpenPolytopia/src/ModuleBindings/SpacetimeDBClient.g.cs new file mode 100644 index 00000000..7b00c635 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/SpacetimeDBClient.g.cs @@ -0,0 +1,447 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteReducers : RemoteBase { + internal RemoteReducers(DbConnection conn, SetReducerFlags flags) : base(conn) => SetCallReducerFlags = flags; + internal readonly SetReducerFlags SetCallReducerFlags; + } + + public sealed partial class RemoteTables : RemoteTablesBase { + public RemoteTables(DbConnection conn) { + AddTable(Lobby = new(conn)); + AddTable(LobbyPlayer = new(conn)); + AddTable(Player = new(conn)); + AddTable(StartLobbySchedule = new(conn)); + } + } + + public sealed partial class SetReducerFlags { } + + public interface IRemoteDbContext : IDbContext { } + + public sealed class EventContext : IEventContext, IRemoteDbContext { + private readonly DbConnection conn; + + /// + /// The event that caused this callback to run. + /// + public readonly Event Event; + + /// + /// Access to tables in the client cache, which stores a read-only replica of the remote database state. + /// + /// The returned DbView will have a method to access each table defined by the module. + /// + public RemoteTables Db => conn.Db; + /// + /// Access to reducers defined by the module. + /// + /// The returned RemoteReducers will have a method to invoke each reducer defined by the module, + /// plus methods for adding and removing callbacks on each of those reducers. + /// + public RemoteReducers Reducers => conn.Reducers; + /// + /// Access to setters for per-reducer flags. + /// + /// The returned SetReducerFlags will have a method to invoke, + /// for each reducer defined by the module, + /// which call-flags for the reducer can be set. + /// + public SetReducerFlags SetReducerFlags => conn.SetReducerFlags; + /// + /// Returns true if the connection is active, i.e. has not yet disconnected. + /// + public bool IsActive => conn.IsActive; + /// + /// Close the connection. + /// + /// Throws an error if the connection is already closed. + /// + public void Disconnect() { + conn.Disconnect(); + } + /// + /// Start building a subscription. + /// + /// A builder-pattern constructor for subscribing to queries, + /// causing matching rows to be replicated into the client cache. + public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder(); + /// + /// Get the Identity of this connection. + /// + /// This method returns null if the connection was constructed anonymously + /// and we have not yet received our newly-generated Identity from the host. + /// + public Identity? Identity => conn.Identity; + /// + /// Get this connection's ConnectionId. + /// + public ConnectionId ConnectionId => conn.ConnectionId; + + internal EventContext(DbConnection conn, Event Event) { + this.conn = conn; + this.Event = Event; + } + } + + public sealed class ReducerEventContext : IReducerEventContext, IRemoteDbContext { + private readonly DbConnection conn; + /// + /// The reducer event that caused this callback to run. + /// + public readonly ReducerEvent Event; + + /// + /// Access to tables in the client cache, which stores a read-only replica of the remote database state. + /// + /// The returned DbView will have a method to access each table defined by the module. + /// + public RemoteTables Db => conn.Db; + /// + /// Access to reducers defined by the module. + /// + /// The returned RemoteReducers will have a method to invoke each reducer defined by the module, + /// plus methods for adding and removing callbacks on each of those reducers. + /// + public RemoteReducers Reducers => conn.Reducers; + /// + /// Access to setters for per-reducer flags. + /// + /// The returned SetReducerFlags will have a method to invoke, + /// for each reducer defined by the module, + /// which call-flags for the reducer can be set. + /// + public SetReducerFlags SetReducerFlags => conn.SetReducerFlags; + /// + /// Returns true if the connection is active, i.e. has not yet disconnected. + /// + public bool IsActive => conn.IsActive; + /// + /// Close the connection. + /// + /// Throws an error if the connection is already closed. + /// + public void Disconnect() { + conn.Disconnect(); + } + /// + /// Start building a subscription. + /// + /// A builder-pattern constructor for subscribing to queries, + /// causing matching rows to be replicated into the client cache. + public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder(); + /// + /// Get the Identity of this connection. + /// + /// This method returns null if the connection was constructed anonymously + /// and we have not yet received our newly-generated Identity from the host. + /// + public Identity? Identity => conn.Identity; + /// + /// Get this connection's ConnectionId. + /// + public ConnectionId ConnectionId => conn.ConnectionId; + + internal ReducerEventContext(DbConnection conn, ReducerEvent reducerEvent) { + this.conn = conn; + Event = reducerEvent; + } + } + + public sealed class ErrorContext : IErrorContext, IRemoteDbContext { + private readonly DbConnection conn; + /// + /// The Exception that caused this error callback to be run. + /// + public readonly Exception Event; + Exception IErrorContext.Event { + get { + return Event; + } + } + + /// + /// Access to tables in the client cache, which stores a read-only replica of the remote database state. + /// + /// The returned DbView will have a method to access each table defined by the module. + /// + public RemoteTables Db => conn.Db; + /// + /// Access to reducers defined by the module. + /// + /// The returned RemoteReducers will have a method to invoke each reducer defined by the module, + /// plus methods for adding and removing callbacks on each of those reducers. + /// + public RemoteReducers Reducers => conn.Reducers; + /// + /// Access to setters for per-reducer flags. + /// + /// The returned SetReducerFlags will have a method to invoke, + /// for each reducer defined by the module, + /// which call-flags for the reducer can be set. + /// + public SetReducerFlags SetReducerFlags => conn.SetReducerFlags; + /// + /// Returns true if the connection is active, i.e. has not yet disconnected. + /// + public bool IsActive => conn.IsActive; + /// + /// Close the connection. + /// + /// Throws an error if the connection is already closed. + /// + public void Disconnect() { + conn.Disconnect(); + } + /// + /// Start building a subscription. + /// + /// A builder-pattern constructor for subscribing to queries, + /// causing matching rows to be replicated into the client cache. + public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder(); + /// + /// Get the Identity of this connection. + /// + /// This method returns null if the connection was constructed anonymously + /// and we have not yet received our newly-generated Identity from the host. + /// + public Identity? Identity => conn.Identity; + /// + /// Get this connection's ConnectionId. + /// + public ConnectionId ConnectionId => conn.ConnectionId; + + internal ErrorContext(DbConnection conn, Exception error) { + this.conn = conn; + Event = error; + } + } + + public sealed class SubscriptionEventContext : ISubscriptionEventContext, IRemoteDbContext { + private readonly DbConnection conn; + + /// + /// Access to tables in the client cache, which stores a read-only replica of the remote database state. + /// + /// The returned DbView will have a method to access each table defined by the module. + /// + public RemoteTables Db => conn.Db; + /// + /// Access to reducers defined by the module. + /// + /// The returned RemoteReducers will have a method to invoke each reducer defined by the module, + /// plus methods for adding and removing callbacks on each of those reducers. + /// + public RemoteReducers Reducers => conn.Reducers; + /// + /// Access to setters for per-reducer flags. + /// + /// The returned SetReducerFlags will have a method to invoke, + /// for each reducer defined by the module, + /// which call-flags for the reducer can be set. + /// + public SetReducerFlags SetReducerFlags => conn.SetReducerFlags; + /// + /// Returns true if the connection is active, i.e. has not yet disconnected. + /// + public bool IsActive => conn.IsActive; + /// + /// Close the connection. + /// + /// Throws an error if the connection is already closed. + /// + public void Disconnect() { + conn.Disconnect(); + } + /// + /// Start building a subscription. + /// + /// A builder-pattern constructor for subscribing to queries, + /// causing matching rows to be replicated into the client cache. + public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder(); + /// + /// Get the Identity of this connection. + /// + /// This method returns null if the connection was constructed anonymously + /// and we have not yet received our newly-generated Identity from the host. + /// + public Identity? Identity => conn.Identity; + /// + /// Get this connection's ConnectionId. + /// + public ConnectionId ConnectionId => conn.ConnectionId; + + internal SubscriptionEventContext(DbConnection conn) { + this.conn = conn; + } + } + + /// + /// Builder-pattern constructor for subscription queries. + /// + public sealed class SubscriptionBuilder { + private readonly IDbConnection conn; + + private event Action? Applied; + private event Action? Error; + + /// + /// Private API, use conn.SubscriptionBuilder() instead. + /// + public SubscriptionBuilder(IDbConnection conn) { + this.conn = conn; + } + + /// + /// Register a callback to run when the subscription is applied. + /// + public SubscriptionBuilder OnApplied( + Action callback + ) { + Applied += callback; + return this; + } + + /// + /// Register a callback to run when the subscription fails. + /// + /// Note that this callback may run either when attempting to apply the subscription, + /// in which case Self::on_applied will never run, + /// or later during the subscription's lifetime if the module's interface changes, + /// in which case Self::on_applied may have already run. + /// + public SubscriptionBuilder OnError( + Action callback + ) { + Error += callback; + return this; + } + + /// + /// Subscribe to the following SQL queries. + /// + /// This method returns immediately, with the data not yet added to the DbConnection. + /// The provided callbacks will be invoked once the data is returned from the remote server. + /// Data from all the provided queries will be returned at the same time. + /// + /// See the SpacetimeDB SQL docs for more information on SQL syntax: + /// https://spacetimedb.com/docs/sql + /// + public SubscriptionHandle Subscribe( + string[] querySqls + ) => new(conn, Applied, Error, querySqls); + + /// + /// Subscribe to all rows from all tables. + /// + /// This method is intended as a convenience + /// for applications where client-side memory use and network bandwidth are not concerns. + /// Applications where these resources are a constraint + /// should register more precise queries via Self.Subscribe + /// in order to replicate only the subset of data which the client needs to function. + /// + /// This method should not be combined with Self.Subscribe on the same DbConnection. + /// A connection may either Self.Subscribe to particular queries, + /// or Self.SubscribeToAllTables, but not both. + /// Attempting to call Self.Subscribe + /// on a DbConnection that has previously used Self.SubscribeToAllTables, + /// or vice versa, may misbehave in any number of ways, + /// including dropping subscriptions, corrupting the client cache, or panicking. + /// + public void SubscribeToAllTables() { + // Make sure we use the legacy handle constructor here, even though there's only 1 query. + // We drop the error handler, since it can't be called for legacy subscriptions. + new SubscriptionHandle( + conn, + Applied, + new string[] { "SELECT * FROM *" } + ); + } + } + + public sealed class SubscriptionHandle : SubscriptionHandleBase { + /// + /// Internal API. Construct SubscriptionHandles using conn.SubscriptionBuilder. + /// + public SubscriptionHandle(IDbConnection conn, Action? onApplied, string[] querySqls) : base(conn, onApplied, querySqls) { } + + /// + /// Internal API. Construct SubscriptionHandles using conn.SubscriptionBuilder. + /// + public SubscriptionHandle( + IDbConnection conn, + Action? onApplied, + Action? onError, + string[] querySqls + ) : base(conn, onApplied, onError, querySqls) { } + } + + public abstract partial class Reducer { + private Reducer() { } + } + + public sealed class DbConnection : DbConnectionBase { + public override RemoteTables Db { get; } + public readonly RemoteReducers Reducers; + public readonly SetReducerFlags SetReducerFlags = new(); + + public DbConnection() { + Db = new(this); + Reducers = new(this, SetReducerFlags); + } + + protected override Reducer ToReducer(TransactionUpdate update) { + var encodedArgs = update.ReducerCall.Args; + return update.ReducerCall.ReducerName switch { + "AddReady" => BSATNHelpers.Decode(encodedArgs), + "ClientConnected" => BSATNHelpers.Decode(encodedArgs), + "ClientDisconnected" => BSATNHelpers.Decode(encodedArgs), + "CreateLobby" => BSATNHelpers.Decode(encodedArgs), + "JoinLobby" => BSATNHelpers.Decode(encodedArgs), + "LeaveLobby" => BSATNHelpers.Decode(encodedArgs), + "RemoveReady" => BSATNHelpers.Decode(encodedArgs), + "SetName" => BSATNHelpers.Decode(encodedArgs), + "StartLobby" => BSATNHelpers.Decode(encodedArgs), + var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") + }; + } + + protected override IEventContext ToEventContext(Event Event) => + new EventContext(this, Event); + + protected override IReducerEventContext ToReducerEventContext(ReducerEvent reducerEvent) => + new ReducerEventContext(this, reducerEvent); + + protected override ISubscriptionEventContext MakeSubscriptionEventContext() => + new SubscriptionEventContext(this); + + protected override IErrorContext ToErrorContext(Exception exception) => + new ErrorContext(this, exception); + + protected override bool Dispatch(IReducerEventContext context, Reducer reducer) { + var eventContext = (ReducerEventContext)context; + return reducer switch { + Reducer.AddReady args => Reducers.InvokeAddReady(eventContext, args), + Reducer.ClientConnected args => Reducers.InvokeClientConnected(eventContext, args), + Reducer.ClientDisconnected args => Reducers.InvokeClientDisconnected(eventContext, args), + Reducer.CreateLobby args => Reducers.InvokeCreateLobby(eventContext, args), + Reducer.JoinLobby args => Reducers.InvokeJoinLobby(eventContext, args), + Reducer.LeaveLobby args => Reducers.InvokeLeaveLobby(eventContext, args), + Reducer.RemoveReady args => Reducers.InvokeRemoveReady(eventContext, args), + Reducer.SetName args => Reducers.InvokeSetName(eventContext, args), + Reducer.StartLobby args => Reducers.InvokeStartLobby(eventContext, args), + _ => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") + }; + } + + public SubscriptionBuilder SubscriptionBuilder() => new(this); + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Tables/Lobby.g.cs b/OpenPolytopia/src/ModuleBindings/Tables/Lobby.g.cs new file mode 100644 index 00000000..f5ab2ff5 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Tables/Lobby.g.cs @@ -0,0 +1,34 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteTables { + public sealed class LobbyHandle : RemoteTableHandle { + protected override string RemoteTableName => "Lobby"; + + public sealed class IdUniqueIndex : UniqueIndexBase { + protected override ulong GetKey(Lobby row) => row.Id; + + public IdUniqueIndex(LobbyHandle table) : base(table) { } + } + + public readonly IdUniqueIndex Id; + + internal LobbyHandle(DbConnection conn) : base(conn) { + Id = new(this); + } + + protected override object GetPrimaryKey(Lobby row) => row.Id; + } + + public readonly LobbyHandle Lobby; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Tables/LobbyPlayer.g.cs b/OpenPolytopia/src/ModuleBindings/Tables/LobbyPlayer.g.cs new file mode 100644 index 00000000..c3a64c94 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Tables/LobbyPlayer.g.cs @@ -0,0 +1,43 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteTables { + public sealed class LobbyPlayerHandle : RemoteTableHandle { + protected override string RemoteTableName => "LobbyPlayer"; + + public sealed class IdUniqueIndex : UniqueIndexBase { + protected override ulong GetKey(LobbyPlayer row) => row.Id; + + public IdUniqueIndex(LobbyPlayerHandle table) : base(table) { } + } + + public readonly IdUniqueIndex Id; + + public sealed class LobbyAndPlayerIndex : BTreeIndexBase<(ulong LobbyId, SpacetimeDB.Identity PlayerId)> { + protected override (ulong LobbyId, SpacetimeDB.Identity PlayerId) GetKey(LobbyPlayer row) => (row.LobbyId, row.PlayerId); + + public LobbyAndPlayerIndex(LobbyPlayerHandle table) : base(table) { } + } + + public readonly LobbyAndPlayerIndex LobbyAndPlayer; + + internal LobbyPlayerHandle(DbConnection conn) : base(conn) { + Id = new(this); + LobbyAndPlayer = new(this); + } + + protected override object GetPrimaryKey(LobbyPlayer row) => row.Id; + } + + public readonly LobbyPlayerHandle LobbyPlayer; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Tables/Player.g.cs b/OpenPolytopia/src/ModuleBindings/Tables/Player.g.cs new file mode 100644 index 00000000..4ab5df77 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Tables/Player.g.cs @@ -0,0 +1,34 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteTables { + public sealed class PlayerHandle : RemoteTableHandle { + protected override string RemoteTableName => "Player"; + + public sealed class IdUniqueIndex : UniqueIndexBase { + protected override SpacetimeDB.Identity GetKey(Player row) => row.Id; + + public IdUniqueIndex(PlayerHandle table) : base(table) { } + } + + public readonly IdUniqueIndex Id; + + internal PlayerHandle(DbConnection conn) : base(conn) { + Id = new(this); + } + + protected override object GetPrimaryKey(Player row) => row.Id; + } + + public readonly PlayerHandle Player; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Tables/StartLobbySchedule.g.cs b/OpenPolytopia/src/ModuleBindings/Tables/StartLobbySchedule.g.cs new file mode 100644 index 00000000..d674db4f --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Tables/StartLobbySchedule.g.cs @@ -0,0 +1,34 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + public sealed partial class RemoteTables { + public sealed class StartLobbyScheduleHandle : RemoteTableHandle { + protected override string RemoteTableName => "StartLobbySchedule"; + + public sealed class IdUniqueIndex : UniqueIndexBase { + protected override ulong GetKey(StartLobbySchedule row) => row.Id; + + public IdUniqueIndex(StartLobbyScheduleHandle table) : base(table) { } + } + + public readonly IdUniqueIndex Id; + + internal StartLobbyScheduleHandle(DbConnection conn) : base(conn) { + Id = new(this); + } + + protected override object GetPrimaryKey(StartLobbySchedule row) => row.Id; + } + + public readonly StartLobbyScheduleHandle StartLobbySchedule; + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Types/Lobby.g.cs b/OpenPolytopia/src/ModuleBindings/Types/Lobby.g.cs new file mode 100644 index 00000000..41375f55 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Types/Lobby.g.cs @@ -0,0 +1,46 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class Lobby { + [DataMember(Name = "Id")] + public ulong Id; + [DataMember(Name = "MaxPlayers")] + public uint MaxPlayers; + [DataMember(Name = "Players")] + public uint Players; + [DataMember(Name = "Ready")] + public uint Ready; + [DataMember(Name = "Started")] + public bool Started; + [DataMember(Name = "Starting")] + public bool Starting; + + public Lobby( + ulong Id, + uint MaxPlayers, + uint Players, + uint Ready, + bool Started, + bool Starting + ) { + this.Id = Id; + this.MaxPlayers = MaxPlayers; + this.Players = Players; + this.Ready = Ready; + this.Started = Started; + this.Starting = Starting; + } + + public Lobby() { + } + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Types/LobbyPlayer.g.cs b/OpenPolytopia/src/ModuleBindings/Types/LobbyPlayer.g.cs new file mode 100644 index 00000000..413f4d26 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Types/LobbyPlayer.g.cs @@ -0,0 +1,38 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class LobbyPlayer { + [DataMember(Name = "Id")] + public ulong Id; + [DataMember(Name = "LobbyId")] + public ulong LobbyId; + [DataMember(Name = "PlayerId")] + public SpacetimeDB.Identity PlayerId; + [DataMember(Name = "Tribe")] + public uint Tribe; + + public LobbyPlayer( + ulong Id, + ulong LobbyId, + SpacetimeDB.Identity PlayerId, + uint Tribe + ) { + this.Id = Id; + this.LobbyId = LobbyId; + this.PlayerId = PlayerId; + this.Tribe = Tribe; + } + + public LobbyPlayer() { + } + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Types/Player.g.cs b/OpenPolytopia/src/ModuleBindings/Types/Player.g.cs new file mode 100644 index 00000000..5e4fea20 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Types/Player.g.cs @@ -0,0 +1,35 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class Player { + [DataMember(Name = "Id")] + public SpacetimeDB.Identity Id; + [DataMember(Name = "Name")] + public string Name; + [DataMember(Name = "Online")] + public bool Online; + + public Player( + SpacetimeDB.Identity Id, + string Name, + bool Online + ) { + this.Id = Id; + this.Name = Name; + this.Online = Online; + } + + public Player() { + this.Name = ""; + } + } +} diff --git a/OpenPolytopia/src/ModuleBindings/Types/StartLobbySchedule.g.cs b/OpenPolytopia/src/ModuleBindings/Types/StartLobbySchedule.g.cs new file mode 100644 index 00000000..47796651 --- /dev/null +++ b/OpenPolytopia/src/ModuleBindings/Types/StartLobbySchedule.g.cs @@ -0,0 +1,31 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types { + [SpacetimeDB.Type] + [DataContract] + public sealed partial class StartLobbySchedule { + [DataMember(Name = "Id")] + public ulong Id; + [DataMember(Name = "ScheduledAt")] + public SpacetimeDB.ScheduleAt ScheduledAt; + + public StartLobbySchedule( + ulong Id, + SpacetimeDB.ScheduleAt ScheduledAt + ) { + this.Id = Id; + this.ScheduledAt = ScheduledAt; + } + + public StartLobbySchedule() { + this.ScheduledAt = null!; + } + } +} diff --git a/OpenPolytopia.Server/Exceptions.cs b/StdbModule/Exceptions.cs similarity index 97% rename from OpenPolytopia.Server/Exceptions.cs rename to StdbModule/Exceptions.cs index 7504f86c..ca17fdde 100644 --- a/OpenPolytopia.Server/Exceptions.cs +++ b/StdbModule/Exceptions.cs @@ -1,5 +1,7 @@ namespace OpenPolytopia.Server; +using System; + public class UserNotRegisteredException() : Exception("User not registered"); public class LobbyNotFoundException() : Exception("Lobby not found"); diff --git a/OpenPolytopia.Server/Module.cs b/StdbModule/Module.cs similarity index 94% rename from OpenPolytopia.Server/Module.cs rename to StdbModule/Module.cs index 81c8e7a3..d05ca079 100644 --- a/OpenPolytopia.Server/Module.cs +++ b/StdbModule/Module.cs @@ -1,5 +1,6 @@ namespace OpenPolytopia.Server; +using System.Linq; using SpacetimeDB; public static partial class Module { @@ -9,7 +10,7 @@ public static partial class Module { [Table(Name = "Player", Public = true)] public partial class Player { [PrimaryKey] public Identity Id; - public required string Name; + public string Name; public bool Online; } @@ -38,17 +39,17 @@ public partial class LobbyPlayer { public uint Tribe; } - [Table(Name = "ScheduledStartLobby", Scheduled = nameof(StartLobby), ScheduledAt = nameof(ScheduleAt))] - public partial class ScheduledStartLobby { + [Table(Name = "StartLobbySchedule", Scheduled = nameof(StartLobby), ScheduledAt = nameof(ScheduledAt))] + public partial class StartLobbySchedule { [PrimaryKey] [AutoInc] public ulong Id; - public required ScheduleAt ScheduleAt; + public ScheduleAt ScheduledAt; } [Reducer(ReducerKind.Init)] public static void Init(ReducerContext ctx) => // add scheduled start lobby to run every 5 seconds - ctx.Db.ScheduledStartLobby.Insert(new ScheduledStartLobby { - Id = 0, ScheduleAt = new ScheduleAt.Interval(new TimeDuration(5_000_000)) + ctx.Db.StartLobbySchedule.Insert(new StartLobbySchedule { + Id = 0, ScheduledAt = new ScheduleAt.Interval(new TimeDuration(5_000_000)) }); [Reducer(ReducerKind.ClientConnected)] @@ -86,7 +87,7 @@ public static void ClientDisconnected(ReducerContext ctx) { } [Reducer] - public static void StartLobby(ReducerContext ctx, ScheduledStartLobby startLobby) { + public static void StartLobby(ReducerContext ctx, StartLobbySchedule schedule) { // check if the reducer was called from the server and not from a client if (ctx.Sender != ctx.Identity) { throw new ReducerNoPermissionException(); diff --git a/OpenPolytopia.Server/ReducerContextExtensions.cs b/StdbModule/ReducerContextExtensions.cs similarity index 95% rename from OpenPolytopia.Server/ReducerContextExtensions.cs rename to StdbModule/ReducerContextExtensions.cs index 6a3cf7d6..3a0e495f 100644 --- a/OpenPolytopia.Server/ReducerContextExtensions.cs +++ b/StdbModule/ReducerContextExtensions.cs @@ -1,6 +1,5 @@ namespace OpenPolytopia.Server; -using System.Runtime.CompilerServices; using SpacetimeDB; public static class ReducerContextExtensions { @@ -12,7 +11,8 @@ public static class ReducerContextExtensions { public static Module.LobbyPlayer? FindLobbyPlayer(this ReducerContext ctx, ulong id) => ctx.Db.LobbyPlayer.Id.Find(id); - public static IEnumerable FilterLobbyPlayer(this ReducerContext ctx, ulong lobbyId, + public static IEnumerable FilterLobbyPlayer(this ReducerContext ctx, + ulong lobbyId, Identity? playerId = null) => playerId == null ? ctx.Db.LobbyPlayer.LobbyAndPlayer.Filter(lobbyId) diff --git a/OpenPolytopia.Server/OpenPolytopia.Server.csproj b/StdbModule/StdbModule.csproj similarity index 69% rename from OpenPolytopia.Server/OpenPolytopia.Server.csproj rename to StdbModule/StdbModule.csproj index d78de9a3..e6a212e2 100644 --- a/OpenPolytopia.Server/OpenPolytopia.Server.csproj +++ b/StdbModule/StdbModule.csproj @@ -1,10 +1,12 @@  - net9.0 + net8.0 latestmajor enable enable + OpenPolytopia.Server + Exe diff --git a/global.json b/global.json deleted file mode 100644 index f4952256..00000000 --- a/global.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "sdk": { - "version": "9.0.103", - "rollForward": "major" - }, - "msbuild-sdks": { - "Godot.NET.Sdk": "4.4.0" - } -} From c010f17026d06c4a5ecdc73e229002fb32645924 Mon Sep 17 00:00:00 2001 From: Enn3Developer Date: Wed, 12 Mar 2025 17:11:16 +0100 Subject: [PATCH 4/8] cleaning up and using spacetimedb on the client --- OpenPolytopia.Common/Lobby.cs | 71 ------ OpenPolytopia.Common/LobbyManager.cs | 22 -- .../Network/ClientConnection.cs | 133 ----------- .../Network/INetworkSerializable.cs | 24 -- .../Network/NetworkConnection.cs | 203 ---------------- .../Network/PacketRegistrar.cs | 59 ----- .../Network/Packets/CreateLobbyPacket.cs | 12 - .../Packets/CreateLobbyResponsePacket.cs | 17 -- .../Network/Packets/GetLobbiesPacket.cs | 10 - .../Packets/GetLobbiesResponsePacket.cs | 16 -- .../Network/Packets/HandshakePacket.cs | 12 - .../Packets/HandshakeResponsePacket.cs | 12 - .../Network/Packets/IPacket.cs | 217 ------------------ .../Network/Packets/KeepAlivePacket.cs | 15 -- .../Network/Packets/LobbyConnectPacket.cs | 11 - .../Packets/LobbyConnectResponsePacket.cs | 11 - .../Network/Packets/LobbyDeletedPacket.cs | 12 - .../Network/Packets/LobbyDisconnectPacket.cs | 12 - .../Packets/LobbyDisconnectResponsePacket.cs | 12 - .../Network/Packets/LobbyUpdatePacket.cs | 11 - .../Network/Packets/RegisterUserPacket.cs | 15 -- .../Packets/RegisterUserResponsePacket.cs | 16 -- .../Network/ServerConnection.cs | 117 ---------- OpenPolytopia.Common/PlayerData.cs | 15 -- OpenPolytopia.sln.DotSettings.user | 1 + OpenPolytopia/src/Client.cs | 107 --------- OpenPolytopia/src/Game.cs | 14 +- OpenPolytopia/src/Lobby.cs | 204 ---------------- OpenPolytopia/src/PlayerData.cs | 18 -- OpenPolytopia/src/PolyGame.cs | 13 -- OpenPolytopia/src/SpacetimeNode.cs | 106 +++++++++ OpenPolytopia/test/src/PacketTest.cs | 54 ----- cspell.json | 4 +- 33 files changed, 111 insertions(+), 1465 deletions(-) delete mode 100644 OpenPolytopia.Common/Lobby.cs delete mode 100644 OpenPolytopia.Common/LobbyManager.cs delete mode 100644 OpenPolytopia.Common/Network/ClientConnection.cs delete mode 100644 OpenPolytopia.Common/Network/INetworkSerializable.cs delete mode 100644 OpenPolytopia.Common/Network/NetworkConnection.cs delete mode 100644 OpenPolytopia.Common/Network/PacketRegistrar.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/CreateLobbyPacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/CreateLobbyResponsePacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/GetLobbiesPacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/GetLobbiesResponsePacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/HandshakePacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/HandshakeResponsePacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/IPacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/KeepAlivePacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/LobbyConnectPacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/LobbyConnectResponsePacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/LobbyDeletedPacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/LobbyDisconnectPacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/LobbyDisconnectResponsePacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/LobbyUpdatePacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/RegisterUserPacket.cs delete mode 100644 OpenPolytopia.Common/Network/Packets/RegisterUserResponsePacket.cs delete mode 100644 OpenPolytopia.Common/Network/ServerConnection.cs delete mode 100644 OpenPolytopia.Common/PlayerData.cs delete mode 100644 OpenPolytopia/src/Client.cs delete mode 100644 OpenPolytopia/src/Lobby.cs delete mode 100644 OpenPolytopia/src/PlayerData.cs create mode 100644 OpenPolytopia/src/SpacetimeNode.cs delete mode 100644 OpenPolytopia/test/src/PacketTest.cs diff --git a/OpenPolytopia.Common/Lobby.cs b/OpenPolytopia.Common/Lobby.cs deleted file mode 100644 index d91ffe80..00000000 --- a/OpenPolytopia.Common/Lobby.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace OpenPolytopia.Common; - -using System.Collections.ObjectModel; -using Network; -using Network.Packets; - -/// -/// Represents a lobby -/// -public class Lobby : INetworkSerializable { - private readonly List _players = []; - - /// - /// ID of the lobby - /// - public uint Id; - - /// - /// Number of max players that can join this lobby - /// - public uint MaxPlayers; - - /// - /// If the game in the lobby has started - /// - public bool Started; - - /// - /// Returns all the players in the lobby as a read-only list - /// - public ReadOnlyCollection Players => _players.AsReadOnly(); - - /// - /// Returns the player data from a given name - /// - /// the player's name - public PlayerData? this[string name] => _players.FirstOrDefault(player => player.PlayerName == name); - - /// - /// Adds a player to the lobby - /// - /// the player to add - /// true if the player can be added to the lobby, false otherwise - public bool AddPlayer(PlayerData player) { - if (_players.Count == MaxPlayers) { - return false; - } - - _players.Add(player); - return true; - } - - public void RemovePlayer(string name) { - var index = _players.FindIndex(player => player.PlayerName == name); - if (index != -1) { - _players.RemoveAt(index); - } - } - - public void Serialize(List bytes) { - Id.Serialize(bytes); - MaxPlayers.Serialize(bytes); - _players.Serialize(bytes); - } - - public void Deserialize(byte[] bytes, ref uint index) { - Id.Deserialize(bytes, ref index); - MaxPlayers.Deserialize(bytes, ref index); - _players.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/LobbyManager.cs b/OpenPolytopia.Common/LobbyManager.cs deleted file mode 100644 index 353e7650..00000000 --- a/OpenPolytopia.Common/LobbyManager.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace OpenPolytopia.Common; - -using System.Security.Cryptography; - -public class LobbyManager { - public readonly List Lobbies = []; - - public Lobby? this[uint id] => Lobbies.FirstOrDefault(lobby => lobby.Id == id); - - public bool AddPlayer(uint id, string name) => this[id]?.AddPlayer(new PlayerData { PlayerName = name }) ?? false; - - public void RemovePlayer(uint id, string name) => this[id]?.RemovePlayer(name); - - public Lobby NewLobby(uint maxPlayers) { - var lobby = new Lobby { MaxPlayers = maxPlayers, Id = (uint)RandomNumberGenerator.GetInt32(0, short.MaxValue) }; - Lobbies.Add(lobby); - return lobby; - } - - public bool CheckPlayer(string name) => - Lobbies.SelectMany(lobby => lobby.Players).Any(player => player.PlayerName == name); -} diff --git a/OpenPolytopia.Common/Network/ClientConnection.cs b/OpenPolytopia.Common/Network/ClientConnection.cs deleted file mode 100644 index 6eb7cbb0..00000000 --- a/OpenPolytopia.Common/Network/ClientConnection.cs +++ /dev/null @@ -1,133 +0,0 @@ -namespace OpenPolytopia.Common.Network; - -using System.Net.Sockets; -using DotNext.Threading; -using Packets; - -public class ClientConnection : NetworkConnection { - public delegate Task HandshakeResponse(bool result); - - public delegate Task LobbiesResponse(Lobby[] lobbies); - - public delegate Task CreateLobbyResponse(uint id); - - public delegate Task LobbyDeleted(uint id); - - public delegate Task LobbyConnectResponse(bool result); - - public delegate Task LobbyUpdate(Lobby lobby); - - public event HandshakeResponse? OnHandshakeResponse; - - /// - /// Fired after querying the server for lobbies - /// - public event LobbiesResponse? OnLobbiesResponse; - - /// - /// Fired after asking the server to create a lobby - /// - public event CreateLobbyResponse? OnCreateLobbyResponse; - - /// - /// Fired when a lobby gets deleted - /// - public event LobbyDeleted? OnLobbyDeleted; - - /// - /// Fired after asking the server to join a lobby - /// - public event LobbyConnectResponse? OnLobbyConnect; - - /// - /// Fired when a lobby gets modified - /// - public event LobbyUpdate? OnLobbyUpdate; - - private readonly string _address; - private readonly int _port; - private readonly CancellationToken _ct; - private readonly TcpClient _tcpClient; - private Task? _clientTask; - - /// - /// Underlying stream with the server - /// - public NetworkStream? Stream { get; private set; } - - public ClientConnection(string address, int port, CancellationToken ct) { - _address = address; - _port = port; - _ct = ct; - _tcpClient = new TcpClient(address, port); - OnPacketReceived += PacketReceivedAsync; - } - - private async Task PacketReceivedAsync(uint id, IPacket packet, NetworkStream stream, List bytes) { - Stream ??= stream; - - switch (packet) { - case HandshakeResponsePacket handshakeResponsePacket: - var handshakeResult = OnHandshakeResponse?.BeginInvoke(handshakeResponsePacket.Ok, null, null); - if (handshakeResult == null) { - return true; - } - - await handshakeResult.AsyncWaitHandle.WaitAsync(_ct); - break; - case GetLobbiesResponsePacket lobbiesResponsePacket: - var lobbiesResponseResult = OnLobbiesResponse?.BeginInvoke(lobbiesResponsePacket.Lobbies.ToArray(), null, null); - if (lobbiesResponseResult == null) { - break; - } - - await lobbiesResponseResult.AsyncWaitHandle.WaitAsync(_ct); - break; - case CreateLobbyResponsePacket createLobbyResponsePacket: - var createLobbyResult = OnCreateLobbyResponse?.BeginInvoke(createLobbyResponsePacket.Id, null, null); - if (createLobbyResult == null) { - break; - } - - await createLobbyResult.AsyncWaitHandle.WaitAsync(_ct); - break; - case LobbyDeletedPacket lobbyDeletedPacket: - var lobbyDeletedResult = OnLobbyDeleted?.BeginInvoke(lobbyDeletedPacket.Id, null, null); - if (lobbyDeletedResult == null) { - break; - } - - await lobbyDeletedResult.AsyncWaitHandle.WaitAsync(_ct); - break; - case LobbyConnectResponsePacket lobbyConnectResponsePacket: - var lobbyConnectResult = OnLobbyConnect?.BeginInvoke(lobbyConnectResponsePacket.Ok, null, null); - if (lobbyConnectResult == null) { - break; - } - - await lobbyConnectResult.AsyncWaitHandle.WaitAsync(_ct); - break; - case LobbyUpdatePacket lobbyUpdatePacket: - var lobbyUpdateResult = OnLobbyUpdate?.BeginInvoke(lobbyUpdatePacket.Lobby, null, null); - if (lobbyUpdateResult == null) { - break; - } - - await lobbyUpdateResult.AsyncWaitHandle.WaitAsync(_ct); - break; - } - - return false; - } - - /// - /// Initializes the connection to the server - /// - public async Task ConnectAsync() { - await _tcpClient.ConnectAsync(_address, _port, _ct); - _clientTask = ManageClientAsync(0, _tcpClient, _ct); - } - - protected override async Task ManageKeepAlivePacketAsync(KeepAlivePacket packet, NetworkStream stream, - List bytes) => await stream.WritePacketAsync(packet, bytes); -} diff --git a/OpenPolytopia.Common/Network/INetworkSerializable.cs b/OpenPolytopia.Common/Network/INetworkSerializable.cs deleted file mode 100644 index 83d92e27..00000000 --- a/OpenPolytopia.Common/Network/INetworkSerializable.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace OpenPolytopia.Common.Network; - -/// -/// Interface for types that need to be serialized to be sent on the network -/// -public interface INetworkSerializable { - /// - /// Serialize data - /// - /// the bytes where to serialize into, use - public void Serialize(List bytes); - - /// - /// Deserialize data - /// - /// the buffer bytes where to read from - /// the index where to start reading - /// - /// It is assumed that every Deserialize operation increments index as needed. - /// For example, increments index by one - /// while increments it by four - /// - public void Deserialize(byte[] bytes, ref uint index); -} diff --git a/OpenPolytopia.Common/Network/NetworkConnection.cs b/OpenPolytopia.Common/Network/NetworkConnection.cs deleted file mode 100644 index 74737289..00000000 --- a/OpenPolytopia.Common/Network/NetworkConnection.cs +++ /dev/null @@ -1,203 +0,0 @@ -namespace OpenPolytopia.Common.Network; - -using System.Collections.Concurrent; -using System.Net.Sockets; -using DotNext.Threading; -using Packets; - -public abstract class NetworkConnection { - private Func? _callback; - - public readonly ConcurrentDictionary> Channels = new(); - - /// - /// Event handler for receiving packets - /// - /// - /// uint id -> id of the client that sent the packet - ///
- /// IPacket packet -> packet that fired this event - ///
- /// NetworkStream stream -> stream with the client that sent this packet - ///
- /// List<byte> bytes -> list to use with - ///
- public delegate Task PacketReceived(uint id, IPacket packet, NetworkStream stream, - List bytes); - - public delegate Task ClientDisconnected(uint id); - - /// - /// Event fired when receiving packets - /// - /// - public event PacketReceived? OnPacketReceived; - - public event ClientDisconnected? OnClientDisconnected; - - /// - /// How to manage the receiving of the - /// - /// the keep alive packet - /// the stream from the client that sent it - /// the bytes where to write the response, if needed - /// a completable task; use async if possible - protected abstract Task ManageKeepAlivePacketAsync(KeepAlivePacket packet, NetworkStream stream, List bytes); - - /// - /// Registers a new callback to use for client connections. - ///
- /// It starts executing before trying to read the first packet from the client and passes the stream of the client - /// and the cancellation token that uses - ///
- /// the callback function - /// - /// Used in to send s to client and close the connection - /// if a certain amount of time have passed without the client sending it back - /// - protected void RegisterCustomStreamHandler(Func callback) => - _callback = callback; - - protected async Task FireClientDisconnected(uint id) { - var result = OnClientDisconnected?.BeginInvoke(id, null, null); - if (result == null) { - return; - } - - await result.AsyncWaitHandle.WaitAsync(); - } - - /// - /// Manages a connected client - /// - /// the id of the client - /// the client object - /// the cancellation token - protected async Task ManageClientAsync(uint id, TcpClient client, CancellationToken ct) { - // Get the stream for the client - await using var stream = client.GetStream(); - - // initialize the channel - Channels[id] = new ConcurrentQueue(); - - // Prepare data - List responseBytes = []; - List bytes = []; - - var cts = new CancellationTokenSource(); - Task? task = null; - if (_callback != null) { - task = _callback(stream, cts, id); - task.Start(); - } - - try { - // Loop while the server's running or the connection wasn't closed because of missing KeepAlivePacket - while (!ct.IsCancellationRequested && !cts.IsCancellationRequested) { - // Prepare data for incoming packet - var newPacket = true; - var contentLength = 0u; - - // Read incoming packet if data available or while missing bytes - while (stream.DataAvailable || (contentLength != 0u && bytes.Count < contentLength)) { - // compute buffer size to read exactly one packet - var bufferSize = 4L; - if (contentLength != 0u) { - bufferSize = contentLength - bytes.Count; - } - - // Read data from stream - var bufferBytes = new byte[bufferSize]; - var read = await stream.ReadAsync(bufferBytes.AsMemory(), ct); - - // Offset of where to start reading in the buffer - var offset = 0; - - // If this is a new packet, read the content length of the packet - if (newPacket) { - newPacket = false; - var indexContentLength = 0u; - contentLength = UIntExtension.Deserialize(contentLength, bufferBytes, ref indexContentLength); - // Set offset to 4 because we read an uint - offset += 4; - } - - // Copy the buffer over to our list starting from the offset - for (var i = offset; i < read; i++) { - bytes.Add(bufferBytes[i]); - } - } - - // Takes the first four bytes and parse the packet id - var packetIdBytes = new[] { bytes[0], bytes[1], bytes[2], bytes[3] }; - bytes.RemoveRange(0, 4); - var indexPacketId = 0u; - var packetId = 0u; - packetId.Deserialize(packetIdBytes, ref indexPacketId); - - // Now we get the packet type from the registered packets and call the Default method to initialize it - var contentBytes = bytes.ToArray(); - var packetType = PacketRegistrar.GetPacket(packetId); - var packet = (IPacket?)Activator.CreateInstance(packetType); - - // check if channel has data - if (!Channels[id].IsEmpty) { - var channel = Channels[id]; - while (channel.TryDequeue(out var channelPacket)) { - stream.PreparePacket(channelPacket, responseBytes); - } - } - - // if packet wasn't registered, skip this packet - if (packet == null) { - continue; - } - - // else, deserialize it and manage it - packet.Deserialize(contentBytes); - if (await ManagePacketAsync(id, packet, stream, responseBytes, ct)) { - break; - } - - if (responseBytes.Count > 0) { - await stream.SendPacketsAsync(responseBytes); - } - } - } - catch (OperationCanceledException) { - } - finally { - // Check if the cancellation token of the callback wasn't already signalled, if so signal to cancel - if (!cts.IsCancellationRequested) { - await cts.CancelAsync(); - } - - // Check if a task was registered, if so waits for its completion - if (task != null) { - await task; - } - - // remove the channel - Channels.Remove(id, out _); - - // Connection should be closed now - client.Close(); - } - } - - private async Task ManagePacketAsync(uint id, IPacket packet, NetworkStream stream, List responseBytes, - CancellationToken ct) { - if (packet is KeepAlivePacket keepAlivePacket) { - await ManageKeepAlivePacketAsync(keepAlivePacket, stream, responseBytes); - - return false; - } - - var result = OnPacketReceived?.BeginInvoke(id, packet, stream, responseBytes, null, null); - if (result == null) { - return false; - } - - return await result.AsyncWaitHandle.WaitAsync(TimeSpan.MaxValue, token: ct); - } -} diff --git a/OpenPolytopia.Common/Network/PacketRegistrar.cs b/OpenPolytopia.Common/Network/PacketRegistrar.cs deleted file mode 100644 index 859b0fe6..00000000 --- a/OpenPolytopia.Common/Network/PacketRegistrar.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace OpenPolytopia.Common.Network; - -using Packets; - -public static class PacketRegistrar { - private static readonly Dictionary _packets = new(32); - private static readonly Dictionary _packetIds = new(32); - - /// - /// Register a new packet with the given ID - /// - /// the id of the packet - /// the type of the packet - public static void RegisterPacket(uint id) where T : IPacket { - _packets.Add(id, typeof(T)); - _packetIds.Add(typeof(T), id); - } - - /// - /// Returns a packet type given the ID - /// - /// the id of the packet - /// the packet type - public static Type GetPacket(uint id) => _packets[id]; - - /// - /// Returns a packet ID given its type - /// - /// the packet - /// the packet type - /// the packet ID - public static uint GetPacketId(T packet) where T : IPacket => _packetIds[typeof(T)]; - - /// - /// Requests a new ID from the registrar - /// - /// a new usable id to register a packet - public static uint RequestNewId() => (uint)_packets.Count; - - /// - /// Register all known packets - /// - public static void RegisterAllPackets() { - RegisterPacket(0); - RegisterPacket(1); - RegisterPacket(2); - RegisterPacket(3); - RegisterPacket(4); - RegisterPacket(5); - RegisterPacket(6); - RegisterPacket(7); - RegisterPacket(8); - RegisterPacket(9); - RegisterPacket(10); - RegisterPacket(11); - RegisterPacket(12); - RegisterPacket(13); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/CreateLobbyPacket.cs b/OpenPolytopia.Common/Network/Packets/CreateLobbyPacket.cs deleted file mode 100644 index 32888819..00000000 --- a/OpenPolytopia.Common/Network/Packets/CreateLobbyPacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class CreateLobbyPacket : IPacket { - public uint MaxPlayers; - - public void Serialize(List bytes) => MaxPlayers.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - MaxPlayers.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/CreateLobbyResponsePacket.cs b/OpenPolytopia.Common/Network/Packets/CreateLobbyResponsePacket.cs deleted file mode 100644 index c63a9b0d..00000000 --- a/OpenPolytopia.Common/Network/Packets/CreateLobbyResponsePacket.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class CreateLobbyResponsePacket : IPacket { - public bool Ok; - public uint Id; - - public void Serialize(List bytes) { - Ok.Serialize(bytes); - Id.Serialize(bytes); - } - - public void Deserialize(byte[] bytes) { - var index = 0u; - Ok.Deserialize(bytes, ref index); - Id.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/GetLobbiesPacket.cs b/OpenPolytopia.Common/Network/Packets/GetLobbiesPacket.cs deleted file mode 100644 index 66237972..00000000 --- a/OpenPolytopia.Common/Network/Packets/GetLobbiesPacket.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -/// -/// Ask for lobbies on the server -/// -public class GetLobbiesPacket : IPacket { - public void Serialize(List bytes) { } - - public void Deserialize(byte[] bytes) { } -} diff --git a/OpenPolytopia.Common/Network/Packets/GetLobbiesResponsePacket.cs b/OpenPolytopia.Common/Network/Packets/GetLobbiesResponsePacket.cs deleted file mode 100644 index 5c16160a..00000000 --- a/OpenPolytopia.Common/Network/Packets/GetLobbiesResponsePacket.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -/// -/// Result of the lobbies query -/// -/// -public class GetLobbiesResponsePacket : IPacket { - public List Lobbies = []; - - public void Serialize(List bytes) => Lobbies.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Lobbies.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/HandshakePacket.cs b/OpenPolytopia.Common/Network/Packets/HandshakePacket.cs deleted file mode 100644 index c7df014b..00000000 --- a/OpenPolytopia.Common/Network/Packets/HandshakePacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class HandshakePacket : IPacket { - public string Version = ""; - - public void Serialize(List bytes) => Version.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Version = Version.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/HandshakeResponsePacket.cs b/OpenPolytopia.Common/Network/Packets/HandshakeResponsePacket.cs deleted file mode 100644 index 18db327e..00000000 --- a/OpenPolytopia.Common/Network/Packets/HandshakeResponsePacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class HandshakeResponsePacket : IPacket { - public bool Ok; - - public void Serialize(List bytes) => Ok.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Ok.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/IPacket.cs b/OpenPolytopia.Common/Network/Packets/IPacket.cs deleted file mode 100644 index 78b1fe19..00000000 --- a/OpenPolytopia.Common/Network/Packets/IPacket.cs +++ /dev/null @@ -1,217 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -using System.Net.Sockets; - -/// -/// Interface to declare a packet -/// -public interface IPacket { - /// - /// Serializes a packet to binary data - /// - /// the bytes to write to - public void Serialize(List bytes); - - /// - /// Deserializes binary data to a packet - /// - /// the binary data to deserialize - public void Deserialize(byte[] bytes); -} - -#region Extensions - -// Every Deserialize should increment the index - -public static class UIntExtension { - private const int FIRST_BYTE = 0; - private const int SECOND_BYTE = 8; - private const int THIRD_BYTE = 16; - private const int FOURTH_BYTE = 24; - - private const int EIGHT_BITS = 255; - - public static void Serialize(this uint value, List bytes) { - bytes.Add((byte)value.GetBits(EIGHT_BITS, FOURTH_BYTE)); - bytes.Add((byte)value.GetBits(EIGHT_BITS, THIRD_BYTE)); - bytes.Add((byte)value.GetBits(EIGHT_BITS, SECOND_BYTE)); - bytes.Add((byte)value.GetBits(EIGHT_BITS, FIRST_BYTE)); - } - - public static byte[] Serialize(this uint value) { - var bytes = new byte[4]; - bytes[0] = (byte)value.GetBits(EIGHT_BITS, FOURTH_BYTE); - bytes[1] = (byte)value.GetBits(EIGHT_BITS, THIRD_BYTE); - bytes[2] = (byte)value.GetBits(EIGHT_BITS, SECOND_BYTE); - bytes[3] = (byte)value.GetBits(EIGHT_BITS, FIRST_BYTE); - return bytes; - } - - public static void Deserialize(this ref uint value, byte[] bytes, ref uint index) { - value.SetBits(bytes[index++], EIGHT_BITS, FOURTH_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, THIRD_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, SECOND_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, FIRST_BYTE); - } - - public static uint Deserialize(uint value, byte[] bytes, ref uint index) { - value.SetBits(bytes[index++], EIGHT_BITS, FOURTH_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, THIRD_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, SECOND_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, FIRST_BYTE); - return value; - } -} - -public static class IntExtension { - private const int FIRST_BYTE = 0; - private const int SECOND_BYTE = 8; - private const int THIRD_BYTE = 16; - private const int FOURTH_BYTE = 24; - - private const int EIGHT_BITS = 255; - - public static void Serialize(this int value, List bytes) { - bytes.Add((byte)value.GetBits(EIGHT_BITS, FOURTH_BYTE)); - bytes.Add((byte)value.GetBits(EIGHT_BITS, THIRD_BYTE)); - bytes.Add((byte)value.GetBits(EIGHT_BITS, SECOND_BYTE)); - bytes.Add((byte)value.GetBits(EIGHT_BITS, FIRST_BYTE)); - } - - public static void Deserialize(this ref int value, byte[] bytes, ref uint index) { - value.SetBits(bytes[index++], EIGHT_BITS, FOURTH_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, THIRD_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, SECOND_BYTE); - value.SetBits(bytes[index++], EIGHT_BITS, FIRST_BYTE); - } -} - -public static class BoolExtension { - public static void Serialize(this bool value, List bytes) => bytes.Add((byte)value.ToUInt()); - - public static void Deserialize(this ref bool value, byte[] bytes, ref uint index) => value = bytes[index++] == 1; -} - -public static class StringExtension { - public static void Serialize(this string str, List bytes) { - str.Length.Serialize(bytes); - bytes.AddRange(str.Select(c => (byte)c)); - } - - public static string Deserialize(this string str, byte[] bytes, ref uint index) { - var length = UIntExtension.Deserialize(0u, bytes, ref index); - - for (var i = 0; i < length; i++) { - str = str.Insert(i, ((char)bytes[index++]).ToString()); - } - - return str; - } -} - -public static class ListExtension { - public static void Serialize(this List list, List bytes) { - list.Count.Serialize(bytes); - foreach (var element in list) { - switch (element) { - case uint uintValue: - uintValue.Serialize(bytes); - break; - case int intValue: - intValue.Serialize(bytes); - break; - case bool boolValue: - boolValue.Serialize(bytes); - break; - case string str: - str.Serialize(bytes); - break; - case INetworkSerializable networkSerializable: - networkSerializable.Serialize(bytes); - break; - } - } - } - - public static void Deserialize(this List list, byte[] bytes, ref uint index) { - var length = UIntExtension.Deserialize(0u, bytes, ref index); - - for (var i = 0; i < length; i++) { - var value = 0u; - value.Deserialize(bytes, ref index); - list.Add(value); - } - } - - public static void Deserialize(this List list, byte[] bytes, ref uint index) { - var length = UIntExtension.Deserialize(0u, bytes, ref index); - - for (var i = 0; i < length; i++) { - var value = 0; - value.Deserialize(bytes, ref index); - list.Add(value); - } - } - - public static void Deserialize(this List list, byte[] bytes, ref uint index) { - var length = UIntExtension.Deserialize(0u, bytes, ref index); - - for (var i = 0; i < length; i++) { - var value = false; - value.Deserialize(bytes, ref index); - list.Add(value); - } - } - - public static void Deserialize(this List list, byte[] bytes, ref uint index) { - var length = UIntExtension.Deserialize(0u, bytes, ref index); - - for (var i = 0; i < length; i++) { - var str = ""; - str = str.Deserialize(bytes, ref index); - list.Add(str); - } - } - - public static void Deserialize(this List list, byte[] bytes, ref uint index) - where T : INetworkSerializable, new() { - var length = UIntExtension.Deserialize(0u, bytes, ref index); - - for (var i = 0; i < length; i++) { - var value = new T(); - value.Deserialize(bytes, ref index); - list.Add(value); - } - } -} - -public static class NetworkStreamExtension { - public static void PreparePacket(this NetworkStream stream, IPacket packet, List bytes) { - // get the packet id - var id = PacketRegistrar.GetPacketId(packet); - // compute the start index of the packet - var startIndex = bytes.Count; - // serialize the id - id.Serialize(bytes); - // serialize the packet - packet.Serialize(bytes); - // get the bytes count and serialize it to a temp byte array - var contentLength = ((uint)bytes.Count).Serialize(); - // insert the temp byte array at the start of the bytes to send - bytes.InsertRange(startIndex, contentLength); - } - - public static async Task SendPacketsAsync(this NetworkStream stream, List bytes) { - // send the bytes - await stream.WriteAsync(bytes.ToArray()); - // clear the list - bytes.Clear(); - } - - public static async Task WritePacketAsync(this NetworkStream stream, IPacket packet, List bytes) { - PreparePacket(stream, packet, bytes); - await SendPacketsAsync(stream, bytes); - } -} - -#endregion diff --git a/OpenPolytopia.Common/Network/Packets/KeepAlivePacket.cs b/OpenPolytopia.Common/Network/Packets/KeepAlivePacket.cs deleted file mode 100644 index a29fde12..00000000 --- a/OpenPolytopia.Common/Network/Packets/KeepAlivePacket.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -/// -/// Keep alive packet -/// -public class KeepAlivePacket : IPacket { - public uint Captcha; - - public void Serialize(List bytes) => Captcha.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Captcha.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/LobbyConnectPacket.cs b/OpenPolytopia.Common/Network/Packets/LobbyConnectPacket.cs deleted file mode 100644 index 55539063..00000000 --- a/OpenPolytopia.Common/Network/Packets/LobbyConnectPacket.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class LobbyConnectPacket : IPacket { - public uint Id; - public void Serialize(List bytes) => Id.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Id.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/LobbyConnectResponsePacket.cs b/OpenPolytopia.Common/Network/Packets/LobbyConnectResponsePacket.cs deleted file mode 100644 index 9e8eb71a..00000000 --- a/OpenPolytopia.Common/Network/Packets/LobbyConnectResponsePacket.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class LobbyConnectResponsePacket : IPacket { - public bool Ok; - public void Serialize(List bytes) => Ok.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Ok.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/LobbyDeletedPacket.cs b/OpenPolytopia.Common/Network/Packets/LobbyDeletedPacket.cs deleted file mode 100644 index 81a50427..00000000 --- a/OpenPolytopia.Common/Network/Packets/LobbyDeletedPacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class LobbyDeletedPacket : IPacket { - public uint Id; - - public void Serialize(List bytes) => Id.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Id.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/LobbyDisconnectPacket.cs b/OpenPolytopia.Common/Network/Packets/LobbyDisconnectPacket.cs deleted file mode 100644 index c7bb4733..00000000 --- a/OpenPolytopia.Common/Network/Packets/LobbyDisconnectPacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class LobbyDisconnectPacket : IPacket { - public uint Id; - - public void Serialize(List bytes) => Id.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Id.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/LobbyDisconnectResponsePacket.cs b/OpenPolytopia.Common/Network/Packets/LobbyDisconnectResponsePacket.cs deleted file mode 100644 index c8bd9f14..00000000 --- a/OpenPolytopia.Common/Network/Packets/LobbyDisconnectResponsePacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class LobbyDisconnectResponsePacket : IPacket { - public bool Ok; - - public void Serialize(List bytes) => Ok.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Ok.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/LobbyUpdatePacket.cs b/OpenPolytopia.Common/Network/Packets/LobbyUpdatePacket.cs deleted file mode 100644 index a3129fb0..00000000 --- a/OpenPolytopia.Common/Network/Packets/LobbyUpdatePacket.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -public class LobbyUpdatePacket : IPacket { - public Lobby Lobby = new(); - public void Serialize(List bytes) => Lobby.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Lobby.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/RegisterUserPacket.cs b/OpenPolytopia.Common/Network/Packets/RegisterUserPacket.cs deleted file mode 100644 index 958dba95..00000000 --- a/OpenPolytopia.Common/Network/Packets/RegisterUserPacket.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -/// -/// Register a new user on the server -/// -public class RegisterUserPacket : IPacket { - public string Name = ""; - - public void Serialize(List bytes) => Name.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Name = Name.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/Packets/RegisterUserResponsePacket.cs b/OpenPolytopia.Common/Network/Packets/RegisterUserResponsePacket.cs deleted file mode 100644 index 8436c5b1..00000000 --- a/OpenPolytopia.Common/Network/Packets/RegisterUserResponsePacket.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace OpenPolytopia.Common.Network.Packets; - -/// -/// Result of the registration -/// -/// -public class RegisterUserResponsePacket : IPacket { - public bool Ok; - - public void Serialize(List bytes) => Ok.Serialize(bytes); - - public void Deserialize(byte[] bytes) { - var index = 0u; - Ok.Deserialize(bytes, ref index); - } -} diff --git a/OpenPolytopia.Common/Network/ServerConnection.cs b/OpenPolytopia.Common/Network/ServerConnection.cs deleted file mode 100644 index 842ab5fc..00000000 --- a/OpenPolytopia.Common/Network/ServerConnection.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace OpenPolytopia.Common.Network; - -using System.Collections.Concurrent; -using System.Net.Sockets; -using System.Security.Cryptography; -using Packets; - -public class ServerConnection(int port) : NetworkConnection, IDisposable { - private readonly TcpListener _tcpListener = TcpListener.Create(port); - private readonly ConcurrentDictionary _clientTasks = []; - private readonly CancellationTokenSource _cts = new(); - private readonly ConcurrentDictionary _timerCancellationTokens = new(); - - - /// - /// Starts the server - /// - public void Start() { - RegisterCustomStreamHandler(SendKeepAliveAsync); - _tcpListener.Start(); - } - - /// - /// Stops the server - /// - public void Stop() { - // Stop the listener, we won't accept incoming requests - _tcpListener.Stop(); - // Stop all the running tasks - _cts.Cancel(); - - // Wait for all tasks to complete - foreach (var task in _clientTasks.Values) { - task.Wait(); - } - - // Dispose of the tasks - RemoveCompletedTasks(); - } - - /// - /// Listens to incoming connections - /// - /// - /// Remember to call before listening to connections - /// - public async Task ListenAsync() { - var client = await _tcpListener.AcceptTcpClientAsync(); - var id = (uint)RandomNumberGenerator.GetInt32(0, short.MaxValue); - var task = ManageClientAsync(id, client, _cts.Token); - task.Start(); - _clientTasks.TryAdd(id, task); - } - - /// - /// Removes all the tasks where the connection was closed - /// - public void Update() => RemoveCompletedTasks(); - - private void RemoveCompletedTasks() { - foreach (var id in _clientTasks.Keys) { - if (!_clientTasks[id].IsCompleted) { - continue; - } - - if (_clientTasks.TryRemove(id, out var task)) { - task.Dispose(); - } - } - } - - protected override async Task ManageKeepAlivePacketAsync(KeepAlivePacket packet, NetworkStream stream, - List bytes) { - if (_timerCancellationTokens.TryGetValue(packet.Captcha, out var cts)) { - await cts.CancelAsync(); - } - } - - private async Task SendKeepAliveAsync(NetworkStream stream, CancellationTokenSource cts, uint id) { - try { - var timer = new PeriodicTimer(TimeSpan.FromSeconds(2)); - List bytes = []; - var ct = new CancellationTokenSource(); - while (!cts.IsCancellationRequested) { - await timer.WaitForNextTickAsync(cts.Token); - var packet = new KeepAlivePacket { Captcha = (uint)RandomNumberGenerator.GetInt32(short.MaxValue) }; - var captcha = packet.Captcha; - await stream.WritePacketAsync(packet, bytes); - - if (ct.TryReset()) { - _timerCancellationTokens[captcha] = ct; - } - else { - ct = new CancellationTokenSource(); - _timerCancellationTokens[captcha] = ct; - } - - try { - await timer.WaitForNextTickAsync(ct.Token); - if (!cts.IsCancellationRequested) { - await cts.CancelAsync(); - await FireClientDisconnected(id); - } - } - catch (OperationCanceledException) { } - } - } - catch (OperationCanceledException) { - } - } - - public void Dispose() { - _tcpListener.Dispose(); - _cts.Dispose(); - GC.SuppressFinalize(this); - } -} diff --git a/OpenPolytopia.Common/PlayerData.cs b/OpenPolytopia.Common/PlayerData.cs deleted file mode 100644 index 9693d990..00000000 --- a/OpenPolytopia.Common/PlayerData.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace OpenPolytopia.Common; - -using Network; -using Network.Packets; - -public class PlayerData : INetworkSerializable { - /// - /// Name of the player - /// - public string PlayerName { get; set; } = ""; - - public void Serialize(List bytes) => PlayerName.Serialize(bytes); - - public void Deserialize(byte[] bytes, ref uint index) => PlayerName = PlayerName.Deserialize(bytes, ref index); -} diff --git a/OpenPolytopia.sln.DotSettings.user b/OpenPolytopia.sln.DotSettings.user index 4b57c481..1ed850a4 100644 --- a/OpenPolytopia.sln.DotSettings.user +++ b/OpenPolytopia.sln.DotSettings.user @@ -1,5 +1,6 @@  ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/OpenPolytopia/src/Client.cs b/OpenPolytopia/src/Client.cs deleted file mode 100644 index 4ad4b704..00000000 --- a/OpenPolytopia/src/Client.cs +++ /dev/null @@ -1,107 +0,0 @@ -namespace OpenPolytopia; - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Common.Network; -using Common.Network.Packets; -using DotNext.Threading; - -public class Client { - /// - /// Public instance of the client - /// - /// - /// Remember to initialize the client at least once - /// - public static Client? Instance { get; private set; } - - public delegate Task Connected(); - - /// - /// Fired when the client successfully connects to the server - /// - public event Connected? OnConnected; - - /// - /// The underlying connection which manages messages to/from the server - /// - public ClientConnection ClientConnection { get; } - - public Client(string address, int port, CancellationToken ct) { - ClientConnection = new ClientConnection(address, port, ct); - ClientConnection.OnHandshakeResponse += async handshakeResult => { - if (OnConnected == null || !handshakeResult) { - return; - } - - var result = OnConnected.BeginInvoke(null, null); - await result.AsyncWaitHandle.WaitAsync(); - }; - Instance ??= this; - } - - /// - /// Initializes the connection to the server - /// - public async Task ConnectAsync() => await ClientConnection.ConnectAsync(); - - /// - /// Registers the user on the server - /// - /// name of the player - /// optional list to use instead of allocating a new one - public async Task RegisterUserAsync(string name, List? bytes = null) { - bytes ??= new List(16); - if (ClientConnection.Stream != null) { - await ClientConnection.Stream.WritePacketAsync(new RegisterUserPacket { Name = name }, bytes); - } - } - - /// - /// Query the server for all the lobbies - /// - /// optional list to use instead of allocating a new one - public async Task GetLobbiesAsync(List? bytes = null) { - bytes ??= new List(16); - if (ClientConnection.Stream != null) { - await ClientConnection.Stream.WritePacketAsync(new GetLobbiesPacket(), bytes); - } - } - - /// - /// Creates a new lobby on the server - /// - /// the number of players - /// optional list to use instead of allocating a new one - public async Task CreateLobbyAsync(uint maxPlayer, List? bytes = null) { - bytes ??= new List(16); - if (ClientConnection.Stream != null) { - await ClientConnection.Stream.WritePacketAsync(new CreateLobbyPacket { MaxPlayers = maxPlayer }, bytes); - } - } - - /// - /// Connects to the lobby - /// - /// the id of the lobby to connect to - /// optional list to use instead of allocating a new one - public async Task LobbyConnectAsync(uint lobbyId, List? bytes = null) { - bytes ??= new List(16); - if (ClientConnection.Stream != null) { - await ClientConnection.Stream.WritePacketAsync(new LobbyConnectPacket { Id = lobbyId }, bytes); - } - } - - /// - /// Disconnects from a lobby - /// - /// the id of the lobby to disconnect from - /// optional list to use instead of allocating a new one - public async Task LobbyDisconnectAsync(uint lobbyId, List? bytes = null) { - bytes ??= new List(16); - if (ClientConnection.Stream != null) { - await ClientConnection.Stream.WritePacketAsync(new LobbyDisconnectPacket { Id = lobbyId }, bytes); - } - } -} diff --git a/OpenPolytopia/src/Game.cs b/OpenPolytopia/src/Game.cs index a263932d..8cb402c3 100644 --- a/OpenPolytopia/src/Game.cs +++ b/OpenPolytopia/src/Game.cs @@ -11,11 +11,6 @@ public override void _Ready() { return; } - // Check if it is the server, if not just wait for the player to click play - if (!OS.HasFeature("dedicated_server")) { - return; - } - GetTree().ChangeSceneToPacked(LobbyScene); } @@ -23,20 +18,13 @@ public override void _Ready() { /// Sets the new name for the player /// /// the new player's name - private void OnNameChanged(string name) => PlayerData.Instance.Data.PlayerName = name; + private void OnNameChanged(string name) { } /// /// Waits until the player press the play button, creates a new random name if the player hasn't chosen one /// and connects him to the lobby /// private void OnPlayPressed() { - var playerData = PlayerData.Instance; - // Generate player's name if missing - if (playerData.Data.PlayerName.Length == 0) { - var rng = new RandomNumberGenerator(); - playerData.Data.PlayerName = $"Player{rng.Randi()}"; - } - GetTree().ChangeSceneToPacked(LobbyScene); } } diff --git a/OpenPolytopia/src/Lobby.cs b/OpenPolytopia/src/Lobby.cs deleted file mode 100644 index 5e322b67..00000000 --- a/OpenPolytopia/src/Lobby.cs +++ /dev/null @@ -1,204 +0,0 @@ -namespace OpenPolytopia; - -using Godot; -using Godot.Collections; - -public partial class Lobby : Control { - private const string ADDRESS = "enn3.ovh"; - private const int PORT = 6969; - - /// - /// Instance of the lobby - /// - public static Lobby Instance = null!; - - /// - /// Fired when a player connects to the lobby - /// - [Signal] - public delegate void PlayerConnectedEventHandler(int id, PlayerData playerData); - - /// - /// Fired when a player disconnects from the lobby - /// - [Signal] - public delegate void PlayerDisconnectedEventHandler(int id); - - /// - /// Fired when the server closes - /// - [Signal] - public delegate void ServerDisconnectedEventHandler(); - - /// - /// The game scene to switch to - /// - public PackedScene? GameScene; - - private uint _playersInLobby; - private uint _playersStarted; - private Dictionary _playerData = new(); - - public override void _Ready() { - // Set the instance of the lobby to the current lobby - Instance = this; - - // Connects all the signals - Multiplayer.PeerConnected += OnPlayerConnected; - Multiplayer.PeerDisconnected += OnPlayerDisconnected; - Multiplayer.ConnectedToServer += OnConnectOk; - Multiplayer.ConnectionFailed += OnConnectionFail; - Multiplayer.ServerDisconnected += OnServerDisconnected; - - // if it is the server, create the lobby - if (OS.HasFeature("dedicated_server")) { - CreateGame(); - } - // else, it's a player then join the lobby - else { - // TODO: choose tribe before joining game - JoinGame(); - } - } - - /// - /// Changes the scene to the game scene - /// - /// - /// This is called on all the players - /// - [Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)] - private void StartGame() { - // Check if the game scene is valid - if (GameScene == null) { - GD.PrintErr("GameScene is null"); - return; - } - - GetTree().ChangeSceneToPacked(GameScene); - } - - /// - /// Called when a player has loaded the game scene - /// - /// - /// This executes both on the player calling it and the server, so we check if we are the server - /// then check if all players are connected, if yes we start the game - /// - [Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)] - private void PlayerLoaded() { - if (!Multiplayer.IsServer()) { - return; - } - - if (++_playersStarted == _playersInLobby) { - GetNode("/root/PolyGame").StartGame(); - } - } - - /// - /// Joins the lobby as a player - /// - private void JoinGame() { - var peer = new ENetMultiplayerPeer(); - var error = peer.CreateClient(ADDRESS, PORT); - - if (error != Error.Ok) { - GD.PrintErr($"Error happened during client creation: {error}"); - } - - Multiplayer.MultiplayerPeer = peer; - } - - /// - /// Creates the lobby as the server - /// - private void CreateGame() { - var peer = new ENetMultiplayerPeer(); - var error = peer.CreateServer(PORT); - - if (error != Error.Ok) { - GD.PrintErr($"Error happened during server creation: {error}"); - } - - Multiplayer.MultiplayerPeer = peer; - } - - /// - /// Registers the connected players - /// - /// - /// This is called on all peers (server and players) for each player that connects to the lobby - /// - /// the player data of the newly connected player - [Rpc(MultiplayerApi.RpcMode.AnyPeer, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)] - private void RegisterPlayer(PlayerData playerData) { - var id = Multiplayer.GetRemoteSenderId(); - if (id == 1) { - return; - } - - _playerData[id] = playerData; - EmitSignal(SignalName.PlayerConnected, id); - } - - /// - /// Called when a new player connects - /// - /// the id of the new player connected to the lobby - private void OnPlayerConnected(long id) { - _playersInLobby++; - - // If we're the server, skip the registration phase - if (id == 1) { - return; - } - - RpcId(id, MethodName.RegisterPlayer, PlayerData.Instance); - } - - /// - /// Called when a player disconnects - /// - /// the id of the player disconnected - private void OnPlayerDisconnected(long id) { - _playersInLobby--; - - // If we're the server, skip the signal - if (id == 1) { - return; - } - - _playerData.Remove(id); - EmitSignal(SignalName.PlayerDisconnected, id); - } - - /// - /// Called on the player connecting when connected to the lobby - /// - private void OnConnectOk() { - var id = Multiplayer.GetUniqueId(); - - // If we're the server, skip the signal - if (id == 1) { - return; - } - - _playerData[id] = PlayerData.Instance; - EmitSignal(SignalName.PlayerConnected, id, PlayerData.Instance); - } - - /// - /// Called when the connection fails - /// - private void OnConnectionFail() => Multiplayer.MultiplayerPeer = null; - - /// - /// Called when the server disconnects - /// - private void OnServerDisconnected() { - Multiplayer.MultiplayerPeer = null; - _playerData.Clear(); - EmitSignal(SignalName.ServerDisconnected); - } -} diff --git a/OpenPolytopia/src/PlayerData.cs b/OpenPolytopia/src/PlayerData.cs deleted file mode 100644 index 90910673..00000000 --- a/OpenPolytopia/src/PlayerData.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace OpenPolytopia; - -using Godot; - -/// -/// Class holding all the player data -/// -public partial class PlayerData : Node { - /// - /// Gets the instance of the current player's data - /// - public static PlayerData Instance => ((SceneTree)Engine.GetMainLoop()).Root.GetNode("/root/PlayerData"); - - /// - /// Internal data to use to read/modify the player's data - /// - public Common.PlayerData Data { get; } = new(); -} diff --git a/OpenPolytopia/src/PolyGame.cs b/OpenPolytopia/src/PolyGame.cs index 260452fe..8381f7fc 100644 --- a/OpenPolytopia/src/PolyGame.cs +++ b/OpenPolytopia/src/PolyGame.cs @@ -3,22 +3,9 @@ namespace OpenPolytopia; using Godot; public partial class PolyGame : Node3D { - /// - /// Report back to the server that we're ready to start - /// public override void _Ready() { - Lobby.Instance.RpcId(1, Lobby.MethodName.PlayerLoaded); } public override void _Process(double delta) { } - - /// - /// Start the game - /// - /// - /// This is called only on the server - /// - public void StartGame() { - } } diff --git a/OpenPolytopia/src/SpacetimeNode.cs b/OpenPolytopia/src/SpacetimeNode.cs new file mode 100644 index 00000000..32136a4f --- /dev/null +++ b/OpenPolytopia/src/SpacetimeNode.cs @@ -0,0 +1,106 @@ +namespace OpenPolytopia; + +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Godot; +using SpacetimeDB; +using SpacetimeDB.Types; + +public partial class SpacetimeNode : Node { + public static SpacetimeNode Instance { get; private set; } = null!; + + private const string HOST = "https://spacetime.enn3.ovh"; + private const string DBNAME = "openpolytopia"; + + private Identity _identity; + public DbConnection Connection = null!; + public readonly ObservableCollection Lobbies = []; + + public override void _EnterTree() { + base._EnterTree(); + Instance = this; + AuthToken.Init(".stdb_openpolytopia"); + Connection = ConnectToDb(); + RegisterCallbacks(); + } + + public override void _ExitTree() { + base._ExitTree(); + Connection.Disconnect(); + } + + public override void _PhysicsProcess(double delta) => Connection.FrameTick(); + + private void RegisterCallbacks() { + Connection.Db.Lobby.OnInsert += (context, row) => { + Lobbies.Add( + new LobbyData { Id = row.Id, MaxPlayers = row.MaxPlayers, Players = row.Players, Ready = row.Ready }); + }; + + Connection.Db.Lobby.OnDelete += (context, row) => { + var data = Lobbies.FirstOrDefault(data => data.Id == row.Id); + if (data == null) { + return; + } + + Lobbies.Remove(data); + }; + + Connection.Db.Lobby.OnUpdate += (context, row, newRow) => { + var data = Lobbies.FirstOrDefault(data => data.Id == row.Id); + if (data != null) { + Lobbies.Remove(data); + } + + Lobbies.Add( + new LobbyData { + Id = newRow.Id, MaxPlayers = newRow.MaxPlayers, Players = newRow.Players, Ready = newRow.Ready + }); + }; + } + + private DbConnection ConnectToDb() { + var conn = DbConnection.Builder() + .WithUri(HOST) + .WithModuleName(DBNAME) + .WithToken(AuthToken.Token) + .OnConnect(OnConnected) + .OnConnectError(OnConnectError) + .OnDisconnect(OnDisconnected) + .Build(); + return conn; + } + + private void OnConnected(DbConnection conn, Identity identity, string authToken) { + AuthToken.SaveToken(authToken); + _identity = identity; + + conn.SubscriptionBuilder().OnApplied(context => { }).OnError((context, exception) => { }).Subscribe([ + // get lobbies the player joined + $"SELECT Lobby.* FROM Lobby JOIN LobbyPlayer ON Lobby.Id = LobbyPlayer.LobbyId WHERE LobbyPlayer.PlayerId = 0x{identity}", + "SELECT * FROM Player", + "SELECT * FROM LobbyPlayer" + ]); + } + + private void OnConnectError(Exception e) { + GD.PushError($"Error while connecting: {e}"); + } + + private void OnDisconnected(DbConnection conn, Exception? e) { + if (e != null) { + GD.PushError($"Disconnected abnormally: {e}"); + } + else { + GD.Print($"Disconnected normally."); + } + } +} + +public class LobbyData { + public ulong Id { get; init; } + public uint MaxPlayers; + public uint Players; + public uint Ready; +} diff --git a/OpenPolytopia/test/src/PacketTest.cs b/OpenPolytopia/test/src/PacketTest.cs deleted file mode 100644 index 16bacb72..00000000 --- a/OpenPolytopia/test/src/PacketTest.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace OpenPolytopia; - -using System.Collections.Generic; -using Chickensoft.GoDotTest; -using Common.Network.Packets; -using Godot; -using Shouldly; - -public class PacketTest(Node testScene) : TestClass(testScene) { - [Test] - public void TestHandshake() { - var packet = new HandshakePacket { Version = "0.1.0" }; - List bytes = []; - packet.Serialize(bytes); - var deserializedPacket = new HandshakePacket(); - deserializedPacket.Deserialize(bytes.ToArray()); - deserializedPacket.Version.ShouldBe("0.1.0"); - } - - [Test] - public void TestHandshakeResponse() { - var packet = new HandshakeResponsePacket { Ok = true }; - List bytes = []; - packet.Serialize(bytes); - var deserializedPacket = new HandshakeResponsePacket(); - deserializedPacket.Deserialize(bytes.ToArray()); - deserializedPacket.Ok.ShouldBeTrue(); - } - - [Test] - public void TestKeepAlive() { - var packet = new KeepAlivePacket { Captcha = 20u }; - List bytes = []; - packet.Serialize(bytes); - var deserializedPacket = new KeepAlivePacket(); - deserializedPacket.Deserialize(bytes.ToArray()); - deserializedPacket.Captcha.ShouldBe(20u); - } - - [Test] - public void TestGetLobbiesResponse() { - var lobby = new Common.Lobby { Id = 123, MaxPlayers = 4 }; - lobby.AddPlayer(new Common.PlayerData { PlayerName = "Test" }); - var packet = new GetLobbiesResponsePacket { Lobbies = [lobby] }; - List bytes = []; - packet.Serialize(bytes); - var deserializedPacket = new GetLobbiesResponsePacket(); - deserializedPacket.Deserialize(bytes.ToArray()); - deserializedPacket.Lobbies.Count.ShouldBe(1); - deserializedPacket.Lobbies[0].Id.ShouldBe(123u); - deserializedPacket.Lobbies[0].Players.Count.ShouldBe(1); - deserializedPacket.Lobbies[0].Players[0].PlayerName.ShouldBe("Test"); - } -} diff --git a/cspell.json b/cspell.json index 93ff143d..c5bca158 100644 --- a/cspell.json +++ b/cspell.json @@ -98,6 +98,8 @@ "Bardur", "Oumaji", "smithery", - "aquatism" + "aquatism", + "stdb", + "openpolytopia" ] } From 6ae5f28a6f02da70f542b57f96654e6ff73e1c02 Mon Sep 17 00:00:00 2001 From: Enn3Developer Date: Wed, 12 Mar 2025 17:13:16 +0100 Subject: [PATCH 5/8] readded global.json --- global.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 00000000..7db70cf2 --- /dev/null +++ b/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + "version": "8.0.113", + "rollForward": "major" + }, + "msbuild-sdks": { + "Godot.NET.Sdk": "4.4.0" + } +} From 9033fa8f55fdcdbc6243aef9aa7de9e627e3cde1 Mon Sep 17 00:00:00 2001 From: Enn3Developer Date: Wed, 12 Mar 2025 19:39:15 +0100 Subject: [PATCH 6/8] better getter/setter --- OpenPolytopia/src/SpacetimeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenPolytopia/src/SpacetimeNode.cs b/OpenPolytopia/src/SpacetimeNode.cs index 32136a4f..bc7f54a1 100644 --- a/OpenPolytopia/src/SpacetimeNode.cs +++ b/OpenPolytopia/src/SpacetimeNode.cs @@ -14,7 +14,7 @@ public partial class SpacetimeNode : Node { private const string DBNAME = "openpolytopia"; private Identity _identity; - public DbConnection Connection = null!; + public DbConnection Connection { get; private set; } = null!; public readonly ObservableCollection Lobbies = []; public override void _EnterTree() { From 1c158337ce577654a68fa5a567c293ded10dce32 Mon Sep 17 00:00:00 2001 From: Enn3Developer Date: Wed, 12 Mar 2025 19:42:01 +0100 Subject: [PATCH 7/8] fixed spellchecking --- cspell.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cspell.json b/cspell.json index c5bca158..d037caa6 100644 --- a/cspell.json +++ b/cspell.json @@ -14,7 +14,8 @@ "**/*.godot", "**/*.cfg", "**/*.csproj", - "**/*.uid" + "**/*.uid", + "**/ModuleBindings/**" ], "words": [ "animatable", @@ -100,6 +101,8 @@ "smithery", "aquatism", "stdb", - "openpolytopia" + "openpolytopia", + "DBNAME", + "SPACETIMEDB" ] } From 6c9459befbfafd09d5cc841607ce22024b14b18f Mon Sep 17 00:00:00 2001 From: Enn3Developer Date: Wed, 12 Mar 2025 19:48:20 +0100 Subject: [PATCH 8/8] added docs --- OpenPolytopia/src/SpacetimeNode.cs | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/OpenPolytopia/src/SpacetimeNode.cs b/OpenPolytopia/src/SpacetimeNode.cs index bc7f54a1..aca966fc 100644 --- a/OpenPolytopia/src/SpacetimeNode.cs +++ b/OpenPolytopia/src/SpacetimeNode.cs @@ -8,15 +8,29 @@ namespace OpenPolytopia; using SpacetimeDB.Types; public partial class SpacetimeNode : Node { + /// + /// Public singleton + /// public static SpacetimeNode Instance { get; private set; } = null!; private const string HOST = "https://spacetime.enn3.ovh"; private const string DBNAME = "openpolytopia"; private Identity _identity; + + /// + /// Accessor to the database connection + /// public DbConnection Connection { get; private set; } = null!; + + /// + /// Lobbies data; it updates + /// public readonly ObservableCollection Lobbies = []; + /// + /// Initialize the connection + /// public override void _EnterTree() { base._EnterTree(); Instance = this; @@ -25,34 +39,55 @@ public override void _EnterTree() { RegisterCallbacks(); } + /// + /// Disconnect from the database + /// public override void _ExitTree() { base._ExitTree(); Connection.Disconnect(); } + /// + /// Process messages + /// + /// ignored public override void _PhysicsProcess(double delta) => Connection.FrameTick(); + /// + /// Registering callbacks + /// private void RegisterCallbacks() { + // When a lobby gets added (because the player joined) Connection.Db.Lobby.OnInsert += (context, row) => { + // add the lobby to the observable list Lobbies.Add( new LobbyData { Id = row.Id, MaxPlayers = row.MaxPlayers, Players = row.Players, Ready = row.Ready }); }; + // When a lobby gets deleted (because the player left the lobby) Connection.Db.Lobby.OnDelete += (context, row) => { + // search for the lobby data in memory var data = Lobbies.FirstOrDefault(data => data.Id == row.Id); + // check if it exists if (data == null) { return; } + // remove it Lobbies.Remove(data); }; + // When a lobby gets updated (because another player joined, etc...) Connection.Db.Lobby.OnUpdate += (context, row, newRow) => { + // search for the lobby data in memory var data = Lobbies.FirstOrDefault(data => data.Id == row.Id); + // check if it exists if (data != null) { + // so delete it Lobbies.Remove(data); } + // finally, add back the lobby data to force the list to emit the event Lobbies.Add( new LobbyData { Id = newRow.Id, MaxPlayers = newRow.MaxPlayers, Players = newRow.Players, Ready = newRow.Ready @@ -60,6 +95,10 @@ private void RegisterCallbacks() { }; } + /// + /// Connect to the database + /// + /// the database connection private DbConnection ConnectToDb() { var conn = DbConnection.Builder() .WithUri(HOST) @@ -76,6 +115,7 @@ private void OnConnected(DbConnection conn, Identity identity, string authToken) AuthToken.SaveToken(authToken); _identity = identity; + // subscribe with these queries to get updates on the tables conn.SubscriptionBuilder().OnApplied(context => { }).OnError((context, exception) => { }).Subscribe([ // get lobbies the player joined $"SELECT Lobby.* FROM Lobby JOIN LobbyPlayer ON Lobby.Id = LobbyPlayer.LobbyId WHERE LobbyPlayer.PlayerId = 0x{identity}",