From cbda5e9974082b2c66239d2298cf5fd4c7ddf45d Mon Sep 17 00:00:00 2001 From: edmand46 Date: Mon, 6 Mar 2023 10:06:43 +0400 Subject: [PATCH] major update --- LICENSE.md | 202 +++++++++ .../Ragon.Client.Property.csproj | 13 + Ragon.Client.Property/Sources/RagonBool.cs | 60 +++ Ragon.Client.Property/Sources/RagonFloat.cs | 104 +++++ Ragon.Client.Property/Sources/RagonInt.cs | 87 ++++ Ragon.Client.Property/Sources/RagonString.cs | 68 +++ Ragon.Client.Simulation/Program.cs | 21 + .../Ragon.Client.Simulation.csproj | 26 ++ Ragon.Client.Simulation/Sources/Game.cs | 110 +++++ .../Sources/IO/RagonENetConnection.cs | 130 ++++++ .../Sources/IO/RagonENetReliableChannel.cs | 40 ++ .../Sources/IO/RagonENetUnreliableChannel.cs | 39 ++ .../Sources/IO/RagonNullConnection.cs | 131 ++++++ .../Sources/IO/RagonNullReliableChannel.cs | 38 ++ .../Sources/IO/RagonNullUnreliableChannel.cs | 38 ++ .../Sources/Player/PlayerPayload.cs | 35 ++ Ragon.Client.Simulation/Sources/Simulation.cs | 42 ++ Ragon.Client/Ragon.Client.csproj | 26 ++ .../Sources/Compressor/FloatCompressor.cs | 57 +++ .../Sources/Compressor/IntCompressor.cs | 44 ++ Ragon.Client/Sources/Entity/RagonEntity.cs | 218 +++++++++ .../Sources/Entity/RagonEntityState.cs | 77 ++++ Ragon.Client/Sources/Entity/RagonPayload.cs | 51 +++ Ragon.Client/Sources/Entity/RagonProperty.cs | 123 +++++ .../Sources/Handler/AuthorizeFailedHandler.cs | 35 ++ .../Handler/AuthorizeSuccessHandler.cs | 38 ++ .../Sources/Handler/EntityCreateHandler.cs | 58 +++ .../Sources/Handler/EntityDestroyHandler.cs | 39 ++ .../Sources/Handler/EntityEventHandler.cs | 57 +++ .../Sources/Handler/EntityStateHandler.cs | 39 ++ Ragon.Client/Sources/Handler/Handler.cs | 25 ++ .../Sources/Handler/JoinFailedHandler.cs | 36 ++ .../Sources/Handler/JoinSuccessHandler.cs | 80 ++++ .../Sources/Handler/LeaveRoomHandler.cs | 44 ++ .../Sources/Handler/LoadSceneHandler.cs | 44 ++ .../Sources/Handler/OwnershipHandler.cs | 53 +++ .../Sources/Handler/PlayerJoinHandler.cs | 50 +++ .../Sources/Handler/PlayerLeftHandler.cs | 60 +++ .../Sources/Handler/SnapshotHandler.cs | 97 ++++ Ragon.Client/Sources/IO/DisconnectReason.cs | 23 + Ragon.Client/Sources/IO/INetworkChannel.cs | 22 + Ragon.Client/Sources/IO/INetworkConnection.cs | 35 ++ Ragon.Client/Sources/IO/NetworkStatistics.cs | 62 +++ Ragon.Client/Sources/IRagonConnection.cs | 22 + Ragon.Client/Sources/IRagonEntityListener.cs | 22 + Ragon.Client/Sources/IRagonEvent.cs | 25 ++ Ragon.Client/Sources/IRagonPayload.cs | 25 ++ Ragon.Client/Sources/IRagonSceneCollector.cs | 22 + .../Listener/IRagonAuthorizationListener.cs | 24 + .../Listener/IRagonConnectionListener.cs | 23 + .../Sources/Listener/IRagonFailedListener.cs | 22 + .../Sources/Listener/IRagonJoinListener.cs | 22 + .../Sources/Listener/IRagonLeftListener.cs | 22 + .../Sources/Listener/IRagonLevelListener.cs | 22 + .../Sources/Listener/IRagonListener.cs | 31 ++ .../IRagonOwnershipChangedListener.cs | 22 + .../Listener/IRagonPlayerJoinListener.cs | 22 + .../Listener/IRagonPlayerLeftListener.cs | 22 + Ragon.Client/Sources/Log/IRagonLogger.cs | 26 ++ .../Sources/Log/Loggers/RagonConsoleLogger.cs | 41 ++ Ragon.Client/Sources/Log/RagonLog.cs | 29 ++ Ragon.Client/Sources/Log/RagonLogLevel.cs | 26 ++ Ragon.Client/Sources/RagonClient.cs | 191 ++++++++ Ragon.Client/Sources/RagonEntityCache.cs | 232 ++++++++++ Ragon.Client/Sources/RagonEventCache.cs | 71 +++ Ragon.Client/Sources/RagonListenerList.cs | 106 +++++ Ragon.Client/Sources/RagonPlayer.cs | 37 ++ Ragon.Client/Sources/RagonPlayerCache.cs | 90 ++++ Ragon.Client/Sources/RagonRoom.cs | 62 +++ Ragon.Client/Sources/RagonScene.cs | 61 +++ Ragon.Client/Sources/RagonSession.cs | 109 +++++ Ragon.Client/Sources/RagonStatus.cs | 26 ++ Ragon.Core/Action/IAction.cs | 6 - Ragon.Core/Action/Loop.cs | 28 -- Ragon.Core/Application.cs | 115 ----- Ragon.Core/Game/Entity.cs | 188 -------- Ragon.Core/Game/EntityEvent.cs | 24 - Ragon.Core/Game/EntityList.cs | 34 -- Ragon.Core/Game/EntityState.cs | 99 ---- Ragon.Core/Game/EntityStateProperty.cs | 41 -- Ragon.Core/Game/RoomInformation.cs | 13 - Ragon.Core/Game/RoomPlayer.cs | 36 -- Ragon.Core/HandlerRegistry.cs | 109 ----- Ragon.Core/Handlers/Abstract/IHandler.cs | 8 - Ragon.Core/Handlers/AuthorizationHandler.cs | 39 -- Ragon.Core/Handlers/EntityCreateHandler.cs | 32 -- Ragon.Core/Handlers/EntityDestroyHandler.cs | 21 - Ragon.Core/Handlers/EntityEventHandler.cs | 47 -- Ragon.Core/Handlers/EntityStateHandler.cs | 29 -- Ragon.Core/Handlers/RoomLeaveHandler.cs | 19 - Ragon.Core/Lobby/Abstract/ILobby.cs | 12 - Ragon.Core/Lobby/LobbyInMemory.cs | 61 --- Ragon.Core/Lobby/LobbyPlayer.cs | 27 -- Ragon.Core/PlayerContext.cs | 28 -- Ragon.Core/Ragon.Core.csproj | 21 - Ragon.Protocol/Ragon.Protocol.csproj | 3 +- Ragon.Protocol/Sources/RagonAuthority.cs | 19 +- Ragon.Protocol/Sources/RagonBuffer.cs | 423 ++++++++++++++++++ Ragon.Protocol/Sources/RagonOperation.cs | 19 +- .../Sources/RagonReplicationMode.cs | 19 +- Ragon.Protocol/Sources/RagonRoomParameters.cs | 30 +- Ragon.Protocol/Sources/RagonSerializable.cs | 23 +- Ragon.Protocol/Sources/RagonSerializer.cs | 335 -------------- Ragon.Protocol/Sources/RagonTarget.cs | 19 +- Ragon.Protocol/Sources/RagonUtils.cs | 80 ++++ Ragon.Protocol/Sources/RagonVersion.cs | 19 +- Ragon.Relay/Dockerfile | 16 + Ragon.Relay/NLog.config | 0 Ragon.Relay/Program.cs | 33 +- Ragon.Relay/Ragon.Relay.csproj | 11 +- Ragon.Relay/Relay.cs | 53 +++ Ragon.Relay/relay.config.json | 4 +- .../Ragon.Server.DotNetWebSockets.csproj | 4 +- .../WebSocketConnection.cs | 20 +- .../WebSocketReliableChannel.cs | 43 ++ .../WebSocketServer.cs | 56 ++- Ragon.Server.ENet/ENetConnection.cs | 17 - Ragon.Server.ENet/ENetReliableChannel.cs | 23 - Ragon.Server.ENet/ENetUnreliableChannel.cs | 23 - Ragon.Server.ENet/Ragon.Server.ENet.csproj | 2 +- Ragon.Server.ENet/Sources/ENetConnection.cs | 33 ++ .../Sources/ENetReliableChannel.cs | 39 ++ Ragon.Server.ENet/{ => Sources}/ENetServer.cs | 252 ++++++----- .../Sources/ENetUnreliableChannel.cs | 39 ++ .../WebSocketReliableChannel.cs | 27 -- Ragon.Server/INetworkChannel.cs | 6 - Ragon.Server/INetworkConnection.cs | 8 - Ragon.Server/INetworkListener.cs | 9 - Ragon.Server/INetworkServer.cs | 8 - Ragon.Server/NetworkConfiguration.cs | 9 - Ragon.Server/Ragon.Server.csproj | 6 + Ragon.Server/Sources/Entity/RagonEntity.cs | 212 +++++++++ .../Sources/Entity/RagonEntityState.cs | 79 ++++ Ragon.Server/Sources/Entity/RagonEvent.cs | 53 +++ Ragon.Server/Sources/Entity/RagonPayload.cs | 46 ++ Ragon.Server/Sources/Entity/RagonProperty.cs | 71 +++ .../Sources/Handler/AuthorizationOperation.cs | 55 +++ .../Sources/Handler/EntityCreateOperation.cs | 54 +++ .../Sources/Handler/EntityDestroyOperation.cs | 45 ++ .../Sources/Handler/EntityEventOperation.cs | 58 +++ .../Sources/Handler/EntityStateOperation.cs | 45 ++ .../Sources/Handler/RoomCreateOperation.cs | 44 +- .../Sources/Handler/RoomJoinOperation.cs | 43 +- .../Handler/RoomJoinOrCreateOperation.cs | 52 ++- .../Sources/Handler/RoomLeaveOperation.cs | 35 ++ .../Sources/Handler/SceneLoadOperation.cs | 50 +++ .../Sources/Handler/SceneLoadedOperation.cs | 83 ++-- Ragon.Server/{ => Sources}/IO/Executor.cs | 22 +- Ragon.Server/Sources/IO/IExecutor.cs | 22 + Ragon.Server/Sources/IO/INetworkChannel.cs | 22 + Ragon.Server/Sources/IO/INetworkConnection.cs | 24 + Ragon.Server/Sources/IO/INetworkListener.cs | 25 ++ Ragon.Server/Sources/IO/INetworkServer.cs | 25 ++ .../Sources/IO/NetworkConfiguration.cs | 25 ++ Ragon.Server/Sources/IRagonOperation.cs | 24 + Ragon.Server/Sources/Lobby/IRagonLobby.cs | 27 ++ .../Sources/Lobby/RagonLobbyInMemory.cs | 79 ++++ .../Sources/Lobby/RagonLobbyPlayer.cs | 41 ++ Ragon.Server/Sources/RagonContext.cs | 54 +++ Ragon.Server/Sources/RagonEntityCache.cs | 50 +++ Ragon.Server/Sources/RagonServer.cs | 180 ++++++++ .../Sources/RagonServerConfiguration.cs | 33 +- .../Sources/Room/RagonRoom.cs | 365 ++++++++------- .../Sources/Room/RagonRoomInformation.cs | 29 ++ Ragon.Server/Sources/Room/RagonRoomPlayer.cs | 65 +++ Ragon.Server/Sources/Time/IRagonAction.cs | 22 + .../Sources/Time/RagonActionExecutor.cs | 43 ++ Ragon.Tests/Basic.cs | 154 +++++++ Ragon.Tests/Ragon.Tests.csproj | 32 ++ Ragon.Tests/Usings.cs | 17 + Ragon.Tests/Utils/TestRagonLogger.cs | 50 +++ Ragon.sln | 34 +- global.json | 7 - license | 21 - 174 files changed, 7441 insertions(+), 1975 deletions(-) create mode 100644 LICENSE.md create mode 100644 Ragon.Client.Property/Ragon.Client.Property.csproj create mode 100644 Ragon.Client.Property/Sources/RagonBool.cs create mode 100644 Ragon.Client.Property/Sources/RagonFloat.cs create mode 100644 Ragon.Client.Property/Sources/RagonInt.cs create mode 100644 Ragon.Client.Property/Sources/RagonString.cs create mode 100644 Ragon.Client.Simulation/Program.cs create mode 100644 Ragon.Client.Simulation/Ragon.Client.Simulation.csproj create mode 100644 Ragon.Client.Simulation/Sources/Game.cs create mode 100644 Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs create mode 100644 Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs create mode 100644 Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs create mode 100644 Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs create mode 100644 Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs create mode 100644 Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs create mode 100644 Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs create mode 100644 Ragon.Client.Simulation/Sources/Simulation.cs create mode 100644 Ragon.Client/Ragon.Client.csproj create mode 100644 Ragon.Client/Sources/Compressor/FloatCompressor.cs create mode 100644 Ragon.Client/Sources/Compressor/IntCompressor.cs create mode 100644 Ragon.Client/Sources/Entity/RagonEntity.cs create mode 100644 Ragon.Client/Sources/Entity/RagonEntityState.cs create mode 100644 Ragon.Client/Sources/Entity/RagonPayload.cs create mode 100644 Ragon.Client/Sources/Entity/RagonProperty.cs create mode 100644 Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs create mode 100644 Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs create mode 100644 Ragon.Client/Sources/Handler/EntityCreateHandler.cs create mode 100644 Ragon.Client/Sources/Handler/EntityDestroyHandler.cs create mode 100644 Ragon.Client/Sources/Handler/EntityEventHandler.cs create mode 100644 Ragon.Client/Sources/Handler/EntityStateHandler.cs create mode 100644 Ragon.Client/Sources/Handler/Handler.cs create mode 100644 Ragon.Client/Sources/Handler/JoinFailedHandler.cs create mode 100644 Ragon.Client/Sources/Handler/JoinSuccessHandler.cs create mode 100644 Ragon.Client/Sources/Handler/LeaveRoomHandler.cs create mode 100644 Ragon.Client/Sources/Handler/LoadSceneHandler.cs create mode 100644 Ragon.Client/Sources/Handler/OwnershipHandler.cs create mode 100644 Ragon.Client/Sources/Handler/PlayerJoinHandler.cs create mode 100644 Ragon.Client/Sources/Handler/PlayerLeftHandler.cs create mode 100644 Ragon.Client/Sources/Handler/SnapshotHandler.cs create mode 100644 Ragon.Client/Sources/IO/DisconnectReason.cs create mode 100644 Ragon.Client/Sources/IO/INetworkChannel.cs create mode 100644 Ragon.Client/Sources/IO/INetworkConnection.cs create mode 100644 Ragon.Client/Sources/IO/NetworkStatistics.cs create mode 100644 Ragon.Client/Sources/IRagonConnection.cs create mode 100644 Ragon.Client/Sources/IRagonEntityListener.cs create mode 100644 Ragon.Client/Sources/IRagonEvent.cs create mode 100644 Ragon.Client/Sources/IRagonPayload.cs create mode 100644 Ragon.Client/Sources/IRagonSceneCollector.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonAuthorizationListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonConnectionListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonFailedListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonJoinListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonLeftListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonLevelListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonOwnershipChangedListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonPlayerJoinListener.cs create mode 100644 Ragon.Client/Sources/Listener/IRagonPlayerLeftListener.cs create mode 100644 Ragon.Client/Sources/Log/IRagonLogger.cs create mode 100644 Ragon.Client/Sources/Log/Loggers/RagonConsoleLogger.cs create mode 100644 Ragon.Client/Sources/Log/RagonLog.cs create mode 100644 Ragon.Client/Sources/Log/RagonLogLevel.cs create mode 100644 Ragon.Client/Sources/RagonClient.cs create mode 100644 Ragon.Client/Sources/RagonEntityCache.cs create mode 100644 Ragon.Client/Sources/RagonEventCache.cs create mode 100644 Ragon.Client/Sources/RagonListenerList.cs create mode 100644 Ragon.Client/Sources/RagonPlayer.cs create mode 100644 Ragon.Client/Sources/RagonPlayerCache.cs create mode 100644 Ragon.Client/Sources/RagonRoom.cs create mode 100644 Ragon.Client/Sources/RagonScene.cs create mode 100644 Ragon.Client/Sources/RagonSession.cs create mode 100644 Ragon.Client/Sources/RagonStatus.cs delete mode 100644 Ragon.Core/Action/IAction.cs delete mode 100644 Ragon.Core/Action/Loop.cs delete mode 100644 Ragon.Core/Application.cs delete mode 100644 Ragon.Core/Game/Entity.cs delete mode 100644 Ragon.Core/Game/EntityEvent.cs delete mode 100644 Ragon.Core/Game/EntityList.cs delete mode 100644 Ragon.Core/Game/EntityState.cs delete mode 100644 Ragon.Core/Game/EntityStateProperty.cs delete mode 100644 Ragon.Core/Game/RoomInformation.cs delete mode 100644 Ragon.Core/Game/RoomPlayer.cs delete mode 100644 Ragon.Core/HandlerRegistry.cs delete mode 100644 Ragon.Core/Handlers/Abstract/IHandler.cs delete mode 100644 Ragon.Core/Handlers/AuthorizationHandler.cs delete mode 100644 Ragon.Core/Handlers/EntityCreateHandler.cs delete mode 100644 Ragon.Core/Handlers/EntityDestroyHandler.cs delete mode 100644 Ragon.Core/Handlers/EntityEventHandler.cs delete mode 100644 Ragon.Core/Handlers/EntityStateHandler.cs delete mode 100644 Ragon.Core/Handlers/RoomLeaveHandler.cs delete mode 100644 Ragon.Core/Lobby/Abstract/ILobby.cs delete mode 100644 Ragon.Core/Lobby/LobbyInMemory.cs delete mode 100644 Ragon.Core/Lobby/LobbyPlayer.cs delete mode 100644 Ragon.Core/PlayerContext.cs delete mode 100644 Ragon.Core/Ragon.Core.csproj create mode 100644 Ragon.Protocol/Sources/RagonBuffer.cs delete mode 100644 Ragon.Protocol/Sources/RagonSerializer.cs create mode 100644 Ragon.Protocol/Sources/RagonUtils.cs create mode 100644 Ragon.Relay/Dockerfile mode change 100755 => 100644 Ragon.Relay/NLog.config mode change 100755 => 100644 Ragon.Relay/Program.cs mode change 100755 => 100644 Ragon.Relay/Ragon.Relay.csproj create mode 100644 Ragon.Relay/Relay.cs rename Ragon.Server.NativeWebSockets/Ragon.Server.NativeWebSockets.csproj => Ragon.Server.DotNetWebSockets/Ragon.Server.DotNetWebSockets.csproj (87%) rename {Ragon.Server.NativeWebSockets => Ragon.Server.DotNetWebSockets}/WebSocketConnection.cs (61%) create mode 100644 Ragon.Server.DotNetWebSockets/WebSocketReliableChannel.cs rename {Ragon.Server.NativeWebSockets => Ragon.Server.DotNetWebSockets}/WebSocketServer.cs (68%) delete mode 100644 Ragon.Server.ENet/ENetConnection.cs delete mode 100644 Ragon.Server.ENet/ENetReliableChannel.cs delete mode 100644 Ragon.Server.ENet/ENetUnreliableChannel.cs create mode 100644 Ragon.Server.ENet/Sources/ENetConnection.cs create mode 100644 Ragon.Server.ENet/Sources/ENetReliableChannel.cs rename Ragon.Server.ENet/{ => Sources}/ENetServer.cs (76%) mode change 100755 => 100644 create mode 100644 Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs delete mode 100644 Ragon.Server.NativeWebSockets/WebSocketReliableChannel.cs delete mode 100644 Ragon.Server/INetworkChannel.cs delete mode 100644 Ragon.Server/INetworkConnection.cs delete mode 100644 Ragon.Server/INetworkListener.cs delete mode 100644 Ragon.Server/INetworkServer.cs delete mode 100644 Ragon.Server/NetworkConfiguration.cs create mode 100644 Ragon.Server/Sources/Entity/RagonEntity.cs create mode 100644 Ragon.Server/Sources/Entity/RagonEntityState.cs create mode 100644 Ragon.Server/Sources/Entity/RagonEvent.cs create mode 100644 Ragon.Server/Sources/Entity/RagonPayload.cs create mode 100644 Ragon.Server/Sources/Entity/RagonProperty.cs create mode 100644 Ragon.Server/Sources/Handler/AuthorizationOperation.cs create mode 100644 Ragon.Server/Sources/Handler/EntityCreateOperation.cs create mode 100644 Ragon.Server/Sources/Handler/EntityDestroyOperation.cs create mode 100644 Ragon.Server/Sources/Handler/EntityEventOperation.cs create mode 100644 Ragon.Server/Sources/Handler/EntityStateOperation.cs rename Ragon.Core/Handlers/RoomCreateHandler.cs => Ragon.Server/Sources/Handler/RoomCreateOperation.cs (62%) rename Ragon.Core/Handlers/RoomJoinHandler.cs => Ragon.Server/Sources/Handler/RoomJoinOperation.cs (50%) rename Ragon.Core/Handlers/RoomJoinOrCreateHandler.cs => Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs (52%) create mode 100644 Ragon.Server/Sources/Handler/RoomLeaveOperation.cs create mode 100644 Ragon.Server/Sources/Handler/SceneLoadOperation.cs rename Ragon.Core/Handlers/SceneLoadedHandler.cs => Ragon.Server/Sources/Handler/SceneLoadedOperation.cs (59%) rename Ragon.Server/{ => Sources}/IO/Executor.cs (59%) create mode 100644 Ragon.Server/Sources/IO/IExecutor.cs create mode 100644 Ragon.Server/Sources/IO/INetworkChannel.cs create mode 100644 Ragon.Server/Sources/IO/INetworkConnection.cs create mode 100644 Ragon.Server/Sources/IO/INetworkListener.cs create mode 100644 Ragon.Server/Sources/IO/INetworkServer.cs create mode 100644 Ragon.Server/Sources/IO/NetworkConfiguration.cs create mode 100644 Ragon.Server/Sources/IRagonOperation.cs create mode 100644 Ragon.Server/Sources/Lobby/IRagonLobby.cs create mode 100644 Ragon.Server/Sources/Lobby/RagonLobbyInMemory.cs create mode 100644 Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs create mode 100644 Ragon.Server/Sources/RagonContext.cs create mode 100644 Ragon.Server/Sources/RagonEntityCache.cs create mode 100644 Ragon.Server/Sources/RagonServer.cs rename Ragon.Core/Configuration.cs => Ragon.Server/Sources/RagonServerConfiguration.cs (54%) rename Ragon.Core/Game/Room.cs => Ragon.Server/Sources/Room/RagonRoom.cs (50%) create mode 100644 Ragon.Server/Sources/Room/RagonRoomInformation.cs create mode 100644 Ragon.Server/Sources/Room/RagonRoomPlayer.cs create mode 100644 Ragon.Server/Sources/Time/IRagonAction.cs create mode 100644 Ragon.Server/Sources/Time/RagonActionExecutor.cs create mode 100644 Ragon.Tests/Basic.cs create mode 100644 Ragon.Tests/Ragon.Tests.csproj create mode 100644 Ragon.Tests/Usings.cs create mode 100644 Ragon.Tests/Utils/TestRagonLogger.cs mode change 100755 => 100644 Ragon.sln delete mode 100755 global.json delete mode 100644 license diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..22edb8d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + \ No newline at end of file diff --git a/Ragon.Client.Property/Ragon.Client.Property.csproj b/Ragon.Client.Property/Ragon.Client.Property.csproj new file mode 100644 index 0000000..d0ebad4 --- /dev/null +++ b/Ragon.Client.Property/Ragon.Client.Property.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/Ragon.Client.Property/Sources/RagonBool.cs b/Ragon.Client.Property/Sources/RagonBool.cs new file mode 100644 index 0000000..2688b07 --- /dev/null +++ b/Ragon.Client.Property/Sources/RagonBool.cs @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client.Simulation +{ + [Serializable] + public class RagonBool : RagonProperty + { + public bool Value + { + get => _value; + set + { + _value = value; + + MarkAsChanged(); + } + } + + private bool _value; + + public RagonBool( + bool initialValue, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _value = initialValue; + SetFixedSize(1); + } + + public override void Serialize(RagonBuffer buffer) + { + buffer.WriteBool(_value); + } + + public override void Deserialize(RagonBuffer buffer) + { + _value = buffer.ReadBool(); + + InvokeChanged(); + } + } +} \ No newline at end of file diff --git a/Ragon.Client.Property/Sources/RagonFloat.cs b/Ragon.Client.Property/Sources/RagonFloat.cs new file mode 100644 index 0000000..db14254 --- /dev/null +++ b/Ragon.Client.Property/Sources/RagonFloat.cs @@ -0,0 +1,104 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client +{ + [Serializable] + public class RagonFloat : RagonProperty + { + public float Value + { + get => _value; + set + { + _value = value; + + if (_value < _min) + _value = _min; + else if (_value > _max) + _value = _max; + + MarkAsChanged(); + } + } + + private float _value; + private float _min; + private float _max; + private int _requiredBits; + private float _precision; + private uint _mask; + + public RagonFloat( + float initialValue, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _value = initialValue; + _min = -1024.0f; + _max = 1024.0f; + _precision = 0.01f; + + _requiredBits = DeBruijn.Log2((uint)((_max - _min) * (1.0f / _precision) + 0.5f)) + 1; + _mask = (uint)((1L << _requiredBits) - 1); + + SetFixedSize(_requiredBits); + } + + public RagonFloat( + float initialValue, + float min = -1024.0f, + float max = 1024.0f, + float precision = 0.01f, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _value = initialValue; + _min = min; + _max = max; + _precision = precision; + + _requiredBits = DeBruijn.Log2((uint)((_max - _min) * (1.0f / _precision) + 0.5f)) + 1; + _mask = (uint)((1L << _requiredBits) - 1); + + SetFixedSize(_requiredBits); + } + + public override void Serialize(RagonBuffer buffer) + { + var compressedValue = (uint)((_value - _min) * (1f / _precision) + 0.5f) & _mask; + buffer.Write(compressedValue, _requiredBits); + } + + public override void Deserialize(RagonBuffer buffer) + { + var compressedValue = buffer.Read(_requiredBits); + _value = compressedValue * _precision + _min; + + if (_value < _min) + _value = _min; + else if (_value > _max) + _value = _max; + + InvokeChanged(); + } + } +} \ No newline at end of file diff --git a/Ragon.Client.Property/Sources/RagonInt.cs b/Ragon.Client.Property/Sources/RagonInt.cs new file mode 100644 index 0000000..063a5b2 --- /dev/null +++ b/Ragon.Client.Property/Sources/RagonInt.cs @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client.Simulation +{ + [Serializable] + public class RagonInt : RagonProperty + { + public int Value + { + get => _value; + set + { + _value = value; + MarkAsChanged(); + } + } + + private int _min; + private int _max; + private int _requiredBits; + private int _value; + + public RagonInt( + int initialValue, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _value = initialValue; + _min = -1000; + _max = 1000; + + _max = Math.Max(Math.Abs(_min), Math.Abs(_max)); + _requiredBits = Bits.Compute(_max); + + SetFixedSize(_requiredBits); + } + + public RagonInt( + int initialValue, + int min = -1000, + int max = 1000, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _value = initialValue; + _min = min; + _max = max; + + _requiredBits = Bits.Compute(_max); + + SetFixedSize(_requiredBits); + } + + public override void Serialize(RagonBuffer buffer) + { + uint compressedValue = (uint)((_value << 1) ^ (_value >> 31)); + buffer.Write(compressedValue, _requiredBits); + } + + public override void Deserialize(RagonBuffer buffer) + { + var compressedValue = buffer.Read(_requiredBits); + _value = (int)((compressedValue >> 1) ^ -(int)(compressedValue & 1)); + + InvokeChanged(); + } + } +} \ No newline at end of file diff --git a/Ragon.Client.Property/Sources/RagonString.cs b/Ragon.Client.Property/Sources/RagonString.cs new file mode 100644 index 0000000..0f7ca94 --- /dev/null +++ b/Ragon.Client.Property/Sources/RagonString.cs @@ -0,0 +1,68 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using System.Text; +using Ragon.Protocol; + +namespace Ragon.Client.Simulation +{ + [Serializable] + public class RagonString : RagonProperty + { + public string Value + { + get => _value; + set + { + _value = value; + + MarkAsChanged(); + } + } + + private string _value; + private readonly UTF8Encoding _utf8Encoding = new UTF8Encoding(false, true); + + public RagonString( + string initialValue, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _value = initialValue; + } + + public override void Serialize(RagonBuffer buffer) + { + var data = _utf8Encoding.GetBytes(_value); + var len = (uint) data.Length; + + buffer.Write(len, 16); + buffer.WriteBytes(data); + } + + public override void Deserialize(RagonBuffer buffer) + { + var len = (int) buffer.Read(16); + var data = buffer.ReadBytes(len); + + _value = _utf8Encoding.GetString(data); + + InvokeChanged(); + } + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Program.cs b/Ragon.Client.Simulation/Program.cs new file mode 100644 index 0000000..a34cb94 --- /dev/null +++ b/Ragon.Client.Simulation/Program.cs @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Client.Simulation; + +var simulation = new Simulation(); +simulation.Start(); \ No newline at end of file diff --git a/Ragon.Client.Simulation/Ragon.Client.Simulation.csproj b/Ragon.Client.Simulation/Ragon.Client.Simulation.csproj new file mode 100644 index 0000000..890cd74 --- /dev/null +++ b/Ragon.Client.Simulation/Ragon.Client.Simulation.csproj @@ -0,0 +1,26 @@ + + + + Exe + net7.0 + enable + enable + Linux + + + + + + + + + + + + + + Always + + + + diff --git a/Ragon.Client.Simulation/Sources/Game.cs b/Ragon.Client.Simulation/Sources/Game.cs new file mode 100644 index 0000000..60a1c21 --- /dev/null +++ b/Ragon.Client.Simulation/Sources/Game.cs @@ -0,0 +1,110 @@ + + +namespace Ragon.Client.Simulation; + +public class Game : IRagonListener +{ + private RagonFloat _health; + private RagonInt _points; + private RagonString _name; + private RagonEntity _entity; + private RagonClient _client; + + public Game(RagonClient client) + { + _client = client; + } + + public void OnConnected(RagonClient client) + { + RagonLog.Trace("Connected"); + _client.Session.AuthorizeWithKey("defaultkey", "Player Eduard", Array.Empty()); + } + + public void OnAuthorizationSuccess(RagonClient client, string playerId, string playerName) + { + RagonLog.Trace("Authorized"); + client.Session.CreateOrJoin("Example", 1, 20); + } + + public void OnAuthorizationFailed(RagonClient client, string message) + { + Console.WriteLine($"Authorization failed: {message}"); + } + + public void OnJoined(RagonClient client) + { + RagonLog.Trace("Joined"); + + _health = new RagonFloat(100.0f, false, 0); + _health.Changed += () => Console.WriteLine($"[Ragon Property] Health: {_health.Value}"); + + _points = new RagonInt(0, -1000, 1000, false, 0); + _points.Changed += () => Console.WriteLine($"[Ragon Property] Points: {_points.Value}"); + + _name = new RagonString("Edmand 000", false); + _name.Changed += () => Console.WriteLine($"[Ragon Property] Name: {_name.Value}"); + + _entity = new RagonEntity(12, 0); + _entity.State.AddProperty(_health); + _entity.State.AddProperty(_points); + _entity.State.AddProperty(_name); + + client.Room.CreateEntity(_entity); + } + + public void OnFailed(RagonClient client, string message) + { + RagonLog.Trace("Failed to join"); + } + + public void OnLeft(RagonClient client) + { + RagonLog.Trace("Left"); + } + + public void OnDisconnected(RagonClient client) + { + RagonLog.Trace("Disconnected"); + } + + public void OnPlayerJoined(RagonClient client, RagonPlayer player) + { + RagonLog.Trace("Player joined"); + } + + public void OnPlayerLeft(RagonClient client, RagonPlayer player) + { + RagonLog.Trace("Player left"); + } + + public void OnOwnershipChanged(RagonClient client, RagonPlayer player) + { + RagonLog.Trace("Owner ship changed"); + } + + public void OnLevel(RagonClient client, string sceneName) + { + RagonLog.Trace($"New level: {sceneName}"); + + client.Room.SceneLoaded(); + } + + private float _timer = 0; + + public void Update() + { + if (_client.Status != RagonStatus.ROOM) + return; + + _timer += 1 / 60.0f; + if (_timer > 1) + { + _health.Value += 20.0f; + _points.Value += 10; + _name.Value = $"Edmand 00{_client.Room.Local.PeerId}"; + Console.WriteLine($"{_health.Value} {_points.Value} {_name.Value}"); + _timer = 0; + } + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs b/Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs new file mode 100644 index 0000000..d9bedb0 --- /dev/null +++ b/Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs @@ -0,0 +1,130 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using ENet; +using Event = ENet.Event; +using EventType = ENet.EventType; + +namespace Ragon.Client +{ + public class RagonENetConnection : INetworkConnection + { + public ushort Id { get; } + + public NetworkStatistics Statistics { get; private set; } + public INetworkChannel Reliable { get; private set; } + public INetworkChannel Unreliable { get; private set; } + + public Action OnData { get; set; } + public Action OnConnected { get; set; } + public Action OnDisconnected { get; set; } + public ulong BytesSent { get; } + public ulong BytesReceived { get; } + public int Ping { get; } + + private static bool _libraryLoaded = false; + private Host _host; + private Peer _peer; + private Event _netEvent; + + public RagonENetConnection() + { + _host = new Host(); + _host.Create(); + } + + + public void Prepare() + { + if (!_libraryLoaded) + { + Library.Initialize(); + _libraryLoaded = true; + } + } + + public void Disconnect() + { + if (_peer.IsSet) + _peer.DisconnectNow(0); + } + + public void Connect(string server, ushort port, uint protocol) + { + Address address = new Address(); + address.SetHost(server); + address.Port = port; + + _peer = _host.Connect(address, 2, protocol); + _peer.Timeout(32, 5000, 5000); + } + + public void Update() + { + bool polled = false; + while (!polled) + { + if (_host.CheckEvents(out _netEvent) <= 0) + { + if (_host.Service(0, out _netEvent) <= 0) + break; + + polled = true; + } + + switch (_netEvent.Type) + { + case EventType.None: + break; + case EventType.Connect: + Statistics = new NetworkStatistics(); + Reliable = new ENetReliableChannel(_netEvent.Peer, 0); + Unreliable = new ENetUnreliableChannel(_netEvent.Peer, 1); + + OnConnected?.Invoke(); + break; + case EventType.Disconnect: + OnDisconnected?.Invoke(DisconnectReason.MANUAL); + break; + case EventType.Timeout: + OnDisconnected?.Invoke(DisconnectReason.TIMEOUT); + break; + case EventType.Receive: + var data = new byte[_netEvent.Packet.Length]; + + _netEvent.Packet.CopyTo(data); + _netEvent.Packet.Dispose(); + + OnData?.Invoke(data); + break; + } + } + } + + public void Dispose() + { + if (_host.IsSet) + { + _host?.Flush(); + _host?.Dispose(); + } + + if (_libraryLoaded) + Library.Deinitialize(); + } + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs b/Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs new file mode 100644 index 0000000..f46b6f4 --- /dev/null +++ b/Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using ENet; +using Ragon.Protocol; + +namespace Ragon.Client; + +public sealed class ENetReliableChannel : INetworkChannel +{ + private Peer _peer; + private byte _channelId; + + public ENetReliableChannel(Peer peer, int channelId) + { + _peer = peer; + _channelId = (byte) channelId; + } + + public void Send(byte[] data) + { + var newPacket = new Packet(); + newPacket.Create(data, data.Length, PacketFlags.Reliable); + + _peer.Send(_channelId, ref newPacket); + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs b/Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs new file mode 100644 index 0000000..a95e9ce --- /dev/null +++ b/Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using ENet; + +namespace Ragon.Client; + +public sealed class ENetUnreliableChannel : INetworkChannel +{ + private Peer _peer; + private byte _channelId; + + public ENetUnreliableChannel(Peer peer, int channelId) + { + _peer = peer; + _channelId = (byte) channelId; + } + + public void Send(byte[] data) + { + var newPacket = new Packet(); + newPacket.Create(data, data.Length, PacketFlags.None); + + _peer.Send(_channelId, ref newPacket); + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs b/Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs new file mode 100644 index 0000000..2ca2758 --- /dev/null +++ b/Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs @@ -0,0 +1,131 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using ENet; +using Event = ENet.Event; +using EventType = ENet.EventType; + +namespace Ragon.Client +{ + public class RagonNullConnection : INetworkConnection + { + public ushort Id { get; } + + public NetworkStatistics Statistics { get; private set; } + public INetworkChannel Reliable { get; private set; } + public INetworkChannel Unreliable { get; private set; } + + public Action OnData { get; set; } + public Action OnConnected { get; set; } + public Action OnDisconnected { get; set; } + public ulong BytesSent { get; } + public ulong BytesReceived { get; } + public int Ping { get; } + + private static bool _libraryLoaded = false; + private Host _host; + private Peer _peer; + private Event _netEvent; + + public RagonNullConnection() + { + _host = new Host(); + _host.Create(); + } + + + public void Prepare() + { + if (!_libraryLoaded) + { + Library.Initialize(); + _libraryLoaded = true; + } + } + + public void Disconnect() + { + if (_peer.IsSet) + _peer.DisconnectNow(0); + } + + public void Connect(string server, ushort port, uint protocol) + { + Address address = new Address(); + address.SetHost(server); + address.Port = port; + + _peer = _host.Connect(address, 2, protocol); + _peer.Timeout(32, 5000, 5000); + + Statistics = new NetworkStatistics(); + Reliable = new NullReliableChannel(_netEvent.Peer, 0); + Unreliable = new NullUnreliableChannel(_netEvent.Peer, 1); + } + + public void Update() + { + bool polled = false; + while (!polled) + { + if (_host.CheckEvents(out _netEvent) <= 0) + { + if (_host.Service(0, out _netEvent) <= 0) + break; + + polled = true; + } + + switch (_netEvent.Type) + { + case EventType.None: + break; + case EventType.Connect: + + OnConnected?.Invoke(); + break; + case EventType.Disconnect: + OnDisconnected?.Invoke(DisconnectReason.MANUAL); + break; + case EventType.Timeout: + OnDisconnected?.Invoke(DisconnectReason.TIMEOUT); + break; + case EventType.Receive: + var data = new byte[_netEvent.Packet.Length]; + + _netEvent.Packet.CopyTo(data); + _netEvent.Packet.Dispose(); + + OnData?.Invoke(data); + break; + } + } + } + + public void Dispose() + { + if (_host.IsSet) + { + _host?.Flush(); + _host?.Dispose(); + } + + if (_libraryLoaded) + Library.Deinitialize(); + } + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs b/Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs new file mode 100644 index 0000000..a685d1d --- /dev/null +++ b/Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using ENet; +using Ragon.Protocol; + +namespace Ragon.Client; + +public sealed class NullReliableChannel : INetworkChannel +{ + private Peer _peer; + private byte _channelId; + + public NullReliableChannel(Peer peer, int channelId) + { + _peer = peer; + _channelId = (byte) channelId; + } + + public void Send(byte[] data) + { + + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs b/Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs new file mode 100644 index 0000000..73b0874 --- /dev/null +++ b/Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using ENet; +using Ragon.Protocol; + +namespace Ragon.Client; + +public sealed class NullUnreliableChannel : INetworkChannel +{ + private Peer _peer; + private byte _channelId; + + public NullUnreliableChannel(Peer peer, int channelId) + { + _peer = peer; + _channelId = (byte) channelId; + } + + public void Send(byte[] data) + { + + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs b/Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs new file mode 100644 index 0000000..d4b5ca8 --- /dev/null +++ b/Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client.Simulation.Sources; + +public class PlayerPayload : IRagonPayload +{ + public uint Name { get; set; } + + public void Serialize(RagonBuffer buffer) + { + buffer.Write(Name, 16); + } + + public void Deserialize(RagonBuffer buffer) + { + Name = buffer.Read(16); + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/Simulation.cs b/Ragon.Client.Simulation/Sources/Simulation.cs new file mode 100644 index 0000000..db8d223 --- /dev/null +++ b/Ragon.Client.Simulation/Sources/Simulation.cs @@ -0,0 +1,42 @@ +namespace Ragon.Client.Simulation; + +public class EntityListener : IRagonEntityListener +{ + public void OnEntityCreated(RagonEntity entity) + { + var health = new RagonFloat(100.0f, false, 0); + health.Value = 50; + health.Changed += () => Console.WriteLine($"[Ragon Property] Another Health: {health.Value}"); + + var points = new RagonInt(0, -1000, 1000, false, 0); + points.Changed += () => Console.WriteLine($"[Ragon Property] Anther Points: {points.Value}"); + + var name = new RagonString("Eduard", false); + name.Changed += () => Console.WriteLine($"[Ragon Property] Another Name: {name.Value}"); + + entity.State.AddProperty(health); + entity.State.AddProperty(points); + entity.State.AddProperty(name); + } +} + +public class Simulation +{ + public void Start() + { + // INetworkConnection protocol = debug ? new RagonNullConnection() : new RagonENetConnection(); + // var network = new RagonClient(protocol, new EntityListener(), 30); + // var game = new Game(network); + // network.AddListener(game); + // network.Connect("127.0.0.1", 5001, "1.0.0"); + // var dt = 1000 / 60.0f; + // while (true) + // { + // game.Update(); + // network.Update(dt); + // Thread.Sleep((int) dt); + // } + // + // network.Disconnect(); + } +} \ No newline at end of file diff --git a/Ragon.Client/Ragon.Client.csproj b/Ragon.Client/Ragon.Client.csproj new file mode 100644 index 0000000..ec23c4d --- /dev/null +++ b/Ragon.Client/Ragon.Client.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.1 + enable + enable + 10 + Ragon.Client.Simulation + Eduard Kargin (Edmand46) + + + + true + none + /Users/edmand46/RagonProjects/ragon-unity-sdk/Assets/Ragon/Runtime/Plugins + + + + /Users/edmand46/RagonProjects/ragon-unity-sdk/Assets/Ragon/Runtime/Plugins + + + + + + + diff --git a/Ragon.Client/Sources/Compressor/FloatCompressor.cs b/Ragon.Client/Sources/Compressor/FloatCompressor.cs new file mode 100644 index 0000000..de051a6 --- /dev/null +++ b/Ragon.Client/Sources/Compressor/FloatCompressor.cs @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client.Compressor; + +public class FloatCompressor +{ + public float Min { get; private set; } + public float Max { get; private set; } + public float Precision { get; private set; } + public int RequiredBits { get; private set; } + + private uint _mask; + + public FloatCompressor(float min = -1024.0f, float max = 1024.0f, float precision = 0.01f) + { + Min = min; + Max = max; + Precision = precision; + RequiredBits = DeBruijn.Log2((uint)((Max - Min) * (1f / Precision) + 0.5f)) + 1; + + _mask = (uint)((1L << RequiredBits) - 1); + } + + public uint Compress(float value) + { + return (uint)((value - Min) * 1f / Precision + 0.5f) & _mask; + } + + public float Decompress(uint value) + { + var result = value * Precision + Min; + + if (result < Min) + result = Min; + else if (result > Max) + result = Max; + + return result; + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Compressor/IntCompressor.cs b/Ragon.Client/Sources/Compressor/IntCompressor.cs new file mode 100644 index 0000000..0334f69 --- /dev/null +++ b/Ragon.Client/Sources/Compressor/IntCompressor.cs @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client.Compressor; + +public class IntCompressor +{ + public int Min { get; private set; } + public int Max { get; private set; } + public int RequiredBits { get; private set; } + + public IntCompressor(int min = -1000, int max = 1000) + { + Min = min; + Max = max; + RequiredBits = Bits.Compute(Max); + } + + public uint Compress(int value) + { + return (uint)((value << 1) ^ (value >> 31));; + } + + public int Decompress(uint value) + { + return (int)((value >> 1) ^ -(int)(value & 1)); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Entity/RagonEntity.cs b/Ragon.Client/Sources/Entity/RagonEntity.cs new file mode 100644 index 0000000..818a6ec --- /dev/null +++ b/Ragon.Client/Sources/Entity/RagonEntity.cs @@ -0,0 +1,218 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client +{ + public sealed class RagonEntity + { + private delegate void OnEventDelegate(RagonPlayer player, RagonBuffer serializer); + private RagonClient _client; + + public ushort Id { get; private set; } + public ushort Type { get; private set; } + public bool Replication { get; private set; } + + public RagonAuthority Authority { get; private set; } + public RagonPlayer Owner { get; private set; } + public RagonEntityState State { get; private set; } + + public bool IsAttached { get; private set; } + public bool HasAuthority { get; private set; } + + public event Action Attached; + public event Action Detached; + public event Action OwnershipChanged; + + internal bool PropertiesChanged => _propertiesChanged; + internal ushort SceneId => _sceneId; + + private ushort _sceneId; + private bool _propertiesChanged; + + private RagonPayload _spawnPayload; + private RagonPayload _destroyPayload; + + private Dictionary _events = new(); + private Dictionary> _localEvents = new(); + + public RagonEntity(ushort type = 0, ushort sceneId = 0) + { + State = new RagonEntityState(this); + Type = type; + + _sceneId = sceneId; + } + + internal void Attach(RagonClient client, ushort entityId, ushort entityType, bool hasAuthority, RagonPlayer owner, RagonPayload payload) + { + Type = entityType; + Id = entityId; + Owner = owner; + IsAttached = true; + Replication = true; + HasAuthority = hasAuthority; + + _client = client; + _spawnPayload = payload; + + Attached?.Invoke(this); + } + + internal void Detach(RagonPayload payload) + { + _destroyPayload = payload; + + Detached?.Invoke(); + } + + internal T GetPayload(RagonPayload data) where T : IRagonPayload, new() + { + var buffer = new RagonBuffer(); + data.Write(buffer); + + var payload = new T(); + payload.Deserialize(buffer); + + return payload; + } + + public T GetSpawnPayload() where T : IRagonPayload, new() + { + return GetPayload(_spawnPayload); + } + + public T GetDestroyPayload() where T : IRagonPayload, new() + { + return GetPayload(_destroyPayload); + } + + public void ReplicateEvent(TEvent evnt, RagonPlayer target, RagonReplicationMode replicationMode) + where TEvent : IRagonEvent, new() + { + var evntId = _client.Event.GetEventCode(evnt); + var buffer = _client.Buffer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); + buffer.WriteUShort(Id); + buffer.WriteUShort(evntId); + buffer.WriteByte((byte) replicationMode); + buffer.WriteByte((byte) RagonTarget.Player); + buffer.WriteUShort((ushort) target.PeerId); + + evnt.Serialize(buffer); + + var sendData = buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + public void ReplicateEvent( + TEvent evnt, + RagonTarget target = RagonTarget.All, + RagonReplicationMode replicationMode = RagonReplicationMode.Server) + where TEvent : IRagonEvent, new() + { + if (target != RagonTarget.ExceptOwner) + { + if (replicationMode == RagonReplicationMode.Local) + { + var eventCode = _client.Event.GetEventCode(evnt); + _localEvents[eventCode].Invoke(_client.Room.Local, evnt); + return; + } + + if (replicationMode == RagonReplicationMode.LocalAndServer) + { + var eventCode = _client.Event.GetEventCode(evnt); + _localEvents[eventCode].Invoke(_client.Room.Local, evnt); + } + } + + var evntId = _client.Event.GetEventCode(evnt); + var buffer = _client.Buffer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); + buffer.WriteUShort(Id); + buffer.WriteUShort(evntId); + buffer.WriteByte((byte) replicationMode); + buffer.WriteByte((byte) target); + + evnt.Serialize(buffer); + + var sendData = buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + public void OnEvent(Action callback) where TEvent : IRagonEvent, new() + { + var t = new TEvent(); + var eventCode = _client.Event.GetEventCode(t); + + if (_events.ContainsKey(eventCode)) + { + RagonLog.Warn($"Event already {eventCode} subscribed"); + return; + } + + _localEvents.Add(eventCode, (player, eventData) => { callback.Invoke(player, (TEvent) eventData); }); + _events.Add(eventCode, (player, serializer) => + { + t.Deserialize(serializer); + callback.Invoke(player, t); + }); + } + + internal void Write(RagonBuffer buffer) + { + buffer.WriteUShort(Id); + + State.WriteState(buffer); + + _propertiesChanged = false; + } + + + internal void Read(RagonBuffer buffer) + { + State.ReadState(buffer); + } + + internal void Event(ushort eventCode, RagonPlayer caller, RagonBuffer buffer) + { + if (_events.ContainsKey(eventCode)) + _events[eventCode]?.Invoke(caller, buffer); + } + + internal void TrackChangedProperty(RagonProperty property) + { + _propertiesChanged = true; + } + + public void OnOwnershipChanged(RagonPlayer player) + { + var prevOwner = Owner; + + Owner = player; + HasAuthority = player.PeerId == _client.Room.Local.PeerId; + + OwnershipChanged?.Invoke(prevOwner, player); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Entity/RagonEntityState.cs b/Ragon.Client/Sources/Entity/RagonEntityState.cs new file mode 100644 index 0000000..f70099a --- /dev/null +++ b/Ragon.Client/Sources/Entity/RagonEntityState.cs @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +public sealed class RagonEntityState +{ + private List _properties; + private RagonEntity _entity; + + public RagonEntityState(RagonEntity entity) + { + _entity = entity; + _properties = new List(6); + } + + public void AddProperty(RagonProperty property) + { + _properties.Add(property); + + property.AssignEntity(_entity); + } + + internal void WriteInfo(RagonBuffer buffer) + { + buffer.WriteUShort((ushort)_properties.Count); + foreach (var property in _properties) + { + buffer.WriteBool(property.IsFixed); + buffer.WriteUShort((ushort)property.Size); + } + } + + internal void ReadState(RagonBuffer buffer) + { + foreach (var property in _properties) + { + var changed = buffer.ReadBool(); + if (changed) + property.Deserialize(buffer); + } + } + + internal void WriteState(RagonBuffer buffer) + { + foreach (var prop in _properties) + { + if (prop.IsDirty) + { + buffer.WriteBool(true); + prop.Write(buffer); + prop.Flush(); + } + else + { + prop.AddTick(); + buffer.WriteBool(false); + } + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Entity/RagonPayload.cs b/Ragon.Client/Sources/Entity/RagonPayload.cs new file mode 100644 index 0000000..dcb7a77 --- /dev/null +++ b/Ragon.Client/Sources/Entity/RagonPayload.cs @@ -0,0 +1,51 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +public struct RagonPayload +{ + private uint[] _data = new uint[128]; + private int _size = 0; + + public RagonPayload(int capacity) + { + _size = capacity; + } + public int Size => _size; + + public void Read(RagonBuffer buffer) + { + var readOnlySpan = _data.AsSpan(); + + buffer.ReadSpan(ref readOnlySpan, _size); + } + + public void Write(RagonBuffer buffer) + { + ReadOnlySpan readOnlySpan = _data.AsSpan(); + + buffer.WriteSpan(ref readOnlySpan, _size); + } + + public override string ToString() + { + return $"Payload Size: {_size}"; + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Entity/RagonProperty.cs b/Ragon.Client/Sources/Entity/RagonProperty.cs new file mode 100644 index 0000000..6c55d7a --- /dev/null +++ b/Ragon.Client/Sources/Entity/RagonProperty.cs @@ -0,0 +1,123 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client +{ + [Serializable] + public class RagonProperty + { + public string Name => _name; + public RagonEntity Entity => _entity; + + public event Action Changed; + public bool IsDirty => _dirty && _ticks >= _priority; + public bool IsFixed => _fixed; + public int Size => _size; + + private bool _fixed; + private string _name; + protected bool _invokeLocal; + + private RagonEntity _entity; + private bool _dirty; + private int _size; + private int _ticks; + private int _priority; + + protected RagonProperty(int priority, bool invokeLocal) + { + _size = 0; + _priority = priority; + _fixed = false; + _invokeLocal = invokeLocal; + } + + public void SetName(string name) + { + _name = name; + } + + protected void SetFixedSize(int size) + { + _size = size; + _fixed = true; + } + + protected void InvokeChanged() + { + if (!_invokeLocal) + return; + + Changed?.Invoke(); + } + + protected void MarkAsChanged() + { + InvokeChanged(); + + if (_dirty) return; + _dirty = true; + + _entity?.TrackChangedProperty(this); + } + + internal void Flush() + { + _dirty = false; + _ticks = 0; + } + + internal void AddTick() + { + _ticks++; + } + + internal void AssignEntity(RagonEntity obj) + { + _entity = obj; + + Changed?.Invoke(); + } + + internal void Write(RagonBuffer buffer) + { + if (_fixed) + { + Serialize(buffer); + return; + } + + var sizeOffset = buffer.WriteOffset; + buffer.Write(0, 16); + var propOffset = buffer.WriteOffset; + + Serialize(buffer); + + var propSize = (uint) (buffer.WriteOffset - propOffset); + buffer.Write(propSize, 16, sizeOffset); + } + + public virtual void Serialize(RagonBuffer buffer) + { + } + + public virtual void Deserialize(RagonBuffer buffer) + { + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs b/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs new file mode 100644 index 0000000..36f0ac4 --- /dev/null +++ b/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class AuthorizeFailedHandler: Handler +{ + private readonly RagonListenerList _listenerList; + public AuthorizeFailedHandler(RagonListenerList list) + { + _listenerList = list; + } + + public void Handle(RagonBuffer buffer) + { + var message = buffer.ReadString(); + _listenerList.OnAuthorizationFailed(message); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs b/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs new file mode 100644 index 0000000..867fc29 --- /dev/null +++ b/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class AuthorizeSuccessHandler: Handler +{ + private readonly RagonListenerList _listenerList; + + public AuthorizeSuccessHandler(RagonListenerList listenerList) + { + _listenerList = listenerList; + } + + public void Handle(RagonBuffer buffer) + { + var playerId = buffer.ReadString(); + var playerName = buffer.ReadString(); + + _listenerList.OnAuthorizationSuccess(playerId, playerName); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/EntityCreateHandler.cs b/Ragon.Client/Sources/Handler/EntityCreateHandler.cs new file mode 100644 index 0000000..fef3f34 --- /dev/null +++ b/Ragon.Client/Sources/Handler/EntityCreateHandler.cs @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class EntityCreateHandler : Handler +{ + private readonly RagonClient _client; + private readonly RagonPlayerCache _playerCache; + private readonly RagonEntityCache _entityCache; + + public EntityCreateHandler( + RagonClient client, + RagonPlayerCache playerCache, + RagonEntityCache entityCache + ) + { + _client = client; + _entityCache = entityCache; + _playerCache = playerCache; + } + + public void Handle(RagonBuffer buffer) + { + var attachId = buffer.ReadUShort(); + var entityType = buffer.ReadUShort(); + var entityId = buffer.ReadUShort(); + var ownerId = buffer.ReadUShort(); + var player = _playerCache.GetPlayerByPeer(ownerId); + var payload = new RagonPayload(buffer.Capacity); + payload.Read(buffer); + + if (player == null) + { + RagonLog.Warn($"Owner {ownerId}|{player.Name} not found in players"); + return; + } + + var hasAuthority = _playerCache.LocalPlayer.Id == player.Id; + var entity = _entityCache.OnCreate(attachId, entityType, 0, entityId, hasAuthority); + entity.Attach(_client, entityId, entityType, hasAuthority, player, payload); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/EntityDestroyHandler.cs b/Ragon.Client/Sources/Handler/EntityDestroyHandler.cs new file mode 100644 index 0000000..2d8639c --- /dev/null +++ b/Ragon.Client/Sources/Handler/EntityDestroyHandler.cs @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class EntityDestroyHandler: Handler +{ + private readonly RagonEntityCache _entityCache; + + public EntityDestroyHandler(RagonEntityCache entityCache) + { + _entityCache = entityCache; + } + + public void Handle(RagonBuffer buffer) + { + var entityId = buffer.ReadUShort(); + var payload = new RagonPayload(buffer.Capacity); + payload.Read(buffer); + + _entityCache.OnDestroy(entityId, payload); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/EntityEventHandler.cs b/Ragon.Client/Sources/Handler/EntityEventHandler.cs new file mode 100644 index 0000000..d08e115 --- /dev/null +++ b/Ragon.Client/Sources/Handler/EntityEventHandler.cs @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class EntityEventHandler : Handler +{ + private readonly RagonClient _client; + private readonly RagonPlayerCache _playerCache; + private readonly RagonEntityCache _entityCache; + + public EntityEventHandler( + RagonClient client, + RagonPlayerCache playerCache, + RagonEntityCache entityCache + ) + { + _client = client; + _playerCache = playerCache; + _entityCache = entityCache; + } + + public void Handle(RagonBuffer buffer) + { + var eventCode = buffer.ReadUShort(); + var peerId = buffer.ReadUShort(); + var executionMode = (RagonReplicationMode)buffer.ReadByte(); + var entityId = buffer.ReadUShort(); + + var player = _playerCache.GetPlayerByPeer(peerId); + if (player == null) + { + RagonLog.Warn($"Player not found for event {eventCode}"); + return; + } + + if (player.IsMe && executionMode == RagonReplicationMode.LocalAndServer) + return; + + _entityCache.OnEvent(player, entityId, eventCode, buffer); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/EntityStateHandler.cs b/Ragon.Client/Sources/Handler/EntityStateHandler.cs new file mode 100644 index 0000000..d59b5dc --- /dev/null +++ b/Ragon.Client/Sources/Handler/EntityStateHandler.cs @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class StateEntityHandler: Handler +{ + private readonly RagonEntityCache _entityCache; + + public StateEntityHandler(RagonEntityCache entityCache) + { + _entityCache = entityCache; + } + + public void Handle(RagonBuffer buffer) + { + var entitiesCount = buffer.ReadUShort(); + for (var i = 0; i < entitiesCount; i++) + { + var entityId = buffer.ReadUShort(); + _entityCache.OnState(entityId, buffer); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/Handler.cs b/Ragon.Client/Sources/Handler/Handler.cs new file mode 100644 index 0000000..ba396d6 --- /dev/null +++ b/Ragon.Client/Sources/Handler/Handler.cs @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +public interface Handler +{ + public void Handle(RagonBuffer buffer); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/JoinFailedHandler.cs b/Ragon.Client/Sources/Handler/JoinFailedHandler.cs new file mode 100644 index 0000000..73d3963 --- /dev/null +++ b/Ragon.Client/Sources/Handler/JoinFailedHandler.cs @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class JoinFailedHandler: Handler +{ + private readonly RagonListenerList _listenerList; + + public JoinFailedHandler(RagonListenerList listenerList) + { + _listenerList = listenerList; + } + + public void Handle(RagonBuffer buffer) + { + var message = buffer.ReadString(); + _listenerList.OnFailed(message); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs b/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs new file mode 100644 index 0000000..1ad67b5 --- /dev/null +++ b/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs @@ -0,0 +1,80 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +public struct RagonRoomInformation +{ + public RagonRoomInformation(string roomId, string playerId, string ownerId, ushort min, ushort max) + { + RoomId = roomId; + PlayerId = playerId; + OwnerId = ownerId; + Min = min; + Max = max; + } + + public string RoomId { get; private set; } + public string PlayerId { get; private set; } + public string OwnerId { get; private set; } + public ushort Min { get; private set; } + public ushort Max { get; private set; } +} + +internal class JoinSuccessHandler : Handler +{ + private readonly RagonListenerList _listenerList; + private readonly RagonPlayerCache _playerCache; + private readonly RagonEntityCache _entityCache; + private readonly RagonClient _client; + private readonly RagonBuffer _buffer; + + public JoinSuccessHandler( + RagonClient client, + RagonBuffer buffer, + RagonListenerList listenerList, + RagonPlayerCache playerCache, + RagonEntityCache entityCache + ) + { + _buffer = buffer; + _client = client; + _listenerList = listenerList; + _entityCache = entityCache; + _playerCache = playerCache; + } + + public void Handle(RagonBuffer buffer) + { + var roomId = buffer.ReadString(); + var localId = buffer.ReadString(); + var ownerId = buffer.ReadString(); + var min = buffer.ReadUShort(); + var max = buffer.ReadUShort(); + var map = buffer.ReadString(); + + var scene = new RagonScene(_client, _playerCache, _entityCache); + var roomInfo = new RagonRoomInformation(roomId, localId, ownerId, min, max); + var room = new RagonRoom(_client, _entityCache, _playerCache, roomInfo, scene); + + _playerCache.SetOwnerAndLocal(ownerId, localId); + _client.AssignRoom(room); + _listenerList.OnLevel(map); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs b/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs new file mode 100644 index 0000000..bd9e6e2 --- /dev/null +++ b/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class LeaveRoomHandler : Handler +{ + private readonly RagonClient _client; + private readonly RagonListenerList _listenerList; + private readonly RagonEntityCache _entityCache; + + public LeaveRoomHandler( + RagonClient client, + RagonListenerList listenerList, + RagonEntityCache entityCache) + { + _client = client; + _listenerList = listenerList; + _entityCache = entityCache; + } + + public void Handle(RagonBuffer buffer) + { + _listenerList.OnLeft(); + _entityCache.Cleanup(); + _client.Room.Cleanup(); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/LoadSceneHandler.cs b/Ragon.Client/Sources/Handler/LoadSceneHandler.cs new file mode 100644 index 0000000..1479275 --- /dev/null +++ b/Ragon.Client/Sources/Handler/LoadSceneHandler.cs @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class SceneLoadHandler: Handler +{ + private readonly RagonClient _client; + private readonly RagonListenerList _listenerList; + + public SceneLoadHandler( + RagonClient ragonClient, + RagonListenerList listenerList + ) + { + _client = ragonClient; + _listenerList = listenerList; + } + + public void Handle(RagonBuffer buffer) + { + var sceneName = buffer.ReadString(); + var room = _client.Room; + + room.Cleanup(); + + _listenerList.OnLevel(sceneName); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/OwnershipHandler.cs b/Ragon.Client/Sources/Handler/OwnershipHandler.cs new file mode 100644 index 0000000..2fb75c6 --- /dev/null +++ b/Ragon.Client/Sources/Handler/OwnershipHandler.cs @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class OwnershipHandler: Handler +{ + private readonly RagonListenerList _listenerList; + private readonly RagonPlayerCache _playerCache; + private readonly RagonEntityCache _entityCache; + + public OwnershipHandler( + RagonListenerList listenerList, + RagonPlayerCache playerCache, + RagonEntityCache entityCache) + { + _listenerList = listenerList; + _playerCache = playerCache; + _entityCache = entityCache; + } + + public void Handle(RagonBuffer buffer) + { + var newOwnerId = buffer.ReadString(); + var player = _playerCache.GetPlayerById(newOwnerId); + + _playerCache.OnOwnershipChanged(newOwnerId); + _listenerList.OnOwnershipChanged(player); + + var entities = buffer.ReadUShort(); + for (var i = 0; i < entities; i++) + { + var entityId = buffer.ReadUShort(); + _entityCache.OnOwnershipChanged(player, entityId); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs b/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs new file mode 100644 index 0000000..3057d37 --- /dev/null +++ b/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class PlayerJoinHandler : Handler +{ + private RagonPlayerCache _playerCache; + private RagonListenerList _listenerList; + + public PlayerJoinHandler( + RagonPlayerCache playerCache, + RagonListenerList listenerList + ) + { + _playerCache = playerCache; + _listenerList = listenerList; + } + + public void Handle(RagonBuffer buffer) + { + var playerPeerId = buffer.ReadUShort(); + var playerId = buffer.ReadString(); + var playerName = buffer.ReadString(); + + _playerCache.AddPlayer(playerPeerId, playerId, playerName); + + var player = _playerCache.GetPlayerById(playerId); + if (player != null) + _listenerList.OnPlayerJoined(player); + else + RagonLog.Trace($"[Joined] {playerId}"); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs b/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs new file mode 100644 index 0000000..fcff2c9 --- /dev/null +++ b/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class PlayerLeftHandler : Handler +{ + private RagonPlayerCache _playerCache; + private RagonEntityCache _entityCache; + private RagonListenerList _listenerList; + + public PlayerLeftHandler( + RagonEntityCache entityCache, + RagonPlayerCache playerCache, + RagonListenerList listenerList + ) + { + _entityCache = entityCache; + _playerCache = playerCache; + _listenerList = listenerList; + } + + public void Handle(RagonBuffer buffer) + { + var playerId = buffer.ReadString(); + var player = _playerCache.GetPlayerById(playerId); + if (player != null) + { + _playerCache.RemovePlayer(playerId); + _listenerList.OnPlayerLeft(player); + + var entities = buffer.ReadUShort(); + var toDeleteIds = new ushort[entities]; + for (var i = 0; i < entities; i++) + { + var entityId = buffer.ReadUShort(); + toDeleteIds[i] = entityId; + } + + foreach (var id in toDeleteIds) + _entityCache.OnDestroy(id, new RagonPayload()); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/SnapshotHandler.cs b/Ragon.Client/Sources/Handler/SnapshotHandler.cs new file mode 100644 index 0000000..5cdde43 --- /dev/null +++ b/Ragon.Client/Sources/Handler/SnapshotHandler.cs @@ -0,0 +1,97 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class SnapshotHandler : Handler +{ + private RagonClient _client; + private RagonListenerList _listenerList; + private RagonEntityCache _entityCache; + private RagonPlayerCache _playerCache; + + public SnapshotHandler( + RagonClient ragonClient, + RagonListenerList listenerList, + RagonEntityCache entityCache, + RagonPlayerCache playerCache + ) + { + _client = ragonClient; + _listenerList = listenerList; + _entityCache = entityCache; + _playerCache = playerCache; + } + + public void Handle(RagonBuffer buffer) + { + var playersCount = buffer.ReadUShort(); + RagonLog.Trace("Players: " + playersCount); + for (var i = 0; i < playersCount; i++) + { + var playerPeerId = buffer.ReadUShort(); + var playerId = buffer.ReadString(); + var playerName = buffer.ReadString(); + + _playerCache.AddPlayer(playerPeerId, playerId, playerName); + } + var dynamicEntities = buffer.ReadUShort(); + RagonLog.Trace("Dynamic Entities: " + dynamicEntities); + for (var i = 0; i < dynamicEntities; i++) + { + var entityType = buffer.ReadUShort(); + var entityId = buffer.ReadUShort(); + var ownerPeerId = buffer.ReadUShort(); + var payloadSize = buffer.ReadUShort(); + var player = _playerCache.GetPlayerByPeer(ownerPeerId); + + var payload = new RagonPayload(payloadSize); + payload.Read(buffer); + + var hasAuthority = _playerCache.LocalPlayer.Id == player.Id; + var entity = _entityCache.OnCreate(0, entityType, 0, entityId, hasAuthority); + + entity.Read(buffer); + entity.Attach(_client, entityId, entityType, hasAuthority, player, payload); + } + + var staticEntities = buffer.ReadUShort(); + RagonLog.Trace("Scene Entities: " + staticEntities); + for (var i = 0; i < staticEntities; i++) + { + var entityType = buffer.ReadUShort(); + var entityId = buffer.ReadUShort(); + var staticId = buffer.ReadUShort(); + var ownerPeerId = buffer.ReadUShort(); + var payloadSize = buffer.ReadUShort(); + var player = _playerCache.GetPlayerByPeer(ownerPeerId); + + var payload = new RagonPayload(payloadSize); + payload.Read(buffer); + + var hasAuthority = _playerCache.LocalPlayer.Id == player.Id; + var entity = _entityCache.OnCreate(0, entityType, staticId, entityId, hasAuthority); + + entity.Read(buffer); + entity.Attach(_client, entityId, entityType, hasAuthority, player, payload); + } + + _listenerList.OnJoined(); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IO/DisconnectReason.cs b/Ragon.Client/Sources/IO/DisconnectReason.cs new file mode 100644 index 0000000..af1901d --- /dev/null +++ b/Ragon.Client/Sources/IO/DisconnectReason.cs @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public enum DisconnectReason +{ + MANUAL, + TIMEOUT, +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IO/INetworkChannel.cs b/Ragon.Client/Sources/IO/INetworkChannel.cs new file mode 100644 index 0000000..161ce63 --- /dev/null +++ b/Ragon.Client/Sources/IO/INetworkChannel.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface INetworkChannel +{ + void Send(byte[] data); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IO/INetworkConnection.cs b/Ragon.Client/Sources/IO/INetworkConnection.cs new file mode 100644 index 0000000..f32b730 --- /dev/null +++ b/Ragon.Client/Sources/IO/INetworkConnection.cs @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Client; + +public interface INetworkConnection: IRagonConnection +{ + public INetworkChannel Reliable { get; } + public INetworkChannel Unreliable { get; } + public Action OnData { get; set; } + public Action OnConnected { get; set; } + public Action OnDisconnected { get; set; } + public ulong BytesSent { get; } + public ulong BytesReceived { get; } + public int Ping { get; } + public void Prepare(); + public void Connect(string address, ushort port, uint protocol); + public void Disconnect(); + public void Update(); + public void Dispose(); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IO/NetworkStatistics.cs b/Ragon.Client/Sources/IO/NetworkStatistics.cs new file mode 100644 index 0000000..37aaece --- /dev/null +++ b/Ragon.Client/Sources/IO/NetworkStatistics.cs @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Client; + +public class NetworkStatistics +{ + private const double Interval = 1.0d; + private double _upstreamBandwidth = 0d; + private double _downstreamBandwidth = 0d; + private double _time = 0d; + private ulong _upstreamData = 0; + private ulong _downstreamData = 0; + private ulong _sent = 0; + private ulong _received = 0; + private int _ping; + + public int Ping => _ping; + public double UpstreamBandwidth => _upstreamBandwidth; + public double DownstreamBandwidth => _downstreamBandwidth; + + public void Update(ulong sent, ulong received, int ping, float dt) + { + _sent = sent; + _received = received; + _ping = ping; + + _time += dt; + if (_time >= Interval) + { + if (_upstreamData > 0) + { + _upstreamData = _sent - _upstreamData; + _upstreamBandwidth = (_upstreamData / _time) / 1000 ; + } + + if (_downstreamData > 0) + { + _downstreamData = _received - _downstreamData; + _downstreamBandwidth = (_downstreamData / _time) / 1000; + } + + _upstreamData = _sent; + _downstreamData = _received; + _time -= Interval; + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IRagonConnection.cs b/Ragon.Client/Sources/IRagonConnection.cs new file mode 100644 index 0000000..2a1c116 --- /dev/null +++ b/Ragon.Client/Sources/IRagonConnection.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonConnection +{ + +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IRagonEntityListener.cs b/Ragon.Client/Sources/IRagonEntityListener.cs new file mode 100644 index 0000000..6101d52 --- /dev/null +++ b/Ragon.Client/Sources/IRagonEntityListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonEntityListener +{ + public void OnEntityCreated(RagonEntity entity); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IRagonEvent.cs b/Ragon.Client/Sources/IRagonEvent.cs new file mode 100644 index 0000000..76a3073 --- /dev/null +++ b/Ragon.Client/Sources/IRagonEvent.cs @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client +{ + public interface IRagonEvent: IRagonSerializable + { + + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IRagonPayload.cs b/Ragon.Client/Sources/IRagonPayload.cs new file mode 100644 index 0000000..f68fbe0 --- /dev/null +++ b/Ragon.Client/Sources/IRagonPayload.cs @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client +{ + public interface IRagonPayload: IRagonSerializable + { + + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IRagonSceneCollector.cs b/Ragon.Client/Sources/IRagonSceneCollector.cs new file mode 100644 index 0000000..f2922e6 --- /dev/null +++ b/Ragon.Client/Sources/IRagonSceneCollector.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonSceneCollector +{ + public RagonEntity[] Collect(); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonAuthorizationListener.cs b/Ragon.Client/Sources/Listener/IRagonAuthorizationListener.cs new file mode 100644 index 0000000..356bb11 --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonAuthorizationListener.cs @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Client; + +public interface IRagonAuthorizationListener +{ + void OnAuthorizationSuccess(RagonClient client, string playerId, string playerName); + void OnAuthorizationFailed(RagonClient client, string message); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonConnectionListener.cs b/Ragon.Client/Sources/Listener/IRagonConnectionListener.cs new file mode 100644 index 0000000..5cec3e0 --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonConnectionListener.cs @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonConnectedListener +{ + void OnConnected(RagonClient client); + void OnDisconnected(RagonClient client); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonFailedListener.cs b/Ragon.Client/Sources/Listener/IRagonFailedListener.cs new file mode 100644 index 0000000..332d8e8 --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonFailedListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonFailedListener +{ + void OnFailed(RagonClient client, string message); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonJoinListener.cs b/Ragon.Client/Sources/Listener/IRagonJoinListener.cs new file mode 100644 index 0000000..c45e337 --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonJoinListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonJoinListener +{ + void OnJoined(RagonClient client); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonLeftListener.cs b/Ragon.Client/Sources/Listener/IRagonLeftListener.cs new file mode 100644 index 0000000..44d5205 --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonLeftListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonLeftListener +{ + void OnLeft(RagonClient client); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonLevelListener.cs b/Ragon.Client/Sources/Listener/IRagonLevelListener.cs new file mode 100644 index 0000000..f527caa --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonLevelListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonLevelListener +{ + void OnLevel(RagonClient client, string sceneName); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonListener.cs b/Ragon.Client/Sources/Listener/IRagonListener.cs new file mode 100644 index 0000000..933804e --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonListener.cs @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client +{ + public interface IRagonListener : + IRagonAuthorizationListener, + IRagonConnectedListener, + IRagonFailedListener, + IRagonJoinListener, + IRagonLeftListener, + IRagonLevelListener, + IRagonOwnershipChangedListener, + IRagonPlayerJoinListener, + IRagonPlayerLeftListener + { + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonOwnershipChangedListener.cs b/Ragon.Client/Sources/Listener/IRagonOwnershipChangedListener.cs new file mode 100644 index 0000000..e7052e9 --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonOwnershipChangedListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonOwnershipChangedListener +{ + void OnOwnershipChanged(RagonClient client, RagonPlayer player); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonPlayerJoinListener.cs b/Ragon.Client/Sources/Listener/IRagonPlayerJoinListener.cs new file mode 100644 index 0000000..6929f1b --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonPlayerJoinListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonPlayerJoinListener +{ + void OnPlayerJoined(RagonClient client, RagonPlayer player); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonPlayerLeftListener.cs b/Ragon.Client/Sources/Listener/IRagonPlayerLeftListener.cs new file mode 100644 index 0000000..2d28fbe --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonPlayerLeftListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonPlayerLeftListener +{ + void OnPlayerLeft(RagonClient client, RagonPlayer player); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Log/IRagonLogger.cs b/Ragon.Client/Sources/Log/IRagonLogger.cs new file mode 100644 index 0000000..d1edcb6 --- /dev/null +++ b/Ragon.Client/Sources/Log/IRagonLogger.cs @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client +{ + public interface IRagonLogger + { + public void Warn(string message); + public void Trace(string message); + public void Info(string message); + public void Error(string message); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Log/Loggers/RagonConsoleLogger.cs b/Ragon.Client/Sources/Log/Loggers/RagonConsoleLogger.cs new file mode 100644 index 0000000..3632003 --- /dev/null +++ b/Ragon.Client/Sources/Log/Loggers/RagonConsoleLogger.cs @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client +{ + public class RagonConsoleLogger: IRagonLogger + { + public void Warn(string message) + { + Console.WriteLine($"[WARN] {message}"); + } + + public void Trace(string message) + { + Console.WriteLine($"[TRACE] {message}"); + } + + public void Info(string message) + { + Console.WriteLine($"[INFO] {message}"); + } + + public void Error(string message) + { + Console.WriteLine($"[ERROR] {message}"); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Log/RagonLog.cs b/Ragon.Client/Sources/Log/RagonLog.cs new file mode 100644 index 0000000..803f0e8 --- /dev/null +++ b/Ragon.Client/Sources/Log/RagonLog.cs @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client +{ + public class RagonLog + { + private static IRagonLogger _ragonLogger; + static RagonLog() => _ragonLogger = new RagonConsoleLogger(); + public static void Set(IRagonLogger logger) => _ragonLogger = logger; + public static void Warn(string message) => _ragonLogger.Warn(message); + public static void Trace(string message) => _ragonLogger.Trace(message); + public static void Info(string message) => _ragonLogger.Info(message); + public static void Error(string message) => _ragonLogger.Error(message); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Log/RagonLogLevel.cs b/Ragon.Client/Sources/Log/RagonLogLevel.cs new file mode 100644 index 0000000..a808c40 --- /dev/null +++ b/Ragon.Client/Sources/Log/RagonLogLevel.cs @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public enum RagonLogLevel +{ + ALL, + CONNECTION, + STATE, + EVENT, + ROOM, +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonClient.cs b/Ragon.Client/Sources/RagonClient.cs new file mode 100644 index 0000000..642dc69 --- /dev/null +++ b/Ragon.Client/Sources/RagonClient.cs @@ -0,0 +1,191 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client +{ + public sealed class RagonClient + { + private readonly INetworkConnection _connection; + private readonly IRagonEntityListener _entityListener; + private readonly IRagonSceneCollector _sceneCollector; + private Handler[] _processors; + private RagonBuffer _readBuffer; + private RagonBuffer _writeBuffer; + private RagonRoom _room; + private RagonSession _session; + private RagonListenerList _listenerList; + private RagonPlayerCache _playerCache; + private RagonEntityCache _entityCache; + private RagonEventCache _eventCache; + private RagonStatus _status; + private NetworkStatistics _stats; + + private float _replicatationRate = 0; + private float _replicatationTime = 0; + + public IRagonConnection Connection => _connection; + public RagonStatus Status => _status; + public RagonSession Session => _session; + public RagonEventCache Event => _eventCache; + public RagonEntityCache Entity => _entityCache; + public NetworkStatistics Statistics => _stats; + public RagonRoom Room => _room; + + internal RagonBuffer Buffer => _writeBuffer; + internal INetworkChannel Reliable => _connection.Reliable; + internal INetworkChannel Unreliable => _connection.Unreliable; + + #region PUBLIC + + public RagonClient( + INetworkConnection connection, + IRagonEntityListener entityListener, + IRagonSceneCollector sceneCollector, + int rate) + { + _listenerList = new RagonListenerList(this); + _entityListener = entityListener; + _sceneCollector = sceneCollector; + + _connection = connection; + _connection.OnData += OnData; + _connection.OnConnected += OnConnected; + _connection.OnDisconnected += OnDisconnected; + + _replicatationRate = (1000.0f / rate) / 1000.0f; + _replicatationTime = 0; + + _eventCache = new RagonEventCache(); + _stats = new NetworkStatistics(); + _status = RagonStatus.DISCONNECTED; + } + + public void AddListener(IRagonListener listener) + { + _listenerList.Add(listener); + } + + public void RemoveListener(IRagonListener listener) + { + _listenerList.Remove(listener); + } + + public void Connect(string address, ushort port, string protocol) + { + _writeBuffer = new RagonBuffer(); + _readBuffer = new RagonBuffer(); + _session = new RagonSession(this, _readBuffer); + + _playerCache = new RagonPlayerCache(); + _entityCache = new RagonEntityCache(this, _playerCache, _entityListener, _sceneCollector); + + _processors = new Handler[byte.MaxValue]; + _processors[(byte)RagonOperation.AUTHORIZED_SUCCESS] = new AuthorizeSuccessHandler(_listenerList); + _processors[(byte)RagonOperation.AUTHORIZED_FAILED] = new AuthorizeFailedHandler(_listenerList); + _processors[(byte)RagonOperation.JOIN_SUCCESS] = + new JoinSuccessHandler(this, _readBuffer, _listenerList, _playerCache, _entityCache); + _processors[(byte)RagonOperation.JOIN_FAILED] = new JoinFailedHandler(_listenerList); + _processors[(byte)RagonOperation.LEAVE_ROOM] = new LeaveRoomHandler(this, _listenerList, _entityCache); + _processors[(byte)RagonOperation.OWNERSHIP_CHANGED] = + new OwnershipHandler(_listenerList, _playerCache, _entityCache); + _processors[(byte)RagonOperation.PLAYER_JOINED] = new PlayerJoinHandler(_playerCache, _listenerList); + _processors[(byte)RagonOperation.PLAYER_LEAVED] = + new PlayerLeftHandler(_entityCache, _playerCache, _listenerList); + _processors[(byte)RagonOperation.LOAD_SCENE] = new SceneLoadHandler(this, _listenerList); + _processors[(byte)RagonOperation.CREATE_ENTITY] = new EntityCreateHandler(this, _playerCache, _entityCache); + _processors[(byte)RagonOperation.DESTROY_ENTITY] = new EntityDestroyHandler(_entityCache); + _processors[(byte)RagonOperation.REPLICATE_ENTITY_STATE] = new StateEntityHandler(_entityCache); + _processors[(byte)RagonOperation.REPLICATE_ENTITY_EVENT] = + new EntityEventHandler(this, _playerCache, _entityCache); + _processors[(byte)RagonOperation.SNAPSHOT] = + new SnapshotHandler(this, _listenerList, _entityCache, _playerCache); + + var protocolRaw = RagonVersion.Parse(protocol); + _connection.Connect(address, port, protocolRaw); + } + + public void Disconnect() + { + _status = RagonStatus.DISCONNECTED; + _room.Cleanup(); + _connection.Disconnect(); + OnDisconnected(DisconnectReason.MANUAL); + } + + public void Update(float dt) + { + _replicatationTime += dt; + if (_replicatationTime >= _replicatationRate) + { + _entityCache.WriteState(_readBuffer); + _replicatationTime = 0; + } + + _stats.Update(_connection.BytesSent, _connection.BytesReceived, _connection.Ping, dt); + _connection.Update(); + } + + public void Dispose() + { + _status = RagonStatus.DISCONNECTED; + _connection.Disconnect(); + _connection.Dispose(); + } + + #endregion + + #region INTERNAL + + internal void AssignRoom(RagonRoom room) + { + _room = room; + _status = RagonStatus.ROOM; + } + + #endregion + + #region PRIVATE + + private void OnConnected() + { + RagonLog.Trace("Connected"); + + _listenerList.OnConnected(); + _status = RagonStatus.CONNECTED; + } + + private void OnDisconnected(DisconnectReason reason) + { + RagonLog.Trace($"Disconnected: {reason}"); + + _listenerList.OnDisconnected(); + _status = RagonStatus.DISCONNECTED; + } + + public void OnData(byte[] data) + { + _readBuffer.Clear(); + _readBuffer.FromArray(data); + + var operation = _readBuffer.ReadByte(); + _processors[operation].Handle(_readBuffer); + } + + #endregion + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonEntityCache.cs b/Ragon.Client/Sources/RagonEntityCache.cs new file mode 100644 index 0000000..0c95774 --- /dev/null +++ b/Ragon.Client/Sources/RagonEntityCache.cs @@ -0,0 +1,232 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +public sealed class RagonEntityCache +{ + private readonly List _entityList = new(); + private readonly Dictionary _entityMap = new(); + private readonly Dictionary _pendingEntities = new(); + private readonly Dictionary _sceneEntities = new(); + + private readonly RagonClient _client; + private readonly IRagonEntityListener _entityListener; + private readonly IRagonSceneCollector _sceneCollector; + private readonly RagonPlayerCache _playerCache; + + private int _localEntitiesCounter = 0; + + public RagonEntityCache( + RagonClient client, + RagonPlayerCache playerCache, + IRagonEntityListener listener, + IRagonSceneCollector sceneCollector + ) + { + _client = client; + _entityListener = listener; + _sceneCollector = sceneCollector; + _playerCache = playerCache; + } + + public RagonEntity FindById(ushort id) + { + return _entityMap[id]; + } + + public void Create(RagonEntity entity, IRagonPayload? spawnPayload) + { + var attachId = (ushort) (_playerCache.LocalPlayer.PeerId + _localEntitiesCounter++) ; + var buffer = _client.Buffer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.CREATE_ENTITY); + buffer.WriteUShort(attachId); + buffer.WriteUShort(entity.Type); + buffer.WriteByte((byte) entity.Authority); + + entity.State.WriteInfo(buffer); + + spawnPayload?.Serialize(buffer); + + _pendingEntities.Add(attachId, entity); + + var sendData = buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + public void Destroy(RagonEntity entity, IRagonPayload? destroyPayload) + { + if (!entity.IsAttached) + { + RagonLog.Warn("Can't destroy object, he is not created"); + return; + } + var buffer = _client.Buffer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.DESTROY_ENTITY); + buffer.WriteUShort(entity.Id); + + destroyPayload?.Serialize(buffer); + + var sendData = buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + internal void WriteState(RagonBuffer buffer) + { + var changedEntities = 0u; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE); + + var offset = buffer.WriteOffset; + buffer.Write(0, 16); + + foreach (var ent in _entityList) + { + if (!ent.IsAttached || + !ent.Replication || + !ent.PropertiesChanged) continue; + + ent.Write(buffer); + + changedEntities++; + } + + if (changedEntities <= 0) return; + + buffer.Write(changedEntities, 16, offset); + + var data = buffer.ToArray(); + _client.Unreliable.Send(data); + } + + internal void WriteScene(RagonBuffer buffer) + { + _sceneEntities.Clear(); + + var entities = _sceneCollector.Collect(); + buffer.WriteUShort((ushort) entities.Length); + foreach (var entity in entities) + { + buffer.WriteUShort(entity.Type); + buffer.WriteByte((byte) entity.Authority); + buffer.WriteUShort(entity.SceneId); + + entity.State.WriteInfo(buffer); + + _sceneEntities.Add(entity.SceneId, entity); + } + } + + internal void CacheScene() + { + _sceneEntities.Clear(); + + var entities = _sceneCollector.Collect(); + foreach (var entity in entities) + _sceneEntities.Add(entity.SceneId, entity); + } + + internal void Cleanup() + { + var payload = new RagonPayload(); + foreach (var ent in _entityList) + ent.Detach(payload); + + _entityMap.Clear(); + _entityList.Clear(); + } + + internal RagonEntity OnCreate(ushort attachId, ushort entityType, ushort sceneId, ushort entityId, bool hasAuthority) + { + if (sceneId > 0) + { + if (_sceneEntities.TryGetValue(sceneId, out var entity)) + { + _entityMap.Add(entityId, entity); + + if (hasAuthority) + _entityList.Add(entity); + + return entity; + } + } + + if (_pendingEntities.Remove(attachId, out var existsEntity)) + { + _entityMap.Add(entityId, existsEntity); + + if (hasAuthority) + _entityList.Add(existsEntity); + + return existsEntity; + } + else + { + var entity = new RagonEntity(entityType, sceneId); + + _entityMap.Add(entityId, entity); + + if (hasAuthority) + _entityList.Add(entity); + + _entityListener.OnEntityCreated(entity); + + return entity; + } + } + + + internal void OnDestroy(ushort entityId, RagonPayload payload) + { + if (_entityMap.Remove(entityId, out var ragonEntity)) + { + _entityList.Remove(ragonEntity); + + ragonEntity.Detach(payload); + } + } + + internal void OnState(ushort entityId, RagonBuffer buffer) + { + if (_entityMap.TryGetValue(entityId, out var entity)) + entity.Read(buffer); + else + RagonLog.Warn($"Entity {entityId} not found!"); + } + + internal void OnEvent(RagonPlayer player, ushort entityId, ushort eventCode, RagonBuffer buffer) + { + if (_entityMap.TryGetValue(entityId, out var entity)) + entity.Event(eventCode, player, buffer); + else + RagonLog.Warn($"Entity {entityId} not found!"); + } + + internal void OnOwnershipChanged(RagonPlayer player, ushort entityId) + { + if (_entityMap.TryGetValue(entityId, out var entity)) + entity.OnOwnershipChanged(player); + else + RagonLog.Warn($"Entity {entityId} not found!"); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonEventCache.cs b/Ragon.Client/Sources/RagonEventCache.cs new file mode 100644 index 0000000..b0b752e --- /dev/null +++ b/Ragon.Client/Sources/RagonEventCache.cs @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public class RagonEventCache +{ + private readonly Dictionary _eventsRegistryByType = new(); + private readonly HashSet _codes = new(); + private readonly HashSet _types = new(); + private ushort _eventIdGenerator = 0; + + public ushort GetEventCode(TEvent _) where TEvent : IRagonEvent + { + var type = typeof(TEvent); + var evntCode = _eventsRegistryByType[type]; + return evntCode; + } + + public void Register() where T : IRagonEvent, new() + { + var type = typeof(T); + if (_types.Contains(type)) + { + RagonLog.Trace($"[Ragon] Event already registered: {type.Name}"); + return; + } + + RagonLog.Trace($"[Ragon] Registered Event: {type.Name} - {_eventIdGenerator}"); + + _eventsRegistryByType.Add(type, _eventIdGenerator); + _codes.Add(_eventIdGenerator); + _types.Add(type); + + _eventIdGenerator++; + } + + public void Register(ushort evntCode) where T : IRagonEvent, new() + { + var type = typeof(T); + if (_codes.Contains(evntCode) || _types.Contains(type)) + { + RagonLog.Warn($"[Ragon] Event already registered: {type.Name} - {evntCode}"); + return; + } + + RagonLog.Trace($"[Ragon] Registered Event: {type.Name} - {evntCode}"); + + _codes.Add(evntCode); + _types.Add(type); + _eventsRegistryByType.Add(type, evntCode); + } + + public T Create() where T : IRagonEvent, new() + { + return new T(); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonListenerList.cs b/Ragon.Client/Sources/RagonListenerList.cs new file mode 100644 index 0000000..cad7677 --- /dev/null +++ b/Ragon.Client/Sources/RagonListenerList.cs @@ -0,0 +1,106 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client +{ + internal class RagonListenerList + { + public int Count => _listeners.Count; + private List _listeners = new(); + private RagonClient _client; + + public RagonListenerList(RagonClient client) + { + _client = client; + } + + public void Add(IRagonListener listener) + { + _listeners.Add(listener); + } + + public void Remove(IRagonListener listener) + { + _listeners.Remove(listener); + } + + public void OnAuthorizationSuccess(string playerId, string playerName) + { + foreach (var listener in _listeners) + listener.OnAuthorizationSuccess(_client, playerId, playerName); + } + + public void OnAuthorizationFailed(string message) + { + foreach (var listener in _listeners) + listener.OnAuthorizationFailed(_client, message); + } + + public void OnLeft() + { + foreach (var listener in _listeners) + listener.OnLeft(_client); + } + + public void OnFailed(string message) + { + foreach (var listener in _listeners) + listener.OnFailed(_client, message); + } + + public void OnOwnershipChanged(RagonPlayer player) + { + foreach (var listener in _listeners) + listener.OnOwnershipChanged(_client, player); + } + + public void OnPlayerLeft(RagonPlayer player) + { + foreach (var listener in _listeners) + listener.OnPlayerLeft(_client, player); + } + + public void OnPlayerJoined(RagonPlayer player) + { + foreach (var listener in _listeners) + listener.OnPlayerJoined(_client, player); + } + + public void OnLevel(string sceneName) + { + foreach (var listener in _listeners) + listener.OnLevel(_client, sceneName); + } + + public void OnJoined() + { + foreach (var listener in _listeners) + listener.OnJoined(_client); + } + + public void OnConnected() + { + foreach (var listener in _listeners) + listener.OnConnected(_client); + } + + public void OnDisconnected() + { + foreach (var listener in _listeners) + listener.OnDisconnected(_client); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonPlayer.cs b/Ragon.Client/Sources/RagonPlayer.cs new file mode 100644 index 0000000..e8b8f4a --- /dev/null +++ b/Ragon.Client/Sources/RagonPlayer.cs @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client +{ + [Serializable] + public class RagonPlayer + { + public string Id { get; private set; } + public string Name { get; set; } + public ushort PeerId { get; set; } + public bool IsRoomOwner { get; set; } + public bool IsMe { get; set; } + + public RagonPlayer(ushort peerId, string playerId, string name, bool isRoomOwner, bool isMe) + { + PeerId = peerId; + IsRoomOwner = isRoomOwner; + IsMe = isMe; + Name = name; + Id = playerId; + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonPlayerCache.cs b/Ragon.Client/Sources/RagonPlayerCache.cs new file mode 100644 index 0000000..c5d8f0a --- /dev/null +++ b/Ragon.Client/Sources/RagonPlayerCache.cs @@ -0,0 +1,90 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public sealed class RagonPlayerCache +{ + private List _players = new List(); + private Dictionary _playersById = new(); + private Dictionary _playersByConnection = new(); + + public RagonPlayer Owner { get; private set; } + public RagonPlayer LocalPlayer { get; private set; } + public bool IsRoomOwner => _ownerId == _localId; + + public RagonPlayer? GetPlayerById(string playerId) => _playersById[playerId]; + public RagonPlayer? GetPlayerByPeer(ushort peerId) => _playersByConnection[peerId]; + + private string _ownerId; + private string _localId; + + public void SetOwnerAndLocal(string ownerId, string localId) + { + _ownerId = ownerId; + _localId = localId; + } + + public void AddPlayer(ushort peerId, string playerId, string playerName) + { + if (_playersById.ContainsKey(playerId)) + return; + + var isOwner = playerId == _ownerId; + var isLocal = playerId == _localId; + + RagonLog.Trace($"Added player {peerId}|{playerId}|{playerName} IsOwner: {isOwner} isLocal: {isLocal}"); + + var player = new RagonPlayer(peerId, playerId, playerName, isOwner, isLocal); + + if (player.IsMe) + LocalPlayer = player; + + if (player.IsRoomOwner) + Owner = player; + + _players.Add(player); + _playersById.Add(player.Id, player); + _playersByConnection.Add(player.PeerId, player); + } + + public void RemovePlayer(string playerId) + { + if (_playersById.Remove(playerId, out var player)) + { + _players.Remove(player); + _playersByConnection.Remove(player.PeerId); + } + } + + public void OnOwnershipChanged(string playerId) + { + foreach (var player in _players) + { + if (player.Id == playerId) + Owner = player; + player.IsRoomOwner = player.Id == playerId; + } + } + + + public void Cleanup() + { + _players.Clear(); + _playersByConnection.Clear(); + _playersById.Clear(); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonRoom.cs b/Ragon.Client/Sources/RagonRoom.cs new file mode 100644 index 0000000..b6bdaab --- /dev/null +++ b/Ragon.Client/Sources/RagonRoom.cs @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client +{ + public class RagonRoom + { + private RagonClient _client; + private RagonScene _scene; + private RagonEntityCache _entityCache; + private RagonPlayerCache _playerCache; + private RagonRoomInformation _information; + + public string Id => _information.RoomId; + public int MinPlayers => _information.Min; + public int MaxPlayers => _information.Max; + + public RagonPlayer Local => _playerCache.LocalPlayer; + public RagonPlayer Owner => _playerCache.Owner; + + public RagonRoom(RagonClient client, + RagonEntityCache entityCache, + RagonPlayerCache playerCache, + RagonRoomInformation information, + RagonScene scene) + { + _client = client; + _information = information; + _entityCache = entityCache; + _playerCache = playerCache; + _scene = scene; + } + + internal void Cleanup() + { + _entityCache.Cleanup(); + _playerCache.Cleanup(); + } + + public void LoadScene(string map) => _scene.Load(map); + public void SceneLoaded() => _scene.SceneLoaded(); + + public void CreateEntity(RagonEntity entity) => CreateEntity(entity, null); + public void CreateEntity(RagonEntity entity, IRagonPayload? payload) => _entityCache.Create(entity, payload); + + public void DestroyEntity(RagonEntity entityId) => DestroyEntity(entityId, null); + public void DestroyEntity(RagonEntity entityId, IRagonPayload? payload) => _entityCache.Destroy(entityId, payload); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonScene.cs b/Ragon.Client/Sources/RagonScene.cs new file mode 100644 index 0000000..38ccab3 --- /dev/null +++ b/Ragon.Client/Sources/RagonScene.cs @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +public class RagonScene +{ + private readonly RagonClient _client; + private readonly RagonEntityCache _entityCache; + private readonly RagonPlayerCache _playerCache; + + public RagonScene(RagonClient client, RagonPlayerCache playerCache, RagonEntityCache entityCache) + { + _client = client; + _playerCache = playerCache; + _entityCache = entityCache; + } + + internal void Load(string map) + { + var buffer = _client.Buffer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.LOAD_SCENE); + buffer.WriteString(map); + + var sendData = buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + internal void SceneLoaded() + { + var buffer = _client.Buffer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.SCENE_LOADED); + + if (_playerCache.IsRoomOwner) + _entityCache.WriteScene(buffer); + else + _entityCache.CacheScene(); + + var sendData = buffer.ToArray(); + _client.Reliable.Send(sendData); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonSession.cs b/Ragon.Client/Sources/RagonSession.cs new file mode 100644 index 0000000..29914ec --- /dev/null +++ b/Ragon.Client/Sources/RagonSession.cs @@ -0,0 +1,109 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client +{ + public class RagonSession + { + private readonly RagonClient _client; + private readonly RagonBuffer _buffer; + + public RagonSession(RagonClient client, RagonBuffer buffer) + { + _client = client; + _buffer = buffer; + } + + public void CreateOrJoin(string map, int minPlayers, int maxPlayers) + { + var parameters = new RagonRoomParameters() {Map = map, Min = minPlayers, Max = maxPlayers}; + CreateOrJoin(parameters); + } + + public void CreateOrJoin(RagonRoomParameters parameters) + { + _buffer.Clear(); + _buffer.WriteOperation(RagonOperation.JOIN_OR_CREATE_ROOM); + + parameters.Serialize(_buffer); + + var sendData = _buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + public void Create(string map, int minPlayers, int maxPlayers) + { + Create(null, new RagonRoomParameters() {Map = map, Min = minPlayers, Max = maxPlayers}); + } + + public void Create(string roomId, string map, int minPlayers, int maxPlayers) + { + Create(roomId, new RagonRoomParameters() {Map = map, Min = minPlayers, Max = maxPlayers}); + } + + public void Create(string roomId, RagonRoomParameters parameters) + { + _buffer.Clear(); + _buffer.WriteOperation(RagonOperation.CREATE_ROOM); + + if (roomId != null) + { + _buffer.WriteBool(true); + _buffer.WriteString(roomId); + } + else + { + _buffer.WriteBool(false); + } + + parameters.Serialize(_buffer); + + var sendData = _buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + public void Leave() + { + var sendData = new[] {(byte) RagonOperation.LEAVE_ROOM}; + _client.Reliable.Send(sendData); + } + + public void Join(string roomId) + { + _buffer.Clear(); + _buffer.WriteOperation(RagonOperation.JOIN_ROOM); + _buffer.WriteString(roomId); + + var sendData = _buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + public void AuthorizeWithKey(string key, string playerName, byte[] additonalData) + { + _buffer.Clear(); + _buffer.WriteOperation(RagonOperation.AUTHORIZE); + _buffer.WriteString(key); + _buffer.WriteString(playerName); + _buffer.WriteBytes(additonalData); + + var sendData = _buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonStatus.cs b/Ragon.Client/Sources/RagonStatus.cs new file mode 100644 index 0000000..c64d029 --- /dev/null +++ b/Ragon.Client/Sources/RagonStatus.cs @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client +{ + public enum RagonStatus + { + DISCONNECTED, + CONNECTED, + ROOM, + LOBBY, + } +} \ No newline at end of file diff --git a/Ragon.Core/Action/IAction.cs b/Ragon.Core/Action/IAction.cs deleted file mode 100644 index a8b0017..0000000 --- a/Ragon.Core/Action/IAction.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ragon.Core.Time; - -public interface IAction -{ - public void Tick(); -} \ No newline at end of file diff --git a/Ragon.Core/Action/Loop.cs b/Ragon.Core/Action/Loop.cs deleted file mode 100644 index 88155a1..0000000 --- a/Ragon.Core/Action/Loop.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Ragon.Core.Time; - -public class Loop -{ - private List _tasks; - - public Loop() - { - - _tasks = new List(35); - } - - public void Run(IAction task) - { - _tasks.Add(task); - } - - public void Stop(IAction task) - { - _tasks.Remove(task); - } - - public void Tick() - { - foreach (var task in _tasks) - task.Tick(); - } -} \ No newline at end of file diff --git a/Ragon.Core/Application.cs b/Ragon.Core/Application.cs deleted file mode 100644 index 074a653..0000000 --- a/Ragon.Core/Application.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Diagnostics; -using NLog; -using Ragon.Common; -using Ragon.Core.Lobby; -using Ragon.Core.Server; -using Ragon.Core.Time; -using Ragon.Server; -using Ragon.Server.ENet; - -namespace Ragon.Core; - -public class Application : INetworkListener -{ - private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - private readonly INetworkServer _server; - private readonly Thread _dedicatedThread; - private readonly Executor _executor; - private readonly Configuration _configuration; - private readonly HandlerRegistry _handlerRegistry; - private readonly ILobby _lobby; - private readonly Loop _loop; - private readonly Dictionary _contexts; - - public Application(Configuration configuration) - { - _configuration = configuration; - _executor = new Executor(); - _dedicatedThread = new Thread(Execute); - _dedicatedThread.IsBackground = true; - _contexts = new Dictionary(); - _handlerRegistry = new HandlerRegistry(); - _lobby = new LobbyInMemory(); - _loop = new Loop(); - - if (configuration.ServerType == "enet") - _server = new ENetServer(); - - if (configuration.ServerType == "websocket") - _server = new NativeWebSocketServer(_executor); - - Debug.Assert(_server != null, $"Socket type not supported: {configuration.ServerType}. Supported: [enet, websocket]"); - } - - public void Execute() - { - while (true) - { - _executor.Execute(); - _loop.Tick(); - _server.Poll(); - - Thread.Sleep((int)1000.0f / _configuration.ServerTickRate); - } - } - - public void Start() - { - var networkConfiguration = new NetworkConfiguration() - { - LimitConnections = _configuration.LimitConnections, - Protocol = RagonVersion.Parse(_configuration.GameProtocol), - Address = "0.0.0.0", - Port = _configuration.Port, - }; - - _server.Start(this, networkConfiguration); - _dedicatedThread.Start(); - } - - public void Stop() - { - _server.Stop(); - _dedicatedThread.Interrupt(); - } - - public void OnConnected(INetworkConnection connection) - { - var context = new PlayerContext(connection, new LobbyPlayer(connection)); - context.Lobby = _lobby; - context.Loop = _loop; - - _logger.Trace($"Connected {connection.Id}"); - _contexts.Add(connection.Id, context); - } - - public void OnDisconnected(INetworkConnection connection) - { - _logger.Trace($"Disconnected {connection.Id}"); - - if (_contexts.Remove(connection.Id, out var context)) - { - var room = context.Room; - if (room != null) - { - room.RemovePlayer(context.RoomPlayer); - - _lobby.RemoveIfEmpty(room); - } - - context.Dispose(); - } - } - - public void OnTimeout(INetworkConnection connection) - { - if (_contexts.Remove(connection.Id, out var context)) - context.Dispose(); - } - - public void OnData(INetworkConnection connection, byte[] data) - { - if (_contexts.TryGetValue(connection.Id, out var context)) - _handlerRegistry.Handle(context, data); - } -} \ No newline at end of file diff --git a/Ragon.Core/Game/Entity.cs b/Ragon.Core/Game/Entity.cs deleted file mode 100644 index 0bbc205..0000000 --- a/Ragon.Core/Game/Entity.cs +++ /dev/null @@ -1,188 +0,0 @@ -using Ragon.Common; - -namespace Ragon.Core.Game; - -public class Entity -{ - private static ushort _idGenerator = 0; - public ushort Id { get; private set; } - public ushort Type { get; private set; } - public ushort StaticId { get; private set; } - public RoomPlayer Owner { get; private set; } - public RagonAuthority Authority { get; private set; } - public EntityState State { get; private set; } - public byte[] Payload { get; private set; } - - private readonly List _bufferedEvents; - - public Entity(RoomPlayer owner, ushort type, ushort staticId, RagonAuthority eventAuthority) - { - Owner = owner; - StaticId = staticId; - Type = type; - Id = _idGenerator++; - Payload = Array.Empty(); - Authority = eventAuthority; - State = new EntityState(this); - - _bufferedEvents = new List(); - } - - public void SetPayload(byte[] payload) - { - Payload = payload; - } - - public void SetOwner(RoomPlayer owner) - { - Owner = owner; - } - - public void RestoreBufferedEvents(RoomPlayer roomPlayer, RagonSerializer writer) - { - foreach (var bufferedEvent in _bufferedEvents) - { - writer.Clear(); - writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); - writer.WriteUShort(bufferedEvent.EventId); - writer.WriteUShort(bufferedEvent.Invoker.Connection.Id); - writer.WriteByte((byte)RagonReplicationMode.Server); - writer.WriteUShort(Id); - - ReadOnlySpan data = bufferedEvent.EventData.AsSpan(); - writer.WriteData(ref data); - - var sendData = writer.ToArray(); - roomPlayer.Connection.Reliable.Send(sendData); - } - } - - public void Create() - { - var room = Owner.Room; - var serializer = room.Writer; - - serializer.Clear(); - serializer.WriteOperation(RagonOperation.CREATE_ENTITY); - serializer.WriteUShort(Type); - serializer.WriteUShort(Id); - serializer.WriteUShort(Owner.Connection.Id); - - ReadOnlySpan entityPayload = Payload.AsSpan(); - serializer.WriteUShort((ushort)entityPayload.Length); - serializer.WriteData(ref entityPayload); - - var sendData = serializer.ToArray(); - foreach (var player in room.ReadyPlayersList) - player.Connection.Reliable.Send(sendData); - } - - public void Destroy(byte[] payload) - { - var room = Owner.Room; - var serializer = room.Writer; - - serializer.Clear(); - serializer.WriteOperation(RagonOperation.DESTROY_ENTITY); - serializer.WriteInt(Id); - serializer.WriteUShort(0); - // serializer.WriteData(ref Payload); - - var sendData = serializer.ToArray(); - foreach (var player in room.ReadyPlayersList) - player.Connection.Reliable.Send(sendData); - } - - public void ReplicateEvent( - RoomPlayer caller, - ushort eventId, - ReadOnlySpan payload, - RagonReplicationMode eventMode, - RoomPlayer targetPlayer - ) - { - var room = Owner.Room; - var serializer = room.Writer; - - serializer.Clear(); - serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); - serializer.WriteUShort(eventId); - serializer.WriteUShort(caller.Connection.Id); - serializer.WriteByte((byte)eventMode); - serializer.WriteUShort(Id); - serializer.WriteData(ref payload); - - var sendData = serializer.ToArray(); - targetPlayer.Connection.Reliable.Send(sendData); - } - - public void ReplicateEvent( - RoomPlayer caller, - ushort eventId, - ReadOnlySpan payload, - RagonReplicationMode eventMode, - RagonTarget targetMode - ) - { - if (Authority == RagonAuthority.OwnerOnly && - Owner.Connection.Id != caller.Connection.Id) - { - Console.WriteLine($"Player have not enought authority for event with Id {eventId}"); - return; - } - - if (eventMode == RagonReplicationMode.Buffered && targetMode != RagonTarget.Owner) - { - var bufferedEvent = new EntityEvent(caller, eventId, payload.ToArray(), targetMode); - _bufferedEvents.Add(bufferedEvent); - } - - var room = Owner.Room; - var serializer = room.Writer; - - serializer.Clear(); - serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); - serializer.WriteUShort(eventId); - serializer.WriteUShort(caller.Connection.Id); - serializer.WriteByte((byte)eventMode); - serializer.WriteUShort(Id); - serializer.WriteData(ref payload); - - var sendData = serializer.ToArray(); - - switch (targetMode) - { - case RagonTarget.Owner: - { - Owner.Connection.Reliable.Send(sendData); - break; - } - case RagonTarget.ExceptOwner: - { - foreach (var roomPlayer in room.ReadyPlayersList) - { - if (roomPlayer.Connection.Id != Owner.Connection.Id) - roomPlayer.Connection.Reliable.Send(sendData); - } - - break; - } - case RagonTarget.ExceptInvoker: - { - foreach (var roomPlayer in room.ReadyPlayersList) - { - if (roomPlayer.Connection.Id != caller.Connection.Id) - roomPlayer.Connection.Reliable.Send(sendData); - } - - break; - } - case RagonTarget.All: - { - foreach (var roomPlayer in room.ReadyPlayersList) - roomPlayer.Connection.Reliable.Send(sendData); - break; - } - } - } -} \ No newline at end of file diff --git a/Ragon.Core/Game/EntityEvent.cs b/Ragon.Core/Game/EntityEvent.cs deleted file mode 100644 index 7d50d5e..0000000 --- a/Ragon.Core/Game/EntityEvent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Ragon.Common; - -namespace Ragon.Core.Game; - -public class EntityEvent -{ - public RoomPlayer Invoker { get; private set; } - public ushort EventId { get; private set; } - public byte[] EventData { get; private set; } - public RagonTarget Target { set; private get; } - - public EntityEvent( - RoomPlayer invoker, - ushort eventId, - byte[] payload, - RagonTarget target - ) - { - Invoker = invoker; - EventId = eventId; - EventData = payload; - Target = target; - } -} \ No newline at end of file diff --git a/Ragon.Core/Game/EntityList.cs b/Ragon.Core/Game/EntityList.cs deleted file mode 100644 index c675684..0000000 --- a/Ragon.Core/Game/EntityList.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Ragon.Core.Game; - -public class EntityList -{ - private readonly List _dynamicEntitiesList = new List(); - private readonly List _staticEntitiesList = new List(); - private readonly Dictionary _entitiesMap = new Dictionary(); - - public IReadOnlyList StaticList => _staticEntitiesList; - public IReadOnlyList DynamicList => _dynamicEntitiesList; - public IReadOnlyDictionary Map => _entitiesMap; - - public void Add(Entity entity) - { - if (entity.StaticId != 0) - _staticEntitiesList.Add(entity); - else - _dynamicEntitiesList.Add(entity); - - _entitiesMap.Add(entity.Id, entity); - } - - public bool Remove(Entity entity) - { - if (_entitiesMap.Remove(entity.Id, out var existEntity)) - { - _staticEntitiesList.Remove(entity); - _dynamicEntitiesList.Remove(entity); - - return true; - } - return false; - } -} \ No newline at end of file diff --git a/Ragon.Core/Game/EntityState.cs b/Ragon.Core/Game/EntityState.cs deleted file mode 100644 index 6056663..0000000 --- a/Ragon.Core/Game/EntityState.cs +++ /dev/null @@ -1,99 +0,0 @@ -using NLog; -using Ragon.Common; - -namespace Ragon.Core.Game; - -public class EntityState -{ - private List _properties; - private Entity _entity; - - public EntityState(Entity entity, int capacity = 10) - { - _entity = entity; - _properties = new List(10); - } - - public void AddProperty(EntityStateProperty property) - { - _properties.Add(property); - } - - public void Write(RagonSerializer serializer) - { - serializer.WriteUShort(_entity.Id); - - for (int propertyIndex = 0; propertyIndex < _properties.Count; propertyIndex++) - { - var property = _properties[propertyIndex]; - if (property.IsDirty) - { - serializer.WriteBool(true); - var span = serializer.GetWritableData(property.Size); - var data = property.Read(); - data.CopyTo(span); - property.Clear(); - } - else - { - serializer.WriteBool(false); - } - } - } - - public void Read(RagonSerializer serializer) - { - for (var i = 0; i < _properties.Count; i++) - { - if (serializer.ReadBool()) - { - var property = _properties[i]; - var size = property.Size; - if (!property.IsFixed) - size = serializer.ReadUShort(); - - if (size > property.Capacity) - { - Console.WriteLine($"Property {i} payload too large, size: {size}"); - continue; - } - - var propertyPayload = serializer.ReadData(size); - property.Write(ref propertyPayload); - property.Size = size; - } - } - } - - public void Snapshot(RagonSerializer serializer) - { - ReadOnlySpan payload = _entity.Payload.AsSpan(); - - serializer.WriteUShort(_entity.Type); - serializer.WriteUShort(_entity.Id); - - if (_entity.StaticId != 0) - serializer.WriteUShort(_entity.StaticId); - - serializer.WriteUShort(_entity.Owner.Connection.Id); - serializer.WriteUShort((ushort) payload.Length); - serializer.WriteData(ref payload); - - for (int propertyIndex = 0; propertyIndex < _properties.Count; propertyIndex++) - { - var property = _properties[propertyIndex]; - var hasPayload = property.IsFixed || property.Size > 0 && !property.IsFixed; - if (hasPayload) - { - serializer.WriteBool(true); - var span = serializer.GetWritableData(property.Size); - var data = property.Read(); - data.CopyTo(span); - } - else - { - serializer.WriteBool(false); - } - } - } -} \ No newline at end of file diff --git a/Ragon.Core/Game/EntityStateProperty.cs b/Ragon.Core/Game/EntityStateProperty.cs deleted file mode 100644 index 2f98956..0000000 --- a/Ragon.Core/Game/EntityStateProperty.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Ragon.Common; - -namespace Ragon.Core.Game; - -public class EntityStateProperty -{ - public int Size { get; set; } - public int Capacity { get; set; } - public bool IsDirty { get; private set; } - public bool IsFixed { get; private set; } - private byte[] _data; - - public EntityStateProperty(int size, bool isFixed) - { - Capacity = 512; - Size = size; - IsFixed = isFixed; - IsDirty = true; - - _data = new byte[Capacity]; - } - - public ReadOnlySpan Read() - { - var dataSpan = _data.AsSpan(); - var src = dataSpan.Slice(0, Size); - return src; - } - - public void Write(ref ReadOnlySpan src) - { - src.CopyTo(_data); - IsDirty = true; - } - - public void Clear() - { - IsDirty = false; - } -} \ No newline at end of file diff --git a/Ragon.Core/Game/RoomInformation.cs b/Ragon.Core/Game/RoomInformation.cs deleted file mode 100644 index f0acc0e..0000000 --- a/Ragon.Core/Game/RoomInformation.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Ragon.Core.Game; - -public class RoomInformation -{ - public string Map { get; init; } = "none"; - public int Min { get; init; } - public int Max { get; init; } - - public override string ToString() - { - return $"Map: {Map} Count: {Min}/{Max}"; - } -} \ No newline at end of file diff --git a/Ragon.Core/Game/RoomPlayer.cs b/Ragon.Core/Game/RoomPlayer.cs deleted file mode 100644 index c6224b0..0000000 --- a/Ragon.Core/Game/RoomPlayer.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Ragon.Server; - -namespace Ragon.Core.Game; - -public class RoomPlayer -{ - public INetworkConnection Connection { get; } - public string Id { get; } - public string Name { get; } - public bool IsLoaded { get; private set; } - public Room Room { get; private set; } - public EntityList Entities { get; private set; } - - public RoomPlayer(INetworkConnection connection, string id, string name) - { - Id = id; - Name = name; - Connection = connection; - Entities = new EntityList(); - } - - public void Attach(Room room) - { - Room = room; - } - - public void Detach() - { - Room = null!; - } - - public void SetReady() - { - IsLoaded = true; - } -} \ No newline at end of file diff --git a/Ragon.Core/HandlerRegistry.cs b/Ragon.Core/HandlerRegistry.cs deleted file mode 100644 index b6acf25..0000000 --- a/Ragon.Core/HandlerRegistry.cs +++ /dev/null @@ -1,109 +0,0 @@ -using NLog; -using Ragon.Common; -using Ragon.Core.Handlers; - -namespace Ragon.Core; - -public sealed class HandlerRegistry -{ - private IHandler _entityEventHandler; - private IHandler _entityCreateHandler; - private IHandler _entityDestroyHandler; - private IHandler _entityStateHandler; - private IHandler _sceneLoadedHandler; - - private IHandler _authorizationHandler; - private IHandler _joinOrCreateHandler; - private IHandler _createHandler; - private IHandler _joinHandler; - private IHandler _leaveHandler; - - private Logger _logger = LogManager.GetCurrentClassLogger(); - private RagonSerializer _reader; - private RagonSerializer _writer; - - public HandlerRegistry() - { - _reader = new RagonSerializer(2048); - _writer = new RagonSerializer(2048); - - _authorizationHandler = new AuthorizationHandler(); - _joinOrCreateHandler = new JoinOrCreateHandler(); - _sceneLoadedHandler = new SceneLoadedHandler(); - _createHandler = new CreateHandler(); - _joinHandler = new JoinHandler(); - _leaveHandler = new LeaveHandler(); - - _entityEventHandler = new EntityEventHandler(); - _entityCreateHandler = new EntityCreateHandler(); - _entityDestroyHandler = new EntityDestroyHandler(); - _entityStateHandler = new EntityStateHandler(); - } - - public void Handle(PlayerContext context, byte[] data) - { - _writer.Clear(); - _reader.Clear(); - _reader.FromArray(data); - - var operation = _reader.ReadOperation(); - switch (operation) - { - case RagonOperation.REPLICATE_ENTITY_EVENT: - { - if (context.RoomPlayer != null) - _entityEventHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.REPLICATE_ENTITY_STATE: - { - if (context.RoomPlayer != null) - _entityStateHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.CREATE_ENTITY: - { - if (context.RoomPlayer != null) - _entityCreateHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.DESTROY_ENTITY: - { - if (context.RoomPlayer != null) - _entityDestroyHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.SCENE_LOADED: - { - if (context.RoomPlayer != null) - _sceneLoadedHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.JOIN_OR_CREATE_ROOM: - { - _joinOrCreateHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.CREATE_ROOM: - { - _createHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.JOIN_ROOM: - { - _joinHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.LEAVE_ROOM: - { - _leaveHandler.Handle(context, _reader, _writer); - break; - } - case RagonOperation.AUTHORIZE: - { - _authorizationHandler.Handle(context, _reader, _writer); - break; - } - } - } -} \ No newline at end of file diff --git a/Ragon.Core/Handlers/Abstract/IHandler.cs b/Ragon.Core/Handlers/Abstract/IHandler.cs deleted file mode 100644 index 1962506..0000000 --- a/Ragon.Core/Handlers/Abstract/IHandler.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Ragon.Common; - -namespace Ragon.Core; - -public interface IHandler -{ - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer); -} \ No newline at end of file diff --git a/Ragon.Core/Handlers/AuthorizationHandler.cs b/Ragon.Core/Handlers/AuthorizationHandler.cs deleted file mode 100644 index 05613e2..0000000 --- a/Ragon.Core/Handlers/AuthorizationHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using NLog; -using Ragon.Common; -using Ragon.Core.Lobby; - -namespace Ragon.Core.Handlers; - -public sealed class AuthorizationHandler: IHandler -{ - private Logger _logger = LogManager.GetCurrentClassLogger(); - - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) - { - if (context.LobbyPlayer.Status == LobbyPlayerStatus.Authorized) - { - _logger.Warn("Player already authorized"); - return; - } - - var key = reader.ReadString(); - var playerName = reader.ReadString(); - var additionalData = reader.ReadData(reader.Size); - - context.LobbyPlayer.Name = playerName; - context.LobbyPlayer.AdditionalData = additionalData.ToArray(); - context.LobbyPlayer.Status = LobbyPlayerStatus.Authorized; - - var playerId = context.LobbyPlayer.Id; - - writer.Clear(); - writer.WriteOperation(RagonOperation.AUTHORIZED_SUCCESS); - writer.WriteString(playerId); - writer.WriteString(playerName); - - var sendData = writer.ToArray(); - context.Connection.Reliable.Send(sendData); - - _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} authorized"); - } -} \ No newline at end of file diff --git a/Ragon.Core/Handlers/EntityCreateHandler.cs b/Ragon.Core/Handlers/EntityCreateHandler.cs deleted file mode 100644 index 9d8a192..0000000 --- a/Ragon.Core/Handlers/EntityCreateHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NLog; -using Ragon.Common; -using Ragon.Core.Game; - - -namespace Ragon.Core.Handlers; - -public sealed class EntityCreateHandler: IHandler -{ - private Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) - { - var entityType = reader.ReadUShort(); - var eventAuthority = (RagonAuthority) reader.ReadByte(); - var propertiesCount = reader.ReadUShort(); - - var entity = new Entity(context.RoomPlayer, entityType, 0, eventAuthority); - for (var i = 0; i < propertiesCount; i++) - { - var propertyType = reader.ReadBool(); - var propertySize = reader.ReadUShort(); - entity.State.AddProperty(new EntityStateProperty(propertySize, propertyType)); - } - - var entityPayload = reader.ReadData(reader.Size); - entity.SetPayload(entityPayload.ToArray()); - - context.Room.AttachEntity(context.RoomPlayer, entity); - - _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} created entity {entity.Id}:{entity.Type}"); - } -} \ No newline at end of file diff --git a/Ragon.Core/Handlers/EntityDestroyHandler.cs b/Ragon.Core/Handlers/EntityDestroyHandler.cs deleted file mode 100644 index 6a5df1a..0000000 --- a/Ragon.Core/Handlers/EntityDestroyHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using NLog; -using Ragon.Common; - -namespace Ragon.Core.Handlers; - -public sealed class EntityDestroyHandler: IHandler -{ - private Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) - { - var entityId = reader.ReadUShort(); - if (context.Room.Entities.TryGetValue(entityId, out var entity)) - { - var player = context.RoomPlayer; - var payload = reader.ReadData(reader.Size); - - context.Room.DetachEntity(player, entity, Array.Empty()); - _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} destoyed entity {entity.Id}"); - } - } -} \ No newline at end of file diff --git a/Ragon.Core/Handlers/EntityEventHandler.cs b/Ragon.Core/Handlers/EntityEventHandler.cs deleted file mode 100644 index be1b3b4..0000000 --- a/Ragon.Core/Handlers/EntityEventHandler.cs +++ /dev/null @@ -1,47 +0,0 @@ -using NLog; -using Ragon.Common; - -namespace Ragon.Core.Handlers; - -public sealed class EntityEventHandler: IHandler -{ - private Logger _logger = LogManager.GetCurrentClassLogger(); - - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) - { - var player = context.RoomPlayer; - var room = context.Room; - var entityId = reader.ReadUShort(); - - if (!room.Entities.TryGetValue(entityId, out var ent)) - { - _logger.Warn($"Entity not found for event with Id {entityId}"); - return; - } - - var eventId = reader.ReadUShort(); - var eventMode = (RagonReplicationMode) reader.ReadByte(); - var targetMode = (RagonTarget) reader.ReadByte(); - var payloadData = reader.ReadData(reader.Size); - var targetPlayerPeerId = reader.ReadUShort(); - - if (targetMode == RagonTarget.Player && context.Room.Players.TryGetValue(targetPlayerPeerId, out var targetPlayer)) - { - Span payloadRaw = stackalloc byte[payloadData.Length]; - ReadOnlySpan payload = payloadRaw; - payloadData.CopyTo(payloadRaw); - - _logger.Trace($"Event {eventId} Payload: {payloadData.Length} to {targetMode}"); - ent.ReplicateEvent(player, eventId, payload, eventMode, targetPlayer); - } - else - { - Span payloadRaw = stackalloc byte[payloadData.Length]; - ReadOnlySpan payload = payloadRaw; - payloadData.CopyTo(payloadRaw); - - _logger.Trace($"Event {eventId} Payload: {payloadData.Length} to {targetMode}"); - ent.ReplicateEvent(player, eventId, payload, eventMode, targetMode); - } - } -} \ No newline at end of file diff --git a/Ragon.Core/Handlers/EntityStateHandler.cs b/Ragon.Core/Handlers/EntityStateHandler.cs deleted file mode 100644 index ad16e17..0000000 --- a/Ragon.Core/Handlers/EntityStateHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using NLog; -using Ragon.Common; - -namespace Ragon.Core.Handlers; - -public sealed class EntityStateHandler: IHandler -{ - private ILogger _logger = LogManager.GetCurrentClassLogger(); - - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) - { - var room = context.Room; - var entitiesCount = reader.ReadUShort(); - for (var entityIndex = 0; entityIndex < entitiesCount; entityIndex++) - { - var entityId = reader.ReadUShort(); - - if (room.Entities.TryGetValue(entityId, out var entity)) - { - entity.State.Read(reader); - room.Track(entity); - } - else - { - _logger.Error($"Entity with Id {entityId} not found, replication interrupted"); - } - } - } -} \ No newline at end of file diff --git a/Ragon.Core/Handlers/RoomLeaveHandler.cs b/Ragon.Core/Handlers/RoomLeaveHandler.cs deleted file mode 100644 index a541fbb..0000000 --- a/Ragon.Core/Handlers/RoomLeaveHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using NLog; -using Ragon.Common; - -namespace Ragon.Core.Handlers; - -public sealed class LeaveHandler: IHandler -{ - private Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) - { - var room = context.Room; - var roomPlayer = context.RoomPlayer; - if (room != null) - { - context.Room?.RemovePlayer(roomPlayer); - _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} leaved from {room.Id}"); - } - } -} \ No newline at end of file diff --git a/Ragon.Core/Lobby/Abstract/ILobby.cs b/Ragon.Core/Lobby/Abstract/ILobby.cs deleted file mode 100644 index 0cb07b9..0000000 --- a/Ragon.Core/Lobby/Abstract/ILobby.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Ragon.Core.Game; - -namespace Ragon.Core.Lobby; - -public interface ILobby -{ - public bool FindRoomById(string roomId, [MaybeNullWhen(false)] out Room room); - public bool FindRoomByMap(string map, [MaybeNullWhen(false)] out Room room); - public void Persist(Room room); - public void RemoveIfEmpty(Room room); -} \ No newline at end of file diff --git a/Ragon.Core/Lobby/LobbyInMemory.cs b/Ragon.Core/Lobby/LobbyInMemory.cs deleted file mode 100644 index 1bdf521..0000000 --- a/Ragon.Core/Lobby/LobbyInMemory.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using NLog; -using Ragon.Core.Game; - -namespace Ragon.Core.Lobby; - -public class LobbyInMemory : ILobby -{ - private readonly List _rooms = new(); - private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - - public bool FindRoomById(string roomId, [MaybeNullWhen(false)] out Room room) - { - foreach (var existRoom in _rooms) - { - var info = existRoom.Info; - if (existRoom.Id == roomId && info.Min < info.Max) - { - room = existRoom; - return true; - } - } - - room = default; - return false; - } - - public bool FindRoomByMap(string map, [MaybeNullWhen(false)] out Room room) - { - foreach (var existRoom in _rooms) - { - var info = existRoom.Info; - if (info.Map == map && existRoom.Players.Count < info.Max) - { - room = existRoom; - return true; - } - } - - room = default; - return false; - } - - public void Persist(Room room) - { - _rooms.Add(room); - - foreach (var r in _rooms) - _logger.Trace($"Room: {r.Id} {r.Info} Players: {r.Players.Count}"); - } - - public void RemoveIfEmpty(Room room) - { - if (room.Players.Count == 0) - _rooms.Remove(room); - - foreach (var r in _rooms) - _logger.Trace($"Room: {r.Id} {r.Info} Players: {r.Players.Count}"); - } -} \ No newline at end of file diff --git a/Ragon.Core/Lobby/LobbyPlayer.cs b/Ragon.Core/Lobby/LobbyPlayer.cs deleted file mode 100644 index 8307f88..0000000 --- a/Ragon.Core/Lobby/LobbyPlayer.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Ragon.Server; - -namespace Ragon.Core.Lobby; - -public enum LobbyPlayerStatus -{ - Unauthorized, - Authorized, -} - -public class LobbyPlayer -{ - public string Id { get; private set; } - public string Name { get; set; } - public byte[] AdditionalData { get; set; } - public LobbyPlayerStatus Status { get; set; } - public INetworkConnection Connection { get; private set; } - - public LobbyPlayer(INetworkConnection connection) - { - Id = Guid.NewGuid().ToString(); - Connection = connection; - Status = LobbyPlayerStatus.Unauthorized; - Name = "None"; - AdditionalData = Array.Empty(); - } -} \ No newline at end of file diff --git a/Ragon.Core/PlayerContext.cs b/Ragon.Core/PlayerContext.cs deleted file mode 100644 index 14d0110..0000000 --- a/Ragon.Core/PlayerContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -using NLog; -using Ragon.Core.Game; -using Ragon.Core.Lobby; -using Ragon.Core.Time; -using Ragon.Server; - -namespace Ragon.Core; - -public class PlayerContext: IDisposable -{ - public INetworkConnection Connection { get; } - public Loop Loop; - public ILobby Lobby { get; set; } - public LobbyPlayer LobbyPlayer { private set; get; } - public Room? Room { get; set; } - public RoomPlayer? RoomPlayer { get; set; } - - public PlayerContext(INetworkConnection conn, LobbyPlayer player) - { - Connection = conn; - LobbyPlayer = player; - } - - public void Dispose() - { - RoomPlayer?.Room.RemovePlayer(RoomPlayer); - } -} \ No newline at end of file diff --git a/Ragon.Core/Ragon.Core.csproj b/Ragon.Core/Ragon.Core.csproj deleted file mode 100644 index 7583214..0000000 --- a/Ragon.Core/Ragon.Core.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - - - - - - - - - diff --git a/Ragon.Protocol/Ragon.Protocol.csproj b/Ragon.Protocol/Ragon.Protocol.csproj index 68be873..935da96 100644 --- a/Ragon.Protocol/Ragon.Protocol.csproj +++ b/Ragon.Protocol/Ragon.Protocol.csproj @@ -16,7 +16,8 @@ - + C:\Users\edmand46\RagonProjects\ragon-unity-sdk\Assets\Ragon-Unity-SDK\Runtime\Plugins TRACE;NETSTACK_SPAN + none diff --git a/Ragon.Protocol/Sources/RagonAuthority.cs b/Ragon.Protocol/Sources/RagonAuthority.cs index 9d13129..1f18721 100644 --- a/Ragon.Protocol/Sources/RagonAuthority.cs +++ b/Ragon.Protocol/Sources/RagonAuthority.cs @@ -1,4 +1,21 @@ -namespace Ragon.Common +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Protocol { public enum RagonAuthority: byte { diff --git a/Ragon.Protocol/Sources/RagonBuffer.cs b/Ragon.Protocol/Sources/RagonBuffer.cs new file mode 100644 index 0000000..24e04b8 --- /dev/null +++ b/Ragon.Protocol/Sources/RagonBuffer.cs @@ -0,0 +1,423 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright (c) 2018 Stanislav Denisov, Maxim Munnig + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * Copyright (c) 2018 Alexander Shoulson + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + + +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Ragon.Protocol +{ + public class RagonBuffer + { + private int _read; + private int _write; + private uint[] _buckets; + private readonly UTF8Encoding _utf8Encoding = new UTF8Encoding(false, true); + + public int ReadOffset => _read; + public int WriteOffset => _write; + public int Length => ((_write - 1) >> 3) + 1; + public int Capacity => _write - _read; + + public RagonBuffer(int capacity = 128) + { + _buckets = new uint[capacity]; + _read = 0; + _write = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBool(bool value) + { + Write(value ? 1u : 0u, 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ReadBool() + { + return Read(1) == 1u; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteByte(byte value) + { + Write(value, 8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() + { + return (byte)Read(8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteOperation(RagonOperation operation) + { + Write((byte)operation, 8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RagonOperation ReadOperation() + { + return (RagonOperation)Read(8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteFloat(float value, float min, float max, float precision) + { + var requiredBits = DeBruijn.Log2((uint)((max - min) * (1.0f / precision) + 0.5f)) + 1; + var mask = (uint)((1L << requiredBits) - 1); + var compressedValue = (uint)((value - min) * (1f / precision) + 0.5f) & mask; + + Write(compressedValue, requiredBits); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float ReadFloat(float min, float max, float precision) + { + var requiredBits = DeBruijn.Log2((uint)((max - min) * (1.0f / precision) + 0.5f)) + 1; + var compressedValue = Read(requiredBits); + + float adjusted = compressedValue * precision + min; + + if (adjusted < min) + adjusted = min; + else if (adjusted > max) + adjusted = max; + + return adjusted; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt(int value, int min, int max) + { + var maxValue = Math.Max(Math.Abs(min), Math.Abs(max)); + var requiredBits = Bits.Compute(maxValue); + uint compressedValue = (uint)((value << 1) ^ (value >> 31)); + + Write(compressedValue, requiredBits); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt(int min, int max) + { + var maxValue = Math.Max(Math.Abs(min), Math.Abs(max)); + var requiredBits = Bits.Compute(maxValue); + var compressedValue = Read(requiredBits); + var value = (int)((compressedValue >> 1) ^ (-(int)(compressedValue & 1))); + + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUShort(ushort value) + { + Write(value, 16); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUShort() + { + return (ushort)Read(16); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteString(string str) + { + var data = _utf8Encoding.GetBytes(str); + var len = (uint)data.Length; + Write(len, 16); + WriteBytes(data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadString() + { + var len = (int)Read(16); + var data = ReadBytes(len); + var str = _utf8Encoding.GetString(data); + return str; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Read(int numBits, int offset) + { + var currentBucketIndex = offset >> 5; + var used = offset & 0x0000001F; + + var chunkMask = ((1UL << numBits) - 1) << used; + var scratch = (ulong)_buckets[currentBucketIndex]; + + if (currentBucketIndex + 1 < _buckets.Length) + scratch |= (ulong)_buckets[currentBucketIndex + 1] << 32; + + var result = (scratch & chunkMask) >> used; + + return (uint)result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(uint value, int numBits, int offset) + { + Debug.Assert(!(numBits < 0)); + Debug.Assert(!(numBits > 32)); + + var index = offset >> 5; + var used = offset & 0x0000001F; + + var valueMask = (1UL << numBits) - 1; + var prepared = (value & valueMask) << used; + var scratch = _buckets[index] | (ulong)_buckets[index + 1] << 32; + var result = scratch | prepared; + + _buckets[index] = (uint)result; + _buckets[index + 1] = (uint)(result >> 32); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(uint value, int numBits = 16) + { + Debug.Assert(!(numBits < 0)); + Debug.Assert(!(numBits > 32)); + + var currentBucketIndex = _write >> 5; + var used = _write & 0x0000001F; + var mask = (1UL << used) - 1; + var scratch = _buckets[currentBucketIndex] & mask; + var result = scratch | ((ulong)value << used); + + if (currentBucketIndex + 1 >= _buckets.Length) + Resize(1); + + _buckets[currentBucketIndex] = (uint)result; + _buckets[currentBucketIndex + 1] = (uint)(result >> 32); + + _write += numBits; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Read(int numBits = 16) + { + var currentBucketIndex = _read >> 5; + var used = _read & 0x0000001F; + + var chunkMask = ((1UL << numBits) - 1) << used; + var scratch = (ulong)_buckets[currentBucketIndex]; + + if (currentBucketIndex + 1 < _buckets.Length) + scratch |= (ulong)_buckets[currentBucketIndex + 1] << 32; + + var result = (scratch & chunkMask) >> used; + + _read += numBits; + + return (uint)result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Do not use this method, will be removed")] + public void WriteBytes(byte[] data) + { + var len = data.Length; + for (int i = 0; i < len; i++) + Write(data[i], 8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Do not use this method, will be removed")] + public byte[] ReadBytes(int lenght) + { + var data = new byte[lenght]; + for (int i = 0; i < lenght; i++) + data[i] = (byte)Read(8); + + return data; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadSpan(ref Span data, int size) + { + var used = _read & 0x0000001F; + var index = _read >> 5; + var limit = (size + 32 - 1) / 32; + var capacity = size; + + for (int i = 0; i < limit; i++) + { + var dataSize = capacity > 32 ? 32 : capacity; + var mask = (1UL << dataSize) - 1; + var bucketRaw = (ulong)_buckets[index]; + if (index + 1 < _buckets.Length) + bucketRaw |= (ulong)_buckets[index + 1] << 32; + + var bucket = bucketRaw >> used; + var result = bucket & mask; + + data[i] = (uint)result; + if (i + 1 < data.Length) + data[i + 1] = (uint)(result >> 32); + + index += 1; + capacity -= dataSize; + } + + _read += size; + } + + public void WriteSpan(ref ReadOnlySpan data, int size) + { + var used = _write & 0x0000001F; + var index = _write >> 5; + var limit = (size + 32 - 1) / 32; + + if (index + limit >= _buckets.Length) + Resize(size); + + for (var i = 0; i < limit; i += 1) + { + var prepared = (ulong) data[i] << used; + var mask = (1UL << used) - 1; + var scratch = _buckets[index] & mask; + var result = scratch | prepared; + + _buckets[index] = (uint)result; + _buckets[index + 1] = (uint)(result >> 32); + + index += 1; + } + + _write += size; + } + + public void Clear() + { + _read = 0; + _write = 0; + } + + public void FromArray(byte[] data) + { + var length = data.Length; + var bucketsCount = length / 4 + 1; + + if (_buckets.Length < bucketsCount) + _buckets = new uint[bucketsCount]; + + for (var i = 0; i < bucketsCount; i++) + { + var dataIdx = i * 4; + var bucket = 0u; + + if (dataIdx < length) + bucket = data[dataIdx]; + + if (dataIdx + 1 < length) + bucket |= (uint)data[dataIdx + 1] << 8; + + if (dataIdx + 2 < length) + bucket |= (uint)data[dataIdx + 2] << 16; + + if (dataIdx + 3 < length) + bucket |= (uint)data[dataIdx + 3] << 24; + + _buckets[i] = bucket; + } + + int positionInByte = Bits.FindBitPosition(data[length - 1]); + + _write = ((length - 1) * 8) + positionInByte; + _read = 0; + } + + public byte[] ToArray() + { + var data = new byte[Length]; + int bucketsCount = (_write >> 5) + 1; + int length = data.Length; + + for (int i = 0; i < bucketsCount; i++) + { + int dataIdx = i * 4; + uint bucket = _buckets[i]; + + if (dataIdx < length) + data[dataIdx] = (byte)(bucket); + + if (dataIdx + 1 < length) + data[dataIdx + 1] = (byte)(bucket >> 8); + + if (dataIdx + 2 < length) + data[dataIdx + 2] = (byte)(bucket >> 16); + + if (dataIdx + 3 < length) + data[dataIdx + 3] = (byte)(bucket >> 24); + } + + return data; + } + + private void Resize(int capacity) + { + var buckets = new uint[_buckets.Length * 2 + capacity]; + Array.Copy(_buckets, buckets, _buckets.Length); + _buckets = buckets; + } + } +} \ No newline at end of file diff --git a/Ragon.Protocol/Sources/RagonOperation.cs b/Ragon.Protocol/Sources/RagonOperation.cs index 5273e44..d4f110f 100644 --- a/Ragon.Protocol/Sources/RagonOperation.cs +++ b/Ragon.Protocol/Sources/RagonOperation.cs @@ -1,4 +1,21 @@ -namespace Ragon.Common +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Protocol { public enum RagonOperation: byte { diff --git a/Ragon.Protocol/Sources/RagonReplicationMode.cs b/Ragon.Protocol/Sources/RagonReplicationMode.cs index fe1ba72..9c1a40d 100644 --- a/Ragon.Protocol/Sources/RagonReplicationMode.cs +++ b/Ragon.Protocol/Sources/RagonReplicationMode.cs @@ -1,4 +1,21 @@ -namespace Ragon.Common +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Protocol { public enum RagonReplicationMode: byte { diff --git a/Ragon.Protocol/Sources/RagonRoomParameters.cs b/Ragon.Protocol/Sources/RagonRoomParameters.cs index ecd2263..f219809 100644 --- a/Ragon.Protocol/Sources/RagonRoomParameters.cs +++ b/Ragon.Protocol/Sources/RagonRoomParameters.cs @@ -1,5 +1,21 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -namespace Ragon.Common + +namespace Ragon.Protocol { public class RagonRoomParameters: IRagonSerializable { @@ -7,18 +23,18 @@ namespace Ragon.Common public int Min { get; set; } public int Max { get; set; } - public void Serialize(RagonSerializer buffer) + public void Serialize(RagonBuffer buffer) { buffer.WriteString(Map); - buffer.WriteInt(Min); - buffer.WriteInt(Max); + buffer.WriteInt(Min, 1, 32); + buffer.WriteInt(Max, 1, 32); } - public void Deserialize(RagonSerializer buffer) + public void Deserialize(RagonBuffer buffer) { Map = buffer.ReadString(); - Min = buffer.ReadInt(); - Max = buffer.ReadInt(); + Min = buffer.ReadInt(1, 32); + Max = buffer.ReadInt(1, 32); } } } \ No newline at end of file diff --git a/Ragon.Protocol/Sources/RagonSerializable.cs b/Ragon.Protocol/Sources/RagonSerializable.cs index 24cf283..10dfe10 100644 --- a/Ragon.Protocol/Sources/RagonSerializable.cs +++ b/Ragon.Protocol/Sources/RagonSerializable.cs @@ -1,8 +1,25 @@ -namespace Ragon.Common +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Protocol { public interface IRagonSerializable { - public void Serialize(RagonSerializer serializer); - public void Deserialize(RagonSerializer serializer); + public void Serialize(RagonBuffer buffer); + public void Deserialize(RagonBuffer buffer); } } \ No newline at end of file diff --git a/Ragon.Protocol/Sources/RagonSerializer.cs b/Ragon.Protocol/Sources/RagonSerializer.cs deleted file mode 100644 index b447d8f..0000000 --- a/Ragon.Protocol/Sources/RagonSerializer.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; - - -namespace Ragon.Common -{ - [StructLayout(LayoutKind.Explicit)] - internal struct ValueConverter - { - [FieldOffset(0)] public int Int; - [FieldOffset(0)] public float Float; - [FieldOffset(0)] public long Long; - [FieldOffset(0)] public byte Byte0; - [FieldOffset(1)] public byte Byte1; - [FieldOffset(2)] public byte Byte2; - [FieldOffset(3)] public byte Byte3; - [FieldOffset(4)] public byte Byte4; - [FieldOffset(5)] public byte Byte5; - [FieldOffset(6)] public byte Byte6; - [FieldOffset(7)] public byte Byte7; - } - - public class RagonSerializer - { - private byte[] _data; - private int _offset; - private int _size; - - public int Lenght => _offset; - public int Size => _size - _offset; - - public RagonSerializer(int capacity = 256) - { - _data = new byte[capacity]; - _offset = 0; - _size = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Reset() - { - _size = _offset; - _offset = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddOffset(int offset) - { - _offset += offset; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteByte(byte value) - { - ResizeIfNeed(1); - _data[_offset] = value; - _offset += 1; - return 1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte ReadByte() - { - var value = _data[_offset]; - _offset += 1; - return value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteBool(bool value) - { - ResizeIfNeed(1); - _data[_offset] = value ? (byte) 1 : (byte) 0; - _offset += 1; - return 1; - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ReadBool() - { - var value = _data[_offset]; - _offset += 1; - return value == 1; - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteInt(int value) - { - ResizeIfNeed(4); - var converter = new ValueConverter() {Int = value}; - _data[_offset] = converter.Byte0; - _data[_offset + 1] = converter.Byte1; - _data[_offset + 2] = converter.Byte2; - _data[_offset + 3] = converter.Byte3; - _offset += 4; - return 4; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteInt(int value, int offset) - { - ResizeIfNeed(4); - var converter = new ValueConverter() {Int = value}; - _data[offset] = converter.Byte0; - _data[offset + 1] = converter.Byte1; - _data[offset + 2] = converter.Byte2; - _data[offset + 3] = converter.Byte3; - return 4; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int ReadInt() - { - var converter = new ValueConverter {Byte0 = _data[_offset], Byte1 = _data[_offset + 1], Byte2 = _data[_offset + 2], Byte3 = _data[_offset + 3]}; - _offset += 4; - return converter.Int; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteLong(long value) - { - ResizeIfNeed(8); - WriteLong(value, _offset); - _offset += 8; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteLong(long value, int offset) - { - var converter = new ValueConverter() {Long = value}; - _data[offset] = converter.Byte0; - _data[offset + 1] = converter.Byte1; - _data[offset + 2] = converter.Byte2; - _data[offset + 3] = converter.Byte3; - _data[offset + 4] = converter.Byte4; - _data[offset + 5] = converter.Byte5; - _data[offset + 6] = converter.Byte6; - _data[offset + 7] = converter.Byte7; - return 8; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long ReadLong() - { - var converter = new ValueConverter - { - Byte0 = _data[_offset], - Byte1 = _data[_offset + 1], - Byte2 = _data[_offset + 2], - Byte3 = _data[_offset + 3], - Byte4 = _data[_offset + 4], - Byte5 = _data[_offset + 5], - Byte6 = _data[_offset + 6], - Byte7 = _data[_offset + 7], - }; - _offset += 8; - return converter.Long; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteFloat(float value) - { - var converter = new ValueConverter() {Float = value}; - WriteInt(converter.Int); - return 4; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float ReadFloat() - { - var rawValue = ReadInt(); - var converter = new ValueConverter() {Int = rawValue}; - var value = converter.Float; - return value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteString(string value) - { - var rawData = Encoding.UTF8.GetBytes(value).AsSpan(); - ResizeIfNeed(2 + rawData.Length); - WriteUShort((ushort) rawData.Length); - var data = _data.AsSpan().Slice(_offset, rawData.Length); - rawData.CopyTo(data); - _offset += rawData.Length; - - return rawData.Length + 2; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadString() - { - var lenght = ReadUShort(); - var stringRaw = _data.AsSpan().Slice(_offset, lenght); - var str = Encoding.UTF8.GetString(stringRaw); - _offset += lenght; - return str; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan ReadData(int lenght) - { - var data = _data.AsSpan(); - var payloadData = data.Slice(_offset, lenght); - - _offset += payloadData.Length; - return payloadData; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteData(ref ReadOnlySpan payload) - { - ResizeIfNeed(payload.Length); - - var data = _data.AsSpan(); - var payloadData = data.Slice(_offset, payload.Length); - - payload.CopyTo(payloadData); - _offset += payload.Length; - return payload.Length; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetWritableData(int lenght) - { - ResizeIfNeed(lenght); - - var data = _data.AsSpan(); - var payloadData = data.Slice(_offset, lenght); - - _offset += lenght; - return payloadData; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteOperation(RagonOperation ragonOperation) - { - ResizeIfNeed(1); - - _data[_offset] = (byte) ragonOperation; - _offset += 1; - - return 1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public RagonOperation ReadOperation() - { - var op = (RagonOperation) _data[_offset]; - _offset += 1; - return op; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteUShort(ushort value) - { - ResizeIfNeed(2); - - _data[_offset] = (byte) (value & 0x00FF); - _data[_offset + 1] = (byte) ((value & 0xFF00) >> 8); - _offset += 2; - - return 2; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int WriteUShort(ushort value, int offset) - { - ResizeIfNeed(2); - _data[offset] = (byte) (value & 0x00FF); - _data[offset + 1] = (byte) ((value & 0xFF00) >> 8); - return 2; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ushort ReadUShort() - { - var value = (ushort) (_data[_offset] + (_data[_offset + 1] << 8)); - _offset += 2; - return value; - } - - public void Clear() - { - _offset = 0; - _size = 0; - } - - public void ToSpan(ref Span data) - { - var span = _data.AsSpan(); - var dataSpan = span.Slice(0, _offset); - dataSpan.CopyTo(data); - } - - public void FromSpan(ref ReadOnlySpan data) - { - Clear(); - ResizeIfNeed(data.Length); - var dataSpan = _data.AsSpan(); - data.CopyTo(dataSpan); - _size = data.Length; - } - - public void FromArray(byte[] data) - { - Clear(); - ResizeIfNeed(data.Length); - Buffer.BlockCopy(data, 0, _data, 0, data.Length); - _size = data.Length; - } - - - public byte[] ToArray() - { - var bytes = new byte[_offset]; - Buffer.BlockCopy(_data, 0, bytes, 0, _offset); - return bytes; - } - - private void ResizeIfNeed(int lenght) - { - if (_offset + lenght < _data.Length) - return; - - var newData = new byte[_data.Length * 4 + lenght]; - Buffer.BlockCopy(_data, 0, newData, 0, _data.Length); - _data = newData; - } - } -} \ No newline at end of file diff --git a/Ragon.Protocol/Sources/RagonTarget.cs b/Ragon.Protocol/Sources/RagonTarget.cs index 25f6678..93898ee 100644 --- a/Ragon.Protocol/Sources/RagonTarget.cs +++ b/Ragon.Protocol/Sources/RagonTarget.cs @@ -1,4 +1,21 @@ -namespace Ragon.Common +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Protocol { public enum RagonTarget: byte { diff --git a/Ragon.Protocol/Sources/RagonUtils.cs b/Ragon.Protocol/Sources/RagonUtils.cs new file mode 100644 index 0000000..acc995a --- /dev/null +++ b/Ragon.Protocol/Sources/RagonUtils.cs @@ -0,0 +1,80 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using System.Runtime.CompilerServices; + +namespace Ragon.Protocol +{ + public static class DeBruijn + { + private static readonly int[] _lookup = new int[32] + { + 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, + 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31 + }; + + public static int Log2(uint value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + + return _lookup[(value * 0x07C4ACDDU) >> 27]; + } + } + + public static class Bits + { + static int[] _lookup = new int[256]; + + static Bits() + { + _lookup[0] = 0; + for (int i = 0; i < 256; i++) + _lookup[i] = (i & 1) + _lookup[i / 2]; + } + + [MethodImpl(256)] + public static int Compute(int value) + { + var count = 0; + do + { + value >>= 8; + count += 8; + } while (value > 0); + + return count; + } + + [MethodImpl(256)] + public static int FindBitPosition(byte data) + { + int shiftCount = 0; + + while (data > 0) + { + data >>= 1; + shiftCount++; + } + + return shiftCount; + } + } +} \ No newline at end of file diff --git a/Ragon.Protocol/Sources/RagonVersion.cs b/Ragon.Protocol/Sources/RagonVersion.cs index 563294e..f5e4bfb 100644 --- a/Ragon.Protocol/Sources/RagonVersion.cs +++ b/Ragon.Protocol/Sources/RagonVersion.cs @@ -1,4 +1,21 @@ -namespace Ragon.Common +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Ragon.Protocol { public static class RagonVersion { diff --git a/Ragon.Relay/Dockerfile b/Ragon.Relay/Dockerfile new file mode 100644 index 0000000..4df0c77 --- /dev/null +++ b/Ragon.Relay/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env + +WORKDIR /App + +COPY . ./ + +RUN dotnet restore +RUN dotnet publish -c Release -o out + +FROM mcr.microsoft.com/dotnet/runtime:7.0 + +WORKDIR /App + +COPY --from=build-env /App/out . + +ENTRYPOINT ["Ragon.Relay"] \ No newline at end of file diff --git a/Ragon.Relay/NLog.config b/Ragon.Relay/NLog.config old mode 100755 new mode 100644 diff --git a/Ragon.Relay/Program.cs b/Ragon.Relay/Program.cs old mode 100755 new mode 100644 index 38c5874..39a26be --- a/Ragon.Relay/Program.cs +++ b/Ragon.Relay/Program.cs @@ -1,7 +1,18 @@ -using System; -using System.Runtime.InteropServices; -using NLog; -using Ragon.Core; +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ namespace Ragon.Relay { @@ -9,20 +20,8 @@ namespace Ragon.Relay { static void Main(string[] args) { - var logger = LogManager.GetLogger("Ragon.Relay"); - - logger.Info("Relay Application"); - - var configuration = Configuration.Load("relay.config.json"); - var relay = new Application(configuration); - - logger.Info("Started"); + var relay = new Relay(); relay.Start(); - - Console.ReadKey(); - - relay.Stop(); - logger.Info("Stopped"); } } } \ No newline at end of file diff --git a/Ragon.Relay/Ragon.Relay.csproj b/Ragon.Relay/Ragon.Relay.csproj old mode 100755 new mode 100644 index 851f760..b492477 --- a/Ragon.Relay/Ragon.Relay.csproj +++ b/Ragon.Relay/Ragon.Relay.csproj @@ -2,8 +2,8 @@ Exe - net6.0 - Game + Ragon.Relay + net7.0 @@ -17,10 +17,15 @@ Always + + Always + - + + + diff --git a/Ragon.Relay/Relay.cs b/Ragon.Relay/Relay.cs new file mode 100644 index 0000000..0192094 --- /dev/null +++ b/Ragon.Relay/Relay.cs @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Server; +using Ragon.Server.ENet; +using Ragon.Server.DotNetWebsockets; + + +namespace Ragon.Relay; + +public class Relay +{ + public void Start() + { + var logger = LogManager.GetLogger("Ragon.Relay"); + logger.Info("Relay Application"); + + var configuration = Configuration.Load("relay.config.json"); + var serverType = Configuration.GetServerType(configuration.ServerType); + + INetworkServer server = null; + switch (serverType) + { + case ServerType.ENET: + server = new ENetServer(); + break; + case ServerType.WEBSOCKET: + server = new DotNetWebSocketServer(); + break; + default: + server = new ENetServer(); + break; + } + + var relay = new RagonServer(server, configuration); + logger.Info("Started"); + relay.Start(); + } +} \ No newline at end of file diff --git a/Ragon.Relay/relay.config.json b/Ragon.Relay/relay.config.json index 434df7d..715fa3c 100644 --- a/Ragon.Relay/relay.config.json +++ b/Ragon.Relay/relay.config.json @@ -1,9 +1,9 @@ { "serverKey": "defaultkey", "serverType": "enet", - "serverTickRate": 20, + "serverTickRate": 30, "gameProtocol": "1.0.0", - "port": 5000, + "port": 5001, "limitConnections": 4095, "limitPlayersPerRoom": 20, "limitRooms": 200 diff --git a/Ragon.Server.NativeWebSockets/Ragon.Server.NativeWebSockets.csproj b/Ragon.Server.DotNetWebSockets/Ragon.Server.DotNetWebSockets.csproj similarity index 87% rename from Ragon.Server.NativeWebSockets/Ragon.Server.NativeWebSockets.csproj rename to Ragon.Server.DotNetWebSockets/Ragon.Server.DotNetWebSockets.csproj index 4680532..5feec38 100644 --- a/Ragon.Server.NativeWebSockets/Ragon.Server.NativeWebSockets.csproj +++ b/Ragon.Server.DotNetWebSockets/Ragon.Server.DotNetWebSockets.csproj @@ -8,11 +8,11 @@ - + - + diff --git a/Ragon.Server.NativeWebSockets/WebSocketConnection.cs b/Ragon.Server.DotNetWebSockets/WebSocketConnection.cs similarity index 61% rename from Ragon.Server.NativeWebSockets/WebSocketConnection.cs rename to Ragon.Server.DotNetWebSockets/WebSocketConnection.cs index c644447..ca8a748 100644 --- a/Ragon.Server.NativeWebSockets/WebSocketConnection.cs +++ b/Ragon.Server.DotNetWebSockets/WebSocketConnection.cs @@ -1,7 +1,23 @@ -using System.Net.WebSockets; +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.WebSockets; using NLog; -namespace Ragon.Server.NativeWebSockets; +namespace Ragon.Server.DotNetWebsockets; public sealed class WebSocketConnection : INetworkConnection { diff --git a/Ragon.Server.DotNetWebSockets/WebSocketReliableChannel.cs b/Ragon.Server.DotNetWebSockets/WebSocketReliableChannel.cs new file mode 100644 index 0000000..79454df --- /dev/null +++ b/Ragon.Server.DotNetWebSockets/WebSocketReliableChannel.cs @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.WebSockets; +using Ragon.Server; + +namespace Ragon.Server.DotNetWebsockets; + +public class WebSocketReliableChannel : INetworkChannel +{ + private Queue _queue; + private WebSocket _socket; + + public WebSocketReliableChannel(WebSocket webSocket) + { + _socket = webSocket; + _queue = new Queue(512); + } + + public void Send(byte[] data) + { + _queue.Enqueue(data); + } + + public async Task Flush() + { + while (_queue.TryDequeue(out var sendData) && _socket.State == WebSocketState.Open) + await _socket.SendAsync(sendData, WebSocketMessageType.Binary, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); + } +} \ No newline at end of file diff --git a/Ragon.Server.NativeWebSockets/WebSocketServer.cs b/Ragon.Server.DotNetWebSockets/WebSocketServer.cs similarity index 68% rename from Ragon.Server.NativeWebSockets/WebSocketServer.cs rename to Ragon.Server.DotNetWebSockets/WebSocketServer.cs index b6cb544..8e98712 100644 --- a/Ragon.Server.NativeWebSockets/WebSocketServer.cs +++ b/Ragon.Server.DotNetWebSockets/WebSocketServer.cs @@ -1,18 +1,30 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using System.Net; using System.Net.WebSockets; using NLog; -using Ragon.Common; -using Ragon.Core.Server; -using Ragon.Server; -using Ragon.Server.NativeWebSockets; +using Ragon.Protocol; -namespace Ragon.Core; +namespace Ragon.Server.DotNetWebsockets; -public class NativeWebSocketServer : INetworkServer +public class DotNetWebSocketServer : INetworkServer { + public Executor Executor => _executor; + private ILogger _logger = LogManager.GetCurrentClassLogger(); private INetworkListener _networkListener; private Stack _sequencer; @@ -21,21 +33,27 @@ public class NativeWebSocketServer : INetworkServer private WebSocketConnection[] _connections; private List _activeConnections; private CancellationTokenSource _cancellationTokenSource; - - public NativeWebSocketServer(Executor executor) + + public DotNetWebSocketServer() { _sequencer = new Stack(); _connections = Array.Empty(); _activeConnections = new List(); - _executor = executor; + _executor = new Executor(); } - + public async void StartAccept(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { var context = await _httpListener.GetContextAsync(); - if (!context.Request.IsWebSocketRequest) continue; + if (!context.Request.IsWebSocketRequest) + { + context.Response.StatusCode = 200; + context.Response.ContentLength64 = 0; + context.Response.Close(); + continue; + } var webSocketContext = await context.AcceptWebSocketAsync(null); var webSocket = webSocketContext.WebSocket; @@ -64,8 +82,8 @@ public class NativeWebSocketServer : INetworkServer { var result = await webSocket.ReceiveAsync(buffer, cancellationToken); var dataRaw = buffer.Slice(0, result.Count); - - _networkListener.OnData(connection, dataRaw.ToArray()); + if (dataRaw.Length > 0) + _networkListener.OnData(connection, dataRaw.ToArray()); } catch (Exception ex) { @@ -78,7 +96,7 @@ public class NativeWebSocketServer : INetworkServer _networkListener.OnDisconnected(connection); } - public void Poll() + public void Update() { Flush(); } @@ -106,13 +124,13 @@ public class NativeWebSocketServer : INetworkServer _connections = new WebSocketConnection[configuration.LimitConnections]; _httpListener = new HttpListener(); - _httpListener.Prefixes.Add($"http://127.0.0.1:{configuration.Port}/"); + _httpListener.Prefixes.Add($"http://+:{configuration.Port}/"); _httpListener.Start(); _executor.Run(() => StartAccept(_cancellationTokenSource.Token)); var protocolDecoded = RagonVersion.Parse(configuration.Protocol); - _logger.Info($"Listen at http://*:{configuration.Port}/"); + _logger.Info($"Listen at http://0.0.0.0:{configuration.Port}/"); _logger.Info($"Protocol: {protocolDecoded}"); } diff --git a/Ragon.Server.ENet/ENetConnection.cs b/Ragon.Server.ENet/ENetConnection.cs deleted file mode 100644 index dde4943..0000000 --- a/Ragon.Server.ENet/ENetConnection.cs +++ /dev/null @@ -1,17 +0,0 @@ -using ENet; - -namespace Ragon.Server.ENet; - -public sealed class ENetConnection: INetworkConnection -{ - public ushort Id { get; } - public INetworkChannel Reliable { get; private set; } - public INetworkChannel Unreliable { get; private set; } - - public ENetConnection(Peer peer) - { - Id = (ushort) peer.ID; - Reliable = new ENetReliableChannel(peer, 0); - Unreliable = new ENetUnreliableChannel(peer, 1); - } -} \ No newline at end of file diff --git a/Ragon.Server.ENet/ENetReliableChannel.cs b/Ragon.Server.ENet/ENetReliableChannel.cs deleted file mode 100644 index 6cb6571..0000000 --- a/Ragon.Server.ENet/ENetReliableChannel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using ENet; - -namespace Ragon.Server.ENet; - -public sealed class ENetReliableChannel: INetworkChannel -{ - private Peer _peer; - private byte _channelId; - - public ENetReliableChannel(Peer peer, int channelId) - { - _peer = peer; - _channelId = (byte) channelId; - } - - public void Send(byte[] data) - { - var newPacket = new Packet(); - newPacket.Create(data, data.Length, PacketFlags.Reliable); - - _peer.Send(_channelId, ref newPacket); - } -} \ No newline at end of file diff --git a/Ragon.Server.ENet/ENetUnreliableChannel.cs b/Ragon.Server.ENet/ENetUnreliableChannel.cs deleted file mode 100644 index a147bee..0000000 --- a/Ragon.Server.ENet/ENetUnreliableChannel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using ENet; - -namespace Ragon.Server.ENet; - -public sealed class ENetUnreliableChannel: INetworkChannel -{ - private Peer _peer; - private byte _channelId; - - public ENetUnreliableChannel(Peer peer, int channelId) - { - _peer = peer; - _channelId = (byte) channelId; - } - - public void Send(byte[] data) - { - var newPacket = new Packet(); - newPacket.Create(data, data.Length, PacketFlags.None); - - _peer.Send(_channelId, ref newPacket); - } -} \ No newline at end of file diff --git a/Ragon.Server.ENet/Ragon.Server.ENet.csproj b/Ragon.Server.ENet/Ragon.Server.ENet.csproj index fdbb1e7..91c6df9 100644 --- a/Ragon.Server.ENet/Ragon.Server.ENet.csproj +++ b/Ragon.Server.ENet/Ragon.Server.ENet.csproj @@ -9,7 +9,7 @@ - + diff --git a/Ragon.Server.ENet/Sources/ENetConnection.cs b/Ragon.Server.ENet/Sources/ENetConnection.cs new file mode 100644 index 0000000..837ca78 --- /dev/null +++ b/Ragon.Server.ENet/Sources/ENetConnection.cs @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using ENet; + +namespace Ragon.Server.ENet; + +public sealed class ENetConnection: INetworkConnection +{ + public ushort Id { get; } + public INetworkChannel Reliable { get; private set; } + public INetworkChannel Unreliable { get; private set; } + + public ENetConnection(Peer peer) + { + Id = (ushort) peer.ID; + Reliable = new ENetReliableChannel(peer, 0); + Unreliable = new ENetUnreliableChannel(peer, 1); + } +} \ No newline at end of file diff --git a/Ragon.Server.ENet/Sources/ENetReliableChannel.cs b/Ragon.Server.ENet/Sources/ENetReliableChannel.cs new file mode 100644 index 0000000..822ba3a --- /dev/null +++ b/Ragon.Server.ENet/Sources/ENetReliableChannel.cs @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using ENet; + +namespace Ragon.Server.ENet; + +public sealed class ENetReliableChannel: INetworkChannel +{ + private Peer _peer; + private byte _channelId; + + public ENetReliableChannel(Peer peer, int channelId) + { + _peer = peer; + _channelId = (byte) channelId; + } + + public void Send(byte[] data) + { + var newPacket = new Packet(); + newPacket.Create(data, data.Length, PacketFlags.Reliable); + + _peer.Send(_channelId, ref newPacket); + } +} \ No newline at end of file diff --git a/Ragon.Server.ENet/ENetServer.cs b/Ragon.Server.ENet/Sources/ENetServer.cs old mode 100755 new mode 100644 similarity index 76% rename from Ragon.Server.ENet/ENetServer.cs rename to Ragon.Server.ENet/Sources/ENetServer.cs index c5d6763..853d934 --- a/Ragon.Server.ENet/ENetServer.cs +++ b/Ragon.Server.ENet/Sources/ENetServer.cs @@ -1,116 +1,138 @@ -using System.Diagnostics; -using ENet; -using NLog; -using Ragon.Common; - - -namespace Ragon.Server.ENet -{ - public sealed class ENetServer: INetworkServer - { - private readonly Host _host; - private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); - - private ENetConnection[] _connections; - private uint _protocol; - private INetworkListener _listener; - private Event _event; - - public ENetServer() - { - _host = new Host(); - _connections = Array.Empty(); - } - - public void Start(INetworkListener listener, NetworkConfiguration configuration) - { - Library.Initialize(); - - _connections = new ENetConnection[configuration.LimitConnections]; - - _listener = listener; - _protocol = configuration.Protocol; - - var address = new Address { Port = (ushort) configuration.Port }; - _host.Create(address, _connections.Length, 2, 0, 0, 1024 * 1024); - - var protocolDecoded = RagonVersion.Parse(_protocol); - _logger.Info($"Listen at 127.0.0.1:{configuration.Port}"); - _logger.Info($"Protocol: {protocolDecoded}"); - } - - public void Poll() - { - bool polled = false; - while (!polled) - { - if (_host.CheckEvents(out _event) <= 0) - { - if (_host.Service(0, out _event) <= 0) - break; - - polled = true; - } - - switch (_event.Type) - { - case EventType.None: - { - _logger.Trace("None event"); - break; - } - case EventType.Connect: - { - if (!IsValidProtocol(_event.Data)) - { - _logger.Warn($"Mismatched protocol Server: {RagonVersion.Parse(_protocol)} Client: {RagonVersion.Parse(_event.Data)}, close connection"); - _event.Peer.DisconnectNow(0); - break; - } - - var connection = new ENetConnection(_event.Peer); - _connections[_event.Peer.ID] = connection; - _listener.OnConnected(connection); - break; - } - case EventType.Disconnect: - { - var connection = _connections[_event.Peer.ID]; - _listener.OnDisconnected(connection); - break; - } - case EventType.Timeout: - { - var connection = _connections[_event.Peer.ID]; - _listener.OnTimeout(connection); - break; - } - case EventType.Receive: - { - var peerId = (ushort) _event.Peer.ID; - var connection = _connections[peerId]; - var dataRaw = new byte[_event.Packet.Length]; - - _event.Packet.CopyTo(dataRaw); - _event.Packet.Dispose(); - - _listener.OnData(connection, dataRaw); - break; - } - } - } - } - - public void Stop() - { - _host?.Dispose(); - - Library.Deinitialize(); - } - - private bool IsValidProtocol(uint protocol) - { - return protocol == _protocol; - } - } +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using ENet; +using NLog; +using Ragon.Protocol; + +namespace Ragon.Server.ENet +{ + public sealed class ENetServer: INetworkServer + { + public Executor Executor => _executor; + + private readonly Host _host; + private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); + + private ENetConnection[] _connections; + private INetworkListener _listener; + private uint _protocol; + private Event _event; + private Executor _executor; + + public ENetServer() + { + _host = new Host(); + _executor = new Executor(); + _connections = Array.Empty(); + } + + public void Start(INetworkListener listener, NetworkConfiguration configuration) + { + Library.Initialize(); + + _connections = new ENetConnection[configuration.LimitConnections]; + + _listener = listener; + _protocol = configuration.Protocol; + + var address = new Address + { + Port = (ushort) configuration.Port, + }; + + _host.Create(address, _connections.Length, 2, 0, 0, 1024 * 1024); + + var protocolDecoded = RagonVersion.Parse(_protocol); + _logger.Info($"Listen at 127.0.0.1:{configuration.Port}"); + _logger.Info($"Protocol: {protocolDecoded}"); + } + + public void Update() + { + bool polled = false; + while (!polled) + { + if (_host.CheckEvents(out _event) <= 0) + { + if (_host.Service(0, out _event) <= 0) + break; + + polled = true; + } + + switch (_event.Type) + { + case EventType.None: + { + _logger.Trace("None event"); + break; + } + case EventType.Connect: + { + if (!IsValidProtocol(_event.Data)) + { + _logger.Warn($"Mismatched protocol Server: {RagonVersion.Parse(_protocol)} Client: {RagonVersion.Parse(_event.Data)}, close connection"); + _event.Peer.DisconnectNow(0); + break; + } + var connection = new ENetConnection(_event.Peer); + + _connections[_event.Peer.ID] = connection; + _listener.OnConnected(connection); + break; + } + case EventType.Disconnect: + { + var connection = _connections[_event.Peer.ID]; + _listener.OnDisconnected(connection); + break; + } + case EventType.Timeout: + { + var connection = _connections[_event.Peer.ID]; + _listener.OnTimeout(connection); + break; + } + case EventType.Receive: + { + var peerId = (ushort) _event.Peer.ID; + var connection = _connections[peerId]; + var dataRaw = new byte[_event.Packet.Length]; + + _event.Packet.CopyTo(dataRaw); + _event.Packet.Dispose(); + + _listener.OnData(connection, dataRaw); + break; + } + } + } + } + + public void Stop() + { + _host?.Dispose(); + + Library.Deinitialize(); + } + + private bool IsValidProtocol(uint protocol) + { + return protocol == _protocol; + } + } } \ No newline at end of file diff --git a/Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs b/Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs new file mode 100644 index 0000000..995fe7c --- /dev/null +++ b/Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using ENet; + +namespace Ragon.Server.ENet; + +public sealed class ENetUnreliableChannel: INetworkChannel +{ + private Peer _peer; + private byte _channelId; + + public ENetUnreliableChannel(Peer peer, int channelId) + { + _peer = peer; + _channelId = (byte) channelId; + } + + public void Send(byte[] data) + { + var newPacket = new Packet(); + newPacket.Create(data, data.Length, PacketFlags.None); + + _peer.Send(_channelId, ref newPacket); + } +} \ No newline at end of file diff --git a/Ragon.Server.NativeWebSockets/WebSocketReliableChannel.cs b/Ragon.Server.NativeWebSockets/WebSocketReliableChannel.cs deleted file mode 100644 index a456eea..0000000 --- a/Ragon.Server.NativeWebSockets/WebSocketReliableChannel.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Net.WebSockets; -using Ragon.Server; - -namespace Ragon.Server.NativeWebSockets; - -public class WebSocketReliableChannel : INetworkChannel -{ - private Queue _queue; - private WebSocket _socket; - - public WebSocketReliableChannel(WebSocket webSocket) - { - _socket = webSocket; - _queue = new Queue(512); - } - - public void Send(byte[] data) - { - _queue.Enqueue(data); - } - - public async Task Flush() - { - while (_queue.TryDequeue(out var sendData) && _socket.State == WebSocketState.Open) - await _socket.SendAsync(sendData, WebSocketMessageType.Binary, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); - } -} \ No newline at end of file diff --git a/Ragon.Server/INetworkChannel.cs b/Ragon.Server/INetworkChannel.cs deleted file mode 100644 index 803ce66..0000000 --- a/Ragon.Server/INetworkChannel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ragon.Server; - -public interface INetworkChannel -{ - void Send(byte[] data); -} \ No newline at end of file diff --git a/Ragon.Server/INetworkConnection.cs b/Ragon.Server/INetworkConnection.cs deleted file mode 100644 index 5a32411..0000000 --- a/Ragon.Server/INetworkConnection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ragon.Server; - -public interface INetworkConnection -{ - public ushort Id { get; } - public INetworkChannel Reliable { get; } - public INetworkChannel Unreliable { get; } -} \ No newline at end of file diff --git a/Ragon.Server/INetworkListener.cs b/Ragon.Server/INetworkListener.cs deleted file mode 100644 index f3102f8..0000000 --- a/Ragon.Server/INetworkListener.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ragon.Server; - -public interface INetworkListener -{ - void OnConnected(INetworkConnection connection); - void OnDisconnected(INetworkConnection connection); - void OnTimeout(INetworkConnection connection); - void OnData(INetworkConnection connection, byte[] data); -} \ No newline at end of file diff --git a/Ragon.Server/INetworkServer.cs b/Ragon.Server/INetworkServer.cs deleted file mode 100644 index ca6a061..0000000 --- a/Ragon.Server/INetworkServer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ragon.Server; - -public interface INetworkServer -{ - public void Stop(); - public void Poll(); - public void Start(INetworkListener listener, NetworkConfiguration configuration); -} \ No newline at end of file diff --git a/Ragon.Server/NetworkConfiguration.cs b/Ragon.Server/NetworkConfiguration.cs deleted file mode 100644 index 15adb9c..0000000 --- a/Ragon.Server/NetworkConfiguration.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ragon.Server; - -public struct NetworkConfiguration -{ - public int LimitConnections { get; set; } - public int Port { get; set; } - public uint Protocol { get; set; } - public string Address { get; set; } -} \ No newline at end of file diff --git a/Ragon.Server/Ragon.Server.csproj b/Ragon.Server/Ragon.Server.csproj index 95c97c8..fb83ed3 100644 --- a/Ragon.Server/Ragon.Server.csproj +++ b/Ragon.Server/Ragon.Server.csproj @@ -4,8 +4,14 @@ net6.0 enable enable + Ragon.Core + + + + + diff --git a/Ragon.Server/Sources/Entity/RagonEntity.cs b/Ragon.Server/Sources/Entity/RagonEntity.cs new file mode 100644 index 0000000..c55bacf --- /dev/null +++ b/Ragon.Server/Sources/Entity/RagonEntity.cs @@ -0,0 +1,212 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Server; + +public class RagonEntity +{ + private static ushort _idGenerator = 100; + public ushort Id { get; private set; } + public ushort Type { get; private set; } + public ushort StaticId { get; private set; } + public ushort AttachId { get; private set; } + public RagonRoomPlayer Owner { get; private set; } + public RagonAuthority Authority { get; private set; } + public RagonPayload Payload { get; private set; } + public RagonEntityState State { get; private set; } + + private readonly List _bufferedEvents; + + public RagonEntity(RagonRoomPlayer owner, ushort type, ushort staticId, ushort attachId, RagonAuthority eventAuthority) + { + Owner = owner; + StaticId = staticId; + Type = type; + AttachId = attachId; + Id = _idGenerator++; + Authority = eventAuthority; + State = new RagonEntityState(this); + Payload = new RagonPayload(); + + _bufferedEvents = new List(); + } + + + public void SetOwner(RagonRoomPlayer owner) + { + Owner = owner; + } + + public void RestoreBufferedEvents(RagonRoomPlayer roomPlayer, RagonBuffer writer) + { + foreach (var evnt in _bufferedEvents) + { + writer.Clear(); + writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); + writer.WriteUShort(evnt.EventCode); + writer.WriteUShort(evnt.Invoker.Connection.Id); + writer.WriteByte((byte)RagonReplicationMode.Server); + writer.WriteUShort(Id); + + evnt.Write(writer); + + var sendData = writer.ToArray(); + roomPlayer.Connection.Reliable.Send(sendData); + } + } + + public void Create() + { + var room = Owner.Room; + var buffer = room.Writer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.CREATE_ENTITY); + buffer.WriteUShort(AttachId); + buffer.WriteUShort(Type); + buffer.WriteUShort(Id); + buffer.WriteUShort(Owner.Connection.Id); + + Payload.Write(buffer); + + var sendData = buffer.ToArray(); + foreach (var player in room.ReadyPlayersList) + player.Connection.Reliable.Send(sendData); + } + + public void Destroy() + { + var room = Owner.Room; + var buffer = room.Writer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.DESTROY_ENTITY); + buffer.WriteUShort(Id); + + Payload.Write(buffer); + + var sendData = buffer.ToArray(); + foreach (var player in room.ReadyPlayersList) + player.Connection.Reliable.Send(sendData); + } + + public void Snapshot(RagonBuffer buffer) + { + buffer.WriteUShort(Type); + buffer.WriteUShort(Id); + if (StaticId != 0) + buffer.WriteUShort(StaticId); + buffer.WriteUShort(Owner.Connection.Id); + + buffer.WriteUShort(Payload.Size); + Payload.Write(buffer); + State.Snapshot(buffer); + } + + public void ReplicateEvent( + RagonRoomPlayer caller, + RagonEvent evnt, + RagonReplicationMode eventMode, + RagonRoomPlayer targetPlayer + ) + { + var room = Owner.Room; + var buffer = room.Writer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); + buffer.WriteUShort(evnt.EventCode); + buffer.WriteUShort(caller.Connection.Id); + buffer.WriteByte((byte)eventMode); + buffer.WriteUShort(Id); + + evnt.Write(buffer); + + var sendData = buffer.ToArray(); + targetPlayer.Connection.Reliable.Send(sendData); + } + + public void ReplicateEvent( + RagonRoomPlayer caller, + RagonEvent evnt, + RagonReplicationMode eventMode, + RagonTarget targetMode + ) + { + if (Authority == RagonAuthority.OwnerOnly && + Owner.Connection.Id != caller.Connection.Id) + { + Console.WriteLine($"Player have not enough authority for event with Id {evnt.EventCode}"); + return; + } + + if (eventMode == RagonReplicationMode.Buffered && targetMode != RagonTarget.Owner) + { + _bufferedEvents.Add(evnt); + } + + var room = Owner.Room; + var buffer = room.Writer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); + buffer.WriteUShort(evnt.EventCode); + buffer.WriteUShort(caller.Connection.Id); + buffer.WriteByte((byte)eventMode); + buffer.WriteUShort(Id); + + evnt.Write(buffer); + + var sendData = buffer.ToArray(); + switch (targetMode) + { + case RagonTarget.Owner: + { + Owner.Connection.Reliable.Send(sendData); + break; + } + case RagonTarget.ExceptOwner: + { + foreach (var roomPlayer in room.ReadyPlayersList) + { + if (roomPlayer.Connection.Id != Owner.Connection.Id) + roomPlayer.Connection.Reliable.Send(sendData); + } + + break; + } + case RagonTarget.ExceptInvoker: + { + foreach (var roomPlayer in room.ReadyPlayersList) + { + if (roomPlayer.Connection.Id != caller.Connection.Id) + roomPlayer.Connection.Reliable.Send(sendData); + } + + break; + } + case RagonTarget.All: + { + foreach (var roomPlayer in room.ReadyPlayersList) + roomPlayer.Connection.Reliable.Send(sendData); + break; + } + } + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Entity/RagonEntityState.cs b/Ragon.Server/Sources/Entity/RagonEntityState.cs new file mode 100644 index 0000000..4bfecff --- /dev/null +++ b/Ragon.Server/Sources/Entity/RagonEntityState.cs @@ -0,0 +1,79 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Server; + +public class RagonEntityState +{ + private List _properties; + private RagonEntity _entity; + + public RagonEntityState(RagonEntity entity, int capacity = 10) + { + _entity = entity; + _properties = new List(10); + } + + public void AddProperty(RagonProperty property) + { + _properties.Add(property); + } + + public void Write(RagonBuffer buffer) + { + buffer.WriteUShort(_entity.Id); + foreach (var property in _properties) + { + if (property.IsDirty) + { + buffer.WriteBool(true); + property.Write(buffer); + property.Clear(); + continue; + } + + buffer.WriteBool(false); + } + } + + public void Read(RagonBuffer buffer) + { + foreach (var property in _properties) + { + if (buffer.ReadBool()) + property.Read(buffer); + } + } + + public void Snapshot(RagonBuffer buffer) + { + foreach (var property in _properties) + { + var hasPayload = property.IsFixed || !property.IsFixed && property.Size > 0; + if (hasPayload) + { + buffer.WriteBool(true); + property.Write(buffer); + continue; + } + + buffer.WriteBool(false); + } + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Entity/RagonEvent.cs b/Ragon.Server/Sources/Entity/RagonEvent.cs new file mode 100644 index 0000000..48069ec --- /dev/null +++ b/Ragon.Server/Sources/Entity/RagonEvent.cs @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Server; + +public class RagonEvent +{ + public RagonRoomPlayer Invoker { get; private set; } + public ushort EventCode { get; private set; } + public ushort Size => (ushort) _size; + + private uint[] _data = new uint[128]; + private int _size = 0; + + public RagonEvent( + RagonRoomPlayer invoker, + ushort eventCode + ) + { + Invoker = invoker; + EventCode = eventCode; + } + + public void Read(RagonBuffer buffer) + { + var readOnlySpan = _data.AsSpan(); + _size = buffer.Capacity; + buffer.ReadSpan(ref readOnlySpan, _size); + } + + public void Write(RagonBuffer buffer) + { + if (_size == 0) return; + ReadOnlySpan readOnlySpan = _data.AsSpan(); + buffer.WriteSpan(ref readOnlySpan, _size); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Entity/RagonPayload.cs b/Ragon.Server/Sources/Entity/RagonPayload.cs new file mode 100644 index 0000000..064eb5e --- /dev/null +++ b/Ragon.Server/Sources/Entity/RagonPayload.cs @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Server; + +public class RagonPayload +{ + private uint[] _data = new uint[128]; + private int _size = 0; + + public ushort Size => (ushort) _size; + + public void Read(RagonBuffer buffer) + { + var readOnlySpan = _data.AsSpan(); + + _size = buffer.Capacity; + + buffer.ReadSpan(ref readOnlySpan, _size); + } + + public void Write(RagonBuffer buffer) + { + if (_size == 0) return; + + ReadOnlySpan readOnlySpan = _data.AsSpan(); + + buffer.WriteSpan(ref readOnlySpan, _size); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Entity/RagonProperty.cs b/Ragon.Server/Sources/Entity/RagonProperty.cs new file mode 100644 index 0000000..3f453c1 --- /dev/null +++ b/Ragon.Server/Sources/Entity/RagonProperty.cs @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using System.ComponentModel; +using Ragon.Protocol; + +namespace Ragon.Server; + +public class RagonProperty : RagonPayload +{ + public int Size { get; set; } + public bool IsDirty { get; private set; } + public bool IsFixed { get; private set; } + + private uint[] _data; + + public RagonProperty(int size, bool isFixed) + { + Size = size; + IsFixed = isFixed; + IsDirty = false; + + _data = new uint[128]; + } + + public void Read(RagonBuffer buffer) + { + var readOnlySpan = _data.AsSpan(); + if (IsFixed) + { + buffer.ReadSpan(ref readOnlySpan, Size); + } + else + { + Size = (int) buffer.Read(); + buffer.ReadSpan(ref readOnlySpan, Size); + } + + IsDirty = true; + } + + public void Write(RagonBuffer buffer) + { + ReadOnlySpan readOnlySpan = _data.AsSpan(); + buffer.WriteSpan(ref readOnlySpan, Size); + } + + public void Clear() + { + IsDirty = false; + } + + public void Dump() + { + Console.WriteLine( $"[{Size.ToString("00")}] {string.Join("", _data.Take(8).Reverse().Select(b => Convert.ToString(b, 2).PadLeft(32, '0')))}"); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/AuthorizationOperation.cs b/Ragon.Server/Sources/Handler/AuthorizationOperation.cs new file mode 100644 index 0000000..6dac1f5 --- /dev/null +++ b/Ragon.Server/Sources/Handler/AuthorizationOperation.cs @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Protocol; + +namespace Ragon.Server; + +public sealed class AuthorizationOperation: IRagonOperation +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + { + if (context.LobbyPlayer.Status == LobbyPlayerStatus.Authorized) + { + _logger.Warn("Player already authorized"); + return; + } + + var key = reader.ReadString(); + var playerName = reader.ReadString(); + var additionalPayload = new RagonPayload(); + additionalPayload.Read(reader); + + context.LobbyPlayer.Name = playerName; + context.LobbyPlayer.AdditionalData = Array.Empty(); + context.LobbyPlayer.Status = LobbyPlayerStatus.Authorized; + + var playerId = context.LobbyPlayer.Id; + + writer.Clear(); + writer.WriteOperation(RagonOperation.AUTHORIZED_SUCCESS); + writer.WriteString(playerId); + writer.WriteString(playerName); + + var sendData = writer.ToArray(); + context.Connection.Reliable.Send(sendData); + + _logger.Trace($"Connection {context.Connection.Id} as {playerId}|{context.LobbyPlayer.Name} authorized"); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/EntityCreateOperation.cs b/Ragon.Server/Sources/Handler/EntityCreateOperation.cs new file mode 100644 index 0000000..32c0069 --- /dev/null +++ b/Ragon.Server/Sources/Handler/EntityCreateOperation.cs @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Protocol; + +namespace Ragon.Server; + +public sealed class EntityCreateOperation : IRagonOperation +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + { + var player = context.RoomPlayer; + var room = context.Room; + var attachId = reader.ReadUShort(); + var entityType = reader.ReadUShort(); + var eventAuthority = (RagonAuthority) reader.ReadByte(); + var propertiesCount = reader.ReadUShort(); + + var entity = new RagonEntity(player, entityType, 0, attachId, eventAuthority); + for (var i = 0; i < propertiesCount; i++) + { + var propertyType = reader.ReadBool(); + var propertySize = reader.ReadUShort(); + + entity.State.AddProperty(new RagonProperty(propertySize, propertyType)); + } + + if (reader.Capacity > 0) + entity.Payload.Read(reader); + + room.AttachEntity(entity); + player.AttachEntity(entity); + + entity.Create(); + + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} created entity {entity.Id}:{entity.Type}"); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/EntityDestroyOperation.cs b/Ragon.Server/Sources/Handler/EntityDestroyOperation.cs new file mode 100644 index 0000000..a112529 --- /dev/null +++ b/Ragon.Server/Sources/Handler/EntityDestroyOperation.cs @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Protocol; + +namespace Ragon.Server; + +public sealed class EntityDestroyOperation: IRagonOperation +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + { + var player = context.RoomPlayer; + var room = context.Room; + var entityId = reader.ReadUShort(); + + if (room.Entities.TryGetValue(entityId, out var entity)) + { + var payload = new RagonPayload(); + payload.Read(reader); + + room.DetachEntity(entity); + player.DetachEntity(entity); + + entity.Destroy(); + + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} destoyed entity {entity.Id}"); + } + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/EntityEventOperation.cs b/Ragon.Server/Sources/Handler/EntityEventOperation.cs new file mode 100644 index 0000000..284721a --- /dev/null +++ b/Ragon.Server/Sources/Handler/EntityEventOperation.cs @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Protocol; + +namespace Ragon.Server; + +public sealed class EntityEventOperation : IRagonOperation +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + { + var player = context.RoomPlayer; + var room = context.Room; + var entityId = reader.ReadUShort(); + + if (!room.Entities.TryGetValue(entityId, out var ent)) + { + _logger.Warn($"Entity not found for event with Id {entityId}"); + return; + } + + var eventId = reader.ReadUShort(); + var eventMode = (RagonReplicationMode)reader.ReadByte(); + var targetMode = (RagonTarget)reader.ReadByte(); + var targetPlayerPeerId = (ushort)0; + + if (targetMode == RagonTarget.Player) + targetPlayerPeerId = reader.ReadUShort(); + + var ragonEvent = new RagonEvent(player, eventId); + ragonEvent.Read(reader); + + if (targetMode == RagonTarget.Player && + context.Room.Players.TryGetValue(targetPlayerPeerId, out var targetPlayer)) + { + ent.ReplicateEvent(player, ragonEvent, eventMode, targetPlayer); + return; + } + + ent.ReplicateEvent(player, ragonEvent, eventMode, targetMode); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/EntityStateOperation.cs b/Ragon.Server/Sources/Handler/EntityStateOperation.cs new file mode 100644 index 0000000..119ee07 --- /dev/null +++ b/Ragon.Server/Sources/Handler/EntityStateOperation.cs @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Protocol; + +namespace Ragon.Server; + +public sealed class EntityStateOperation: IRagonOperation +{ + private ILogger _logger = LogManager.GetCurrentClassLogger(); + + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + { + var room = context.Room; + var entitiesCount = reader.ReadUShort(); + + for (var entityIndex = 0; entityIndex < entitiesCount; entityIndex++) + { + var entityId = reader.ReadUShort(); + if (room.Entities.TryGetValue(entityId, out var entity)) + { + entity.State.Read(reader); + room.Track(entity); + } + else + { + _logger.Error($"Entity with Id {entityId} not found, replication interrupted"); + } + } + } +} \ No newline at end of file diff --git a/Ragon.Core/Handlers/RoomCreateHandler.cs b/Ragon.Server/Sources/Handler/RoomCreateOperation.cs similarity index 62% rename from Ragon.Core/Handlers/RoomCreateHandler.cs rename to Ragon.Server/Sources/Handler/RoomCreateOperation.cs index 8b668f1..a97fad2 100644 --- a/Ragon.Core/Handlers/RoomCreateHandler.cs +++ b/Ragon.Server/Sources/Handler/RoomCreateOperation.cs @@ -1,16 +1,30 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using NLog; -using Ragon.Common; -using Ragon.Core.Game; -using Ragon.Core.Lobby; +using Ragon.Protocol; -namespace Ragon.Core.Handlers; +namespace Ragon.Server; -public sealed class CreateHandler: IHandler +public sealed class RoomCreateOperation: IRagonOperation { private RagonRoomParameters _roomParameters = new(); private Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) { if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized) { @@ -48,26 +62,22 @@ public sealed class CreateHandler: IHandler }; var lobbyPlayer = context.LobbyPlayer; - var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name); - var room = new Room(roomId, information); - room.AddPlayer(roomPlayer); - - context.Room?.RemovePlayer(context.RoomPlayer); - context.Room = room; - context.RoomPlayer = roomPlayer; + var room = new RagonRoom(roomId, information); + context.Scheduler.Run(room); context.Lobby.Persist(room); + var player = new RagonRoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name); + context.SetRoom(room, player); + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} create room {room.Id} {information}"); - JoinSuccess(roomPlayer, room, writer); - - context.Loop.Run(room); + JoinSuccess(player, room, writer); _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} joined to room {room.Id}"); } - private void JoinSuccess(RoomPlayer player, Room room, RagonSerializer writer) + private void JoinSuccess(RagonRoomPlayer player, RagonRoom room, RagonBuffer writer) { writer.Clear(); writer.WriteOperation(RagonOperation.JOIN_SUCCESS); diff --git a/Ragon.Core/Handlers/RoomJoinHandler.cs b/Ragon.Server/Sources/Handler/RoomJoinOperation.cs similarity index 50% rename from Ragon.Core/Handlers/RoomJoinHandler.cs rename to Ragon.Server/Sources/Handler/RoomJoinOperation.cs index 74f9e25..a9eda75 100644 --- a/Ragon.Core/Handlers/RoomJoinHandler.cs +++ b/Ragon.Server/Sources/Handler/RoomJoinOperation.cs @@ -1,15 +1,29 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using NLog; -using Ragon.Common; -using Ragon.Core.Game; -using Ragon.Core.Lobby; +using Ragon.Protocol; -namespace Ragon.Core.Handlers; +namespace Ragon.Server; -public sealed class JoinHandler : IHandler +public sealed class RoomJoinOperation : IRagonOperation { private Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) { var roomId = reader.ReadString(); var lobbyPlayer = context.LobbyPlayer; @@ -22,20 +36,15 @@ public sealed class JoinHandler : IHandler return; } - var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name); - - context.Room?.RemovePlayer(context.RoomPlayer); - context.Room = existsRoom; - context.RoomPlayer = roomPlayer; - - existsRoom.AddPlayer(roomPlayer); - - JoinSuccess(roomPlayer, existsRoom, writer); + var player = new RagonRoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name); + context.SetRoom(existsRoom, player); + + JoinSuccess(player, existsRoom, writer); _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} joined to {existsRoom.Id}"); } - private void JoinSuccess(RoomPlayer player, Room room, RagonSerializer writer) + private void JoinSuccess(RagonRoomPlayer player, RagonRoom room, RagonBuffer writer) { writer.Clear(); writer.WriteOperation(RagonOperation.JOIN_SUCCESS); @@ -50,7 +59,7 @@ public sealed class JoinHandler : IHandler player.Connection.Reliable.Send(sendData); } - private void JoinFailed(LobbyPlayer player, RagonSerializer writer) + private void JoinFailed(RagonLobbyPlayer player, RagonBuffer writer) { writer.Clear(); writer.WriteOperation(RagonOperation.JOIN_FAILED); diff --git a/Ragon.Core/Handlers/RoomJoinOrCreateHandler.cs b/Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs similarity index 52% rename from Ragon.Core/Handlers/RoomJoinOrCreateHandler.cs rename to Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs index 50ecb22..17f509f 100644 --- a/Ragon.Core/Handlers/RoomJoinOrCreateHandler.cs +++ b/Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs @@ -1,16 +1,30 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using NLog; -using Ragon.Common; -using Ragon.Core.Game; -using Ragon.Core.Lobby; +using Ragon.Protocol; -namespace Ragon.Core.Handlers; +namespace Ragon.Server; -public sealed class JoinOrCreateHandler : IHandler +public sealed class RoomJoinOrCreateOperation : IRagonOperation { private RagonRoomParameters _roomParameters = new(); private Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) { if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized) { @@ -25,15 +39,10 @@ public sealed class JoinOrCreateHandler : IHandler if (context.Lobby.FindRoomByMap(_roomParameters.Map, out var existsRoom)) { - var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name); + var player = new RagonRoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name); + context.SetRoom(existsRoom, player); - context.Room?.RemovePlayer(context.RoomPlayer); - context.Room = existsRoom; - context.RoomPlayer = roomPlayer; - - existsRoom.AddPlayer(roomPlayer); - - JoinSuccess(roomPlayer, existsRoom, writer); + JoinSuccess(player, existsRoom, writer); } else { @@ -44,25 +53,20 @@ public sealed class JoinOrCreateHandler : IHandler Min = _roomParameters.Min, }; - var room = new Room(roomId, information); + var room = new RagonRoom(roomId, information); context.Lobby.Persist(room); + context.Scheduler.Run(room); - var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name); - room.AddPlayer(roomPlayer); - - context.Room?.RemovePlayer(context.RoomPlayer); - context.Room = room; - context.RoomPlayer = roomPlayer; + var roomPlayer = new RagonRoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name); + context.SetRoom(room, roomPlayer); _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} create room {room.Id} {information}"); JoinSuccess(roomPlayer, room, writer); - - context.Loop.Run(room); } } - private void JoinSuccess(RoomPlayer player, Room room, RagonSerializer writer) + private void JoinSuccess(RagonRoomPlayer player, RagonRoom room, RagonBuffer writer) { writer.Clear(); writer.WriteOperation(RagonOperation.JOIN_SUCCESS); diff --git a/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs b/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs new file mode 100644 index 0000000..c118216 --- /dev/null +++ b/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Protocol; + +namespace Ragon.Server; + +public sealed class RoomLeaveOperation: IRagonOperation +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + { + var room = context.Room; + var roomPlayer = context.RoomPlayer; + if (room != null) + { + context.Room?.DetachPlayer(roomPlayer); + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} leaved from {room.Id}"); + } + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/SceneLoadOperation.cs b/Ragon.Server/Sources/Handler/SceneLoadOperation.cs new file mode 100644 index 0000000..7aae87c --- /dev/null +++ b/Ragon.Server/Sources/Handler/SceneLoadOperation.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using NLog; +using Ragon.Protocol; + +namespace Ragon.Server; + +public class SceneLoadOperation: IRagonOperation +{ + private Logger _logger = LogManager.GetCurrentClassLogger(); + + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + { + var roomOwner = context.Room.Owner; + var currentPlayer = context.RoomPlayer; + var room = context.Room; + var sceneName = reader.ReadString(); + + if (roomOwner.Connection.Id != currentPlayer.Connection.Id) + { + _logger.Warn("Only owner can change map!"); + return; + } + + room.UpdateMap(sceneName); + + writer.Clear(); + writer.WriteOperation(RagonOperation.LOAD_SCENE); + writer.WriteString(sceneName); + + var sendData = writer.ToArray(); + foreach (var player in room.PlayerList) + player.Connection.Reliable.Send(sendData); + } +} \ No newline at end of file diff --git a/Ragon.Core/Handlers/SceneLoadedHandler.cs b/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs similarity index 59% rename from Ragon.Core/Handlers/SceneLoadedHandler.cs rename to Ragon.Server/Sources/Handler/SceneLoadedOperation.cs index de40a96..7c20e6e 100644 --- a/Ragon.Core/Handlers/SceneLoadedHandler.cs +++ b/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs @@ -1,15 +1,29 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using NLog; -using Ragon.Common; -using Ragon.Core.Game; -using Ragon.Core.Lobby; +using Ragon.Protocol; -namespace Ragon.Core.Handlers; +namespace Ragon.Server; -public sealed class SceneLoadedHandler : IHandler +public sealed class SceneLoadedOperation : IRagonOperation { private Logger _logger = LogManager.GetCurrentClassLogger(); - - public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer) + + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) { if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized) return; @@ -24,48 +38,53 @@ public sealed class SceneLoadedHandler : IHandler for (var staticIndex = 0; staticIndex < statics; staticIndex++) { var entityType = reader.ReadUShort(); - var eventAuthority = (RagonAuthority) reader.ReadByte(); + var eventAuthority = (RagonAuthority)reader.ReadByte(); var staticId = reader.ReadUShort(); var propertiesCount = reader.ReadUShort(); - - var entity = new Entity(player, entityType, staticId, eventAuthority); + + var entity = new RagonEntity(player, entityType, staticId, 0, eventAuthority); for (var propertyIndex = 0; propertyIndex < propertiesCount; propertyIndex++) { var propertyType = reader.ReadBool(); var propertySize = reader.ReadUShort(); - entity.State.AddProperty(new EntityStateProperty(propertySize, propertyType)); + entity.State.AddProperty(new RagonProperty(propertySize, propertyType)); } + + var playerInfo = $"Player {context.Connection.Id}|{context.LobbyPlayer.Name}"; + var entityInfo = $"{entity.Id}:{entity.Type}"; - _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} created entity {entity.Id}:{entity.Type}"); - room.AttachEntity(player, entity); + _logger.Trace($"{playerInfo} created entity {entityInfo}"); + + room.AttachEntity(entity); + player.AttachEntity(entity); } _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} loaded"); - + room.WaitPlayersList.Add(player); foreach (var roomPlayer in room.WaitPlayersList) { DispatchPlayerJoinExcludePlayer(room, roomPlayer, writer); - + roomPlayer.SetReady(); } - + room.UpdateReadyPlayerList(); - + DispatchSnapshot(room, room.WaitPlayersList, writer); - + room.WaitPlayersList.Clear(); } else if (owner.IsLoaded) { player.SetReady(); - + DispatchPlayerJoinExcludePlayer(room, player, writer); - + room.UpdateReadyPlayerList(); - - DispatchSnapshot(room, new List() { player }, writer); + + DispatchSnapshot(room, new List() { player }, writer); foreach (var entity in room.EntityList) entity.RestoreBufferedEvents(player, writer); @@ -77,14 +96,14 @@ public sealed class SceneLoadedHandler : IHandler } } - private void DispatchPlayerJoinExcludePlayer(Room room, RoomPlayer roomPlayer, RagonSerializer writer) + private void DispatchPlayerJoinExcludePlayer(RagonRoom room, RagonRoomPlayer roomPlayer, RagonBuffer writer) { writer.Clear(); writer.WriteOperation(RagonOperation.PLAYER_JOINED); writer.WriteUShort(roomPlayer.Connection.Id); writer.WriteString(roomPlayer.Id); writer.WriteString(roomPlayer.Name); - + var sendData = writer.ToArray(); foreach (var awaiter in room.ReadyPlayersList) { @@ -93,30 +112,30 @@ public sealed class SceneLoadedHandler : IHandler } } - private void DispatchSnapshot(Room room, List receviersList, RagonSerializer writer) + private void DispatchSnapshot(RagonRoom room, List receviersList, RagonBuffer writer) { writer.Clear(); writer.WriteOperation(RagonOperation.SNAPSHOT); - writer.WriteUShort((ushort) room.ReadyPlayersList.Count); + writer.WriteUShort((ushort)room.ReadyPlayersList.Count); foreach (var roomPlayer in room.ReadyPlayersList) { writer.WriteUShort(roomPlayer.Connection.Id); writer.WriteString(roomPlayer.Id); writer.WriteString(roomPlayer.Name); } - + var dynamicEntities = room.DynamicEntitiesList; - var dynamicEntitiesCount = (ushort) dynamicEntities.Count; + var dynamicEntitiesCount = (ushort)dynamicEntities.Count; writer.WriteUShort(dynamicEntitiesCount); foreach (var entity in dynamicEntities) - entity.State.Snapshot(writer); + entity.Snapshot(writer); var staticEntities = room.StaticEntitiesList; - var staticEntitiesCount = (ushort) staticEntities.Count; + var staticEntitiesCount = (ushort)staticEntities.Count; writer.WriteUShort(staticEntitiesCount); foreach (var entity in staticEntities) - entity.State.Snapshot(writer); - + entity.Snapshot(writer); + var sendData = writer.ToArray(); foreach (var player in receviersList) player.Connection.Reliable.Send(sendData); diff --git a/Ragon.Server/IO/Executor.cs b/Ragon.Server/Sources/IO/Executor.cs similarity index 59% rename from Ragon.Server/IO/Executor.cs rename to Ragon.Server/Sources/IO/Executor.cs index b0451c6..a503546 100644 --- a/Ragon.Server/IO/Executor.cs +++ b/Ragon.Server/Sources/IO/Executor.cs @@ -1,8 +1,24 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using System.Threading.Channels; -namespace Ragon.Core.Server; +namespace Ragon.Server; -public class Executor: TaskScheduler +public class Executor: TaskScheduler, IExecutor { private ChannelReader _reader; private ChannelWriter _writer; @@ -39,7 +55,7 @@ public class Executor: TaskScheduler return false; } - public void Execute() + public void Update() { while (_reader.TryRead(out var task)) { diff --git a/Ragon.Server/Sources/IO/IExecutor.cs b/Ragon.Server/Sources/IO/IExecutor.cs new file mode 100644 index 0000000..e1560ba --- /dev/null +++ b/Ragon.Server/Sources/IO/IExecutor.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public interface IExecutor +{ + public void Run(Action action); +} \ No newline at end of file diff --git a/Ragon.Server/Sources/IO/INetworkChannel.cs b/Ragon.Server/Sources/IO/INetworkChannel.cs new file mode 100644 index 0000000..fa8c7e2 --- /dev/null +++ b/Ragon.Server/Sources/IO/INetworkChannel.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public interface INetworkChannel +{ + void Send(byte[] data); +} \ No newline at end of file diff --git a/Ragon.Server/Sources/IO/INetworkConnection.cs b/Ragon.Server/Sources/IO/INetworkConnection.cs new file mode 100644 index 0000000..4ad3c86 --- /dev/null +++ b/Ragon.Server/Sources/IO/INetworkConnection.cs @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public interface INetworkConnection +{ + public ushort Id { get; } + public INetworkChannel Reliable { get; } + public INetworkChannel Unreliable { get; } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/IO/INetworkListener.cs b/Ragon.Server/Sources/IO/INetworkListener.cs new file mode 100644 index 0000000..7dd3928 --- /dev/null +++ b/Ragon.Server/Sources/IO/INetworkListener.cs @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public interface INetworkListener +{ + void OnConnected(INetworkConnection connection); + void OnDisconnected(INetworkConnection connection); + void OnTimeout(INetworkConnection connection); + void OnData(INetworkConnection connection, byte[] data); +} \ No newline at end of file diff --git a/Ragon.Server/Sources/IO/INetworkServer.cs b/Ragon.Server/Sources/IO/INetworkServer.cs new file mode 100644 index 0000000..107506a --- /dev/null +++ b/Ragon.Server/Sources/IO/INetworkServer.cs @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public interface INetworkServer +{ + public Executor Executor { get; } + public void Stop(); + public void Update(); + public void Start(INetworkListener listener, NetworkConfiguration configuration); +} \ No newline at end of file diff --git a/Ragon.Server/Sources/IO/NetworkConfiguration.cs b/Ragon.Server/Sources/IO/NetworkConfiguration.cs new file mode 100644 index 0000000..b76768b --- /dev/null +++ b/Ragon.Server/Sources/IO/NetworkConfiguration.cs @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public struct NetworkConfiguration +{ + public int LimitConnections { get; set; } + public int Port { get; set; } + public uint Protocol { get; set; } + public string Address { get; set; } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/IRagonOperation.cs b/Ragon.Server/Sources/IRagonOperation.cs new file mode 100644 index 0000000..e94882e --- /dev/null +++ b/Ragon.Server/Sources/IRagonOperation.cs @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Server; + +public interface IRagonOperation +{ + public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer); +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Lobby/IRagonLobby.cs b/Ragon.Server/Sources/Lobby/IRagonLobby.cs new file mode 100644 index 0000000..7003df4 --- /dev/null +++ b/Ragon.Server/Sources/Lobby/IRagonLobby.cs @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Diagnostics.CodeAnalysis; + +namespace Ragon.Server; + +public interface IRagonLobby +{ + public bool FindRoomById(string roomId, [MaybeNullWhen(false)] out RagonRoom room); + public bool FindRoomByMap(string map, [MaybeNullWhen(false)] out RagonRoom room); + public void Persist(RagonRoom room); + public void RemoveIfEmpty(RagonRoom room); +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Lobby/RagonLobbyInMemory.cs b/Ragon.Server/Sources/Lobby/RagonLobbyInMemory.cs new file mode 100644 index 0000000..2f1f5f5 --- /dev/null +++ b/Ragon.Server/Sources/Lobby/RagonLobbyInMemory.cs @@ -0,0 +1,79 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Diagnostics.CodeAnalysis; +using NLog; + +namespace Ragon.Server; + +public class LobbyInMemory : IRagonLobby +{ + private readonly List _rooms = new(); + private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public bool FindRoomById(string RagonRoomId, [MaybeNullWhen(false)] out RagonRoom room) + { + foreach (var existRagonRoom in _rooms) + { + var info = existRagonRoom.Info; + if (existRagonRoom.Id == RagonRoomId && info.Min < info.Max) + { + room = existRagonRoom; + return true; + } + } + + room = default; + return false; + } + + public bool FindRoomByMap(string map, [MaybeNullWhen(false)] out RagonRoom room) + { + foreach (var existsRoom in _rooms) + { + var info = existsRoom.Info; + if (info.Map == map && existsRoom.Players.Count < info.Max) + { + room = existsRoom; + return true; + } + } + + room = default; + return false; + } + + public void Persist(RagonRoom room) + { + _rooms.Add(room); + _logger.Trace($"New room: {room.Id}"); + + foreach (var r in _rooms) + _logger.Trace($"Room: {r.Id} {r.Info} Players: {r.Players.Count} Entities: {r.Entities.Count}"); + } + + public void RemoveIfEmpty(RagonRoom room) + { + if (room.Players.Count == 0) + { + _rooms.Remove(room); + _logger.Trace($"Room {room.Id} removed"); + } + + foreach (var r in _rooms) + _logger.Trace($"Room: {r.Id} {r.Info} Players: {r.Players.Count} Entities: {r.Entities.Count}"); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs b/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs new file mode 100644 index 0000000..54bde74 --- /dev/null +++ b/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public enum LobbyPlayerStatus +{ + Unauthorized, + Authorized, +} + +public class RagonLobbyPlayer +{ + public string Id { get; private set; } + public string Name { get; set; } + public byte[] AdditionalData { get; set; } + public LobbyPlayerStatus Status { get; set; } + public INetworkConnection Connection { get; private set; } + + public RagonLobbyPlayer(INetworkConnection connection) + { + Id = Guid.NewGuid().ToString(); + Connection = connection; + Status = LobbyPlayerStatus.Unauthorized; + Name = "None"; + AdditionalData = Array.Empty(); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/RagonContext.cs b/Ragon.Server/Sources/RagonContext.cs new file mode 100644 index 0000000..1cdb384 --- /dev/null +++ b/Ragon.Server/Sources/RagonContext.cs @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Core.Time; +using Ragon.Server; + +namespace Ragon.Server; + +public class RagonContext +{ + public INetworkConnection Connection { get; } + public IExecutor Executor { get; private set; } + + public IRagonLobby Lobby { get; private set; } + public RagonLobbyPlayer LobbyPlayer { get; private set; } + + public RagonRoom? Room { get; private set; } + public RagonRoomPlayer? RoomPlayer { get; private set; } + + public RagonScheduler Scheduler { get; private set; } + + public RagonContext(INetworkConnection connection, IExecutor executor, IRagonLobby lobby, RagonScheduler scheduler, RagonLobbyPlayer lobbyPlayer) + { + Connection = connection; + Executor = executor; + Lobby = lobby; + Scheduler = scheduler; + LobbyPlayer = lobbyPlayer; + } + + internal void SetRoom(RagonRoom room, RagonRoomPlayer player) + { + Room?.DetachPlayer(RoomPlayer); + + Room = room; + RoomPlayer = player; + + Room.AttachPlayer(RoomPlayer); + } + +} \ No newline at end of file diff --git a/Ragon.Server/Sources/RagonEntityCache.cs b/Ragon.Server/Sources/RagonEntityCache.cs new file mode 100644 index 0000000..9a55352 --- /dev/null +++ b/Ragon.Server/Sources/RagonEntityCache.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public class RagonEntityCache +{ + private readonly List _dynamicEntitiesList = new List(); + private readonly List _staticEntitiesList = new List(); + private readonly Dictionary _entitiesMap = new Dictionary(); + + public IReadOnlyList StaticList => _staticEntitiesList; + public IReadOnlyList DynamicList => _dynamicEntitiesList; + public IReadOnlyDictionary Map => _entitiesMap; + + public void Add(RagonEntity entity) + { + if (entity.StaticId != 0) + _staticEntitiesList.Add(entity); + else + _dynamicEntitiesList.Add(entity); + + _entitiesMap.Add(entity.Id, entity); + } + + public bool Remove(RagonEntity entity) + { + if (_entitiesMap.Remove(entity.Id, out var existEntity)) + { + _staticEntitiesList.Remove(entity); + _dynamicEntitiesList.Remove(entity); + + return true; + } + return false; + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/RagonServer.cs b/Ragon.Server/Sources/RagonServer.cs new file mode 100644 index 0000000..317f3e0 --- /dev/null +++ b/Ragon.Server/Sources/RagonServer.cs @@ -0,0 +1,180 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Diagnostics; +using NLog; +using Ragon.Core.Time; +using Ragon.Protocol; +using Ragon.Server; + +namespace Ragon.Server; + +public class RagonServer : INetworkListener +{ + private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + private readonly INetworkServer _server; + private readonly Thread _dedicatedThread; + private readonly Executor _executor; + private readonly Configuration _configuration; + private readonly IRagonOperation[] _handlers; + private readonly RagonBuffer _reader; + private readonly RagonBuffer _writer; + private readonly IRagonLobby _lobby; + private readonly RagonScheduler _scheduler; + private readonly Dictionary _contexts; + private long _tickrate = 0; + private Stopwatch _timer; + + public RagonServer(INetworkServer server, Configuration configuration) + { + _server = server; + _executor = _server.Executor; + _configuration = configuration; + _dedicatedThread = new Thread(Execute); + _dedicatedThread.IsBackground = true; + _contexts = new Dictionary(); + _lobby = new LobbyInMemory(); + _scheduler = new RagonScheduler(); + + _reader = new RagonBuffer(); + _writer = new RagonBuffer(); + _tickrate = _configuration.ServerTickRate; + _timer = new Stopwatch(); + + _handlers = new IRagonOperation[byte.MaxValue]; + _handlers[(byte) RagonOperation.AUTHORIZE] = new AuthorizationOperation(); + _handlers[(byte) RagonOperation.JOIN_OR_CREATE_ROOM] = new RoomJoinOrCreateOperation(); + _handlers[(byte) RagonOperation.CREATE_ROOM] = new RoomCreateOperation(); + _handlers[(byte) RagonOperation.JOIN_ROOM] = new RoomJoinOperation(); + _handlers[(byte) RagonOperation.LEAVE_ROOM] = new RoomLeaveOperation(); + _handlers[(byte) RagonOperation.LOAD_SCENE] = new SceneLoadOperation(); + _handlers[(byte) RagonOperation.SCENE_LOADED] = new SceneLoadedOperation(); + _handlers[(byte) RagonOperation.CREATE_ENTITY] = new EntityCreateOperation(); + _handlers[(byte) RagonOperation.DESTROY_ENTITY] = new EntityDestroyOperation(); + _handlers[(byte) RagonOperation.REPLICATE_ENTITY_EVENT] = new EntityEventOperation(); + _handlers[(byte) RagonOperation.REPLICATE_ENTITY_STATE] = new EntityStateOperation(); + } + + public void Execute() + { + _timer.Start(); + while (true) + { + if (_timer.ElapsedMilliseconds > _tickrate) + { + _executor.Update(); + _timer.Restart(); + } + + _scheduler.Update(); + _server.Update(); + + Thread.Sleep(1); + } + } + + public void Start(bool executeInDedicatedThread = false) + { + var networkConfiguration = new NetworkConfiguration() + { + LimitConnections = _configuration.LimitConnections, + Protocol = RagonVersion.Parse(_configuration.GameProtocol), + Address = "0.0.0.0", + Port = _configuration.Port, + }; + + _server.Start(this, networkConfiguration); + + if (executeInDedicatedThread) + _dedicatedThread.Start(); + else + Execute(); + } + + public void Dispose() + { + _server.Stop(); + _dedicatedThread.Interrupt(); + } + + public void OnConnected(INetworkConnection connection) + { + var lobbyPlayer = new RagonLobbyPlayer(connection); + var context = new RagonContext(connection, _executor, _lobby, _scheduler, lobbyPlayer); + + _logger.Trace($"Connected: {connection.Id}"); + _contexts.Add(connection.Id, context); + } + + public void OnDisconnected(INetworkConnection connection) + { + if (_contexts.Remove(connection.Id, out var context)) + { + var room = context.Room; + if (room != null) + { + room.DetachPlayer(context.RoomPlayer); + _lobby.RemoveIfEmpty(room); + } + + _logger.Trace($"Disconnected: {connection.Id}|{context.LobbyPlayer.Name}|{context.LobbyPlayer.Id}"); + } + else + { + _logger.Trace($"Disconnected: {connection.Id}"); + } + } + + public void OnTimeout(INetworkConnection connection) + { + if (_contexts.Remove(connection.Id, out var context)) + { + var room = context.Room; + if (room != null) + { + room.DetachPlayer(context.RoomPlayer); + _lobby.RemoveIfEmpty(room); + } + + _logger.Trace($"Timeout: {connection.Id}|{context.LobbyPlayer.Name}|{context.LobbyPlayer.Id}"); + } + else + { + _logger.Trace($"Timeout: {connection.Id}"); + } + } + + public void OnData(INetworkConnection connection, byte[] data) + { + try + { + if (_contexts.TryGetValue(connection.Id, out var context)) + { + _writer.Clear(); + _reader.Clear(); + _reader.FromArray(data); + + // Console.WriteLine($"{string.Join(",", data.Select(d => d.ToString()))}"); + var operation = _reader.ReadByte(); + _handlers[operation].Handle(context, _reader, _writer); + } + } + catch (Exception ex) + { + _logger.Error(ex); + } + } +} \ No newline at end of file diff --git a/Ragon.Core/Configuration.cs b/Ragon.Server/Sources/RagonServerConfiguration.cs similarity index 54% rename from Ragon.Core/Configuration.cs rename to Ragon.Server/Sources/RagonServerConfiguration.cs index dbdc447..5c55ddd 100644 --- a/Ragon.Core/Configuration.cs +++ b/Ragon.Server/Sources/RagonServerConfiguration.cs @@ -1,7 +1,29 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using Newtonsoft.Json; using NLog; -namespace Ragon.Core; +namespace Ragon.Server; + +public enum ServerType +{ + ENET, + WEBSOCKET, +} [Serializable] public struct Configuration @@ -16,7 +38,12 @@ public struct Configuration public int LimitRooms; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - private static readonly string ServerVersion = "1.0.31-rc"; + private static readonly string ServerVersion = "1.1.0-rc"; + private static Dictionary _serverTypes = new Dictionary() + { + {"enet", Server.ServerType.ENET}, + {"websocket", Server.ServerType.WEBSOCKET} + }; private static void CopyrightInfo() { @@ -40,4 +67,6 @@ public struct Configuration var configuration = JsonConvert.DeserializeObject(data); return configuration; } + + public static ServerType GetServerType(string type) => _serverTypes[type]; } \ No newline at end of file diff --git a/Ragon.Core/Game/Room.cs b/Ragon.Server/Sources/Room/RagonRoom.cs similarity index 50% rename from Ragon.Core/Game/Room.cs rename to Ragon.Server/Sources/Room/RagonRoom.cs index 26a3ccf..655820d 100644 --- a/Ragon.Core/Game/Room.cs +++ b/Ragon.Server/Sources/Room/RagonRoom.cs @@ -1,166 +1,201 @@ -using Ragon.Common; -using Ragon.Core.Time; - -namespace Ragon.Core.Game; - -public class Room: IAction -{ - public string Id { get; private set; } - public RoomInformation Info { get; private set; } - public RoomPlayer Owner { get; private set; } - public RagonSerializer Writer { get; } - public Dictionary Players { get; private set; } - public List WaitPlayersList { get; private set; } - public List ReadyPlayersList { get; private set; } - public List PlayerList { get; private set; } - - public Dictionary Entities { get; private set; } - public List DynamicEntitiesList { get; private set; } - public List StaticEntitiesList { get; private set; } - public List EntityList { get; private set; } - - private readonly HashSet _entitiesDirtySet; - - public Room(string roomId, RoomInformation info) - { - Id = roomId; - Info = info; - - Players = new Dictionary(info.Max); - WaitPlayersList = new List(info.Max); - ReadyPlayersList = new List(info.Max); - PlayerList = new List(info.Max); - - Entities = new Dictionary(); - DynamicEntitiesList = new List(); - StaticEntitiesList = new List(); - EntityList = new List(); - - _entitiesDirtySet = new HashSet(); - Writer = new RagonSerializer(512); - } - - public void AttachEntity(RoomPlayer newOwner, Entity entity) - { - Entities.Add(entity.Id, entity); - EntityList.Add(entity); - - if (entity.StaticId == 0) - DynamicEntitiesList.Add(entity); - else - StaticEntitiesList.Add(entity); - - entity.Create(); - - newOwner.Entities.Add(entity); - } - - public void DetachEntity(RoomPlayer currentOwner, Entity entity, byte[] payload) - { - Entities.Remove(entity.Id); - EntityList.Remove(entity); - StaticEntitiesList.Remove(entity); - DynamicEntitiesList.Remove(entity); - _entitiesDirtySet.Remove(entity); - - entity.Destroy(payload); - currentOwner.Entities.Remove(entity); - } - - public void Tick() - { - var entities = (ushort) _entitiesDirtySet.Count; - if (entities > 0) - { - Writer.Clear(); - Writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE); - Writer.WriteUShort(entities); - - foreach (var entity in _entitiesDirtySet) - entity.State.Write(Writer); - - _entitiesDirtySet.Clear(); - - var sendData = Writer.ToArray(); - foreach (var roomPlayer in ReadyPlayersList) - roomPlayer.Connection.Unreliable.Send(sendData); - } - } - - public void AddPlayer(RoomPlayer player) - { - if (Players.Count == 0) - Owner = player; - - player.Attach(this); - - PlayerList.Add(player); - Players.Add(player.Connection.Id, player); - } - - public void RemovePlayer(RoomPlayer roomPlayer) - { - if (Players.Remove(roomPlayer.Connection.Id, out var player)) - { - PlayerList.Remove(player); - - { - Writer.Clear(); - Writer.WriteOperation(RagonOperation.PLAYER_LEAVED); - Writer.WriteString(player.Id); - - var entitiesToDelete = player.Entities.DynamicList; - Writer.WriteUShort((ushort) entitiesToDelete.Count); - foreach (var entity in entitiesToDelete) - { - Writer.WriteUShort(entity.Id); - EntityList.Remove(entity); - } - - var sendData = Writer.ToArray(); - Broadcast(sendData); - } - - if (roomPlayer.Connection.Id == Owner.Connection.Id && PlayerList.Count > 0) - { - var nextOwner = PlayerList[0]; - - Owner = nextOwner; - - var entitiesToUpdate = roomPlayer.Entities.StaticList; - - Writer.Clear(); - Writer.WriteOperation(RagonOperation.OWNERSHIP_CHANGED); - Writer.WriteString(Owner.Id); - Writer.WriteUShort((ushort) entitiesToUpdate.Count); - - foreach (var entity in entitiesToUpdate) - { - Writer.WriteUShort(entity.Id); - - entity.SetOwner(nextOwner); - nextOwner.Entities.Add(entity); - } - - var sendData = Writer.ToArray(); - Broadcast(sendData); - } - } - } - - public void UpdateReadyPlayerList() - { - ReadyPlayersList = PlayerList.Where(p => p.IsLoaded).ToList(); - } - - public void Track(Entity entity) - { - _entitiesDirtySet.Add(entity); - } - - public void Broadcast(byte[] data) - { - foreach (var readyPlayer in ReadyPlayersList) - readyPlayer.Connection.Reliable.Send(data); - } +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Core.Time; +using Ragon.Protocol; + +namespace Ragon.Server; + +public class RagonRoom: IRagonAction +{ + public string Id { get; private set; } + public RoomInformation Info { get; private set; } + public RagonRoomPlayer Owner { get; private set; } + public RagonBuffer Writer { get; } + public Dictionary Players { get; private set; } + public List WaitPlayersList { get; private set; } + public List ReadyPlayersList { get; private set; } + public List PlayerList { get; private set; } + + public Dictionary Entities { get; private set; } + public List DynamicEntitiesList { get; private set; } + public List StaticEntitiesList { get; private set; } + public List EntityList { get; private set; } + + private readonly HashSet _entitiesDirtySet; + + public RagonRoom(string roomId, RoomInformation info) + { + Id = roomId; + Info = info; + + Players = new Dictionary(info.Max); + WaitPlayersList = new List(info.Max); + ReadyPlayersList = new List(info.Max); + PlayerList = new List(info.Max); + + Entities = new Dictionary(); + DynamicEntitiesList = new List(); + StaticEntitiesList = new List(); + EntityList = new List(); + + _entitiesDirtySet = new HashSet(); + + Writer = new RagonBuffer(); + } + + public void AttachEntity(RagonEntity entity) + { + Entities.Add(entity.Id, entity); + EntityList.Add(entity); + + if (entity.StaticId == 0) + DynamicEntitiesList.Add(entity); + else + StaticEntitiesList.Add(entity); + } + + public void DetachEntity(RagonEntity entity) + { + Entities.Remove(entity.Id); + EntityList.Remove(entity); + StaticEntitiesList.Remove(entity); + DynamicEntitiesList.Remove(entity); + + _entitiesDirtySet.Remove(entity); + } + + public void Tick() + { + var entities = (ushort) _entitiesDirtySet.Count; + if (entities > 0) + { + Writer.Clear(); + Writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE); + Writer.WriteUShort(entities); + + foreach (var entity in _entitiesDirtySet) + entity.State.Write(Writer); + + _entitiesDirtySet.Clear(); + + var sendData = Writer.ToArray(); + foreach (var roomPlayer in ReadyPlayersList) + roomPlayer.Connection.Unreliable.Send(sendData); + } + } + + public void AttachPlayer(RagonRoomPlayer player) + { + if (Players.Count == 0) + Owner = player; + + player.OnAttached(this); + + PlayerList.Add(player); + Players.Add(player.Connection.Id, player); + } + + public void DetachPlayer(RagonRoomPlayer roomPlayer) + { + if (Players.Remove(roomPlayer.Connection.Id, out var player)) + { + PlayerList.Remove(player); + + { + Writer.Clear(); + Writer.WriteOperation(RagonOperation.PLAYER_LEAVED); + Writer.WriteString(player.Id); + + var entitiesToDelete = player.Entities.DynamicList; + Writer.WriteUShort((ushort) entitiesToDelete.Count); + foreach (var entity in entitiesToDelete) + { + Writer.WriteUShort(entity.Id); + DetachEntity(entity); + } + + var sendData = Writer.ToArray(); + Broadcast(sendData); + } + + if (roomPlayer.Connection.Id == Owner.Connection.Id && PlayerList.Count > 0) + { + var nextOwner = PlayerList[0]; + + Owner = nextOwner; + + var entitiesToUpdate = roomPlayer.Entities.StaticList; + + Writer.Clear(); + Writer.WriteOperation(RagonOperation.OWNERSHIP_CHANGED); + Writer.WriteString(Owner.Id); + Writer.WriteUShort((ushort) entitiesToUpdate.Count); + + foreach (var entity in entitiesToUpdate) + { + Writer.WriteUShort(entity.Id); + + entity.SetOwner(nextOwner); + nextOwner.Entities.Add(entity); + } + + var sendData = Writer.ToArray(); + Broadcast(sendData); + } + + player.OnDetached(); + + UpdateReadyPlayerList(); + } + } + + public void UpdateReadyPlayerList() + { + ReadyPlayersList = PlayerList.Where(p => p.IsLoaded).ToList(); + } + + public void UpdateMap(string sceneName) + { + Info = new RoomInformation() + { + Max = Info.Max, + Min = Info.Min, + Map = sceneName, + }; + + DynamicEntitiesList.Clear(); + StaticEntitiesList.Clear(); + Entities.Clear(); + EntityList.Clear(); + + foreach (var player in PlayerList) + player.UnsetReady(); + + UpdateReadyPlayerList(); + } + + public void Track(RagonEntity entity) + { + _entitiesDirtySet.Add(entity); + } + + public void Broadcast(byte[] data) + { + foreach (var readyPlayer in ReadyPlayersList) + readyPlayer.Connection.Reliable.Send(data); + } } \ No newline at end of file diff --git a/Ragon.Server/Sources/Room/RagonRoomInformation.cs b/Ragon.Server/Sources/Room/RagonRoomInformation.cs new file mode 100644 index 0000000..6bfd205 --- /dev/null +++ b/Ragon.Server/Sources/Room/RagonRoomInformation.cs @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public class RoomInformation +{ + public string Map { get; init; } = "none"; + public int Min { get; init; } + public int Max { get; init; } + + public override string ToString() + { + return $"Map: {Map} Count: {Min}/{Max}"; + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Room/RagonRoomPlayer.cs b/Ragon.Server/Sources/Room/RagonRoomPlayer.cs new file mode 100644 index 0000000..47ae625 --- /dev/null +++ b/Ragon.Server/Sources/Room/RagonRoomPlayer.cs @@ -0,0 +1,65 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Server; + +public class RagonRoomPlayer +{ + public INetworkConnection Connection { get; } + public string Id { get; } + public string Name { get; } + public bool IsLoaded { get; private set; } + public RagonRoom Room { get; private set; } + public RagonEntityCache Entities { get; private set; } + + public RagonRoomPlayer(INetworkConnection connection, string id, string name) + { + Id = id; + Name = name; + Connection = connection; + Entities = new RagonEntityCache(); + } + + public void AttachEntity(RagonEntity entity) + { + Entities.Add(entity); + } + + public void DetachEntity(RagonEntity entity) + { + Entities.Remove(entity); + } + + internal void OnAttached(RagonRoom room) + { + Room = room; + } + + internal void OnDetached() + { + Room = null!; + } + + internal void SetReady() + { + IsLoaded = true; + } + + internal void UnsetReady() + { + IsLoaded = false; + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Time/IRagonAction.cs b/Ragon.Server/Sources/Time/IRagonAction.cs new file mode 100644 index 0000000..63c9adf --- /dev/null +++ b/Ragon.Server/Sources/Time/IRagonAction.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Core.Time; + +public interface IRagonAction +{ + public void Tick(); +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Time/RagonActionExecutor.cs b/Ragon.Server/Sources/Time/RagonActionExecutor.cs new file mode 100644 index 0000000..e6198a7 --- /dev/null +++ b/Ragon.Server/Sources/Time/RagonActionExecutor.cs @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Core.Time; + +public class RagonScheduler +{ + private List _tasks; + + public RagonScheduler() + { + _tasks = new List(35); + } + + public void Run(IRagonAction task) + { + _tasks.Add(task); + } + + public void Stop(IRagonAction task) + { + _tasks.Remove(task); + } + + public void Update() + { + foreach (var task in _tasks) + task.Tick(); + } +} \ No newline at end of file diff --git a/Ragon.Tests/Basic.cs b/Ragon.Tests/Basic.cs new file mode 100644 index 0000000..4e7e448 --- /dev/null +++ b/Ragon.Tests/Basic.cs @@ -0,0 +1,154 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Client; +using Ragon.Protocol; +using Xunit.Abstractions; + +namespace Ragon.Core.Tests; + +public class Connection +{ + private readonly ITestOutputHelper _testOutputHelper; + + public Connection(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + public class Game : IRagonListener + { + private RagonClient _client; + + public Game(RagonClient client, TaskCompletionSource taskCompletionSource) + { + _client = client; + } + + public void OnConnected(RagonClient client) + { + Console.WriteLine("Console Connected"); + RagonLog.Trace("Connected"); + + client.Session.AuthorizeWithKey("defaultkey", "Player Eduard", Array.Empty()); + } + + public void OnAuthorizationSuccess(RagonClient client, string playerId, string playerName) + { + RagonLog.Trace("Authorized"); + + client.Session.Create("Example", 1, 20); + } + + public void OnAuthorizationFailed(RagonClient client, string message) + { + RagonLog.Trace($"Authorization failed: {message}"); + } + + public void OnJoined(RagonClient client) + { + } + + public void OnFailed(RagonClient client, string message) + { + RagonLog.Trace("Failed to join"); + } + + public void OnLeft(RagonClient client) + { + RagonLog.Trace("Left"); + } + + public void OnDisconnected(RagonClient client) + { + RagonLog.Trace("Disconnected"); + } + + public void OnPlayerJoined(RagonClient client, RagonPlayer player) + { + RagonLog.Trace("Player joined"); + } + + public void OnPlayerLeft(RagonClient client, RagonPlayer player) + { + RagonLog.Trace("Player left"); + } + + public void OnOwnershipChanged(RagonClient client, RagonPlayer player) + { + RagonLog.Trace("Owner ship changed"); + } + + public void OnLevel(RagonClient client, string sceneName) + { + RagonLog.Trace($"New level: {sceneName}"); + client.Room.SceneLoaded(); + } + } + + // [Fact] + // public async void Connect() + // { + // RagonLog.Set(new RagonXUnitLogger(_testOutputHelper)); + // + // var joining = new TaskCompletionSource(); + // var network = new RagonNetwork(); + // var game = new Game(network, joining); + // + // network.AddListener(game); + // + // var clientConfiguration = new RagonConnectionConfiguration(); + // clientConfiguration.Type = RagonConnectionType.UDP; + // clientConfiguration.Protocol = "1.0.0"; + // clientConfiguration.Address = "127.0.0.1"; + // clientConfiguration.Port = 5000; + // + // network.Connect(clientConfiguration); + // + // var relayConfiguration = new Configuration() + // { + // GameProtocol = "1.0.0", + // LimitConnections = 4095, + // LimitRooms = 20, + // LimitPlayersPerRoom = 20, + // Port = 5000, + // ServerKey = "defaultkey", + // ServerTickRate = 30, + // ServerType = "enet" + // }; + // + // var relay = new RagonServer(relayConfiguration); + // relay.Start(true); + // + // var ticks = 0; + // while (true) + // { + // ticks += 1; + // network.Update(); + // + // if (ticks > 100) + // break; + // + // await Task.Delay(100); + // } + // + // Assert.Equal(network.Status, RagonStatus.ROOM); + // + // network.Dispose(); + // relay.Dispose(); + // } +} \ No newline at end of file diff --git a/Ragon.Tests/Ragon.Tests.csproj b/Ragon.Tests/Ragon.Tests.csproj new file mode 100644 index 0000000..0085550 --- /dev/null +++ b/Ragon.Tests/Ragon.Tests.csproj @@ -0,0 +1,32 @@ + + + + net7.0 + enable + enable + + false + + Ragon.Core.Tests + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/Ragon.Tests/Usings.cs b/Ragon.Tests/Usings.cs new file mode 100644 index 0000000..096f36b --- /dev/null +++ b/Ragon.Tests/Usings.cs @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +global using Xunit; \ No newline at end of file diff --git a/Ragon.Tests/Utils/TestRagonLogger.cs b/Ragon.Tests/Utils/TestRagonLogger.cs new file mode 100644 index 0000000..766387f --- /dev/null +++ b/Ragon.Tests/Utils/TestRagonLogger.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Client; +using Ragon.Protocol; +using Xunit.Abstractions; + +namespace Ragon.Core.Tests; + +public class RagonXUnitLogger: IRagonLogger +{ + private ITestOutputHelper _outputHelper; + public RagonXUnitLogger(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + public void Warn(string message) + { + _outputHelper.WriteLine($"[Warn] {message}"); + } + + public void Trace(string message) + { + _outputHelper.WriteLine($"[Trace] {message}"); + } + + public void Info(string message) + { + _outputHelper.WriteLine($"[Info] {message}"); + } + + public void Error(string message) + { + _outputHelper.WriteLine($"[Error] {message}"); + } +} \ No newline at end of file diff --git a/Ragon.sln b/Ragon.sln old mode 100755 new mode 100644 index 87f940f..20fb719 --- a/Ragon.sln +++ b/Ragon.sln @@ -4,14 +4,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Relay", "Ragon.Relay\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Protocol", "Ragon.Protocol\Ragon.Protocol.csproj", "{F478B2A2-36F4-43B9-9BB7-382A57C449B2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server", "Ragon.Server\Ragon.Server.csproj", "{45D3B686-8960-4656-91B2-6F8BFCCAA225}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server", "Ragon.Server\Ragon.Server.csproj", "{F4AA86B9-2486-4B53-BA77-43D958A2FDC3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Core", "Ragon.Core\Ragon.Core.csproj", "{F4AA86B9-2486-4B53-BA77-43D958A2FDC3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server.NativeWebSockets", "Ragon.Server.NativeWebSockets\Ragon.Server.NativeWebSockets.csproj", "{81050343-A9B8-487B-86C8-7A5B7DD9C39B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server.DotNetWebSockets", "Ragon.Server.DotNetWebSockets\Ragon.Server.DotNetWebSockets.csproj", "{81050343-A9B8-487B-86C8-7A5B7DD9C39B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server.ENet", "Ragon.Server.ENet\Ragon.Server.ENet.csproj", "{DD79AC4F-9E5C-4938-850E-805D537E68D0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Client", "Ragon.Client\Ragon.Client.csproj", "{C82D65BF-6D80-4263-ADFE-CB9ED990B6C3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Client.Simulation", "Ragon.Client.Simulation\Ragon.Client.Simulation.csproj", "{0384848D-3B63-4B3A-B15F-A836EBB3E95D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Tests", "Ragon.Tests\Ragon.Tests.csproj", "{D1F44471-A700-4E7C-B047-334685850A5C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Client.Property", "Ragon.Client.Property\Ragon.Client.Property.csproj", "{46A60DAB-F854-4BB6-A119-BD4C5B2B0D29}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,10 +32,6 @@ Global {F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Release|Any CPU.Build.0 = Release|Any CPU - {45D3B686-8960-4656-91B2-6F8BFCCAA225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {45D3B686-8960-4656-91B2-6F8BFCCAA225}.Debug|Any CPU.Build.0 = Debug|Any CPU - {45D3B686-8960-4656-91B2-6F8BFCCAA225}.Release|Any CPU.ActiveCfg = Release|Any CPU - {45D3B686-8960-4656-91B2-6F8BFCCAA225}.Release|Any CPU.Build.0 = Release|Any CPU {F4AA86B9-2486-4B53-BA77-43D958A2FDC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F4AA86B9-2486-4B53-BA77-43D958A2FDC3}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4AA86B9-2486-4B53-BA77-43D958A2FDC3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -42,5 +44,21 @@ Global {DD79AC4F-9E5C-4938-850E-805D537E68D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD79AC4F-9E5C-4938-850E-805D537E68D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD79AC4F-9E5C-4938-850E-805D537E68D0}.Release|Any CPU.Build.0 = Release|Any CPU + {C82D65BF-6D80-4263-ADFE-CB9ED990B6C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C82D65BF-6D80-4263-ADFE-CB9ED990B6C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C82D65BF-6D80-4263-ADFE-CB9ED990B6C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C82D65BF-6D80-4263-ADFE-CB9ED990B6C3}.Release|Any CPU.Build.0 = Release|Any CPU + {0384848D-3B63-4B3A-B15F-A836EBB3E95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0384848D-3B63-4B3A-B15F-A836EBB3E95D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0384848D-3B63-4B3A-B15F-A836EBB3E95D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0384848D-3B63-4B3A-B15F-A836EBB3E95D}.Release|Any CPU.Build.0 = Release|Any CPU + {D1F44471-A700-4E7C-B047-334685850A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1F44471-A700-4E7C-B047-334685850A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1F44471-A700-4E7C-B047-334685850A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1F44471-A700-4E7C-B047-334685850A5C}.Release|Any CPU.Build.0 = Release|Any CPU + {46A60DAB-F854-4BB6-A119-BD4C5B2B0D29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46A60DAB-F854-4BB6-A119-BD4C5B2B0D29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46A60DAB-F854-4BB6-A119-BD4C5B2B0D29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46A60DAB-F854-4BB6-A119-BD4C5B2B0D29}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/global.json b/global.json deleted file mode 100755 index 9e5e1fd..0000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "6.0.0", - "rollForward": "latestMajor", - "allowPrerelease": true - } -} \ No newline at end of file diff --git a/license b/license deleted file mode 100644 index 18b411b..0000000 --- a/license +++ /dev/null @@ -1,21 +0,0 @@ -Copyright (c) 2022 Eduard Kargin (theedison4@gmail.com) - -MIT License: - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file