diff --git a/README.md b/README.md index 5135a6754..fa8f0279e 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,16 @@ cd Framework ``` ### Build on macOS/Linux + +On Debian/Ubuntu, install the required system development packages first: + +```sh +sudo apt install build-essential cmake \ + libssl-dev zlib1g-dev libssh2-1-dev libcurl4-openssl-dev +``` + +Equivalents on other distributions: `openssl-devel`, `zlib-devel`, `libssh2-devel`, `libcurl-devel` (Fedora/RHEL). + ``` # Configure CMake project cmake -B build diff --git a/code/framework/CMakeLists.txt b/code/framework/CMakeLists.txt index a6de98537..bcb6dbfd3 100644 --- a/code/framework/CMakeLists.txt +++ b/code/framework/CMakeLists.txt @@ -57,6 +57,11 @@ set(FRAMEWORK_SERVER_SRC src/integrations/server/instance.cpp src/integrations/server/networking/engine.cpp + # Native plugin system (server-side) + src/integrations/server/plugins/loader.cpp + src/integrations/server/plugins/host_impl.cpp + src/integrations/server/plugins/manager.cpp + # JavaScript scripting (server module) src/integrations/server/scripting/module.cpp ) diff --git a/code/framework/src/integrations/server/instance.cpp b/code/framework/src/integrations/server/instance.cpp index 2b71fc198..20faaf3a4 100644 --- a/code/framework/src/integrations/server/instance.cpp +++ b/code/framework/src/integrations/server/instance.cpp @@ -44,6 +44,7 @@ namespace Framework::Integrations::Server { _masterlist = std::make_unique(); _commandListener = std::make_unique(); _commandProcessor = std::make_unique(); + _pluginManager = std::make_unique(); } Instance::~Instance() { @@ -139,7 +140,14 @@ namespace Framework::Integrations::Server { // Initialize mod subsystems PostInit(); - + + // Native plugins load after PostInit so the mod has had a chance to + // register subsystems plugins may depend on, but before the scripting + // engine comes up so plugins can register scripting builtins from + // their PostScriptInit hook. + _pluginManager->Init(_webServer.get(), _commandProcessor.get(), _worldEngine.get()); + _pluginManager->LoadAll(_opts.modulesDir, _opts.modulesList); + const auto sdkCallback = [this](Framework::Scripting::Engine *engine) { this->RegisterScriptingBuiltins(engine); }; @@ -154,6 +162,7 @@ namespace Framework::Integrations::Server { CoreModules::SetScriptingModule(_scriptingModule.get()); PostScriptInit(); + _pluginManager->PostScriptInit(); // Discover resources _scriptingModule->GetResourceManager()->DiscoverResources(); @@ -231,6 +240,22 @@ namespace Framework::Integrations::Server { _opts.bindMapName = _fileConfig->Get("map"); _opts.maxPlayers = _fileConfig->Get("maxplayers"); _opts.bindSecretKey = _fileConfig->Get("server-token"); + + // Plugin loading: optional. modules_dir defaults to "modules" + // relative to the server's working directory; modules is the + // ordered list of plugin names to load. + _opts.modulesDir = _fileConfig->GetDefault("modules_dir", _opts.modulesDir); + if (auto *doc = _fileConfig->GetDocument(); doc && doc->contains("modules")) { + if ((*doc)["modules"].is_array()) { + _opts.modulesList.clear(); + for (const auto &m : (*doc)["modules"]) { + if (m.is_string()) _opts.modulesList.push_back(m.get()); + } + } + else { + Logging::GetLogger(FRAMEWORK_INNER_SERVER)->warn("server.json 'modules' field exists but is not an array; ignoring"); + } + } } catch (const std::exception &ex) { Logging::GetLogger(FRAMEWORK_INNER_SERVER)->critical("JSON config has missing fields: {}", ex.what()); @@ -314,6 +339,9 @@ namespace Framework::Integrations::Server { if (_onPlayerDisconnectCallback) _onPlayerDisconnectCallback(e, guid.g); + if (_pluginManager) + _pluginManager->DispatchPlayerDisconnect(e.id(), guid.g); + _worldEngine->RemoveEntity(e); } @@ -345,8 +373,12 @@ namespace Framework::Integrations::Server { net->RegisterMessage(Framework::Networking::Messages::GameMessages::GAME_INIT_PLAYER, [this, net](SLNet::RakNetGUID guid, ClientInitPlayer *stub) { const auto e = _worldEngine->GetEntityByGUID(guid.g); - if (_onPlayerConnectCallback && e.is_valid() && e.is_alive()) - _onPlayerConnectCallback(e, guid.g); + if (e.is_valid() && e.is_alive()) { + if (_onPlayerConnectCallback) + _onPlayerConnectCallback(e, guid.g); + if (_pluginManager) + _pluginManager->DispatchPlayerConnect(e.id(), guid.g); + } }); // Note: Client-to-server events are handled through the JS Events system @@ -500,6 +532,10 @@ namespace Framework::Integrations::Server { PreShutdown(); + if (_pluginManager) { + _pluginManager->PreShutdown(); + } + if (_scriptingModule) { _scriptingModule->PreShutdown(); } @@ -516,6 +552,13 @@ namespace Framework::Integrations::Server { _webServer->Shutdown(); } + // Plugin libraries are unloaded last so any in-flight HTTP requests + // handled by plugin trampolines have already finished with the + // webserver stopped above. + if (_pluginManager) { + _pluginManager->Shutdown(); + } + if (_worldEngine) { _worldEngine->Shutdown(); } @@ -555,6 +598,10 @@ namespace Framework::Integrations::Server { _commandListener->Update(); } + if (_pluginManager) { + _pluginManager->Update(_opts.worldConfig.tickInterval); + } + if (_masterlist->IsInitialized()) { Services::ServerInfo info {}; info.port = _opts.bindPort; diff --git a/code/framework/src/integrations/server/instance.h b/code/framework/src/integrations/server/instance.h index eb747bb14..cbc1bf31a 100644 --- a/code/framework/src/integrations/server/instance.h +++ b/code/framework/src/integrations/server/instance.h @@ -14,6 +14,7 @@ #include "http/webserver.h" #include "logging/logger.h" #include "networking/engine.h" +#include "plugins/manager.h" #include "scripting/module.h" #include "services/masterlist.h" #include "utils/config.h" @@ -70,6 +71,13 @@ namespace Framework::Integrations::Server { int32_t maxPlayers; std::string httpServeDir; + // Native plugins. modulesDir is searched for each name in modulesList; + // a plugin called "foo" lives at /foo/foo.module.json plus + // its compiled library. Both fields are populated from server.json + // ("modules_dir" and "modules") and overridable at code level. + std::string modulesDir = "modules"; + std::vector modulesList; + bool enableSignals; // update intervals @@ -98,6 +106,7 @@ namespace Framework::Integrations::Server { std::unique_ptr _masterlist; std::unique_ptr _commandListener; std::unique_ptr _commandProcessor; + std::unique_ptr _pluginManager; void InitEndpoints(); void InitModules() const; @@ -181,6 +190,10 @@ namespace Framework::Integrations::Server { World::Archetypes::StreamingFactory *GetStreamingFactory() const { return _streamingFactory.get(); } + + Plugins::PluginManager *GetPluginManager() const { + return _pluginManager.get(); + } // Register a custom command with the command processor Utils::Result RegisterCommand(std::string_view name, std::initializer_list options, const Utils::CommandProc &proc, const std::string &desc) { diff --git a/code/framework/src/integrations/server/plugins/host_impl.cpp b/code/framework/src/integrations/server/plugins/host_impl.cpp new file mode 100644 index 000000000..3f5eb6fb3 --- /dev/null +++ b/code/framework/src/integrations/server/plugins/host_impl.cpp @@ -0,0 +1,270 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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 "host_impl.h" + +#include "http/webserver.h" +#include "logging/logger.h" +#include "utils/command_processor.h" +#include "world/modules/base.hpp" +#include "world/server.h" + +#include +#include +#include + +namespace Framework::Integrations::Server::Plugins { + + /* + * Bridging between the C ABI's opaque FwPlayer* and the world's + * flecs::entity. The dispatcher fills a stack-local PlayerHandle and + * hands its address to plugin callbacks. The handle is valid only for + * the duration of the dispatched call. + */ + struct PlayerHandle { + HostImpl *host; + uint64_t entityId; + uint64_t guid; + }; + + /* ----------------------------------------------------------------------- */ + /* Vtable function implementations */ + /* ----------------------------------------------------------------------- */ + + static FwLogger *VT_logger_for(FwHost *host, const char *plugin_name) { + auto *impl = static_cast(host->internal); + /* No name (or empty) → return the plugin's default logger. */ + if (!plugin_name || !*plugin_name) { + return reinterpret_cast(impl->logger.get()); + } + + /* Namespace every plugin-requested logger under "plugin:" so a + * plugin can't impersonate a Framework-internal logger by passing + * names like FRAMEWORK_INNER_SERVER. */ + std::string fullName = std::string("plugin:") + plugin_name; + + std::lock_guard lock(impl->loggerCacheMutex); + auto it = impl->loggerCache.find(fullName); + if (it != impl->loggerCache.end()) { + return reinterpret_cast(it->second.get()); + } + auto sub = Framework::Logging::GetLogger(fullName.c_str()); + auto [inserted, _] = impl->loggerCache.emplace(fullName, std::move(sub)); + return reinterpret_cast(inserted->second.get()); + } + + static void VT_log_debug(FwLogger *logger, const char *message) { + if (logger && message) reinterpret_cast(logger)->debug("{}", message); + } + static void VT_log_info(FwLogger *logger, const char *message) { + if (logger && message) reinterpret_cast(logger)->info("{}", message); + } + static void VT_log_warn(FwLogger *logger, const char *message) { + if (logger && message) reinterpret_cast(logger)->warn("{}", message); + } + static void VT_log_error(FwLogger *logger, const char *message) { + if (logger && message) reinterpret_cast(logger)->error("{}", message); + } + + static int VT_register_command(FwHost *host, const char *name, const char *description, FwCommandCallback callback, void *userdata) { + auto *impl = static_cast(host->internal); + if (!impl->commandProcessor || !name || !callback) return 1; + + std::string nameStr(name); + std::string descStr(description ? description : ""); + + /* cxxopts treats the first parsed token as the program name and + * excludes it from unmatched(); we re-prepend the command name so + * the plugin sees the conventional argv[0]==command shape. */ + auto proc = [callback, userdata, nameStr](cxxopts::ParseResult &result) { + std::vector args = result.unmatched(); + std::vector argv; + argv.reserve(args.size() + 1); + argv.push_back(nameStr.c_str()); + for (auto &a : args) argv.push_back(a.c_str()); + try { + callback(static_cast(argv.size()), argv.data(), userdata); + } + catch (const std::exception &e) { + Framework::Logging::GetLogger("plugins")->error("Plugin command '{}' threw: {}", nameStr, e.what()); + } + catch (...) { + Framework::Logging::GetLogger("plugins")->error("Plugin command '{}' threw non-std exception", nameStr); + } + }; + + auto result = impl->commandProcessor->RegisterCommand(nameStr, std::initializer_list {}, proc, descStr); + if (result.GetError() != Framework::Utils::CommandProcessorError::COMMAND_NONE) { + return static_cast(result.GetError()); + } + impl->registeredCommands.push_back(nameStr); + return 0; + } + + static int VT_register_http_endpoint(FwHost *host, const char *path, FwHttpCallback callback, void *userdata) { + auto *impl = static_cast(host->internal); + if (!impl->webserver || !path || !callback) return 1; + + std::string pathStr(path); + impl->webserver->RegisterRequest(pathStr, [callback, userdata, pathStr](const httplib::Request &req, httplib::Response &res) { + try { + callback(req.method.c_str(), req.path.c_str(), req.body.data(), req.body.size(), reinterpret_cast(&res), userdata); + } + catch (const std::exception &e) { + Framework::Logging::GetLogger("plugins")->error("Plugin HTTP handler '{}' threw: {}", pathStr, e.what()); + /* Drop whatever the handler partially wrote so the client + * doesn't get a 500 with a half-built body. */ + res.body.clear(); + res.status = 500; + } + catch (...) { + Framework::Logging::GetLogger("plugins")->error("Plugin HTTP handler '{}' threw non-std exception", pathStr); + res.body.clear(); + res.status = 500; + } + }); + return 0; + } + + static void VT_http_response_set_status(FwHttpResponse *response, int status) { + if (response) reinterpret_cast(response)->status = status; + } + + static void VT_http_response_set_body(FwHttpResponse *response, const char *body, size_t body_len) { + if (response && body) reinterpret_cast(response)->body.assign(body, body_len); + } + + static void VT_http_response_set_header(FwHttpResponse *response, const char *key, const char *value) { + if (response && key && value) reinterpret_cast(response)->set_header(key, value); + } + + static int VT_on_player_connect(FwHost *host, FwPlayerEventCallback callback, void *userdata) { + auto *impl = static_cast(host->internal); + if (!callback) return 1; + auto slot = std::make_unique(); + slot->fn = callback; + slot->userdata = userdata; + std::lock_guard lock(impl->slotMutex); + impl->onConnect.push_back(std::move(slot)); + return 0; + } + + static int VT_on_player_disconnect(FwHost *host, FwPlayerEventCallback callback, void *userdata) { + auto *impl = static_cast(host->internal); + if (!callback) return 1; + auto slot = std::make_unique(); + slot->fn = callback; + slot->userdata = userdata; + std::lock_guard lock(impl->slotMutex); + impl->onDisconnect.push_back(std::move(slot)); + return 0; + } + + static uint64_t VT_player_get_guid(FwPlayer *player) { + if (!player) return 0; + return reinterpret_cast(player)->guid; + } + + static size_t VT_player_get_nickname(FwPlayer *player, char *buf, size_t buf_size) { + if (!player) return 0; + auto *handle = reinterpret_cast(player); + if (!handle->host || !handle->host->worldEngine) return 0; + auto world = handle->host->worldEngine->GetWorld(); + if (!world) return 0; + flecs::entity e(*world, handle->entityId); + if (!e.is_valid()) return 0; + const auto *streamer = e.get(); + if (!streamer) return 0; + const std::string &nick = streamer->nickname; + if (buf && buf_size > 0) { + const size_t toCopy = std::min(nick.size(), buf_size - 1); + std::memcpy(buf, nick.data(), toCopy); + buf[toCopy] = '\0'; + } + return nick.size(); + } + + /* ----------------------------------------------------------------------- */ + /* Public surface */ + /* ----------------------------------------------------------------------- */ + + const FwHostVTable *GetHostVTable() { + static const FwHostVTable vt = { + /* abi_version */ FW_PLUGIN_ABI_VERSION, + /* logger_for */ &VT_logger_for, + /* log_debug */ &VT_log_debug, + /* log_info */ &VT_log_info, + /* log_warn */ &VT_log_warn, + /* log_error */ &VT_log_error, + /* register_command */ &VT_register_command, + /* register_http_endpoint */ &VT_register_http_endpoint, + /* http_response_set_status */ &VT_http_response_set_status, + /* http_response_set_body */ &VT_http_response_set_body, + /* http_response_set_header */ &VT_http_response_set_header, + /* on_player_connect */ &VT_on_player_connect, + /* on_player_disconnect */ &VT_on_player_disconnect, + /* player_get_guid */ &VT_player_get_guid, + /* player_get_nickname */ &VT_player_get_nickname, + }; + return &vt; + } + + FwHost MakeFwHost(HostImpl *impl) { + FwHost host {}; + host.vtable = GetHostVTable(); + host.internal = impl; + return host; + } + + void DispatchPlayerConnect(HostImpl *impl, uint64_t entityId, uint64_t guid) { + if (!impl) return; + PlayerHandle handle {impl, entityId, guid}; + /* Copy the slot list under the lock so callbacks can register more + * slots without us iterating a mutating vector. */ + std::vector snapshot; + { + std::lock_guard lock(impl->slotMutex); + snapshot.reserve(impl->onConnect.size()); + for (auto &slot : impl->onConnect) snapshot.push_back(*slot); + } + for (auto &slot : snapshot) { + try { + slot.fn(reinterpret_cast(&handle), guid, slot.userdata); + } + catch (const std::exception &e) { + Framework::Logging::GetLogger("plugins")->error("Plugin '{}' onConnect threw: {}", impl->pluginName, e.what()); + } + catch (...) { + Framework::Logging::GetLogger("plugins")->error("Plugin '{}' onConnect threw non-std exception", impl->pluginName); + } + } + } + + void DispatchPlayerDisconnect(HostImpl *impl, uint64_t entityId, uint64_t guid) { + if (!impl) return; + PlayerHandle handle {impl, entityId, guid}; + std::vector snapshot; + { + std::lock_guard lock(impl->slotMutex); + snapshot.reserve(impl->onDisconnect.size()); + for (auto &slot : impl->onDisconnect) snapshot.push_back(*slot); + } + for (auto &slot : snapshot) { + try { + slot.fn(reinterpret_cast(&handle), guid, slot.userdata); + } + catch (const std::exception &e) { + Framework::Logging::GetLogger("plugins")->error("Plugin '{}' onDisconnect threw: {}", impl->pluginName, e.what()); + } + catch (...) { + Framework::Logging::GetLogger("plugins")->error("Plugin '{}' onDisconnect threw non-std exception", impl->pluginName); + } + } + } + +} // namespace Framework::Integrations::Server::Plugins diff --git a/code/framework/src/integrations/server/plugins/host_impl.h b/code/framework/src/integrations/server/plugins/host_impl.h new file mode 100644 index 000000000..61a9d10e1 --- /dev/null +++ b/code/framework/src/integrations/server/plugins/host_impl.h @@ -0,0 +1,87 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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 "sdk/fw_plugin_abi.h" + +#include +#include +#include +#include +#include +#include + +namespace Framework { + namespace HTTP { + class Webserver; + } + namespace Utils { + class CommandProcessor; + } + namespace World { + class ServerEngine; + } +} // namespace Framework + +namespace Framework::Integrations::Server::Plugins { + + /* + * Concrete backing for FwHost::internal. One instance per loaded plugin + * so callback userdata lifetimes match the plugin's own lifetime, and + * we can tear everything down at unload without leaking handles. + * + * Stores raw subsystem pointers — never owns. The owning Server::Instance + * outlives every plugin (plugins unload during Instance::Shutdown). + */ + struct HostImpl { + std::string pluginName; + Framework::HTTP::Webserver *webserver = nullptr; + Framework::Utils::CommandProcessor *commandProcessor = nullptr; + Framework::World::ServerEngine *worldEngine = nullptr; + + std::shared_ptr logger; /* scoped: "plugin:" */ + + /* On-demand sub-loggers requested via logger_for(host, name). + * Keyed by the "plugin:" full identifier, holding + * the shared_ptr so the raw pointer handed to the plugin stays + * valid for the plugin's lifetime. */ + std::unordered_map> loggerCache; + std::mutex loggerCacheMutex; + + /* Names of commands the plugin registered, so we can deregister + * cleanly when the plugin unloads. */ + std::vector registeredCommands; + + /* Per-event callback slots. Pointers are stable across vector growth + * because we store unique_ptrs. */ + struct PlayerEventSlot { + FwPlayerEventCallback fn; + void *userdata; + }; + std::vector> onConnect; + std::vector> onDisconnect; + + std::mutex slotMutex; /* guards onConnect/onDisconnect for safe iteration */ + }; + + /* Returns the shared, statically-initialised host vtable. Pointer is + * valid for the lifetime of the process and identical across plugins. */ + const FwHostVTable *GetHostVTable(); + + /* Construct a FwHost owning the given HostImpl. The caller owns the impl; + * the returned FwHost references it by raw pointer. */ + FwHost MakeFwHost(HostImpl *impl); + + /* Invoke every registered onConnect callback for the given entity/guid. + * Exceptions thrown by plugin callbacks are caught and logged; one + * misbehaving plugin does not prevent others from running. */ + void DispatchPlayerConnect(HostImpl *impl, uint64_t entityId, uint64_t guid); + void DispatchPlayerDisconnect(HostImpl *impl, uint64_t entityId, uint64_t guid); + +} // namespace Framework::Integrations::Server::Plugins diff --git a/code/framework/src/integrations/server/plugins/loader.cpp b/code/framework/src/integrations/server/plugins/loader.cpp new file mode 100644 index 000000000..45b04a20a --- /dev/null +++ b/code/framework/src/integrations/server/plugins/loader.cpp @@ -0,0 +1,102 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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 "loader.h" + +#if defined(_WIN32) +# include +#else +# include +#endif + +namespace Framework::Integrations::Server::Plugins { + +#if defined(_WIN32) + static std::string FormatWin32Error(DWORD code) { + if (code == 0) return {}; + LPSTR buf = nullptr; + DWORD size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast(&buf), 0, nullptr); + std::string msg(buf, size); + LocalFree(buf); + while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r')) msg.pop_back(); + return msg; + } +#endif + + SharedLibrary::~SharedLibrary() { + Close(); + } + + SharedLibrary::SharedLibrary(SharedLibrary &&other) noexcept: _handle(other._handle), _path(std::move(other._path)), _lastError(std::move(other._lastError)) { + other._handle = nullptr; + } + + SharedLibrary &SharedLibrary::operator=(SharedLibrary &&other) noexcept { + if (this != &other) { + Close(); + _handle = other._handle; + _path = std::move(other._path); + _lastError = std::move(other._lastError); + other._handle = nullptr; + } + return *this; + } + + void SharedLibrary::Close() { + if (!_handle) return; +#if defined(_WIN32) + FreeLibrary(static_cast(_handle)); +#else + dlclose(_handle); +#endif + _handle = nullptr; + } + + bool SharedLibrary::Open(const std::string &path) { + Close(); + _path = path; + _lastError.clear(); +#if defined(_WIN32) + _handle = LoadLibraryA(path.c_str()); + if (!_handle) { + _lastError = FormatWin32Error(GetLastError()); + return false; + } +#else + _handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!_handle) { + const char *err = dlerror(); + _lastError = err ? err : "unknown dlopen error"; + return false; + } +#endif + return true; + } + + void *SharedLibrary::Symbol(const char *name) const { + if (!_handle) return nullptr; +#if defined(_WIN32) + return reinterpret_cast(GetProcAddress(static_cast(_handle), name)); +#else + /* Clear any prior error so the nullptr-vs-actually-null check works */ + dlerror(); + return dlsym(_handle, name); +#endif + } + + std::string SharedLibrary::PlatformFilename(const std::string &entry) { +#if defined(_WIN32) + return entry + ".dll"; +#elif defined(__APPLE__) + return "lib" + entry + ".dylib"; +#else + return "lib" + entry + ".so"; +#endif + } + +} // namespace Framework::Integrations::Server::Plugins diff --git a/code/framework/src/integrations/server/plugins/loader.h b/code/framework/src/integrations/server/plugins/loader.h new file mode 100644 index 000000000..df12b4f9a --- /dev/null +++ b/code/framework/src/integrations/server/plugins/loader.h @@ -0,0 +1,61 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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::Integrations::Server::Plugins { + + /* + * RAII wrapper around platform-specific shared-library loading. + * Move-only; closes the handle on destruction. + */ + class SharedLibrary final { + public: + SharedLibrary() = default; + ~SharedLibrary(); + + SharedLibrary(const SharedLibrary &) = delete; + SharedLibrary &operator=(const SharedLibrary &) = delete; + + SharedLibrary(SharedLibrary &&other) noexcept; + SharedLibrary &operator=(SharedLibrary &&other) noexcept; + + /* Open the library at the given absolute path. Returns true on success; + * on failure, GetLastError() describes the platform error. */ + bool Open(const std::string &path); + + /* Returns the address of an exported symbol or nullptr if missing. */ + void *Symbol(const char *name) const; + + bool IsOpen() const { + return _handle != nullptr; + } + + const std::string &GetPath() const { + return _path; + } + const std::string &GetLastError() const { + return _lastError; + } + + /* Returns the platform-appropriate filename for a plugin "entry" stem + * (e.g. "hello-plugin" -> "hello-plugin.dll" on Windows, + * "libhello-plugin.so" on Linux, "libhello-plugin.dylib" on macOS). */ + static std::string PlatformFilename(const std::string &entry); + + private: + void Close(); + + void *_handle = nullptr; + std::string _path; + std::string _lastError; + }; + +} // namespace Framework::Integrations::Server::Plugins diff --git a/code/framework/src/integrations/server/plugins/manager.cpp b/code/framework/src/integrations/server/plugins/manager.cpp new file mode 100644 index 000000000..83bcdb464 --- /dev/null +++ b/code/framework/src/integrations/server/plugins/manager.cpp @@ -0,0 +1,277 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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 "manager.h" + +#include "logging/logger.h" +#include "utils/command_processor.h" + +#include +#include +#include +#include +#include +#include + +namespace Framework::Integrations::Server::Plugins { + + static constexpr const char *kLoggerName = "plugins"; + + PluginManager::PluginManager() = default; + + PluginManager::~PluginManager() { + if (!_shutdownCalled) Shutdown(); + } + + void PluginManager::Init(Framework::HTTP::Webserver *webserver, Framework::Utils::CommandProcessor *commandProcessor, Framework::World::ServerEngine *worldEngine) { + _webserver = webserver; + _commandProcessor = commandProcessor; + _worldEngine = worldEngine; + } + + size_t PluginManager::LoadAll(const std::string &modulesDir, const std::vector &pluginNames) { + auto log = Framework::Logging::GetLogger(kLoggerName); + if (pluginNames.empty()) { + log->debug("No plugins listed in server config; skipping load"); + return 0; + } + log->info("Loading {} plugin(s) from '{}'", pluginNames.size(), modulesDir); + size_t loaded = 0; + for (const auto &name : pluginNames) { + if (LoadOne(modulesDir, name)) ++loaded; + } + log->info("Plugin load complete: {}/{} succeeded", loaded, pluginNames.size()); + return loaded; + } + + bool PluginManager::LoadOne(const std::string &modulesDir, const std::string &name) { + auto log = Framework::Logging::GetLogger(kLoggerName); + + const std::filesystem::path baseDir = std::filesystem::path(modulesDir) / name; + const std::filesystem::path manifestPath = baseDir / (name + ".module.json"); + + if (!std::filesystem::exists(manifestPath)) { + log->error("Plugin '{}' missing manifest at {}", name, manifestPath.string()); + return false; + } + + nlohmann::json manifest; + try { + std::ifstream in(manifestPath); + std::stringstream buf; + buf << in.rdbuf(); + manifest = nlohmann::json::parse(buf.str()); + } + catch (const std::exception &e) { + log->error("Plugin '{}' manifest parse error: {}", name, e.what()); + return false; + } + + const std::string declaredName = manifest.value("name", ""); + if (declaredName != name) { + log->error("Plugin '{}' manifest name mismatch (got '{}')", name, declaredName); + return false; + } + + const uint32_t manifestAbi = manifest.value("abi_version", 0u); + if (manifestAbi != FW_PLUGIN_ABI_VERSION) { + log->error("Plugin '{}' manifest abi_version {} != host {}", name, manifestAbi, FW_PLUGIN_ABI_VERSION); + return false; + } + + const std::string entry = manifest.value("entry", name); + const auto libPath = baseDir / SharedLibrary::PlatformFilename(entry); + + auto plugin = std::make_unique(); + plugin->name = name; + plugin->version = manifest.value("version", ""); + + if (!plugin->library.Open(libPath.string())) { + log->error("Plugin '{}' dlopen failed for {}: {}", name, libPath.string(), plugin->library.GetLastError()); + return false; + } + + plugin->infoFn = reinterpret_cast(plugin->library.Symbol("fw_plugin_info")); + plugin->initFn = reinterpret_cast(plugin->library.Symbol("fw_plugin_init")); + plugin->shutdownFn = reinterpret_cast(plugin->library.Symbol("fw_plugin_shutdown")); + plugin->postScriptInitFn = reinterpret_cast(plugin->library.Symbol("fw_plugin_post_script_init")); + plugin->updateFn = reinterpret_cast(plugin->library.Symbol("fw_plugin_update")); + plugin->preShutdownFn = reinterpret_cast(plugin->library.Symbol("fw_plugin_pre_shutdown")); + + if (!plugin->infoFn || !plugin->initFn || !plugin->shutdownFn) { + log->error("Plugin '{}' missing required exports (fw_plugin_info / fw_plugin_init / fw_plugin_shutdown)", name); + return false; + } + + const FwPluginInfo *info = nullptr; + try { + info = plugin->infoFn(); + } + catch (...) { + log->error("Plugin '{}' fw_plugin_info threw", name); + return false; + } + if (!info || info->abi_version != FW_PLUGIN_ABI_VERSION) { + log->error("Plugin '{}' reported abi_version {} != host {}", name, info ? info->abi_version : 0u, FW_PLUGIN_ABI_VERSION); + return false; + } + if (info->name && declaredName != info->name) { + log->warn("Plugin '{}' info->name='{}' disagrees with manifest", name, info->name); + } + if (info->version && plugin->version != info->version) { + log->warn("Plugin '{}' info->version='{}' disagrees with manifest '{}'", name, info->version, plugin->version); + } + + plugin->impl = std::make_unique(); + plugin->impl->pluginName = name; + plugin->impl->webserver = _webserver; + plugin->impl->commandProcessor = _commandProcessor; + plugin->impl->worldEngine = _worldEngine; + plugin->impl->logger = Framework::Logging::GetLogger((std::string("plugin:") + name).c_str()); + plugin->host = MakeFwHost(plugin->impl.get()); + + int initRc = 0; + try { + initRc = plugin->initFn(&plugin->host); + } + catch (const std::exception &e) { + log->error("Plugin '{}' fw_plugin_init threw: {}", name, e.what()); + return false; + } + catch (...) { + log->error("Plugin '{}' fw_plugin_init threw non-std exception", name); + return false; + } + if (initRc != 0) { + log->error("Plugin '{}' fw_plugin_init returned {}", name, initRc); + return false; + } + + /* Informational: warn about unsatisfied dependencies. v1 does not + * reorder loads, but flags missing deps so authors notice. */ + if (manifest.contains("depends_on") && manifest["depends_on"].is_array()) { + for (const auto &dep : manifest["depends_on"]) { + const std::string depName = dep.value("name", ""); + bool found = false; + for (const auto &p : _plugins) { + if (p->name == depName) { + found = true; + break; + } + } + if (!found) { + log->warn("Plugin '{}' declares dependency on '{}' which is not loaded (yet)", name, depName); + } + } + } + + plugin->initSucceeded = true; + log->info("Loaded plugin '{}' v{}", name, plugin->version); + _plugins.push_back(std::move(plugin)); + return true; + } + + /* ----------------------------------------------------------------------- */ + /* Lifecycle dispatch */ + /* ----------------------------------------------------------------------- */ + + void PluginManager::PostScriptInit() { + for (auto &p : _plugins) { + if (!p->initSucceeded || !p->postScriptInitFn) continue; + try { + p->postScriptInitFn(&p->host); + } + catch (const std::exception &e) { + Framework::Logging::GetLogger(kLoggerName)->error("Plugin '{}' fw_plugin_post_script_init threw: {}", p->name, e.what()); + } + catch (...) { + Framework::Logging::GetLogger(kLoggerName)->error("Plugin '{}' fw_plugin_post_script_init threw non-std exception", p->name); + } + } + } + + void PluginManager::Update(double dtSeconds) { + for (auto &p : _plugins) { + if (!p->initSucceeded || !p->updateFn) continue; + try { + p->updateFn(&p->host, dtSeconds); + } + catch (const std::exception &e) { + Framework::Logging::GetLogger(kLoggerName)->error("Plugin '{}' fw_plugin_update threw: {}", p->name, e.what()); + p->initSucceeded = false; /* disable to stop the spam */ + } + catch (...) { + Framework::Logging::GetLogger(kLoggerName)->error("Plugin '{}' fw_plugin_update threw non-std exception", p->name); + p->initSucceeded = false; + } + } + } + + void PluginManager::PreShutdown() { + for (auto &p : _plugins) { + if (!p->initSucceeded || !p->preShutdownFn) continue; + try { + p->preShutdownFn(&p->host); + } + catch (const std::exception &e) { + Framework::Logging::GetLogger(kLoggerName)->error("Plugin '{}' fw_plugin_pre_shutdown threw: {}", p->name, e.what()); + } + catch (...) { + Framework::Logging::GetLogger(kLoggerName)->error("Plugin '{}' fw_plugin_pre_shutdown threw non-std exception", p->name); + } + } + } + + void PluginManager::Shutdown() { + if (_shutdownCalled) return; + _shutdownCalled = true; + + /* Reverse order: last-loaded plugin shuts down first, so a plugin + * built on top of another's services can clean up before that + * service goes away. */ + for (auto it = _plugins.rbegin(); it != _plugins.rend(); ++it) { + auto &p = *it; + if (p->initSucceeded && p->shutdownFn) { + try { + p->shutdownFn(&p->host); + } + catch (const std::exception &e) { + Framework::Logging::GetLogger(kLoggerName)->error("Plugin '{}' fw_plugin_shutdown threw: {}", p->name, e.what()); + } + catch (...) { + Framework::Logging::GetLogger(kLoggerName)->error("Plugin '{}' fw_plugin_shutdown threw non-std exception", p->name); + } + } + /* Tear down host side bindings (commands etc.) before the + * library is unmapped, so trampoline pointers can't be invoked + * against freed code. */ + if (p->impl && _commandProcessor) { + for (const auto &cmd : p->impl->registeredCommands) { + _commandProcessor->RemoveCommand(cmd); + } + } + } + + _plugins.clear(); + } + + void PluginManager::DispatchPlayerConnect(uint64_t entityId, uint64_t guid) { + for (auto &p : _plugins) { + if (!p->initSucceeded) continue; + Framework::Integrations::Server::Plugins::DispatchPlayerConnect(p->impl.get(), entityId, guid); + } + } + + void PluginManager::DispatchPlayerDisconnect(uint64_t entityId, uint64_t guid) { + for (auto &p : _plugins) { + if (!p->initSucceeded) continue; + Framework::Integrations::Server::Plugins::DispatchPlayerDisconnect(p->impl.get(), entityId, guid); + } + } + +} // namespace Framework::Integrations::Server::Plugins diff --git a/code/framework/src/integrations/server/plugins/manager.h b/code/framework/src/integrations/server/plugins/manager.h new file mode 100644 index 000000000..f6f6bd8c2 --- /dev/null +++ b/code/framework/src/integrations/server/plugins/manager.h @@ -0,0 +1,113 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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 "host_impl.h" +#include "loader.h" +#include "sdk/fw_plugin_abi.h" + +#include +#include +#include + +namespace Framework { + namespace HTTP { + class Webserver; + } + namespace Utils { + class CommandProcessor; + } + namespace World { + class ServerEngine; + } +} // namespace Framework + +namespace Framework::Integrations::Server::Plugins { + + /* + * Resolved plugin entry points, kept alongside the loaded library so + * the function pointers stay valid for the plugin's lifetime. + */ + struct LoadedPlugin { + std::string name; + std::string version; + SharedLibrary library; + std::unique_ptr impl; + FwHost host {}; + + const FwPluginInfo *(*infoFn)(void) = nullptr; + int (*initFn)(FwHost *) = nullptr; + void (*postScriptInitFn)(FwHost *) = nullptr; + void (*updateFn)(FwHost *, double) = nullptr; + void (*preShutdownFn)(FwHost *) = nullptr; + void (*shutdownFn)(FwHost *) = nullptr; + + bool initSucceeded = false; + }; + + /* + * Loads, ticks, and unloads native plugins listed in server.json. + * + * Lifecycle (called from Server::Instance): + * 1. Init(webserver, commands, world) — wire subsystem pointers + * 2. LoadAll(modulesDir, names) — find manifests, dlopen, + * validate ABI, run fw_plugin_init + * 3. PostScriptInit() — after scripting engine is up + * 4. Update(dt) every tick — fw_plugin_update on each + * 5. PreShutdown() — fw_plugin_pre_shutdown + * 6. Shutdown() — fw_plugin_shutdown + unload + * + * The manager guarantees that exceptions thrown out of any plugin entry + * point are caught and logged. A plugin that fails to load or that + * throws during a lifecycle hook is disabled, not propagated. + */ + class PluginManager final { + public: + PluginManager(); + ~PluginManager(); + + void Init(Framework::HTTP::Webserver *webserver, Framework::Utils::CommandProcessor *commandProcessor, Framework::World::ServerEngine *worldEngine); + + /* + * Load each plugin in `pluginNames`, in order. For each name, looks + * up //.module.json. Plugins that fail to + * load are logged and skipped; load failures are not fatal. + * Returns the number of plugins successfully loaded. + */ + size_t LoadAll(const std::string &modulesDir, const std::vector &pluginNames); + + void PostScriptInit(); + void Update(double dtSeconds); + void PreShutdown(); + void Shutdown(); + + /* + * Dispatch points wired from Instance's existing player connect / + * disconnect callbacks. entityId is the flecs entity id; guid is + * the network GUID. + */ + void DispatchPlayerConnect(uint64_t entityId, uint64_t guid); + void DispatchPlayerDisconnect(uint64_t entityId, uint64_t guid); + + size_t Count() const { + return _plugins.size(); + } + + private: + bool LoadOne(const std::string &modulesDir, const std::string &name); + + Framework::HTTP::Webserver *_webserver = nullptr; + Framework::Utils::CommandProcessor *_commandProcessor = nullptr; + Framework::World::ServerEngine *_worldEngine = nullptr; + + std::vector> _plugins; + bool _shutdownCalled = false; + }; + +} // namespace Framework::Integrations::Server::Plugins diff --git a/code/framework/src/integrations/server/plugins/sample/hello-plugin/CMakeLists.txt b/code/framework/src/integrations/server/plugins/sample/hello-plugin/CMakeLists.txt new file mode 100644 index 000000000..d74738628 --- /dev/null +++ b/code/framework/src/integrations/server/plugins/sample/hello-plugin/CMakeLists.txt @@ -0,0 +1,36 @@ +# Hello-plugin: standalone sample. Builds against the Framework Plugin SDK +# headers only — no link dependency on the Framework itself. The output +# .dll/.so is loaded at runtime by the Framework server's plugin loader. +# +# Standalone build: +# cmake -S . -B build +# cmake --build build +# +# The result lives in build/ as hello-plugin.dll (Windows) or +# libhello-plugin.so (Linux/macOS). Drop it next to its manifest in the +# server's modules directory. + +cmake_minimum_required(VERSION 3.20) +project(hello-plugin CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +add_library(hello-plugin SHARED plugin.cpp) + +# Resolve the SDK headers. When built from the in-tree location, the path +# is relative; when extracted as a template for an out-of-tree plugin, +# override with -DFW_PLUGIN_SDK_INCLUDE_DIR=... +set(FW_PLUGIN_SDK_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../sdk" + CACHE PATH "Path to fw_plugin_abi.h and fw_plugin.hpp") +target_include_directories(hello-plugin PRIVATE "${FW_PLUGIN_SDK_INCLUDE_DIR}") + +# Strip the lib prefix on Unix so the on-disk name matches the manifest +# "entry" field across platforms. +set_target_properties(hello-plugin PROPERTIES + PREFIX "" + OUTPUT_NAME "hello-plugin" +) diff --git a/code/framework/src/integrations/server/plugins/sample/hello-plugin/hello-plugin.module.json b/code/framework/src/integrations/server/plugins/sample/hello-plugin/hello-plugin.module.json new file mode 100644 index 000000000..dae404827 --- /dev/null +++ b/code/framework/src/integrations/server/plugins/sample/hello-plugin/hello-plugin.module.json @@ -0,0 +1,8 @@ +{ + "name": "hello-plugin", + "version": "0.1.0", + "abi_version": 1, + "entry": "hello-plugin", + "depends_on": [], + "capabilities": ["commands", "http_endpoints", "player_events"] +} diff --git a/code/framework/src/integrations/server/plugins/sample/hello-plugin/plugin.cpp b/code/framework/src/integrations/server/plugins/sample/hello-plugin/plugin.cpp new file mode 100644 index 000000000..95081c8d4 --- /dev/null +++ b/code/framework/src/integrations/server/plugins/sample/hello-plugin/plugin.cpp @@ -0,0 +1,56 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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. + */ + +/* + * Hello-plugin: a minimal native plugin for the Framework server. + * Exercises every v1 host capability (logger, commands, HTTP endpoint, + * player connect/disconnect) and nothing else. + */ + +#include "fw_plugin.hpp" + +#include + +class HelloPlugin final : public Framework::Plugin::Base { + public: + int OnInit(Framework::Plugin::Host &host) override { + _log = host.LoggerFor("hello-plugin"); + _log.Info("hello-plugin loaded"); + + host.RegisterCommand("hello", "Print a greeting", [this](int argc, const char *const *argv) { + std::string who = argc > 1 ? argv[1] : "world"; + _log.Info("hello, " + who); + }); + + host.RegisterHttpEndpoint("/hello", [this](std::string_view /*method*/, std::string_view /*path*/, std::string_view /*body*/, Framework::Plugin::HttpResponse &response) { + response.SetStatus(200); + response.SetHeader("Content-Type", "text/plain"); + response.SetBody("hello from the plugin\n"); + _log.Debug("served /hello"); + }); + + host.OnPlayerConnect([this](Framework::Plugin::Player &player) { + _log.Info(player.Nickname() + " (guid=" + std::to_string(player.Guid()) + ") connected"); + }); + + host.OnPlayerDisconnect([this](Framework::Plugin::Player &player) { + _log.Info(player.Nickname() + " (guid=" + std::to_string(player.Guid()) + ") disconnected"); + }); + + return 0; + } + + void OnShutdown(Framework::Plugin::Host & /*host*/) override { + _log.Info("hello-plugin shutting down"); + } + + private: + Framework::Plugin::Logger _log; +}; + +FW_PLUGIN_DECLARE(HelloPlugin, "hello-plugin", "0.1.0") diff --git a/code/framework/src/integrations/server/plugins/sdk/fw_plugin.hpp b/code/framework/src/integrations/server/plugins/sdk/fw_plugin.hpp new file mode 100644 index 000000000..8be1a6100 --- /dev/null +++ b/code/framework/src/integrations/server/plugins/sdk/fw_plugin.hpp @@ -0,0 +1,306 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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. + */ + +/* + * Framework Server Plugin SDK — C++ wrapper + * =========================================================================== + * + * Header-only C++17 ergonomics layered on top of fw_plugin_abi.h. Plugin + * authors get type-safe lambdas, RAII handles, and a single macro to wire + * up the required C exports. + * + * Minimal plugin: + * + * #include "fw_plugin.hpp" + * + * class MyPlugin : public Framework::Plugin::Base { + * public: + * int OnInit(Framework::Plugin::Host &host) override { + * auto log = host.LoggerFor("my-plugin"); + * log.Info("hello, framework"); + * + * host.RegisterCommand("greet", "Say hi to a player", + * [log](int argc, const char *const *argv) { + * log.Info(argc > 1 ? argv[1] : "anonymous"); + * }); + * + * host.OnPlayerConnect([log](Framework::Plugin::Player &p) { + * log.Info(p.Nickname() + " joined"); + * }); + * return 0; + * } + * }; + * + * FW_PLUGIN_DECLARE(MyPlugin, "my-plugin", "1.0.0") + * + * Callback storage: lambdas are heap-allocated and owned by the Host wrapper + * for the lifetime of the plugin. There is no Unregister API in v1 — all + * registrations live for the plugin's lifetime and are torn down on unload. + * =========================================================================== + */ + +#pragma once + +#include "fw_plugin_abi.h" + +#include +#include +#include +#include +#include + +namespace Framework::Plugin { + + /* --------------------------------------------------------------------- */ + /* Logger */ + /* --------------------------------------------------------------------- */ + + class Logger final { + public: + Logger() = default; + Logger(const FwHostVTable *vtable, FwLogger *logger): _vtable(vtable), _logger(logger) {} + + void Debug(const std::string &msg) const { + if (_vtable && _logger) _vtable->log_debug(_logger, msg.c_str()); + } + void Info(const std::string &msg) const { + if (_vtable && _logger) _vtable->log_info(_logger, msg.c_str()); + } + void Warn(const std::string &msg) const { + if (_vtable && _logger) _vtable->log_warn(_logger, msg.c_str()); + } + void Error(const std::string &msg) const { + if (_vtable && _logger) _vtable->log_error(_logger, msg.c_str()); + } + + private: + const FwHostVTable *_vtable = nullptr; + FwLogger *_logger = nullptr; + }; + + /* --------------------------------------------------------------------- */ + /* Player */ + /* --------------------------------------------------------------------- */ + + class Player final { + public: + Player(const FwHostVTable *vtable, FwPlayer *player, uint64_t guid): _vtable(vtable), _player(player), _guid(guid) {} + + uint64_t Guid() const { + return _guid; + } + + std::string Nickname() const { + if (!_vtable || !_player) return {}; + char stackBuf[128]; + size_t needed = _vtable->player_get_nickname(_player, stackBuf, sizeof(stackBuf)); + if (needed < sizeof(stackBuf)) { + return std::string(stackBuf, needed); + } + std::string heap(needed, '\0'); + _vtable->player_get_nickname(_player, heap.data(), heap.size() + 1); + return heap; + } + + private: + const FwHostVTable *_vtable = nullptr; + FwPlayer *_player = nullptr; + uint64_t _guid = 0; + }; + + /* --------------------------------------------------------------------- */ + /* HTTP response builder */ + /* --------------------------------------------------------------------- */ + + class HttpResponse final { + public: + HttpResponse(const FwHostVTable *vtable, FwHttpResponse *response): _vtable(vtable), _response(response) {} + + void SetStatus(int status) const { + _vtable->http_response_set_status(_response, status); + } + void SetBody(std::string_view body) const { + _vtable->http_response_set_body(_response, body.data(), body.size()); + } + void SetHeader(const std::string &key, const std::string &value) const { + _vtable->http_response_set_header(_response, key.c_str(), value.c_str()); + } + + private: + const FwHostVTable *_vtable; + FwHttpResponse *_response; + }; + + /* --------------------------------------------------------------------- */ + /* Host wrapper */ + /* --------------------------------------------------------------------- */ + + using CommandFn = std::function; + using HttpFn = std::function; + using PlayerEventFn = std::function; + + class Host final { + public: + explicit Host(FwHost *host): _host(host), _vtable(host ? host->vtable : nullptr) {} + + bool Valid() const { + return _host && _vtable && _vtable->abi_version == FW_PLUGIN_ABI_VERSION; + } + + Logger LoggerFor(const std::string &pluginName) const { + return Logger(_vtable, _vtable->logger_for(_host, pluginName.c_str())); + } + + bool RegisterCommand(const std::string &name, const std::string &description, CommandFn callback) { + auto slot = std::make_unique(std::move(callback)); + int rc = _vtable->register_command( + _host, name.c_str(), description.c_str(), + [](int argc, const char *const *argv, void *userdata) { + (*static_cast(userdata))(argc, argv); + }, + slot.get()); + if (rc == 0) { + _commandSlots.push_back(std::move(slot)); + return true; + } + return false; + } + + bool RegisterHttpEndpoint(const std::string &path, HttpFn callback) { + auto slot = std::make_unique(std::move(callback)); + auto binding = std::make_unique(); + binding->vtable = _vtable; + binding->fn = slot.get(); + int rc = _vtable->register_http_endpoint( + _host, path.c_str(), + [](const char *method, const char *path_c, const char *body, size_t body_len, FwHttpResponse *response, void *userdata) { + auto *b = reinterpret_cast(userdata); + HttpResponse wrappedResponse(b->vtable, response); + (*b->fn)(method, path_c, std::string_view(body, body_len), wrappedResponse); + }, + binding.get()); + if (rc == 0) { + _httpSlots.push_back(std::move(slot)); + _httpBindings.push_back(std::move(binding)); + return true; + } + /* slot + binding destroyed on this return; the host did not + * store the userdata pointer on failure. */ + return false; + } + + bool OnPlayerConnect(PlayerEventFn callback) { + return RegisterPlayerEvent(_vtable->on_player_connect, std::move(callback)); + } + bool OnPlayerDisconnect(PlayerEventFn callback) { + return RegisterPlayerEvent(_vtable->on_player_disconnect, std::move(callback)); + } + + private: + /* + * The HTTP trampoline needs both the std::function* and the vtable* in + * its userdata. We bundle them in a tiny owned struct. + */ + struct HostBindings { + const FwHostVTable *vtable; + HttpFn *fn; + }; + std::vector> _httpBindings; + + bool RegisterPlayerEvent(int (*registerFn)(FwHost *, FwPlayerEventCallback, void *), PlayerEventFn callback) { + auto slot = std::make_unique(std::move(callback)); + auto binding = std::make_unique(); + binding->vtable = _vtable; + binding->fn = slot.get(); + int rc = registerFn( + _host, + [](FwPlayer *player, uint64_t guid, void *userdata) { + auto *b = static_cast(userdata); + Player wrappedPlayer(b->vtable, player, guid); + (*b->fn)(wrappedPlayer); + }, + binding.get()); + if (rc == 0) { + _playerEventSlots.push_back(std::move(slot)); + _playerEventBindings.push_back(std::move(binding)); + return true; + } + return false; + } + + struct PlayerEventBinding { + const FwHostVTable *vtable; + PlayerEventFn *fn; + }; + + FwHost *_host; + const FwHostVTable *_vtable; + std::vector> _commandSlots; + std::vector> _httpSlots; + std::vector> _playerEventSlots; + std::vector> _playerEventBindings; + }; + + /* --------------------------------------------------------------------- */ + /* Plugin base class */ + /* --------------------------------------------------------------------- */ + + class Base { + public: + virtual ~Base() = default; + + /* Return 0 on success, nonzero to abort plugin load. */ + virtual int OnInit(Host &host) = 0; + + virtual void OnPostScriptInit(Host & /*host*/) {} + virtual void OnUpdate(Host & /*host*/, double /*dt_seconds*/) {} + virtual void OnPreShutdown(Host & /*host*/) {} + virtual void OnShutdown(Host & /*host*/) {} + }; + +} // namespace Framework::Plugin + +/* --------------------------------------------------------------------------- */ +/* Required-export glue macro */ +/* --------------------------------------------------------------------------- */ + +/* + * Expand once per plugin, at file scope outside any namespace. Generates the + * required C exports and wires them to a singleton instance of PluginClass. + * + * FW_PLUGIN_DECLARE(MyPlugin, "my-plugin", "1.2.3") + */ +#define FW_PLUGIN_DECLARE(PluginClass, PluginName, PluginVersion) \ + namespace { \ + PluginClass g_fw_plugin_instance; \ + std::unique_ptr g_fw_plugin_host; \ + const FwPluginInfo g_fw_plugin_info_value = {PluginName, PluginVersion, FW_PLUGIN_ABI_VERSION}; \ + } \ + extern "C" FW_PLUGIN_EXPORT const FwPluginInfo *fw_plugin_info(void) { \ + return &g_fw_plugin_info_value; \ + } \ + extern "C" FW_PLUGIN_EXPORT int fw_plugin_init(FwHost *host) { \ + g_fw_plugin_host = std::make_unique(host); \ + if (!g_fw_plugin_host->Valid()) return 1; \ + return g_fw_plugin_instance.OnInit(*g_fw_plugin_host); \ + } \ + extern "C" FW_PLUGIN_EXPORT void fw_plugin_post_script_init(FwHost * /*host*/) { \ + if (g_fw_plugin_host) g_fw_plugin_instance.OnPostScriptInit(*g_fw_plugin_host); \ + } \ + extern "C" FW_PLUGIN_EXPORT void fw_plugin_update(FwHost * /*host*/, double dt) { \ + if (g_fw_plugin_host) g_fw_plugin_instance.OnUpdate(*g_fw_plugin_host, dt); \ + } \ + extern "C" FW_PLUGIN_EXPORT void fw_plugin_pre_shutdown(FwHost * /*host*/) { \ + if (g_fw_plugin_host) g_fw_plugin_instance.OnPreShutdown(*g_fw_plugin_host); \ + } \ + extern "C" FW_PLUGIN_EXPORT void fw_plugin_shutdown(FwHost * /*host*/) { \ + if (g_fw_plugin_host) { \ + g_fw_plugin_instance.OnShutdown(*g_fw_plugin_host); \ + g_fw_plugin_host.reset(); \ + } \ + } diff --git a/code/framework/src/integrations/server/plugins/sdk/fw_plugin_abi.h b/code/framework/src/integrations/server/plugins/sdk/fw_plugin_abi.h new file mode 100644 index 000000000..bc169d71a --- /dev/null +++ b/code/framework/src/integrations/server/plugins/sdk/fw_plugin_abi.h @@ -0,0 +1,244 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2026, 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. + */ + +/* + * Framework Server Plugin SDK — C ABI (v1) + * =========================================================================== + * + * This header defines the binary contract between the Framework server and + * a native plugin shared library (.dll / .so) loaded at server startup. + * + * It is pure C and self-contained. Plugins may be written in C or C++. + * C++ plugin authors are encouraged to use the higher-level wrapper in + * fw_plugin.hpp, which sits entirely on top of this ABI. + * + * --------------------------------------------------------------------------- + * Manifest + * --------------------------------------------------------------------------- + * + * Each plugin ships alongside a JSON manifest (.module.json) declaring: + * + * { + * "name": "anticheat-vanilla", + * "version": "1.2.0", + * "abi_version": 1, + * "entry": "anticheat-vanilla", // platform suffix appended + * "depends_on": [ + * { "name": "telemetry-core", "version": ">=0.3.0" } + * ], + * "capabilities": ["http_endpoints", "commands"] + * } + * + * The server's `server.json` enumerates which plugins to load: + * + * { "modules": ["anticheat-vanilla", "telemetry-core"] } + * + * There is no filesystem auto-discovery. Plugins not listed are ignored. + * + * --------------------------------------------------------------------------- + * Required exports + * --------------------------------------------------------------------------- + * + * FW_PLUGIN_EXPORT const FwPluginInfo* fw_plugin_info(void); + * FW_PLUGIN_EXPORT int fw_plugin_init(FwHost* host); + * FW_PLUGIN_EXPORT void fw_plugin_shutdown(FwHost* host); + * + * Optional exports (host uses dlsym and treats missing as no-op): + * + * FW_PLUGIN_EXPORT void fw_plugin_post_script_init(FwHost* host); + * FW_PLUGIN_EXPORT void fw_plugin_update(FwHost* host, double dt_seconds); + * FW_PLUGIN_EXPORT void fw_plugin_pre_shutdown(FwHost* host); + * + * --------------------------------------------------------------------------- + * Lifecycle (mirrors Framework::Integrations::Server::Instance hooks) + * --------------------------------------------------------------------------- + * + * load host validates manifest + abi_version, dlopens the .so + * fw_plugin_info host reads name/version, verifies abi_version + * fw_plugin_init plugin registers commands/endpoints/callbacks via host + * fw_plugin_post_script_init plugin may register scripting builtins + * fw_plugin_update (each tick) plugin runs per-tick work, if any + * fw_plugin_pre_shutdown plugin flushes state, closes resources + * fw_plugin_shutdown plugin releases everything; host dlcloses + * + * Every host call into the plugin is wrapped in an exception fence by the + * loader. A throwing/crashing plugin is logged and disabled, not propagated. + * + * --------------------------------------------------------------------------- + * Memory ownership + * --------------------------------------------------------------------------- + * + * - All strings are null-terminated UTF-8 unless explicitly paired with a + * length argument. + * - Pointers handed from host to plugin are valid only for the duration of + * the call that received them, with the following carve-outs: + * * FwLogger* returned by logger_for() is valid for the plugin's + * lifetime (until fw_plugin_shutdown returns). + * * FwHost* itself is valid for the plugin's lifetime. + * FwPlayer* and FwHttpResponse* are strictly call-scoped and must not + * be stored for later use. + * - The plugin never returns heap memory to the host. To produce output + * (e.g. HTTP response bodies), the plugin writes through host-provided + * setter functions that own the allocation. + * + * This sidesteps allocator-mismatch UB across CRT boundaries. + * + * --------------------------------------------------------------------------- + * ABI compatibility + * --------------------------------------------------------------------------- + * + * - FW_PLUGIN_ABI_VERSION is bumped on any breaking change to the structs + * or callback signatures below. Plugin's FwPluginInfo.abi_version must + * match exactly. The host refuses to load otherwise. + * - The FwHostVTable is append-only across patch versions. New function + * pointers go at the end; older plugins ignore them. Removing or + * reordering vtable entries requires bumping FW_PLUGIN_ABI_VERSION. + * =========================================================================== + */ + +#ifndef FW_PLUGIN_ABI_H +#define FW_PLUGIN_ABI_H + +#include +#include + +#define FW_PLUGIN_ABI_VERSION 1 + +#if defined(_WIN32) +# define FW_PLUGIN_EXPORT __declspec(dllexport) +#else +# define FW_PLUGIN_EXPORT __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------------------------------------------------------- */ +/* Opaque handle types */ +/* ------------------------------------------------------------------------- */ + +typedef struct FwHost FwHost; +typedef struct FwLogger FwLogger; +typedef struct FwPlayer FwPlayer; +typedef struct FwHttpResponse FwHttpResponse; + +/* ------------------------------------------------------------------------- */ +/* Plugin descriptor */ +/* ------------------------------------------------------------------------- */ + +typedef struct FwPluginInfo { + const char *name; /* matches manifest "name", e.g. "anticheat-vanilla" */ + const char *version; /* semver string, matches manifest "version" */ + uint32_t abi_version; /* must equal FW_PLUGIN_ABI_VERSION */ +} FwPluginInfo; + +/* ------------------------------------------------------------------------- */ +/* Callback signatures (host → plugin) */ +/* ------------------------------------------------------------------------- */ + +/* + * Command invocation. argv has argc entries, all null-terminated UTF-8. + * Strings remain valid only for the duration of the call. + */ +typedef void (*FwCommandCallback)(int argc, const char *const *argv, void *userdata); + +/* + * HTTP request handler. method/path/body strings remain valid for the call. + * The plugin writes the reply through response_set_* host functions. + */ +typedef void (*FwHttpCallback)(const char *method, + const char *path, + const char *body, + size_t body_len, + FwHttpResponse *response, + void *userdata); + +/* + * Player event. The FwPlayer pointer is valid for the duration of the call. + * The guid is also passed inline for convenience and lifetime-safe storage. + */ +typedef void (*FwPlayerEventCallback)(FwPlayer *player, uint64_t guid, void *userdata); + +/* ------------------------------------------------------------------------- */ +/* Host vtable */ +/* ------------------------------------------------------------------------- */ + +/* + * Append-only across patch versions. Adding new function pointers at the + * end is non-breaking for older plugins (which simply don't call them). + * Any reordering or removal requires bumping FW_PLUGIN_ABI_VERSION. + */ +typedef struct FwHostVTable { + uint32_t abi_version; /* set to FW_PLUGIN_ABI_VERSION by the host */ + + /* Logging --------------------------------------------------------------- */ + + /* Returns a logger scoped to the plugin name. Owned by host; do not free. */ + FwLogger *(*logger_for)(FwHost *host, const char *plugin_name); + + void (*log_debug)(FwLogger *logger, const char *message); + void (*log_info)(FwLogger *logger, const char *message); + void (*log_warn)(FwLogger *logger, const char *message); + void (*log_error)(FwLogger *logger, const char *message); + + /* Commands -------------------------------------------------------------- */ + + /* + * Registers a console command. Returns 0 on success, nonzero on failure + * (e.g. name already taken). Registration persists for plugin lifetime; + * commands are removed automatically when the plugin unloads. + */ + int (*register_command)(FwHost *host, const char *name, const char *description, FwCommandCallback callback, void *userdata); + + /* HTTP endpoints -------------------------------------------------------- */ + + /* + * Registers an HTTP endpoint on the webserver. Returns 0 on success. + * Path collisions with the Framework's built-in endpoints (e.g. "/") + * are rejected. + */ + int (*register_http_endpoint)(FwHost *host, const char *path, FwHttpCallback callback, void *userdata); + + void (*http_response_set_status)(FwHttpResponse *response, int status); + void (*http_response_set_body)(FwHttpResponse *response, const char *body, size_t body_len); + void (*http_response_set_header)(FwHttpResponse *response, const char *key, const char *value); + + /* Player events --------------------------------------------------------- */ + + int (*on_player_connect)(FwHost *host, FwPlayerEventCallback callback, void *userdata); + int (*on_player_disconnect)(FwHost *host, FwPlayerEventCallback callback, void *userdata); + + /* Player accessors ------------------------------------------------------ */ + + uint64_t (*player_get_guid)(FwPlayer *player); + + /* + * Copies the player's nickname into buf (null-terminated, truncated to + * fit). Returns the full length in bytes (excluding null terminator), + * so caller can detect truncation when (return >= buf_size). + */ + size_t (*player_get_nickname)(FwPlayer *player, char *buf, size_t buf_size); + + /* --- new entries appended below in future patch versions --- */ +} FwHostVTable; + +/* ------------------------------------------------------------------------- */ +/* Host instance handed to every plugin entry point */ +/* ------------------------------------------------------------------------- */ + +struct FwHost { + const FwHostVTable *vtable; + void *internal; /* opaque host implementation; plugin must not touch */ +}; + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* FW_PLUGIN_ABI_H */ diff --git a/vendors/lua-5.4.4/CMakeLists.txt b/vendors/lua-5.4.4/CMakeLists.txt index 0b5932a3a..d7e855761 100644 --- a/vendors/lua-5.4.4/CMakeLists.txt +++ b/vendors/lua-5.4.4/CMakeLists.txt @@ -63,3 +63,9 @@ set_target_properties(lua54_shared PROPERTIES OUTPUT_NAME "lua") # Executable add_executable(lua54_exe ${LUA_FILES}) set_target_properties(lua54_exe PROPERTIES OUTPUT_NAME "lua54") + +if(UNIX AND NOT APPLE) + target_link_libraries(lua54_exe PRIVATE m dl) + target_link_libraries(lua54_static PRIVATE m dl) + target_link_libraries(lua54_shared PRIVATE m dl) +endif()