diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | cmake/FindRapidJSON.cmake | 16 | ||||
-rw-r--r-- | cmake/Findjson-c.cmake | 99 | ||||
-rw-r--r-- | src/CMakeLists.txt | 6 | ||||
-rw-r--r-- | src/init.cpp | 44 | ||||
-rw-r--r-- | src/internal/concommandproxy.h | 48 | ||||
-rw-r--r-- | src/internal/convarproxy.h | 87 | ||||
-rw-r--r-- | src/internal/logging.h | 5 | ||||
-rw-r--r-- | src/internal/proxy.h | 15 | ||||
-rw-r--r-- | src/internal/types.h | 38 | ||||
-rw-r--r-- | src/ns_plugin.h | 69 | ||||
-rw-r--r-- | src/plugin.cpp | 239 | ||||
-rw-r--r-- | src/plugin.h | 54 | ||||
-rw-r--r-- | src/server.cpp | 370 | ||||
-rw-r--r-- | src/server.h | 71 |
15 files changed, 995 insertions, 167 deletions
@@ -1 +1,2 @@ build +.vs diff --git a/cmake/FindRapidJSON.cmake b/cmake/FindRapidJSON.cmake new file mode 100644 index 0000000..857bc9e --- /dev/null +++ b/cmake/FindRapidJSON.cmake @@ -0,0 +1,16 @@ +### Get same spdlog as Northstar + +if (RapidJSON_FOUND) + return() +endif() + +find_package(NorthstarPluginABI REQUIRED) + +check_init_submodule(${NS_LAUNCHER_DIR}/thirdparty/rapidjson) + +add_library(rapidjson_header INTERFACE) +target_include_directories(rapidjson_header INTERFACE "${NS_LAUNCHER_DIR}/thirdparty") +target_include_directories(rapidjson_header INTERFACE "${NS_LAUNCHER_DIR}/thirdparty/rapidjson") + +set(RapidJSON_FOUND 1) + diff --git a/cmake/Findjson-c.cmake b/cmake/Findjson-c.cmake deleted file mode 100644 index 80a82dc..0000000 --- a/cmake/Findjson-c.cmake +++ /dev/null @@ -1,99 +0,0 @@ -# -# Tries to find json-c through the config -# before trying to query for it -# - -if (json-c_FOUND) - return() -endif() - -#find_package(json-c CONFIG) - -if (json-c_FOUND) - return() -endif() - -if (NOT "${CMAKE_CXX_COMPILER_ID}" MATCHES "MSVC") - find_package(PkgConfig QUIET) - if (PKG_CONFIG_FOUND) - pkg_check_modules(_JSONC json-c) - endif() -endif() - -if (_JSONC_FOUND) # we can rely on pkg-config - set(json-c_LINK_LIBRARIES ${_JSONC_LINK_LIBRARIES}) - if (NOT BUILD_STATIC) - set(json-c_INCLUDE_DIRS ${_JSONC_INCLUDE_DIRS}) - set(json-c_CFLAGS ${_JSONC_CFLAGS_OTHER}) - else() - set(json-c_INCLUDE_DIRS ${_JSONC_STATIC_INCLUDE_DIRS}) - set(json-c_CFLAGS ${_JSONC_STATIC_CFLAGS_OTHER}) - endif() - set(json-c_FOUND 1) -else() - if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(_lib_suffix 64) - else() - set(_lib_suffix 32) - endif() - - find_path(JSONC_INC - NAMES json.h - HINTS - ENV jsoncPath${_lib_suffix} - ENV jsoncPath - ${_JSONC_INCLUDE_DIRS} - ) - - find_library(JSONC_LIB - NAMES ${_JSONC_LIBRARIES} jsonc json-c - HINTS - ENV jsoncPath${_lib_suffix} - ENV jsoncPath - ${_JSONC_LIBRARY_DIRS} - ${_JSONC_STATIC_LIBRARY_DIRS} - ) - - include(FindPackageHandleStandardArgs) - #find_package_handle_standard_args(json-c DEFAULT_MSG JSONC_LIB JSONC_INC) - mark_as_advanced(JSONC_INC JSONC_LIB) - - if(json-c_FOUND) - set(json-c_INCLUDE_DIRS ${JSONC_INC}) - set(json-c_LINK_LIBRARIES ${JSONC_LIB}) - if (BUILD_STATIC) - set(json-c_LINK_LIBRARIES ${json-c_LINK_LIBRARIES} ${_JSONC_STATIC_LIBRARIES}) - endif() - endif() -endif() - - -if (json-c_FOUND) - # Reconstruct the official interface - add_library(json-c::json-c UNKNOWN IMPORTED) - set_target_properties(json-c::json-c PROPERTIES - IMPORTED_LOCATION "${json-c_LINK_LIBRARIES}" - ) - target_compile_definitions(json-c::json-c INTERFACE ${json-c_CFLAGS}) - target_include_directories(json-c::json-c INTERFACE ${json-c_INCLUDE_DIRS}) -else() - include(FetchContent) - cmake_policy(SET CMP0077 NEW) - - message(STATUS "Downloading json-c...") - FetchContent_Declare( - jsonc - GIT_REPOSITORY https://github.com/json-c/json-c - GIT_TAG json-c-0.17 - GIT_SHALLOW TRUE - ) - - set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "") - set(BUILD_STATIC_LIBS ON CACHE INTERNAL "") - FetchContent_MakeAvailable(jsonc) - - # Only the config file includes the namespace - add_library(json-c::json-c ALIAS json-c) - - set(json-c_FOUND 1) -endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a55c541..674d664 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,7 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}) -find_package(json-c REQUIRED) +find_package(RapidJSON REQUIRED) plugin_manifest(SouthRPC name "SouthRPC") plugin_manifest(SouthRPC displayname "SouthRPC") @@ -17,7 +17,9 @@ add_library(SouthRPC SHARED ${CMAKE_CURRENT_SOURCE_DIR}/server.h ) -target_link_libraries(SouthRPC json-c::json-c) +target_include_directories(SouthRPC PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(SouthRPC rapidjson_header) +target_link_libraries(SouthRPC ws2_32) target_precompile_headers(SouthRPC PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/ns_plugin.h) plugin_link(SouthRPC) diff --git a/src/init.cpp b/src/init.cpp index 165c574..d0005c0 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -10,7 +10,7 @@ extern "C" __declspec(dllexport) void PLUGIN_INIT(PluginInitFuncs* funcs, PluginNorthstarData* data) { spdlog::default_logger()->sinks().pop_back(); - spdlog::default_logger()->sinks().push_back(std::make_shared<PluginSink>(funcs->logger)); + spdlog::default_logger()->sinks().push_back(std::make_shared<PluginSink>(funcs->logger, data->pluginHandle)); plugin = new Plugin(funcs, data); } @@ -18,20 +18,22 @@ void PLUGIN_INIT(PluginInitFuncs* funcs, PluginNorthstarData* data) extern "C" __declspec(dllexport) void PLUGIN_DEINIT() { - if (plugin) - { - delete plugin; - plugin = nullptr; - } + assert(plugin); + + delete plugin; + plugin = nullptr; } extern "C" __declspec(dllexport) void PLUGIN_INFORM_DLL_LOAD(PluginLoadDLL dll, void* data) { + assert(plugin); + switch (dll) { case PluginLoadDLL::ENGINE: plugin->LoadEngineData(data); - break; + plugin->StartServer(); case PluginLoadDLL::CLIENT: + break; case PluginLoadDLL::SERVER: break; default: @@ -40,6 +42,34 @@ void PLUGIN_INFORM_DLL_LOAD(PluginLoadDLL dll, void* data) { } } +extern "C" __declspec(dllexport) +void PLUGIN_INIT_SQVM_CLIENT(SquirrelFunctions* funcs) +{ + assert(plugin); + plugin->LoadSQVMFunctions(ScriptContext::CLIENT, funcs); +} + +extern "C" __declspec(dllexport) +void PLUGIN_INIT_SQVM_SERVER(SquirrelFunctions* funcs) +{ + assert(plugin); + plugin->LoadSQVMFunctions(ScriptContext::SERVER, funcs); +} + +extern "C" __declspec(dllexport) +void PLUGIN_INFORM_SQVM_CREATED(ScriptContext context, CSquirrelVM* sqvm) +{ + assert(plugin); + plugin->LoadSQVM(context, sqvm); +} + +extern "C" __declspec(dllexport) +void PLUGIN_INFORM_SQVM_DESTROYED(ScriptContext context) +{ + assert(plugin); + plugin->RemoveSQVM(context); +} + // There is no deinit logic for Plugins // Recreate it using DllMain BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) diff --git a/src/internal/concommandproxy.h b/src/internal/concommandproxy.h new file mode 100644 index 0000000..145339a --- /dev/null +++ b/src/internal/concommandproxy.h @@ -0,0 +1,48 @@ +#ifndef CONCOMMANDPROXY_H +#define CONCOMMANDPROXY_H + +#include "ns_plugin.h" +#include "proxy.h" +#include "types.h" + +class ConCommandProxy : public ClassProxy<ConCommand> { + private: + const char* name; + FnCommandCallback_t callback; + const char* helpString; + int flags; + void* parent; + + public: + ConCommandProxy( + const char* name, + FnCommandCallback_t callback, + const char* helpString, + int flags, + void* parent + ) : + name(name), + callback(callback), + helpString(helpString), + flags(flags), + parent(parent) + {} + + virtual void initialize(void* data) + { + PLUGIN_DATA_TYPES* plugin_data = static_cast<PLUGIN_DATA_TYPES*>(data); + + PluginInitFuncs* funcs = plugin_data->funcs; + EngineData* engine_data = plugin_data->engine_data; + + assert(funcs->createObject); + assert(engine_data->ConCommandConstructor); + + ptr = static_cast<ConCommand*>(funcs->createObject(ObjectType::CONCOMMANDS)); + + spdlog::info("Registering ConCommand {}", name); + engine_data->ConCommandConstructor(ptr, name, callback, helpString, flags, parent); + } +}; + +#endif
\ No newline at end of file diff --git a/src/internal/convarproxy.h b/src/internal/convarproxy.h new file mode 100644 index 0000000..08aea5a --- /dev/null +++ b/src/internal/convarproxy.h @@ -0,0 +1,87 @@ +#ifndef CONVARPROXY_H +#define CONVARPROXY_H + +#include "ns_plugin.h" +#include "proxy.h" +#include "types.h" + +class ConVarProxy: public ClassProxy<ConVar> { +private: + const char* pszName; + const char* pszDefaultValue; + int nFlags; + const char* pszHelpString; + bool bMin; + float fMin; + bool bMax; + float fMax; + FnChangeCallback_t pCallback; + +public: + ConVarProxy( + const char* pszName, + const char* pszDefaultValue, + int nFlags, + const char* pszHelpString, + bool bMin, + float fMin, + bool bMax, + float fMax, + FnChangeCallback_t pCallback + ) : + pszName(pszName), + pszDefaultValue(pszDefaultValue), + nFlags(nFlags), + pszHelpString(pszHelpString), + bMin(bMin), + fMin(fMin), + bMax(bMax), + fMax(fMax), + pCallback(pCallback) + {} + + virtual void initialize(void* data) + { + PLUGIN_DATA_TYPES* plugin_data = static_cast<PLUGIN_DATA_TYPES*>(data); + + PluginInitFuncs* funcs = plugin_data->funcs; + EngineData* engine_data = plugin_data->engine_data; + + assert(funcs->createObject); + assert(engine_data->ConCommandConstructor); + + this->ptr = static_cast<ConVar*>(funcs->createObject(ObjectType::CONVAR)); + + spdlog::info("Registering Convar {}", pszName); + + this->ptr->m_ConCommandBase.m_pConCommandBaseVTable = engine_data->ConVar_Vtable; + this->ptr->m_ConCommandBase.s_pConCommandBases = (ConCommandBase*)engine_data->IConVar_Vtable; + + engine_data->conVarMalloc(&(this->ptr->m_pMalloc), 0, 0); + engine_data->conVarRegister(this->ptr, pszName, pszDefaultValue, nFlags, pszHelpString, bMin, fMin, bMax, fMax, (void*)pCallback); + } + + const char* GetString() const { + assert(this->ptr); + + return this->ptr->m_Value.m_pszString; + } + + bool Getbool() const { + return !!GetInt(); + } + + int GetInt() const { + assert(this->ptr); + + return this->ptr->m_Value.m_nValue; + } + + float GetFloat() const { + assert(this->ptr); + + return this->ptr->m_Value.m_fValue; + } +}; + +#endif
\ No newline at end of file diff --git a/src/internal/logging.h b/src/internal/logging.h index fb0e415..3587e7f 100644 --- a/src/internal/logging.h +++ b/src/internal/logging.h @@ -13,9 +13,10 @@ class PluginSink : public spdlog_base_sink { public: - PluginSink(loggerfunc_t logger): spdlog_base_sink() + PluginSink(loggerfunc_t logger, int pluginHandle): spdlog_base_sink() { this->ns_logger_ = logger; + this->pluginHandle = pluginHandle; } void sink_it_(const spdlog::details::log_msg& in_msg) override @@ -28,6 +29,7 @@ public: msg.source.file = in_msg.source.filename; msg.source.func = in_msg.source.funcname; msg.source.line = in_msg.source.line; + msg.pluginHandle = this->pluginHandle; this->ns_logger_(&msg); } @@ -38,6 +40,7 @@ public: protected: loggerfunc_t ns_logger_; + int pluginHandle = 0; // sink log level - default is all spdlog::level_t level_{ spdlog::level::trace }; diff --git a/src/internal/proxy.h b/src/internal/proxy.h new file mode 100644 index 0000000..4029f42 --- /dev/null +++ b/src/internal/proxy.h @@ -0,0 +1,15 @@ +#ifndef PROXY_H +#define PROXY_H + +template <typename T> +class ClassProxy { + protected: + T* ptr = nullptr; + + public: + T* get() { return this->ptr; }; + + virtual void initialize(void*) = 0; +}; + +#endif
\ No newline at end of file diff --git a/src/internal/types.h b/src/internal/types.h new file mode 100644 index 0000000..a19c00c --- /dev/null +++ b/src/internal/types.h @@ -0,0 +1,38 @@ +#ifndef TYPES_H +#define TYPES_H + +#include "ns_plugin.h" + +typedef struct { + PluginInitFuncs* funcs; + PluginNorthstarData* data; + EngineData* engine_data; +} PLUGIN_DATA_TYPES ; + +#ifndef SQTrue +#define SQTrue (1) +#endif + +#ifndef SQFalse +#define SQFalse (0) +#endif + +#ifndef SQ_FAILED +#define SQ_FAILED(res) (res<0) +#endif + +#ifndef SQ_SUCCEEDED +#define SQ_SUCCEEDED(res) (res>=0) +#endif + +#ifndef ISREFCOUNTED +#define ISREFCOUNTED(t) (t&SQOBJECT_REF_COUNTED) +#endif + +#ifndef sq_isnull +#define sq_isnull(o) ((o)._type==OT_NULL) +#endif + +#define SQ_NULL_OBJ SQObject { OT_NULL, 0, nullptr } + +#endif diff --git a/src/ns_plugin.h b/src/ns_plugin.h index 73c805e..99c90e9 100644 --- a/src/ns_plugin.h +++ b/src/ns_plugin.h @@ -2,6 +2,10 @@ #define NS_PLUGIN_H #define WIN32_LEAN_AND_MEAN +// Needed for RapidJSON to function +#define RAPIDJSON_NOMEMBERITERATORCLASS +#define NOMINMAX +#define RAPIDJSON_HAS_STDSTRING 1 // Needed to bootstrap plugin abi #include <windows.h> @@ -18,14 +22,69 @@ #include "core/convar/convar.h" #include "core/convar/concommand.h" +//#include "engine/r2engine.h" +// Import r2engine isn't possible, vendor stuff +enum class ECommandTarget_t +{ + CBUF_FIRST_PLAYER = 0, + CBUF_LAST_PLAYER = 1, // MAX_SPLITSCREEN_CLIENTS - 1, MAX_SPLITSCREEN_CLIENTS = 2 + CBUF_SERVER = CBUF_LAST_PLAYER + 1, + + CBUF_COUNT, +}; + +enum class cmd_source_t +{ + // Added to the console buffer by gameplay code. Generally unrestricted. + kCommandSrcCode, + + // Sent from code via engine->ClientCmd, which is restricted to commands visible + // via FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS. + kCommandSrcClientCmd, + + // Typed in at the console or via a user key-bind. Generally unrestricted, although + // the client will throttle commands sent to the server this way to 16 per second. + kCommandSrcUserInput, + + // Came in over a net connection as a clc_stringcmd + // host_client will be valid during this state. + // + // Restricted to FCVAR_GAMEDLL commands (but not convars) and special non-ConCommand + // server commands hardcoded into gameplay code (e.g. "joingame") + kCommandSrcNetClient, + + // Received from the server as the client + // + // Restricted to commands with FCVAR_SERVER_CAN_EXECUTE + kCommandSrcNetServer, + + // Being played back from a demo file + // + // Not currently restricted by convar flag, but some commands manually ignore calls + // from this source. FIXME: Should be heavily restricted as demo commands can come + // from untrusted sources. + kCommandSrcDemoFile, + + // Invalid value used when cleared + kCommandSrcInvalid = -1 +}; + +typedef ECommandTarget_t (*Cbuf_GetCurrentPlayerType)(); +typedef void (*Cbuf_AddTextType)(ECommandTarget_t eTarget, const char* text, cmd_source_t source); +typedef void (*Cbuf_ExecuteType)(); + // This is a mess -// hope Plugins V3 includes these in the ABI -// pls cat :womp: typedef void (*ConCommandConstructorType)(ConCommand* newCommand, const char* name, FnCommandCallback_t callback, const char* helpString, int flags, void* parent); typedef void (*ConVarMallocType)(void* pConVarMaloc, int a2, int a3); typedef void (*ConVarRegisterType)(ConVar* pConVar, const char* pszName, const char* pszDefaultValue, int nFlags, const char* pszHelpString, bool bMin, float fMin, bool bMax, float fMax, void* pCallback); -extern "C" { - typedef void* (*extern_CreateObjectFunc)(ObjectType type); -} + +struct EngineData +{ + ConCommandConstructorType ConCommandConstructor; + ConVarMallocType conVarMalloc; + ConVarRegisterType conVarRegister; + void* ConVar_Vtable; + void* IConVar_Vtable; +}; #endif diff --git a/src/plugin.cpp b/src/plugin.cpp index d13ffc6..387846c 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -1,44 +1,245 @@ +#include <windows.h> +#include <tchar.h> +#include <stdio.h> +#include <psapi.h> + #include "ns_plugin.h" #include "plugin.h" #include "server.h" +#include "internal/types.h" +#include "internal/concommandproxy.h" +#include "internal/convarproxy.h" Plugin::Plugin(PluginInitFuncs* funcs, PluginNorthstarData* data) - : server(new jsonrpc_server(this)) { - this->funcs = funcs; - this->data = data; + this->funcs = *funcs; + this->data = *data; + + this->server = new rpc_server(this); - spdlog::info(PLUGIN_NAME " initialised!"); + spdlog::info(PLUGIN_NAME " initialised!"); } Plugin::~Plugin() { - delete server; + for (ConCommandProxy* proxy : this->commands) + { + delete proxy; + } + this->commands.clear(); + + for (ConVarProxy* proxy : this->variables) + { + delete proxy; + } + this->variables.clear(); + + server->stop(); + delete server; +} + +HMODULE Plugin::GetModuleByName(const char* name) +{ + HMODULE hMods[1024]; + HANDLE hProcess = GetCurrentProcess(); + DWORD cbNeeded; + if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) + { + for (size_t i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) + { + char szFullModName[MAX_PATH]; + char* szModName = nullptr; + + if (GetModuleFileNameEx(hProcess, hMods[i], szFullModName, sizeof(szFullModName) / sizeof(*szFullModName))) + { + szModName = szFullModName + (strlen(szFullModName) - strlen(name)); + if (!strcmp(szModName, name)) + return hMods[i]; + } + } + } + + return nullptr; } void Plugin::LoadEngineData(void* data) { - this->engine_data = static_cast<EngineData*>(data); + this->engine_data = *static_cast<EngineData*>(data); + + PLUGIN_DATA_TYPES plugin_data = { + &this->funcs, + &this->data, + &this->engine_data + }; + for (ConCommandProxy* proxy : this->commands) + { + proxy->initialize(&plugin_data); + } + + for (ConVarProxy* proxy : this->variables) + { + proxy->initialize(&plugin_data); + } + + this->engine = GetModuleByName("engine.dll"); + if (this->engine) + { + // Offsets by Northstar + // https://github.com/R2Northstar/NorthstarLauncher/blob/0cbdd5672815f956e6b2d2de48d596e87514a07b/NorthstarDLL/engine/r2engine.cpp#L29 + + this->engine_funcs.Cbuf_GetCurrentPlayer = reinterpret_cast<Cbuf_GetCurrentPlayerType>(((uintptr_t)this->engine) + 0x120630); + this->engine_funcs.Cbuf_AddText = reinterpret_cast<Cbuf_AddTextType>(((uintptr_t)this->engine) + 0x1203B0); + this->engine_funcs.Cbuf_Execute = reinterpret_cast<Cbuf_ExecuteType>(((uintptr_t)this->engine) + 0x1204B0); + } +} + +void Plugin::LoadSQVMFunctions(ScriptContext context, SquirrelFunctions* funcs) +{ + switch (context) + { + case ScriptContext::CLIENT: + this->client_sqvm_funcs = *funcs; + break; - spdlog::info("Engine data loaded"); + case ScriptContext::SERVER: + this->client_sqvm_funcs = *funcs; + break; - this->RegisterConCommand("south_test", [](const CCommand& command){ spdlog::info("Gaming"); }, "", 0); + case ScriptContext::INVALID: + default: + spdlog::warn("Received invalid script context"); + break; + } } -void Plugin::RegisterConCommand(const char* name, FnCommandCallback_t callback, const char* helpString, int flags) +void Plugin::LoadSQVM(ScriptContext context, CSquirrelVM* sqvm) { - if (!this->engine_data) - { - return; - } + switch (context) + { + case ScriptContext::CLIENT: + this->client_vm = *sqvm; + break; + + case ScriptContext::SERVER: + this->server_vm = *sqvm; + break; - spdlog::info("Registering ConCommand {}", name); + case ScriptContext::UI: + this->ui_vm = *sqvm; + break; - extern_CreateObjectFunc createObject = static_cast<extern_CreateObjectFunc>(this->funcs->createObject); + case ScriptContext::INVALID: + default: + spdlog::warn("Received invalid script context"); + break; + } +} + +void Plugin::RemoveSQVM(ScriptContext context) +{ + switch (context) + { + case ScriptContext::CLIENT: + memset(&this->client_vm, 0, sizeof(CSquirrelVM)); + break; - spdlog::info("Creating Object"); - void* command = createObject(ObjectType::CONCOMMANDS); + case ScriptContext::SERVER: + memset(&this->server_vm, 0, sizeof(CSquirrelVM)); + break; + + case ScriptContext::UI: + memset(&this->ui_vm, 0, sizeof(CSquirrelVM)); + break; + + case ScriptContext::INVALID: + default: + spdlog::warn("Received invalid script context"); + break; + } +} + +void Plugin::StartServer() +{ + server->start(); +} + +void Plugin::RunCommand(const char* cmd) +{ + this->engine_funcs.Cbuf_AddText(this->engine_funcs.Cbuf_GetCurrentPlayer(), cmd, cmd_source_t::kCommandSrcCode); + this->engine_funcs.Cbuf_Execute(); +} + +SQRESULT Plugin::RunSquirrelCode(ScriptContext context, std::string code, SQObject* ret_val) +{ + CSquirrelVM* sqvm = nullptr; + SquirrelFunctions* funcs = nullptr; + + switch (context) + { + case ScriptContext::CLIENT: + sqvm = &this->client_vm; + funcs = &this->client_sqvm_funcs; + break; + + case ScriptContext::SERVER: + sqvm = &this->server_vm; + funcs = &this->server_sqvm_funcs; + break; + + case ScriptContext::UI: + sqvm = &this->ui_vm; + funcs = &this->client_sqvm_funcs; + break; + + case ScriptContext::INVALID: + default: + spdlog::warn("Received invalid script context"); + return SQRESULT_ERROR; + } + + CompileBufferState bufferState = CompileBufferState(code); + HSquirrelVM* v = sqvm->sqvm; + + if (SQ_FAILED(funcs->__sq_compilebuffer(v, &bufferState, "rpc", -1, false))) + { + spdlog::info("Failed to compile buffer"); + return SQRESULT_ERROR; + } + + funcs->__sq_pushroottable(v); + + if (SQ_FAILED(funcs->__sq_call(v, 0 + 1, SQTrue, SQFalse))) + { + spdlog::info("Failed to execute closure"); + return SQRESULT_ERROR; + } + + *ret_val = v->_stack[v->_top - 1]; + + if (ISREFCOUNTED(ret_val->_Type)) + { + // pray + ret_val->_VAL.asString->uiRef++; + } + + v->_stack[--(v->_top)] = SQ_NULL_OBJ; + + if (ret_val->_Type == OT_NULL) + return SQRESULT_NULL; + + return SQRESULT_NOTNULL; +} + +ConCommandProxy* Plugin::ConCommand(const char* name, FnCommandCallback_t callback, const char* helpString, int flags, void* parent) +{ + ConCommandProxy* proxy = new ConCommandProxy(name, callback, helpString, flags, parent); + + return this->commands.emplace_back(proxy); +} + +ConVarProxy* Plugin::ConVar(const char* pszName, const char* pszDefaultValue, int nFlags, const char* pszHelpString, bool bMin, float fMin, bool bMax, float fMax, FnChangeCallback_t pCallback) +{ + ConVarProxy* proxy = new ConVarProxy(pszName, pszDefaultValue, nFlags, pszHelpString, bMin, fMin, bMax, fMax, pCallback); - spdlog::info("Constructing Command"); - this->engine_data->ConCommandConstructor((ConCommand*)command, name, callback, helpString, flags, nullptr); + return this->variables.emplace_back(proxy); } diff --git a/src/plugin.h b/src/plugin.h index 2af5f30..2d3d2d9 100644 --- a/src/plugin.h +++ b/src/plugin.h @@ -1,33 +1,57 @@ #ifndef PLUGIN_H #define PLUGIN_H -#include "ns_plugin.h" +#include <vector> -class jsonrpc_server; +#include "ns_plugin.h" +#include "internal/concommandproxy.h" +#include "internal/convarproxy.h" -struct EngineData -{ - ConCommandConstructorType ConCommandConstructor; - ConVarMallocType conVarMalloc; - ConVarRegisterType conVarRegister; - void* ConVar_Vtable; - void* IConVar_Vtable; -}; +class rpc_server; class Plugin { private: - PluginInitFuncs* funcs = nullptr; - PluginNorthstarData* data = nullptr; - EngineData* engine_data = nullptr; + PluginInitFuncs funcs = { 0 }; + PluginNorthstarData data = { 0 }; + EngineData engine_data = { 0 }; + + SquirrelFunctions client_sqvm_funcs = { 0 }; + SquirrelFunctions server_sqvm_funcs = { 0 }; + + CSquirrelVM client_vm = { 0 }; + CSquirrelVM server_vm = { 0 }; + CSquirrelVM ui_vm = { 0 }; // uses same functions as client - jsonrpc_server* server; + HMODULE engine = nullptr; + struct { + Cbuf_GetCurrentPlayerType Cbuf_GetCurrentPlayer; + Cbuf_AddTextType Cbuf_AddText; + Cbuf_ExecuteType Cbuf_Execute; + } engine_funcs = { 0 }; + + rpc_server* server = nullptr; + + std::vector<ConCommandProxy*> commands; + std::vector<ConVarProxy*> variables; + + HMODULE GetModuleByName(const char* name); public: Plugin(PluginInitFuncs* funcs, PluginNorthstarData* data); ~Plugin(); void LoadEngineData(void* data); - void RegisterConCommand(const char* name, FnCommandCallback_t callback, const char* helpString, int flags); + void LoadSQVMFunctions(ScriptContext context, SquirrelFunctions* funcs); + void LoadSQVM(ScriptContext context, CSquirrelVM* sqvm); + void RemoveSQVM(ScriptContext context); + + void StartServer(); + void RunCommand(const char* cmd); + SQRESULT RunSquirrelCode(ScriptContext context, std::string code, SQObject* ret_val); + + // Wraps around the internals we receive + ConCommandProxy* ConCommand(const char* name, FnCommandCallback_t callback, const char* helpString, int flags, void* parent = nullptr); + ConVarProxy* ConVar(const char* pszName, const char* pszDefaultValue, int nFlags, const char* pszHelpString, bool bMin = 0, float fMin = 0, bool bMax = 0, float fMax = 0, FnChangeCallback_t pCallback = nullptr); }; #endif
\ No newline at end of file diff --git a/src/server.cpp b/src/server.cpp index b9f7b74..199c2b9 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -3,16 +3,374 @@ #include <spdlog/spdlog.h> +#include <rapidjson/document.h> +#include <rapidjson/writer.h> +#include <rapidjson/stringbuffer.h> + #include "server.h" #include "plugin.h" -jsonrpc_server::jsonrpc_server(Plugin* plugin) +ConVar* Cvar_southrpc_port; + +void ConCommand_southrpc_status(const CCommand& args) +{ + spdlog::info("Yes"); +} + +rpc_server::rpc_server(Plugin* plugin) + : parent(plugin) { - this->parent = plugin; - spdlog::info("jsonrpc_server::jsonrpc_server()"); + plugin->ConCommand("southrpc_status", ConCommand_southrpc_status, "", 0); + + this->Convar_Port = plugin->ConVar("southrpc_port", DEFAULT_PORT, FCVAR_ARCHIVE, "South RPC HTTP Port (requires restart, default: " DEFAULT_PORT ")"); + + if (WSAStartup(MAKEWORD(2, 2), &this->wsaData) != 0) { + spdlog::error("Failed to open Windows Socket"); + return; + } + + if (LOBYTE(this->wsaData.wVersion) != 2 || + HIBYTE(this->wsaData.wVersion) != 2) + { + spdlog::error("Incorrect Winsock version loaded"); + WSACleanup(); + return; + } + + initialized = true; +} + +rpc_server::~rpc_server() +{ + this->stop(); +} + +static DWORD WINAPI static_server_run(void* param) +{ + rpc_server* server = (rpc_server*)param; + + return server->run(); +} + +void rpc_server::start() +{ + if (!initialized) + { + return; + } + + this->running = true; + + DWORD thread_id; + this->thread = CreateThread(NULL, 0, static_server_run, (void*)this, 0, &thread_id); } -jsonrpc_server::~jsonrpc_server() +void rpc_server::stop() { - spdlog::info("jsonrpc_server::~jsonrpc_server()"); -}
\ No newline at end of file + if (!this->thread) + return; + + this->running = false; + this->thread = nullptr; +} + +DWORD rpc_server::run() +{ + // Waiting for engine to init so we can actually use convar values from the archive + Sleep(SLEEP_DURATION); + + int port = this->Convar_Port->GetInt(); + + struct sockaddr_in local = { 0 }; + local.sin_family = AF_INET; + local.sin_addr.s_addr = INADDR_ANY; + local.sin_port = htons(port); + + SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == INVALID_SOCKET) + { + spdlog::error("Failed to create socket"); + goto run_error; + } + + if (bind(sock, (struct sockaddr*)&local, sizeof(local)) == SOCKET_ERROR) + { + spdlog::error("Failed to bindsocket"); + goto run_error; + } + + if (listen(sock, 10) == SOCKET_ERROR) + { + spdlog::error("Failed to listen to socket"); + goto run_error; + } + + spdlog::info("Running under {}", port); + + SOCKET msg; + struct sockaddr_in addr; + int addr_len; + + char buf[REQUEST_SIZE]; + char http_method[10]; + char protocol[10]; + + this->running = true; + while (this->running) + { + addr_len = sizeof(addr); + msg = accept(sock, (struct sockaddr*)&addr, &addr_len); + if (msg == INVALID_SOCKET || msg == -1) + { + spdlog::error("Received invalid request ({})", WSAGetLastError()); + continue; + }; + + spdlog::info("Connection opened by {}", inet_ntoa(addr.sin_addr)); + + memset(buf, 0, sizeof(buf)); + if (recv(msg, buf, sizeof(buf), 0) == SOCKET_ERROR) + { + spdlog::error("Received no data ({})", WSAGetLastError()); + closesocket(msg); + continue; + } + + buf[REQUEST_SIZE - 1] = '\0'; + + memset(http_method, 0, sizeof(http_method)); + if (sscanf(buf, "%s /rpc %s" HTTP_LF, &http_method, &protocol) < 2) + { + spdlog::error("Request is not to the correct endpoint"); + send(msg, RESP_INVALID_ENDPOINT, strlen(RESP_INVALID_ENDPOINT), 0); + closesocket(msg); + continue; + } + + if (strncmp(http_method, METHOD_POST, strlen(METHOD_POST)+1)) + { + spdlog::error("Request is not a " METHOD_POST " request"); + send(msg, RESP_INVALID_METHOD, strlen(RESP_INVALID_METHOD), 0); + closesocket(msg); + continue; + } + + char* body = nullptr; + char* ptr = buf; + while (*ptr) + { + if (!memcmp(ptr, BODY_SEP, 4)) + { + body = ptr + 4; + break; + } + ++ptr; + } + + if (!body || !*body) + { + spdlog::error("No request body found"); + send(msg, RESP_INVALID_RPC, strlen(RESP_INVALID_RPC), 0); + closesocket(msg); + continue; + } + + rapidjson::Document json_body; + json_body.Parse(body); + + if (json_body.HasParseError()) + { + spdlog::error("Failed to parse request"); + send(msg, RESP_FAIL_PARSE, strlen(RESP_FAIL_PARSE), 0); + } + else if (!json_body.IsObject() || + !(json_body.HasMember("jsonrpc") && json_body["jsonrpc"] == "2.0") || + !(json_body.HasMember("id") && (json_body["id"].IsInt() || json_body["id"].IsNull())) || + !(json_body.HasMember("method") && json_body["method"].IsString()) || + (json_body.HasMember("params") && !(json_body["params"].IsObject() ))) + { + spdlog::error("Request is not valid JSON-RPC 2.0"); + send(msg, RESP_INVALID_RPC, strlen(RESP_INVALID_RPC), 0); + } + else + { + const char* method = json_body["method"].GetString(); + auto params = json_body["params"].GetObject(); + + spdlog::info("Received request for method \"{}\"", method); + + if (!strcmp(method, "execute_command")) + { + if (!params.HasMember("command")) + { + send(msg, RESP_RPC_MISSING_PARAM, strlen(RESP_RPC_MISSING_PARAM), 0); + closesocket(msg); + continue; + } + + const char* cmd = params["command"].GetString(); + this->parent->RunCommand(cmd); + + send(msg, RESP_JSON, strlen(RESP_JSON), 0); + if (!json_body["id"].IsNull()) + { + rapidjson::Document doc; + rapidjson::MemoryPoolAllocator<>& allocator = doc.GetAllocator(); + doc.SetObject(); + + rapidjson::Value result; + + doc.AddMember("jsonrpc", "2.0", allocator); + doc.AddMember("id", json_body["id"], allocator); + doc.AddMember("result", result, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + + doc.Accept(writer); + + const char* json_str = buffer.GetString(); + + send(msg, json_str, strlen(json_str), 0); + } + } + else if (!strcmp(method, "execute_squirrel")) + { + if (!params.HasMember("code")) + { + send(msg, RESP_RPC_MISSING_PARAM, strlen(RESP_RPC_MISSING_PARAM), 0); + closesocket(msg); + continue; + } + + const char* code = params["code"].GetString(); + ScriptContext context = ScriptContext::UI; + + if (params.HasMember("context")) + { + if (params["context"] == "client") + { + context = ScriptContext::CLIENT; + } + else if (params["context"] == "server") + { + context = ScriptContext::SERVER; + } + else if (params["context"] == "ui") + { + context = ScriptContext::UI; + } + else + { + send(msg, RESP_SQUIRREL_INVALID_CONTEXT, strlen(RESP_SQUIRREL_INVALID_CONTEXT), 0); + closesocket(msg); + continue; + } + } + SQObject obj_ptr; + + this->parent->RunSquirrelCode(context, code, &obj_ptr); + + send(msg, RESP_JSON, strlen(RESP_JSON), 0); + + if (!json_body["id"].IsNull()) + { + rapidjson::Document doc; + rapidjson::MemoryPoolAllocator<>& allocator = doc.GetAllocator(); + doc.SetObject(); + + rapidjson::Value result; + SquirrelToJSON(&result, allocator, &obj_ptr); + + doc.AddMember("jsonrpc", "2.0", allocator); + doc.AddMember("id", json_body["id"], allocator); + doc.AddMember("result", result, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + + doc.Accept(writer); + + const char* json_str = buffer.GetString(); + + send(msg, json_str, strlen(json_str), 0); + } + + if (ISREFCOUNTED(obj_ptr._Type)) + { + // pray + obj_ptr._VAL.asString->uiRef--; + } + } + else + { + send(msg, RESP_RPC_INVALID_METHOD, strlen(RESP_RPC_INVALID_METHOD), 0); + } + + } + + closesocket(msg); + } + +run_error: + this->running = false; + + return 0; +} + +void rpc_server::SquirrelToJSON( + rapidjson::Value* out_val, + rapidjson::MemoryPoolAllocator<>& allocator, + SQObject* obj_ptr +) { + + if (obj_ptr) + { + switch (obj_ptr->_Type) + { + case OT_BOOL: + out_val->SetBool(obj_ptr->_VAL.asInteger); + break; + + case OT_INTEGER: + out_val->SetInt(obj_ptr->_VAL.asInteger); + break; + + case OT_STRING: + out_val->SetString(obj_ptr->_VAL.asString->_val, obj_ptr->_VAL.asString->length); + break; + + case OT_ARRAY: + out_val->SetArray(); + + for (int i = 0; i < obj_ptr->_VAL.asArray->_usedSlots; ++i) + { + rapidjson::Value n; + SquirrelToJSON(&n, allocator, &obj_ptr->_VAL.asArray->_values[i]); + out_val->PushBack(n, allocator); + } + break; + + case OT_TABLE: + out_val->SetObject(); + + for (int i = 0; i < obj_ptr->_VAL.asTable->_numOfNodes; ++i) + { + auto node = &obj_ptr->_VAL.asTable->_nodes[i]; + if (node->key._Type != OT_STRING) + continue; + + rapidjson::Value k; + SquirrelToJSON(&k, allocator, &node->key); + + rapidjson::Value v; + SquirrelToJSON(&v, allocator, &node->val); + out_val->AddMember(k, v, allocator); + } + break; + + default: + break; + } + } +} diff --git a/src/server.h b/src/server.h index a3414f6..d45ef15 100644 --- a/src/server.h +++ b/src/server.h @@ -4,26 +4,71 @@ #include <winsock2.h> #include <ws2tcpip.h> -#define RPC_PORT 26503 -#define MAX_CONNECTIONS 5 +#include "rapidjson/error/en.h" +#include "rapidjson/document.h" +#include "rapidjson/writer.h" +#include "rapidjson/allocators.h" -class Plugin; +#include "ns_plugin.h" +#include "internal/convarproxy.h" -struct thread_info -{ - HANDLE thread_handle; - SOCKET socket_fd; -}; +#define SLEEP_DURATION 5000 +#define DEFAULT_PORT "26503" + +#define METHOD_POST "POST" + +#define REQUEST_SIZE 4096 +#define HTTP_LF "\r\n" +#define BODY_SEP HTTP_LF HTTP_LF +#define RESP(STATUS, TYPE, BODY) "HTTP/1.1 " STATUS HTTP_LF "Content-Type: " TYPE BODY_SEP BODY + +#define RESP_200(TYPE, BODY) RESP("200 OK", TYPE, BODY) +#define RESP_400(TYPE, BODY) RESP("400 Bad Request", TYPE, BODY) +#define RESP_404(TYPE, BODY) RESP("404 Not Found", TYPE, BODY) + +#define RESP_OK RESP_200("text/plain", "") +#define RESP_JSON RESP_200("application/json", "") +#define RESP_FAIL_PARSE RESP_400("text/plain", "Failed to parse request") + +#define RESP_RPC_PARAMS_ARR RESP_400("text/plain", "SouthRPC cannot handle parameters in an array") +#define RESP_RPC_INVALID_METHOD RESP_400("text/plain", "Invalid RPC Method") +#define RESP_RPC_MISSING_PARAM RESP_400("text/plain", "Missing RPC Parameter") + +#define RESP_INVALID_RPC RESP_400("text/plain", "Request is invalid JSON-RPC 2.0") +#define RESP_INVALID_METHOD RESP_400("text/plain", "Invalid HTTP Method") +#define RESP_INVALID_ENDPOINT RESP_404("text/plain", "Invalid Endpoint") -class jsonrpc_server { +#define RESP_SQUIRREL_ERROR RESP_400("text/plain", "Failed to execute squirrel code") +#define RESP_SQUIRREL_INVALID_CONTEXT RESP_400("text/plain", "Invalid Squirrel Context") + + +class Plugin; + +class rpc_server { private: Plugin* parent; - struct thread_info threads[MAX_CONNECTIONS] = {0}; + bool initialized = false; + + WSADATA wsaData; + ConVarProxy* Convar_Port = nullptr; + ConVarProxy* Convar_Connections = nullptr; + + bool running = false; + HANDLE thread = nullptr; + + void SquirrelToJSON( + rapidjson::Value* out_val, + rapidjson::MemoryPoolAllocator<>& allocator, + SQObject* obj_ptr + ); public: - jsonrpc_server(Plugin* plugin); - ~jsonrpc_server(); - + rpc_server(Plugin* plugin); + ~rpc_server(); + + void start(); + void stop(); + DWORD run(); }; #endif
\ No newline at end of file |