diff --git a/Server/CMakeLists.txt b/Server/CMakeLists.txt index b84a07ad..16cf94cd 100644 --- a/Server/CMakeLists.txt +++ b/Server/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.20) +include(${CMAKE_BINARY_DIR}/conan_toolchain.cmake) + project(r-type_server VERSION 0.0.1 DESCRIPTION "R-Type Server" @@ -12,12 +14,16 @@ if(PROJECT_IS_TOP_LEVEL) message(WARNING "Building Server standalone, adding Shuvlog and rtnt manually") add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../lib/shuvlog shuvlog) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../lib/cli_parser cli_parser) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../lib/rtnt rtnt) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../lib/rteng rteng) endif() # --- Sources / Headers --- add_executable(${PROJECT_NAME} src/main.cpp + src/lobby/lobby.cpp + src/lobby/lobby_manager.cpp + src/app.cpp ) target_include_directories(${PROJECT_NAME} PRIVATE @@ -26,8 +32,12 @@ target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../common ) +find_package(asio REQUIRED) + # --- Libraries --- target_link_libraries(${PROJECT_NAME} PRIVATE + asio::asio + rtnt rteng cli_parser shuvlog @@ -52,7 +62,14 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) if (MSVC) - target_compile_options(${PROJECT_NAME} PRIVATE /W4 /permissive-) + target_compile_definitions(${PROJECT_NAME} + PUBLIC + WIN32_LEAN_AND_MEAN + NOMINMAX + NOGDI + NOUSER + ) + target_compile_options(${PROJECT_NAME} PRIVATE /W4) else() target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror -pedantic diff --git a/Server/src/app.cpp b/Server/src/app.cpp new file mode 100644 index 00000000..49cedefd --- /dev/null +++ b/Server/src/app.cpp @@ -0,0 +1,55 @@ +#include "app.hpp" + +#include "logger/Logger.h" +#include "logger/Thread.h" +#include "utils.hpp" + +namespace server { + +App::App(const unsigned short port) + : _server(_context, + port) +{ + registerCallbacks(); + _server.onConnect( + [](std::shared_ptr) { LOG_INFO("Accepting new connection"); }); + _server.onDisconnect( + [](std::shared_ptr s) { LOG_INFO("Disconnected {}", s->getId()); }); + _server.onMessage([](std::shared_ptr s, rtnt::core::Packet& p) { + LOG_INFO("Client {} sent a packet of type {}", s->getId(), p.getId()); + }); + _lobbyManager.createLobby(); +} + +App::~App() +{ + LOG_INFO("Shutting down server."); + _lobbyManager.stopAll(); + _context.stop(); + if (_ioThread.joinable()) { + _ioThread.join(); + } +} + +void App::start() +{ + _server.start(); + _ioThread = std::thread([this]() { + logger::setThreadLabel("IoThread"); + _context.run(); + }); + _ioThread.detach(); + utils::LoopTimer loopTimer(TPS); + + while (true) { + _server.update(); + loopTimer.waitForNextTick(); + } +} + +void App::registerCallbacks() +{ + // Empty for now but register the packet callbacks here; +} + +} // namespace server diff --git a/Server/src/app.hpp b/Server/src/app.hpp new file mode 100644 index 00000000..f217cf69 --- /dev/null +++ b/Server/src/app.hpp @@ -0,0 +1,40 @@ +#pragma once +#include +#include + +#include "lobby/lobby_manager.hpp" +#include "rtnt/core/server.hpp" + +#define TPS 20 + +namespace server { + +/** + * @class App + * @brief A server application containing a lobby manager. + */ +class App +{ +public: + /** + * @brief Creates a server listening to specified port. + * @param port The port to listen to. + */ + explicit App(unsigned short port); + ~App(); + + /** + * @brief Starts the server and updates it periodically. + */ + [[noreturn]] void start(); + +private: + asio::io_context _context; + rtnt::core::Server _server; + std::thread _ioThread; + lobby::Manager _lobbyManager; + + void registerCallbacks(); +}; + +} // namespace server diff --git a/Server/src/lobby/lobby.cpp b/Server/src/lobby/lobby.cpp new file mode 100644 index 00000000..57712e39 --- /dev/null +++ b/Server/src/lobby/lobby.cpp @@ -0,0 +1,85 @@ +#include "lobby.hpp" + +#include "components/all.hpp" +#include "components/position.hpp" +#include "components/type.hpp" +#include "enums/entity_types.hpp" +#include "logger/Thread.h" + +Lobby::Lobby(const lobby::Id id) + : _roomId(id), + _engine(components::GameComponents{}), + _isRunning(false) +{ + LOG_INFO("Creating new lobby."); +} + +lobby::Id Lobby::getRoomId() const { return _roomId; } + +void Lobby::pushTask(lobby::Callback action) { _actionQueue.push(std::move(action)); } + +bool Lobby::hasJoined(const rtnt::core::session::Id sessionId) const +{ + return _players.contains(sessionId); +} + +bool Lobby::join(const rtnt::core::session::Id sessionId) +{ + if (_players.contains(sessionId)) { + LOG_WARN("Player already joined this lobby."); + return false; + } + _players.try_emplace(sessionId, 0); + if (_players.contains(sessionId)) { + _actionQueue.push( + [](rteng::GameEngine& + engine) { // Create a new player entity on join (maybe do it otherwise) + engine.registerEntity( + nullptr, {10, 10}, {entity::Type::kPlayer}); + }); + return true; + } + LOG_WARN("Player couldn't join this lobby"); + return false; +} + +void Lobby::leave(const rtnt::core::session::Id sessionId) +{ + if (_players.contains(sessionId)) { + _players.erase(sessionId); + } else { + LOG_WARN("Player was not in this lobby"); + } +} + +void Lobby::stop() +{ + if (!_isRunning) { + return; + } + LOG_INFO("Stopping lobby {}.", _roomId); + _isRunning = false; + if (_thread.joinable()) { + _thread.join(); + } +} + +void Lobby::start() +{ + _isRunning = true; + _thread = std::thread(&Lobby::run, this); + _thread.detach(); +} + +void Lobby::run() +{ + logger::setThreadLabel(("Lobby " + std::to_string(_roomId)).c_str()); + lobby::Callback callbackFunction; + while (_isRunning) { + while (_actionQueue.pop(callbackFunction)) { + callbackFunction(_engine); + } + _engine.runOnce(0.16); + } +} + diff --git a/Server/src/lobby/lobby.hpp b/Server/src/lobby/lobby.hpp new file mode 100644 index 00000000..0b554839 --- /dev/null +++ b/Server/src/lobby/lobby.hpp @@ -0,0 +1,79 @@ +#pragma once +#include + +#include "concurrent_queue.hpp" +#include "rteng.hpp" +#include "rtnt/core/session.hpp" + +namespace lobby { + +using Id = uint32_t; +using Callback = std::function; + +} // namespace lobby + +/** + * @class Lobby + * @brief Encapsulates a gameEngine instance and it's networking interface + */ +class Lobby +{ +public: + /** + * @brief Creates a lobby with the specified @code id@endcode. + * @param id an uint32(lobby::Id) that represents the id of the lobby. + * + * Note that the uniqueness of the ID depends on the user providing a distinct value. + */ + explicit Lobby(lobby::Id id); + + /** + * @brief Tries to join this lobby. + * @param sessionId The id of the session trying to join. + * @return A boolean representing the status of the request. + */ + bool join(rtnt::core::session::Id sessionId); + + /** + * @return The id of this lobby. + */ + lobby::Id getRoomId() const; + + /** + * @brief Removes the @code SessionId@endcode from this lobby. + * @param sessionId The id of the session to remove. + */ + void leave(rtnt::core::session::Id sessionId); + /** + * @param sessionId The researched id. + * @return Whether this lobby contains this @code sessionId@endcode. + */ + bool hasJoined(rtnt::core::session::Id sessionId) const; + + /** + * @brief Pushes a task to be made inside the running thread. + * This function is thread-safe. + * @param action A function performing the required action. + */ + void pushTask(lobby::Callback action); + + /** + * @brief Start this lobby. + */ + void start(); + + /** + * @brief Stop this lobby. + */ + void stop(); + +private: + lobby::Id _roomId; + utils::ConcurrentQueue _actionQueue; + rteng::GameEngine _engine; + std::unordered_map _players; + std::atomic _isRunning; + std::thread _thread; + + void run(); +}; diff --git a/Server/src/lobby/lobby_manager.cpp b/Server/src/lobby/lobby_manager.cpp new file mode 100644 index 00000000..15a57a57 --- /dev/null +++ b/Server/src/lobby/lobby_manager.cpp @@ -0,0 +1,53 @@ +#include "lobby_manager.hpp" + +#include + +namespace lobby { + +Id Manager::createLobby() +{ + static Id nbLobbies = 0; + _lobbies.emplace(nbLobbies, std::make_unique(nbLobbies)); + _lobbies.at(nbLobbies)->start(); + return nbLobbies++; +} + +Manager::~Manager() { stopAll(); } + +void Manager::stopAll() const +{ + for (const auto& lobby : _lobbies | std::views::values) { + lobby->stop(); + } +} + +void Manager::pushActionToLobby(rtnt::core::session::Id sessionId, + Callback action) +{ + const auto it = _playerLookup.find(sessionId); + if (it != _playerLookup.end()) { + Lobby* lobby = it->second; + lobby->pushTask(std::move(action)); + } else { + LOG_WARN("Session {} is not in any lobby.", sessionId); + } +} + +bool Manager::joinRoom(const rtnt::core::session::Id sessionId, + const lobby::Id roomId) const +{ + if (_lobbies.contains(roomId)) { + return _lobbies.at(roomId)->join(sessionId); + } + return false; +} + +void Manager::leaveRoom(rtnt::core::session::Id sessionId) +{ + const auto it = _playerLookup.find(sessionId); + if (it != _playerLookup.end()) { + it->second->leave(sessionId); + } +} + +} // namespace lobby diff --git a/Server/src/lobby/lobby_manager.hpp b/Server/src/lobby/lobby_manager.hpp new file mode 100644 index 00000000..5e3d096a --- /dev/null +++ b/Server/src/lobby/lobby_manager.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "lobby.hpp" +#include "rtnt/core/session.hpp" + +namespace lobby { + +/** + * @class Manager + * @brief A simple lobby manager + */ +class Manager +{ +public: + explicit Manager() = default; + ~Manager(); + + /** + * @brief Tries to join a specific lobby. + * @param sessionId The id of the session trying to join. + * @param roomId The id of the lobby to join. + * @return Whether the request is successful. + */ + [[nodiscard]] bool joinRoom(rtnt::core::session::Id sessionId, + Id roomId = 0) const; + /** + * @brief Stops all lobbies. + */ + void stopAll() const; + /** + * @brief Leaves the lobby corresponding to this sessionId. + * @param sessionId The id of the disconnecting session. + */ + void leaveRoom(rtnt::core::session::Id sessionId); + /** + * @brief Pushes an action to be performed by the lobby. + * @param sessionId The id of the session triggering the action. + * @param action A function performing the triggered action. + */ + void pushActionToLobby(rtnt::core::session::Id sessionId, + Callback action); + /** + * @brief Creates a new lobby. + * The creation of the lobbies is not automatic. + * @return The id of the newly created lobby. + */ + Id createLobby(); + +private: + std::unordered_map> _lobbies; + std::unordered_map _playerLookup; +}; + +} // namespace lobby diff --git a/Server/src/main.cpp b/Server/src/main.cpp index 2dc04b12..26386f63 100644 --- a/Server/src/main.cpp +++ b/Server/src/main.cpp @@ -1,3 +1,4 @@ +#include "app.hpp" #include "cli_parser.hpp" #include "logger/Logger.h" #include "logger/Sinks/LogFileSink.h" @@ -9,14 +10,16 @@ int main(int argc, Logger::getInstance().addSink(); Logger::getInstance().addSink("logs/latest.log"); - Logger::initialize( - "R-Type Server", argc, const_cast(argv), logger::BuildInfo::fromCMake()); + Logger::initialize("R-Type Server", argc, argv, logger::BuildInfo::fromCMake()); cli_parser::Parser p(argc, argv); - rteng::GameEngine eng(p.getValue("-p").as()); - eng.init(); - eng.run(); - LOG_INFO("Shutting down server."); + if (!p.hasFlag("-p")) { + LOG_FATAL("No port specified, use \"-p {port}\"."); + } + if (p.hasFlag("--graphical")) { + LOG_INFO("Running server with debug window. (unimplemented)"); + } - return 0; + server::App server(p.getValue("-p").as()); + server.start(); } diff --git a/lib/rteng/include/comp/IO.hpp b/common/components/IO.hpp similarity index 100% rename from lib/rteng/include/comp/IO.hpp rename to common/components/IO.hpp diff --git a/lib/rteng/include/comp/Sprite.hpp b/common/components/Sprite.hpp similarity index 100% rename from lib/rteng/include/comp/Sprite.hpp rename to common/components/Sprite.hpp diff --git a/lib/rteng/include/comp/Transform.hpp b/common/components/Transform.hpp similarity index 100% rename from lib/rteng/include/comp/Transform.hpp rename to common/components/Transform.hpp diff --git a/lib/rteng/include/comp/position.hpp b/common/components/position.hpp similarity index 100% rename from lib/rteng/include/comp/position.hpp rename to common/components/position.hpp diff --git a/lib/rteng/include/comp/rect.hpp b/common/components/rect.hpp similarity index 100% rename from lib/rteng/include/comp/rect.hpp rename to common/components/rect.hpp diff --git a/common/components/type.hpp b/common/components/type.hpp new file mode 100644 index 00000000..03c0eed2 --- /dev/null +++ b/common/components/type.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "enums/entity_types.hpp" + +namespace comp { + +struct Type +{ + entity::Type type; + + template + void serialize(Archive& ar) + { + ar & type; + } +}; + +} // namespace comp diff --git a/common/concurrent_queue.hpp b/common/concurrent_queue.hpp new file mode 100644 index 00000000..a255461c --- /dev/null +++ b/common/concurrent_queue.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +namespace utils { + +template +class ConcurrentQueue +{ +public: + bool pop(T& a) + { + std::lock_guard lock(_mutex); + if (_queue.empty()) { + return false; + } + a = _queue.front(); + _queue.pop(); + return true; + } + + void push(const T& value) + { + std::lock_guard lock(_mutex); + _queue.push(value); + } + +private: + std::queue _queue; + std::mutex _mutex; +}; + +} // namespace utils diff --git a/common/enums/entity_types.hpp b/common/enums/entity_types.hpp new file mode 100644 index 00000000..e521aead --- /dev/null +++ b/common/enums/entity_types.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace entity { + +enum class Type +{ + kPlayer = 1, + kEnemy, +}; + +} diff --git a/common/utils.hpp b/common/utils.hpp new file mode 100644 index 00000000..eca4bd77 --- /dev/null +++ b/common/utils.hpp @@ -0,0 +1,57 @@ +#pragma once +#include +#include + +#ifdef _WIN32 +#include +#pragma comment(lib, "winmm.lib") + +void enableHighPrecisionTimer() { timeBeginPeriod(1); } +#endif + +#define SPIN_THRESHOLD std::chrono::milliseconds(2) + +namespace utils { + +using namespace std::chrono; + +inline double getElapsedTime() +{ + static time_point lastCallTime = steady_clock::now(); + const duration elapsed_seconds = steady_clock::now() - lastCallTime; + const double seconds = elapsed_seconds.count(); + lastCallTime = steady_clock::now(); + return seconds; +} + +class LoopTimer +{ +public: + explicit LoopTimer(const double tps) + : _interval(duration_cast(duration(1.0 / tps))) + { + _nextTick = steady_clock::now(); + } + + void waitForNextTick() + { + _nextTick += _interval; + const time_point now = steady_clock::now(); + if (now >= _nextTick) { + return; + } + const auto remaining = duration(_nextTick - now); + if (remaining > SPIN_THRESHOLD) { + std::this_thread::sleep_for(remaining - SPIN_THRESHOLD); + } + while (steady_clock::now() < _nextTick) { + std::this_thread::yield(); + } + } + +private: + steady_clock::duration _interval; + steady_clock::time_point _nextTick; +}; + +} // namespace utils diff --git a/lib/rteng/include/comp/Behaviour.hpp b/lib/rteng/include/Behaviour.hpp similarity index 100% rename from lib/rteng/include/comp/Behaviour.hpp rename to lib/rteng/include/Behaviour.hpp diff --git a/lib/rteng/include/rteng.hpp b/lib/rteng/include/rteng.hpp index 537acc0c..f643ab89 100644 --- a/lib/rteng/include/rteng.hpp +++ b/lib/rteng/include/rteng.hpp @@ -3,11 +3,17 @@ #include #include +#include "Behaviour.hpp" #include "ECS.hpp" #include "MonoBehaviour.hpp" namespace rteng { +template +struct ComponentsList +{ +}; + /** * @class GameEngine * @brief A class used to wrap and run an ecs. @@ -20,7 +26,10 @@ class GameEngine * @tparam Components The list of components to create the @code ecs@endcode with. */ template - explicit GameEngine(); + explicit GameEngine(ComponentsList) + { + _ecs = rtecs::ECS::createWithComponents(); + } /** * @brief Registers a new entity into the @code ecs@endcode. @@ -31,7 +40,25 @@ class GameEngine */ template rtecs::EntityID registerEntity(const std::shared_ptr& mono_behaviour, - Components&&... components); + Components&&... components) + { + const rtecs::EntityID entityId = _ecs->registerEntity...>( + std::forward(components)...); + + if (!mono_behaviour || !_ecs->hasEntityComponent(entityId)) { + return entityId; + } + auto& behaviourComponents = _ecs->getComponent(); + auto& behaviourSparseSet = + dynamic_cast&>(behaviourComponents); + + comp::Behaviour behaviourComp; + behaviourComp.instance = mono_behaviour; + behaviourComp.started = false; + + behaviourSparseSet.put(entityId, behaviourComp); + return entityId; + } /** * @brief Runs one round of the game loop and apply all registered systems. diff --git a/lib/rteng/src/rteng.cpp b/lib/rteng/src/rteng.cpp index 1fcfa564..1747c1c4 100644 --- a/lib/rteng/src/rteng.cpp +++ b/lib/rteng/src/rteng.cpp @@ -1,39 +1,10 @@ #include "rteng.hpp" +#include "Behaviour.hpp" #include "SparseSet.hpp" -#include "comp/Behaviour.hpp" namespace rteng { -template -GameEngine::GameEngine() -{ - _ecs = rtecs::ECS::createWithComponents(); -} - -template -rtecs::EntityID GameEngine::registerEntity( - const std::shared_ptr& mono_behaviour, - Components&&... components) -{ - const rtecs::EntityID entityId = - _ecs->registerEntity...>(std::forward(components)...); - - if (!mono_behaviour || !_ecs->hasEntityComponent(entityId)) { - return entityId; - } - auto& behaviourComponents = _ecs->getComponent(); - auto& behaviourSparseSet = - dynamic_cast&>(behaviourComponents); - - comp::Behaviour behaviourComp; - behaviourComp.instance = mono_behaviour; - behaviourComp.started = false; - - behaviourSparseSet.put(entityId, behaviourComp); - return entityId; -} - void GameEngine::runOnce(const double dt) const { {