aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan200101 <sentrycraft123@gmail.com>2023-09-24 12:43:27 +0200
committerJan200101 <sentrycraft123@gmail.com>2023-09-24 12:43:27 +0200
commitb92f3d1d6ad49881eb31ac911a8ad368743af108 (patch)
tree60d8ad809d19a5c392349f4dd38e4b818796150b
parent13df46ba41efe2d94e3dbec240ff0889ac054eb7 (diff)
downloadSouthRPC-b92f3d1d6ad49881eb31ac911a8ad368743af108.tar.gz
SouthRPC-b92f3d1d6ad49881eb31ac911a8ad368743af108.zip
implement RPC
-rw-r--r--.gitignore1
-rw-r--r--cmake/FindRapidJSON.cmake16
-rw-r--r--cmake/Findjson-c.cmake99
-rw-r--r--src/CMakeLists.txt6
-rw-r--r--src/init.cpp44
-rw-r--r--src/internal/concommandproxy.h48
-rw-r--r--src/internal/convarproxy.h87
-rw-r--r--src/internal/logging.h5
-rw-r--r--src/internal/proxy.h15
-rw-r--r--src/internal/types.h38
-rw-r--r--src/ns_plugin.h69
-rw-r--r--src/plugin.cpp239
-rw-r--r--src/plugin.h54
-rw-r--r--src/server.cpp370
-rw-r--r--src/server.h71
15 files changed, 995 insertions, 167 deletions
diff --git a/.gitignore b/.gitignore
index 378eac2..268336e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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