diff --git a/CMakeLists.txt b/CMakeLists.txt index a0ba9e91..6c25a434 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,8 @@ add_subdirectory ( "src/lib-game" ) add_subdirectory ( "src/lib-app" ) add_subdirectory ( "src/bin" ) add_subdirectory ( "src/demo-ordered-dither" ) +add_subdirectory ( "src/server-test" ) +add_subdirectory ( "src/client-test" ) if ( NOT ${DISABLE_TESTING} ) add_subdirectory ( "src/tests" ) diff --git a/src/client-test/.clang-format b/src/client-test/.clang-format new file mode 100644 index 00000000..0c2d22c6 --- /dev/null +++ b/src/client-test/.clang-format @@ -0,0 +1,75 @@ +Language: Cpp +IndentWidth: 4 +ColumnLimit: '80' +NamespaceIndentation: All +AccessModifierOffset: -4 +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +AlignAfterOpenBracket: 'AlwaysBreak' +BinPackArguments: 'false' +BinPackParameters: 'false' +PointerAlignment: Left +ReferenceAlignment: Pointer +SortIncludes: CaseSensitive +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpacesInAngles: Never +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: WithoutElse +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +# BreakAfterAttributes: Always +BreakBeforeConceptDeclarations: Always +BreakBeforeBinaryOperators: NonAssignment +CompactNamespaces: false +BreakStringLiterals: true +Cpp11BracedListStyle: false +EmptyLineBeforeAccessModifier: Always +FixNamespaceComments: true +IncludeBlocks: Merge +QualifierAlignment: Left # Left - west const, Right - east const +ReflowComments: true +RequiresClausePosition: OwnLine +SeparateDefinitionBlocks: Always +PackConstructorInitializers: NextLine #NextLineOnly is better +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true + +# Unsupported in MSVC 17.5.2 +# LanguageStandard: Cpp20 +# SpaceBeforeJsonColon: false +# QualifierOrder: ['inline', 'static', 'constexpr', 'volatile', 'const', 'type', ] +# RequiresExpressionIndentation: OuterScope +# NextLineOnly for PackConstructorInitializers +# BreakAfterAttributes: Always diff --git a/src/client-test/CMakeLists.txt b/src/client-test/CMakeLists.txt new file mode 100644 index 00000000..5e112c0d --- /dev/null +++ b/src/client-test/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required ( VERSION 3.26 ) + +file ( + COPY "${CMAKE_BINARY_DIR}/.clang-format" + DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}" +) + +project ( client-test ) + +add_executable ( ${PROJECT_NAME} Main.cpp ) + +target_link_libraries ( ${PROJECT_NAME} lib-network lib-misc ) + +autoset_target_compile_options ( ${PROJECT_NAME} FALSE ) \ No newline at end of file diff --git a/src/client-test/Main.cpp b/src/client-test/Main.cpp new file mode 100644 index 00000000..ea17d935 --- /dev/null +++ b/src/client-test/Main.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +import Network; + +int main(int, char*[]) +{ + auto&& client = Client::create("127.0.0.1", 10666); + if (!client) + { + std::println(std::cerr, "{}", client.error()); + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/src/lib-network/CMakeLists.txt b/src/lib-network/CMakeLists.txt index b08694ff..2916ed0d 100644 --- a/src/lib-network/CMakeLists.txt +++ b/src/lib-network/CMakeLists.txt @@ -11,6 +11,6 @@ glob_modules ( SRCS ) add_library ( ${PROJECT_NAME} ${SRCS} ) -target_link_libraries ( ${PROJECT_NAME} Dep_json Dep_sfml Dep_dgm ) +target_link_libraries ( ${PROJECT_NAME} Dep_json Dep_sfml Dep_dgm lib-misc lib-memory ) autoset_target_compile_options ( ${PROJECT_NAME} FALSE ) diff --git a/src/lib-network/src/Client.ixx b/src/lib-network/src/Client.ixx index fcfc26d3..d29f5f4e 100644 --- a/src/lib-network/src/Client.ixx +++ b/src/lib-network/src/Client.ixx @@ -1,3 +1,116 @@ module; +#pragma warning(push, 0) +#include +#pragma warning(pop) +#include +#include +#include +#include +#include +#include +#include + export module Client; + +import Message; +import Error; +import Memory; + +export class Client +{ +public: + Client(const sf::IpAddress& address, unsigned short port) + : remoteAddress(address), remotePort(port) + { + } + +public: + static [[nodiscard]] std::expected + create(const sf::IpAddress& address, unsigned short port) + { + try + { + auto&& client = Client(address, port); + if (auto&& result = client.bindToAnyPort(); !result) + return std::unexpected(result.error()); + + auto&& id = client.registerToServer(); + if (!id) return std::unexpected(id.error()); + client.myClientId = *id; + + return client; + } + catch (std::exception& e) + { + return std::unexpected(e.what()); + } + } + +private: + ExpectSuccess bindToAnyPort() + { + if (socket->bind(sf::Socket::AnyPort) != sf::Socket::Status::Done) + { + return std::unexpected( + std::format("Cannot bind socket to any port")); + } + + myPort = socket->getLocalPort(); + std::println("Socket bound to port {}", myPort); + + return ReturnFlag::Success; + } + + std::expected registerToServer() + { + if (auto&& result = sendConnectPacket(); !result) + return std::unexpected(result.error()); + + auto&& message = getConnectResponse(); + if (!message) return std::unexpected(message.error()); + return message->playerId; + } + + ExpectSuccess sendConnectPacket() + { + auto&& packet = Message { .type = MessageType::Connect }.toPacket(); + if (socket->send(packet, remoteAddress, remotePort) + != sf::Socket::Status::Done) + { + return std::unexpected(std::format( + "Could not send message to {}:{}", + remoteAddress.toString(), + remotePort)); + } + } + + std::expected getConnectResponse() + { + auto&& packet = sf::Packet(); + if (socket->receive(packet, remoteAddress, remotePort) + != sf::Socket::Status::Done) + { + return std::unexpected( + std::format("Got no response from remote server")); + } + + auto&& message = Message::parseMessage(packet); + if (message.type != MessageType::ConnectConfirmed) + { + return std::unexpected(std::format( + "Expected ConnectConfirmed from the server, got {}", + static_cast>( + message.type))); + } + + return message; + } + +private: + sf::IpAddress remoteAddress; + unsigned short remotePort; + mem::Box socket; + unsigned short myPort; + PlayerIdType myClientId; +}; diff --git a/src/lib-network/src/Message.ixx b/src/lib-network/src/Message.ixx index d432976d..421bc2c1 100644 --- a/src/lib-network/src/Message.ixx +++ b/src/lib-network/src/Message.ixx @@ -10,21 +10,57 @@ export using PlayerIdType = std::uint8_t; using ChecksumType = std::uint64_t; + enum class [[nodiscard]] ClientMessageType : uint8_t + { + ConnectionRequest, + PeerSettingsUpdate, + GameSettingsUpdate, + CommitLobby, + MapLoaded, + ReportInput, + Disconnect + }; + + struct [[nodiscard]] ClientMessage + { + }; + + enum class [[nodiscard]] ServerMessageType : uint8_t + { + ConnectionAccepted, + ConnectionRefused, + LobbyCommited, + StartGame, + UpdateInput + }; + + struct [[nodiscard]] ServerMessage + { + }; + enum class MessageType : std::uint8_t { Connect, + ConnectConfirmed, + ConnectionRefused, + PeerSettingsUpdate, + GameSettingsUpdate, Update, Disconnect }; - struct Message + struct [[nodiscard]] Message { - PlayerIdType playerId; MessageType type; + PlayerIdType playerId; std::string playerName; std::string inputJson; std::size_t tick; ChecksumType checksum; + + static Message parseMessage(sf::Packet& packet); + + [[nodiscard]] sf::Packet toPacket() const; }; sf::Packet& operator<<(sf::Packet& packet, const Message& message) @@ -44,4 +80,20 @@ export message.type = static_cast(type); return packet; } -} \ No newline at end of file +} + +module :private; + +Message Message::parseMessage(sf::Packet& packet) +{ + auto&& result = Message(); + packet >> result; + return result; +} + +sf::Packet Message::toPacket() const +{ + auto&& packet = sf::Packet(); + packet << *this; + return packet; +} diff --git a/src/lib-network/src/Server.ixx b/src/lib-network/src/Server.ixx index eb9e676d..4adab13a 100644 --- a/src/lib-network/src/Server.ixx +++ b/src/lib-network/src/Server.ixx @@ -6,6 +6,7 @@ module; #pragma warning(pop) #include #include +#include #include #include @@ -13,53 +14,152 @@ export module Server; import Message; import ClientData; +import Error; + +export struct ServerConfiguration final +{ + unsigned short port; + unsigned short maxClientCount; +}; export class [[nodiscard]] Server final { public: - Server(unsigned short port) + Server(ServerConfiguration config) : MAX_CLIENT_COUNT(config.maxClientCount) { - if (socket.bind(port) != sf::Socket::Status::Done) + if (socket.bind(config.port) != sf::Socket::Status::Done) { - throw std::runtime_error( - std::format("Server: Cannot bind socket to port {}", port)); + throw std::runtime_error(std::format( + "Server: Cannot bind socket to port {}", config.port)); } + + std::println("Server: Listening on port {}", config.port); } -private: +public: void startLoop() { socket.setBlocking(false); - sf::IpAddress sender; - unsigned short port; + sf::IpAddress remoteAddress; + unsigned short remotePort; sf::Packet packet; Message message; // Read all incoming messages - while (socket.receive(packet, sender, port) == sf::Socket::Status::Done) + while (true) { - packet >> message; - handleMessage(message); - } + while (socket.receive(packet, remoteAddress, remotePort) + == sf::Socket::Status::Done) + { + handleMessage( + Message::parseMessage(packet), remoteAddress, remotePort); + } - // Send them back to all clients - // TODO: + // Send them back to all clients + // TODO: + } } - void handleMessage(const Message& message) +private: + void handleMessage( + const Message& message, + const sf::IpAddress& address, + unsigned short port) { switch (message.type) { case MessageType::Connect: + if (auto&& result = handleConnectionAttempt(address, port); !result) + { + std::println(std::cerr, "Server: {}", result.error()); + } break; case MessageType::Update: + std::println("Update"); break; case MessageType::Disconnect: + std::println("Disconnect"); break; } } + ExpectSuccess + handleConnectionAttempt(const sf::IpAddress& address, unsigned short port) + { + return registeredClients.size() == MAX_CLIENT_COUNT + ? denyNewClient(address, port) + : registeredClients.contains(address.toInteger()) + ? denyReconnection(address, port) + : registerNewClient(address, port); + } + + ExpectSuccess + denyNewClient(const sf::IpAddress& address, unsigned short port) + { + std::println("Server: Already at full capacity, refusing new peer"); + + auto&& packet = + Message { .type = MessageType::ConnectionRefused }.toPacket(); + if (socket.send(packet, address, port) != sf::Socket::Status::Done) + { + return std::unexpected(std::format( + "Failed to send ConnectionRefused to remote peer at {}:{}", + address.toString(), + port)); + } + + return ReturnFlag::Success; + } + + ExpectSuccess + denyReconnection(const sf::IpAddress& address, unsigned short port) + { + std::println( + "Server: Client at {}:{} is trying to connect again, refusing", + address.toString(), + port); + auto&& packet = + Message { .type = MessageType::ConnectionRefused }.toPacket(); + if (socket.send(packet, address, port) != sf::Socket::Status::Done) + { + return std::unexpected(std::format( + "Failed to send ConnectionRefused to remote peer at {}:{}", + address.toString(), + port)); + } + + return ReturnFlag::Success; + } + + ExpectSuccess + registerNewClient(const sf::IpAddress& address, unsigned short port) + { + auto&& newId = static_cast(registeredClients.size()); + auto&& packet = + Message { .type = MessageType::ConnectConfirmed, .playerId = newId } + .toPacket(); + std::println( + "Registering new client at {}:{} with ID: {}", + address.toString(), + port, + newId); + + if (socket.send(packet, address, port) != sf::Socket::Status::Done) + { + return std::unexpected(std::format( + "Could not send ConnectionConfirmed to new peer at {}:{}", + address.toString(), + port)); + } + + registeredClients[address.toInteger()] = newId; + + return ReturnFlag::Success; + } + private: + const unsigned short MAX_CLIENT_COUNT; sf::UdpSocket socket; std::deque connectedClients; + std::map registeredClients; }; diff --git a/src/server-test/.clang-format b/src/server-test/.clang-format new file mode 100644 index 00000000..0c2d22c6 --- /dev/null +++ b/src/server-test/.clang-format @@ -0,0 +1,75 @@ +Language: Cpp +IndentWidth: 4 +ColumnLimit: '80' +NamespaceIndentation: All +AccessModifierOffset: -4 +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +AlignAfterOpenBracket: 'AlwaysBreak' +BinPackArguments: 'false' +BinPackParameters: 'false' +PointerAlignment: Left +ReferenceAlignment: Pointer +SortIncludes: CaseSensitive +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpacesInAngles: Never +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: WithoutElse +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +# BreakAfterAttributes: Always +BreakBeforeConceptDeclarations: Always +BreakBeforeBinaryOperators: NonAssignment +CompactNamespaces: false +BreakStringLiterals: true +Cpp11BracedListStyle: false +EmptyLineBeforeAccessModifier: Always +FixNamespaceComments: true +IncludeBlocks: Merge +QualifierAlignment: Left # Left - west const, Right - east const +ReflowComments: true +RequiresClausePosition: OwnLine +SeparateDefinitionBlocks: Always +PackConstructorInitializers: NextLine #NextLineOnly is better +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true + +# Unsupported in MSVC 17.5.2 +# LanguageStandard: Cpp20 +# SpaceBeforeJsonColon: false +# QualifierOrder: ['inline', 'static', 'constexpr', 'volatile', 'const', 'type', ] +# RequiresExpressionIndentation: OuterScope +# NextLineOnly for PackConstructorInitializers +# BreakAfterAttributes: Always diff --git a/src/server-test/CMakeLists.txt b/src/server-test/CMakeLists.txt new file mode 100644 index 00000000..0498a084 --- /dev/null +++ b/src/server-test/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required ( VERSION 3.26 ) + +file ( + COPY "${CMAKE_BINARY_DIR}/.clang-format" + DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}" +) + + +project ( server-test ) + +add_executable ( ${PROJECT_NAME} Main.cpp ) + +target_link_libraries ( ${PROJECT_NAME} lib-network lib-misc ) + +autoset_target_compile_options ( ${PROJECT_NAME} FALSE ) \ No newline at end of file diff --git a/src/server-test/Main.cpp b/src/server-test/Main.cpp new file mode 100644 index 00000000..fb88fd4a --- /dev/null +++ b/src/server-test/Main.cpp @@ -0,0 +1,10 @@ +#include + +import Network; + +int main(int, char*[]) +{ + auto&& server = + Server(ServerConfiguration { .port = 10666, .maxClientCount = 4 }); + server.startLoop(); +} \ No newline at end of file