diff --git a/.gitignore b/.gitignore index 0032665ce..0109284ae 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,7 @@ vendors/directxtk/MakeSpriteFont/obj/Debug/MakeSpriteFont.csproj.AssemblyReferen vendors/directxtk/MakeSpriteFont/obj/Release/MakeSpriteFont.csproj.AssemblyReference.cache /vendors/directxtk/MakeSpriteFont/obj /vendors/directxtk/MakeSpriteFont/obj + +# MafiaHub Services build outputs +code/framework/src/services/lib/Debug/ +code/framework/src/services/lib/Release/ diff --git a/code/framework/CMakeLists.txt b/code/framework/CMakeLists.txt index d1cd75388..24eef1109 100644 --- a/code/framework/CMakeLists.txt +++ b/code/framework/CMakeLists.txt @@ -19,14 +19,15 @@ set(FRAMEWORK_SRC src/external/sentry/wrapper.cpp - src/world/engine.cpp - src/world/server.cpp - src/world/client.cpp - src/world/modules/modules_impl.cpp - src/networking/network_peer.cpp src/networking/errors.cpp + src/networking/replication/network_entity.cpp + src/networking/replication/entity_registry.cpp + src/networking/replication/interest_grid.cpp + src/networking/replication/replication_manager.cpp + src/networking/replication/replication_connection.cpp + # JavaScript scripting engine base and builtins (used by both client and server) # Note: node_engine.cpp and v8_engine.cpp are in OBJECT libraries due to special compile flags src/scripting/engine.cpp @@ -91,6 +92,7 @@ list(APPEND FRAMEWORK_CLIENT_SRC src/gui/sdk.cpp src/gui/backend/view_d3d11.cpp src/gui/cef/app.cpp + src/gui/cef/renderer_app.cpp src/gui/cef/client.cpp src/gui/cef/render_handler.cpp src/gui/cef/life_span_handler.cpp @@ -174,8 +176,8 @@ macro(link_shared_deps target_name) ${CMAKE_SOURCE_DIR}/vendors/json/include ${CMAKE_SOURCE_DIR}/vendors/spdlog/include ${CMAKE_SOURCE_DIR}/vendors/fmt/include - ${CMAKE_SOURCE_DIR}/vendors/fu2 # function2 (used in world/modules/base.hpp) - ${CMAKE_SOURCE_DIR}/vendors/mafianet/Source/include # Networking / MafiaNet (used in messages.h) + ${CMAKE_SOURCE_DIR}/vendors/fu2 # function2 (used in network_peer.h) + ${CMAKE_SOURCE_DIR}/vendors/mafianet/Source/include # Networking / MafiaNet (used in connection.h) ${CMAKE_SOURCE_DIR}/vendors/cxxopts # Command-line parsing (used in integrations) ${CMAKE_SOURCE_DIR}/vendors # flecs, etc. ) @@ -201,7 +203,7 @@ macro(link_shared_deps target_name) endif() # Global libraries (v8/v8pp excluded - linked explicitly to scripting targets only) - target_link_libraries(${target_name} MafiaNet glm spdlog cppfs nlohmann_json Sentry httplib OpenSSL::SSL OpenSSL::Crypto Curl flecs_static semver Hash ftl) + target_link_libraries(${target_name} MafiaNet glm spdlog cppfs nlohmann_json Sentry httplib OpenSSL::SSL OpenSSL::Crypto Curl semver Hash ftl) # Required libraries for windows if(WIN32) diff --git a/code/framework/src/core_modules.h b/code/framework/src/core_modules.h index 44b8b68a1..ae866dc11 100644 --- a/code/framework/src/core_modules.h +++ b/code/framework/src/core_modules.h @@ -16,9 +16,9 @@ namespace Framework::Networking { class NetworkPeer; } // namespace Framework::Networking -namespace Framework::World { - class Engine; -} // namespace Framework::World +namespace Framework::Networking::Replication { + class ReplicationManager; +} // namespace Framework::Networking::Replication namespace Framework::GUI { @@ -47,7 +47,7 @@ namespace Framework { public: static void Reset() noexcept { _networkPeer = nullptr; - _engine = nullptr; + _replication = nullptr; _scriptingModule = nullptr; _webManager = nullptr; _input = nullptr; @@ -59,9 +59,9 @@ namespace Framework { _networkPeer = peer; } - static void SetWorldEngine(World::Engine *engine) { - FW_ASSERT_MODULE_REGISTRATION(_engine, engine, "WorldEngine"); - _engine = engine; + static void SetReplication(Networking::Replication::ReplicationManager *replication) { + FW_ASSERT_MODULE_REGISTRATION(_replication, replication, "Replication"); + _replication = replication; } static void SetScriptingModule(Scripting::ScriptingModule *module) { @@ -88,8 +88,8 @@ namespace Framework { return _networkPeer; } - static World::Engine *GetWorldEngine() noexcept { - return _engine; + static Networking::Replication::ReplicationManager *GetReplication() noexcept { + return _replication; } static Scripting::ScriptingModule *GetScriptingModule() noexcept { @@ -110,7 +110,7 @@ namespace Framework { private: static inline Networking::NetworkPeer *_networkPeer {}; - static inline World::Engine *_engine {}; + static inline Networking::Replication::ReplicationManager *_replication {}; static inline Scripting::ScriptingModule *_scriptingModule {}; static inline GUI::Manager *_webManager {}; static inline Input::IInput *_input {}; diff --git a/code/framework/src/external/discord/wrapper.cpp b/code/framework/src/external/discord/wrapper.cpp index 88ea45e14..e9e3d7dbd 100644 --- a/code/framework/src/external/discord/wrapper.cpp +++ b/code/framework/src/external/discord/wrapper.cpp @@ -85,6 +85,11 @@ namespace Framework::External::Discord { }); } + std::string Wrapper::GetUserId() const { + const auto id = _user.GetId(); + return id ? std::to_string(id) : std::string {}; + } + discord::UserManager &Wrapper::GetUserManager() const { return _instance->UserManager(); } diff --git a/code/framework/src/external/discord/wrapper.h b/code/framework/src/external/discord/wrapper.h index ecef51670..6995142e2 100644 --- a/code/framework/src/external/discord/wrapper.h +++ b/code/framework/src/external/discord/wrapper.h @@ -35,6 +35,9 @@ namespace Framework::External::Discord { void SignInWithDiscord(const DiscordLoginProc &proc) const; + // Snowflake of the signed-in user once OnCurrentUserUpdate has fired, empty otherwise. + std::string GetUserId() const; + discord::ActivityManager &GetActivityManager() const; discord::UserManager &GetUserManager() const; discord::ImageManager &GetImageManager() const; diff --git a/code/framework/src/gui/cef/app.h b/code/framework/src/gui/cef/app.h index 210353961..d19c6d87a 100644 --- a/code/framework/src/gui/cef/app.h +++ b/code/framework/src/gui/cef/app.h @@ -8,6 +8,8 @@ #pragma once +#include "renderer_app.h" + #include "include/cef_app.h" #include "include/cef_browser_process_handler.h" @@ -46,9 +48,15 @@ namespace Framework::GUI::CEF { return this; } + CefRefPtr GetRenderProcessHandler() override { + return this; + } + void OnBeforeCommandLineProcessing(const CefString &processType, CefRefPtr commandLine) override; void OnContextInitialized() override; + void OnContextCreated(CefRefPtr browser, CefRefPtr frame, CefRefPtr context) override; + bool IsContextInitialized() const { return _contextInitialized; } diff --git a/code/framework/src/integrations/client/instance.cpp b/code/framework/src/integrations/client/instance.cpp index 1be09908f..a636abc8c 100644 --- a/code/framework/src/integrations/client/instance.cpp +++ b/code/framework/src/integrations/client/instance.cpp @@ -8,23 +8,18 @@ #include "instance.h" -#include -#include -#include -#include -#include -#include - -#include - #include "integrations/shared/rpc/emit_lua_event.h" -#include "../shared/modules/mod.hpp" +#include "networking/rpc/rpc.h" +#include "networking/rpc/chat_message.h" +#include "networking/rpc/client_identity.h" +#include "networking/rpc/server_resources.h" #include "scripting/resource/resource_manager.h" #include "scripting/builtins/events.h" #include "networking/state.h" +#include "networking/replication/replication_manager.h" #include #include @@ -44,6 +39,68 @@ #include "graphics/backend/d3d9.h" namespace Framework::Integrations::Client { + namespace { + // Handler for server-emitted scripting events; reaches the scripting engine through the + // CoreModules singleton. + void OnEmitLuaEvent(const Shared::RPC::EmitLuaEvent &rpc, MafiaNet::Packet *packet) { + (void)packet; + const auto eventName = rpc.GetEventName(); + if (eventName.empty()) { + return; + } + const auto payloadStr = rpc.GetPayload(); + + auto *scriptingModule = static_cast(Framework::CoreModules::GetScriptingModule()); + if (!scriptingModule) { + return; + } + + // Emit to JavaScript resources via the Events system + auto resourceManager = scriptingModule->GetResourceManager(); + if (!resourceManager) { + return; + } + + auto *engine = scriptingModule->GetEngine(); + if (!engine || !engine->IsInitialized()) { + return; + } + + v8::Isolate *isolate = engine->GetIsolate(); + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Local context = engine->GetContext(); + v8::Context::Scope contextScope(context); + + // Parse JSON payload and emit event + std::vector> args; + if (!payloadStr.empty()) { + v8::Local jsonStr; + if (!v8::String::NewFromUtf8(isolate, payloadStr.c_str()).ToLocal(&jsonStr)) { + Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->error("Failed to create V8 string from event payload: {}", payloadStr); + return; + } + + v8::TryCatch tryCatch(isolate); + v8::Local parsed; + if (!v8::JSON::Parse(context, jsonStr).ToLocal(&parsed)) { + if (tryCatch.HasCaught()) { + v8::String::Utf8Value errorMsg(isolate, tryCatch.Exception()); + Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->error("Failed to parse event payload JSON: {}", *errorMsg ? *errorMsg : "unknown error"); + } + else { + Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->error("Failed to parse event payload JSON: {}", payloadStr); + } + return; + } + args.push_back(parsed); + } + + resourceManager->GetEvents().EmitReserved(isolate, context, eventName, args); + } + } // namespace + bool AssetDownloadFileProgress::OnFile(MafiaNet::FileListTransferCBInterface::OnFileStruct *onFileStruct) { if (onFileStruct->numberOfFilesInThisSet > 0) { auto &downloadStatus = _instance->GetAssetDownloadStatus(); @@ -77,11 +134,10 @@ namespace Framework::Integrations::Client { _presence = std::make_unique(); _imguiApp = std::make_unique(); _renderer = std::make_unique(); - _worldEngine = std::make_shared(); _renderIO = std::make_unique(); _playerFactory = std::make_unique(); _streamingFactory = std::make_unique(); - _scriptingModule = std::make_unique(_worldEngine); + _scriptingModule = std::make_unique(); _webManager = std::make_unique(); } @@ -115,18 +171,6 @@ namespace Framework::Integrations::Client { Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->info("Networking engine initialized"); } - if (_worldEngine) { - if (_worldEngine->Init() != World::WorldError::WORLD_NONE) { - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->error("World engine failed to initialize"); - return ClientError::CLIENT_ENGINES_ERROR; - } - CoreModules::SetWorldEngine(_worldEngine.get()); - - _worldEngine->GetWorld()->import (); - - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->info("Core ecs modules have been imported!"); - } - CoreModules::SetWebManager(_webManager.get()); InitNetworkingMessages(); @@ -234,14 +278,10 @@ namespace Framework::Integrations::Client { _imguiApp->Shutdown(); } - if (_worldEngine) { - _worldEngine->Shutdown(); - } - CoreModules::SetScriptingModule(nullptr); CoreModules::SetWebManager(nullptr); CoreModules::SetNetworkPeer(nullptr); - CoreModules::SetWorldEngine(nullptr); + CoreModules::SetReplication(nullptr); CoreModules::SetInput(nullptr); CoreModules::Reset(); @@ -261,10 +301,6 @@ namespace Framework::Integrations::Client { _scriptingModule->Update(); } - if (_worldEngine) { - _worldEngine->Update(); - } - if (_imguiApp && _imguiApp->IsInitialized()) { _imguiApp->Update(); } @@ -293,85 +329,67 @@ namespace Framework::Integrations::Client { } void Instance::InitNetworkingMessages() { - using namespace Framework::Networking::Messages; const auto net = _networkingEngine->GetNetworkClient(); - net->SetOnPlayerConnectedCallback([this, net](MafiaNet::Packet *packet) { - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Connection accepted by server, sending handshake"); - - ClientHandshake msg; - msg.FromParameters(_opts.modVersion, Utils::Version::rel, _opts.gameVersion, _opts.gameName); + // Build gate: NetworkClient challenges automatically on connect; a mismatch drops us. + net->SetBuildToken(Framework::Networking::NetworkPeer::BuildToken(_opts.gameName, _opts.gameVersion, Utils::Version::rel, _opts.modVersion)); - net->Send(msg, MafiaNet::UNASSIGNED_RAKNET_GUID); + net->SetOnPlayerConnectedCallback([](MafiaNet::Packet *) { + Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Connection accepted by server, verifying build"); }); - net->RegisterMessage(GameMessages::GAME_CONNECTION_READY_ASSETS, [this, net](MafiaNet::RakNetGUID _guid, ClientReadyAssets *msg) { - // Store resource list on instance (survives scripting module reset) - if (msg->GetResourceCount() > 0) { - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Received resource list from server with {} resources", msg->GetResourceCount()); - - _pendingServerResources.clear(); - _pendingServerResources.reserve(msg->GetResourceCount()); - for (const auto &resInfo : msg->GetResources()) { - Client::Scripting::ServerResourceInfo info; - info.name = resInfo.name; - info.version = resInfo.version; - info.hash = resInfo.hash; - _pendingServerResources.push_back(info); - } + + // Server's resource list. Store it (survives a scripting module reset) and start the asset + // phase; the ready-event id and tick rate are held until the spawn barrier completes. + net->RegisterRPC([this](const Framework::Networking::RPC::ServerResources &payload, MafiaNet::Packet *) { + _readyEventId = payload.readyEventId; + _serverTickRate = payload.tickRate; + + _pendingServerResources.clear(); + _pendingServerResources.reserve(payload.resources.size()); + for (const auto &resInfo : payload.resources) { + Client::Scripting::ServerResourceInfo info; + info.name = resInfo.name; + info.version = resInfo.version; + _pendingServerResources.push_back(info); } + Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Received resource list from server with {} resources", _pendingServerResources.size()); DownloadsAssetsFromConnectedServer(); }); - net->RegisterMessage(GameMessages::GAME_CONNECTION_FINALIZED, [this, net](MafiaNet::RakNetGUID _guid, ClientConnectionFinalized *msg) { - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Connection request finalized"); - _worldEngine->OnConnect(net, msg->GetServerTickRate()); - const auto guid = GetNetworkingEngine()->GetNetworkClient()->GetPeer()->GetMyGUID(); - - const auto newPlayer = GetWorldEngine()->CreateEntity(msg->GetEntityID()); - GetStreamingFactory()->SetupClient(newPlayer, guid.g); - GetPlayerFactory()->SetupClient(newPlayer, guid.g); - - // Notify server we are ready to obtain player data - Framework::Networking::Messages::ClientInitPlayer initPlayer {}; - net->Send(initPlayer, MafiaNet::UNASSIGNED_RAKNET_GUID); - // Notify mod-level that network integration whole process succeeded + // Spawn barrier complete: activate replication and report the connection final. + net->SetOnConnectionReadyCallback([this, net](int eventId) { + Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Connection ready (event {}), finalizing", eventId); + // tickInterval is in seconds; SetAutoSerializeInterval wants milliseconds. + if (auto *replication = net->GetReplicationManager()) { + CoreModules::SetReplication(replication); + replication->SetAutoSerializeInterval(static_cast(_serverTickRate * 1000.0f)); + } if (_onConnectionFinalized) { - _onConnectionFinalized(newPlayer, msg->GetServerTickRate()); + _onConnectionFinalized(_serverTickRate); } }); - net->RegisterMessage(GameMessages::GAME_CONNECTION_KICKED, [](MafiaNet::RakNetGUID guid, ClientKick *msg) { - std::string reason = "Unknown."; - switch (msg->GetDisconnectionReason()) { - case Framework::Networking::Messages::DisconnectionReason::BANNED: reason = "You are banned."; break; - case Framework::Networking::Messages::DisconnectionReason::KICKED: reason = "You have been kicked."; break; - case Framework::Networking::Messages::DisconnectionReason::KICKED_CUSTOM: reason = "You have been kicked. Reason: " + msg->GetCustomReason(); break; - case Framework::Networking::Messages::DisconnectionReason::KICKED_INVALID_PACKET: reason = "You have been kicked (invalid packet)."; break; - case Framework::Networking::Messages::DisconnectionReason::WRONG_VERSION: reason = "You have been kicked (wrong client version)."; break; - case Framework::Networking::Messages::DisconnectionReason::INVALID_PASSWORD: reason = "You have been kicked (wrong password)."; break; + // Version mismatches don't reach here — they fail the build challenge (WRONG_VERSION). + net->SetOnPlayerDisconnectedCallback([this](MafiaNet::Packet *packet, Framework::Networking::DisconnectionReason reasonId, const std::string &customReason) { + std::string reason = "Unknown."; + switch (reasonId) { + case Framework::Networking::DisconnectionReason::BANNED: reason = "You are banned."; break; + case Framework::Networking::DisconnectionReason::KICKED: reason = "You have been kicked."; break; + case Framework::Networking::DisconnectionReason::KICKED_CUSTOM: reason = "You have been kicked. Reason: " + customReason; break; + case Framework::Networking::DisconnectionReason::KICKED_INVALID_PACKET: reason = "You have been kicked (invalid packet)."; break; + case Framework::Networking::DisconnectionReason::WRONG_VERSION: reason = "You have been kicked (wrong client version)."; break; + case Framework::Networking::DisconnectionReason::INVALID_PASSWORD: reason = "You have been kicked (wrong password)."; break; default: break; } Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Connection dropped: {}", reason); - }); - net->RegisterGameRPC([this](MafiaNet::RakNetGUID guid, Framework::World::RPC::SetTransform *msg) { - if (!msg->Valid()) { - return; - } - const auto e = GetWorldEngine()->GetEntityByServerID(msg->GetServerID()); - if (!e.is_alive()) { - return; - } - const auto tr = e.try_get_mut(); - *tr = msg->GetTransform(); - }); - net->SetOnPlayerDisconnectedCallback([this](MafiaNet::Packet *packet, Framework::Networking::Messages::DisconnectionReason reasonId) { // Reset initial asset download state _initialDownloadDone = false; _downloadStatus = {}; - // Request the world engine to clean up entities - _worldEngine->OnDisconnect(); + // Entity teardown is native: ReplicaManager3 deletes server-created replicas when the + // connection drops (QueryActionOnPopConnection_Client). + CoreModules::SetReplication(nullptr); // Notify mod-level that network integration got closed if (_onConnectionClosed) { @@ -391,59 +409,26 @@ namespace Framework::Integrations::Client { } }); - net->RegisterRPC([this](MafiaNet::RakNetGUID guid, Shared::RPC::EmitLuaEvent *rpc) { - if (!rpc->Valid()) - return; - const auto eventName = rpc->GetEventName(); - const auto payloadStr = rpc->GetPayload(); + net->RegisterRPC(&OnEmitLuaEvent); - // Emit to JavaScript resources via the Events system - auto resourceManager = _scriptingModule->GetResourceManager(); - if (!resourceManager) { - return; - } - - auto *engine = _scriptingModule->GetEngine(); - if (!engine || !engine->IsInitialized()) { - return; - } - - v8::Isolate *isolate = engine->GetIsolate(); - v8::Locker locker(isolate); - v8::Isolate::Scope isolateScope(isolate); - v8::HandleScope handleScope(isolate); - v8::Local context = engine->GetContext(); - v8::Context::Scope contextScope(context); - - // Parse JSON payload and emit event - std::vector> args; - if (!payloadStr.empty()) { - v8::Local jsonStr; - if (!v8::String::NewFromUtf8(isolate, payloadStr.c_str()).ToLocal(&jsonStr)) { - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->error("Failed to create V8 string from event payload: {}", payloadStr); - return; - } - - v8::TryCatch tryCatch(isolate); - v8::Local parsed; - if (!v8::JSON::Parse(context, jsonStr).ToLocal(&parsed)) { - if (tryCatch.HasCaught()) { - v8::String::Utf8Value errorMsg(isolate, tryCatch.Exception()); - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->error("Failed to parse event payload JSON: {}", *errorMsg ? *errorMsg : "unknown error"); - } else { - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->error("Failed to parse event payload JSON: {}", payloadStr); - } - return; - } - args.push_back(parsed); - } - - resourceManager->GetEvents().EmitReserved(isolate, context, eventName, args); + // Chat lines from the server are forwarded to the mod's UI via the received callback. + net->RegisterRPC([this](const Framework::Networking::RPC::ChatMessage &payload, MafiaNet::Packet *) { + DispatchReceivedChat(payload.text); }); - Framework::World::Modules::Base::SetupClientReceivers(net, _worldEngine.get(), _streamingFactory.get()); + Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Networking messages registered"); + } - Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->debug("Game sync networking messages registered"); + void Instance::SendChatMessage(const std::string &text) { + if (text.empty()) { + return; + } + const auto net = GetNetworkingEngine()->GetNetworkClient(); + if (!net) { + return; + } + Framework::Networking::RPC::ChatMessage payload {text}; + net->BroadcastRPC(payload); } void Instance::DownloadsAssetsFromConnectedServer() { @@ -545,14 +530,22 @@ namespace Framework::Integrations::Client { } Logging::GetLogger(FRAMEWORK_INNER_CLIENT)->flush(); - // Send the server a request to initialise our client and assign a streamer - // but only do so the first time we connect to the server + // Announce ourselves (server builds the avatar and opens the replication gate), then arm our + // half of the spawn barrier. First connect only. if (!_initialDownloadDone) { _initialDownloadDone = true; - Framework::Networking::Messages::ClientRequestStreamer req; - req.FromParameters(_currentState.nickname, "MY_SUPER_ID_1", "MY_SUPER_ID_2", Framework::Utils::GetHardwareId()); - net->Send(req, MafiaNet::UNASSIGNED_RAKNET_GUID); + const auto serverGuid = net->GetPeer()->GetGUIDFromIndex(0); + + Framework::Networking::RPC::ClientIdentity identity; + identity.name = _currentState.nickname; + identity.steamId = ""; // no Steam integration wired into the client yet + identity.discordId = _presence ? _presence->GetUserId() : ""; + identity.hardwareId = Framework::Utils::GetHardwareId(); + net->SendRPC(identity, serverGuid); + + net->GetReadyEvent()->AddToWaitList(_readyEventId, serverGuid); + net->GetReadyEvent()->SetEvent(_readyEventId, true); } _downloadStatus = {}; diff --git a/code/framework/src/integrations/client/instance.h b/code/framework/src/integrations/client/instance.h index 191f112a3..64c1feade 100644 --- a/code/framework/src/integrations/client/instance.h +++ b/code/framework/src/integrations/client/instance.h @@ -22,7 +22,6 @@ #include #include -#include #include "scripting/module.h" @@ -33,12 +32,13 @@ #include -#include - namespace Framework::Integrations::Client { - using NetworkConnectionFinalizedCallback = fu2::function; + // Fired once the connection handshake completes, with the server tick rate. + using NetworkConnectionFinalizedCallback = fu2::function; using NetworkConnectionClosedCallback = fu2::function; using AssetsDownloadFinishedCallback = fu2::function; + // Fired when the server sends a chat line to display. + using NetworkChatMessageCallback = fu2::function; class Instance; @@ -94,7 +94,6 @@ namespace Framework::Integrations::Client { std::unique_ptr _networkingEngine; std::unique_ptr _presence; std::unique_ptr _renderer; - std::shared_ptr _worldEngine; std::unique_ptr _renderIO; std::unique_ptr _scriptingModule; std::unique_ptr _webManager; @@ -107,6 +106,7 @@ namespace Framework::Integrations::Client { NetworkConnectionFinalizedCallback _onConnectionFinalized; NetworkConnectionClosedCallback _onConnectionClosed; AssetsDownloadFinishedCallback _onAssetsDownloadFinished; + NetworkChatMessageCallback _onChatMessageReceived; // Entity factories std::unique_ptr _playerFactory; @@ -120,6 +120,10 @@ namespace Framework::Integrations::Client { // Pending resources from server (stored here to survive scripting module reset) std::vector _pendingServerResources; + // Handshake state carried from ServerResources until the ReadyEvent spawn barrier completes. + int _readyEventId {}; + float _serverTickRate {}; + void InitNetworkingMessages(); void InitAssetDownloader(); void OnAssetsDownloaded(bool success); @@ -174,12 +178,22 @@ namespace Framework::Integrations::Client { _onAssetsDownloadFinished = std::move(cb); } - Networking::Engine *GetNetworkingEngine() const { - return _networkingEngine.get(); + void SetOnChatMessageReceivedCallback(NetworkChatMessageCallback cb) { + _onChatMessageReceived = std::move(cb); } - World::ClientEngine *GetWorldEngine() const { - return _worldEngine.get(); + // Invoked by the chat RPC handler with a line received from the server. + void DispatchReceivedChat(const std::string &text) const { + if (_onChatMessageReceived) { + _onChatMessageReceived(text); + } + } + + // Send a chat line to the server (the local player's outgoing text). + void SendChatMessage(const std::string &text); + + Networking::Engine *GetNetworkingEngine() const { + return _networkingEngine.get(); } External::Discord::Wrapper *GetPresence() const { diff --git a/code/framework/src/integrations/client/scripting/module.cpp b/code/framework/src/integrations/client/scripting/module.cpp index 19d0e5e78..b0793d7c9 100644 --- a/code/framework/src/integrations/client/scripting/module.cpp +++ b/code/framework/src/integrations/client/scripting/module.cpp @@ -35,8 +35,7 @@ namespace Framework::Integrations::Client::Scripting { } } // anonymous namespace - ClientScriptingModule::ClientScriptingModule(std::shared_ptr world) - : _world(world) { + ClientScriptingModule::ClientScriptingModule() { // Create standalone V8 engine for client (no Node.js runtime) // moduleRootPath is set later in Init() or SetResourceCachePath() // when the actual resource cache path is known. @@ -89,7 +88,7 @@ namespace Framework::Integrations::Client::Scripting { config.cascadeStopDependents = true; _resourceManager = std::make_unique( - _engine.get(), _world->GetWorld(), config); + _engine.get(), config); // Register Framework SDK bindings for the new ResourceManager RegisterFrameworkBindings(); diff --git a/code/framework/src/integrations/client/scripting/module.h b/code/framework/src/integrations/client/scripting/module.h index 949fce04a..7f2cd11dc 100644 --- a/code/framework/src/integrations/client/scripting/module.h +++ b/code/framework/src/integrations/client/scripting/module.h @@ -18,7 +18,6 @@ #include #include #include -#include namespace Framework::Integrations::Client::Scripting { @@ -28,7 +27,6 @@ namespace Framework::Integrations::Client::Scripting { struct ServerResourceInfo { std::string name; std::string version; - uint32_t hash = 0; }; /** @@ -49,7 +47,7 @@ namespace Framework::Integrations::Client::Scripting { */ class ClientScriptingModule final : public Framework::Lifecycle, public Framework::Scripting::ScriptingModule { public: - explicit ClientScriptingModule(std::shared_ptr world); + ClientScriptingModule(); ~ClientScriptingModule(); /** @@ -89,12 +87,6 @@ namespace Framework::Integrations::Client::Scripting { return _engine.get(); } - /** - * Get the world engine. - */ - std::shared_ptr GetWorldEngine() const { - return _world; - } /** * Get the JavaScript resource manager. @@ -185,7 +177,6 @@ namespace Framework::Integrations::Client::Scripting { private: std::unique_ptr _engine; - std::shared_ptr _world; std::unique_ptr _resourceManager; // Resource synchronization state diff --git a/code/framework/src/integrations/server/instance.cpp b/code/framework/src/integrations/server/instance.cpp index d60cfb5ec..db8d0f94e 100644 --- a/code/framework/src/integrations/server/instance.cpp +++ b/code/framework/src/integrations/server/instance.cpp @@ -10,22 +10,21 @@ #include #include +#include #include "core_modules.h" -#include "world/server.h" -#include "networking/messages/client_connection_finalized.h" -#include "networking/messages/client_handshake.h" -#include "networking/messages/client_initialise_player.h" -#include "networking/messages/client_kick.h" -#include "networking/messages/client_ready_assets.h" -#include "networking/messages/client_request_streamer.h" -#include "networking/messages/messages.h" +#include "networking/replication/network_entity.h" +#include "networking/replication/replication_manager.h" +#include "networking/rpc/chat_message.h" +#include "networking/rpc/client_identity.h" +#include "networking/rpc/server_resources.h" + +#include "networking/connection.h" #include "utils/command_processor.h" #include "utils/path.h" #include "utils/version.h" -#include "../shared/modules/mod.hpp" #include "cxxopts.hpp" #include @@ -33,12 +32,12 @@ #include namespace Framework::Integrations::Server { + Instance::Instance(): _shuttingDown(false) { _networkingEngine = std::make_unique(); _webServer = std::make_unique(); _fileConfig = std::make_unique(); - _worldEngine = std::make_shared(); - _scriptingModule = std::make_unique(_worldEngine); + _scriptingModule = std::make_unique(); _playerFactory = std::make_unique(); _streamingFactory = std::make_unique(); _masterlist = std::make_unique(); @@ -107,14 +106,21 @@ namespace Framework::Integrations::Server { CoreModules::SetNetworkPeer(_networkingEngine->GetNetworkServer()); - // Initialize the world - if (_worldEngine->Init(_networkingEngine->GetNetworkServer(), _opts.worldConfig) != World::WorldError::WORLD_NONE) { - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->critical("Failed to initialize the world engine"); - return ServerError::SERVER_WORLD_INIT_FAILED; + // The networked world is the replication manager owned by the peer. Serialize entity updates + // at the configured tick rate (tickInterval is in seconds). + auto *replication = _networkingEngine->GetNetworkServer()->GetReplicationManager(); + CoreModules::SetReplication(replication); + if (replication) { + replication->SetAutoSerializeInterval(static_cast(_opts.worldConfig.tickInterval * 1000.0f)); + // Replication owns connection teardown: when a peer drops, it notifies the game (avatar + // still resolvable) just before destroying and broadcasting the destruction of the avatar. + replication->SetOnClientDisconnect([this](uint64_t guid) { + if (_onPlayerDisconnectCallback) { + _onPlayerDisconnectCallback(guid); + } + }); } - CoreModules::SetWorldEngine(_worldEngine.get()); - if (!_opts.bindPublicServer || !_masterlist->Init(_opts.services.apiUrl, _opts.services.masterlistUrl, _opts.bindSecretKey)) { Logging::GetLogger(FRAMEWORK_INNER_SERVER)->warn("Server will not be announced to masterlist"); } @@ -128,9 +134,6 @@ namespace Framework::Integrations::Server { // Register the default endpoints InitEndpoints(); - // Register built in modules - InitModules(); - // Initialize default messages InitNetworkingMessages(); @@ -153,6 +156,17 @@ namespace Framework::Integrations::Server { CoreModules::SetScriptingModule(_scriptingModule.get()); + // A resource owns the entities it spawns; they are destroyed when it stops. + if (replication) { + auto *resourceManager = _scriptingModule->GetResourceManager(); + replication->SetOnEntityCreated([resourceManager](uint64_t networkId) { + resourceManager->OnEntityCreated(networkId); + }); + replication->SetOnEntityDestroyed([resourceManager](uint64_t networkId) { + resourceManager->OnEntityDestroyed(networkId); + }); + } + PostScriptInit(); // Discover resources @@ -200,15 +214,6 @@ namespace Framework::Integrations::Server { Logging::GetLogger(FRAMEWORK_INNER_HTTP)->debug("All core endpoints have been registered!"); } - void Instance::InitModules() const { - if (_worldEngine) { - const auto world = _worldEngine->GetWorld(); - - world->import (); - } - - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->debug("Core ecs modules have been imported!"); - } bool Instance::LoadConfigFromJSON() { const auto configHandle = cppfs::fs::open(_opts.modConfigFile); @@ -244,122 +249,124 @@ namespace Framework::Integrations::Server { } void Instance::InitNetworkingMessages() const { - using namespace Framework::Networking::Messages; const auto net = _networkingEngine->GetNetworkServer(); - net->RegisterMessage(Framework::Networking::Messages::GameMessages::GAME_CONNECTION_HANDSHAKE, [this, net](MafiaNet::RakNetGUID guid, ClientHandshake *msg) { - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->debug("Received handshake message for incoming player guid {}", guid.g); - - // Make sure handshake payload was correctly formatted - if (!msg->Valid()) { - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->error("Handshake payload was invalid, force-disconnecting peer"); - net->GetPeer()->CloseConnection(guid, true); - return; - } + // Build gate: a mismatched token fails the challenge inside NetworkServer; the peer never + // reaches the asset phase. + net->SetBuildToken(Framework::Networking::NetworkPeer::BuildToken(_opts.gameName, _opts.gameVersion, Utils::Version::rel, _opts.modVersion)); - if (msg->GetGameName() != _opts.gameName) { - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->error("Client has invalid game, force-disconnecting peer"); - Framework::Networking::Messages::ClientKick kick; - kick.FromParameters(Framework::Networking::Messages::DisconnectionReason::WRONG_VERSION); - net->Send(kick, guid); - net->GetPeer()->CloseConnection(guid, true); - return; - } - - const auto fwVersion = msg->GetFWVersion(); - - if (!Utils::Version::VersionSatisfies(fwVersion, Utils::Version::rel)) { - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->error("Client has invalid Framework version, force-disconnecting peer"); - Framework::Networking::Messages::ClientKick kick; - kick.FromParameters(Framework::Networking::Messages::DisconnectionReason::WRONG_VERSION); - net->Send(kick, guid); - net->GetPeer()->CloseConnection(guid, true); - return; - } - - const auto clientVersion = msg->GetClientVersion(); - - if (!Utils::Version::VersionSatisfies(clientVersion, _opts.modVersion)) { - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->error("Client has invalid version, force-disconnecting peer"); - Framework::Networking::Messages::ClientKick kick; - kick.FromParameters(Framework::Networking::Messages::DisconnectionReason::WRONG_VERSION); - net->Send(kick, guid); - net->GetPeer()->CloseConnection(guid, true); - return; - } - - const auto mpClientVersion = msg->GetGameVersion(); - - if (mpClientVersion != _opts.gameVersion) { - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->error("Client has invalid game version, force-disconnecting peer"); - Framework::Networking::Messages::ClientKick kick; - kick.FromParameters(Framework::Networking::Messages::DisconnectionReason::WRONG_VERSION); - net->Send(kick, guid); - net->GetPeer()->CloseConnection(guid, true); - return; - } + // Build verified -> send the resource list (carries the ReadyEvent id and tick rate). + net->SetOnClientAuthenticatedCallback([this, net](MafiaNet::RakNetGUID guid) { + Logging::GetLogger(FRAMEWORK_INNER_SERVER)->debug("Build verified for player guid {}, sending resource list", guid.g); - // Let the client know they can ask for client-side assets now. - // Include the resource list in the message. - ClientReadyAssets readyMsg; + Framework::Networking::RPC::ServerResources resources; + resources.readyEventId = Framework::Networking::NetworkServer::ReadyEventId(guid); + resources.tickRate = _opts.worldConfig.tickInterval; if (_scriptingModule) { for (const auto &resource : _scriptingModule->GetClientResourceList()) { - readyMsg.AddResource(resource.name, resource.version, 0); // Hash computed on demand + resources.resources.push_back({resource.name, resource.version}); } } - net->Send(readyMsg, guid); + net->SendRPC(resources, guid); }); - net->SetOnPlayerDisconnectCallback([this, net](MafiaNet::Packet *packet, Framework::Networking::Messages::DisconnectionReason reason) { + net->SetOnPlayerDisconnectCallback([net](MafiaNet::Packet *packet, Framework::Networking::DisconnectionReason reason, const std::string &) { const auto guid = packet->guid; Logging::GetLogger(FRAMEWORK_INNER_SERVER)->debug("Disconnecting peer {}, reason: {}", guid.g, static_cast(reason)); - const auto e = _worldEngine->GetEntityByGUID(guid.g); - if (e.is_valid()) { - if (_onPlayerDisconnectCallback) - _onPlayerDisconnectCallback(e, guid.g); - - _worldEngine->RemoveEntity(e); - } - + // Player notification and avatar teardown run in ReplicationManager::OnClosedConnection, + // which RakNet fires before this packet is delivered; here we just finalise the connection. net->GetPeer()->CloseConnection(guid, true); }); - - net->RegisterMessage(GameMessages::GAME_CONNECTION_REQUEST_STREAMER, [this, net](MafiaNet::RakNetGUID guid, ClientRequestStreamer *msg) { - // Create player entity and add on world - const auto newPlayer = _worldEngine->CreateEntity(); - _streamingFactory->SetupServer(newPlayer, guid.g); + // Client announces itself after assets. Gated on authentication so an unverified peer can't + // conjure an avatar by sending this directly. + net->RegisterRPC([this, net](const Framework::Networking::RPC::ClientIdentity &payload, MafiaNet::Packet *packet) { + const auto guid = packet->guid; + if (!net->IsAuthenticated(guid)) { + Logging::GetLogger(FRAMEWORK_INNER_SERVER)->warn("Ignoring identity from unauthenticated peer {}", guid.g); + return; + } - auto nickname = msg->GetPlayerName(); + auto nickname = payload.name; if (nickname.size() > 64) { nickname = nickname.substr(0, 64); } - auto hardwareId = msg->GetPlayerHardwareID(); - - _playerFactory->SetupServer(newPlayer, guid.g, guid.systemIndex, nickname, hardwareId); + Logging::GetLogger(FRAMEWORK_INNER_SERVER)->info("Player {} guid {} hwid {}", nickname, guid.g, payload.hardwareId); + + // The game builds the avatar and registers it as this connection's viewer; hand it the + // metadata so it spawns with the real nickname/slot. + if (_onPlayerConnectCallback) { + PlayerConnectionData data; + data.guid = guid.g; + data.playerIndex = guid.systemIndex; + data.nickname = nickname; + data.hardwareID = payload.hardwareId; + _onPlayerConnectCallback(data); + } - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->info("Player {} guid {} entity id {} hwid {}", msg->GetPlayerName(), guid.g, newPlayer.id(), hardwareId); + // Gate opens: this connection now starts receiving the replicated world. + net->PushReplicationConnection(guid); - // Send the connection finalized packet - Framework::Networking::Messages::ClientConnectionFinalized answer; - answer.FromParameters(_opts.worldConfig.tickInterval, newPlayer.id()); - net->Send(answer, guid); + // Arm our half of the spawn barrier (the client armed its half before sending identity). + const int eventId = Framework::Networking::NetworkServer::ReadyEventId(guid); + net->GetReadyEvent()->AddToWaitList(eventId, guid); + net->GetReadyEvent()->SetEvent(eventId, true); }); - net->RegisterMessage(Framework::Networking::Messages::GameMessages::GAME_INIT_PLAYER, [this, net](MafiaNet::RakNetGUID guid, ClientInitPlayer *stub) { - const auto e = _worldEngine->GetEntityByGUID(guid.g); - if (_onPlayerConnectCallback && e.is_valid() && e.is_alive()) - _onPlayerConnectCallback(e, guid.g); + // Incoming chat from clients. Sender resolution + command parsing happen here; the mod + // observes via SetOnChatMessageCallback / SetOnChatCommandCallback. + net->RegisterRPC([this](const Framework::Networking::RPC::ChatMessage &payload, MafiaNet::Packet *packet) { + if (payload.text.empty()) { + return; + } + // Resolve the sender from its connection's viewer entity. + auto *engine = GetNetworkingEngine(); + auto *server = engine ? engine->GetNetworkServer() : nullptr; + auto *repl = server ? server->GetReplicationManager() : nullptr; + auto *sender = repl ? repl->GetViewer(packet->guid.g) : nullptr; + if (!sender) { + return; + } + HandleIncomingChat(sender->GetNetworkID(), payload.text); }); // Note: Client-to-server events are handled through the JS Events system // The client can emit events via Framework.events.emitToServer() which uses // the networking messages system to send events to the server - Framework::World::Modules::Base::SetupServerReceivers(net, _worldEngine.get()); + Logging::GetLogger(FRAMEWORK_INNER_SERVER)->debug("Networking messages registered"); + } - Logging::GetLogger(FRAMEWORK_INNER_SERVER)->debug("Game sync networking messages registered"); + void Instance::HandleIncomingChat(uint64_t senderNetworkId, const std::string &text) const { + if (text.empty()) { + return; + } + if (text[0] == '/') { + std::string command, argsPart; + const auto space = text.find(' '); + if (space != std::string::npos) { + command = text.substr(1, space - 1); + argsPart = text.substr(space + 1); + } + else { + command = text.substr(1); + } + std::vector args; + std::string arg; + std::istringstream iss(argsPart); + while (iss >> arg) { + args.push_back(arg); + } + if (_onChatCommandCallback) { + _onChatCommandCallback(senderNetworkId, text, command, args); + } + } + else { + if (_onChatMessageCallback) { + _onChatMessageCallback(senderNetworkId, text); + } + } } void Instance::InitAssetStreamer() { @@ -520,10 +527,6 @@ namespace Framework::Integrations::Server { _webServer->Shutdown(); } - if (_worldEngine) { - _worldEngine->Shutdown(); - } - if (_commandListener) { _commandListener->Shutdown(); } @@ -533,7 +536,7 @@ namespace Framework::Integrations::Server { sig_detach(SIGTERM, sig_slot(this, &Instance::OnSignal)); CoreModules::SetNetworkPeer(nullptr); - CoreModules::SetWorldEngine(nullptr); + CoreModules::SetReplication(nullptr); CoreModules::SetScriptingModule(nullptr); CoreModules::Reset(); @@ -551,10 +554,6 @@ namespace Framework::Integrations::Server { _scriptingModule->Update(); } - if (_worldEngine) { - _worldEngine->Update(); - } - if (_commandListener) { _commandListener->Update(); } diff --git a/code/framework/src/integrations/server/instance.h b/code/framework/src/integrations/server/instance.h index eb747bb14..833f0a4b9 100644 --- a/code/framework/src/integrations/server/instance.h +++ b/code/framework/src/integrations/server/instance.h @@ -15,13 +15,12 @@ #include "logging/logger.h" #include "networking/engine.h" #include "scripting/module.h" + +#include #include "services/masterlist.h" #include "utils/config.h" #include "utils/command_listener.h" #include "utils/command_processor.h" -#include "world/server.h" - -#include #include "world/types/player.hpp" #include "world/types/streaming.hpp" @@ -35,6 +34,7 @@ #include #include #include +#include namespace Framework::Integrations::Server { struct InstanceOptions { @@ -73,7 +73,9 @@ namespace Framework::Integrations::Server { bool enableSignals; // update intervals - Framework::World::ServerEngine::ServerConfig worldConfig; + struct WorldConfig { + float tickInterval = 0.016667f; + } worldConfig; // args int argc; @@ -81,7 +83,25 @@ namespace Framework::Integrations::Server { }; - using OnPlayerConnectionCallback = fu2::function; + // Connection metadata handed to the player-connect callback so the game can create and fully + // populate the player's avatar (nickname, slot index, hardware id) instead of leaving spawn-time + // fields at their defaults. + struct PlayerConnectionData { + uint64_t guid = 0; + uint16_t playerIndex = MafiaNet::UNASSIGNED_PLAYER_INDEX; // the connection's dense slot + std::string nickname; + std::string hardwareID; + }; + + // Invoked when a player joins (with its connection metadata) or leaves (with its GUID); the game + // creates and owns the player's entity. + using OnPlayerConnectCallback = fu2::function; + using OnPlayerDisconnectCallback = fu2::function; + + // Chat. The sender is resolved to its viewer entity's NetworkID; command lines ("/...") are + // pre-parsed into a command name and whitespace-separated arguments. + using OnChatMessageCallback = fu2::function; + using OnChatCommandCallback = fu2::function &args) const>; class Instance : public Framework::Lifecycle { private: @@ -94,13 +114,11 @@ namespace Framework::Integrations::Server { std::unique_ptr _networkingEngine; std::unique_ptr _webServer; std::unique_ptr _fileConfig; - std::shared_ptr _worldEngine; std::unique_ptr _masterlist; std::unique_ptr _commandListener; std::unique_ptr _commandProcessor; void InitEndpoints(); - void InitModules() const; void InitNetworkingMessages() const; void InitAssetStreamer(); void InitCommandListener(); @@ -110,16 +128,15 @@ namespace Framework::Integrations::Server { // Command handlers void HandleCommand(std::string_view command); - // managers - flecs::entity _weatherManager; - // entity factories std::unique_ptr _playerFactory; std::unique_ptr _streamingFactory; // callbacks - OnPlayerConnectionCallback _onPlayerConnectCallback; - OnPlayerConnectionCallback _onPlayerDisconnectCallback; + OnPlayerConnectCallback _onPlayerConnectCallback; + OnPlayerDisconnectCallback _onPlayerDisconnectCallback; + OnChatMessageCallback _onChatMessageCallback; + OnChatCommandCallback _onChatCommandCallback; public: Instance(); @@ -146,14 +163,25 @@ namespace Framework::Integrations::Server { void OnSignal(sig_signal_t); - void SetOnPlayerConnectCallback(OnPlayerConnectionCallback onPlayerConnectCallback) { + void SetOnPlayerConnectCallback(OnPlayerConnectCallback onPlayerConnectCallback) { _onPlayerConnectCallback = std::move(onPlayerConnectCallback); } - void SetOnPlayerDisconnectCallback(OnPlayerConnectionCallback onPlayerDisconnectCallback) { + void SetOnPlayerDisconnectCallback(OnPlayerDisconnectCallback onPlayerDisconnectCallback) { _onPlayerDisconnectCallback = std::move(onPlayerDisconnectCallback); } + void SetOnChatMessageCallback(OnChatMessageCallback cb) { + _onChatMessageCallback = std::move(cb); + } + + void SetOnChatCommandCallback(OnChatCommandCallback cb) { + _onChatCommandCallback = std::move(cb); + } + + // Parse a received chat line and dispatch it to the chat callbacks above. + void HandleIncomingChat(uint64_t senderNetworkId, const std::string &text) const; + InstanceOptions &GetOptions() { return _opts; } @@ -162,10 +190,6 @@ namespace Framework::Integrations::Server { return _scriptingModule.get(); } - std::shared_ptr GetWorldEngine() const { - return _worldEngine; - } - Networking::Engine *GetNetworkingEngine() const { return _networkingEngine.get(); } diff --git a/code/framework/src/integrations/server/scripting/module.cpp b/code/framework/src/integrations/server/scripting/module.cpp index 5054389fb..c99a5ac44 100644 --- a/code/framework/src/integrations/server/scripting/module.cpp +++ b/code/framework/src/integrations/server/scripting/module.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -20,8 +21,7 @@ namespace Framework::Integrations::Server::Scripting { - ServerScriptingModule::ServerScriptingModule(std::shared_ptr world) - : _world(world) { + ServerScriptingModule::ServerScriptingModule() { // Create Node.js engine without sandbox for full server capabilities Framework::Scripting::NodeEngineOptions options; options.sandboxed = false; @@ -58,7 +58,7 @@ namespace Framework::Integrations::Server::Scripting { config.cascadeStopDependents = true; _resourceManager = std::make_unique( - _nodeEngine.get(), _world->GetWorld(), config); + _nodeEngine.get(), config); // Register Framework SDK bindings RegisterFrameworkBindings(); @@ -137,6 +137,9 @@ namespace Framework::Integrations::Server::Scripting { // Register environment info Framework::Scripting::Environment::Register(isolate, context, coreObj, false); + // Register the chat API on the global object (Chat.sendToAll / Chat.sendToPlayer) + Framework::Scripting::Builtins::Chat::Register(isolate, global); + Logging::GetLogger(FRAMEWORK_INNER_SCRIPTING)->debug("Registered Framework JS bindings"); } diff --git a/code/framework/src/integrations/server/scripting/module.h b/code/framework/src/integrations/server/scripting/module.h index 6c7355738..182bc7ce7 100644 --- a/code/framework/src/integrations/server/scripting/module.h +++ b/code/framework/src/integrations/server/scripting/module.h @@ -16,7 +16,6 @@ #include #include #include -#include namespace Framework::Integrations::Server::Scripting { @@ -36,7 +35,7 @@ namespace Framework::Integrations::Server::Scripting { */ class ServerScriptingModule final : public Framework::Lifecycle, public Framework::Scripting::ScriptingModule { public: - explicit ServerScriptingModule(std::shared_ptr world); + ServerScriptingModule(); ~ServerScriptingModule(); /** @@ -73,13 +72,6 @@ namespace Framework::Integrations::Server::Scripting { return _nodeEngine.get(); } - /** - * Get the world engine. - */ - std::shared_ptr GetWorldEngine() const { - return _world; - } - /** * Get the JavaScript resource manager. */ @@ -115,7 +107,6 @@ namespace Framework::Integrations::Server::Scripting { private: std::unique_ptr _nodeEngine; - std::shared_ptr _world; std::unique_ptr _resourceManager; std::string _resourcesPath = "resources"; diff --git a/code/framework/src/integrations/shared/modules/mod.hpp b/code/framework/src/integrations/shared/modules/mod.hpp deleted file mode 100644 index 3aee5e26a..000000000 --- a/code/framework/src/integrations/shared/modules/mod.hpp +++ /dev/null @@ -1,20 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include -#include - -namespace Framework::Integrations::Shared::Modules { - struct Mod { - Mod(flecs::world &world) { - world.module(); - } - }; -} // namespace Framework::Integrations::Shared::Modules diff --git a/code/framework/src/integrations/shared/rpc/emit_lua_event.h b/code/framework/src/integrations/shared/rpc/emit_lua_event.h index 75f7b7dc6..a5efa098a 100644 --- a/code/framework/src/integrations/shared/rpc/emit_lua_event.h +++ b/code/framework/src/integrations/shared/rpc/emit_lua_event.h @@ -8,37 +8,36 @@ #pragma once -#include +#include +#include #include namespace Framework::Integrations::Shared::RPC { - class EmitLuaEvent final: public Framework::Networking::RPC::IRPC { - private: - MafiaNet::RakString _eventName; - MafiaNet::RakString _payload; - - public: - void FromParameters(const std::string &name, const std::string &payload) { - _eventName = name.c_str(); - _payload = payload.c_str(); - } + // RPC payload forwarding a named scripting event (with a JSON payload) to the other peer's + // resource layer. See networking/rpc/rpc.h for the dispatch model. + struct EmitLuaEvent { + static constexpr const char *kIdentifier = "Framework::EmitLuaEvent"; + + MafiaNet::RakString eventName; + MafiaNet::RakString payload; - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _eventName); - bs->Serialize(write, _payload); + void FromParameters(const std::string &name, const std::string &payloadJson) { + eventName = name.c_str(); + payload = payloadJson.c_str(); } - bool Valid() const override { - return !_eventName.IsEmpty(); + void Serialize(MafiaNet::BitStream *bs, bool write) { + bs->Serialize(write, eventName); + bs->Serialize(write, payload); } std::string GetEventName() const { - return _eventName.C_String(); + return eventName.C_String(); } std::string GetPayload() const { - return _payload.C_String(); + return payload.C_String(); } }; -} +} // namespace Framework::Integrations::Shared::RPC diff --git a/code/framework/src/logging/formatters.h b/code/framework/src/logging/formatters.h deleted file mode 100644 index 8fee9a098..000000000 --- a/code/framework/src/logging/formatters.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2024, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "src/world/modules/base.hpp" -#include - -template <> -struct fmt::formatter { - constexpr auto parse(format_parse_context &ctx) { - return ctx.begin(); - } - - template - auto format(const glm::vec3 &t, FormatContext &ctx) { - return fmt::format_to(ctx.out(), "{:.2f}, {:.2f}, {:.2f}", t.x, t.y, t.z); - } -}; - -template <> -struct fmt::formatter { - constexpr auto parse(format_parse_context &ctx) { - return ctx.begin(); - } - - template - auto format(const glm::quat &t, FormatContext &ctx) { - return fmt::format_to(ctx.out(), "{:.2f}, {:.2f}, {:.2f}, {:.2f}", t.x, t.y, t.z, t.w); - } -}; - -template <> -struct fmt::formatter { - constexpr auto parse(format_parse_context &ctx) { - return ctx.begin(); - } - - template - auto format(const Framework::World::Modules::Base::Transform &t, FormatContext &ctx) { - return fmt::format_to(ctx.out(), "Transform(pos=({}), vel=({}), rot=({}), genID={})", t.pos, t.vel, t.rot, t.GetGeneration()); - } -}; diff --git a/code/framework/src/networking/connection.h b/code/framework/src/networking/connection.h new file mode 100644 index 000000000..99e79f1d1 --- /dev/null +++ b/code/framework/src/networking/connection.h @@ -0,0 +1,44 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include +#include +#include +#include + +namespace Framework::Networking { + enum class DisconnectionReason : uint32_t { + NO_FREE_SLOT, + GRACEFUL_SHUTDOWN, + LOST, + FAILED, + INVALID_PASSWORD, + WRONG_VERSION, + BANNED, + KICKED, + KICKED_CUSTOM, + KICKED_INVALID_PACKET, + UNKNOWN + }; + + // Optional reason carried on a graceful disconnect (CloseConnection's reasonData). + struct DisconnectPayload { + uint32_t reason = static_cast(DisconnectionReason::UNKNOWN); + std::string customReason; + + void Serialize(MafiaNet::BitStream *bs, bool write) { + bs->Serialize(write, reason); + bs->Serialize(write, customReason); + } + }; + + using PacketCallback = fu2::function; + using DisconnectPacketCallback = fu2::function; +} // namespace Framework::Networking diff --git a/code/framework/src/networking/messages/client_connection_finalized.h b/code/framework/src/networking/messages/client_connection_finalized.h deleted file mode 100644 index 9783388c2..000000000 --- a/code/framework/src/networking/messages/client_connection_finalized.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "messages.h" - -#include - -#include - -namespace Framework::Networking::Messages { - class ClientConnectionFinalized final: public IMessage { - private: - float _serverTickRate = 0.0f; - flecs::entity_t _entityID = 0; - - public: - uint8_t GetMessageID() const override { - return GAME_CONNECTION_FINALIZED; - } - - void FromParameters(float tickRate, flecs::entity_t entityID) { - _serverTickRate = tickRate; - _entityID = entityID; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _serverTickRate); - bs->Serialize(write, _entityID); - } - - bool Valid() const override { - return _serverTickRate > 0.0f && _entityID > 0; - } - - float GetServerTickRate() const { - return _serverTickRate; - } - - flecs::entity_t GetEntityID() const { - return _entityID; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/client_handshake.h b/code/framework/src/networking/messages/client_handshake.h deleted file mode 100644 index e25bc8c5f..000000000 --- a/code/framework/src/networking/messages/client_handshake.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "messages.h" - -#include - -namespace Framework::Networking::Messages { - class ClientHandshake final: public IMessage { - private: - MafiaNet::RakString _clientVersion = ""; - MafiaNet::RakString _fwVersion = ""; - MafiaNet::RakString _gameVersion = ""; - MafiaNet::RakString _gameName = ""; - - public: - uint8_t GetMessageID() const override { - return GAME_CONNECTION_HANDSHAKE; - } - - void FromParameters(const std::string &clientVersion, const std::string &fwVersion, const std::string &gameVersion, const std::string &gameName) { - _fwVersion = MafiaNet::RakString(fwVersion.c_str()); - _clientVersion = MafiaNet::RakString(clientVersion.c_str()); - _gameVersion = MafiaNet::RakString(gameVersion.c_str()); - _gameName = MafiaNet::RakString(gameName.c_str()); - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _fwVersion); - bs->Serialize(write, _clientVersion); - bs->Serialize(write, _gameVersion); - bs->Serialize(write, _gameName); - } - - bool Valid() const override { - return _fwVersion.GetLength() > 0 && _clientVersion.GetLength() > 0 && _gameVersion.GetLength() > 0 && _gameName.GetLength() > 0; - } - - std::string GetFWVersion() const { - return _fwVersion.C_String(); - } - - std::string GetClientVersion() const { - return _clientVersion.C_String(); - } - - std::string GetGameVersion() const { - return _gameVersion.C_String(); - } - - std::string GetGameName() const { - return _gameName.C_String(); - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/client_initialise_player.h b/code/framework/src/networking/messages/client_initialise_player.h deleted file mode 100644 index 47f11b2b8..000000000 --- a/code/framework/src/networking/messages/client_initialise_player.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "messages.h" - -#include - -#include - -namespace Framework::Networking::Messages { - class ClientInitPlayer final: public IMessage { - public: - uint8_t GetMessageID() const override { - return GAME_INIT_PLAYER; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override {} - - bool Valid() const override { - return true; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/client_kick.h b/code/framework/src/networking/messages/client_kick.h deleted file mode 100644 index 33c51daac..000000000 --- a/code/framework/src/networking/messages/client_kick.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "messages.h" - -#include - -#include - -namespace Framework::Networking::Messages { - class ClientKick final: public IMessage { - private: - DisconnectionReason _reason = DisconnectionReason::UNKNOWN; - MafiaNet::RakString _customReason; - - public: - uint8_t GetMessageID() const override { - return GAME_CONNECTION_KICKED; - } - - void FromParameters(DisconnectionReason reason, const std::string customReason = "") { - _reason = reason; - _customReason = customReason.c_str(); - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _reason); - bs->Serialize(write, _customReason); - } - - bool Valid() const override { - if (_reason == DisconnectionReason::KICKED_CUSTOM && _customReason.GetLength() == 0) { - return false; - } - return true; - } - - DisconnectionReason GetDisconnectionReason() const { - return _reason; - } - - std::string GetCustomReason() const { - return std::string(_customReason.C_String()); - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/client_ready_assets.h b/code/framework/src/networking/messages/client_ready_assets.h deleted file mode 100644 index 1f2a3cf9c..000000000 --- a/code/framework/src/networking/messages/client_ready_assets.h +++ /dev/null @@ -1,131 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "messages.h" - -#include -#include -#include -#include - -namespace Framework::Networking::Messages { - - /** - * Information about a single resource for synchronization. - */ - struct ResourceInfo { - std::string name; - std::string version; - uint32_t hash = 0; // Content hash for cache invalidation - - void Serialize(MafiaNet::BitStream *bs, bool write) { - if (write) { - // Write name - uint16_t nameLen = static_cast(name.length()); - bs->Write(nameLen); - if (nameLen > 0) { - bs->Write(name.c_str(), nameLen); - } - - // Write version - uint16_t versionLen = static_cast(version.length()); - bs->Write(versionLen); - if (versionLen > 0) { - bs->Write(version.c_str(), versionLen); - } - - // Write hash - bs->Write(hash); - } - else { - // Read name - uint16_t nameLen = 0; - bs->Read(nameLen); - if (nameLen > 0 && nameLen < 256) { - name.resize(nameLen); - bs->Read(&name[0], nameLen); - } - - // Read version - uint16_t versionLen = 0; - bs->Read(versionLen); - if (versionLen > 0 && versionLen < 64) { - version.resize(versionLen); - bs->Read(&version[0], versionLen); - } - - // Read hash - bs->Read(hash); - } - } - }; - - /** - * Message sent from server to client when assets are ready for download. - * Contains the client entry point script and list of resources to load. - */ - class ClientReadyAssets final: public IMessage { - private: - MafiaNet::RakString _clientEntryPoint; - std::vector _resources; - - public: - uint8_t GetMessageID() const override { - return GAME_CONNECTION_READY_ASSETS; - } - - void FromParameters(const std::string& clientEntryPoint) { - _clientEntryPoint = clientEntryPoint.c_str(); - } - - void AddResource(const std::string &name, const std::string &version, uint32_t hash) { - _resources.push_back({name, version, hash}); - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _clientEntryPoint); - - if (write) { - uint16_t count = static_cast(_resources.size()); - bs->Write(count); - for (auto &resource : _resources) { - resource.Serialize(bs, true); - } - } - else { - uint16_t count = 0; - bs->Read(count); - _resources.clear(); - _resources.reserve(count); - for (uint16_t i = 0; i < count && i < 1000; ++i) { // Limit to 1000 resources - ResourceInfo info; - info.Serialize(bs, false); - _resources.push_back(info); - } - } - } - - const std::string GetClientEntryPoint() const { - return _clientEntryPoint.C_String(); - } - - const std::vector &GetResources() const { - return _resources; - } - - size_t GetResourceCount() const { - return _resources.size(); - } - - bool Valid() const override { - return true; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/client_request_streamer.h b/code/framework/src/networking/messages/client_request_streamer.h deleted file mode 100644 index 4608765f0..000000000 --- a/code/framework/src/networking/messages/client_request_streamer.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "messages.h" - -#include - -namespace Framework::Networking::Messages { - class ClientRequestStreamer final: public IMessage { - private: - MafiaNet::RakString _playerName = ""; - MafiaNet::RakString _playerSteamId = ""; - MafiaNet::RakString _playerDiscordId = ""; - MafiaNet::RakString _playerHardwareId = ""; - - public: - uint8_t GetMessageID() const override { - return GAME_CONNECTION_REQUEST_STREAMER; - } - - void FromParameters(const std::string &playerName, const std::string &playerSteamId, const std::string &playerDiscordId, const std::string &playerHardwareId = "") { - _playerName = MafiaNet::RakString(playerName.c_str()); - _playerSteamId = MafiaNet::RakString(playerSteamId.c_str()); - _playerDiscordId = MafiaNet::RakString(playerDiscordId.c_str()); - _playerHardwareId = MafiaNet::RakString(playerHardwareId.c_str()); - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _playerName); - bs->Serialize(write, _playerSteamId); - bs->Serialize(write, _playerDiscordId); - bs->Serialize(write, _playerHardwareId); - } - - bool Valid() const override { - return _playerName.GetLength() > 0 && (_playerSteamId.GetLength() > 0 || _playerDiscordId.GetLength() > 0); - } - - std::string GetPlayerName() const { - return _playerName.C_String(); - } - - std::string GetPlayerSteamID() const { - return _playerSteamId.C_String(); - } - - std::string GetPlayerDiscordID() const { - return _playerDiscordId.C_String(); - } - - std::string GetPlayerHardwareID() const { - return _playerHardwareId.C_String(); - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/game_sync/entity_despawn.h b/code/framework/src/networking/messages/game_sync/entity_despawn.h deleted file mode 100644 index 43ea6494e..000000000 --- a/code/framework/src/networking/messages/game_sync/entity_despawn.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "../messages.h" -#include "message.h" -#include "world/modules/base.hpp" - -#include - -namespace Framework::Networking::Messages { - class GameSyncEntityDespawn final: public GameSyncMessage { - public: - uint8_t GetMessageID() const override { - return GAME_SYNC_ENTITY_DESPAWN; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - // noop - } - - bool Valid() const override { - return true; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/game_sync/entity_messages.h b/code/framework/src/networking/messages/game_sync/entity_messages.h deleted file mode 100644 index bc3a46d6e..000000000 --- a/code/framework/src/networking/messages/game_sync/entity_messages.h +++ /dev/null @@ -1,15 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "entity_despawn.h" -#include "entity_owner_update.h" -#include "entity_self_update.h" -#include "entity_spawn.h" -#include "entity_update.h" diff --git a/code/framework/src/networking/messages/game_sync/entity_owner_update.h b/code/framework/src/networking/messages/game_sync/entity_owner_update.h deleted file mode 100644 index 7374dd776..000000000 --- a/code/framework/src/networking/messages/game_sync/entity_owner_update.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "../messages.h" -#include "message.h" - -#include - -namespace Framework::Networking::Messages { - class GameSyncEntityOwnerUpdate final: public GameSyncMessage { - private: - uint64_t _owner = MafiaNet::UNASSIGNED_RAKNET_GUID.g; - - public: - uint8_t GetMessageID() const override { - return GAME_SYNC_ENTITY_OWNER_UPDATE; - } - - void FromParameters(uint64_t owner) { - _owner = owner; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _owner); - } - - bool Valid() const override { - return _owner != MafiaNet::UNASSIGNED_RAKNET_GUID.g; - } - - uint64_t GetOwner() const { - return _owner; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/game_sync/entity_self_update.h b/code/framework/src/networking/messages/game_sync/entity_self_update.h deleted file mode 100644 index 17ab3121c..000000000 --- a/code/framework/src/networking/messages/game_sync/entity_self_update.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "../messages.h" -#include "message.h" -#include "world/modules/base.hpp" - -#include - -namespace Framework::Networking::Messages { - class GameSyncEntitySelfUpdate final: public GameSyncMessage { - public: - uint8_t GetMessageID() const override { - return GAME_SYNC_ENTITY_SELF_UPDATE; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - // noop - } - - bool Valid() const override { - return true; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/game_sync/entity_spawn.h b/code/framework/src/networking/messages/game_sync/entity_spawn.h deleted file mode 100644 index 9510f444d..000000000 --- a/code/framework/src/networking/messages/game_sync/entity_spawn.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "../messages.h" -#include "message.h" -#include "world/modules/base.hpp" - -#include - -namespace Framework::Networking::Messages { - class GameSyncEntitySpawn final: public GameSyncMessage { - private: - World::Modules::Base::Transform _transform {}; - - public: - uint8_t GetMessageID() const override { - return GAME_SYNC_ENTITY_SPAWN; - } - - void FromParameters(World::Modules::Base::Transform tr) { - _transform = tr; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _transform); - } - - bool Valid() const override { - return true; - } - - World::Modules::Base::Transform GetTransform() const { - return _transform; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/game_sync/entity_update.h b/code/framework/src/networking/messages/game_sync/entity_update.h deleted file mode 100644 index 83f2dbba9..000000000 --- a/code/framework/src/networking/messages/game_sync/entity_update.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "../messages.h" -#include "message.h" -#include "world/modules/base.hpp" - -#include - -namespace Framework::Networking::Messages { - class GameSyncEntityUpdate final: public GameSyncMessage { - private: - World::Modules::Base::Transform _transform {}; - uint64_t _owner = MafiaNet::UNASSIGNED_RAKNET_GUID.g; - - public: - uint8_t GetMessageID() const override { - return GAME_SYNC_ENTITY_UPDATE; - } - - void FromParameters(World::Modules::Base::Transform tr, uint64_t owner) { - _transform = tr; - _owner = owner; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _transform); - bs->Serialize(write, _owner); - } - - bool Valid() const override { - return _owner != MafiaNet::UNASSIGNED_RAKNET_GUID.g; - } - - World::Modules::Base::Transform GetTransform() const { - return _transform; - } - - uint64_t GetOwner() const { - return _owner; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/game_sync/message.h b/code/framework/src/networking/messages/game_sync/message.h deleted file mode 100644 index 68a23a2df..000000000 --- a/code/framework/src/networking/messages/game_sync/message.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "../messages.h" -#include "world/modules/base.hpp" - -#include - -namespace Framework::Networking::Messages { - class GameSyncMessage: public IMessage { - protected: - flecs::entity_t _serverID = 0; - - public: - void SetServerID(flecs::entity_t serverID) { - _serverID = serverID; - } - - flecs::entity_t GetServerID() const { - return _serverID; - } - - void Serialize2(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _serverID); - }; - - inline bool ValidServerID() const { - return _serverID > 0; - } - - /** - * Validates if the server id was set. - * @return - */ - bool Valid2() const override { - return ValidServerID(); - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/messages/messages.h b/code/framework/src/networking/messages/messages.h deleted file mode 100644 index 461845e12..000000000 --- a/code/framework/src/networking/messages/messages.h +++ /dev/null @@ -1,101 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include -#include -#include -#include - -namespace Framework::Networking::Messages { - enum class DisconnectionReason : uint32_t { - NO_FREE_SLOT, - GRACEFUL_SHUTDOWN, - LOST, - FAILED, - INVALID_PASSWORD, - WRONG_VERSION, - BANNED, - KICKED, - KICKED_CUSTOM, - KICKED_INVALID_PACKET, - UNKNOWN - }; - - using PacketCallback = fu2::function; - using DisconnectPacketCallback = fu2::function; - - // Internal Framework messages - enum InternalMessages : uint8_t { - INTERNAL_RPC = ID_USER_PACKET_ENUM + 1, - INTERNAL_NEXT_MESSAGE_ID - }; - - // Internal game flow messages - enum GameMessages : uint8_t { - // Game messages handling common client connection flow - GAME_CONNECTION_HANDSHAKE = INTERNAL_NEXT_MESSAGE_ID, - GAME_CONNECTION_ACKNOWLEDGE_CLIENT, - GAME_CONNECTION_READY_ASSETS, - GAME_CONNECTION_REQUEST_STREAMER, - GAME_CONNECTION_FINALIZED, - GAME_CONNECTION_KICKED, - GAME_INIT_PLAYER, - - // Game sync entity streamer messages - GAME_SYNC_ENTITY_SPAWN, - GAME_SYNC_ENTITY_UPDATE, - GAME_SYNC_ENTITY_SELF_UPDATE, // server sends data to streamer - GAME_SYNC_ENTITY_OWNER_UPDATE, // server sends data about owned entity - GAME_SYNC_ENTITY_DESPAWN, - - // Messages used by the mod - GAME_NEXT_MESSAGE_ID - }; - - /** - * Base interface for network message - * \see NetworkPeer::RegisterMessage - */ - class IMessage { - private: - MafiaNet::Packet *packet {}; - - public: - virtual ~IMessage() = default; - virtual uint8_t GetMessageID() const = 0; - - virtual void Serialize(MafiaNet::BitStream *bs, bool write) = 0; - - /** - * Extra serialization for middleware data - * @param bs - * @param write - */ - virtual void Serialize2(MafiaNet::BitStream *bs, bool write) {}; - - virtual bool Valid() const = 0; - - /** - * Extra validation for middleware data - * @return - */ - virtual bool Valid2() const { - return true; - } - - void SetPacket(MafiaNet::Packet *p) { - packet = p; - } - - MafiaNet::Packet *GetPacket() const { - return packet; - } - }; -} // namespace Framework::Networking::Messages diff --git a/code/framework/src/networking/network_client.cpp b/code/framework/src/networking/network_client.cpp index 5369211f7..08aac87ab 100644 --- a/code/framework/src/networking/network_client.cpp +++ b/code/framework/src/networking/network_client.cpp @@ -8,6 +8,8 @@ #include "network_client.h" +#include "replication/replication_manager.h" + #include namespace Framework::Networking { @@ -29,6 +31,9 @@ namespace Framework::Networking { _peer->AttachPlugin(&_fileListTransfer); _peer->AttachPlugin(&_assetStreamer); + // Run replication as a client: receive constructions and serialize owned entities upstream. + _replicationManager->Init(this, false); + _initialized = true; return NetworkPeerError::NETWORK_PEER_NONE; } @@ -37,7 +42,6 @@ namespace Framework::Networking { if (!_peer) { return; } - _registeredMessageCallbacks.clear(); MafiaNet::RakPeerInterface::DestroyInstance(_peer); _peer = nullptr; @@ -84,7 +88,8 @@ namespace Framework::Networking { Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("Disconnecting from the server..."); if (_onPlayerDisconnectedCallback) { - _onPlayerDisconnectedCallback(_packet, Messages::DisconnectionReason::GRACEFUL_SHUTDOWN); + // Locally initiated: there is no inbound packet, so pass null rather than a stale _packet. + _onPlayerDisconnectedCallback(nullptr, DisconnectionReason::GRACEFUL_SHUTDOWN, ""); } _state = PeerState::DISCONNECTED; @@ -102,60 +107,106 @@ namespace Framework::Networking { bool NetworkClient::HandlePacket(uint8_t packetID, MafiaNet::Packet *packet) { switch (packetID) { case ID_CONNECTION_REQUEST_ACCEPTED: { + _state = PeerState::CONNECTED; if (_onPlayerConnectedCallback) { _onPlayerConnectedCallback(_packet); } - _state = PeerState::CONNECTED; + // Prove our build matches before anything else; the server sends ServerResources only + // after this passes. A mismatch is handled by the auth-failure cases below. + _twoWayAuth.Challenge(NetworkPeer::kBuildChallengeId, _packet->guid); return true; }; case ID_NO_FREE_INCOMING_CONNECTIONS: { - if (_onPlayerDisconnectedCallback) { - _onPlayerDisconnectedCallback(_packet, Messages::DisconnectionReason::NO_FREE_SLOT); + if (_state != PeerState::DISCONNECTED && _onPlayerDisconnectedCallback) { + _onPlayerDisconnectedCallback(_packet, DisconnectionReason::NO_FREE_SLOT, ""); } _state = PeerState::DISCONNECTED; return true; }; case ID_DISCONNECTION_NOTIFICATION: { - if (_onPlayerDisconnectedCallback) { - _onPlayerDisconnectedCallback(_packet, Messages::DisconnectionReason::GRACEFUL_SHUTDOWN); + if (_state != PeerState::DISCONNECTED && _onPlayerDisconnectedCallback) { + // A kick carries a reason payload after the id; a plain disconnect has none. + DisconnectionReason reason = DisconnectionReason::GRACEFUL_SHUTDOWN; + std::string customReason; + if (_packet->length - _packetDataOffset > sizeof(MafiaNet::MessageID)) { + MafiaNet::BitStream bs(_packet->data + _packetDataOffset, _packet->length - _packetDataOffset, false); + bs.IgnoreBytes(sizeof(MafiaNet::MessageID)); + DisconnectPayload payload; + payload.Serialize(&bs, false); + reason = static_cast(payload.reason); + customReason = payload.customReason; + } + _onPlayerDisconnectedCallback(_packet, reason, customReason); } _state = PeerState::DISCONNECTED; return true; }; case ID_CONNECTION_LOST: { - if (_onPlayerDisconnectedCallback) { - _onPlayerDisconnectedCallback(_packet, Messages::DisconnectionReason::LOST); + if (_state != PeerState::DISCONNECTED && _onPlayerDisconnectedCallback) { + _onPlayerDisconnectedCallback(_packet, DisconnectionReason::LOST, ""); } _state = PeerState::DISCONNECTED; return true; }; case ID_CONNECTION_ATTEMPT_FAILED: { - if (_onPlayerDisconnectedCallback) { - _onPlayerDisconnectedCallback(_packet, Messages::DisconnectionReason::FAILED); + if (_state != PeerState::DISCONNECTED && _onPlayerDisconnectedCallback) { + _onPlayerDisconnectedCallback(_packet, DisconnectionReason::FAILED, ""); } _state = PeerState::DISCONNECTED; return true; }; case ID_INVALID_PASSWORD: { - if (_onPlayerDisconnectedCallback) { - _onPlayerDisconnectedCallback(_packet, Messages::DisconnectionReason::INVALID_PASSWORD); + if (_state != PeerState::DISCONNECTED && _onPlayerDisconnectedCallback) { + _onPlayerDisconnectedCallback(_packet, DisconnectionReason::INVALID_PASSWORD, ""); } _state = PeerState::DISCONNECTED; return true; }; case ID_CONNECTION_BANNED: { - if (_onPlayerDisconnectedCallback) { - _onPlayerDisconnectedCallback(_packet, Messages::DisconnectionReason::BANNED); + if (_state != PeerState::DISCONNECTED && _onPlayerDisconnectedCallback) { + _onPlayerDisconnectedCallback(_packet, DisconnectionReason::BANNED, ""); + } + _state = PeerState::DISCONNECTED; + return true; + }; + + // Build gate: verified -> wait for the server's ServerResources RPC; mismatch -> disconnect. + case ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_SUCCESS: { + Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("Build verified by server"); + return true; + }; + case ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_FAILURE: + case ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_TIMEOUT: { + Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->error("Build mismatch with server, disconnecting"); + if (_state != PeerState::DISCONNECTED && _onPlayerDisconnectedCallback) { + _onPlayerDisconnectedCallback(_packet, DisconnectionReason::WRONG_VERSION, ""); } _state = PeerState::DISCONNECTED; return true; }; + + case ID_READY_EVENT_ALL_SET: { + // Payload: message id then int eventId (ReadyEvent::PushCompletionPacket). + MafiaNet::BitStream bs(_packet->data + _packetDataOffset, _packet->length - _packetDataOffset, false); + bs.IgnoreBytes(sizeof(MafiaNet::MessageID)); + int eventId = 0; + bs.Read(eventId); + if (_onConnectionReadyCallback) { + _onConnectionReadyCallback(eventId); + } + return true; + }; + case ID_READY_EVENT_SET: + case ID_READY_EVENT_UNSET: + case ID_READY_EVENT_QUERY: + case ID_READY_EVENT_FORCE_ALL_SET: + return true; } return false; } diff --git a/code/framework/src/networking/network_client.h b/code/framework/src/networking/network_client.h index 766917506..6a7a7b9f2 100644 --- a/code/framework/src/networking/network_client.h +++ b/code/framework/src/networking/network_client.h @@ -9,7 +9,7 @@ #pragma once #include "errors.h" -#include "messages/messages.h" +#include "connection.h" #include "network_peer.h" #include "state.h" @@ -35,9 +35,10 @@ namespace Framework::Networking { PeerState _state; - Messages::PacketCallback _onPlayerConnectedCallback; - Messages::DisconnectPacketCallback _onPlayerDisconnectedCallback; + PacketCallback _onPlayerConnectedCallback; + DisconnectPacketCallback _onPlayerDisconnectedCallback; OnAssetsDownloadFailedCallback _onAssetsDownloadFailedCallback; + fu2::function _onConnectionReadyCallback; AssetFileTransfer _fileListTransfer; public: @@ -65,11 +66,11 @@ namespace Framework::Networking { return &_fileListTransfer; } - void SetOnPlayerConnectedCallback(Messages::PacketCallback callback) { + void SetOnPlayerConnectedCallback(PacketCallback callback) { _onPlayerConnectedCallback = std::move(callback); } - void SetOnPlayerDisconnectedCallback(Messages::DisconnectPacketCallback callback) { + void SetOnPlayerDisconnectedCallback(DisconnectPacketCallback callback) { _onPlayerDisconnectedCallback = std::move(callback); } @@ -77,18 +78,9 @@ namespace Framework::Networking { _onAssetsDownloadFailedCallback = std::move(callback); } - template - bool SendGameRPC(T &rpc, MafiaNet::RakNetGUID guid = MafiaNet::UNASSIGNED_RAKNET_GUID, PacketPriority priority = HIGH_PRIORITY, PacketReliability reliability = RELIABLE_ORDERED) { - MafiaNet::BitStream bs; - bs.Write(Messages::INTERNAL_RPC); - bs.Write(rpc.GetHashName()); - rpc.Serialize(&bs, true); - rpc.Serialize2(&bs, true); - - if (_peer->Send(&bs, priority, reliability, 0, guid, guid == MafiaNet::UNASSIGNED_RAKNET_GUID) <= 0) { - return false; - } - return true; + // Fired when the spawn barrier completes — the client activates replication and finalizes. + void SetOnConnectionReadyCallback(fu2::function callback) { + _onConnectionReadyCallback = std::move(callback); } }; } // namespace Framework::Networking diff --git a/code/framework/src/networking/network_peer.cpp b/code/framework/src/networking/network_peer.cpp index ebd5d5661..616d9fece 100644 --- a/code/framework/src/networking/network_peer.cpp +++ b/code/framework/src/networking/network_peer.cpp @@ -9,6 +9,7 @@ #include "network_peer.h" #include "errors.h" +#include "replication/replication_manager.h" #include @@ -16,48 +17,22 @@ namespace Framework::Networking { NetworkPeer::NetworkPeer() { _peer = MafiaNet::RakPeerInterface::GetInstance(); - RegisterMessage(Messages::INTERNAL_RPC, [this](MafiaNet::Packet *p) { - MafiaNet::BitStream bs(p->data + _packetDataOffset + 1, p->length - _packetDataOffset - 1, false); - uint32_t hashName; - bs.Read(hashName); + // RPC4 and StatisticsHistory can be attached before Startup(); the ReplicationManager is + // attached by the concrete peer's Init() once its connection factory exists. + _peer->AttachPlugin(&_rpc); + _peer->AttachPlugin(&_statisticsHistory); + _peer->AttachPlugin(&_twoWayAuth); + _peer->AttachPlugin(&_readyEvent); + _statisticsHistory.SetTrackConnections(true, 0, true); - if (_registeredRPCs.contains(hashName)) { - for (const auto &cb : _registeredRPCs[hashName]) { - cb(p); - } - } - }); + _replicationManager = std::make_unique(); } NetworkPeer::~NetworkPeer() = default; - bool NetworkPeer::Send(Messages::IMessage &msg, MafiaNet::RakNetGUID guid, PacketPriority priority, PacketReliability reliability) const { - if (!_peer) { - return false; - } - - MafiaNet::BitStream bsOut; - bsOut.Write(msg.GetMessageID()); - msg.Serialize(&bsOut, true); - msg.Serialize2(&bsOut, true); - - if (_peer->Send(&bsOut, priority, reliability, 0, guid, guid == MafiaNet::UNASSIGNED_RAKNET_GUID) <= 0) { - return false; - } - - return true; - } - - bool NetworkPeer::Send(Messages::IMessage &msg, uint64_t guid, PacketPriority priority, PacketReliability reliability) { - return Send(msg, MafiaNet::RakNetGUID(guid), priority, reliability); - } - - void NetworkPeer::RegisterMessage(uint8_t message, Messages::PacketCallback callback) { - if (callback == nullptr) { - return; - } - - _registeredMessageCallbacks[message] = callback; + void NetworkPeer::SetBuildToken(const std::string &token) { + // Re-registering the identifier overwrites the previous password; safe to call again. + _twoWayAuth.AddPassword(kBuildChallengeId, MafiaNet::RakString(token.c_str())); } void NetworkPeer::Update() { @@ -65,6 +40,11 @@ namespace Framework::Networking { return; } + // Rebuild the spatial index before ReplicaManager3 computes per-connection relevance. + if (_replicationManager) { + _replicationManager->RebuildInterest(); + } + for (_packet = _peer->Receive(); _packet; _peer->DeallocatePacket(_packet), _packet = _peer->Receive()) { _packetDataOffset = 0; if (_packet->length == 0) { @@ -83,14 +63,9 @@ namespace Framework::Networking { uint8_t packetID = _packet->data[_packetDataOffset]; if (!HandlePacket(packetID, _packet)) { - if (_registeredMessageCallbacks.contains(packetID)) { - _registeredMessageCallbacks[packetID](_packet); - } - else { - Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->trace("Received unknown packet {}", packetID); - if (_onUnknownPacketCallback) { - _onUnknownPacketCallback(_packet); - } + Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->trace("Received unknown packet {}", packetID); + if (_onUnknownPacketCallback) { + _onUnknownPacketCallback(_packet); } } } diff --git a/code/framework/src/networking/network_peer.h b/code/framework/src/networking/network_peer.h index de198fe57..d78a2d960 100644 --- a/code/framework/src/networking/network_peer.h +++ b/code/framework/src/networking/network_peer.h @@ -8,134 +8,133 @@ #pragma once -#include "messages/messages.h" +#include "connection.h" +#include "rpc/rpc.h" +#include #include #include #include #include +#include +#include +#include +#include +#include #include #include -#include +#include #include #include #include +namespace Framework::Networking::Replication { + class ReplicationManager; +} // namespace Framework::Networking::Replication + namespace Framework::Networking { class NetworkPeer : public Lifecycle { protected: MafiaNet::RakPeerInterface *_peer = nullptr; MafiaNet::Packet *_packet = nullptr; int _packetDataOffset = 0; // Offset to skip timestamp prefix if present - std::unordered_map> _registeredRPCs; - std::unordered_map _registeredMessageCallbacks; - Messages::PacketCallback _onUnknownPacketCallback; + PacketCallback _onUnknownPacketCallback; mutable MafiaNet::DirectoryDeltaTransfer _assetStreamer; + // RPC4 dispatches remote-procedure calls by identifier to C handlers. NetworkIDManager hands + // out the cross-network object handles used by replicas. StatisticsHistoryPlugin tracks + // per-connection bandwidth/RTT/loss. + MafiaNet::RPC4 _rpc; + MafiaNet::NetworkIDManager _networkIDManager; + MafiaNet::StatisticsHistoryPlugin _statisticsHistory; + + // Connection gate: TwoWayAuthentication proves an identical build token without sending it; + // ReadyEvent is the per-connection spawn barrier. Flow in network_{server,client}.cpp. + MafiaNet::TwoWayAuthentication _twoWayAuth; + MafiaNet::ReadyEvent _readyEvent; + + // Owns the replicated entity world. The concrete peer's Init() attaches it and sets its role. + std::unique_ptr _replicationManager; + + // Decoded RPC handlers registered via RegisterRPC. Each is kept alive here for the peer's + // lifetime; its address is the context RPC4 hands back to DispatchRPC, so handlers can capture. + using RPCSlot = fu2::function; + std::vector> _rpcHandlers; + + static void DispatchRPC(MafiaNet::BitStream *bs, MafiaNet::Packet *packet, void *context) { + auto *slot = static_cast(context); + if (slot && *slot) { + (*slot)(bs, packet); + } + } + public: + // TwoWayAuthentication identifier under which the build token is registered/challenged. + static constexpr const char *kBuildChallengeId = "Framework::Build"; + NetworkPeer(); ~NetworkPeer(); - bool Send(Messages::IMessage &msg, MafiaNet::RakNetGUID guid = MafiaNet::UNASSIGNED_RAKNET_GUID, PacketPriority priority = HIGH_PRIORITY, PacketReliability reliability = RELIABLE_ORDERED) const; - - bool Send(Messages::IMessage &msg, uint64_t guid = (uint64_t)-1, PacketPriority priority = HIGH_PRIORITY, PacketReliability reliability = RELIABLE_ORDERED); - - void RegisterMessage(uint8_t message, Messages::PacketCallback callback); - - template - void RegisterMessage(uint8_t message, fu2::function callback) { - if (callback == nullptr) { - return; - } - - _registeredMessageCallbacks[message] = [this, callback, message](MafiaNet::Packet *p) { - MafiaNet::BitStream bs(p->data + _packetDataOffset + 1, p->length - _packetDataOffset - 1, false); - T msg = {}; - msg.SetPacket(p); - msg.Serialize(&bs, false); - msg.Serialize2(&bs, false); - if (msg.Valid2()) { - if (msg.Valid()) { - callback(p->guid, &msg); - } - else { - Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("Message {} has failed to pass Valid() check, skipping!", message); - } - } - else { - Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("Message {} has failed to pass Valid2() check, skipping!", message); - } - }; + // Single source of truth for the gated build identity — must produce the same string on both + // peers or the challenge fails. + static std::string BuildToken(const std::string &gameName, const std::string &gameVersion, const std::string &fwVersion, const std::string &modVersion) { + return gameName + '|' + gameVersion + '|' + fwVersion + '|' + modVersion; } - template - void RegisterRPC(fu2::function callback) { - T _rpc = {}; + // Register the local build token (see BuildToken). Call before connecting/accepting. + void SetBuildToken(const std::string &token); - if (callback == nullptr) { - return; - } - - _registeredRPCs[_rpc.GetHashName()].push_back([this, callback, _rpc](MafiaNet::Packet *p) { - MafiaNet::BitStream bs(p->data + _packetDataOffset + 5, p->length - _packetDataOffset - 5, false); - T rpc = {}; - rpc.SetPacket(p); - rpc.Serialize(&bs, false); - if (rpc.Valid()) { - callback(p->guid, &rpc); - } - else { - Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("RPC {} ({}) has failed to pass Valid() check, skipping!", _rpc.GetName(), _rpc.GetHashName()); - } + // Register a handler for RPC payload type T (see networking/rpc/rpc.h). The handler receives + // the already-decoded payload and the raw packet, and may capture (e.g. the owning instance). + // The callable is stored for the peer's lifetime and reached through RPC4's per-slot context, + // so no file-static handler pointers are needed. Matches the Signal() send below. + template + void RegisterRPC(fu2::function handler) { + auto slot = std::make_unique([cb = std::move(handler)](MafiaNet::BitStream *bs, MafiaNet::Packet *packet) { + cb(RPC::Read(bs), packet); }); + void *context = slot.get(); + _rpcHandlers.push_back(std::move(slot)); + _rpc.RegisterSlot(T::kIdentifier, &NetworkPeer::DispatchRPC, context, 0); } + // Send an RPC payload to every connected system. template - void RegisterGameRPC(fu2::function callback) { - T _rpc = {}; - - if (callback == nullptr) { - return; - } - - _registeredRPCs[_rpc.GetHashName()].push_back([this, callback, _rpc](MafiaNet::Packet *p) { - MafiaNet::BitStream bs(p->data + _packetDataOffset + 5, p->length - _packetDataOffset - 5, false); - T rpc = {}; - rpc.SetPacket(p); - rpc.Serialize(&bs, false); - rpc.Serialize2(&bs, false); - if (rpc.Valid2()) { - if (rpc.Valid()) { - callback(p->guid, &rpc); - } - else { - Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("RPC {} has failed to pass Valid() check, skipping!", _rpc.GetHashName()); - } - } - else { - Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("RPC {} has failed to pass Valid2() check, skipping!", _rpc.GetHashName()); - } - }); + void BroadcastRPC(T &payload, PacketPriority priority = HIGH_PRIORITY, PacketReliability reliability = RELIABLE_ORDERED) { + MafiaNet::BitStream bs; + payload.Serialize(&bs, true); + _rpc.Signal(T::kIdentifier, &bs, priority, reliability, 0, MafiaNet::UNASSIGNED_RAKNET_GUID, true, false); } + // Send an RPC payload to a single system. template - bool SendRPC(T &rpc, MafiaNet::RakNetGUID guid = MafiaNet::UNASSIGNED_RAKNET_GUID, PacketPriority priority = HIGH_PRIORITY, PacketReliability reliability = RELIABLE_ORDERED) { + void SendRPC(T &payload, MafiaNet::RakNetGUID guid, PacketPriority priority = HIGH_PRIORITY, PacketReliability reliability = RELIABLE_ORDERED) { MafiaNet::BitStream bs; - bs.Write(Messages::INTERNAL_RPC); - bs.Write(rpc.GetHashName()); - rpc.Serialize(&bs, true); - assert(!rpc.IsGameRPC() && "Game RPCs cannot be sent via SendRPC()"); + payload.Serialize(&bs, true); + _rpc.Signal(T::kIdentifier, &bs, priority, reliability, 0, guid, false, false); + } - if (_peer->Send(&bs, priority, reliability, 0, guid, guid == MafiaNet::UNASSIGNED_RAKNET_GUID) <= 0) { - return false; - } - return true; + // Raw variant of RegisterRPC for handlers that decode the bitstream themselves (e.g. a + // polymorphic tail). Prefer the typed RegisterRPC for fixed-shape payloads. + void RegisterRawRPC(const char *identifier, fu2::function handler) { + auto slot = std::make_unique(std::move(handler)); + void *context = slot.get(); + _rpcHandlers.push_back(std::move(slot)); + _rpc.RegisterSlot(identifier, &NetworkPeer::DispatchRPC, context, 0); + } + + // Send a pre-encoded bitstream under a raw identifier (pairs with RegisterRawRPC). + void SendRawRPC(const char *identifier, MafiaNet::BitStream &bs, MafiaNet::RakNetGUID guid, PacketPriority priority = HIGH_PRIORITY, PacketReliability reliability = RELIABLE_ORDERED) { + _rpc.Signal(identifier, &bs, priority, reliability, 0, guid, false, false); } void Update() override; virtual bool HandlePacket(uint8_t packetID, MafiaNet::Packet *packet) = 0; - void SetUnknownPacketHandler(Messages::PacketCallback callback) { + // Server-only; base no-op lets shared code kick through a NetworkPeer* without a cast. + virtual void KickPlayer(MafiaNet::RakNetGUID, DisconnectionReason, const std::string & = "") {} + + void SetUnknownPacketHandler(PacketCallback callback) { _onUnknownPacketCallback = std::move(callback); } @@ -157,5 +156,29 @@ namespace Framework::Networking { MafiaNet::DirectoryDeltaTransfer* GetAssetStreamer() const noexcept { return &_assetStreamer; } + + MafiaNet::RPC4 *GetRPC() noexcept { + return &_rpc; + } + + MafiaNet::NetworkIDManager *GetNetworkIDManager() noexcept { + return &_networkIDManager; + } + + MafiaNet::StatisticsHistoryPlugin *GetStatisticsHistory() noexcept { + return &_statisticsHistory; + } + + MafiaNet::TwoWayAuthentication *GetTwoWayAuth() noexcept { + return &_twoWayAuth; + } + + MafiaNet::ReadyEvent *GetReadyEvent() noexcept { + return &_readyEvent; + } + + Replication::ReplicationManager *GetReplicationManager() const noexcept { + return _replicationManager.get(); + } }; } // namespace Framework::Networking diff --git a/code/framework/src/networking/network_server.cpp b/code/framework/src/networking/network_server.cpp index e970f4204..dca212243 100644 --- a/code/framework/src/networking/network_server.cpp +++ b/code/framework/src/networking/network_server.cpp @@ -8,6 +8,8 @@ #include "network_server.h" +#include "replication/replication_manager.h" + #include #include #include @@ -32,6 +34,13 @@ namespace Framework::Networking { _peer->AttachPlugin(&_fileListTransfer); _peer->AttachPlugin(&_assetStreamer); + // Run replication as the authoritative server. + _replicationManager->Init(this, true); + + // Gate replication behind the handshake: don't auto-create the connection on connect (see + // PushReplicationConnection). autoDestroy stays on so dropped peers are torn down. + _replicationManager->SetAutoManageConnections(false, true); + _initialized = true; return NetworkPeerError::NETWORK_PEER_NONE; } @@ -49,17 +58,45 @@ namespace Framework::Networking { case ID_DISCONNECTION_NOTIFICATION: { Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("Disconnection from {}", packet->guid.ToString()); if (_onPlayerDisconnectCallback) { - _onPlayerDisconnectCallback(_packet, Messages::DisconnectionReason::GRACEFUL_SHUTDOWN); + _onPlayerDisconnectCallback(_packet, DisconnectionReason::GRACEFUL_SHUTDOWN, ""); } + ClearClientState(packet->guid); return true; }; case ID_CONNECTION_LOST: { Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("Connection lost for {}", packet->guid.ToString()); if (_onPlayerDisconnectCallback) { - _onPlayerDisconnectCallback(_packet, Messages::DisconnectionReason::LOST); + _onPlayerDisconnectCallback(_packet, DisconnectionReason::LOST, ""); } + ClearClientState(packet->guid); return true; }; + + // Build gate: the client challenges us with its build token. Match -> asset phase; mismatch + // -> drop (the version-incompatibility path). + case ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_SUCCESS: { + Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->debug("Build verified for {}", packet->guid.ToString()); + _authenticatedClients.insert(packet->guid.g); + if (_onClientAuthenticatedCallback) { + _onClientAuthenticatedCallback(packet->guid); + } + return true; + }; + case ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_FAILURE: { + Framework::Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->warn("Build mismatch from {}, dropping peer", packet->guid.ToString()); + _peer->CloseConnection(packet->guid, true); + return true; + }; + + // ReadyEvent: the server arms its half from the integration layer and takes no action on + // completion (the avatar already exists); consumed so they don't hit the unknown-packet path. + case ID_READY_EVENT_SET: + case ID_READY_EVENT_UNSET: + case ID_READY_EVENT_ALL_SET: + case ID_READY_EVENT_QUERY: + case ID_READY_EVENT_FORCE_ALL_SET: + return true; + default: break; } return false; @@ -79,26 +116,39 @@ namespace Framework::Networking { int NetworkServer::GetPing(MafiaNet::RakNetGUID guid) const { return _peer->GetAveragePing(guid); } - bool NetworkServer::SendGameRPCInternal(MafiaNet::BitStream &bs, Framework::World::ServerEngine *world, flecs::entity_t ent_id, MafiaNet::RakNetGUID guid, MafiaNet::RakNetGUID excludeGUID, PacketPriority priority, PacketReliability reliability) const { - const auto ent = world->WrapEntity(ent_id); + void NetworkServer::SignalExcept(const char *identifier, MafiaNet::BitStream &bs, MafiaNet::RakNetGUID excludeGUID, PacketPriority priority, PacketReliability reliability) { + // When broadcasting, the system identifier is the peer to exclude, so a single Signal reaches + // everyone but the sender. + _rpc.Signal(identifier, &bs, priority, reliability, 0, excludeGUID, true, false); + } - if (!ent.is_alive()) { - return false; + void NetworkServer::PushReplicationConnection(MafiaNet::RakNetGUID guid) { + if (!_replicationManager) { + return; + } + if (_replicationManager->GetConnectionByGUID(guid) != nullptr) { + return; // already pushed (a client could send ClientIdentity twice) + } + const auto address = _peer->GetSystemAddressFromGuid(guid); + if (MafiaNet::Connection_RM3 *connection = _replicationManager->AllocConnection(address, guid)) { + _replicationManager->PushConnection(connection); } + } - const auto streamers = world->FindVisibleStreamers(ent); + void NetworkServer::KickPlayer(MafiaNet::RakNetGUID guid, DisconnectionReason reason, const std::string &customReason) { + DisconnectPayload payload; + payload.reason = static_cast(reason); + payload.customReason = customReason; - for (const auto &streamer_ent : streamers) { - const auto streamer = streamer_ent.try_get(); - if (streamer->guid != guid.g && guid.g != MafiaNet::UNASSIGNED_RAKNET_GUID.g) { - continue; - } - if (streamer->guid == excludeGUID.g) { - continue; - } - _peer->Send(&bs, priority, reliability, 0, MafiaNet::RakNetGUID(streamer->guid), false); - } + // Reason rides the disconnect notification, so no separate message races the close. + MafiaNet::BitStream reasonData; + payload.Serialize(&reasonData, true); + _peer->CloseConnection(guid, true, 0, LOW_PRIORITY, &reasonData); + ClearClientState(guid); + } - return true; + void NetworkServer::ClearClientState(MafiaNet::RakNetGUID guid) { + _authenticatedClients.erase(guid.g); + _readyEvent.DeleteEvent(ReadyEventId(guid)); // recycle the slot for reconnects } } // namespace Framework::Networking diff --git a/code/framework/src/networking/network_server.h b/code/framework/src/networking/network_server.h index c69e46d43..0114773db 100644 --- a/code/framework/src/networking/network_server.h +++ b/code/framework/src/networking/network_server.h @@ -11,25 +11,31 @@ #include #include "errors.h" -#include "messages/messages.h" +#include "connection.h" #include "network_peer.h" -#include "world/server.h" +#include "rpc/rpc.h" #include #include #include #include +#include #include namespace Framework::Networking { + using ClientGuidCallback = fu2::function; + class NetworkServer: public NetworkPeer { private: - Messages::PacketCallback _onPlayerConnectCallback; - Messages::DisconnectPacketCallback _onPlayerDisconnectCallback; + PacketCallback _onPlayerConnectCallback; + DisconnectPacketCallback _onPlayerDisconnectCallback; + ClientGuidCallback _onClientAuthenticatedCallback; MafiaNet::FileListTransfer _fileListTransfer; - bool SendGameRPCInternal(MafiaNet::BitStream &bs, Framework::World::ServerEngine *world, flecs::entity_t ent, MafiaNet::RakNetGUID guid = MafiaNet::UNASSIGNED_RAKNET_GUID, MafiaNet::RakNetGUID excludeGUID = MafiaNet::UNASSIGNED_RAKNET_GUID, PacketPriority priority = HIGH_PRIORITY, - PacketReliability reliability = RELIABLE_ORDERED) const; + // Guids whose build challenge passed — the gate keeping unverified peers out of replication. + std::unordered_set _authenticatedClients; + + void ClearClientState(MafiaNet::RakNetGUID guid); public: NetworkServer(): NetworkPeer() {} @@ -39,27 +45,40 @@ namespace Framework::Networking { bool HandlePacket(uint8_t packetID, MafiaNet::Packet *packet) override; - template - bool SendGameRPC(Framework::World::ServerEngine *world, T &rpc, MafiaNet::RakNetGUID guid = MafiaNet::UNASSIGNED_RAKNET_GUID, MafiaNet::RakNetGUID excludeGUID = MafiaNet::UNASSIGNED_RAKNET_GUID, PacketPriority priority = HIGH_PRIORITY, - PacketReliability reliability = RELIABLE_ORDERED) { - MafiaNet::BitStream bs; - bs.Write(Messages::INTERNAL_RPC); - bs.Write(rpc.GetHashName()); - rpc.Serialize(&bs, true); - rpc.Serialize2(&bs, true); - assert(rpc.IsGameRPC() && "Regular RPCs cannot be sent via SendGameRPC()"); - - return SendGameRPCInternal(bs, world, rpc.GetServerID(), guid, excludeGUID, priority, reliability); - } + // Signal an RPC to every connected system except one (typically the originator) — the + // server-authoritative relay primitive, since RPC4::Signal has no exclusion parameter. The + // bitstream holds the already-written RPC arguments. + void SignalExcept(const char *identifier, MafiaNet::BitStream &bs, MafiaNet::RakNetGUID excludeGUID, PacketPriority priority = HIGH_PRIORITY, PacketReliability reliability = RELIABLE_ORDERED); int GetPing(MafiaNet::RakNetGUID guid) const; - void SetOnPlayerConnectCallback(Messages::PacketCallback callback) { + bool IsAuthenticated(MafiaNet::RakNetGUID guid) const { + return _authenticatedClients.contains(guid.g); + } + + // Start replicating to an authenticated peer (idempotent). Replication begins for no peer + // until this is called — connections are not auto-managed (see Init). + void PushReplicationConnection(MafiaNet::RakNetGUID guid); + + // Send a Kick RPC then close the connection. + void KickPlayer(MafiaNet::RakNetGUID guid, DisconnectionReason reason, const std::string &customReason = "") override; + + // Per-connection ReadyEvent id, derived from the slot so both ends agree without coordination. + static int ReadyEventId(MafiaNet::RakNetGUID guid) { + return static_cast(guid.systemIndex); + } + + void SetOnPlayerConnectCallback(PacketCallback callback) { _onPlayerConnectCallback = std::move(callback); } - void SetOnPlayerDisconnectCallback(Messages::DisconnectPacketCallback callback) { + void SetOnPlayerDisconnectCallback(DisconnectPacketCallback callback) { _onPlayerDisconnectCallback = std::move(callback); } + + // Fired when a peer's build challenge succeeds (integration responds with ServerResources). + void SetOnClientAuthenticatedCallback(ClientGuidCallback callback) { + _onClientAuthenticatedCallback = std::move(callback); + } }; } // namespace Framework::Networking diff --git a/code/framework/src/networking/replication/entity_registry.cpp b/code/framework/src/networking/replication/entity_registry.cpp new file mode 100644 index 000000000..9fdaef0ca --- /dev/null +++ b/code/framework/src/networking/replication/entity_registry.cpp @@ -0,0 +1,47 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#include "entity_registry.h" + +#include +#include + +namespace Framework::Networking::Replication { + EntityRegistry &EntityRegistry::Get() { + static EntityRegistry instance; + return instance; + } + + uint32_t EntityRegistry::TypeId(const std::string &name) const { + return Utils::Hashing::CalculateCRC32(name.c_str()); + } + + uint32_t EntityRegistry::Register(const std::string &name, Constructor constructor) { + const uint32_t id = TypeId(name); + const auto existing = _types.find(id); + // Re-registering the same name is benign (e.g. a second init); a different name on the same id + // is a CRC32 collision that would silently shadow the first type — surface it loudly. + if (existing != _types.end() && existing->second.name != name) { + Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->error("EntityRegistry: CRC32 collision — '{}' and '{}' both hash to {}; the latter shadows the former", existing->second.name, name, id); + } + _types[id] = Entry {id, name, std::move(constructor)}; + return id; + } + + NetworkEntity *EntityRegistry::Create(uint32_t typeId) const { + const auto it = _types.find(typeId); + if (it == _types.end() || !it->second.constructor) { + return nullptr; + } + NetworkEntity *entity = it->second.constructor(); + if (entity) { + entity->typeId = typeId; + } + return entity; + } +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/entity_registry.h b/code/framework/src/networking/replication/entity_registry.h new file mode 100644 index 000000000..78cbd3f73 --- /dev/null +++ b/code/framework/src/networking/replication/entity_registry.h @@ -0,0 +1,62 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "network_entity.h" + +#include + +#include +#include +#include +#include + +namespace Framework::Networking::Replication { + // Process-wide registry mapping a stable type id (CRC32 of a name) to a constructor for the + // concrete NetworkEntity subclass. The server creates entities by type id and the client's + // AllocReplica reconstructs them from the same id, so both sides must register identical types. + class EntityRegistry final { + public: + // The trailing `const` is required, not cosmetic: Create() is a const method and invokes the + // stored constructor through the const _types map, so the callable must be const-invocable. + using Constructor = fu2::function; + + static EntityRegistry &Get(); + + // Registers a type and returns its id. Logs if two distinct names collide on the same CRC32 + // (the later one would otherwise silently shadow the former). Not thread-safe: register every + // type at startup, before networking begins, because Create() runs on the sim/network path. + uint32_t Register(const std::string &name, Constructor constructor); + + // Convenience overload: default-constructs T. Prefer this — it removes the `[]{ return new T; }` + // boilerplate at every registration site. Defined out-of-class below. + template + uint32_t Register(const std::string &name); + + // Constructs an instance and stamps its typeId. Returns nullptr for an unknown id. + NetworkEntity *Create(uint32_t typeId) const; + + uint32_t TypeId(const std::string &name) const; + + private: + struct Entry { + uint32_t id = 0; + std::string name; // kept for collision diagnostics + Constructor constructor; + }; + std::unordered_map _types; + }; + + template + inline uint32_t EntityRegistry::Register(const std::string &name) { + return Register(name, [] { + return new T(); + }); + } +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/interest_grid.cpp b/code/framework/src/networking/replication/interest_grid.cpp new file mode 100644 index 000000000..9ca7e970f --- /dev/null +++ b/code/framework/src/networking/replication/interest_grid.cpp @@ -0,0 +1,127 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#include "interest_grid.h" + +#include + +namespace Framework::Networking::Replication { + namespace { + // Half-extent of a point entity's bounding box in the spatial index. GridSectorizer requires + // min < max (it asserts otherwise), so a point is inserted as a tiny box around its position. + constexpr float kPointEpsilon = 0.01f; + } // namespace + + void InterestGrid::Configure(float cellSize, float worldMin, float worldMax) { + _cellSize = cellSize; + _min = worldMin; + _max = worldMax; + _ready = false; // re-initialised on next BeginRebuild() + } + + void InterestGrid::BeginRebuild() { + if (!_ready) { + _grid.Init(_cellSize, _cellSize, _min, _min, _max, _max); + _ready = true; + } + _grid.Clear(); + _ownedByGuid.clear(); + _alwaysVisible.clear(); + } + + void InterestGrid::Insert(NetworkEntity *entity) { + if (!entity) { + return; + } + // GridSectorizer asserts on a zero-area entry, so insert a tiny box around the XZ position. + _grid.AddEntry(entity, entity->position.x - kPointEpsilon, entity->position.z - kPointEpsilon, entity->position.x + kPointEpsilon, entity->position.z + kPointEpsilon); + if (entity->ownerGUID != MafiaNet::UNASSIGNED_RAKNET_GUID.g) { + _ownedByGuid[entity->ownerGUID].insert(entity); + } + if (entity->streaming.alwaysVisible) { + _alwaysVisible.insert(entity); + } + } + + void InterestGrid::Remove(NetworkEntity *entity) { + for (auto it = _ownedByGuid.begin(); it != _ownedByGuid.end();) { + it->second.erase(entity); + if (it->second.empty()) { + it = _ownedByGuid.erase(it); + } + else { + ++it; + } + } + _alwaysVisible.erase(entity); + } + + void InterestGrid::QueryRadius(const glm::vec3 ¢er, float radius, std::unordered_set &out) { + if (!_ready) { + return; + } + DataStructures::List hits; + _grid.GetEntries(hits, center.x - radius, center.z - radius, center.x + radius, center.z + radius); + + const float radiusSq = radius * radius; + for (unsigned i = 0; i < hits.Size(); ++i) { + auto *entity = static_cast(hits[i]); + if (!entity) { + continue; + } + const glm::vec3 delta = entity->position - center; + // 2D (XZ) distance check; entries spanning multiple cells can repeat — the set dedupes. + if (delta.x * delta.x + delta.z * delta.z > radiusSq) { + continue; + } + out.insert(entity); + } + } + + const std::unordered_set *InterestGrid::OwnedBy(uint64_t guid) const { + const auto it = _ownedByGuid.find(guid); + return it != _ownedByGuid.end() ? &it->second : nullptr; + } + + void InterestGrid::CollectVisible(NetworkEntity *viewer, uint64_t viewerGUID, std::unordered_set &out) { + if (!viewer) { + return; + } + const auto observerWorld = viewer->GetVirtualWorld(); + + std::unordered_set inRange; + QueryRadius(viewer->position, viewer->streaming.range, inRange); + + // Owned and always-visible entities bypass range/dimension culling so they never drop out. + const auto visible = [&](NetworkEntity *entity) { + if (!entity || !entity->streaming.visible) { + return false; + } + return entity->streaming.alwaysVisible || entity == viewer || entity->ownerGUID == viewerGUID || (MafiaNet::VirtualWorldsCanSee(entity->GetVirtualWorld(), observerWorld) && inRange.contains(entity)); + }; + + const auto consider = [&](NetworkEntity *entity) { + if (visible(entity)) { + out.insert(entity); + } + }; + + for (NetworkEntity *entity : inRange) { + consider(entity); + } + if (const auto *owned = OwnedBy(viewerGUID)) { + for (NetworkEntity *entity : *owned) { + consider(entity); + } + } + for (NetworkEntity *entity : AlwaysVisible()) { + consider(entity); + } + consider(viewer); + } +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/interest_grid.h b/code/framework/src/networking/replication/interest_grid.h new file mode 100644 index 000000000..c4c2c31b0 --- /dev/null +++ b/code/framework/src/networking/replication/interest_grid.h @@ -0,0 +1,62 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "network_entity.h" + +#include + +#include + +#include +#include +#include + +namespace Framework::Networking::Replication { + // Server-side spatial interest index, split out of ReplicationManager so the relevance data and + // the queries over it live in one place. GridSectorizer has no incremental removal, so it is + // rebuilt from scratch each tick: BeginRebuild() then Insert() per live entity. The owned-by-guid + // and always-visible sets are rebuilt alongside it (kept authoritative against direct + // ownerGUID/streaming writes) and give ReplicationConnection::QueryReplicaList O(1) membership. + class InterestGrid { + public: + // Spatial index extent. Defaults cover a 20km² map at 100m cells (~40k cells). Pick bounds + // that enclose the playable area; entities outside clamp to edge cells (still found by radius + // queries, just less precisely). Takes effect on the next BeginRebuild(). + void Configure(float cellSize, float worldMin, float worldMax); + + // Start a fresh rebuild: (re)initialise the grid if needed, then clear it and the indices. + void BeginRebuild(); + // Add one live entity to the grid and the owned/always-visible indices. + void Insert(NetworkEntity *entity); + // Drop an entity from the indices so an intra-tick delete can't dangle before the next rebuild. + void Remove(NetworkEntity *entity); + + // Fill `out` with the entities relevant to `viewer`: in range and dimension, owned, or + // always-visible. The home of the server's relevance rule. + void CollectVisible(NetworkEntity *viewer, uint64_t viewerGUID, std::unordered_set &out); + + private: + // Range query on the XZ plane; the set dedupes per-cell hits. + void QueryRadius(const glm::vec3 ¢er, float radius, std::unordered_set &out); + + const std::unordered_set *OwnedBy(uint64_t guid) const; + const std::unordered_set &AlwaysVisible() const { + return _alwaysVisible; + } + + bool _ready = false; + float _cellSize = 100.0f; + float _min = -10000.0f; + float _max = 10000.0f; + GridSectorizer _grid; + std::unordered_map> _ownedByGuid; + std::unordered_set _alwaysVisible; + }; +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/network_entity.cpp b/code/framework/src/networking/replication/network_entity.cpp new file mode 100644 index 000000000..da5412952 --- /dev/null +++ b/code/framework/src/networking/replication/network_entity.cpp @@ -0,0 +1,205 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#include "network_entity.h" + +#include "replication_manager.h" + +#include + +namespace Framework::Networking::Replication { + ReplicationManager *NetworkEntity::Manager() { + return static_cast(replicaManager); + } + + const ReplicationManager *NetworkEntity::Manager() const { + return static_cast(replicaManager); + } + + bool NetworkEntity::IsServerPeer() const { + const auto *manager = Manager(); + return manager && manager->IsServer(); + } + + uint64_t NetworkEntity::MyGUID() const { + const auto *manager = Manager(); + return manager ? manager->GetMyGUID() : MafiaNet::UNASSIGNED_RAKNET_GUID.g; + } + + void NetworkEntity::AdoptIncomingOwner(uint64_t incomingOwner) { + // The server keeps its own authoritative owner assignment and must not let an owning client + // dictate it back; clients adopt whatever the server sends. + if (!IsServerPeer()) { + ownerGUID = incomingOwner; + } + } + + void NetworkEntity::WriteAllocationID(MafiaNet::Connection_RM3 *, MafiaNet::BitStream *allocationIdBitstream) const { + allocationIdBitstream->Write(typeId); + } + + void NetworkEntity::SerializeBaseState(MafiaNet::BitStream *bs, bool write) { + if (write) { + bs->Write(ownerGUID); + } + else { + uint64_t incomingOwner = ownerGUID; + bs->Read(incomingOwner); + AdoptIncomingOwner(incomingOwner); + } + bs->Serialize(write, position); + bs->Serialize(write, velocity); + bs->Serialize(write, rotation); + } + + void NetworkEntity::SerializeConstruction(MafiaNet::BitStream *constructionBitstream, MafiaNet::Connection_RM3 *) { + SerializeBaseState(constructionBitstream, true); + OnSerializeConstruction(constructionBitstream, true); + } + + bool NetworkEntity::DeserializeConstruction(MafiaNet::BitStream *constructionBitstream, MafiaNet::Connection_RM3 *) { + SerializeBaseState(constructionBitstream, false); + OnSerializeConstruction(constructionBitstream, false); + OnConstructed(); + return true; + } + + void NetworkEntity::SerializeDestruction(MafiaNet::BitStream *, MafiaNet::Connection_RM3 *) {} + + bool NetworkEntity::DeserializeDestruction(MafiaNet::BitStream *, MafiaNet::Connection_RM3 *) { + return true; + } + + void NetworkEntity::DeallocReplica(MafiaNet::Connection_RM3 *) { + delete this; + } + + void NetworkEntity::SerializeForcedState(MafiaNet::BitStream *bs, bool write) { + bs->Serialize(write, position); + bs->Serialize(write, rotation); + } + + void NetworkEntity::ForceState() { + if (auto *manager = Manager()) { + manager->ForceState(this); + } + } + + void NetworkEntity::SetOwner(uint64_t guid) { + if (auto *manager = Manager()) { + manager->SetOwner(this, guid); + } + else { + ownerGUID = guid; + } + } + + bool NetworkEntity::IsOwner() const { + if (ownerGUID == MyGUID()) { + return true; + } + // The server holds authority over entities left unowned (server-owned). + return IsServerPeer() && ownerGUID == MafiaNet::UNASSIGNED_RAKNET_GUID.g; + } + + // --- Per-tick delta serialization (VariableDeltaSerializer) --- + + void NetworkEntity::OnUserReplicaPreSerializeTick() { + // Reset the per-tick "already compared" flag so identical-broadcast caching works (see + // VariableDeltaSerializer::OnPreSerializeTick). Called once per replica per serialize tick. + _vds.OnPreSerializeTick(); + } + + MafiaNet::RM3SerializationResult NetworkEntity::Serialize(MafiaNet::SerializeParameters *serializeParameters) { + serializeParameters->messageTimestamp = MafiaNet::GetTime(); + + MafiaNet::VariableDeltaSerializer::SerializationContext ctx; + // whenLastSerialized == 0 means this is the first send to a fresh system: write every + // variable in full; otherwise only changed variables are written. + _vds.BeginIdenticalSerialize(&ctx, serializeParameters->whenLastSerialized == 0, &serializeParameters->outputBitstream[0]); + // ownerGUID stays explicit; the receiver filters it via AdoptIncomingOwner. + _vds.SerializeVariable(&ctx, ownerGUID); + FieldSerializer fields(&_vds, &ctx); + fields.Field(position); + fields.Field(velocity); + fields.Field(rotation); + SerializeFields(fields); + _vds.EndSerialize(&ctx); + + // BeginIdenticalSerialize already produces one delta bitstream shared across all recipients + // (the state is identical for every viewer), so pair it with the broadcast-identical result: + // ReplicaManager3 serializes once per tick and reuses those bytes for every connection, + // suppressing the send when nothing changed. Per-connection filtering (owner exclusion) still + // happens upstream in QuerySerializationWithinWorld. + return MafiaNet::RM3SR_BROADCAST_IDENTICALLY; + } + + void NetworkEntity::Deserialize(MafiaNet::DeserializeParameters *deserializeParameters) { + // Server authority gate: only accept state from the entity's current owner, rejecting a + // stale owner whose in-flight packets land after an ownership handover. + if (IsServerPeer() && deserializeParameters->sourceConnection && deserializeParameters->sourceConnection->GetRakNetGUID().g != ownerGUID) { + return; + } + + MafiaNet::VariableDeltaSerializer::DeserializationContext ctx; + _vds.BeginDeserialize(&ctx, &deserializeParameters->serializationBitstream[0]); + // Read into a temporary so the server can ignore a client-supplied owner (see + // AdoptIncomingOwner); clients adopt the owner the server sends. + uint64_t incomingOwner = ownerGUID; + _vds.DeserializeVariable(&ctx, incomingOwner); + AdoptIncomingOwner(incomingOwner); + FieldSerializer fields(&_vds, &ctx); + fields.Field(position); + fields.Field(velocity); + fields.Field(rotation); + SerializeFields(fields); + _vds.EndDeserialize(&ctx); + + // Already shifted to our local clock by RakPeer; do not subtract GetClockDifferential. + if (deserializeParameters->timeStamp != 0) { + lastUpdateTime = deserializeParameters->timeStamp; + } + } + + MafiaNet::Time NetworkEntity::GetUpdateAge() const { + if (lastUpdateTime == 0) { + return 0; + } + const MafiaNet::Time now = MafiaNet::GetTime(); + return now > lastUpdateTime ? now - lastUpdateTime : 0; + } + + glm::vec3 NetworkEntity::GetExtrapolatedPosition() const { + return position + velocity * (static_cast(GetUpdateAge()) / 1000.0f); + } + + MafiaNet::RM3ConstructionState NetworkEntity::QueryConstructionWithinWorld(MafiaNet::Connection_RM3 *destinationConnection, MafiaNet::ReplicaManager3 *) { + return QueryConstruction_ServerConstruction(destinationConnection, IsServerPeer()); + } + + bool NetworkEntity::QueryRemoteConstruction(MafiaNet::Connection_RM3 *sourceConnection) { + return QueryRemoteConstruction_ServerConstruction(sourceConnection, IsServerPeer()); + } + + MafiaNet::RM3QuerySerializationResult NetworkEntity::QuerySerializationWithinWorld(MafiaNet::Connection_RM3 *destinationConnection) { + if (IsServerPeer()) { + // Relay to everyone except the authoritative owner (no echo back to it). + if (destinationConnection->GetRakNetGUID().g == ownerGUID) { + return MafiaNet::RM3QSR_DO_NOT_CALL_SERIALIZE; + } + return MafiaNet::RM3QSR_CALL_SERIALIZE; + } + + // Client: only push upstream for entities we currently own. + return ownerGUID == MyGUID() ? MafiaNet::RM3QSR_CALL_SERIALIZE : MafiaNet::RM3QSR_DO_NOT_CALL_SERIALIZE; + } + + MafiaNet::RM3ActionOnPopConnection NetworkEntity::QueryActionOnPopConnection(MafiaNet::Connection_RM3 *droppedConnection) const { + return IsServerPeer() ? QueryActionOnPopConnection_Server(droppedConnection) : QueryActionOnPopConnection_Client(droppedConnection); + } +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/network_entity.h b/code/framework/src/networking/replication/network_entity.h new file mode 100644 index 000000000..7a2076887 --- /dev/null +++ b/code/framework/src/networking/replication/network_entity.h @@ -0,0 +1,160 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +#include + +namespace Framework::Networking::Replication { + class ReplicationManager; + class EntityRegistry; + + // Field() writes on the sender and reads on the receiver, so a replica's field list stays in sync. + class FieldSerializer final { + public: + FieldSerializer(MafiaNet::VariableDeltaSerializer *vds, MafiaNet::VariableDeltaSerializer::SerializationContext *ctx) : _vds(vds), _serialize(ctx) {} + FieldSerializer(MafiaNet::VariableDeltaSerializer *vds, MafiaNet::VariableDeltaSerializer::DeserializationContext *ctx) : _vds(vds), _deserialize(ctx) {} + + bool Writing() const { + return _serialize != nullptr; + } + + template + void Field(T &value) { + if (_serialize) { + _vds->SerializeVariable(_serialize, value); + } + else { + _vds->DeserializeVariable(_deserialize, value); + } + } + + private: + MafiaNet::VariableDeltaSerializer *_vds = nullptr; + MafiaNet::VariableDeltaSerializer::SerializationContext *_serialize = nullptr; + MafiaNet::VariableDeltaSerializer::DeserializationContext *_deserialize = nullptr; + }; + + // A replicated game object. Game entities derive from this and override SerializeFields (per-tick + // delta state) and/or OnSerializeConstruction (one-shot spawn state). + // + // Authority is keyed on ownerGUID: the server serializes to everyone except the owner, the owning + // client serializes upstream, and Deserialize accepts state only from the current owner. + class NetworkEntity : public MafiaNet::VirtualWorldReplica3 { + public: + NetworkEntity() = default; + ~NetworkEntity() override = default; + + // --- Common replicated state --- + glm::vec3 position = glm::vec3(0.0f); + glm::vec3 velocity = glm::vec3(0.0f); + glm::quat rotation = glm::identity(); + + // --- Authority (replicated) --- + uint64_t ownerGUID = MafiaNet::UNASSIGNED_RAKNET_GUID.g; + + // Local-clock send time of the last applied update (MafiaNet shifts it on receipt). Not replicated. + MafiaNet::Time lastUpdateTime = 0; + + // --- Server-only streaming metadata (never replicated; unused on the client) --- + // Grouped under `streaming` so the server-only nature is explicit and these don't read as + // per-entity wire state. Dimension lives in the VirtualWorldReplica3 base (Get/SetVirtualWorld). + struct Streaming { + bool alwaysVisible = false; // bypass interest culling; replicated to everyone + bool visible = true; // master visibility switch + bool isViewer = false; // drives a connection's interest set (the player's avatar) + float range = 100.0f; // interest radius (world units) when acting as a viewer + }; + Streaming streaming; + + // --- Game extension points --- + virtual void OnSerializeConstruction(MafiaNet::BitStream *bs, bool write) { + (void)bs; + (void)write; + } + virtual void SerializeFields(FieldSerializer &fields) { + (void)fields; + } + virtual void OnConstructed() {} + + // Server -> owner override of an owned entity (the owner is otherwise authoritative). Default + // carries the transform; override to add state, e.g. a vehicle's engine/config. + virtual void SerializeForcedState(MafiaNet::BitStream *bs, bool write); + + // Called on the owning client after SerializeForcedState has applied the forced fields. + virtual void OnStateForced() {} + + // Server: push this entity's forced state to its owner. No-op for unowned (server-owned) + // entities, which replicate to everyone normally. + void ForceState(); + + // Server: change this entity's owner. The new owner is told directly (the server withholds + // serialize to an owner, so it would otherwise never learn it gained authority); other peers + // and a revoked previous owner pick up the change through normal serialization. Pass + // UNASSIGNED_RAKNET_GUID.g to return ownership to the server. + void SetOwner(uint64_t guid); + + // True on the peer with authority over this entity: the owning client, or the server for + // server-owned entities. The game decides what owning means (bind the local avatar, drive + // updates upstream, ...); this just answers who holds authority. + bool IsOwner() const; + + MafiaNet::Time GetUpdateAge() const; + glm::vec3 GetExtrapolatedPosition() const; + + // --- Replica3 implementation --- + void WriteAllocationID(MafiaNet::Connection_RM3 *destinationConnection, MafiaNet::BitStream *allocationIdBitstream) const override; + void SerializeConstruction(MafiaNet::BitStream *constructionBitstream, MafiaNet::Connection_RM3 *destinationConnection) override; + bool DeserializeConstruction(MafiaNet::BitStream *constructionBitstream, MafiaNet::Connection_RM3 *sourceConnection) override; + void SerializeDestruction(MafiaNet::BitStream *destructionBitstream, MafiaNet::Connection_RM3 *destinationConnection) override; + bool DeserializeDestruction(MafiaNet::BitStream *destructionBitstream, MafiaNet::Connection_RM3 *sourceConnection) override; + void DeallocReplica(MafiaNet::Connection_RM3 *sourceConnection) override; + + void OnUserReplicaPreSerializeTick() override; + MafiaNet::RM3SerializationResult Serialize(MafiaNet::SerializeParameters *serializeParameters) override; + void Deserialize(MafiaNet::DeserializeParameters *deserializeParameters) override; + + bool QueryRemoteConstruction(MafiaNet::Connection_RM3 *sourceConnection) override; + MafiaNet::RM3ActionOnPopConnection QueryActionOnPopConnection(MafiaNet::Connection_RM3 *droppedConnection) const override; + + // VirtualWorldReplica3 filters by dimension, then delegates the topology decision to these. + MafiaNet::RM3ConstructionState QueryConstructionWithinWorld(MafiaNet::Connection_RM3 *destinationConnection, MafiaNet::ReplicaManager3 *replicaManager3) override; + MafiaNet::RM3QuerySerializationResult QuerySerializationWithinWorld(MafiaNet::Connection_RM3 *destinationConnection) override; + + private: + // The owning manager, typed. The base Replica3::replicaManager is a raw ReplicaManager3*; + // every entity belongs to one of ours, so this downcast is the single sanctioned place for it. + ReplicationManager *Manager(); + const ReplicationManager *Manager() const; + + // True if we are the server peer (read from the owning ReplicationManager). + bool IsServerPeer() const; + uint64_t MyGUID() const; + + // Apply an owner value received over the wire: the server is authoritative and ignores it; + // clients adopt it. Single source of truth for the rule shared by construction and deltas. + void AdoptIncomingOwner(uint64_t incomingOwner); + + void SerializeBaseState(MafiaNet::BitStream *bs, bool write); + + // CRC32 of the registered name; stamped by EntityRegistry, not game-settable. + uint32_t typeId = 0; + friend class EntityRegistry; + + // Tracks the last value of each serialized variable per connection so updates carry only + // what changed (the documented ReplicaManager3 delta path). + MafiaNet::VariableDeltaSerializer _vds; + }; +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/replication_connection.cpp b/code/framework/src/networking/replication/replication_connection.cpp new file mode 100644 index 000000000..0ce4b3114 --- /dev/null +++ b/code/framework/src/networking/replication/replication_connection.cpp @@ -0,0 +1,62 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#include "replication_connection.h" + +#include "entity_registry.h" +#include "network_entity.h" +#include "replication_manager.h" + +#include + +namespace Framework::Networking::Replication { + ReplicationConnection::ReplicationConnection(const MafiaNet::SystemAddress &systemAddress, MafiaNet::RakNetGUID guid, ReplicationManager *manager, bool isServer) + : Connection_RM3(systemAddress, guid), _manager(manager), _isServer(isServer) {} + + MafiaNet::Replica3 *ReplicationConnection::AllocReplica(MafiaNet::BitStream *allocationIdBitstream, MafiaNet::ReplicaManager3 *) { + uint32_t typeId = 0; + allocationIdBitstream->Read(typeId); + // The instance's state is populated by DeserializeConstruction (called immediately after); + // any backing game object is requested from NetworkEntity::OnConstructed. + return EntityRegistry::Get().Create(typeId); + } + + void ReplicationConnection::QueryReplicaList(DataStructures::List &newReplicasToCreate, DataStructures::List &existingReplicasToDestroy) { + // Only the server decides what exists on a remote system. + if (!_isServer || !_manager) { + return; + } + + NetworkEntity *viewer = _manager->GetViewer(GetRakNetGUID().g); + if (!viewer) { + // Connection not yet associated with a controlled entity (still handshaking). + return; + } + + // Keep the observer's dimension in sync with its avatar. + SetVirtualWorld(viewer->GetVirtualWorld()); + + std::unordered_set relevant; + _manager->CollectInterest(viewer, GetRakNetGUID().g, relevant); + + for (NetworkEntity *entity : relevant) { + if (!HasReplicaConstructed(entity)) { + newReplicasToCreate.Push(entity, _FILE_AND_LINE_); + } + } + + DataStructures::List constructed; + GetConstructedReplicas(constructed); + for (unsigned i = 0; i < constructed.Size(); ++i) { + auto *entity = static_cast(constructed[i]); + if (entity && !relevant.contains(entity)) { + existingReplicasToDestroy.Push(entity, _FILE_AND_LINE_); + } + } + } +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/replication_connection.h b/code/framework/src/networking/replication/replication_connection.h new file mode 100644 index 000000000..f5eb84313 --- /dev/null +++ b/code/framework/src/networking/replication/replication_connection.h @@ -0,0 +1,36 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include + +namespace Framework::Networking::Replication { + class ReplicationManager; + + // Per-remote-system state. On the client it constructs incoming replicas (AllocReplica); on the + // server it decides which replicas should exist on this connection (QueryReplicaList). It runs in + // QUERY_CONNECTION_FOR_REPLICA_LIST mode, so the streaming relevance rules live in + // QueryReplicaList rather than in Replica3::QueryConstruction/QueryDestruction. + class ReplicationConnection final : public MafiaNet::Connection_RM3 { + public: + ReplicationConnection(const MafiaNet::SystemAddress &systemAddress, MafiaNet::RakNetGUID guid, ReplicationManager *manager, bool isServer); + + MafiaNet::Replica3 *AllocReplica(MafiaNet::BitStream *allocationIdBitstream, MafiaNet::ReplicaManager3 *replicaManager3) override; + + ConstructionMode QueryConstructionMode() const override { + return QUERY_CONNECTION_FOR_REPLICA_LIST; + } + + void QueryReplicaList(DataStructures::List &newReplicasToCreate, DataStructures::List &existingReplicasToDestroy) override; + + private: + ReplicationManager *_manager = nullptr; + bool _isServer = false; + }; +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/replication_manager.cpp b/code/framework/src/networking/replication/replication_manager.cpp new file mode 100644 index 000000000..4cff0e97c --- /dev/null +++ b/code/framework/src/networking/replication/replication_manager.cpp @@ -0,0 +1,200 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#include "replication_manager.h" + +#include "../network_peer.h" +#include "entity_registry.h" +#include "replication_connection.h" + +namespace Framework::Networking::Replication { + namespace { + // Raw RPC: the tail is the entity's polymorphic SerializeForcedState payload. + constexpr const char *kForceStateId = "Framework::ForceState"; + + struct SetOwnerRPC { + static constexpr const char *kIdentifier = "Framework::SetOwner"; + + MafiaNet::NetworkID networkId; + uint64_t ownerGUID = 0; + + void Serialize(MafiaNet::BitStream *bs, bool write) { + bs->SerializeCompressed(write, networkId); + bs->Serialize(write, ownerGUID); + } + }; + } // namespace + + ReplicationManager::ReplicationManager() = default; + ReplicationManager::~ReplicationManager() = default; + + void ReplicationManager::ConfigureGrid(float cellSize, float worldMin, float worldMax) { + _interest.Configure(cellSize, worldMin, worldMax); + } + + void ReplicationManager::Init(NetworkPeer *owner, bool isServer) { + _owner = owner; + _isServer = isServer; + _myGUID = owner->GetPeer()->GetMyGUID().g; + SetNetworkIDManager(owner->GetNetworkIDManager()); + owner->GetPeer()->AttachPlugin(this); + + // Client-only: these are server->owner pushes, so the server must never accept them inbound. + if (!_isServer) { + owner->RegisterRawRPC(kForceStateId, [this](MafiaNet::BitStream *bs, MafiaNet::Packet *) { + MafiaNet::NetworkID networkId; + bs->ReadCompressed(networkId); + if (auto *entity = GetEntityByNetworkID(networkId)) { + entity->SerializeForcedState(bs, false); + entity->OnStateForced(); + } + }); + owner->RegisterRPC([this](const SetOwnerRPC &payload, MafiaNet::Packet *) { + if (auto *entity = GetEntityByNetworkID(payload.networkId)) { + entity->ownerGUID = payload.ownerGUID; + } + }); + } + } + + void ReplicationManager::ForceState(NetworkEntity *entity) { + if (!entity || !_owner || entity->ownerGUID == MafiaNet::UNASSIGNED_RAKNET_GUID.g) { + return; + } + MafiaNet::BitStream bs; + MafiaNet::NetworkID networkId = entity->GetNetworkID(); + // NetworkIDs are small and monotonic, so WriteCompressed strips the leading zero bytes. + bs.WriteCompressed(networkId); + entity->SerializeForcedState(&bs, true); + _owner->SendRawRPC(kForceStateId, bs, MafiaNet::RakNetGUID(entity->ownerGUID)); + } + + void ReplicationManager::SetOwner(NetworkEntity *entity, uint64_t guid) { + if (!entity) { + return; + } + entity->ownerGUID = guid; + // Serialize to an owner is withheld, so the grant can't ride normal replication: tell the new + // owner directly. Other peers (and any prior owner) pick it up through serialize. + if (_owner && _isServer && guid != MafiaNet::UNASSIGNED_RAKNET_GUID.g) { + SetOwnerRPC payload; + payload.networkId = entity->GetNetworkID(); + payload.ownerGUID = guid; + _owner->SendRPC(payload, MafiaNet::RakNetGUID(guid)); + } + } + + NetworkEntity *ReplicationManager::CreateEntity(uint32_t typeId) { + NetworkEntity *entity = EntityRegistry::Get().Create(typeId); + if (!entity) { + return nullptr; + } + // Assign a small, sequential id before Reference() so the NetworkIDManager tracks the entity + // under it. Ids must stay within JavaScript's 2^53 exact-integer range so scripts can hold + // them as plain numbers. Clients adopt this id via the construction snapshot. + entity->SetNetworkID(++_nextNetworkId); + Reference(entity); + if (_onEntityCreated) { + _onEntityCreated(entity->GetNetworkID()); + } + return entity; + } + + void ReplicationManager::DestroyEntity(NetworkEntity *entity) { + if (!entity) { + return; + } + // Only a viewer entity owns a viewer mapping; owned non-viewer entities share the owner GUID. + if (entity->streaming.isViewer && entity->ownerGUID != MafiaNet::UNASSIGNED_RAKNET_GUID.g) { + ClearViewer(entity->ownerGUID); + } + // Scrub the interest indices so this delete can't dangle before the next rebuild. + _interest.Remove(entity); + if (_onEntityDestroyed) { + _onEntityDestroyed(entity->GetNetworkID()); + } + // BroadcastDestruction must precede deletion; ~Replica3 dereferences automatically. + entity->BroadcastDestruction(); + delete entity; + } + + NetworkEntity *ReplicationManager::GetEntityByNetworkID(MafiaNet::NetworkID networkId) const { + auto *idm = GetNetworkIDManager(); + if (!idm) { + return nullptr; + } + return idm->GET_OBJECT_FROM_ID(networkId); + } + + void ReplicationManager::ForEachEntity(const fu2::function &fn) const { + const unsigned count = GetReplicaCount(); + for (unsigned i = 0; i < count; ++i) { + auto *entity = static_cast(GetReplicaAtIndex(i)); + if (entity) { + fn(entity); + } + } + } + + void ReplicationManager::SetViewer(uint64_t guid, NetworkEntity *entity) { + if (entity) { + entity->streaming.isViewer = true; + } + _viewers[guid] = entity; + } + + NetworkEntity *ReplicationManager::GetViewer(uint64_t guid) const { + const auto it = _viewers.find(guid); + return it != _viewers.end() ? it->second : nullptr; + } + + void ReplicationManager::ClearViewer(uint64_t guid) { + _viewers.erase(guid); + } + + void ReplicationManager::RebuildInterest() { + if (!_isServer) { + return; + } + _interest.BeginRebuild(); + ForEachEntity([this](NetworkEntity *entity) { + _interest.Insert(entity); + }); + } + + void ReplicationManager::CollectInterest(NetworkEntity *viewer, uint64_t viewerGUID, std::unordered_set &out) { + _interest.CollectVisible(viewer, viewerGUID, out); + } + + void ReplicationManager::OnClosedConnection(const MafiaNet::SystemAddress &systemAddress, MafiaNet::RakNetGUID rakNetGUID, MafiaNet::PI2_LostConnectionReason lostConnectionReason) { + // The player's avatar is server-created, so the base PopConnection (which only tears down + // replicas a dropped peer itself created) leaves it behind. Notify the game while the avatar + // is still resolvable, then destroy it — DestroyEntity broadcasts the destruction to the + // remaining clients and clears the viewer mapping. Clients keep the base behaviour: their + // replicas all originate from the server, so PopConnection cleans them up on its own. + if (_isServer) { + if (_onClientDisconnect) { + _onClientDisconnect(rakNetGUID.g); + } + if (auto *viewer = GetViewer(rakNetGUID.g)) { + DestroyEntity(viewer); + } + } + ReplicaManager3::OnClosedConnection(systemAddress, rakNetGUID, lostConnectionReason); + } + + MafiaNet::Connection_RM3 *ReplicationManager::AllocConnection(const MafiaNet::SystemAddress &systemAddress, MafiaNet::RakNetGUID rakNetGUID) const { + // ReplicaManager3 declares this const, but the connection needs a mutable manager back-pointer + // for its QueryReplicaList interest queries; the const_cast is forced by the upstream API. + return new ReplicationConnection(systemAddress, rakNetGUID, const_cast(this), _isServer); + } + + void ReplicationManager::DeallocConnection(MafiaNet::Connection_RM3 *connection) const { + delete connection; + } +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/replication/replication_manager.h b/code/framework/src/networking/replication/replication_manager.h new file mode 100644 index 000000000..bedb4cc45 --- /dev/null +++ b/code/framework/src/networking/replication/replication_manager.h @@ -0,0 +1,123 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "interest_grid.h" +#include "network_entity.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace Framework::Networking { + class NetworkPeer; +} // namespace Framework::Networking + +namespace Framework::Networking::Replication { + // The replicated world: a ReplicaManager3 that owns the set of NetworkEntity objects. It + // creates/destroys entities, resolves them by NetworkID, tracks each connection's "viewer" + // entity, and drives an InterestGrid that ReplicationConnection::QueryReplicaList reads for + // interest management. + class ReplicationManager final : public MafiaNet::ReplicaManager3 { + public: + ReplicationManager(); + ~ReplicationManager(); + + void Init(NetworkPeer *owner, bool isServer); + + // Server: push the entity's forced state to its owner — the server's authoritative override + // of an owned entity (see NetworkEntity::ForceState / OnStateForced). No-op for unowned + // entities, which already replicate to everyone. + void ForceState(NetworkEntity *entity); + + // Server: change an entity's owner and notify the new owner directly (see + // NetworkEntity::SetOwner). Needed because serialize to an owner is withheld, so the grant + // can't ride normal replication. + void SetOwner(NetworkEntity *entity, uint64_t guid); + + bool IsServer() const { + return _isServer; + } + uint64_t GetMyGUID() const { + return _myGUID; + } + + // --- Entity lifecycle --- + // Server: construct and start replicating an entity of the given type, nullptr if unknown. + // Non-owning: the manager owns it; destroy via DestroyEntity. + NetworkEntity *CreateEntity(uint32_t typeId); + // Broadcast destruction and delete the entity. + void DestroyEntity(NetworkEntity *entity); + NetworkEntity *GetEntityByNetworkID(MafiaNet::NetworkID networkId) const; + void ForEachEntity(const fu2::function &fn) const; + + // --- Viewers (a connection's controlled entity, e.g. a player's avatar) --- + void SetViewer(uint64_t guid, NetworkEntity *entity); + NetworkEntity *GetViewer(uint64_t guid) const; + void ClearViewer(uint64_t guid); + + // --- Interest management --- + // Configure the spatial index extent (see InterestGrid::Configure). Call before the first + // RebuildInterest(). + void ConfigureGrid(float cellSize, float worldMin, float worldMax); + // Rebuild the spatial index from current entity positions. Server only; call once per tick + // before ReplicaManager3 serializes (driven from NetworkPeer::Update). + void RebuildInterest(); + void CollectInterest(NetworkEntity *viewer, uint64_t viewerGUID, std::unordered_set &out); + + // Server: invoked from OnClosedConnection just before the dropped peer's avatar is destroyed, + // while it is still resolvable. The integration layer wires its player-disconnect notification + // here. + void SetOnClientDisconnect(fu2::function callback) { + _onClientDisconnect = std::move(callback); + } + + // Fired with the NetworkID at the end of CreateEntity / start of DestroyEntity. + void SetOnEntityCreated(fu2::function callback) { + _onEntityCreated = std::move(callback); + } + void SetOnEntityDestroyed(fu2::function callback) { + _onEntityDestroyed = std::move(callback); + } + + // --- ReplicaManager3 hooks --- + // Connection-drop teardown. The base only removes replicas a dropped peer itself created; + // player avatars are server-created, so on the server we additionally notify the game and + // destroy the dropped peer's viewer (DestroyEntity broadcasts the destruction to remaining + // clients), which is the missing half that otherwise leaks avatars across reconnects. + void OnClosedConnection(const MafiaNet::SystemAddress &systemAddress, MafiaNet::RakNetGUID rakNetGUID, MafiaNet::PI2_LostConnectionReason lostConnectionReason) override; + + MafiaNet::Connection_RM3 *AllocConnection(const MafiaNet::SystemAddress &systemAddress, MafiaNet::RakNetGUID rakNetGUID) const override; + void DeallocConnection(MafiaNet::Connection_RM3 *connection) const override; + + private: + bool _isServer = false; + uint64_t _myGUID = MafiaNet::UNASSIGNED_RAKNET_GUID.g; + // Server-side monotonic NetworkID allocator. Starts at 1 (0 reads as "none" in game code) and + // stays well within JavaScript's safe-integer range so scripting can hold ids as plain numbers. + // Bumped only from CreateEntity on the sim thread, so it needs no synchronization. + uint64_t _nextNetworkId = 0; + NetworkPeer *_owner = nullptr; + InterestGrid _interest; + std::unordered_map _viewers; + fu2::function _onClientDisconnect; + fu2::function _onEntityCreated; + fu2::function _onEntityDestroyed; + }; +} // namespace Framework::Networking::Replication diff --git a/code/framework/src/networking/rpc/chat_message.h b/code/framework/src/networking/rpc/chat_message.h new file mode 100644 index 000000000..677fc034e --- /dev/null +++ b/code/framework/src/networking/rpc/chat_message.h @@ -0,0 +1,28 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "rpc.h" + +#include + +namespace Framework::Networking::RPC { + // A chat line. Client->server carries a player's outgoing text (the server resolves the sender + // from the connection); server->client carries a line to display. Lines starting with '/' are + // parsed into a command and arguments on the server. + struct ChatMessage { + static constexpr const char *kIdentifier = "Framework::ChatMessage"; + + std::string text; + + void Serialize(MafiaNet::BitStream *bs, bool write) { + bs->Serialize(write, text); + } + }; +} // namespace Framework::Networking::RPC diff --git a/code/framework/src/networking/rpc/client_identity.h b/code/framework/src/networking/rpc/client_identity.h new file mode 100644 index 000000000..dfe92818e --- /dev/null +++ b/code/framework/src/networking/rpc/client_identity.h @@ -0,0 +1,33 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "rpc.h" + +#include + +namespace Framework::Networking::RPC { + // Client -> server after assets download: announces the player. Only honoured for an + // authenticated connection (NetworkServer::IsAuthenticated). + struct ClientIdentity { + static constexpr const char *kIdentifier = "Framework::ClientIdentity"; + + std::string name; + std::string steamId; + std::string discordId; + std::string hardwareId; + + void Serialize(MafiaNet::BitStream *bs, bool write) { + bs->Serialize(write, name); + bs->Serialize(write, steamId); + bs->Serialize(write, discordId); + bs->Serialize(write, hardwareId); + } + }; +} // namespace Framework::Networking::RPC diff --git a/code/framework/src/networking/rpc/game_rpc.h b/code/framework/src/networking/rpc/game_rpc.h deleted file mode 100644 index 419f4bf3f..000000000 --- a/code/framework/src/networking/rpc/game_rpc.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -#include "world/modules/base.hpp" - -namespace Framework::Networking::RPC { - template - class IGameRPC { - private: - MafiaNet::Packet *packet {}; - uint32_t _hashName = 0; - std::string _rpcName; - - protected: - flecs::entity_t _serverID = 0; - - public: - IGameRPC(): _rpcName(typeid(T).name()), _hashName(Utils::Hashing::CalculateCRC32(typeid(T).name())) {}; - void SetServerID(flecs::entity_t serverID) { - _serverID = serverID; - } - - flecs::entity_t GetServerID() const { - return _serverID; - } - - const std::string& GetName() const { - return _rpcName; - } - - virtual void Serialize(MafiaNet::BitStream *bs, bool write) = 0; - virtual bool Valid() const = 0; - - void Serialize2(MafiaNet::BitStream *bs, bool write) { - bs->Serialize(write, _serverID); - }; - - inline bool ValidServerID() const { - return _serverID > 0; - } - - /** - * Validates if the server id was set. - * @return - */ - bool Valid2() const { - return ValidServerID(); - } - - uint32_t GetHashName() const { - return _hashName; - } - - void SetPacket(MafiaNet::Packet *p) { - packet = p; - } - - MafiaNet::Packet *GetPacket() const { - return packet; - } - - bool IsGameRPC() const { - return true; - } - }; -} // namespace Framework::Networking::RPC diff --git a/code/framework/src/networking/rpc/rpc.h b/code/framework/src/networking/rpc/rpc.h index 15c9c2df3..f6bba3abc 100644 --- a/code/framework/src/networking/rpc/rpc.h +++ b/code/framework/src/networking/rpc/rpc.h @@ -9,46 +9,21 @@ #pragma once #include -#include -#include -#include -#include - -#include namespace Framework::Networking::RPC { - template - class IRPC { - private: - MafiaNet::Packet *packet {}; - uint32_t _hashName = 0; - std::string _rpcName; - - public: - virtual ~IRPC() = default; - IRPC(): _rpcName(typeid(T).name()), _hashName(Utils::Hashing::CalculateCRC32(typeid(T).name())) {}; - - virtual void Serialize(MafiaNet::BitStream *bs, bool write) = 0; - virtual bool Valid() const = 0; - - uint32_t GetHashName() const { - return _hashName; - } - - const std::string &GetName() const { - return _rpcName; - } - - void SetPacket(MafiaNet::Packet *p) { - packet = p; - } - - MafiaNet::Packet *GetPacket() const { - return packet; - } - - bool IsGameRPC() const { - return false; - } - }; + // An RPC is a payload struct that provides: + // static constexpr const char *kIdentifier; // unique, identical on both peers + // void Serialize(MafiaNet::BitStream *bs, bool write); // symmetric read/write + // + // Register a handler with NetworkPeer::RegisterRPC and send with NetworkPeer::BroadcastRPC / + // SendRPC (and NetworkServer::BroadcastRPCExcept). A handler is a function of shape + // void(MafiaNet::BitStream *, MafiaNet::Packet *); decode its payload with Read. To target a + // specific entity, give the payload a MafiaNet::NetworkID field and resolve it through the + // ReplicationManager in the handler. + template + inline T Read(MafiaNet::BitStream *bs) { + T payload {}; + payload.Serialize(bs, false); + return payload; + } } // namespace Framework::Networking::RPC diff --git a/code/framework/src/networking/rpc/server_resources.h b/code/framework/src/networking/rpc/server_resources.h new file mode 100644 index 000000000..4439712ab --- /dev/null +++ b/code/framework/src/networking/rpc/server_resources.h @@ -0,0 +1,68 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "rpc.h" + +#include + +#include +#include +#include +#include +#include + +namespace Framework::Networking::RPC { + struct ResourceInfo { + std::string name; + std::string version; + + void Serialize(MafiaNet::BitStream *bs, bool write) { + bs->Serialize(write, name); + bs->Serialize(write, version); + } + }; + + // Server -> client once the build challenge passes: opens the asset phase. readyEventId is the + // per-connection ReadyEvent id both peers use as the spawn barrier; tickRate is the serialize + // interval (s) the client applies once that barrier completes. + struct ServerResources { + static constexpr const char *kIdentifier = "Framework::ServerResources"; + static constexpr uint16_t kMaxResources = 1000; // bound untrusted input + + int32_t readyEventId = 0; + float tickRate = 0.0f; + std::vector resources; + + void Serialize(MafiaNet::BitStream *bs, bool write) { + bs->Serialize(write, readyEventId); + bs->Serialize(write, tickRate); + + if (write && resources.size() > std::numeric_limits::max()) { + Logging::GetLogger(FRAMEWORK_INNER_NETWORKING)->error("ServerResources holds {} resources, exceeding the wire limit; truncating", resources.size()); + } + + uint16_t count = static_cast(std::min(resources.size(), std::numeric_limits::max())); + bs->Serialize(write, count); + if (!write) { + resources.clear(); + resources.resize(std::min(count, kMaxResources)); + } + for (uint16_t i = 0; i < count; ++i) { + // Entries past the sane cap are still consumed so the bitstream stays aligned. + if (!write && i >= kMaxResources) { + ResourceInfo discard; + discard.Serialize(bs, write); + continue; + } + resources[i].Serialize(bs, write); + } + } + }; +} // namespace Framework::Networking::RPC diff --git a/code/framework/src/scripting/builtins/chat.h b/code/framework/src/scripting/builtins/chat.h new file mode 100644 index 000000000..7c6070a2a --- /dev/null +++ b/code/framework/src/scripting/builtins/chat.h @@ -0,0 +1,76 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "entity.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +namespace Framework::Scripting::Builtins { + // Server-side chat API exposed to scripts as the global `Chat`. Reusable across mods: it sends + // the framework ChatMessage RPC and targets players through the base Entity handle (a mod's own + // Player/Human handle resolves to Entity via v8pp inheritance), so it needs no game-specific type. + class Chat { + public: + static void SendToAll(std::string message) { + Send(message, MafiaNet::UNASSIGNED_RAKNET_GUID, true); + } + + // The argument is any replicated entity handle; the message is delivered to that entity's + // owning connection (typically a player's avatar). + static void SendToPlayer(Entity *entity, std::string message) { + if (!entity) { + return; + } + auto *handle = entity->GetHandle(); + if (!handle) { + return; + } + Send(message, MafiaNet::RakNetGUID(handle->ownerGUID), false); + } + + static void Register(v8::Isolate *isolate, v8::Local global) { + if (!isolate || global.IsEmpty()) { + return; + } + v8pp::module chatModule(isolate); + chatModule.function("sendToAll", &Chat::SendToAll); + chatModule.function("sendToPlayer", &Chat::SendToPlayer); + + auto ctx = isolate->GetCurrentContext(); + global->Set(ctx, v8pp::to_v8(isolate, "Chat"), chatModule.new_instance()).Check(); + } + + private: + static void Send(const std::string &message, MafiaNet::RakNetGUID target, bool broadcast) { + auto *net = CoreModules::GetNetworkPeer(); + if (!net) { + return; + } + Networking::RPC::ChatMessage payload {message}; + if (broadcast) { + net->BroadcastRPC(payload); + } + else { + net->SendRPC(payload, target); + } + } + }; +} // namespace Framework::Scripting::Builtins diff --git a/code/framework/src/scripting/builtins/entity.h b/code/framework/src/scripting/builtins/entity.h new file mode 100644 index 000000000..0f46eec34 --- /dev/null +++ b/code/framework/src/scripting/builtins/entity.h @@ -0,0 +1,192 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "quaternion.h" +#include "vector3.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Framework::Scripting::Builtins { + // Base scripting handle for a replicated entity, reusable across mods. Wraps the entity's + // NetworkID and resolves the live NetworkEntity on demand through the world engine, so a stale JS + // handle to a destroyed entity resolves to null instead of dangling. Exposes the common transform + // (position/rotation); writing it goes through NetworkEntity::ForceState, so the server's value is + // authoritative even over an entity a client owns. Mods derive their own handles (player, vehicle) + // via v8pp inherit() and add their game-specific properties. + // + // Header-only so it compiles against whichever V8 the including target links (libnode on the + // server, standalone V8 on the client). + class Entity { + public: + Entity(uint64_t networkId): _id(networkId) { + if (!Resolve()) { + throw std::runtime_error(fmt::format("Entity handle '{}' is not valid!", networkId)); + } + } + virtual ~Entity() = default; + + uint64_t GetId() const { + return _id; + } + + Networking::Replication::NetworkEntity *GetHandle() const { + return Resolve(); + } + + Vector3 GetPosition() const { + if (auto *e = Resolve()) { + return Vector3(e->position.x, e->position.y, e->position.z); + } + return Vector3(0, 0, 0); + } + + void SetPosition(const Vector3 &pos) { + if (auto *e = Resolve()) { + e->position = pos.vec(); + e->ForceState(); + } + } + + Vector3 GetRotation() const { + if (auto *e = Resolve()) { + glm::vec3 euler = glm::eulerAngles(e->rotation); + return Vector3(glm::degrees(euler.x), glm::degrees(euler.y), glm::degrees(euler.z)); + } + return Vector3(0, 0, 0); + } + + void SetRotationFromEuler(const Vector3 &rot) { + if (auto *e = Resolve()) { + glm::vec3 radians(glm::radians(rot.vec().x), glm::radians(rot.vec().y), glm::radians(rot.vec().z)); + e->rotation = glm::quat(radians); + e->ForceState(); + } + } + + void SetRotationFromQuaternion(const Quaternion &quat) { + if (auto *e = Resolve()) { + e->rotation = quat.quat(); + e->ForceState(); + } + } + + virtual std::string ToString() const { + std::ostringstream ss; + ss << "Entity{ id: " << _id << " }"; + return ss.str(); + } + + static v8pp::class_ &GetClass(v8::Isolate *isolate) { + auto it = _classes.find(isolate); + if (it != _classes.end()) { + return *it->second; + } + + auto &cls = _classes[isolate]; + cls = std::make_unique>(isolate); + cls->auto_wrap_objects(true); + cls->ctor() + .function("toString", &Entity::ToString); + + auto protoTemplate = cls->class_function_template()->PrototypeTemplate(); + + // Read-only property: id + protoTemplate->SetNativeDataProperty( + v8pp::to_v8(isolate, "id").As(), + [](v8::Local, const v8::PropertyCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (self) info.GetReturnValue().Set(static_cast(self->GetId())); + }); + + // Property: position (Vector3). An accessor pair so the setter fires when a script + // assigns to it through the prototype chain. + { + auto positionGetter = v8::FunctionTemplate::New(isolate, [](const v8::FunctionCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (self) { + auto pos = self->GetPosition(); + auto &vecCls = Vector3::GetClass(info.GetIsolate()); + info.GetReturnValue().Set(vecCls.import_external(info.GetIsolate(), new Vector3(pos))); + } + }); + auto positionSetter = v8::FunctionTemplate::New(isolate, [](const v8::FunctionCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (!self || info.Length() < 1) return; + auto *vec = v8pp::class_::unwrap_object(info.GetIsolate(), info[0]); + if (vec) self->SetPosition(*vec); + }); + protoTemplate->SetAccessorProperty(v8pp::to_v8(isolate, "position").As(), positionGetter, positionSetter); + } + + // Property: rotation (accepts both Vector3 euler degrees and Quaternion). + { + auto rotationGetter = v8::FunctionTemplate::New(isolate, [](const v8::FunctionCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (self) { + auto rot = self->GetRotation(); + auto &vecCls = Vector3::GetClass(info.GetIsolate()); + info.GetReturnValue().Set(vecCls.import_external(info.GetIsolate(), new Vector3(rot))); + } + }); + auto rotationSetter = v8::FunctionTemplate::New(isolate, [](const v8::FunctionCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (!self || info.Length() < 1) return; + + auto *vec = v8pp::class_::unwrap_object(info.GetIsolate(), info[0]); + if (vec) { + self->SetRotationFromEuler(*vec); + return; + } + auto *quat = v8pp::class_::unwrap_object(info.GetIsolate(), info[0]); + if (quat) { + self->SetRotationFromQuaternion(*quat); + return; + } + info.GetIsolate()->ThrowException(v8::Exception::TypeError( + v8pp::to_v8(info.GetIsolate(), "rotation must be a Vector3 (euler degrees) or Quaternion"))); + }); + protoTemplate->SetAccessorProperty(v8pp::to_v8(isolate, "rotation").As(), rotationGetter, rotationSetter); + } + + return *cls; + } + + static void Register(v8::Isolate *isolate, v8::Local global) { + v8pp::class_ &cls = GetClass(isolate); + auto ctx = isolate->GetCurrentContext(); + global->Set(ctx, v8pp::to_v8(isolate, "Entity"), cls.js_function_template()->GetFunction(ctx).ToLocalChecked()).Check(); + } + + protected: + Networking::Replication::NetworkEntity *Resolve() const { + auto *replication = CoreModules::GetReplication(); + return replication ? replication->GetEntityByNetworkID(_id) : nullptr; + } + + uint64_t _id = 0; + inline static std::unordered_map>> _classes; + }; +} // namespace Framework::Scripting::Builtins diff --git a/code/framework/src/scripting/builtins/player.h b/code/framework/src/scripting/builtins/player.h new file mode 100644 index 000000000..19cc757da --- /dev/null +++ b/code/framework/src/scripting/builtins/player.h @@ -0,0 +1,76 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include "entity.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Framework::Scripting::Builtins { + // A connection's player entity. Connection-level ops (kick, ...) live here, not on Entity, so + // they stay off non-player entities. Mods derive via inherit(). + class Player: public Entity { + public: + Player(uint64_t networkId): Entity(networkId) {} + + // Server-only; a no-op on the client (see NetworkPeer::KickPlayer). + void Kick(const std::string &reason) { + auto *entity = Resolve(); + if (!entity) { + return; + } + auto *peer = CoreModules::GetNetworkPeer(); + if (!peer) { + return; + } + peer->KickPlayer(MafiaNet::RakNetGUID(entity->ownerGUID), + reason.empty() ? Networking::DisconnectionReason::KICKED : Networking::DisconnectionReason::KICKED_CUSTOM, + reason); + } + + std::string ToString() const override { + std::ostringstream ss; + ss << "Player{ id: " << _id << " }"; + return ss.str(); + } + + static v8pp::class_ &GetClass(v8::Isolate *isolate) { + auto it = _classes.find(isolate); + if (it != _classes.end()) { + return *it->second; + } + + // v8pp inherit requires Entity registered first. + Entity::GetClass(isolate); + + auto &cls = _classes[isolate]; + cls = std::make_unique>(isolate); + cls->auto_wrap_objects(true); + cls->inherit() + .ctor() + .function("toString", &Player::ToString) + .function("kick", &Player::Kick); + return *cls; + } + + protected: + inline static std::unordered_map>> _classes; + }; +} // namespace Framework::Scripting::Builtins diff --git a/code/framework/src/scripting/builtins/property.h b/code/framework/src/scripting/builtins/property.h new file mode 100644 index 000000000..1e051deff --- /dev/null +++ b/code/framework/src/scripting/builtins/property.h @@ -0,0 +1,150 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace Framework::Scripting::Builtins { + // Helpers to register JS accessor properties on a v8pp-wrapped class from plain getter/setter + // member functions. V8 accessor callbacks must be non-capturing function pointers, so the + // getter/setter are passed as template parameters (compile-time constants) rather than captured. + // + // RegisterProperty(isolate, proto, "x"); // scalar/string + // RegisterReadonlyProperty(isolate, proto, "x"); + // RegisterObjectProperty(...); // wrapped value + // RegisterReadonlyObjectProperty(...); + + namespace detail { + template + struct MemberReturn; + template + struct MemberReturn { + using type = R; + }; + template + struct MemberReturn { + using type = R; + }; + + template + struct MemberArg; + template + struct MemberArg { + using type = A; + }; + + // Push a scalar/string value onto the V8 return slot. Generic over the callback-info type so + // it serves both property accessors and FunctionTemplate-based accessors. + template + inline void Return(const Info &info, T &&value) { + using V = std::decay_t; + if constexpr (std::is_same_v) { + info.GetReturnValue().Set(v8pp::to_v8(info.GetIsolate(), value)); + } + else { + info.GetReturnValue().Set(value); + } + } + + // Apply a JS value to a scalar/string setter, ignoring mismatched types (as before). + template + inline void Apply(v8::Isolate *isolate, v8::Local value, Fn &&apply) { + using A = std::decay_t; + auto ctx = isolate->GetCurrentContext(); + if constexpr (std::is_same_v) { + if (value->IsBoolean()) apply(value->BooleanValue(isolate)); + } + else if constexpr (std::is_same_v) { + if (value->IsString()) { + v8::String::Utf8Value str(isolate, value); + apply(std::string(*str ? *str : "")); + } + } + else if constexpr (std::is_floating_point_v) { + if (value->IsNumber()) apply(static_cast(value->NumberValue(ctx).FromMaybe(0.0))); + } + else if constexpr (std::is_integral_v) { + // Any JS number, not just int32, so values up to 2^53 (e.g. NetworkIDs) round-trip. + if (value->IsNumber()) apply(static_cast(value->IntegerValue(ctx).FromMaybe(0))); + } + } + } // namespace detail + + // Read/write scalar or string property. Installed as an accessor pair so the setter fires when a + // script assigns to the property through the prototype chain. + template + void RegisterProperty(v8::Isolate *isolate, v8::Local proto, const char *name) { + auto getter = v8::FunctionTemplate::New(isolate, [](const v8::FunctionCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (self) detail::Return(info, (self->*Getter)()); + }); + auto setter = v8::FunctionTemplate::New(isolate, [](const v8::FunctionCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (!self || info.Length() < 1) return; + using Arg = typename detail::MemberArg::type; + detail::Apply(info.GetIsolate(), info[0], [&](auto &&v) { + (self->*Setter)(std::forward(v)); + }); + }); + proto->SetAccessorProperty(v8pp::to_v8(isolate, name).As(), getter, setter); + } + + // Read-only scalar or string property. + template + void RegisterReadonlyProperty(v8::Isolate *isolate, v8::Local proto, const char *name) { + proto->SetNativeDataProperty( + v8pp::to_v8(isolate, name).As(), + [](v8::Local, const v8::PropertyCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (self) detail::Return(info, (self->*Getter)()); + }); + } + + // Read/write property whose value is itself a v8pp-wrapped class returned/taken by value + // (e.g. Color, Vector3). The value type is deduced from the getter; its result is copied into a + // fresh JS-owned object. + template + void RegisterObjectProperty(v8::Isolate *isolate, v8::Local proto, const char *name) { + using Value = std::decay_t::type>; + auto getter = v8::FunctionTemplate::New(isolate, [](const v8::FunctionCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (self) { + auto &cls = Value::GetClass(info.GetIsolate()); + info.GetReturnValue().Set(cls.import_external(info.GetIsolate(), new Value((self->*Getter)()))); + } + }); + auto setter = v8::FunctionTemplate::New(isolate, [](const v8::FunctionCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (!self || info.Length() < 1) return; + auto *val = v8pp::class_::unwrap_object(info.GetIsolate(), info[0]); + if (val) (self->*Setter)(*val); + }); + proto->SetAccessorProperty(v8pp::to_v8(isolate, name).As(), getter, setter); + } + + // Read-only wrapped-value property (e.g. Vector3). The value type is deduced from the getter. + template + void RegisterReadonlyObjectProperty(v8::Isolate *isolate, v8::Local proto, const char *name) { + using Value = std::decay_t::type>; + proto->SetNativeDataProperty( + v8pp::to_v8(isolate, name).As(), + [](v8::Local, const v8::PropertyCallbackInfo &info) { + auto *self = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (self) { + auto &cls = Value::GetClass(info.GetIsolate()); + info.GetReturnValue().Set(cls.import_external(info.GetIsolate(), new Value((self->*Getter)()))); + } + }); + } +} // namespace Framework::Scripting::Builtins diff --git a/code/framework/src/scripting/resource/resource.cpp b/code/framework/src/scripting/resource/resource.cpp index b1573ae01..a12bd0bce 100644 --- a/code/framework/src/scripting/resource/resource.cpp +++ b/code/framework/src/scripting/resource/resource.cpp @@ -7,7 +7,10 @@ */ #include "resource.h" -#include "world/engine.h" + +#include +#include + #include namespace Framework::Scripting { @@ -24,7 +27,7 @@ namespace Framework::Scripting { } } - Resource::Resource(const std::string &path, flecs::world* world) + Resource::Resource(const std::string &path) : _path(path) , _stateTimestamp(std::chrono::system_clock::now()) { // Try to load the manifest @@ -35,16 +38,9 @@ namespace Framework::Scripting { _errorMessage = _manifest.GetError(); _state = ResourceState::Error; } - - _rootEntity = world->entity(_manifest.GetName().c_str()); - _rootEntity.set({this}); } Resource::~Resource() { - if (_rootEntity.is_valid()) { - _rootEntity.destruct(); - } - ClearExports(); } @@ -57,9 +53,9 @@ namespace Framework::Scripting { , _stateTimestamp(other._stateTimestamp) , _loadTimestamp(other._loadTimestamp) , _isolate(other._isolate) - , _rootEntity(other._rootEntity) , _exports(std::move(other._exports)) - , _restartAttempts(std::move(other._restartAttempts)) { + , _restartAttempts(std::move(other._restartAttempts)) + , _ownedEntities(std::move(other._ownedEntities)) { other._isolate = nullptr; } @@ -75,12 +71,11 @@ namespace Framework::Scripting { _stateTimestamp = other._stateTimestamp; _loadTimestamp = other._loadTimestamp; _isolate = other._isolate; - _rootEntity = other._rootEntity; _exports = std::move(other._exports); _restartAttempts = std::move(other._restartAttempts); + _ownedEntities = std::move(other._ownedEntities); other._isolate = nullptr; - other._rootEntity = {}; } return *this; } @@ -283,18 +278,6 @@ namespace Framework::Scripting { return it->second.Get(_isolate); } - void Resource::DestroyChildEntities() { - if (!_rootEntity.is_valid()) { - return; - } - - _rootEntity.world().defer_begin(); - _rootEntity.children([&](flecs::entity child) { - child.destruct(); - }); - _rootEntity.world().defer_end(); - } - bool Resource::TransitionTo(ResourceState newState) { if (!IsValidTransition(_state, newState)) { return false; @@ -304,7 +287,7 @@ namespace Framework::Scripting { _stateTimestamp = std::chrono::system_clock::now(); if (newState == ResourceState::Stopped || newState == ResourceState::Error) { - DestroyChildEntities(); + DestroyOwnedEntities(); } if (newState != ResourceState::Error) { @@ -314,6 +297,35 @@ namespace Framework::Scripting { return true; } + void Resource::TrackEntity(uint64_t networkId) { + std::scoped_lock lock(_ownedEntitiesMutex); + _ownedEntities.insert(networkId); + } + + void Resource::UntrackEntity(uint64_t networkId) { + std::scoped_lock lock(_ownedEntitiesMutex); + _ownedEntities.erase(networkId); + } + + void Resource::DestroyOwnedEntities() { + // Swap out first: DestroyEntity re-enters UntrackEntity via the destroy hook. + std::unordered_set owned; + { + std::scoped_lock lock(_ownedEntitiesMutex); + owned.swap(_ownedEntities); + } + + auto *replication = CoreModules::GetReplication(); + if (!replication) { + return; + } + for (uint64_t networkId : owned) { + if (auto *entity = replication->GetEntityByNetworkID(networkId)) { + replication->DestroyEntity(entity); + } + } + } + void Resource::SetError(const std::string &error) { _errorMessage = error; TransitionTo(ResourceState::Error); diff --git a/code/framework/src/scripting/resource/resource.h b/code/framework/src/scripting/resource/resource.h index 6815b9424..9b102740e 100644 --- a/code/framework/src/scripting/resource/resource.h +++ b/code/framework/src/scripting/resource/resource.h @@ -11,14 +11,15 @@ #include "package_manifest.h" #include -#include #include +#include #include #include #include #include #include +#include #include namespace Framework::Scripting { @@ -57,10 +58,6 @@ namespace Framework::Scripting { class Resource; - struct OwnedResource { - Resource *value; - }; - /** * Convert ResourceState to string representation. */ @@ -78,7 +75,7 @@ namespace Framework::Scripting { * Create a resource from a directory path. * @param path Path to the resource directory (containing package.json) */ - explicit Resource(const std::string &path, flecs::world* world); + explicit Resource(const std::string &path); ~Resource(); @@ -206,8 +203,9 @@ namespace Framework::Scripting { v8::Isolate *GetIsolate() const { return _isolate; } void SetIsolate(v8::Isolate *isolate) { _isolate = isolate; } - // Flecs world integration - flecs::entity GetRootEntity() const { return _rootEntity; } + // Replicated entities spawned while this resource was executing; destroyed on stop/error. + void TrackEntity(uint64_t networkId); + void UntrackEntity(uint64_t networkId); // State transitions (called by ResourceManager) friend class ResourceManager; @@ -225,8 +223,7 @@ namespace Framework::Scripting { // Get restart attempt count without locking int GetRestartAttemptCountUnlocked() const; - // Remove all child entities of the flecs root entity - void DestroyChildEntities(); + void DestroyOwnedEntities(); // Path to resource directory std::string _path; @@ -244,15 +241,15 @@ namespace Framework::Scripting { // V8 isolate for this resource (set by manager) v8::Isolate *_isolate = nullptr; - // Flecs root entity - flecs::entity _rootEntity; - // Exports registered by this resource std::map, std::less<>> _exports; mutable std::mutex _exportsMutex; std::vector _restartAttempts; mutable std::mutex _restartAttemptsMutex; + + std::unordered_set _ownedEntities; + mutable std::mutex _ownedEntitiesMutex; }; } // namespace Framework::Scripting diff --git a/code/framework/src/scripting/resource/resource_manager.cpp b/code/framework/src/scripting/resource/resource_manager.cpp index 2a7ec9418..44f2c52b6 100644 --- a/code/framework/src/scripting/resource/resource_manager.cpp +++ b/code/framework/src/scripting/resource/resource_manager.cpp @@ -9,7 +9,6 @@ #include "resource_manager.h" #include "../builtins/events.h" -#include "world/engine.h" #include #include @@ -42,15 +41,12 @@ namespace Framework::Scripting { } } // anonymous namespace - ResourceManager::ResourceManager(Engine *jsEngine, flecs::world *world, const ResourceManagerConfig &config) + ResourceManager::ResourceManager(Engine *jsEngine, const ResourceManagerConfig &config) : _config(config) - , _world(world) , _jsEngine(jsEngine) { if (_jsEngine) { _jsEngine->SetResourceManager(this); } - - _rootEntity = world->entity("Resources"); } ResourceManager::~ResourceManager() { @@ -58,9 +54,6 @@ namespace Framework::Scripting { if (_jsEngine) { _jsEngine->SetResourceManager(nullptr); } - if (_rootEntity.is_valid()) { - _rootEntity.destruct(); - } } const ResourceManagerConfig &ResourceManager::GetConfig() const { @@ -103,7 +96,7 @@ namespace Framework::Scripting { } bool ResourceManager::DiscoverResource(const std::string &path) { - auto resource = std::make_unique(path, _world); + auto resource = std::make_unique(path); if (!resource->IsManifestValid()) { Logging::GetLogger(FRAMEWORK_INNER_SCRIPTING)->warn("Invalid package.json in {}: {}", path, resource->GetErrorMessage()); @@ -120,7 +113,6 @@ namespace Framework::Scripting { return false; } - resource->GetRootEntity().child_of(_rootEntity); _resources[name] = std::move(resource); } @@ -724,6 +716,22 @@ namespace Framework::Scripting { return count; } + void ResourceManager::OnEntityCreated(uint64_t networkId) { + // The stack fallback touches V8; CreateEntity also fires for avatars outside a JS context. + v8::Isolate *isolate = _jsEngine ? _jsEngine->GetIsolate() : nullptr; + Resource *resource = (isolate && isolate->InContext()) ? GetCurrentResourceWithStackFallback(isolate) : GetCurrentResource(); + if (resource) { + resource->TrackEntity(networkId); + } + } + + void ResourceManager::OnEntityDestroyed(uint64_t networkId) { + std::scoped_lock lock(_resourcesMutex); + for (auto &[name, resource] : _resources) { + resource->UntrackEntity(networkId); + } + } + void ResourceManager::HandleResourceRuntimeError(const std::string &resourceName, const std::string &error) { Resource *resource = GetResourceMutable(resourceName); if (!resource) { diff --git a/code/framework/src/scripting/resource/resource_manager.h b/code/framework/src/scripting/resource/resource_manager.h index dd6ed7d72..a9c281063 100644 --- a/code/framework/src/scripting/resource/resource_manager.h +++ b/code/framework/src/scripting/resource/resource_manager.h @@ -73,7 +73,7 @@ namespace Framework::Scripting { */ class ResourceManager final { public: - explicit ResourceManager(Engine *jsEngine, flecs::world *world, const ResourceManagerConfig &config = {}); + explicit ResourceManager(Engine *jsEngine, const ResourceManagerConfig &config = {}); ~ResourceManager(); // Non-copyable @@ -280,6 +280,10 @@ namespace Framework::Scripting { */ Resource *GetCurrentResourceWithStackFallback(v8::Isolate *isolate); + // Wired to ReplicationManager::SetOnEntityCreated/Destroyed. + void OnEntityCreated(uint64_t networkId); + void OnEntityDestroyed(uint64_t networkId); + // Statistics /** @@ -342,9 +346,6 @@ namespace Framework::Scripting { // JS engine (not owned) Engine *_jsEngine = nullptr; - // Flecs world (not owned) - flecs::world *_world = nullptr; - // Resource registry std::map, std::less<>> _resources; mutable std::mutex _resourcesMutex; @@ -375,9 +376,6 @@ namespace Framework::Scripting { // Events instance owned by this manager Events _events; - - // Root entity for all resources - flecs::entity _rootEntity; }; } // namespace Framework::Scripting diff --git a/code/framework/src/world/client.cpp b/code/framework/src/world/client.cpp deleted file mode 100644 index 667bfa5bc..000000000 --- a/code/framework/src/world/client.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#include "client.h" - -#include "game_rpc/set_frame.h" -#include "game_rpc/set_transform.h" - -namespace Framework::World { - WorldError ClientEngine::Init() { - if (Engine::Init(nullptr) != WorldError::WORLD_NONE) { // assigned by OnConnect - return WorldError::WORLD_FLECS_INIT_FAILED; - } - - _queryGetEntityByServerID = _world->query_builder().build(); - - return WorldError::WORLD_NONE; - } - - void ClientEngine::Shutdown() { - Engine::Shutdown(); - } - - void ClientEngine::Update() { - Engine::Update(); - } - - flecs::entity ClientEngine::GetEntityByServerID(flecs::entity_t id) const { - flecs::entity ent = {}; - _queryGetEntityByServerID.each([&ent, id](flecs::entity e, Modules::Base::ServerID& rhs) { - if (id == rhs.id) { - ent = e; - return; - } - }); - return ent; - } - - flecs::entity_t ClientEngine::GetServerID(flecs::entity entity) { - if (!entity.is_alive()) { - return 0; - } - - if(const auto serverID = entity.try_get()) - return serverID->id; - return 0; - } - - flecs::entity ClientEngine::CreateEntity(flecs::entity_t serverID) const { - const auto e = _world->entity(); - - auto &sid = e.ensure(); - sid.id = serverID; - return e; - } - - void ClientEngine::OnConnect(Networking::NetworkPeer *peer, float tickInterval) { - _networkPeer = peer; - - _streamEntities = _world->system("StreamEntities").kind(flecs::PostUpdate).interval(tickInterval).run([this](flecs::iter &it) { - const auto myGUID = _networkPeer->GetPeer()->GetMyGUID(); - - while (it.next()) { - const auto tr = it.field(0); - const auto rs = it.field(1); - - for (auto i : it) { - const auto &es = &rs[i]; - - if (es->GetBaseEvents().updateProc && es->performTickUpdates && Framework::World::Engine::IsEntityOwner(it.entity(i), myGUID.g)) { - es->GetBaseEvents().updateProc(_networkPeer, (MafiaNet::UNASSIGNED_RAKNET_GUID).g, it.entity(i)); - } - } - } - }); - - // Register built-in RPCs - InitRPCs(peer); - } - - void ClientEngine::OnDisconnect() { - if (_streamEntities.is_alive()) { - _streamEntities.destruct(); - } - - _world->defer_begin(); - _allStreamableEntities.each([this](flecs::entity e, Modules::Base::Transform&, Modules::Base::Streamable& str) { - if (_onEntityDestroyCallback) { - if (!_onEntityDestroyCallback(e)) { - return; - } - } - - if (str.modEvents.disconnectProc) { - str.modEvents.disconnectProc(e); - } - - e.destruct(); - }); - _world->defer_end(); - - _networkPeer = nullptr; - } - void ClientEngine::InitRPCs(Networking::NetworkPeer *net) const { - net->RegisterGameRPC([this](MafiaNet::RakNetGUID guid, RPC::SetTransform *msg) { - if (!msg->Valid()) { - return; - } - const auto e = GetEntityByServerID(msg->GetServerID()); - if (!e.is_alive()) { - return; - } - const auto tr = e.try_get_mut(); - *tr = msg->GetTransform(); - e.modified(); - }); - net->RegisterGameRPC([this](MafiaNet::RakNetGUID guid, RPC::SetFrame *msg) { - if (!msg->Valid()) { - return; - } - const auto e = GetEntityByServerID(msg->GetServerID()); - if (!e.is_alive()) { - return; - } - const auto fr = e.try_get_mut(); - *fr = msg->GetFrame(); - e.modified(); - }); - } - - void ClientEngine::UpdateEntityTransform(flecs::entity entity, const Modules::Base::Transform &rhs) { - if (!entity.is_valid() || !entity.is_alive()) { - return; - } - - auto tr = entity.try_get_mut(); - *tr = rhs; - - const auto str = entity.try_get_mut(); - if (str->modEvents.updateTransformProc) { - str->modEvents.updateTransformProc(entity); - } - entity.modified(); - } -} // namespace Framework::World diff --git a/code/framework/src/world/client.h b/code/framework/src/world/client.h deleted file mode 100644 index d1b1e30c1..000000000 --- a/code/framework/src/world/client.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "engine.h" - -#include - -#include - -#define FW_SEND_CLIENT_COMPONENT_GAME_RPC(rpc, ent, ...) \ - do { \ - auto s = rpc {}; \ - s.FromParameters(__VA_ARGS__); \ - s.SetServerID(ent.id()); \ - auto __net = static_cast(Framework::CoreModules::GetNetworkPeer()); \ - if (__net) { \ - __net->SendGameRPC(s); \ - } \ - } while (0) - -#define FW_SEND_CLIENT_COMPONENT_GAME_RPC_TO(rpc, ent, guid, ...) \ - do { \ - auto s = rpc {}; \ - s.FromParameters(__VA_ARGS__); \ - s.SetServerID(ent.id()); \ - auto __net = static_cast(Framework::CoreModules::GetNetworkPeer()); \ - if (__net) { \ - __net->SendGameRPC(s, guid); \ - } \ - } while (0) - -namespace Framework::World { - class ClientEngine final : public Engine { - public: - using OnEntityDestroyCallback = fu2::function; - - protected: - flecs::entity _streamEntities; - flecs::query _queryGetEntityByServerID; - OnEntityDestroyCallback _onEntityDestroyCallback; - - private: - void InitRPCs(Networking::NetworkPeer *peer) const; - - public: - [[nodiscard]] WorldError Init(); - - void Shutdown() override; - - void OnConnect(Networking::NetworkPeer *peer, float tickInterval); - void OnDisconnect(); - - void Update() override; - - flecs::entity CreateEntity(flecs::entity_t serverID) const; - flecs::entity GetEntityByServerID(flecs::entity_t id) const; - static flecs::entity_t GetServerID(flecs::entity entity); - static void UpdateEntityTransform(flecs::entity entity, const Modules::Base::Transform &rhs); - - void SetOnEntityDestroyCallback(OnEntityDestroyCallback cb) { - _onEntityDestroyCallback = cb; - } - }; -} // namespace Framework::World diff --git a/code/framework/src/world/engine.cpp b/code/framework/src/world/engine.cpp deleted file mode 100644 index ded2bd83f..000000000 --- a/code/framework/src/world/engine.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#include "engine.h" - -#include "modules/base.hpp" - -namespace Framework::World { - WorldError Engine::Init(Networking::NetworkPeer *networkPeer) { - _networkPeer = networkPeer; - _world = std::make_unique(); - - // Register a base module - _world->import (); - - _allStreamableEntities = _world->query_builder().build(); - _findAllStreamerEntities = _world->query_builder().build(); - - _initialized = true; - return WorldError::WORLD_NONE; - } - - void Engine::Shutdown() { - Lifecycle::Shutdown(); - } - - void Engine::Update() { - _world->progress(); - } - - bool Engine::IsEntityOwner(flecs::entity e, uint64_t guid) { - const auto es = e.try_get(); - if (!es) { - return false; - } - return (es->owner == guid); - } - - void Engine::WakeEntity(flecs::entity e) { - if (!e.has()) { - return; - } - const auto tr = e.try_get_mut(); - tr->lastGenID--; - const auto es = e.try_get_mut(); - es->updateInterval = es->defaultUpdateInterval; - } - - flecs::entity Engine::GetEntityByGUID(uint64_t guid) const { - flecs::entity ourEntity = {}; - _findAllStreamerEntities.each([&ourEntity, guid](flecs::entity e, Modules::Base::Streamer &s) { - if (ourEntity == flecs::entity::null() && s.guid == guid) { - ourEntity = e; - } - }); - return ourEntity; - } - - flecs::entity Engine::WrapEntity(flecs::entity_t serverID) const { - return flecs::entity(_world->get_world(), serverID); - } - - void Engine::PurgeAllResourceEntities() const { - _world->defer_begin(); - _findAllResourceEntities.each([this](flecs::entity e, Modules::Base::RemovedOnResourceReload &rhs) { - if (e.is_alive()) - e.add(); - }); - _world->defer_end(); - } -} // namespace Framework::World diff --git a/code/framework/src/world/engine.h b/code/framework/src/world/engine.h deleted file mode 100644 index 9c5e5c21c..000000000 --- a/code/framework/src/world/engine.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "errors.h" -#include "modules/base.hpp" - -#include - -#include "networking/network_peer.h" - -#include -#include - -#include "core_modules.h" - -#define FW_SEND_COMPONENT_RPC(rpc, ...) \ - do { \ - auto s = rpc {}; \ - s.FromParameters(__VA_ARGS__); \ - auto __net = Framework::CoreModules::GetNetworkPeer(); \ - if (__net) { \ - __net->SendRPC(s); \ - } \ - } while (0) - -#define FW_SEND_COMPONENT_RPC_TO(rpc, guid, ...) \ - do { \ - auto s = rpc {}; \ - s.FromParameters(__VA_ARGS__); \ - auto __net = Framework::CoreModules::GetNetworkPeer(); \ - if (__net) { \ - __net->SendRPC(s, guid); \ - } \ - } while (0) - -namespace Framework::Scripting { - class ResourceManager; -} - -namespace Framework::World { - class Engine : public Lifecycle { - private: - friend class Framework::Scripting::ResourceManager; - void PurgeAllResourceEntities() const; - - protected: - // NOTE: _world must be declared BEFORE queries so it's destroyed LAST. - // Queries reference the world and must be destroyed before the world. - std::unique_ptr _world; - flecs::query _findAllStreamerEntities; - flecs::query _allStreamableEntities; - flecs::query _findAllResourceEntities; - Networking::NetworkPeer *_networkPeer = nullptr; - - public: - [[nodiscard]] WorldError Init(Networking::NetworkPeer *networkPeer); - - void Shutdown() override; - - void Update() override; - - flecs::entity GetEntityByGUID(uint64_t guid) const; - flecs::entity WrapEntity(flecs::entity_t serverID) const; - static bool IsEntityOwner(flecs::entity e, uint64_t guid); - void WakeEntity(flecs::entity e); - - flecs::world *GetWorld() const { - return _world.get(); - } - }; -} // namespace Framework::World diff --git a/code/framework/src/world/errors.h b/code/framework/src/world/errors.h deleted file mode 100644 index 04a0e20eb..000000000 --- a/code/framework/src/world/errors.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -namespace Framework::World { - enum class WorldError { - WORLD_NONE, - WORLD_FLECS_INIT_FAILED, - WORLD_PEER_NULL - }; -} // namespace Framework::World diff --git a/code/framework/src/world/game_rpc/set_frame.h b/code/framework/src/world/game_rpc/set_frame.h deleted file mode 100644 index 8aab5d2d1..000000000 --- a/code/framework/src/world/game_rpc/set_frame.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "networking/rpc/game_rpc.h" -#include "world/modules/base.hpp" - -#include - -namespace Framework::World::RPC { - class SetFrame final: public Networking::RPC::IGameRPC { - private: - World::Modules::Base::Frame _frame; - - public: - void FromParameters(const World::Modules::Base::Frame &fr) { - _frame = fr; - } - - World::Modules::Base::Frame GetFrame() { - return _frame; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - // Frame holds a std::string (modelName) and so is not trivially-copyable; - // MafiaNet's BitStream refuses to raw-copy such types. Serialize the members - // explicitly and route the string through RakString, matching how the - // networking message classes put strings on the wire. - bs->Serialize(write, _frame.modelHash); - bs->Serialize(write, _frame.scale); - MafiaNet::RakString modelName(_frame.modelName.c_str()); - bs->Serialize(write, modelName); - if (!write) { - _frame.modelName = modelName.C_String(); - } - } - - bool Valid() const override { - return true; - } - }; -} // namespace Framework::World::RPC diff --git a/code/framework/src/world/game_rpc/set_transform.h b/code/framework/src/world/game_rpc/set_transform.h deleted file mode 100644 index 2c589f1cd..000000000 --- a/code/framework/src/world/game_rpc/set_transform.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "networking/rpc/game_rpc.h" -#include "world/modules/base.hpp" - -namespace Framework::World::RPC { - class SetTransform final: public Networking::RPC::IGameRPC { - private: - World::Modules::Base::Transform _transform; - - public: - void FromParameters(const World::Modules::Base::Transform &tr) { - _transform = tr; - } - - World::Modules::Base::Transform GetTransform() const { - return _transform; - } - - void Serialize(MafiaNet::BitStream *bs, bool write) override { - bs->Serialize(write, _transform); - } - - bool Valid() const override { - return true; - } - }; -} // namespace Framework::World::RPC diff --git a/code/framework/src/world/modules/base.hpp b/code/framework/src/world/modules/base.hpp deleted file mode 100644 index e04f3bfe0..000000000 --- a/code/framework/src/world/modules/base.hpp +++ /dev/null @@ -1,197 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace Framework::Networking { - class NetworkPeer; -} // namespace Framework::Networking - -namespace Framework::World { - class Engine; - class ClientEngine; - - namespace Archetypes { - class StreamingFactory; - } -} // namespace Framework::World - -namespace Framework::World::Modules { - struct Base { - struct Transform { - private: - uint16_t genID = 0; - - public: - glm::vec3 pos {}; - glm::vec3 vel {}; - glm::quat rot = glm::identity(); - - uint16_t GetGeneration() const { - return genID; - } - - void IncrementGeneration() { - ++genID; - } - - bool ValidateGeneration(const Transform &tr) const { - return genID == tr.genID; - } - }; - - struct TickRateRegulator: public Transform { - uint16_t lastGenID = 0; - }; - - struct Frame { - uint64_t modelHash {}; - glm::vec3 scale = glm::vec3(1.0f, 1.0f, 1.0f); - std::string modelName; - }; - - struct PendingRemoval { - [[maybe_unused]] uint8_t _unused; - }; - - struct RemovedOnResourceReload { - [[maybe_unused]] uint8_t _unused; - }; - - struct ServerID { - flecs::entity_t id; - }; - - struct Streamable { - using IsVisibleProc = fu2::function; - using AssignOwnerProc = fu2::function; - using OnDisconnectProc = fu2::function; - using OnUpdateTransformProc = fu2::function; - - enum class HeuristicMode { - ADD, - REPLACE, - REPLACE_POSITION - }; - - int virtualWorld = 0; - bool isVisible = true; - bool alwaysVisible = false; - double defaultUpdateInterval = (1000.0 / 60.0); // 16.1667~ ms interval - double updateInterval = defaultUpdateInterval; - uint64_t owner = 0; - - // If set to true, the owner will not be assigned automatically by the framework - bool assignOwnerManually = false; - - // Allows custom owner assignment logic, if method returns true we bypass framework's proximity based owner assignment - AssignOwnerProc assignOwnerProc; - - struct Events { - using Proc = fu2::function; - Proc spawnProc; - Proc despawnProc; - Proc selfUpdateProc; - Proc updateProc; - Proc ownerUpdateProc; - - // Events used locally for special needs - // These are NOT emitted through the network! - OnDisconnectProc disconnectProc; // called when the client disconnects from server - OnUpdateTransformProc updateTransformProc; // called whenever the server enforces a new transform upon the entity - }; - - // Extra set of events so mod can supply custom data. - Events modEvents; - - // Custom visibility proc that either complements the existing heuristic or replaces it - HeuristicMode isVisibleHeuristic = HeuristicMode::ADD; - IsVisibleProc isVisibleProc; - - // Used to specify list of entities this streamable entity relies on. - // If any of these entities are visible and ours is not, we force ours to be visible too. - std::vector dependentEntities; - - // Controls whether this entity gets to be updated continuously or not - // When set to false, we only stream spawn and despawn events, useful for immovable objects - bool performTickUpdates = true; - - // Framework-level events. - friend Base; - - private: - Events events; - - public: - Events& GetBaseEvents() { - return events; - } - - [[maybe_unused]] Events& GetModEvents() { - return modEvents; - } - }; - - struct Streamer { - using CollectRangeExemptEntities = fu2::function; - struct StreamData { - double lastUpdate = 0.0; - }; - float range = 100.0f; - uint64_t guid = 0xFFFFFFFFFFFFFFFF; - uint16_t playerIndex = 0xFFFF; - std::string nickname; - std::string hardwareId; - std::unordered_map entities; - std::unordered_set rangeExemptEntities; - CollectRangeExemptEntities collectRangeExemptEntitiesProc; - }; - - explicit Base(flecs::world &world) { - world.module(); - - // TODO expose STL types once https://github.com/SanderMertens/flecs/issues/712 is resolved. - - auto _transform = world.component(); - auto _frame = world.component(); - auto _streamable = world.component(); - auto _streamer = world.component(); - - world.component(); - world.component(); - world.component(); - world.component(); - -// Windows bind metadata -#ifdef _WIN32 - { - auto _vec3 = world.component(); - auto _quat = world.component(); - _vec3.member("x").member("y").member("z"); - _quat.member("w").member("x").member("y").member("z"); - _transform.member("generation").member("pos").member("vel").member("rot"); - _frame.member("modelHash").member("scale"); - _streamable.member("virtualWorld").member("isVisible").member("alwaysVisible").member("updateInterval").member("owner"); - _streamer.member("range").member("guid"); - } -#endif - } - - static void SetupServerEmitters(Streamable& streamable); - static void SetupClientEmitters(Streamable& streamable); - static void SetupServerReceivers(Framework::Networking::NetworkPeer *net, Framework::World::Engine *worldEngine); - static void SetupClientReceivers(Framework::Networking::NetworkPeer *net, Framework::World::ClientEngine *worldEngine, Framework::World::Archetypes::StreamingFactory *streamingFactory); - }; -} // namespace Framework::World::Modules diff --git a/code/framework/src/world/modules/modules_impl.cpp b/code/framework/src/world/modules/modules_impl.cpp deleted file mode 100644 index 6532b93ef..000000000 --- a/code/framework/src/world/modules/modules_impl.cpp +++ /dev/null @@ -1,198 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "base.hpp" -#include "networking/messages/game_sync/entity_messages.h" -#include "networking/network_peer.h" -#include "world/client.h" -#include "world/engine.h" - -#include "world/types/streaming.hpp" - -#define CALL_CUSTOM_PROC(kind) \ - const auto streamable = e.try_get(); \ - if (streamable != nullptr) { \ - if (streamable->modEvents.kind != nullptr) { \ - streamable->modEvents.kind(peer, guid, e); \ - } \ - } - -namespace Framework::World::Modules { - void Base::SetupServerEmitters(Streamable& streamable) { - streamable.events.spawnProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - Framework::Networking::Messages::GameSyncEntitySpawn entitySpawn; - const auto tr = e.try_get(); - if (tr) - entitySpawn.FromParameters(*tr); - entitySpawn.SetServerID(e.id()); - peer->Send(entitySpawn, guid); - CALL_CUSTOM_PROC(spawnProc); - return true; - }; - - streamable.events.despawnProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - CALL_CUSTOM_PROC(despawnProc); - Framework::Networking::Messages::GameSyncEntityDespawn entityDespawn; - entityDespawn.SetServerID(e.id()); - peer->Send(entityDespawn, guid); - return true; - }; - - streamable.events.selfUpdateProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - Framework::Networking::Messages::GameSyncEntitySelfUpdate entitySelfUpdate; - entitySelfUpdate.SetServerID(e.id()); - peer->Send(entitySelfUpdate, guid); - CALL_CUSTOM_PROC(selfUpdateProc); - return true; - }; - - streamable.events.updateProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - const auto tr = e.try_get(); - const auto es = e.try_get(); - // Only send framework update if entity has a valid owner - if (tr && es && es->owner != MafiaNet::UNASSIGNED_RAKNET_GUID.g) { - Framework::Networking::Messages::GameSyncEntityUpdate entityUpdate; - entityUpdate.FromParameters(*tr, es->owner); - entityUpdate.SetServerID(e.id()); - peer->Send(entityUpdate, guid); - } - CALL_CUSTOM_PROC(updateProc); - return true; - }; - - streamable.events.ownerUpdateProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - const auto tr = e.try_get(); - const auto es = e.try_get(); - // Only send framework owner update if entity has a valid owner - if (tr && es && es->owner != MafiaNet::UNASSIGNED_RAKNET_GUID.g) { - Framework::Networking::Messages::GameSyncEntityOwnerUpdate entityUpdate; - entityUpdate.FromParameters(es->owner); - entityUpdate.SetServerID(e.id()); - peer->Send(entityUpdate, guid); - } - CALL_CUSTOM_PROC(ownerUpdateProc); - return true; - }; - } - void Base::SetupClientEmitters(Streamable& streamable) { - streamable.events.updateProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - Framework::Networking::Messages::GameSyncEntityUpdate entityUpdate; - const auto tr = e.try_get(); - const auto sid = e.try_get(); - if (tr && sid) { - entityUpdate.FromParameters(*tr, 0); - entityUpdate.SetServerID(sid->id); - } - peer->Send(entityUpdate, guid); - CALL_CUSTOM_PROC(updateProc); - return true; - }; - } - - void Base::SetupServerReceivers(Framework::Networking::NetworkPeer *net, Framework::World::Engine *worldEngine) { - using namespace Framework::Networking::Messages; - net->RegisterMessage(GameMessages::GAME_SYNC_ENTITY_UPDATE, [worldEngine](MafiaNet::RakNetGUID guid, GameSyncEntityUpdate *msg) { - if (!msg->Valid()) { - return; - } - - const auto e = worldEngine->WrapEntity(msg->GetServerID()); - - if (!e.is_alive()) { - return; - } - - if (!worldEngine->IsEntityOwner(e, guid.g)) { - return; - } - - const auto tr = e.try_get_mut(); - const auto incomingTr = msg->GetTransform(); - - if (tr->ValidateGeneration(incomingTr)) { - *tr = incomingTr; - } - }); - } - - void Base::SetupClientReceivers(Framework::Networking::NetworkPeer *net, Framework::World::ClientEngine *worldEngine, Framework::World::Archetypes::StreamingFactory *streamingFactory) { - using namespace Framework::Networking::Messages; - net->RegisterMessage(GameMessages::GAME_SYNC_ENTITY_SPAWN, [worldEngine, streamingFactory](MafiaNet::RakNetGUID guid, GameSyncEntitySpawn *msg) { - if (!msg->Valid()) { - return; - } - if (worldEngine->GetEntityByServerID(msg->GetServerID()).is_alive()) { - return; - } - const auto e = worldEngine->CreateEntity(msg->GetServerID()); - streamingFactory->SetupClient(e, MafiaNet::UNASSIGNED_RAKNET_GUID.g); - - e.add(); - const auto tr = e.try_get_mut(); - *tr = msg->GetTransform(); - }); - net->RegisterMessage(GameMessages::GAME_SYNC_ENTITY_DESPAWN, [worldEngine](MafiaNet::RakNetGUID guid, GameSyncEntityDespawn *msg) { - if (!msg->Valid()) { - return; - } - - const auto e = worldEngine->GetEntityByServerID(msg->GetServerID()); - - if (!e.is_alive()) { - return; - } - - e.destruct(); - }); - net->RegisterMessage(GameMessages::GAME_SYNC_ENTITY_UPDATE, [worldEngine](MafiaNet::RakNetGUID guid, GameSyncEntityUpdate *msg) { - if (!msg->Valid()) { - return; - } - - const auto e = worldEngine->GetEntityByServerID(msg->GetServerID()); - - if (!e.is_alive()) { - return; - } - - const auto tr = e.try_get_mut(); - *tr = msg->GetTransform(); - - const auto es = e.try_get_mut(); - es->owner = msg->GetOwner(); - }); - net->RegisterMessage(GameMessages::GAME_SYNC_ENTITY_OWNER_UPDATE, [worldEngine](MafiaNet::RakNetGUID guid, GameSyncEntityUpdate *msg) { - if (!msg->Valid()) { - return; - } - - const auto e = worldEngine->GetEntityByServerID(msg->GetServerID()); - - if (!e.is_alive()) { - return; - } - const auto es = e.try_get_mut(); - es->owner = msg->GetOwner(); - }); - net->RegisterMessage(GameMessages::GAME_SYNC_ENTITY_SELF_UPDATE, [worldEngine](MafiaNet::RakNetGUID guid, GameSyncEntitySelfUpdate *msg) { - if (!msg->Valid()) { - return; - } - - const auto e = worldEngine->GetEntityByServerID(msg->GetServerID()); - - if (!e.is_alive()) { - return; - } - - // Nothing to do for now. - }); - } -} // namespace Framework::World::Modules diff --git a/code/framework/src/world/server.cpp b/code/framework/src/world/server.cpp deleted file mode 100644 index 3a52a4494..000000000 --- a/code/framework/src/world/server.cpp +++ /dev/null @@ -1,341 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#include "server.h" - -#include "utils/time.h" - -namespace Framework::World { - WorldError ServerEngine::Init(Framework::Networking::NetworkPeer *networkPeer, ServerConfig cfg) { - if (Engine::Init(networkPeer) != WorldError::WORLD_NONE) { - return WorldError::WORLD_FLECS_INIT_FAILED; - } - - _findAllResourceEntities = _world->query_builder().build(); - - // Set up a system to remove entities we no longer need. - _world->system("RemoveEntities").kind(flecs::PostUpdate).interval(cfg.removeEntitiesTickInterval).each([this](flecs::entity e, Modules::Base::PendingRemoval &pd, Modules::Base::Streamable &streamable) { - // Remove the entity from all streamers. - _findAllStreamerEntities.each([this, &e, &streamable](flecs::entity rhsE, Modules::Base::Streamer &rhsS) { - if (rhsS.entities.contains(e)) { - rhsS.entities.erase(e); - - // Ensure we despawn the entity from the client. - if (streamable.GetBaseEvents().despawnProc) - streamable.GetBaseEvents().despawnProc(_networkPeer, rhsS.guid, e); - } - }); - - e.destruct(); - }); - - // Set up a system to assign entity owners. - _world->system("AssignEntityOwnership").kind(flecs::PostUpdate).interval(cfg.assignOwnershipTickInterval).each([this](flecs::entity e, Modules::Base::Transform &tr, Modules::Base::Streamable &streamable) { - // Let user provide custom ownership assignment. - if (streamable.assignOwnerManually || (streamable.assignOwnerProc && streamable.assignOwnerProc(e, streamable))) { - /* no op */ - } - else { - // Assign the entity to the closest streamer. - uint64_t closestOwnerGUID = MafiaNet::UNASSIGNED_RAKNET_GUID.g; - float closestDist = std::numeric_limits::max(); - _findAllStreamerEntities.each([this, &e, &tr, &closestDist, &closestOwnerGUID, &streamable](flecs::entity rhsE, Modules::Base::Streamer &rhsS) { - const auto rhsTr = rhsE.try_get(); - const auto rhsRs = rhsE.try_get(); - const auto canBeOwner = this->IsEntityVisibleToStreamer(rhsE, e, *rhsTr, rhsS, *rhsRs, tr, streamable); - if (canBeOwner) { - const auto dist = glm::distance(tr.pos, rhsTr->pos); - if (dist < closestDist) { - closestDist = dist; - closestOwnerGUID = rhsS.guid; - } - } - }); - - streamable.owner = closestOwnerGUID; - } - }); - - // Set up a system to collect stream range exempt entities. - _world->system("CollectRangeExemptEntities").kind(flecs::PostUpdate).interval(cfg.collectRangeExemptEntitiesTickInterval).each([this](flecs::entity e, Modules::Base::Streamer &streamer) { - streamer.rangeExemptEntities.clear(); - if (streamer.collectRangeExemptEntitiesProc) - streamer.collectRangeExemptEntitiesProc(e, streamer); - }); - - _world->system("TickRateRegulator").interval(cfg.tickRegulatorInterval).run([](flecs::iter &it) { - while (it.next()) { - const auto tr = it.field(0); - const auto t = it.field(1); - const auto s = it.field(2); - - for (auto i : it) { - bool decreaseRate = true; - constexpr float EPSILON = 0.01f; - - // Check if position has changed - if (glm::abs(t[i].pos.x - tr[i].pos.x) > EPSILON || glm::abs(t[i].pos.y - tr[i].pos.y) > EPSILON || glm::abs(t[i].pos.z - tr[i].pos.z) > EPSILON) { - decreaseRate = false; - } - - // Check if rotation quaternion has changed - if (glm::abs(t[i].rot.x - tr[i].rot.x) > EPSILON || glm::abs(t[i].rot.y - tr[i].rot.y) > EPSILON || glm::abs(t[i].rot.z - tr[i].rot.z) > EPSILON || glm::abs(t[i].rot.w - tr[i].rot.w) > EPSILON) { - decreaseRate = false; - } - - // Check if velocity has changed - if (glm::abs(t[i].vel.x - tr[i].vel.x) > EPSILON || glm::abs(t[i].vel.y - tr[i].vel.y) > EPSILON || glm::abs(t[i].vel.z - tr[i].vel.z) > EPSILON) { - decreaseRate = false; - } - - // Check if generation ID has changed - if (t[i].GetGeneration() != tr[i].lastGenID) { - decreaseRate = true; - } - - // Update all values - tr[i].lastGenID = t[i].GetGeneration(); - tr[i].pos = t[i].pos; - tr[i].rot = t[i].rot; - tr[i].vel = t[i].vel; - - // Decrease tick rate if needed - if (decreaseRate) { - s[i].updateInterval += 5.0f; - } - else { - s[i].updateInterval = s[i].defaultUpdateInterval; - } - } - } - }); - - // Set up a system to stream entities to clients. - _world->system("StreamEntities") - .kind(flecs::PostUpdate) - .interval(cfg.tickInterval) - .run([this](flecs::iter &it) { - while (it.next()) { - const auto tr = it.field(0); - const auto s = it.field(1); - const auto rs = it.field(2); - - for (auto i : it) { - // Skip streamer entities we plan to remove. - if (it.entity(i).has()) - continue; - - // Grab all streamable entities. - _allStreamableEntities.each([&](flecs::entity e, Modules::Base::Transform &otherTr, Modules::Base::Streamable &otherS) { - // Skip dead entities. - if (!e.is_alive()) - return; - - // Let streamer send an update to self if an event is assigned. - if (e == it.entity(i) && rs[i].GetBaseEvents().selfUpdateProc && rs[i].performTickUpdates) { - rs[i].GetBaseEvents().selfUpdateProc(_networkPeer, s[i].guid, e); - return; - } - - // Figure out entity visibility. - const auto id = e.id(); - const auto canSend = this->IsEntityVisibleToStreamer(it.entity(i), e, tr[i], s[i], rs[i], otherTr, otherS); - const auto map_it = s[i].entities.find(id); - - // Entity is already known to this streamer. - if (map_it != s[i].entities.end()) { - // If we can't stream an entity anymore, despawn it - if (!canSend) { - s[i].entities.erase(map_it); - if (otherS.GetBaseEvents().despawnProc) - otherS.GetBaseEvents().despawnProc(_networkPeer, s[i].guid, e); - } - - // otherwise we do regular updates - else if (rs[i].owner != otherS.owner) { - auto &data = map_it->second; - if (static_cast(Utils::Time::GetTime()) - data.lastUpdate > otherS.updateInterval) { - if (otherS.GetBaseEvents().updateProc && rs[i].performTickUpdates) - otherS.GetBaseEvents().updateProc(_networkPeer, s[i].guid, e); - data.lastUpdate = static_cast(Utils::Time::GetTime()); - } - } - else { - auto &data = map_it->second; - - // If the entity is owned by this streamer, we send a full update. - if (static_cast(Utils::Time::GetTime()) - data.lastUpdate > otherS.updateInterval) { - if (otherS.GetBaseEvents().ownerUpdateProc) - otherS.GetBaseEvents().ownerUpdateProc(_networkPeer, s[i].guid, e); - data.lastUpdate = static_cast(Utils::Time::GetTime()); - } - } - } - - // this is a new entity, spawn it unless user says otherwise - else if (canSend && otherS.GetBaseEvents().spawnProc) { - if (otherS.GetBaseEvents().spawnProc(_networkPeer, s[i].guid, e)) { - Modules::Base::Streamer::StreamData data; - data.lastUpdate = static_cast(Utils::Time::GetTime()); - s[i].entities[id] = data; - } - } - }); - } - } - }); - - return WorldError::WORLD_NONE; - } - - void ServerEngine::Shutdown() { - Engine::Shutdown(); - } - - void ServerEngine::Update() { - Engine::Update(); - } - - flecs::entity ServerEngine::CreateEntity(const std::string &name) const { - if (name.empty()) { - return _world->entity(); - } - else { - return _world->entity(name.c_str()); - } - } - - void ServerEngine::SetOwner(flecs::entity e, uint64_t guid) { - const auto es = e.try_get_mut(); - if (!es) { - return; - } - es->owner = guid; - } - - flecs::entity ServerEngine::GetOwner(flecs::entity e) const { - const auto es = e.try_get(); - if (!es) { - return flecs::entity::null(); - } - return GetEntityByGUID(es->owner); - } - - std::vector ServerEngine::FindVisibleStreamers(flecs::entity e) const { - std::vector streamers; - const auto es = e.try_get(); - if (!es) { - return {}; - } - _findAllStreamerEntities.each([this, e, &streamers, es](flecs::entity rhsE, Modules::Base::Streamer &rhsS) { - const auto rhsTr = rhsE.try_get(); - const auto rhsST = rhsE.try_get(); - const auto lhsTr = e.try_get(); - if (!rhsTr || !rhsST || !lhsTr) { - return; - } - - if (this->IsEntityVisibleToStreamer(rhsE, e, *rhsTr, rhsS, *rhsST, *lhsTr, *es)) { - streamers.push_back(rhsE); - } - }); - return streamers; - } - - bool ServerEngine::RemoveEntity(flecs::entity e) { - if (e.is_alive() && !e.has()) { - e.add(); - return true; - } - return false; - } - - bool ServerEngine::IsEntityVisibleToStreamer(const flecs::entity streamerEntity, const flecs::entity e, const Modules::Base::Transform &lhsTr, const Modules::Base::Streamer &streamer, const Modules::Base::Streamable &lhsS, const Modules::Base::Transform &rhsTr, - const Modules::Base::Streamable& rhsS) const - { - std::unordered_set visited; - return IsEntityVisibleToStreamerInternal(streamerEntity, e, lhsTr, streamer, lhsS, rhsTr, rhsS, visited); - } - - bool ServerEngine::IsEntityVisibleToStreamerInternal(const flecs::entity streamerEntity, const flecs::entity e, const Modules::Base::Transform &lhsTr, const Modules::Base::Streamer &streamer, const Modules::Base::Streamable &lhsS, const Modules::Base::Transform &rhsTr, - const Modules::Base::Streamable& rhsS, std::unordered_set &visited) const - { - if (!e.is_valid()) - return false; - if (!e.is_alive()) - return false; - - // Discard entities that we plan to remove. - if (e.has()) - return false; - - // Allow user to override visibility rules completely. - if (rhsS.isVisibleProc && rhsS.isVisibleHeuristic == Modules::Base::Streamable::HeuristicMode::REPLACE) { - return rhsS.isVisibleProc(streamerEntity, e); - } - - // Mark this entity as visited to prevent infinite recursion in cyclic dependencies. - if (!visited.insert(e.id()).second) { - // Already visited - we're in a cycle, skip dependent check for this entity. - // Continue with the remaining visibility checks. - } - else { - // Check our dependents, if any of them are visible, we are visible as well. - for (const auto &dependentEntity : rhsS.dependentEntities) { - if (!dependentEntity.is_valid() || !dependentEntity.is_alive()) - continue; - if (e == dependentEntity) - continue; - // Skip if already visited (part of a cycle) - if (visited.contains(dependentEntity.id())) - continue; - const auto &dependentS = dependentEntity.try_get(); - const auto &dependentTr = dependentEntity.try_get(); - if (!dependentS || !dependentTr) - continue; - if (IsEntityVisibleToStreamerInternal(streamerEntity, dependentEntity, lhsTr, streamer, lhsS, *dependentTr, *dependentS, visited)) { - return true; - } - } - } - - // Entity is always visible to clients. - if (rhsS.alwaysVisible) - return true; - - // Entity can be hidden from clients. - if (!rhsS.isVisible) - return false; - - // Validate if the entity resides in the same virtual world client does. - if (lhsS.virtualWorld != rhsS.virtualWorld) - return false; - - // Let user replace the distance check. - if (rhsS.isVisibleProc && rhsS.isVisibleHeuristic == Modules::Base::Streamable::HeuristicMode::REPLACE_POSITION) { - return rhsS.isVisibleProc(streamerEntity, e); - } - - // Perform distance check. - const auto dist = glm::distance(lhsTr.pos, rhsTr.pos); - auto isVisible = dist < streamer.range; - - // If we made it this far and the entity is streaming range check exempt - // we override isVisible state to True. - if (streamer.rangeExemptEntities.contains(e.id())) { - isVisible = true; - } - - // Allow user to provide additional rules for visibility. - if (rhsS.isVisibleProc && rhsS.isVisibleHeuristic == Modules::Base::Streamable::HeuristicMode::ADD) { - isVisible = isVisible && rhsS.isVisibleProc(streamerEntity, e); - } - - return isVisible; - } -} // namespace Framework::World diff --git a/code/framework/src/world/server.h b/code/framework/src/world/server.h deleted file mode 100644 index e4e1ba4d3..000000000 --- a/code/framework/src/world/server.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - * MafiaHub OSS license - * Copyright (c) 2021-2023, MafiaHub. All rights reserved. - * - * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. - * See LICENSE file in the source repository for information regarding licensing. - */ - -#pragma once - -#include "engine.h" - -#include - -#include -#include -#include -#include -#include - -#define FW_SEND_SERVER_COMPONENT_GAME_RPC(rpc, ent, ...) \ - do { \ - auto s = rpc {}; \ - s.FromParameters(__VA_ARGS__); \ - s.SetServerID(ent.id()); \ - auto __net = static_cast(Framework::CoreModules::GetNetworkPeer()); \ - if (__net) { \ - __net->SendGameRPC(static_cast(Framework::CoreModules::GetWorldEngine()), s); \ - } \ - } while (0) - -#define FW_SEND_SERVER_COMPONENT_GAME_RPC_EXCEPT(rpc, ent, guid, ...) \ - do { \ - auto s = rpc {}; \ - s.FromParameters(__VA_ARGS__); \ - s.SetServerID(ent.id()); \ - auto __net = static_cast(Framework::CoreModules::GetNetworkPeer()); \ - if (__net) { \ - __net->SendGameRPC(static_cast(Framework::CoreModules::GetWorldEngine()), s, MafiaNet::UNASSIGNED_RAKNET_GUID, guid); \ - } \ - } while (0) - -#define FW_SEND_SERVER_COMPONENT_GAME_RPC_TO(rpc, ent, guid, ...) \ - do { \ - auto s = rpc {}; \ - s.FromParameters(__VA_ARGS__); \ - s.SetServerID(ent.id()); \ - auto __net = static_cast(Framework::CoreModules::GetNetworkPeer()); \ - if (__net) { \ - __net->SendGameRPC(static_cast(Framework::CoreModules::GetWorldEngine()), s, guid); \ - } \ - } while (0) - -namespace Framework::World { - class ServerEngine final : public Engine { - protected: - using IsVisibleProc = fu2::function; - - private: - bool IsEntityVisibleToStreamerInternal(const flecs::entity streamerEntity, const flecs::entity e, const Modules::Base::Transform &lhsTr, const Modules::Base::Streamer &streamer, const Modules::Base::Streamable &lhsS, const Modules::Base::Transform &rhsTr, - const Modules::Base::Streamable &rhsS, std::unordered_set &visited) const; - - public: - struct ServerConfig { - float tickInterval = 0.016667f; - float streamerTickInterval = 0.033334f; - float assignOwnershipTickInterval = 3.0f; - float collectRangeExemptEntitiesTickInterval = 0.066668f; - float removeEntitiesTickInterval = 0.066668f; - float tickRegulatorInterval = 3.0f; - }; - - [[nodiscard]] WorldError Init(Framework::Networking::NetworkPeer *networkPeer, ServerConfig cfg); - - void Shutdown() override; - - void Update() override; - - flecs::entity CreateEntity(const std::string &name = "") const; - static bool RemoveEntity(flecs::entity e); - - static void SetOwner(flecs::entity e, uint64_t guid); - flecs::entity GetOwner(flecs::entity e) const; - [[maybe_unused]] std::vector FindVisibleStreamers(flecs::entity e) const; - bool IsEntityVisibleToStreamer(const flecs::entity streamerEntity, const flecs::entity e, const Modules::Base::Transform &lhsTr, const Modules::Base::Streamer &streamer, const Modules::Base::Streamable &lhsS, const Modules::Base::Transform &rhsTr, - const Modules::Base::Streamable &rhsS) const; - }; -} // namespace Framework::World diff --git a/code/framework/src/world/types/player.hpp b/code/framework/src/world/types/player.hpp index b428d6b66..5563dd0ad 100644 --- a/code/framework/src/world/types/player.hpp +++ b/code/framework/src/world/types/player.hpp @@ -8,38 +8,30 @@ #pragma once -#include +#include "networking/replication/network_entity.h" -#include "world/modules/base.hpp" - -#include +#include namespace Framework::World::Archetypes { - class PlayerFactory { - private: - inline void SetupDefaults(flecs::entity e, uint64_t guid) { - auto &streamer = e.ensure(); - streamer.guid = guid; - } + namespace Replication = Framework::Networking::Replication; + // Configures an entity as a player's avatar: it owns itself and (on the server) acts as the + // connection's viewer — its position/streamRange drive that client's interest set. The caller + // registers it as the viewer for the player's GUID via ReplicationManager::SetViewer. Player + // metadata (nickname, hardware id) belongs on the game's player NetworkEntity subclass. + class PlayerFactory { public: - inline void SetupClient(flecs::entity e, uint64_t guid) { - SetupDefaults(e, guid); + void SetupClient(Replication::NetworkEntity *entity, uint64_t guid) { + if (entity) { + entity->ownerGUID = guid; + } } - inline void SetupServer(flecs::entity e, uint64_t guid, uint16_t playerIndex, const std::string &nickname, const std::string &hardwareId = "") { - SetupDefaults(e, guid); - - auto &streamable = e.ensure(); - streamable.assignOwnerProc = [](flecs::entity, World::Modules::Base::Streamable &) { - return true; /* always keep current owner */ - }; - - auto &streamer = e.ensure(); - streamer.nickname = nickname; - streamer.playerIndex = playerIndex; - streamer.guid = guid; - streamer.hardwareId = hardwareId; + void SetupServer(Replication::NetworkEntity *entity, uint64_t guid) { + if (entity) { + entity->ownerGUID = guid; + entity->streaming.isViewer = true; + } } }; } // namespace Framework::World::Archetypes diff --git a/code/framework/src/world/types/streaming.hpp b/code/framework/src/world/types/streaming.hpp index f87366cc5..35aff9661 100644 --- a/code/framework/src/world/types/streaming.hpp +++ b/code/framework/src/world/types/streaming.hpp @@ -8,39 +8,25 @@ #pragma once -#include +#include "networking/replication/network_entity.h" -#include "world/modules/base.hpp" +#include namespace Framework::World::Archetypes { - class StreamingFactory { - private: - inline void SetupDefaults(flecs::entity e, uint64_t guid) { - e.add(); - - auto &streamable = e.ensure(); - streamable.owner = guid; - streamable.defaultUpdateInterval = CoreModules::GetTickRate() * 1000.0f; // we need ms here - - e.add(); - } + namespace Replication = Framework::Networking::Replication; + // Stamps ownership on a streamed entity. + class StreamingFactory { public: - inline void SetupClient(flecs::entity e, uint64_t guid) { - SetupDefaults(e, guid); - - auto& streamable = e.ensure(); - Framework::World::Modules::Base::SetupClientEmitters(streamable); - - auto ass = e.get_mut(); - (void)ass; + void SetupServer(Replication::NetworkEntity *entity, uint64_t guid) { + if (entity) { + entity->ownerGUID = guid; + } } - - inline void SetupServer(flecs::entity e, uint64_t guid) { - SetupDefaults(e, guid); - - auto& streamable = e.ensure(); - Framework::World::Modules::Base::SetupServerEmitters(streamable); + void SetupClient(Replication::NetworkEntity *entity, uint64_t guid) { + if (entity) { + entity->ownerGUID = guid; + } } }; } // namespace Framework::World::Archetypes diff --git a/code/tests/modules/js_features_ut.h b/code/tests/modules/js_features_ut.h index 58ef70ef1..f755384f0 100644 --- a/code/tests/modules/js_features_ut.h +++ b/code/tests/modules/js_features_ut.h @@ -228,10 +228,9 @@ MODULE(js_features, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = EventsTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); { v8::Isolate *isolate = engine.GetIsolate(); @@ -275,10 +274,9 @@ MODULE(js_features, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = EventsTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); { v8::Isolate *isolate = engine.GetIsolate(); @@ -318,10 +316,9 @@ MODULE(js_features, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = EventsTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); { v8::Isolate *isolate = engine.GetIsolate(); @@ -362,10 +359,9 @@ MODULE(js_features, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = EventsTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); { v8::Isolate *isolate = engine.GetIsolate(); @@ -406,10 +402,9 @@ MODULE(js_features, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = EventsTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); { v8::Isolate *isolate = engine.GetIsolate(); @@ -439,10 +434,9 @@ MODULE(js_features, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = EventsTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); { v8::Isolate *isolate = engine.GetIsolate(); @@ -486,10 +480,9 @@ MODULE(js_features, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = EventsTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); { v8::Isolate *isolate = engine.GetIsolate(); diff --git a/code/tests/modules/resource_manager_ut.h b/code/tests/modules/resource_manager_ut.h index 5d3b1db04..bd424f0fb 100644 --- a/code/tests/modules/resource_manager_ut.h +++ b/code/tests/modules/resource_manager_ut.h @@ -83,14 +83,13 @@ MODULE(resource_manager, { IT("can create and destroy resource manager", { NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager *manager = new ResourceManager(&engine, &world, config); + ResourceManager *manager = new ResourceManager(&engine, config); NEQUALS(manager, nullptr); delete manager; @@ -99,7 +98,6 @@ MODULE(resource_manager, { IT("GetConfig returns configuration", { NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); @@ -108,7 +106,7 @@ MODULE(resource_manager, { config.isClient = true; config.cascadeStopDependents = false; - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); const auto &retrievedConfig = manager.GetConfig(); STREQUALS(retrievedConfig.resourcesPath.c_str(), config.resourcesPath.c_str()); @@ -120,14 +118,13 @@ MODULE(resource_manager, { IT("SetConfig updates configuration", { NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = "/old/path"; - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); ResourceManagerConfig newConfig; newConfig.resourcesPath = "/new/path"; @@ -151,14 +148,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); size_t discovered = manager.DiscoverResources(); EQUALS(discovered, 2u); @@ -177,14 +173,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); bool result = manager.DiscoverResource(TestManagerHelper::GetTestResourcePath() + "/single-disc"); EQUALS(result, true); @@ -203,14 +198,13 @@ MODULE(resource_manager, { } NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); size_t discovered = manager.DiscoverResources(); EQUALS(discovered, 0u); @@ -233,14 +227,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); auto names = manager.GetAllResourceNames(); @@ -257,14 +250,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); const Resource *resource = manager.GetResource("get-test"); @@ -278,14 +270,13 @@ MODULE(resource_manager, { IT("GetResource returns nullptr for unknown resource", { NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); const Resource *resource = manager.GetResource("nonexistent"); EQUALS(resource, nullptr); @@ -300,14 +291,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); EQUALS(manager.HasResource("has-test"), true); @@ -326,14 +316,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); ResourceState state = manager.GetResourceState("state-test"); @@ -350,14 +339,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); EQUALS(manager.IsResourceRunning("running-test"), false); @@ -382,14 +370,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); auto deps = manager.GetDependencies("dep-child"); @@ -413,14 +400,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); auto dependents = manager.GetDependents("parent-res"); @@ -439,14 +425,13 @@ MODULE(resource_manager, { })"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); EQUALS(manager.GetRunningResourceCount(), 0u); @@ -470,14 +455,13 @@ MODULE(resource_manager, { outsideFile.close(); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); auto startResult = manager.StartResource("escape-test"); @@ -502,14 +486,13 @@ MODULE(resource_manager, { TestManagerHelper::CreateTestScript("callback-start", "main.js", "// empty script"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); std::string startedResource; manager.SetOnResourceStarted([&startedResource](const std::string &name) { @@ -537,14 +520,13 @@ MODULE(resource_manager, { TestManagerHelper::CreateTestScript("callback-stop", "main.js", "// empty script"); NodeEngine engine; - flecs::world world; EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); std::string stoppedResource; manager.SetOnResourceStopped([&stoppedResource](const std::string &name) { @@ -568,11 +550,10 @@ MODULE(resource_manager, { EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); ResourceManagerConfig config; - flecs::world world; config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); STREQUALS(manager.GetCurrentResourceContext().c_str(), ""); @@ -585,61 +566,6 @@ MODULE(resource_manager, { engine.Shutdown(); }); - // ==================== Root entity ==================== - - IT("child entities should be removed", { - TestManagerHelper::CreateTestResource("child-entities", R"({ - "name": "child-entities", - "version": "1.0.0", - "mafiahub": { - "server": "main.js" - } - })"); - TestManagerHelper::CreateTestScript("child-entities", "main.js", "// empty script"); - - NodeEngine engine; - flecs::world world; - - EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - - ResourceManagerConfig config; - config.resourcesPath = TestManagerHelper::GetTestResourcePath(); - - flecs::entity root; - { - ResourceManager manager(&engine, &world, config); - - manager.DiscoverResources(); - manager.StartResource("child-entities"); - - const Resource *resource = manager.GetResource("child-entities"); - flecs::entity childEntity = world.entity("child"); - root = resource->GetRootEntity(); - childEntity.child_of(root); - - int beforeChildCount = 0; - root.children([&](flecs::entity) { - beforeChildCount++; - }); - - manager.StopResource("child-entities"); - - int childCount = 0; - root.children([&](flecs::entity) { - childCount++; - }); - - EQUALS(beforeChildCount, 1); - EQUALS(childCount, 0); - EQUALS(root.is_alive(), true); // Root entity should be alive even if resource is stopped - } - - EQUALS(root.is_alive(), false); - - engine.Shutdown(); - TestManagerHelper::Cleanup(); - }); - // ==================== Cleanup ==================== IT("final cleanup", { diff --git a/code/tests/modules/resource_ut.h b/code/tests/modules/resource_ut.h index 4904b57e8..510a9ddf9 100644 --- a/code/tests/modules/resource_ut.h +++ b/code/tests/modules/resource_ut.h @@ -102,8 +102,7 @@ MODULE(resource, { "description": "A test resource" })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/pkg-test-1", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/pkg-test-1"); EQUALS(resource.IsManifestValid(), true); STREQUALS(resource.GetName().c_str(), "pkg-test-1"); @@ -130,8 +129,7 @@ MODULE(resource, { resourceDir.createDirectory(); } - flecs::world world; - Resource resource(resourcePath, &world); + Resource resource(resourcePath); EQUALS(resource.IsManifestValid(), false); EQUALS(resource.GetState(), ResourceState::Error); @@ -145,8 +143,7 @@ MODULE(resource, { "version": "1.0.0" })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/pkg-test-2", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/pkg-test-2"); EQUALS(resource.IsManifestValid(), false); EQUALS(resource.GetState(), ResourceState::Error); @@ -161,8 +158,7 @@ MODULE(resource, { })"); // Path with trailing slash - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/path-test/", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/path-test/"); EQUALS(resource.IsManifestValid(), true); STREQUALS(resource.GetName().c_str(), "path-test"); @@ -178,8 +174,7 @@ MODULE(resource, { "version": "1.0.0" })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/state-test-1", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/state-test-1"); EQUALS(resource.GetState(), ResourceState::Unloaded); EQUALS(resource.IsRunning(), false); @@ -194,8 +189,7 @@ MODULE(resource, { "version": "1.0.0" })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/state-test-2", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/state-test-2"); EQUALS(resource.GetState(), ResourceState::Error); EQUALS(resource.IsRunning(), false); @@ -217,8 +211,7 @@ MODULE(resource, { } })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/entry-test", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/entry-test"); std::string serverEntry = resource.GetServerEntryPoint(); std::string clientEntry = resource.GetClientEntryPoint(); @@ -236,8 +229,7 @@ MODULE(resource, { "version": "1.0.0" })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/no-entry", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/no-entry"); STREQUALS(resource.GetServerEntryPoint().c_str(), ""); STREQUALS(resource.GetClientEntryPoint().c_str(), ""); @@ -256,8 +248,7 @@ MODULE(resource, { } })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/export-test", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/export-test"); EQUALS(resource.HasExport("getData"), true); EQUALS(resource.HasExport("setConfig"), true); @@ -280,8 +271,7 @@ MODULE(resource, { } })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/depends-test", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/depends-test"); EQUALS(resource.DependsOn("core"), true); EQUALS(resource.DependsOn("utils"), true); @@ -298,8 +288,7 @@ MODULE(resource, { "version": "1.0.0" })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/restart-test-1", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/restart-test-1"); EQUALS(resource.GetRestartAttemptCount(), 0); @@ -319,8 +308,7 @@ MODULE(resource, { "version": "1.0.0" })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/restart-test-2", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/restart-test-2"); resource.RecordRestartAttempt(); resource.RecordRestartAttempt(); @@ -338,8 +326,7 @@ MODULE(resource, { "version": "1.0.0" })"); - flecs::world world; - Resource resource(TestResourceHelper::GetTestResourcePath() + "/backoff-test", &world); + Resource resource(TestResourceHelper::GetTestResourcePath() + "/backoff-test"); EQUALS(resource.GetRestartBackoffMs(), 0); @@ -362,8 +349,7 @@ MODULE(resource, { "version": "1.0.0" })"); - flecs::world world; - Resource original(TestResourceHelper::GetTestResourcePath() + "/move-test-1", &world); + Resource original(TestResourceHelper::GetTestResourcePath() + "/move-test-1"); original.RecordRestartAttempt(); Resource moved(std::move(original)); @@ -385,9 +371,8 @@ MODULE(resource, { "version": "2.0.0" })"); - flecs::world world; - Resource original(TestResourceHelper::GetTestResourcePath() + "/move-test-2a", &world); - Resource target(TestResourceHelper::GetTestResourcePath() + "/move-test-2b", &world); + Resource original(TestResourceHelper::GetTestResourcePath() + "/move-test-2a"); + Resource target(TestResourceHelper::GetTestResourcePath() + "/move-test-2b"); target = std::move(original); @@ -406,38 +391,13 @@ MODULE(resource, { })"); std::string expectedPath = TestResourceHelper::GetTestResourcePath() + "/path-access"; - flecs::world world; - Resource resource(expectedPath, &world); + Resource resource(expectedPath); STREQUALS(resource.GetPath().c_str(), expectedPath.c_str()); TestResourceHelper::Cleanup(); }); - // ==================== Root entity ==================== - - IT("Resource creates root resource", { - TestResourceHelper::CreateTestResource("fooResource", R"({ - "name": "foo-bar", - "version": "1.0.0" - })"); - - std::string expectedPath = TestResourceHelper::GetTestResourcePath() + "/fooResource"; - flecs::world world; - flecs::entity root; - { - Resource resource(expectedPath, &world); - - root = resource.GetRootEntity(); - EQUALS(root.is_alive(), true); - STREQUALS(root.name().c_str(), "foo-bar"); - EQUALS(root.get().value, &resource); - } - EQUALS(root.is_alive(), false); - - TestResourceHelper::Cleanup(); - }); - // ==================== Cleanup ==================== IT("final cleanup", { diff --git a/code/tests/modules/timer_context_ut.h b/code/tests/modules/timer_context_ut.h index 16ef5fb7b..9a17d849e 100644 --- a/code/tests/modules/timer_context_ut.h +++ b/code/tests/modules/timer_context_ut.h @@ -126,10 +126,9 @@ MODULE(timer_context, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = TimerContextTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); { @@ -181,10 +180,9 @@ MODULE(timer_context, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = TimerContextTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); { v8::Isolate *isolate = engine.GetIsolate(); @@ -218,10 +216,9 @@ MODULE(timer_context, { NodeEngine engine({}); EQUALS(engine.Init(), ScriptingError::SCRIPTING_NONE); - flecs::world world; ResourceManagerConfig config; config.resourcesPath = TimerContextTestHelper::GetTestPath(); - ResourceManager manager(&engine, &world, config); + ResourceManager manager(&engine, config); manager.DiscoverResources(); { diff --git a/vendors/mafianet/CMakeLists.txt b/vendors/mafianet/CMakeLists.txt index ccaa4ac56..e5d3827f6 100644 --- a/vendors/mafianet/CMakeLists.txt +++ b/vendors/mafianet/CMakeLists.txt @@ -1,5 +1,5 @@ # MafiaNet - MafiaHub's networking engine (fork of RakNet/SLikeNet). -# Vendored, pinned to tag v0.7.0 (see VERSION.txt). Built as a static library. +# Vendored, pinned to tag v0.8.0 (see VERSION.txt). Built as a static library. # # Public headers live under Source/include and are consumed with the # "mafianet/" prefix, e.g. #include . diff --git a/vendors/mafianet/README.md b/vendors/mafianet/README.md index bb0a953fc..3d74682a1 100644 --- a/vendors/mafianet/README.md +++ b/vendors/mafianet/README.md @@ -271,7 +271,11 @@ Available tests include: `EightPeerTest`, `MaximumConnectTest`, `PeerConnectDisc ## Changelog -### Version 0.7.0 (Latest) +### Version 0.8.0 (Latest) +- **Optional disconnect reason**: `CloseConnection` gains a final optional `const BitStream *reasonData` argument whose bytes are appended right after the `ID_DISCONNECTION_NOTIFICATION` message ID, so the remote peer can learn *why* it was dropped (e.g. a kick/ban enum plus a custom string). The receiver reads it like any other body — `packet->data + 1` for `packet->length - 1` bytes. Only graceful disconnects carry a reason; locally-synthesized notifications (`ID_CONNECTION_LOST`, timeout/dead-connection) stay payload-less, so always tolerate a zero-length body. Wire-backward-compatible: peers that only inspect `data[0]` are unaffected +- **Bug fix**: `RakPeer::CloseConnection` no longer coerces an unresolved target index (`-1`) to `0` and reads `remoteSystemList[0]` — which targeted an unrelated peer's slot or crashed on an unallocated list; the close socket is now resolved without assuming a valid slot index + +### Version 0.7.0 - **Virtual worlds (dimensions)**: new per-entity / per-observer `VirtualWorldId` scoping on top of ReplicaManager3 — the SA-MP `SetPlayerVirtualWorld` / routing-bucket model for instanced interiors (e.g. apartments). Players only see entities sharing their virtual world (or `VIRTUAL_WORLD_GLOBAL`), switchable at runtime with no reconnect. Derive entities from `VirtualWorldReplica3`; `Connection_RM3` gets `Get/SetVirtualWorld`; `ReplicaManager3` gets `GetConnectionsInVirtualWorld`/`GetGuidsInVirtualWorld` and `SetPlayerVirtualWorld`. The filter is authority-only, so a downloaded copy never despawns the entity at its owner. See `Samples/VirtualWorld` ### Version 0.6.1 diff --git a/vendors/mafianet/Source/include/mafianet/peer.h b/vendors/mafianet/Source/include/mafianet/peer.h index a5eb80742..1426946da 100644 --- a/vendors/mafianet/Source/include/mafianet/peer.h +++ b/vendors/mafianet/Source/include/mafianet/peer.h @@ -293,7 +293,7 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler /// \param[in] sendDisconnectionNotification True to send ID_DISCONNECTION_NOTIFICATION to the recipient. False to close it silently. /// \param[in] channel Which ordering channel to send the disconnection notification on, if any /// \param[in] disconnectionNotificationPriority Priority to send ID_DISCONNECTION_NOTIFICATION on. - void CloseConnection( const AddressOrGUID target, bool sendDisconnectionNotification, unsigned char orderingChannel=0, PacketPriority disconnectionNotificationPriority=LOW_PRIORITY ); + void CloseConnection( const AddressOrGUID target, bool sendDisconnectionNotification, unsigned char orderingChannel=0, PacketPriority disconnectionNotificationPriority=LOW_PRIORITY, const MafiaNet::BitStream *reasonData=nullptr ); /// \brief Cancel a pending connection attempt. /// \details If we are already connected, the connection stays open @@ -686,6 +686,15 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler RakNetSocket2* rakNetSocket; SystemIndex remoteSystemIndex; + // Optional disconnect-reason payload received with an incoming ID_DISCONNECTION_NOTIFICATION. The payload is + // stashed here when the notification arrives and copied into the user-facing notification packet that is + // synthesized after outstanding ACKs are flushed (the raw reliability-layer buffer is freed in between, so it + // cannot be delivered directly). null/0 when the remote sent no reason. Owned by this struct; copied out at + // delivery and freed via ClearDisconnectReason() on every slot teardown (including the immediate close that + // directly follows delivery) and on slot reuse. + unsigned char* disconnectReasonData; + unsigned int disconnectReasonLength; + #if LIBCAT_SECURITY==1 // Cached answer used internally by RakPeer to prevent DoS attacks based on the connexion handshake char answer[cat::EasyHandshake::ANSWER_BYTES]; @@ -729,7 +738,7 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler void ParseConnectionRequestPacket( RakPeer::RemoteSystemStruct *remoteSystem, const SystemAddress &systemAddress, const char *data, int byteSize); void OnConnectionRequest( RakPeer::RemoteSystemStruct *remoteSystem, MafiaNet::Time incomingTimestamp ); ///Send a reliable disconnect packet to this player and disconnect them when it is delivered - void NotifyAndFlagForShutdown( const SystemAddress systemAddress, bool performImmediate, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority ); + void NotifyAndFlagForShutdown( const SystemAddress systemAddress, bool performImmediate, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority, const MafiaNet::BitStream *reasonData=nullptr ); ///Returns how many remote systems initiated a connection to us unsigned int GetNumberOfRemoteInitiatedConnections( void ) const; /// \brief Get a free remote system from the list and assign our systemAddress to it. @@ -1031,7 +1040,9 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler private: // internal helpers - void CloseConnectionInternal2(const AddressOrGUID& systemIdentifier, bool sendDisconnectionNotification, bool performImmediate, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority, RakNetSocket2& socket); + void CloseConnectionInternal2(const AddressOrGUID& systemIdentifier, bool sendDisconnectionNotification, bool performImmediate, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority, RakNetSocket2& socket, const MafiaNet::BitStream *reasonData=nullptr); + // Free and null any stashed disconnect-reason payload for the given remote system (safe on null). + void ClearDisconnectReason(RemoteSystemStruct *remoteSystem); } // #if defined(SN_TARGET_PSP2) // __attribute__((aligned(8))) diff --git a/vendors/mafianet/Source/include/mafianet/peerinterface.h b/vendors/mafianet/Source/include/mafianet/peerinterface.h index 35b662386..4de44130a 100644 --- a/vendors/mafianet/Source/include/mafianet/peerinterface.h +++ b/vendors/mafianet/Source/include/mafianet/peerinterface.h @@ -264,7 +264,14 @@ class RAK_DLL_EXPORT RakPeerInterface /// \param[in] sendDisconnectionNotification True to send ID_DISCONNECTION_NOTIFICATION to the recipient. False to close it silently. /// \param[in] channel Which ordering channel to send the disconnection notification on, if any /// \param[in] disconnectionNotificationPriority Priority to send ID_DISCONNECTION_NOTIFICATION on. - virtual void CloseConnection( const AddressOrGUID target, bool sendDisconnectionNotification, unsigned char orderingChannel=0, PacketPriority disconnectionNotificationPriority=LOW_PRIORITY )=0; + /// \param[in] reasonData Optional payload appended after the ID_DISCONNECTION_NOTIFICATION message ID so the + /// remote peer can learn *why* it was dropped (e.g. an enum + custom string). The receiver reads it + /// from packet->data+1 (length packet->length-1), exactly like any other message body. Only graceful + /// disconnects (sendDisconnectionNotification==true) carry a reason; locally-synthesized notifications + /// (ID_CONNECTION_LOST and timeout/dead-connection paths) stay payload-less, so consumers must tolerate + /// a zero-length body. Pass nullptr (the default) for no reason. Appending bytes after the 1-byte ID is + /// wire-backward-compatible: peers that only read data[0] are unaffected. + virtual void CloseConnection( const AddressOrGUID target, bool sendDisconnectionNotification, unsigned char orderingChannel=0, PacketPriority disconnectionNotificationPriority=LOW_PRIORITY, const MafiaNet::BitStream *reasonData=nullptr )=0; /// Returns if a system is connected, disconnected, connecting in progress, or various other states /// \param[in] systemIdentifier The system we are referring to diff --git a/vendors/mafianet/Source/include/mafianet/version.h b/vendors/mafianet/Source/include/mafianet/version.h index e891d418e..2a1a875b7 100644 --- a/vendors/mafianet/Source/include/mafianet/version.h +++ b/vendors/mafianet/Source/include/mafianet/version.h @@ -15,10 +15,10 @@ // MafiaNet version. This is the current, authoritative version of the library. // Keep in sync with the project() VERSION in the root CMakeLists.txt. -#define MAFIANET_VERSION "0.7.0" -#define MAFIANET_VERSION_NUMBER_INT 700 +#define MAFIANET_VERSION "0.8.0" +#define MAFIANET_VERSION_NUMBER_INT 800 #define MAFIANET_VERSION_MAJOR 0 -#define MAFIANET_VERSION_MINOR 7 +#define MAFIANET_VERSION_MINOR 8 #define MAFIANET_VERSION_PATCH 0 // Defines kept here for backwards compatibility with RAKNET 4.081/4.082. diff --git a/vendors/mafianet/Source/src/RakPeer.cpp b/vendors/mafianet/Source/src/RakPeer.cpp index a9b2c2f7e..24938994b 100644 --- a/vendors/mafianet/Source/src/RakPeer.cpp +++ b/vendors/mafianet/Source/src/RakPeer.cpp @@ -619,6 +619,10 @@ StartupResult RakPeer::Startup( unsigned int maxConnections, SocketDescriptor *s remoteSystemList[ i ].connectMode=RemoteSystemStruct::NO_ACTION; remoteSystemList[ i ].MTUSize = defaultMTUSize; remoteSystemList[ i ].remoteSystemIndex = (SystemIndex) i; + // One-time zero-init: the array is OP_NEW_ARRAY allocated with no member init, so prime the + // reason-payload fields here before ClearDisconnectReason() can ever free them. + remoteSystemList[ i ].disconnectReasonData = 0; + remoteSystemList[ i ].disconnectReasonLength = 0; #ifdef _DEBUG remoteSystemList[ i ].reliabilityLayer.ApplyNetworkSimulator(_packetloss, _minExtraPing, _extraPingVariance); #endif @@ -1153,6 +1157,7 @@ void RakPeer::Shutdown( unsigned int blockDuration, unsigned char orderingChanne RakAssert(remoteSystemList[ i ].MTUSize <= MAXIMUM_MTU_SIZE); remoteSystemList[ i ].reliabilityLayer.Reset(false, remoteSystemList[ i ].MTUSize, false); remoteSystemList[ i ].rakNetSocket = 0; + ClearDisconnectReason(&remoteSystemList[ i ]); } @@ -1632,7 +1637,7 @@ unsigned int RakPeer::GetMaximumNumberOfPeers( void ) const // sendDisconnectionNotification: True to send ID_DISCONNECTION_NOTIFICATION to the recipient. False to close it silently. // channel: If blockDuration > 0, the disconnect packet will be sent on this channel // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -void RakPeer::CloseConnection( const AddressOrGUID target, bool sendDisconnectionNotification, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority ) +void RakPeer::CloseConnection( const AddressOrGUID target, bool sendDisconnectionNotification, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority, const MafiaNet::BitStream *reasonData ) { /* // This only be called from the user thread, for the user shutting down. @@ -1649,23 +1654,21 @@ void RakPeer::CloseConnection( const AddressOrGUID target, bool sendDisconnectio const SystemAddress address = (target.systemAddress == UNASSIGNED_SYSTEM_ADDRESS) ? GetSystemAddressFromGuid(target.rakNetGuid) : target.systemAddress; int remoteSystemListIndex = GetIndexFromSystemAddress(address); - // fallback to index 0 (i.e. preserve old behavior for now) - // #med - review this whole design here - if (remoteSystemListIndex == -1) { - remoteSystemListIndex = 0; - } - - // remoteSystemList[remoteSystemListIndex].rakNetSocket may be null: the socket - // can be released between resolving the connection and closing it (e.g. during - // rapid connect/disconnect churn), and the index-0 fallback above can land on a - // free slot. RakAssert is a no-op in release (NDEBUG), so the bare dereference - // would crash there. Guard explicitly and fall back to the primary socket — the - // same pattern used by the BCS_CLOSE_CONNECTION path below. - RakNetSocket2 *closeSocket = remoteSystemList[remoteSystemListIndex].rakNetSocket; + + // Resolve the socket to close on WITHOUT assuming a valid slot index. + // GetIndexFromSystemAddress returns -1 when the target isn't in the list; never + // coerce that to 0 — reading remoteSystemList[0] would crash if the list is + // unallocated, or target an unrelated peer's slot. When the index is valid use + // that slot's socket (it may itself be null during rapid connect/disconnect + // churn). Either way, fall back to the primary socket — the same pattern used by + // the BCS_CLOSE_CONNECTION path below. With no socket at all there is nothing to + // close, so bail out. + RakNetSocket2 *closeSocket = (remoteSystemListIndex != -1) ? remoteSystemList[remoteSystemListIndex].rakNetSocket : nullptr; if (closeSocket == nullptr && socketList.Size() > 0) closeSocket = socketList[0]; - if (closeSocket != nullptr) - CloseConnectionInternal2(target, sendDisconnectionNotification, false, orderingChannel, disconnectionNotificationPriority, *closeSocket); + if (closeSocket == nullptr) + return; + CloseConnectionInternal2(target, sendDisconnectionNotification, false, orderingChannel, disconnectionNotificationPriority, *closeSocket, reasonData); // 12/14/09 Return ID_CONNECTION_LOST when calling CloseConnection with sendDisconnectionNotification==false, elsewise it is never returned if (sendDisconnectionNotification==false && GetConnectionState(target)==IS_CONNECTED) @@ -1674,7 +1677,7 @@ void RakPeer::CloseConnection( const AddressOrGUID target, bool sendDisconnectio packet->data[ 0 ] = ID_CONNECTION_LOST; // DeadConnection packet->guid = target.rakNetGuid==UNASSIGNED_RAKNET_GUID ? GetGuidFromSystemAddress(target.systemAddress) : target.rakNetGuid; packet->systemAddress = address; - packet->systemAddress.systemIndex = static_cast(remoteSystemListIndex); + packet->systemAddress.systemIndex = static_cast(remoteSystemListIndex == -1 ? 0 : remoteSystemListIndex); packet->guid.systemIndex=packet->systemAddress.systemIndex; packet->wasGeneratedLocally=true; // else processed twice AddPacketToProducer(packet); @@ -3551,10 +3554,15 @@ void RakPeer::OnConnectionRequest( RakPeer::RemoteSystemStruct *remoteSystem, Ma } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -void RakPeer::NotifyAndFlagForShutdown( const SystemAddress systemAddress, bool performImmediate, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority ) +void RakPeer::NotifyAndFlagForShutdown( const SystemAddress systemAddress, bool performImmediate, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority, const MafiaNet::BitStream *reasonData ) { MafiaNet::BitStream temp( sizeof(unsigned char) ); temp.Write( (MessageID)ID_DISCONNECTION_NOTIFICATION ); + // Optionally append a caller-supplied reason payload right after the 1-byte ID. The ID occupies exactly 8 bits so + // the payload stays byte-aligned; the remote reads it from packet->data+1 (length packet->length-1). Wire-backward + // compatible: peers that only inspect data[0] ignore the extra bytes. + if (reasonData != nullptr && reasonData->GetNumberOfBytesUsed() > 0) + temp.Write( (const char*)reasonData->GetData(), reasonData->GetNumberOfBytesUsed() ); if (performImmediate) { SendImmediate((char*)temp.GetData(), temp.GetNumberOfBitsUsed(), disconnectionNotificationPriority, RELIABLE_ORDERED, orderingChannel, systemAddress, false, false, MafiaNet::GetTimeUS(), 0); @@ -3567,6 +3575,18 @@ void RakPeer::NotifyAndFlagForShutdown( const SystemAddress systemAddress, bool } } // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +void RakPeer::ClearDisconnectReason( RemoteSystemStruct *remoteSystem ) +{ + if (remoteSystem==0) + return; + if (remoteSystem->disconnectReasonData!=0) + { + rakFree_Ex(remoteSystem->disconnectReasonData, _FILE_AND_LINE_); + remoteSystem->disconnectReasonData=0; + } + remoteSystem->disconnectReasonLength=0; +} +// -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- unsigned int RakPeer::GetNumberOfRemoteInitiatedConnections( void ) const { if ( remoteSystemList == 0 || endThreads == true ) @@ -3632,6 +3652,8 @@ RakPeer::RemoteSystemStruct * RakPeer::AssignSystemAddressToRemoteSystemList( co remoteSystem=remoteSystemList+assignedIndex; ReferenceRemoteSystem(systemAddress, assignedIndex); + // Stale reason payload from a prior occupant of this slot must never leak into a new connection. + ClearDisconnectReason(remoteSystem); remoteSystem->MTUSize=defaultMTUSize; remoteSystem->guid=guid; remoteSystem->isActive = true; // This one line causes future incoming packets to go through the reliability layer @@ -4126,7 +4148,7 @@ void RakPeer::CloseConnectionInternal( const AddressOrGUID& systemIdentifier, bo } // #med - better integrate directly in CloseConnectionInternal2() -void RakPeer::CloseConnectionInternal2(const AddressOrGUID& systemIdentifier, bool sendDisconnectionNotification, bool performImmediate, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority, RakNetSocket2& socket) +void RakPeer::CloseConnectionInternal2(const AddressOrGUID& systemIdentifier, bool sendDisconnectionNotification, bool performImmediate, unsigned char orderingChannel, PacketPriority disconnectionNotificationPriority, RakNetSocket2& socket, const MafiaNet::BitStream *reasonData) { RakAssert(orderingChannel < 32); @@ -4152,7 +4174,7 @@ void RakPeer::CloseConnectionInternal2(const AddressOrGUID& systemIdentifier, bo if (sendDisconnectionNotification) { - NotifyAndFlagForShutdown(target, performImmediate, orderingChannel, disconnectionNotificationPriority); + NotifyAndFlagForShutdown(target, performImmediate, orderingChannel, disconnectionNotificationPriority, reasonData); } else { @@ -4178,6 +4200,9 @@ void RakPeer::CloseConnectionInternal2(const AddressOrGUID& systemIdentifier, bo RakAssert(remoteSystemList[index].MTUSize <= MAXIMUM_MTU_SIZE); remoteSystemList[index].reliabilityLayer.Reset(false, remoteSystemList[index].MTUSize, false); + // Free any stashed disconnect-reason payload (e.g. delivered already, or never consumed) + ClearDisconnectReason(&remoteSystemList[index]); + // Not using this socket remoteSystemList[index].rakNetSocket = 0; } @@ -5909,7 +5934,12 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) // remoteSystem->reliabilityLayer.GetUndeliveredMessages(&undeliveredMessages,remoteSystem->MTUSize); // packet=AllocPacket(sizeof( char ) + undeliveredMessages.GetNumberOfBytesUsed()); - packet=AllocPacket(sizeof( char ), _FILE_AND_LINE_); + // A graceful remote disconnect (DISCONNECT_ON_NO_ACK) may carry a reason payload stashed when the + // notification arrived. Only ID_DISCONNECTION_NOTIFICATION carries it; ID_CONNECTION_LOST and + // ID_CONNECTION_ATTEMPT_FAILED are locally synthesized and stay payload-less. + const bool attachReason = remoteSystem->connectMode==RemoteSystemStruct::DISCONNECT_ON_NO_ACK && remoteSystem->disconnectReasonData!=0; + const unsigned int reasonLength = attachReason ? remoteSystem->disconnectReasonLength : 0; + packet=AllocPacket(sizeof( char ) + reasonLength, _FILE_AND_LINE_); if (remoteSystem->connectMode==RemoteSystemStruct::REQUESTED_CONNECTION) packet->data[ 0 ] = ID_CONNECTION_ATTEMPT_FAILED; // Attempted a connection and couldn't else if (remoteSystem->connectMode==RemoteSystemStruct::CONNECTED) @@ -5917,6 +5947,9 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) else packet->data[ 0 ] = ID_DISCONNECTION_NOTIFICATION; // DeadConnection + if (attachReason) + memcpy(packet->data + sizeof(unsigned char), remoteSystem->disconnectReasonData, reasonLength); + // memcpy(packet->data+1, undeliveredMessages.GetData(), undeliveredMessages.GetNumberOfBytesUsed()); packet->guid = remoteSystem->guid; @@ -6113,6 +6146,21 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) } else if ( (unsigned char) data[ 0 ] == ID_DISCONNECTION_NOTIFICATION ) { + // Stash any reason payload (everything after the 1-byte ID) so it can ride along with the + // user-facing notification packet synthesized once outstanding ACKs are flushed. This raw + // reliability-layer buffer is freed below, so the bytes have to be copied out now. + ClearDisconnectReason(remoteSystem); + if (byteSize > sizeof(unsigned char)) + { + const unsigned int reasonLength = byteSize - (unsigned int) sizeof(unsigned char); + remoteSystem->disconnectReasonData = (unsigned char*) rakMalloc_Ex(reasonLength, _FILE_AND_LINE_); + if (remoteSystem->disconnectReasonData != 0) + { + memcpy(remoteSystem->disconnectReasonData, data + sizeof(unsigned char), reasonLength); + remoteSystem->disconnectReasonLength = reasonLength; + } + } + // We shouldn't close the connection immediately because we need to ack the ID_DISCONNECTION_NOTIFICATION remoteSystem->connectMode=RemoteSystemStruct::DISCONNECT_ON_NO_ACK; rakFree_Ex(data, _FILE_AND_LINE_ ); diff --git a/vendors/mafianet/VERSION.txt b/vendors/mafianet/VERSION.txt index 422d97938..a4fd99f07 100644 --- a/vendors/mafianet/VERSION.txt +++ b/vendors/mafianet/VERSION.txt @@ -1,2 +1,2 @@ MafiaNet vendored from https://github.com/MafiaHub/MafiaNet -Pinned: tag v0.7.0 (commit 0d2a630d54c34361389e258576197ce938c0d48e) +Pinned: tag v0.8.0 (commit 34d7472e458d00f5ec79a097afda5d0f3042983b)