From f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287 Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Wed, 27 Dec 2023 00:32:01 +0000 Subject: Folder restructuring from primedev (#624) Copies of over the primedev folder structure for easier cherry-picking of further changes Co-authored-by: F1F7Y --- primedev/plugins/plugin_abi.h | 151 ++++++++++++++++ primedev/plugins/pluginbackend.cpp | 50 ++++++ primedev/plugins/pluginbackend.h | 40 +++++ primedev/plugins/plugins.cpp | 340 +++++++++++++++++++++++++++++++++++++ primedev/plugins/plugins.h | 59 +++++++ 5 files changed, 640 insertions(+) create mode 100644 primedev/plugins/plugin_abi.h create mode 100644 primedev/plugins/pluginbackend.cpp create mode 100644 primedev/plugins/pluginbackend.h create mode 100644 primedev/plugins/plugins.cpp create mode 100644 primedev/plugins/plugins.h (limited to 'primedev/plugins') diff --git a/primedev/plugins/plugin_abi.h b/primedev/plugins/plugin_abi.h new file mode 100644 index 00000000..16b26a1c --- /dev/null +++ b/primedev/plugins/plugin_abi.h @@ -0,0 +1,151 @@ +#pragma once +#include "squirrel/squirrelclasstypes.h" + +#define ABI_VERSION 3 + +enum PluginLoadDLL +{ + ENGINE = 0, + CLIENT, + SERVER +}; + +enum ObjectType +{ + CONCOMMANDS = 0, + CONVAR = 1, +}; + +struct SquirrelFunctions +{ + RegisterSquirrelFuncType RegisterSquirrelFunc; + sq_defconstType __sq_defconst; + + sq_compilebufferType __sq_compilebuffer; + sq_callType __sq_call; + sq_raiseerrorType __sq_raiseerror; + sq_compilefileType __sq_compilefile; + + sq_newarrayType __sq_newarray; + sq_arrayappendType __sq_arrayappend; + + sq_newtableType __sq_newtable; + sq_newslotType __sq_newslot; + + sq_pushroottableType __sq_pushroottable; + sq_pushstringType __sq_pushstring; + sq_pushintegerType __sq_pushinteger; + sq_pushfloatType __sq_pushfloat; + sq_pushboolType __sq_pushbool; + sq_pushassetType __sq_pushasset; + sq_pushvectorType __sq_pushvector; + sq_pushobjectType __sq_pushobject; + + sq_getstringType __sq_getstring; + sq_getintegerType __sq_getinteger; + sq_getfloatType __sq_getfloat; + sq_getboolType __sq_getbool; + sq_getType __sq_get; + sq_getassetType __sq_getasset; + sq_getuserdataType __sq_getuserdata; + sq_getvectorType __sq_getvector; + sq_getthisentityType __sq_getthisentity; + sq_getobjectType __sq_getobject; + + sq_stackinfosType __sq_stackinfos; + + sq_createuserdataType __sq_createuserdata; + sq_setuserdatatypeidType __sq_setuserdatatypeid; + sq_getfunctionType __sq_getfunction; + + sq_schedule_call_externalType __sq_schedule_call_external; + + sq_getentityfrominstanceType __sq_getentityfrominstance; + sq_GetEntityConstantType __sq_GetEntityConstant_CBaseEntity; + + sq_pushnewstructinstanceType __sq_pushnewstructinstance; + sq_sealstructslotType __sq_sealstructslot; +}; + +struct MessageSource +{ + const char* file; + const char* func; + int line; +}; + +// This is a modified version of spdlog::details::log_msg +// This is so that we can make it cross DLL boundaries +struct LogMsg +{ + int level; + uint64_t timestamp; + const char* msg; + MessageSource source; + int pluginHandle; +}; + +extern "C" +{ + typedef void (*loggerfunc_t)(LogMsg* msg); + typedef void (*PLUGIN_RELAY_INVITE_TYPE)(const char* invite); + typedef void* (*CreateObjectFunc)(ObjectType type); + + typedef void (*PluginFnCommandCallback_t)(void* command); + typedef void (*PluginConCommandConstructorType)( + void* newCommand, const char* name, PluginFnCommandCallback_t callback, const char* helpString, int flags, void* parent); + typedef void (*PluginConVarRegisterType)( + void* pConVar, + const char* pszName, + const char* pszDefaultValue, + int nFlags, + const char* pszHelpString, + bool bMin, + float fMin, + bool bMax, + float fMax, + void* pCallback); + typedef void (*PluginConVarMallocType)(void* pConVarMaloc, int a2, int a3); +} + +struct PluginNorthstarData +{ + const char* version; + HMODULE northstarModule; + int pluginHandle; +}; + +struct PluginInitFuncs +{ + loggerfunc_t logger; + PLUGIN_RELAY_INVITE_TYPE relayInviteFunc; + CreateObjectFunc createObject; +}; + +struct PluginEngineData +{ + PluginConCommandConstructorType ConCommandConstructor; + PluginConVarMallocType conVarMalloc; + PluginConVarRegisterType conVarRegister; + void* ConVar_Vtable; + void* IConVar_Vtable; + void* g_pCVar; +}; + +/// Async communication within the plugin system +/// Due to the asynchronous nature of plugins, combined with the limitations of multi-compiler support +/// and the custom memory allocator used by r2, is it difficult to safely get data across DLL boundaries +/// from Northstar to plugin unless Northstar can own that memory. +/// This means that plugins should manage their own memory and can only receive data from northstar using one of the functions below. +/// These should be exports of the plugin DLL. If they are not exported, they will not be called. +/// Note that it is not required to have these exports if you do not use them. +/// + +// Northstar -> Plugin +typedef void (*PLUGIN_INIT_TYPE)(PluginInitFuncs* funcs, PluginNorthstarData* data); +typedef void (*PLUGIN_INIT_SQVM_TYPE)(SquirrelFunctions* funcs); +typedef void (*PLUGIN_INFORM_SQVM_CREATED_TYPE)(ScriptContext context, CSquirrelVM* sqvm); +typedef void (*PLUGIN_INFORM_SQVM_DESTROYED_TYPE)(ScriptContext context); + +typedef void (*PLUGIN_INFORM_DLL_LOAD_TYPE)(const char* dll, PluginEngineData* data, void* dllPtr); +typedef void (*PLUGIN_RUNFRAME)(); diff --git a/primedev/plugins/pluginbackend.cpp b/primedev/plugins/pluginbackend.cpp new file mode 100644 index 00000000..850394ac --- /dev/null +++ b/primedev/plugins/pluginbackend.cpp @@ -0,0 +1,50 @@ +#include "pluginbackend.h" +#include "plugin_abi.h" +#include "server/serverpresence.h" +#include "masterserver/masterserver.h" +#include "squirrel/squirrel.h" +#include "plugins.h" + +#include "core/convar/concommand.h" + +#include + +#define EXPORT extern "C" __declspec(dllexport) + +AUTOHOOK_INIT() + +PluginCommunicationHandler* g_pPluginCommunicationhandler; + +static PluginDataRequest storedRequest {PluginDataRequestType::END, (PluginRespondDataCallable) nullptr}; + +void PluginCommunicationHandler::RunFrame() +{ + std::lock_guard lock(requestMutex); + if (!requestQueue.empty()) + { + storedRequest = requestQueue.front(); + switch (storedRequest.type) + { + default: + spdlog::error("{} was called with invalid request type '{}'", __FUNCTION__, static_cast(storedRequest.type)); + } + requestQueue.pop(); + } +} + +void PluginCommunicationHandler::PushRequest(PluginDataRequestType type, PluginRespondDataCallable func) +{ + std::lock_guard lock(requestMutex); + requestQueue.push(PluginDataRequest {type, func}); +} + +void InformPluginsDLLLoad(fs::path dllPath, void* address) +{ + std::string dllName = dllPath.filename().string(); + + void* data = NULL; + if (strncmp(dllName.c_str(), "engine.dll", 10) == 0) + data = &g_pPluginCommunicationhandler->m_sEngineData; + + g_pPluginManager->InformDLLLoad(dllName.c_str(), data, address); +} diff --git a/primedev/plugins/pluginbackend.h b/primedev/plugins/pluginbackend.h new file mode 100644 index 00000000..45cd42f3 --- /dev/null +++ b/primedev/plugins/pluginbackend.h @@ -0,0 +1,40 @@ +#pragma once +#include "plugin_abi.h" + +#include +#include + +enum PluginDataRequestType +{ + END = 0, +}; + +union PluginRespondDataCallable +{ + // Empty for now + void* UNUSED; +}; + +class PluginDataRequest +{ +public: + PluginDataRequestType type; + PluginRespondDataCallable func; + PluginDataRequest(PluginDataRequestType type, PluginRespondDataCallable func) : type(type), func(func) {} +}; + +class PluginCommunicationHandler +{ +public: + void RunFrame(); + void PushRequest(PluginDataRequestType type, PluginRespondDataCallable func); + +public: + std::queue requestQueue = {}; + std::mutex requestMutex; + + PluginEngineData m_sEngineData {}; +}; + +void InformPluginsDLLLoad(fs::path dllPath, void* address); +extern PluginCommunicationHandler* g_pPluginCommunicationhandler; diff --git a/primedev/plugins/plugins.cpp b/primedev/plugins/plugins.cpp new file mode 100644 index 00000000..72b64566 --- /dev/null +++ b/primedev/plugins/plugins.cpp @@ -0,0 +1,340 @@ +#include "plugins.h" +#include "config/profile.h" + +#include "squirrel/squirrel.h" +#include "plugins.h" +#include "masterserver/masterserver.h" +#include "core/convar/convar.h" +#include "server/serverpresence.h" +#include +#include + +#include "util/version.h" +#include "pluginbackend.h" +#include "util/wininfo.h" +#include "logging/logging.h" +#include "dedicated/dedicated.h" + +PluginManager* g_pPluginManager; + +void freeLibrary(HMODULE hLib) +{ + if (!FreeLibrary(hLib)) + { + spdlog::error("There was an error while trying to free library"); + } +} + +EXPORT void PLUGIN_LOG(LogMsg* msg) +{ + spdlog::source_loc src {}; + src.filename = msg->source.file; + src.funcname = msg->source.func; + src.line = msg->source.line; + auto&& logger = g_pPluginManager->m_vLoadedPlugins[msg->pluginHandle].logger; + logger->log(src, (spdlog::level::level_enum)msg->level, msg->msg); +} + +EXPORT void* CreateObject(ObjectType type) +{ + switch (type) + { + case ObjectType::CONVAR: + return (void*)new ConVar; + case ObjectType::CONCOMMANDS: + return (void*)new ConCommand; + default: + return NULL; + } +} + +std::optional PluginManager::LoadPlugin(fs::path path, PluginInitFuncs* funcs, PluginNorthstarData* data) +{ + + Plugin plugin {}; + + std::string pathstring = path.string(); + std::wstring wpath = path.wstring(); + + LPCWSTR wpptr = wpath.c_str(); + HMODULE datafile = LoadLibraryExW(wpptr, 0, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE); // Load the DLL as a data file + if (datafile == NULL) + { + NS::log::PLUGINSYS->info("Failed to load library '{}': ", std::system_category().message(GetLastError())); + return std::nullopt; + } + HRSRC manifestResource = FindResourceW(datafile, MAKEINTRESOURCEW(IDR_RCDATA1), RT_RCDATA); + + if (manifestResource == NULL) + { + NS::log::PLUGINSYS->info("Could not find manifest for library '{}'", pathstring); + freeLibrary(datafile); + return std::nullopt; + } + HGLOBAL myResourceData = LoadResource(datafile, manifestResource); + if (myResourceData == NULL) + { + NS::log::PLUGINSYS->error("Failed to load manifest from library '{}'", pathstring); + freeLibrary(datafile); + return std::nullopt; + } + int manifestSize = SizeofResource(datafile, manifestResource); + std::string manifest = std::string((const char*)LockResource(myResourceData), 0, manifestSize); + freeLibrary(datafile); + + rapidjson_document manifestJSON; + manifestJSON.Parse(manifest.c_str()); + + if (manifestJSON.HasParseError()) + { + NS::log::PLUGINSYS->error("Manifest for '{}' was invalid", pathstring); + return std::nullopt; + } + if (!manifestJSON.HasMember("name")) + { + NS::log::PLUGINSYS->error("'{}' is missing a name in its manifest", pathstring); + return std::nullopt; + } + if (!manifestJSON.HasMember("displayname")) + { + NS::log::PLUGINSYS->error("'{}' is missing a displayname in its manifest", pathstring); + return std::nullopt; + } + if (!manifestJSON.HasMember("description")) + { + NS::log::PLUGINSYS->error("'{}' is missing a description in its manifest", pathstring); + return std::nullopt; + } + if (!manifestJSON.HasMember("api_version")) + { + NS::log::PLUGINSYS->error("'{}' is missing a api_version in its manifest", pathstring); + return std::nullopt; + } + if (!manifestJSON.HasMember("version")) + { + NS::log::PLUGINSYS->error("'{}' is missing a version in its manifest", pathstring); + return std::nullopt; + } + if (!manifestJSON.HasMember("run_on_server")) + { + NS::log::PLUGINSYS->error("'{}' is missing 'run_on_server' in its manifest", pathstring); + return std::nullopt; + } + if (!manifestJSON.HasMember("run_on_client")) + { + NS::log::PLUGINSYS->error("'{}' is missing 'run_on_client' in its manifest", pathstring); + return std::nullopt; + } + auto test = manifestJSON["api_version"].GetString(); + if (strcmp(manifestJSON["api_version"].GetString(), std::to_string(ABI_VERSION).c_str())) + { + NS::log::PLUGINSYS->error( + "'{}' has an incompatible API version number in its manifest. Current ABI version is '{}'", pathstring, ABI_VERSION); + return std::nullopt; + } + // Passed all checks, going to actually load it now + + HMODULE pluginLib = + LoadLibraryExW(wpptr, 0, LOAD_LIBRARY_SEARCH_USER_DIRS | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // Load the DLL with lib folders + if (pluginLib == NULL) + { + NS::log::PLUGINSYS->info("Failed to load library '{}': ", std::system_category().message(GetLastError())); + return std::nullopt; + } + plugin.init = (PLUGIN_INIT_TYPE)GetProcAddress(pluginLib, "PLUGIN_INIT"); + if (plugin.init == NULL) + { + NS::log::PLUGINSYS->info("Library '{}' has no function 'PLUGIN_INIT'", pathstring); + return std::nullopt; + } + NS::log::PLUGINSYS->info("Succesfully loaded {}", pathstring); + + plugin.name = manifestJSON["name"].GetString(); + plugin.displayName = manifestJSON["displayname"].GetString(); + plugin.description = manifestJSON["description"].GetString(); + plugin.api_version = manifestJSON["api_version"].GetString(); + plugin.version = manifestJSON["version"].GetString(); + + plugin.run_on_client = manifestJSON["run_on_client"].GetBool(); + plugin.run_on_server = manifestJSON["run_on_server"].GetBool(); + + if (!plugin.run_on_server && IsDedicatedServer()) + return std::nullopt; + + if (manifestJSON.HasMember("dependencyName")) + { + plugin.dependencyName = manifestJSON["dependencyName"].GetString(); + } + else + { + plugin.dependencyName = plugin.name; + } + + if (std::find_if( + plugin.dependencyName.begin(), + plugin.dependencyName.end(), + [&](char c) -> bool { return !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'); }) != + plugin.dependencyName.end()) + { + NS::log::PLUGINSYS->warn("Dependency string \"{}\" in {} is not valid a squirrel constant!", plugin.dependencyName, plugin.name); + } + + plugin.init_sqvm_client = (PLUGIN_INIT_SQVM_TYPE)GetProcAddress(pluginLib, "PLUGIN_INIT_SQVM_CLIENT"); + plugin.init_sqvm_server = (PLUGIN_INIT_SQVM_TYPE)GetProcAddress(pluginLib, "PLUGIN_INIT_SQVM_SERVER"); + plugin.inform_sqvm_created = (PLUGIN_INFORM_SQVM_CREATED_TYPE)GetProcAddress(pluginLib, "PLUGIN_INFORM_SQVM_CREATED"); + plugin.inform_sqvm_destroyed = (PLUGIN_INFORM_SQVM_DESTROYED_TYPE)GetProcAddress(pluginLib, "PLUGIN_INFORM_SQVM_DESTROYED"); + + plugin.inform_dll_load = (PLUGIN_INFORM_DLL_LOAD_TYPE)GetProcAddress(pluginLib, "PLUGIN_INFORM_DLL_LOAD"); + + plugin.run_frame = (PLUGIN_RUNFRAME)GetProcAddress(pluginLib, "PLUGIN_RUNFRAME"); + + plugin.handle = m_vLoadedPlugins.size(); + plugin.logger = std::make_shared(plugin.displayName.c_str(), NS::Colors::PLUGIN); + RegisterLogger(plugin.logger); + NS::log::PLUGINSYS->info("Loading plugin {} version {}", plugin.displayName, plugin.version); + m_vLoadedPlugins.push_back(plugin); + + plugin.init(funcs, data); + + return plugin; +} + +inline void FindPlugins(fs::path pluginPath, std::vector& paths) +{ + // ensure dirs exist + if (!fs::exists(pluginPath) || !fs::is_directory(pluginPath)) + { + return; + } + + for (const fs::directory_entry& entry : fs::directory_iterator(pluginPath)) + { + if (fs::is_regular_file(entry) && entry.path().extension() == ".dll") + paths.emplace_back(entry.path()); + } +} + +bool PluginManager::LoadPlugins() +{ + if (strstr(GetCommandLineA(), "-noplugins") != NULL) + { + NS::log::PLUGINSYS->warn("-noplugins detected; skipping loading plugins"); + return false; + } + + fs::create_directories(GetThunderstoreModFolderPath()); + + std::vector paths; + + pluginPath = GetNorthstarPrefix() + "\\plugins"; + + PluginNorthstarData data {}; + std::string ns_version {version}; + + PluginInitFuncs funcs {}; + funcs.logger = PLUGIN_LOG; + funcs.relayInviteFunc = nullptr; + funcs.createObject = CreateObject; + + data.version = ns_version.c_str(); + data.northstarModule = g_NorthstarModule; + + fs::path libPath = fs::absolute(pluginPath + "\\lib"); + if (fs::exists(libPath) && fs::is_directory(libPath)) + AddDllDirectory(libPath.wstring().c_str()); + + FindPlugins(pluginPath, paths); + + // Special case for Thunderstore mods dir + std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath()); + // Set up regex for `AUTHOR-MOD-VERSION` pattern + std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))"); + for (fs::directory_entry dir : thunderstoreModsDir) + { + fs::path pluginsDir = dir.path() / "plugins"; + // Use regex to match `AUTHOR-MOD-VERSION` pattern + if (!std::regex_match(dir.path().string(), pattern)) + { + spdlog::warn("The following directory did not match 'AUTHOR-MOD-VERSION': {}", dir.path().string()); + continue; // skip loading package that doesn't match + } + + fs::path libDir = fs::absolute(pluginsDir / "lib"); + if (fs::exists(libDir) && fs::is_directory(libDir)) + AddDllDirectory(libDir.wstring().c_str()); + + FindPlugins(pluginsDir, paths); + } + + if (paths.empty()) + { + NS::log::PLUGINSYS->warn("Could not find any plugins. Skipped loading plugins"); + return false; + } + + for (fs::path path : paths) + { + if (LoadPlugin(path, &funcs, &data)) + data.pluginHandle += 1; + } + return true; +} + +void PluginManager::InformSQVMLoad(ScriptContext context, SquirrelFunctions* s) +{ + for (auto plugin : m_vLoadedPlugins) + { + if (context == ScriptContext::CLIENT && plugin.init_sqvm_client != NULL) + { + plugin.init_sqvm_client(s); + } + else if (context == ScriptContext::SERVER && plugin.init_sqvm_server != NULL) + { + plugin.init_sqvm_server(s); + } + } +} + +void PluginManager::InformSQVMCreated(ScriptContext context, CSquirrelVM* sqvm) +{ + for (auto plugin : m_vLoadedPlugins) + { + if (plugin.inform_sqvm_created != NULL) + { + plugin.inform_sqvm_created(context, sqvm); + } + } +} + +void PluginManager::InformSQVMDestroyed(ScriptContext context) +{ + for (auto plugin : m_vLoadedPlugins) + { + if (plugin.inform_sqvm_destroyed != NULL) + { + plugin.inform_sqvm_destroyed(context); + } + } +} + +void PluginManager::InformDLLLoad(const char* dll, void* data, void* dllPtr) +{ + for (auto plugin : m_vLoadedPlugins) + { + if (plugin.inform_dll_load != NULL) + { + plugin.inform_dll_load(dll, (PluginEngineData*)data, dllPtr); + } + } +} + +void PluginManager::RunFrame() +{ + for (auto plugin : m_vLoadedPlugins) + { + if (plugin.run_frame != NULL) + { + plugin.run_frame(); + } + } +} diff --git a/primedev/plugins/plugins.h b/primedev/plugins/plugins.h new file mode 100644 index 00000000..4e841f27 --- /dev/null +++ b/primedev/plugins/plugins.h @@ -0,0 +1,59 @@ +#pragma once +#include "plugin_abi.h" + +const int IDR_RCDATA1 = 101; + +class Plugin +{ +public: + std::string name; + std::string displayName; + std::string dependencyName; + std::string description; + + std::string api_version; + std::string version; + + // For now this is just implemented as the index into the plugins array + // Maybe a bit shit but it works + int handle; + + std::shared_ptr logger; + + bool run_on_client = false; + bool run_on_server = false; + +public: + PLUGIN_INIT_TYPE init; + PLUGIN_INIT_SQVM_TYPE init_sqvm_client; + PLUGIN_INIT_SQVM_TYPE init_sqvm_server; + PLUGIN_INFORM_SQVM_CREATED_TYPE inform_sqvm_created; + PLUGIN_INFORM_SQVM_DESTROYED_TYPE inform_sqvm_destroyed; + + PLUGIN_INFORM_DLL_LOAD_TYPE inform_dll_load; + + PLUGIN_RUNFRAME run_frame; +}; + +class PluginManager +{ +public: + std::vector m_vLoadedPlugins; + +public: + bool LoadPlugins(); + std::optional LoadPlugin(fs::path path, PluginInitFuncs* funcs, PluginNorthstarData* data); + + void InformSQVMLoad(ScriptContext context, SquirrelFunctions* s); + void InformSQVMCreated(ScriptContext context, CSquirrelVM* sqvm); + void InformSQVMDestroyed(ScriptContext context); + + void InformDLLLoad(const char* dll, void* data, void* dllPtr); + + void RunFrame(); + +private: + std::string pluginPath; +}; + +extern PluginManager* g_pPluginManager; -- cgit v1.2.3