aboutsummaryrefslogtreecommitdiff
path: root/NorthstarDLL/scripts
diff options
context:
space:
mode:
authorBobTheBob9 <for.oliver.kirkham@gmail.com>2023-01-04 18:08:13 +0000
committerBobTheBob9 <for.oliver.kirkham@gmail.com>2023-01-04 18:08:13 +0000
commite9f8a34dc3ca736f307cf195390aee7c8f5cb456 (patch)
treee65d1c2b1888895baf65bbde73e0a1a02cbb091a /NorthstarDLL/scripts
parent6c45c7e94634c340019d12615b7eae8f3c0f49b3 (diff)
parent4fb1ae07d8f078e7abde99900c6a8286148a380a (diff)
downloadNorthstarLauncher-e9f8a34dc3ca736f307cf195390aee7c8f5cb456.tar.gz
NorthstarLauncher-e9f8a34dc3ca736f307cf195390aee7c8f5cb456.zip
Merge remote-tracking branch 'origin/main' into experimental-bots-pr
Diffstat (limited to 'NorthstarDLL/scripts')
-rw-r--r--NorthstarDLL/scripts/client/clientchathooks.cpp70
-rw-r--r--NorthstarDLL/scripts/client/scriptbrowserhooks.cpp25
-rw-r--r--NorthstarDLL/scripts/client/scriptmainmenupromos.cpp124
-rw-r--r--NorthstarDLL/scripts/client/scriptmodmenu.cpp166
-rw-r--r--NorthstarDLL/scripts/client/scriptserverbrowser.cpp410
-rw-r--r--NorthstarDLL/scripts/client/scriptservertoclientstringcommand.cpp19
-rw-r--r--NorthstarDLL/scripts/scriptdatatables.cpp910
-rw-r--r--NorthstarDLL/scripts/scripthttprequesthandler.cpp586
-rw-r--r--NorthstarDLL/scripts/scripthttprequesthandler.h132
-rw-r--r--NorthstarDLL/scripts/scriptjson.cpp249
-rw-r--r--NorthstarDLL/scripts/scriptutility.cpp29
-rw-r--r--NorthstarDLL/scripts/server/miscserverfixes.cpp7
-rw-r--r--NorthstarDLL/scripts/server/miscserverscript.cpp101
-rw-r--r--NorthstarDLL/scripts/server/scriptuserinfo.cpp105
14 files changed, 2933 insertions, 0 deletions
diff --git a/NorthstarDLL/scripts/client/clientchathooks.cpp b/NorthstarDLL/scripts/client/clientchathooks.cpp
new file mode 100644
index 00000000..0fc68302
--- /dev/null
+++ b/NorthstarDLL/scripts/client/clientchathooks.cpp
@@ -0,0 +1,70 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+
+#include "server/serverchathooks.h"
+#include "client/localchatwriter.h"
+
+#include <rapidjson/document.h>
+
+AUTOHOOK_INIT()
+
+// clang-format off
+AUTOHOOK(CHudChat__AddGameLine, client.dll + 0x22E580,
+void, __fastcall, (void* self, const char* message, int inboxId, bool isTeam, bool isDead))
+// clang-format on
+{
+ // This hook is called for each HUD, but we only want our logic to run once.
+ if (self != *CHudChat::allHuds)
+ return;
+
+ int senderId = inboxId & CUSTOM_MESSAGE_INDEX_MASK;
+ bool isAnonymous = senderId == 0;
+ bool isCustom = isAnonymous || (inboxId & CUSTOM_MESSAGE_INDEX_BIT);
+
+ // Type is set to 0 for non-custom messages, custom messages have a type encoded as the first byte
+ int type = 0;
+ const char* payload = message;
+ if (isCustom)
+ {
+ type = message[0];
+ payload = message + 1;
+ }
+
+ SQRESULT result = g_pSquirrel<ScriptContext::CLIENT>->Call(
+ "CHudChat_ProcessMessageStartThread", static_cast<int>(senderId) - 1, payload, isTeam, isDead, type);
+ if (result == SQRESULT_ERROR)
+ for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next)
+ CHudChat__AddGameLine(hud, message, inboxId, isTeam, isDead);
+}
+
+ADD_SQFUNC("void", NSChatWrite, "int context, string text", "", ScriptContext::CLIENT)
+{
+ int chatContext = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 1);
+ const char* str = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 2);
+
+ LocalChatWriter((LocalChatWriter::Context)chatContext).Write(str);
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("void", NSChatWriteRaw, "int context, string text", "", ScriptContext::CLIENT)
+{
+ int chatContext = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 1);
+ const char* str = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 2);
+
+ LocalChatWriter((LocalChatWriter::Context)chatContext).InsertText(str);
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("void", NSChatWriteLine, "int context, string text", "", ScriptContext::CLIENT)
+{
+ int chatContext = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 1);
+ const char* str = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 2);
+
+ LocalChatWriter((LocalChatWriter::Context)chatContext).WriteLine(str);
+ return SQRESULT_NULL;
+}
+
+ON_DLL_LOAD_CLIENT("client.dll", ClientChatHooks, (CModule module))
+{
+ AUTOHOOK_DISPATCH()
+}
diff --git a/NorthstarDLL/scripts/client/scriptbrowserhooks.cpp b/NorthstarDLL/scripts/client/scriptbrowserhooks.cpp
new file mode 100644
index 00000000..df4014de
--- /dev/null
+++ b/NorthstarDLL/scripts/client/scriptbrowserhooks.cpp
@@ -0,0 +1,25 @@
+#include "pch.h"
+
+AUTOHOOK_INIT()
+
+bool* bIsOriginOverlayEnabled;
+
+// clang-format off
+AUTOHOOK(OpenExternalWebBrowser, engine.dll + 0x184E40,
+void, __fastcall, (char* pUrl, char flags))
+// clang-format on
+{
+ bool bIsOriginOverlayEnabledOriginal = *bIsOriginOverlayEnabled;
+ if (flags & 2 && !strncmp(pUrl, "http", 4)) // custom force external browser flag
+ *bIsOriginOverlayEnabled = false; // if this bool is false, game will use an external browser rather than the origin overlay one
+
+ OpenExternalWebBrowser(pUrl, flags);
+ *bIsOriginOverlayEnabled = bIsOriginOverlayEnabledOriginal;
+}
+
+ON_DLL_LOAD_CLIENT("engine.dll", ScriptExternalBrowserHooks, (CModule module))
+{
+ AUTOHOOK_DISPATCH()
+
+ bIsOriginOverlayEnabled = module.Offset(0x13978255).As<bool*>();
+}
diff --git a/NorthstarDLL/scripts/client/scriptmainmenupromos.cpp b/NorthstarDLL/scripts/client/scriptmainmenupromos.cpp
new file mode 100644
index 00000000..0ea167f8
--- /dev/null
+++ b/NorthstarDLL/scripts/client/scriptmainmenupromos.cpp
@@ -0,0 +1,124 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+#include "masterserver/masterserver.h"
+
+// mirror this in script
+enum eMainMenuPromoDataProperty
+{
+ newInfoTitle1,
+ newInfoTitle2,
+ newInfoTitle3,
+
+ largeButtonTitle,
+ largeButtonText,
+ largeButtonUrl,
+ largeButtonImageIndex,
+
+ smallButton1Title,
+ smallButton1Url,
+ smallButton1ImageIndex,
+
+ smallButton2Title,
+ smallButton2Url,
+ smallButton2ImageIndex
+};
+ADD_SQFUNC("void", NSRequestCustomMainMenuPromos, "", "", ScriptContext::UI)
+{
+ g_pMasterServerManager->RequestMainMenuPromos();
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("bool", NSHasCustomMainMenuPromoData, "", "", ScriptContext::UI)
+{
+ g_pSquirrel<ScriptContext::UI>->pushbool(sqvm, g_pMasterServerManager->m_bHasMainMenuPromoData);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("var", NSGetCustomMainMenuPromoData, "int promoDataKey", "", ScriptContext::UI)
+{
+ if (!g_pMasterServerManager->m_bHasMainMenuPromoData)
+ return SQRESULT_NULL;
+
+ switch (g_pSquirrel<ScriptContext::UI>->getinteger(sqvm, 1))
+ {
+ case eMainMenuPromoDataProperty::newInfoTitle1:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle1.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::newInfoTitle2:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle2.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::newInfoTitle3:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle3.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::largeButtonTitle:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonTitle.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::largeButtonText:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonText.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::largeButtonUrl:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonUrl.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::largeButtonImageIndex:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonImageIndex);
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::smallButton1Title:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1Title.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::smallButton1Url:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1Url.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::smallButton1ImageIndex:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1ImageIndex);
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::smallButton2Title:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2Title.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::smallButton2Url:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2Url.c_str());
+ break;
+ }
+
+ case eMainMenuPromoDataProperty::smallButton2ImageIndex:
+ {
+ g_pSquirrel<ScriptContext::UI>->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2ImageIndex);
+ break;
+ }
+ }
+
+ return SQRESULT_NOTNULL;
+}
diff --git a/NorthstarDLL/scripts/client/scriptmodmenu.cpp b/NorthstarDLL/scripts/client/scriptmodmenu.cpp
new file mode 100644
index 00000000..75d05acc
--- /dev/null
+++ b/NorthstarDLL/scripts/client/scriptmodmenu.cpp
@@ -0,0 +1,166 @@
+#include "pch.h"
+#include "mods/modmanager.h"
+#include "squirrel/squirrel.h"
+
+ADD_SQFUNC("array<string>", NSGetModNames, "", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ g_pSquirrel<context>->newarray(sqvm, 0);
+
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ g_pSquirrel<context>->pushstring(sqvm, mod.Name.c_str());
+ g_pSquirrel<context>->arrayappend(sqvm, -2);
+ }
+
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("bool", NSIsModEnabled, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
+
+ // manual lookup, not super performant but eh not a big deal
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.Name.compare(modName))
+ {
+ g_pSquirrel<context>->pushbool(sqvm, mod.m_bEnabled);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("void", NSSetModEnabled, "string modName, bool enabled", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
+ const SQBool enabled = g_pSquirrel<context>->getbool(sqvm, 2);
+
+ // manual lookup, not super performant but eh not a big deal
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.Name.compare(modName))
+ {
+ mod.m_bEnabled = enabled;
+ return SQRESULT_NULL;
+ }
+ }
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("string", NSGetModDescriptionByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
+
+ // manual lookup, not super performant but eh not a big deal
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.Name.compare(modName))
+ {
+ g_pSquirrel<context>->pushstring(sqvm, mod.Description.c_str());
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("string", NSGetModVersionByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
+
+ // manual lookup, not super performant but eh not a big deal
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.Name.compare(modName))
+ {
+ g_pSquirrel<context>->pushstring(sqvm, mod.Version.c_str());
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("string", NSGetModDownloadLinkByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
+
+ // manual lookup, not super performant but eh not a big deal
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.Name.compare(modName))
+ {
+ g_pSquirrel<context>->pushstring(sqvm, mod.DownloadLink.c_str());
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("int", NSGetModLoadPriority, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
+
+ // manual lookup, not super performant but eh not a big deal
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.Name.compare(modName))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, mod.LoadPriority);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("bool", NSIsModRequiredOnClient, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
+
+ // manual lookup, not super performant but eh not a big deal
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.Name.compare(modName))
+ {
+ g_pSquirrel<context>->pushbool(sqvm, mod.RequiredOnClient);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC(
+ "array<string>", NSGetModConvarsByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
+{
+ const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
+ g_pSquirrel<context>->newarray(sqvm, 0);
+
+ // manual lookup, not super performant but eh not a big deal
+ for (Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.Name.compare(modName))
+ {
+ for (ModConVar* cvar : mod.ConVars)
+ {
+ g_pSquirrel<context>->pushstring(sqvm, cvar->Name.c_str());
+ g_pSquirrel<context>->arrayappend(sqvm, -2);
+ }
+
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ return SQRESULT_NOTNULL; // return empty array
+}
+
+ADD_SQFUNC("void", NSReloadMods, "", "", ScriptContext::UI)
+{
+ g_pModManager->LoadMods();
+ return SQRESULT_NULL;
+}
diff --git a/NorthstarDLL/scripts/client/scriptserverbrowser.cpp b/NorthstarDLL/scripts/client/scriptserverbrowser.cpp
new file mode 100644
index 00000000..5f1287ad
--- /dev/null
+++ b/NorthstarDLL/scripts/client/scriptserverbrowser.cpp
@@ -0,0 +1,410 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+#include "masterserver/masterserver.h"
+#include "server/auth/serverauthentication.h"
+#include "engine/r2engine.h"
+#include "client/r2client.h"
+
+// functions for viewing server browser
+
+ADD_SQFUNC("bool", NSIsMasterServerAuthenticated, "", "", ScriptContext::UI)
+{
+ g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_bOriginAuthWithMasterServerDone);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("void", NSRequestServerList, "", "", ScriptContext::UI)
+{
+ g_pMasterServerManager->RequestServerList();
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("bool", NSIsRequestingServerList, "", "", ScriptContext::UI)
+{
+ g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_bScriptRequestingServerList);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("bool", NSMasterServerConnectionSuccessful, "", "", ScriptContext::UI)
+{
+ g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_bSuccessfullyConnected);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("int", NSGetServerCount, "", "", ScriptContext::UI)
+{
+ g_pSquirrel<context>->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers.size());
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("string", NSGetServerName, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get name of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].name);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("string", NSGetServerDescription, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get description of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].description.c_str());
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("string", NSGetServerMap, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get map of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].map);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("string", NSGetServerPlaylist, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get playlist of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].playlist);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("int", NSGetServerPlayerCount, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get playercount of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].playerCount);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("int", NSGetServerMaxPlayerCount, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get max playercount of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].maxPlayers);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("string", NSGetServerID, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get id of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].id);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("bool", NSServerRequiresPassword, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get hasPassword of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiresPassword);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("int", NSGetServerRequiredModsCount, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get required mods count of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size());
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("string", NSGetServerRegion, "int serverIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get region of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].region, -1);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("string", NSGetServerRequiredModName, "int serverIndex, int modIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+ SQInteger modIndex = g_pSquirrel<context>->getinteger(sqvm, 2);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get hasPassword of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ if (modIndex >= g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get required mod name of mod index {} when only {} mod are available",
+ modIndex,
+ g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods[modIndex].Name.c_str());
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("string", NSGetServerRequiredModVersion, "int serverIndex, int modIndex", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+ SQInteger modIndex = g_pSquirrel<context>->getinteger(sqvm, 2);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get required mod version of server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ if (modIndex >= g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to get required mod version of mod index {} when only {} mod are available",
+ modIndex,
+ g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods[modIndex].Version.c_str());
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("void", NSClearRecievedServerList, "", "", ScriptContext::UI)
+{
+ g_pMasterServerManager->ClearServerList();
+ return SQRESULT_NULL;
+}
+
+// functions for authenticating with servers
+
+ADD_SQFUNC("void", NSTryAuthWithServer, "int serverIndex, string password = ''", "", ScriptContext::UI)
+{
+ SQInteger serverIndex = g_pSquirrel<context>->getinteger(sqvm, 1);
+ const SQChar* password = g_pSquirrel<context>->getstring(sqvm, 2);
+
+ if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "Tried to auth with server index {} when only {} servers are available",
+ serverIndex,
+ g_pMasterServerManager->m_vRemoteServers.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ // send off persistent data first, don't worry about server/client stuff, since m_additionalPlayerData should only have entries when
+ // we're a local server note: this seems like it could create a race condition, test later
+ for (auto& pair : g_pServerAuthentication->m_PlayerAuthenticationData)
+ g_pServerAuthentication->WritePersistentData(pair.first);
+
+ // do auth
+ g_pMasterServerManager->AuthenticateWithServer(
+ R2::g_pLocalPlayerUserID,
+ g_pMasterServerManager->m_sOwnClientAuthToken,
+ g_pMasterServerManager->m_vRemoteServers[serverIndex].id,
+ (char*)password);
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("bool", NSIsAuthenticatingWithServer, "", "", ScriptContext::UI)
+{
+ g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_bScriptAuthenticatingWithGameServer);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("bool", NSWasAuthSuccessful, "", "", ScriptContext::UI)
+{
+ g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_bSuccessfullyAuthenticatedWithGameServer);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("void", NSConnectToAuthedServer, "", "", ScriptContext::UI)
+{
+ if (!g_pMasterServerManager->m_bHasPendingConnectionInfo)
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm, fmt::format("Tried to connect to authed server before any pending connection info was available").c_str());
+ return SQRESULT_ERROR;
+ }
+
+ RemoteServerConnectionInfo& info = g_pMasterServerManager->m_pendingConnectionInfo;
+
+ // set auth token, then try to connect
+ // i'm honestly not entirely sure how silentconnect works regarding ports and encryption so using connect for now
+ R2::g_pCVar->FindVar("serverfilter")->SetValue(info.authToken);
+ R2::Cbuf_AddText(
+ R2::Cbuf_GetCurrentPlayer(),
+ fmt::format(
+ "connect {}.{}.{}.{}:{}",
+ info.ip.S_un.S_un_b.s_b1,
+ info.ip.S_un.S_un_b.s_b2,
+ info.ip.S_un.S_un_b.s_b3,
+ info.ip.S_un.S_un_b.s_b4,
+ info.port)
+ .c_str(),
+ R2::cmd_source_t::kCommandSrcCode);
+
+ g_pMasterServerManager->m_bHasPendingConnectionInfo = false;
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("void", NSTryAuthWithLocalServer, "", "", ScriptContext::UI)
+{
+ // do auth request
+ g_pMasterServerManager->AuthenticateWithOwnServer(R2::g_pLocalPlayerUserID, g_pMasterServerManager->m_sOwnClientAuthToken);
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("void", NSCompleteAuthWithLocalServer, "", "", ScriptContext::UI)
+{
+ // literally just set serverfilter
+ // note: this assumes we have no authdata other than our own
+ if (g_pServerAuthentication->m_RemoteAuthenticationData.size())
+ R2::g_pCVar->FindVar("serverfilter")->SetValue(g_pServerAuthentication->m_RemoteAuthenticationData.begin()->first.c_str());
+
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("string", NSGetAuthFailReason, "", "", ScriptContext::UI)
+{
+ g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_sAuthFailureReason.c_str(), -1);
+ return SQRESULT_NOTNULL;
+}
diff --git a/NorthstarDLL/scripts/client/scriptservertoclientstringcommand.cpp b/NorthstarDLL/scripts/client/scriptservertoclientstringcommand.cpp
new file mode 100644
index 00000000..f3cb2f18
--- /dev/null
+++ b/NorthstarDLL/scripts/client/scriptservertoclientstringcommand.cpp
@@ -0,0 +1,19 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+#include "core/convar/convar.h"
+#include "core/convar/concommand.h"
+
+void ConCommand_ns_script_servertoclientstringcommand(const CCommand& arg)
+{
+ if (g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM)
+ g_pSquirrel<ScriptContext::CLIENT>->Call("NSClientCodeCallback_RecievedServerToClientStringCommand", arg.ArgS());
+}
+
+ON_DLL_LOAD_CLIENT_RELIESON("client.dll", ScriptServerToClientStringCommand, ClientSquirrel, (CModule module))
+{
+ RegisterConCommand(
+ "ns_script_servertoclientstringcommand",
+ ConCommand_ns_script_servertoclientstringcommand,
+ "",
+ FCVAR_CLIENTDLL | FCVAR_SERVER_CAN_EXECUTE);
+}
diff --git a/NorthstarDLL/scripts/scriptdatatables.cpp b/NorthstarDLL/scripts/scriptdatatables.cpp
new file mode 100644
index 00000000..915d4df0
--- /dev/null
+++ b/NorthstarDLL/scripts/scriptdatatables.cpp
@@ -0,0 +1,910 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+#include "core/filesystem/rpakfilesystem.h"
+#include "core/convar/convar.h"
+#include "dedicated/dedicated.h"
+#include "core/filesystem/filesystem.h"
+#include "core/math/vector.h"
+#include "core/tier0.h"
+#include "engine/r2engine.h"
+#include <iostream>
+#include <sstream>
+#include <map>
+#include <fstream>
+#include <filesystem>
+
+const uint64_t USERDATA_TYPE_DATATABLE = 0xFFF7FFF700000004;
+const uint64_t USERDATA_TYPE_DATATABLE_CUSTOM = 0xFFFCFFFC12345678;
+
+enum class DatatableType : int
+{
+ BOOL = 0,
+ INT,
+ FLOAT,
+ VECTOR,
+ STRING,
+ ASSET,
+ UNK_STRING // unknown but deffo a string type
+};
+
+struct ColumnInfo
+{
+ char* name;
+ DatatableType type;
+ int offset;
+};
+
+struct Datatable
+{
+ int numColumns;
+ int numRows;
+ ColumnInfo* columnInfo;
+ char* data; // actually data pointer
+ int rowInfo;
+};
+
+ConVar* Cvar_ns_prefer_datatable_from_disk;
+
+template <ScriptContext context> Datatable* (*SQ_GetDatatableInternal)(HSquirrelVM* sqvm);
+
+struct CSVData
+{
+ std::string m_sAssetName;
+ std::string m_sCSVString;
+ char* m_pDataBuf;
+ size_t m_nDataBufSize;
+
+ std::vector<char*> columns;
+ std::vector<std::vector<char*>> dataPointers;
+};
+
+std::unordered_map<std::string, CSVData> CSVCache;
+
+Vector3 StringToVector(char* pString)
+{
+ Vector3 vRet;
+
+ int length = 0;
+ while (pString[length])
+ {
+ if ((pString[length] == '<') || (pString[length] == '>'))
+ pString[length] = '\0';
+ length++;
+ }
+
+ int startOfFloat = 1;
+ int currentIndex = 1;
+
+ while (pString[currentIndex] && (pString[currentIndex] != ','))
+ currentIndex++;
+ pString[currentIndex] = '\0';
+ vRet.x = std::stof(&pString[startOfFloat]);
+ startOfFloat = ++currentIndex;
+
+ while (pString[currentIndex] && (pString[currentIndex] != ','))
+ currentIndex++;
+ pString[currentIndex] = '\0';
+ vRet.y = std::stof(&pString[startOfFloat]);
+ startOfFloat = ++currentIndex;
+
+ while (pString[currentIndex] && (pString[currentIndex] != ','))
+ currentIndex++;
+ pString[currentIndex] = '\0';
+ vRet.z = std::stof(&pString[startOfFloat]);
+ startOfFloat = ++currentIndex;
+
+ return vRet;
+}
+
+// var function GetDataTable( asset path )
+REPLACE_SQFUNC(GetDataTable, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ const char* pAssetName;
+ g_pSquirrel<context>->getasset(sqvm, 2, &pAssetName);
+
+ if (strncmp(pAssetName, "datatable/", 10))
+ {
+ g_pSquirrel<context>->raiseerror(sqvm, fmt::format("Asset \"{}\" doesn't start with \"datatable/\"", pAssetName).c_str());
+ return SQRESULT_ERROR;
+ }
+ else if (!Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->LoadFile(pAssetName))
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTable"](sqvm);
+ // either we prefer disk datatables, or we're loading a datatable that wasn't found in rpak
+ else
+ {
+ std::string sAssetPath(fmt::format("scripts/{}", pAssetName));
+
+ // first, check the cache
+ if (CSVCache.find(pAssetName) != CSVCache.end())
+ {
+ CSVData** pUserdata = g_pSquirrel<context>->createuserdata<CSVData*>(sqvm, sizeof(CSVData*));
+ g_pSquirrel<context>->setuserdatatypeid(sqvm, -1, USERDATA_TYPE_DATATABLE_CUSTOM);
+ *pUserdata = &CSVCache[pAssetName];
+
+ return SQRESULT_NOTNULL;
+ }
+
+ // check files on disk
+ // we don't use .rpak as the extension for on-disk datatables, so we need to replace .rpak with .csv in the filename we're reading
+ fs::path diskAssetPath("scripts");
+ if (fs::path(pAssetName).extension() == ".rpak")
+ diskAssetPath /= fs::path(pAssetName).remove_filename() / (fs::path(pAssetName).stem().string() + ".csv");
+ else
+ diskAssetPath /= fs::path(pAssetName);
+
+ std::string sDiskAssetPath(diskAssetPath.string());
+ if ((*R2::g_pFilesystem)->m_vtable2->FileExists(&(*R2::g_pFilesystem)->m_vtable2, sDiskAssetPath.c_str(), "GAME"))
+ {
+ std::string sTableCSV = R2::ReadVPKFile(sDiskAssetPath.c_str());
+ if (!sTableCSV.size())
+ {
+ g_pSquirrel<context>->raiseerror(sqvm, fmt::format("Datatable \"{}\" is empty", pAssetName).c_str());
+ return SQRESULT_ERROR;
+ }
+
+ // somewhat shit, but ensure we end with a newline to make parsing easier
+ if (sTableCSV[sTableCSV.length() - 1] != '\n')
+ sTableCSV += '\n';
+
+ CSVData csv;
+ csv.m_sAssetName = pAssetName;
+ csv.m_sCSVString = sTableCSV;
+ csv.m_nDataBufSize = sTableCSV.size();
+ csv.m_pDataBuf = new char[csv.m_nDataBufSize];
+ memcpy(csv.m_pDataBuf, &sTableCSV[0], csv.m_nDataBufSize);
+
+ // parse the csv
+ // csvs are essentially comma and newline-deliniated sets of strings for parsing, only thing we need to worry about is quoted
+ // entries when we parse an element of the csv, rather than allocating an entry for it, we just convert that element to a
+ // null-terminated string i.e., store the ptr to the first char of it, then make the comma that delinates it a nullchar
+
+ bool bHasColumns = false;
+ bool bInQuotes = false;
+
+ std::vector<char*> vCurrentRow;
+ char* pElemStart = csv.m_pDataBuf;
+ char* pElemEnd = nullptr;
+
+ for (int i = 0; i < csv.m_nDataBufSize; i++)
+ {
+ if (csv.m_pDataBuf[i] == '\r' && csv.m_pDataBuf[i + 1] == '\n')
+ {
+ if (!pElemEnd)
+ pElemEnd = csv.m_pDataBuf + i;
+
+ continue; // next iteration can handle the \n
+ }
+
+ // newline, end of a row
+ if (csv.m_pDataBuf[i] == '\n')
+ {
+ // shouldn't have newline in string
+ if (bInQuotes)
+ {
+ g_pSquirrel<context>->raiseerror(sqvm, "Unexpected \\n in string");
+ return SQRESULT_ERROR;
+ }
+
+ // push last entry to current row
+ if (pElemEnd)
+ *pElemEnd = '\0';
+ else
+ csv.m_pDataBuf[i] = '\0';
+
+ vCurrentRow.push_back(pElemStart);
+
+ // newline, push last line to csv data and go from there
+ if (!bHasColumns)
+ {
+ bHasColumns = true;
+ csv.columns = vCurrentRow;
+ }
+ else
+ csv.dataPointers.push_back(vCurrentRow);
+
+ vCurrentRow.clear();
+ // put start of current element at char after newline
+ pElemStart = csv.m_pDataBuf + i + 1;
+ pElemEnd = nullptr;
+ }
+ // we're starting or ending a quoted string
+ else if (csv.m_pDataBuf[i] == '"')
+ {
+ // start quoted string
+ if (!bInQuotes)
+ {
+ // shouldn't have quoted strings in column names
+ if (!bHasColumns)
+ {
+ g_pSquirrel<context>->raiseerror(sqvm, "Unexpected \" in column name");
+ return SQRESULT_ERROR;
+ }
+
+ bInQuotes = true;
+ // put start of current element at char after string begin
+ pElemStart = csv.m_pDataBuf + i + 1;
+ }
+ // end quoted string
+ else
+ {
+ pElemEnd = csv.m_pDataBuf + i;
+ bInQuotes = false;
+ }
+ }
+ // don't parse commas in quotes
+ else if (bInQuotes)
+ {
+ continue;
+ }
+ // comma, push new entry to current row
+ else if (csv.m_pDataBuf[i] == ',')
+ {
+ if (pElemEnd)
+ *pElemEnd = '\0';
+ else
+ csv.m_pDataBuf[i] = '\0';
+
+ vCurrentRow.push_back(pElemStart);
+ // put start of next element at char after comma
+ pElemStart = csv.m_pDataBuf + i + 1;
+ pElemEnd = nullptr;
+ }
+ }
+
+ // add to cache and return
+ CSVData** pUserdata = g_pSquirrel<context>->createuserdata<CSVData*>(sqvm, sizeof(CSVData*));
+ g_pSquirrel<context>->setuserdatatypeid(sqvm, -1, USERDATA_TYPE_DATATABLE_CUSTOM);
+ CSVCache[pAssetName] = csv;
+ *pUserdata = &CSVCache[pAssetName];
+
+ return SQRESULT_NOTNULL;
+ }
+ // the file doesn't exist on disk, check rpak if we haven't already
+ else if (Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->LoadFile(pAssetName))
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTable"](sqvm);
+ // the file doesn't exist at all, error
+ else
+ {
+ g_pSquirrel<context>->raiseerror(sqvm, fmt::format("Datatable {} not found", pAssetName).c_str());
+ return SQRESULT_ERROR;
+ }
+ }
+}
+
+// int function GetDataTableColumnByName( var datatable, string columnName )
+REPLACE_SQFUNC(GetDataTableColumnByName, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableColumnByName"](sqvm);
+
+ CSVData* csv = *pData;
+ const char* pColumnName = g_pSquirrel<context>->getstring(sqvm, 2);
+
+ for (int i = 0; i < csv->columns.size(); i++)
+ {
+ if (!strcmp(csv->columns[i], pColumnName))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ // column not found
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowCount( var datatable )
+REPLACE_SQFUNC(GetDataTableRowCount, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDatatableRowCount"](sqvm);
+
+ CSVData* csv = *pData;
+ g_pSquirrel<context>->pushinteger(sqvm, csv->dataPointers.size());
+ return SQRESULT_NOTNULL;
+}
+
+// string function GetDataTableString( var datatable, int row, int col )
+REPLACE_SQFUNC(GetDataTableString, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableString"](sqvm);
+
+ CSVData* csv = *pData;
+ const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3);
+ if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushstring(sqvm, csv->dataPointers[nRow][nCol], -1);
+ return SQRESULT_NOTNULL;
+}
+
+// asset function GetDataTableAsset( var datatable, int row, int col )
+REPLACE_SQFUNC(GetDataTableAsset, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableAsset"](sqvm);
+
+ CSVData* csv = *pData;
+ const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3);
+ if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushasset(sqvm, csv->dataPointers[nRow][nCol], -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableInt( var datatable, int row, int col )
+REPLACE_SQFUNC(GetDataTableInt, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableInt"](sqvm);
+
+ CSVData* csv = *pData;
+ const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3);
+ if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, std::stoi(csv->dataPointers[nRow][nCol]));
+ return SQRESULT_NOTNULL;
+}
+
+// float function GetDataTableFloat( var datatable, int row, int col )
+REPLACE_SQFUNC(GetDataTableFloat, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableFloat"](sqvm);
+
+ CSVData* csv = *pData;
+ const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3);
+ if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushfloat(sqvm, std::stof(csv->dataPointers[nRow][nCol]));
+ return SQRESULT_NOTNULL;
+}
+
+// bool function GetDataTableBool( var datatable, int row, int col )
+REPLACE_SQFUNC(GetDataTableBool, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableBool"](sqvm);
+
+ CSVData* csv = *pData;
+ const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3);
+ if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushbool(sqvm, std::stoi(csv->dataPointers[nRow][nCol]));
+ return SQRESULT_NOTNULL;
+}
+
+// vector function GetDataTableVector( var datatable, int row, int col )
+REPLACE_SQFUNC(GetDataTableVector, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableVector"](sqvm);
+
+ CSVData* csv = *pData;
+ const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3);
+ if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size())
+ {
+ g_pSquirrel<context>->raiseerror(
+ sqvm,
+ fmt::format(
+ "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size())
+ .c_str());
+ return SQRESULT_ERROR;
+ }
+
+ g_pSquirrel<context>->pushvector(sqvm, StringToVector(csv->dataPointers[nRow][nCol]));
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowMatchingStringValue( var datatable, int col, string value )
+REPLACE_SQFUNC(GetDataTableRowMatchingStringValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingStringValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const char* pStringVal = g_pSquirrel<context>->getstring(sqvm, 3);
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (!strcmp(csv->dataPointers[i][nCol], pStringVal))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowMatchingAssetValue( var datatable, int col, asset value )
+REPLACE_SQFUNC(GetDataTableMatchingAssetValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingAssetValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const char* pStringVal;
+ g_pSquirrel<context>->getasset(sqvm, 3, &pStringVal);
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (!strcmp(csv->dataPointers[i][nCol], pStringVal))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowMatchingFloatValue( var datatable, int col, float value )
+REPLACE_SQFUNC(GetDataTableRowMatchingFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingFloatValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const float flFloatVal = g_pSquirrel<context>->getfloat(sqvm, 3);
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (flFloatVal == std::stof(csv->dataPointers[i][nCol]))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowMatchingIntValue( var datatable, int col, int value )
+REPLACE_SQFUNC(GetDataTableRowMatchingIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingIntValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nIntVal = g_pSquirrel<context>->getinteger(sqvm, 3);
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (nIntVal == std::stoi(csv->dataPointers[i][nCol]))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowMatchingVectorValue( var datatable, int col, vector value )
+REPLACE_SQFUNC(GetDataTableRowMatchingVectorValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingVectorValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const Vector3 vVectorVal = g_pSquirrel<context>->getvector(sqvm, 3);
+
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (vVectorVal == StringToVector(csv->dataPointers[i][nCol]))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowGreaterThanOrEqualToIntValue( var datatable, int col, int value )
+REPLACE_SQFUNC(GetDataTableRowGreaterThanOrEqualToIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToIntValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nIntVal = g_pSquirrel<context>->getinteger(sqvm, 3);
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (nIntVal >= std::stoi(csv->dataPointers[i][nCol]))
+ {
+ spdlog::info("datatable not loaded");
+ g_pSquirrel<context>->pushinteger(sqvm, 1);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowLessThanOrEqualToIntValue( var datatable, int col, int value )
+REPLACE_SQFUNC(GetDataTableRowLessThanOrEqualToIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowLessThanOrEqualToIntValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const int nIntVal = g_pSquirrel<context>->getinteger(sqvm, 3);
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (nIntVal <= std::stoi(csv->dataPointers[i][nCol]))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowGreaterThanOrEqualToFloatValue( var datatable, int col, float value )
+REPLACE_SQFUNC(GetDataTableRowGreaterThanOrEqualToFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToFloatValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const float flFloatVal = g_pSquirrel<context>->getfloat(sqvm, 3);
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (flFloatVal >= std::stof(csv->dataPointers[i][nCol]))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+// int function GetDataTableRowLessThanOrEqualToFloatValue( var datatable, int col, float value )
+REPLACE_SQFUNC(GetDataTableRowLessThanOrEqualToFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER))
+{
+ CSVData** pData;
+ uint64_t typeId;
+ g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId);
+
+ if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM)
+ return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowLessThanOrEqualToFloatValue"](sqvm);
+
+ CSVData* csv = *pData;
+ int nCol = g_pSquirrel<context>->getinteger(sqvm, 2);
+ const float flFloatVal = g_pSquirrel<context>->getfloat(sqvm, 3);
+ for (int i = 0; i < csv->dataPointers.size(); i++)
+ {
+ if (flFloatVal <= std::stof(csv->dataPointers[i][nCol]))
+ {
+ g_pSquirrel<context>->pushinteger(sqvm, i);
+ return SQRESULT_NOTNULL;
+ }
+ }
+
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+}
+
+std::string DataTableToString(Datatable* datatable)
+{
+ std::string sCSVString;
+
+ // write columns
+ bool bShouldComma = false;
+ for (int i = 0; i < datatable->numColumns; i++)
+ {
+ if (bShouldComma)
+ sCSVString += ',';
+ else
+ bShouldComma = true;
+
+ sCSVString += datatable->columnInfo[i].name;
+ }
+
+ // write rows
+ for (int row = 0; row < datatable->numRows; row++)
+ {
+ sCSVString += '\n';
+
+ bool bShouldComma = false;
+ for (int col = 0; col < datatable->numColumns; col++)
+ {
+ if (bShouldComma)
+ sCSVString += ',';
+ else
+ bShouldComma = true;
+
+ // output typed data
+ ColumnInfo column = datatable->columnInfo[col];
+ const void* pUntypedVal = datatable->data + column.offset + row * datatable->rowInfo;
+ switch (column.type)
+ {
+ case DatatableType::BOOL:
+ {
+ sCSVString += *(bool*)pUntypedVal ? '1' : '0';
+ break;
+ }
+
+ case DatatableType::INT:
+ {
+ sCSVString += std::to_string(*(int*)pUntypedVal);
+ break;
+ }
+
+ case DatatableType::FLOAT:
+ {
+ sCSVString += std::to_string(*(float*)pUntypedVal);
+ break;
+ }
+
+ case DatatableType::VECTOR:
+ {
+ Vector3 pVector((float*)pUntypedVal);
+ sCSVString += fmt::format("<{},{},{}>", pVector.x, pVector.y, pVector.z);
+ break;
+ }
+
+ case DatatableType::STRING:
+ case DatatableType::ASSET:
+ case DatatableType::UNK_STRING:
+ {
+ sCSVString += fmt::format("\"{}\"", *(char**)pUntypedVal);
+ break;
+ }
+ }
+ }
+ }
+
+ return sCSVString;
+}
+
+void DumpDatatable(const char* pDatatablePath)
+{
+ Datatable* pDatatable = (Datatable*)g_pPakLoadManager->LoadFile(pDatatablePath);
+ if (!pDatatable)
+ {
+ spdlog::error("couldn't load datatable {} (rpak containing it may not be loaded?)", pDatatablePath);
+ return;
+ }
+
+ std::string sOutputPath(fmt::format("{}/scripts/datatable/{}.csv", R2::g_pModName, fs::path(pDatatablePath).stem().string()));
+ std::string sDatatableContents(DataTableToString(pDatatable));
+
+ fs::create_directories(fs::path(sOutputPath).remove_filename());
+ std::ofstream outputStream(sOutputPath);
+ outputStream.write(sDatatableContents.c_str(), sDatatableContents.size());
+ outputStream.close();
+
+ spdlog::info("dumped datatable {} {} to {}", pDatatablePath, (void*)pDatatable, sOutputPath);
+}
+
+void ConCommand_dump_datatable(const CCommand& args)
+{
+ if (args.ArgC() < 2)
+ {
+ spdlog::info("usage: dump_datatable datatable/tablename.rpak");
+ return;
+ }
+
+ DumpDatatable(args.Arg(1));
+}
+
+void ConCommand_dump_datatables(const CCommand& args)
+{
+ // likely not a comprehensive list, might be missing a couple?
+ static const std::vector<const char*> VANILLA_DATATABLE_PATHS = {
+ "datatable/burn_meter_rewards.rpak",
+ "datatable/burn_meter_store.rpak",
+ "datatable/calling_cards.rpak",
+ "datatable/callsign_icons.rpak",
+ "datatable/camo_skins.rpak",
+ "datatable/default_pilot_loadouts.rpak",
+ "datatable/default_titan_loadouts.rpak",
+ "datatable/faction_leaders.rpak",
+ "datatable/fd_awards.rpak",
+ "datatable/features_mp.rpak",
+ "datatable/non_loadout_weapons.rpak",
+ "datatable/pilot_abilities.rpak",
+ "datatable/pilot_executions.rpak",
+ "datatable/pilot_passives.rpak",
+ "datatable/pilot_properties.rpak",
+ "datatable/pilot_weapons.rpak",
+ "datatable/pilot_weapon_features.rpak",
+ "datatable/pilot_weapon_mods.rpak",
+ "datatable/pilot_weapon_mods_common.rpak",
+ "datatable/playlist_items.rpak",
+ "datatable/titans_mp.rpak",
+ "datatable/titan_abilities.rpak",
+ "datatable/titan_executions.rpak",
+ "datatable/titan_fd_upgrades.rpak",
+ "datatable/titan_nose_art.rpak",
+ "datatable/titan_passives.rpak",
+ "datatable/titan_primary_mods.rpak",
+ "datatable/titan_primary_mods_common.rpak",
+ "datatable/titan_primary_weapons.rpak",
+ "datatable/titan_properties.rpak",
+ "datatable/titan_skins.rpak",
+ "datatable/titan_voices.rpak",
+ "datatable/unlocks_faction_level.rpak",
+ "datatable/unlocks_fd_titan_level.rpak",
+ "datatable/unlocks_player_level.rpak",
+ "datatable/unlocks_random.rpak",
+ "datatable/unlocks_titan_level.rpak",
+ "datatable/unlocks_weapon_level_pilot.rpak",
+ "datatable/weapon_skins.rpak",
+ "datatable/xp_per_faction_level.rpak",
+ "datatable/xp_per_fd_titan_level.rpak",
+ "datatable/xp_per_player_level.rpak",
+ "datatable/xp_per_titan_level.rpak",
+ "datatable/xp_per_weapon_level.rpak",
+ "datatable/faction_leaders_dropship_anims.rpak",
+ "datatable/score_events.rpak",
+ "datatable/startpoints.rpak",
+ "datatable/sp_levels.rpak",
+ "datatable/community_entries.rpak",
+ "datatable/spotlight_images.rpak",
+ "datatable/death_hints_mp.rpak",
+ "datatable/flightpath_assets.rpak",
+ "datatable/earn_meter_mp.rpak",
+ "datatable/battle_chatter_voices.rpak",
+ "datatable/battle_chatter.rpak",
+ "datatable/titan_os_conversations.rpak",
+ "datatable/faction_dialogue.rpak",
+ "datatable/grunt_chatter_mp.rpak",
+ "datatable/spectre_chatter_mp.rpak",
+ "datatable/pain_death_sounds.rpak",
+ "datatable/caller_ids_mp.rpak"};
+
+ for (const char* datatable : VANILLA_DATATABLE_PATHS)
+ DumpDatatable(datatable);
+}
+
+ON_DLL_LOAD_RELIESON("server.dll", ServerScriptDatatables, ServerSquirrel, (CModule module))
+{
+ SQ_GetDatatableInternal<ScriptContext::SERVER> = module.Offset(0x1250f0).As<Datatable* (*)(HSquirrelVM*)>();
+}
+
+ON_DLL_LOAD_RELIESON("client.dll", ClientScriptDatatables, ClientSquirrel, (CModule module))
+{
+ SQ_GetDatatableInternal<ScriptContext::CLIENT> = module.Offset(0x1C9070).As<Datatable* (*)(HSquirrelVM*)>();
+ SQ_GetDatatableInternal<ScriptContext::UI> = SQ_GetDatatableInternal<ScriptContext::CLIENT>;
+}
+
+ON_DLL_LOAD_RELIESON("engine.dll", SharedScriptDataTables, ConVar, (CModule module))
+{
+ Cvar_ns_prefer_datatable_from_disk = new ConVar(
+ "ns_prefer_datatable_from_disk",
+ IsDedicatedServer() && Tier0::CommandLine()->CheckParm("-nopakdedi") ? "1" : "0",
+ FCVAR_NONE,
+ "whether to prefer loading datatables from disk, rather than rpak");
+
+ RegisterConCommand("dump_datatables", ConCommand_dump_datatables, "dumps all datatables from a hardcoded list", FCVAR_NONE);
+ RegisterConCommand("dump_datatable", ConCommand_dump_datatable, "dump a datatable", FCVAR_NONE);
+}
diff --git a/NorthstarDLL/scripts/scripthttprequesthandler.cpp b/NorthstarDLL/scripts/scripthttprequesthandler.cpp
new file mode 100644
index 00000000..17ccc888
--- /dev/null
+++ b/NorthstarDLL/scripts/scripthttprequesthandler.cpp
@@ -0,0 +1,586 @@
+#include "pch.h"
+#include "scripthttprequesthandler.h"
+#include "util/version.h"
+#include "squirrel/squirrel.h"
+#include "core/tier0.h"
+
+HttpRequestHandler* g_httpRequestHandler;
+
+bool IsHttpDisabled()
+{
+ const static bool bIsHttpDisabled = Tier0::CommandLine()->FindParm("-disablehttprequests");
+ return bIsHttpDisabled;
+}
+
+bool IsLocalHttpAllowed()
+{
+ const static bool bIsLocalHttpAllowed = Tier0::CommandLine()->FindParm("-allowlocalhttp");
+ return bIsLocalHttpAllowed;
+}
+
+bool DisableHttpSsl()
+{
+ const static bool bDisableHttpSsl = Tier0::CommandLine()->FindParm("-disablehttpssl");
+ return bDisableHttpSsl;
+}
+
+HttpRequestHandler::HttpRequestHandler()
+{
+ // Cache the launch parameters as early as possible in order to avoid possible exploits that change them at runtime.
+ IsHttpDisabled();
+ IsLocalHttpAllowed();
+ DisableHttpSsl();
+}
+
+void HttpRequestHandler::StartHttpRequestHandler()
+{
+ if (IsRunning())
+ {
+ spdlog::warn("%s was called while IsRunning() is true!", __FUNCTION__);
+ return;
+ }
+
+ m_bIsHttpRequestHandlerRunning = true;
+ spdlog::info("HttpRequestHandler started.");
+}
+
+void HttpRequestHandler::StopHttpRequestHandler()
+{
+ if (!IsRunning())
+ {
+ spdlog::warn("%s was called while IsRunning() is false", __FUNCTION__);
+ return;
+ }
+
+ m_bIsHttpRequestHandlerRunning = false;
+ spdlog::info("HttpRequestHandler stopped.");
+}
+
+bool IsHttpDestinationHostAllowed(const std::string& host, std::string& outHostname, std::string& outAddress, std::string& outPort)
+{
+ CURLU* url = curl_url();
+ if (!url)
+ {
+ spdlog::error("Failed to call curl_url() for http request.");
+ return false;
+ }
+
+ if (curl_url_set(url, CURLUPART_URL, host.c_str(), CURLU_DEFAULT_SCHEME) != CURLUE_OK)
+ {
+ spdlog::error("Failed to parse destination URL for http request.");
+
+ curl_url_cleanup(url);
+ return false;
+ }
+
+ char* urlHostname = nullptr;
+ if (curl_url_get(url, CURLUPART_HOST, &urlHostname, 0) != CURLUE_OK)
+ {
+ spdlog::error("Failed to parse hostname from destination URL for http request.");
+
+ curl_url_cleanup(url);
+ return false;
+ }
+
+ char* urlScheme = nullptr;
+ if (curl_url_get(url, CURLUPART_SCHEME, &urlScheme, CURLU_DEFAULT_SCHEME) != CURLUE_OK)
+ {
+ spdlog::error("Failed to parse scheme from destination URL for http request.");
+
+ curl_url_cleanup(url);
+ curl_free(urlHostname);
+ return false;
+ }
+
+ char* urlPort = nullptr;
+ if (curl_url_get(url, CURLUPART_PORT, &urlPort, CURLU_DEFAULT_PORT) != CURLUE_OK)
+ {
+ spdlog::error("Failed to parse port from destination URL for http request.");
+
+ curl_url_cleanup(url);
+ curl_free(urlHostname);
+ curl_free(urlScheme);
+ return false;
+ }
+
+ // Resolve the hostname into an address.
+ addrinfo* result;
+ addrinfo hints;
+ std::memset(&hints, 0, sizeof(addrinfo));
+ hints.ai_family = AF_UNSPEC;
+
+ if (getaddrinfo(urlHostname, urlScheme, &hints, &result) != 0)
+ {
+ spdlog::error("Failed to resolve http request destination {} using getaddrinfo().", urlHostname);
+
+ curl_url_cleanup(url);
+ curl_free(urlHostname);
+ curl_free(urlScheme);
+ curl_free(urlPort);
+ return false;
+ }
+
+ bool bFoundIPv6 = false;
+ sockaddr_in* sockaddr_ipv4 = nullptr;
+ for (addrinfo* info = result; info; info = info->ai_next)
+ {
+ if (info->ai_family == AF_INET)
+ {
+ sockaddr_ipv4 = (sockaddr_in*)info->ai_addr;
+ break;
+ }
+
+ bFoundIPv6 = bFoundIPv6 || info->ai_family == AF_INET6;
+ }
+
+ if (sockaddr_ipv4 == nullptr)
+ {
+ if (bFoundIPv6)
+ {
+ spdlog::error("Only IPv4 destinations are supported for HTTP requests. To allow IPv6, launch the game using -allowlocalhttp.");
+ }
+ else
+ {
+ spdlog::error("Failed to resolve http request destination {} into a valid IPv4 address.", urlHostname);
+ }
+
+ curl_free(urlHostname);
+ curl_free(urlScheme);
+ curl_free(urlPort);
+ curl_url_cleanup(url);
+
+ return false;
+ }
+
+ // Fast checks for private ranges of IPv4.
+ // clang-format off
+ {
+ auto addrBytes = sockaddr_ipv4->sin_addr.S_un.S_un_b;
+
+ if (addrBytes.s_b1 == 10 // 10.0.0.0 - 10.255.255.255 (Class A Private)
+ || addrBytes.s_b1 == 172 && addrBytes.s_b2 >= 16 && addrBytes.s_b2 <= 31 // 172.16.0.0 - 172.31.255.255 (Class B Private)
+ || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 168 // 192.168.0.0 - 192.168.255.255 (Class C Private)
+ || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 0 // 192.0.0.0 - 192.0.0.255 (IETF Assignment)
+ || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 2 // 192.0.2.0 - 192.0.2.255 (TEST-NET-1)
+ || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 88 && addrBytes.s_b3 == 99 // 192.88.99.0 - 192.88.99.255 (IPv4-IPv6 Relay)
+ || addrBytes.s_b1 == 192 && addrBytes.s_b2 >= 18 && addrBytes.s_b2 <= 19 // 192.18.0.0 - 192.19.255.255 (Internet Benchmark)
+ || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 51 && addrBytes.s_b3 == 100 // 192.51.100.0 - 192.51.100.255 (TEST-NET-2)
+ || addrBytes.s_b1 == 203 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 113 // 203.0.113.0 - 203.0.113.255 (TEST-NET-3)
+ || addrBytes.s_b1 == 169 && addrBytes.s_b2 == 254 // 169.254.00 - 169.254.255.255 (Link-local/APIPA)
+ || addrBytes.s_b1 == 127 // 127.0.0.0 - 127.255.255.255 (Loopback)
+ || addrBytes.s_b1 == 0 // 0.0.0.0 - 0.255.255.255 (Current network)
+ || addrBytes.s_b1 == 100 && addrBytes.s_b2 >= 64 && addrBytes.s_b2 <= 127 // 100.64.0.0 - 100.127.255.255 (Shared address space)
+ || sockaddr_ipv4->sin_addr.S_un.S_addr == 0xFFFFFFFF // 255.255.255.255 (Broadcast)
+ || addrBytes.s_b1 >= 224 && addrBytes.s_b2 <= 239 // 224.0.0.0 - 239.255.255.255 (Multicast)
+ || addrBytes.s_b1 == 233 && addrBytes.s_b2 == 252 && addrBytes.s_b3 == 0 // 233.252.0.0 - 233.252.0.255 (MCAST-TEST-NET)
+ || addrBytes.s_b1 >= 240 && addrBytes.s_b4 <= 254) // 240.0.0.0 - 255.255.255.254 (Future Use Class E)
+ {
+ curl_free(urlHostname);
+ curl_free(urlScheme);
+ curl_free(urlPort);
+ curl_url_cleanup(url);
+
+ return false;
+ }
+ }
+
+ // clang-format on
+
+ char resolvedStr[INET_ADDRSTRLEN];
+ inet_ntop(AF_INET, &sockaddr_ipv4->sin_addr, resolvedStr, INET_ADDRSTRLEN);
+
+ // Use the resolved address as the new request host.
+ outHostname = urlHostname;
+ outAddress = resolvedStr;
+ outPort = urlPort;
+
+ freeaddrinfo(result);
+
+ curl_free(urlHostname);
+ curl_free(urlScheme);
+ curl_free(urlPort);
+ curl_url_cleanup(url);
+
+ return true;
+}
+
+size_t HttpCurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp)
+{
+ ((std::string*)userp)->append((char*)contents, size * nmemb);
+ return size * nmemb;
+}
+
+template <ScriptContext context> int HttpRequestHandler::MakeHttpRequest(const HttpRequest& requestParameters)
+{
+ if (!IsRunning())
+ {
+ spdlog::warn("%s was called while IsRunning() is false!", __FUNCTION__);
+ return -1;
+ }
+
+ if (IsHttpDisabled())
+ {
+ spdlog::warn("NS_InternalMakeHttpRequest called while the game is running with -disablehttprequests."
+ " Please check if requests are allowed using NSIsHttpEnabled() first.");
+ return -1;
+ }
+
+ bool bAllowLocalHttp = IsLocalHttpAllowed();
+
+ // This handle will be returned to Squirrel so it can wait for the response and assign a callback for it.
+ int handle = ++m_iLastRequestHandle;
+
+ std::thread requestThread(
+ [this, handle, requestParameters, bAllowLocalHttp]()
+ {
+ std::string hostname, resolvedAddress, resolvedPort;
+
+ if (!bAllowLocalHttp)
+ {
+ if (!IsHttpDestinationHostAllowed(requestParameters.baseUrl, hostname, resolvedAddress, resolvedPort))
+ {
+ spdlog::warn(
+ "HttpRequestHandler::MakeHttpRequest attempted to make a request to a private network. This is only allowed when "
+ "running the game with -allowlocalhttp.");
+ g_pSquirrel<context>->AsyncCall(
+ "NSHandleFailedHttpRequest",
+ handle,
+ (int)0,
+ "Cannot make HTTP requests to private network hosts without -allowlocalhttp. Check your console for more "
+ "information.");
+ return;
+ }
+ }
+
+ CURL* curl = curl_easy_init();
+ if (!curl)
+ {
+ spdlog::error("HttpRequestHandler::MakeHttpRequest failed to init libcurl for request.");
+ g_pSquirrel<context>->AsyncCall(
+ "NSHandleFailedHttpRequest", handle, static_cast<int>(CURLE_FAILED_INIT), curl_easy_strerror(CURLE_FAILED_INIT));
+ return;
+ }
+
+ // HEAD has no body.
+ if (requestParameters.method == HttpRequestMethod::HRM_HEAD)
+ {
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
+ }
+
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, HttpRequestMethod::ToString(requestParameters.method).c_str());
+
+ // Only resolve to IPv4 if we don't allow private network requests.
+ curl_slist* host = nullptr;
+ if (!bAllowLocalHttp)
+ {
+ curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+ host = curl_slist_append(host, fmt::format("{}:{}:{}", hostname, resolvedPort, resolvedAddress).c_str());
+ curl_easy_setopt(curl, CURLOPT_RESOLVE, host);
+ }
+
+ // Ensure we only allow HTTP or HTTPS.
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
+
+ // Allow redirects
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
+
+ // Check if the url already contains a query.
+ // If so, we'll know to append with & instead of start with ?
+ std::string queryUrl = requestParameters.baseUrl;
+ bool bUrlContainsQuery = false;
+
+ // If this fails, just ignore the parsing and trust what the user wants to query.
+ // Probably will fail but handling it here would be annoying.
+ CURLU* curlUrl = curl_url();
+ if (curlUrl)
+ {
+ if (curl_url_set(curlUrl, CURLUPART_URL, queryUrl.c_str(), CURLU_DEFAULT_SCHEME) == CURLUE_OK)
+ {
+ char* currentQuery;
+ if (curl_url_get(curlUrl, CURLUPART_QUERY, &currentQuery, 0) == CURLUE_OK)
+ {
+ if (currentQuery && std::strlen(currentQuery) != 0)
+ {
+ bUrlContainsQuery = true;
+ }
+ }
+
+ curl_free(currentQuery);
+ }
+
+ curl_url_cleanup(curlUrl);
+ }
+
+ // GET requests, or POST-like requests with an empty body, can have query parameters.
+ // Append them to the base url.
+ if (HttpRequestMethod::CanHaveQueryParameters(requestParameters.method) &&
+ !HttpRequestMethod::UsesCurlPostOptions(requestParameters.method) ||
+ requestParameters.body.empty())
+ {
+ bool isFirstValue = true;
+ for (const auto& kv : requestParameters.queryParameters)
+ {
+ char* key = curl_easy_escape(curl, kv.first.c_str(), kv.first.length());
+
+ for (const std::string& queryValue : kv.second)
+ {
+ char* value = curl_easy_escape(curl, queryValue.c_str(), queryValue.length());
+
+ if (isFirstValue && !bUrlContainsQuery)
+ {
+ queryUrl.append(fmt::format("?{}={}", key, value));
+ isFirstValue = false;
+ }
+ else
+ {
+ queryUrl.append(fmt::format("&{}={}", key, value));
+ }
+
+ curl_free(value);
+ }
+
+ curl_free(key);
+ }
+ }
+
+ // If this method uses POST-like curl options, set those and set the body.
+ // The body won't be sent if it's empty anyway, meaning the query parameters above, if any, would be.
+ if (HttpRequestMethod::UsesCurlPostOptions(requestParameters.method))
+ {
+ // Grab the body and set it as a POST field
+ curl_easy_setopt(curl, CURLOPT_POST, 1L);
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, requestParameters.body.length());
+ curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, requestParameters.body.c_str());
+ }
+
+ // Set the full URL for this http request.
+ curl_easy_setopt(curl, CURLOPT_URL, queryUrl.c_str());
+
+ std::string bodyBuffer;
+ std::string headerBuffer;
+
+ // Set up buffers to write the response headers and body.
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HttpCurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &bodyBuffer);
+ curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, HttpCurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headerBuffer);
+
+ // Add all the headers for the request.
+ curl_slist* headers = nullptr;
+
+ // Content-Type header for POST-like requests.
+ if (HttpRequestMethod::UsesCurlPostOptions(requestParameters.method) && !requestParameters.body.empty())
+ {
+ headers = curl_slist_append(headers, fmt::format("Content-Type: {}", requestParameters.contentType).c_str());
+ }
+
+ for (const auto& kv : requestParameters.headers)
+ {
+ for (const std::string& headerValue : kv.second)
+ {
+ headers = curl_slist_append(headers, fmt::format("{}: {}", kv.first, headerValue).c_str());
+ }
+ }
+
+ if (headers != nullptr)
+ {
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ }
+
+ // Disable SSL checks if requested by the user.
+ if (DisableHttpSsl())
+ {
+ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
+ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYSTATUS, 0L);
+ }
+
+ // Enforce the Northstar user agent, unless an override was specified.
+ if (requestParameters.userAgent.empty())
+ {
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent);
+ }
+ else
+ {
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, requestParameters.userAgent.c_str());
+ }
+
+ // Set the timeout for this request. Max 60 seconds so mods can't just spin up native threads all the time.
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT, std::clamp<long>(requestParameters.timeout, 1, 60));
+
+ CURLcode result = curl_easy_perform(curl);
+ if (IsRunning())
+ {
+ if (result == CURLE_OK)
+ {
+ // While the curl request is OK, it could return a non success code.
+ // Squirrel side will handle firing the correct callback.
+ long httpCode = 0;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
+ g_pSquirrel<context>->AsyncCall(
+ "NSHandleSuccessfulHttpRequest", handle, static_cast<int>(httpCode), bodyBuffer, headerBuffer);
+ }
+ else
+ {
+ // Pass CURL result code & error.
+ spdlog::error(
+ "curl_easy_perform() failed with code {}, error: {}", static_cast<int>(result), curl_easy_strerror(result));
+
+ // If it's an SSL issue, tell the user they may disable SSL checks using -disablehttpssl.
+ if (result == CURLE_PEER_FAILED_VERIFICATION || result == CURLE_SSL_CERTPROBLEM ||
+ result == CURLE_SSL_INVALIDCERTSTATUS)
+ {
+ spdlog::error("You can try disabling SSL verifications for this issue using the -disablehttpssl launch argument. "
+ "Keep in mind this is potentially dangerous!");
+ }
+
+ g_pSquirrel<context>->AsyncCall(
+ "NSHandleFailedHttpRequest", handle, static_cast<int>(result), curl_easy_strerror(result));
+ }
+ }
+
+ curl_easy_cleanup(curl);
+ curl_slist_free_all(headers);
+ curl_slist_free_all(host);
+ });
+
+ requestThread.detach();
+ return handle;
+}
+
+template <ScriptContext context> void HttpRequestHandler::RegisterSQFuncs()
+{
+ g_pSquirrel<context>->AddFuncRegistration(
+ "int",
+ "NS_InternalMakeHttpRequest",
+ "int method, string baseUrl, table<string, array<string> > headers, table<string, array<string> > queryParams, string contentType, "
+ "string body, "
+ "int timeout, string userAgent",
+ "[Internal use only] Passes the HttpRequest struct fields to be reconstructed in native and used for an http request",
+ SQ_InternalMakeHttpRequest<context>);
+
+ g_pSquirrel<context>->AddFuncRegistration(
+ "bool",
+ "NSIsHttpEnabled",
+ "",
+ "Whether or not HTTP requests are enabled. You can opt-out by starting the game with -disablehttprequests.",
+ SQ_IsHttpEnabled<context>);
+
+ g_pSquirrel<context>->AddFuncRegistration(
+ "bool",
+ "NSIsLocalHttpAllowed",
+ "",
+ "Whether or not HTTP requests can be made to a private network address. You can enable this by starting the game with "
+ "-allowlocalhttp.",
+ SQ_IsLocalHttpAllowed<context>);
+}
+
+// int NS_InternalMakeHttpRequest(int method, string baseUrl, table<string, string> headers, table<string, string> queryParams,
+// string contentType, string body, int timeout, string userAgent)
+template <ScriptContext context> SQRESULT SQ_InternalMakeHttpRequest(HSquirrelVM* sqvm)
+{
+ if (!g_httpRequestHandler || !g_httpRequestHandler->IsRunning())
+ {
+ spdlog::warn("NS_InternalMakeHttpRequest called while the http request handler isn't running.");
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+ }
+
+ if (IsHttpDisabled())
+ {
+ spdlog::warn("NS_InternalMakeHttpRequest called while the game is running with -disablehttprequests."
+ " Please check if requests are allowed using NSIsHttpEnabled() first.");
+ g_pSquirrel<context>->pushinteger(sqvm, -1);
+ return SQRESULT_NOTNULL;
+ }
+
+ HttpRequest request;
+ request.method = static_cast<HttpRequestMethod::Type>(g_pSquirrel<context>->getinteger(sqvm, 1));
+ request.baseUrl = g_pSquirrel<context>->getstring(sqvm, 2);
+
+ // Read the tables for headers and query parameters.
+ SQTable* headerTable = sqvm->_stackOfCurrentFunction[3]._VAL.asTable;
+ for (int idx = 0; idx < headerTable->_numOfNodes; ++idx)
+ {
+ tableNode* node = &headerTable->_nodes[idx];
+
+ if (node->key._Type == OT_STRING && node->val._Type == OT_ARRAY)
+ {
+ SQArray* valueArray = node->val._VAL.asArray;
+ std::vector<std::string> headerValues;
+
+ for (int vIdx = 0; vIdx < valueArray->_usedSlots; ++vIdx)
+ {
+ if (valueArray->_values[vIdx]._Type == OT_STRING)
+ {
+ headerValues.push_back(valueArray->_values[vIdx]._VAL.asString->_val);
+ }
+ }
+
+ request.headers[node->key._VAL.asString->_val] = headerValues;
+ }
+ }
+
+ SQTable* queryTable = sqvm->_stackOfCurrentFunction[4]._VAL.asTable;
+ for (int idx = 0; idx < queryTable->_numOfNodes; ++idx)
+ {
+ tableNode* node = &queryTable->_nodes[idx];
+
+ if (node->key._Type == OT_STRING && node->val._Type == OT_ARRAY)
+ {
+ SQArray* valueArray = node->val._VAL.asArray;
+ std::vector<std::string> queryValues;
+
+ for (int vIdx = 0; vIdx < valueArray->_usedSlots; ++vIdx)
+ {
+ if (valueArray->_values[vIdx]._Type == OT_STRING)
+ {
+ queryValues.push_back(valueArray->_values[vIdx]._VAL.asString->_val);
+ }
+ }
+
+ request.queryParameters[node->key._VAL.asString->_val] = queryValues;
+ }
+ }
+
+ request.contentType = g_pSquirrel<context>->getstring(sqvm, 5);
+ request.body = g_pSquirrel<context>->getstring(sqvm, 6);
+ request.timeout = g_pSquirrel<context>->getinteger(sqvm, 7);
+ request.userAgent = g_pSquirrel<context>->getstring(sqvm, 8);
+
+ int handle = g_httpRequestHandler->MakeHttpRequest<context>(request);
+ g_pSquirrel<context>->pushinteger(sqvm, handle);
+ return SQRESULT_NOTNULL;
+}
+
+// bool NSIsHttpEnabled()
+template <ScriptContext context> SQRESULT SQ_IsHttpEnabled(HSquirrelVM* sqvm)
+{
+ g_pSquirrel<context>->pushbool(sqvm, !IsHttpDisabled());
+ return SQRESULT_NOTNULL;
+}
+
+// bool NSIsLocalHttpAllowed()
+template <ScriptContext context> SQRESULT SQ_IsLocalHttpAllowed(HSquirrelVM* sqvm)
+{
+ g_pSquirrel<context>->pushbool(sqvm, IsLocalHttpAllowed());
+ return SQRESULT_NOTNULL;
+}
+
+ON_DLL_LOAD_RELIESON("client.dll", HttpRequestHandler_ClientInit, ClientSquirrel, (CModule module))
+{
+ g_httpRequestHandler->RegisterSQFuncs<ScriptContext::CLIENT>();
+ g_httpRequestHandler->RegisterSQFuncs<ScriptContext::UI>();
+}
+
+ON_DLL_LOAD_RELIESON("server.dll", HttpRequestHandler_ServerInit, ServerSquirrel, (CModule module))
+{
+ g_httpRequestHandler->RegisterSQFuncs<ScriptContext::SERVER>();
+}
+
+ON_DLL_LOAD("engine.dll", HttpRequestHandler_Init, (CModule module))
+{
+ g_httpRequestHandler = new HttpRequestHandler;
+ g_httpRequestHandler->StartHttpRequestHandler();
+}
diff --git a/NorthstarDLL/scripts/scripthttprequesthandler.h b/NorthstarDLL/scripts/scripthttprequesthandler.h
new file mode 100644
index 00000000..0f888b6e
--- /dev/null
+++ b/NorthstarDLL/scripts/scripthttprequesthandler.h
@@ -0,0 +1,132 @@
+#pragma once
+
+#include "pch.h"
+
+enum class ScriptContext;
+
+// These definitions below should match on the Squirrel side so we can easily pass them along through a function.
+
+/**
+ * Allowed methods for an HttpRequest.
+ */
+namespace HttpRequestMethod
+{
+ enum Type
+ {
+ HRM_GET = 0,
+ HRM_POST = 1,
+ HRM_HEAD = 2,
+ HRM_PUT = 3,
+ HRM_DELETE = 4,
+ HRM_PATCH = 5,
+ HRM_OPTIONS = 6,
+ };
+
+ /** Returns the HTTP string representation of the given method. */
+ inline std::string ToString(HttpRequestMethod::Type method)
+ {
+ switch (method)
+ {
+ case HttpRequestMethod::HRM_GET:
+ return "GET";
+ case HttpRequestMethod::HRM_POST:
+ return "POST";
+ case HttpRequestMethod::HRM_HEAD:
+ return "HEAD";
+ case HttpRequestMethod::HRM_PUT:
+ return "PUT";
+ case HttpRequestMethod::HRM_DELETE:
+ return "DELETE";
+ case HttpRequestMethod::HRM_PATCH:
+ return "PATCH";
+ case HttpRequestMethod::HRM_OPTIONS:
+ return "OPTIONS";
+ default:
+ return "INVALID";
+ }
+ }
+
+ /** Whether or not the given method should be treated like a POST for curlopts. */
+ bool UsesCurlPostOptions(HttpRequestMethod::Type method)
+ {
+ switch (method)
+ {
+ case HttpRequestMethod::HRM_POST:
+ case HttpRequestMethod::HRM_PUT:
+ case HttpRequestMethod::HRM_DELETE:
+ case HttpRequestMethod::HRM_PATCH:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /** Whether or not the given http request method can have query parameters in the URL. */
+ bool CanHaveQueryParameters(HttpRequestMethod::Type method)
+ {
+ return method == HttpRequestMethod::HRM_GET || UsesCurlPostOptions(method);
+ }
+}; // namespace HttpRequestMethod
+
+/** Contains data about an http request that has been queued. */
+struct HttpRequest
+{
+ /** Method used for this http request. */
+ HttpRequestMethod::Type method;
+
+ /** Base URL of this http request. */
+ std::string baseUrl;
+
+ /** Headers used for this http request. Some may get overridden or ignored. */
+ std::unordered_map<std::string, std::vector<std::string>> headers;
+
+ /** Query parameters for this http request. */
+ std::unordered_map<std::string, std::vector<std::string>> queryParameters;
+
+ /** The content type of this http request. Defaults to text/plain & UTF-8 charset. */
+ std::string contentType = "text/plain; charset=utf-8";
+
+ /** The body of this http request. If set, will override queryParameters.*/
+ std::string body;
+
+ /** The timeout for the http request, in seconds. Must be between 1 and 60. */
+ int timeout;
+
+ /** If set, the override to use for the User-Agent header. */
+ std::string userAgent;
+};
+
+/**
+ * Handles making HTTP requests and sending the responses back to Squirrel.
+ */
+class HttpRequestHandler
+{
+ public:
+ HttpRequestHandler();
+
+ // Start/Stop the HTTP request handler. Right now this doesn't do much.
+ void StartHttpRequestHandler();
+ void StopHttpRequestHandler();
+
+ // Whether or not this http request handler is currently running.
+ bool IsRunning() const
+ {
+ return m_bIsHttpRequestHandlerRunning;
+ }
+
+ /**
+ * Creates a new thread to execute an HTTP request.
+ * @param requestParameters The parameters to use for this http request.
+ * @returns The handle for the http request being sent, or -1 if the request failed.
+ */
+ template <ScriptContext context> int MakeHttpRequest(const HttpRequest& requestParameters);
+
+ /** Registers the HTTP request Squirrel functions for the given script context. */
+ template <ScriptContext context> void RegisterSQFuncs();
+
+ private:
+ int m_iLastRequestHandle = 0;
+ std::atomic_bool m_bIsHttpRequestHandlerRunning = false;
+};
+
+extern HttpRequestHandler* g_httpRequestHandler;
diff --git a/NorthstarDLL/scripts/scriptjson.cpp b/NorthstarDLL/scripts/scriptjson.cpp
new file mode 100644
index 00000000..f41b0457
--- /dev/null
+++ b/NorthstarDLL/scripts/scriptjson.cpp
@@ -0,0 +1,249 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+
+#include "rapidjson/error/en.h"
+#include "rapidjson/document.h"
+#include "rapidjson/writer.h"
+#include "rapidjson/stringbuffer.h"
+
+#ifdef _MSC_VER
+#undef GetObject // fuck microsoft developers
+#endif
+
+template <ScriptContext context> void
+DecodeJsonArray(HSquirrelVM* sqvm, rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>* arr)
+{
+ g_pSquirrel<context>->newarray(sqvm, 0);
+
+ for (auto& itr : arr->GetArray())
+ {
+ switch (itr.GetType())
+ {
+ case rapidjson::kObjectType:
+ DecodeJsonTable<context>(sqvm, &itr);
+ g_pSquirrel<context>->arrayappend(sqvm, -2);
+ break;
+ case rapidjson::kArrayType:
+ DecodeJsonArray<context>(sqvm, &itr);
+ g_pSquirrel<context>->arrayappend(sqvm, -2);
+ break;
+ case rapidjson::kStringType:
+ g_pSquirrel<context>->pushstring(sqvm, itr.GetString(), -1);
+ g_pSquirrel<context>->arrayappend(sqvm, -2);
+ break;
+ case rapidjson::kTrueType:
+ case rapidjson::kFalseType:
+ g_pSquirrel<context>->pushbool(sqvm, itr.GetBool());
+ g_pSquirrel<context>->arrayappend(sqvm, -2);
+ break;
+ case rapidjson::kNumberType:
+ if (itr.IsDouble() || itr.IsFloat())
+ g_pSquirrel<context>->pushfloat(sqvm, itr.GetFloat());
+ else
+ g_pSquirrel<context>->pushinteger(sqvm, itr.GetInt());
+ g_pSquirrel<context>->arrayappend(sqvm, -2);
+ break;
+ }
+ }
+}
+
+template <ScriptContext context> void
+DecodeJsonTable(HSquirrelVM* sqvm, rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>* obj)
+{
+ g_pSquirrel<context>->newtable(sqvm);
+
+ for (auto itr = obj->MemberBegin(); itr != obj->MemberEnd(); itr++)
+ {
+ switch (itr->value.GetType())
+ {
+ case rapidjson::kObjectType:
+ g_pSquirrel<context>->pushstring(sqvm, itr->name.GetString(), -1);
+ DecodeJsonTable<context>(
+ sqvm, (rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>*)&itr->value);
+ g_pSquirrel<context>->newslot(sqvm, -3, false);
+ break;
+ case rapidjson::kArrayType:
+ g_pSquirrel<context>->pushstring(sqvm, itr->name.GetString(), -1);
+ DecodeJsonArray<context>(
+ sqvm, (rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>*)&itr->value);
+ g_pSquirrel<context>->newslot(sqvm, -3, false);
+ break;
+ case rapidjson::kStringType:
+ g_pSquirrel<context>->pushstring(sqvm, itr->name.GetString(), -1);
+ g_pSquirrel<context>->pushstring(sqvm, itr->value.GetString(), -1);
+
+ g_pSquirrel<context>->newslot(sqvm, -3, false);
+ break;
+ case rapidjson::kTrueType:
+ case rapidjson::kFalseType:
+ g_pSquirrel<context>->pushstring(sqvm, itr->name.GetString(), -1);
+ g_pSquirrel<context>->pushbool(sqvm, itr->value.GetBool());
+ g_pSquirrel<context>->newslot(sqvm, -3, false);
+ break;
+ case rapidjson::kNumberType:
+ if (itr->value.IsDouble() || itr->value.IsFloat())
+ {
+ g_pSquirrel<context>->pushstring(sqvm, itr->name.GetString(), -1);
+ g_pSquirrel<context>->pushfloat(sqvm, itr->value.GetFloat());
+ }
+ else
+ {
+ g_pSquirrel<context>->pushstring(sqvm, itr->name.GetString(), -1);
+ g_pSquirrel<context>->pushinteger(sqvm, itr->value.GetInt());
+ }
+ g_pSquirrel<context>->newslot(sqvm, -3, false);
+ break;
+ }
+ }
+}
+
+template <ScriptContext context> void EncodeJSONTable(
+ SQTable* table,
+ rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>* obj,
+ rapidjson::MemoryPoolAllocator<SourceAllocator>& allocator)
+{
+ for (int i = 0; i < table->_numOfNodes; i++)
+ {
+ tableNode* node = &table->_nodes[i];
+ if (node->key._Type == OT_STRING)
+ {
+ rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>> newObj(rapidjson::kObjectType);
+ rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>> newArray(rapidjson::kArrayType);
+
+ switch (node->val._Type)
+ {
+ case OT_STRING:
+ obj->AddMember(
+ rapidjson::StringRef(node->key._VAL.asString->_val), rapidjson::StringRef(node->val._VAL.asString->_val), allocator);
+ break;
+ case OT_INTEGER:
+ obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), node->val._VAL.asInteger, allocator);
+ break;
+ case OT_FLOAT:
+ obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), node->val._VAL.asFloat, allocator);
+ break;
+ case OT_BOOL:
+ if (node->val._VAL.asInteger)
+ {
+ obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), true, allocator);
+ }
+ else
+ {
+ obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), false, allocator);
+ }
+ break;
+ case OT_TABLE:
+ EncodeJSONTable<context>(node->val._VAL.asTable, &newObj, allocator);
+ obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), newObj, allocator);
+ break;
+ case OT_ARRAY:
+ EncodeJSONArray<context>(node->val._VAL.asArray, &newArray, allocator);
+ obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), newArray, allocator);
+ break;
+ default:
+ spdlog::warn("SQ_EncodeJSON: squirrel type {} not supported", SQTypeNameFromID(node->val._Type));
+ break;
+ }
+ }
+ }
+}
+
+template <ScriptContext context> void EncodeJSONArray(
+ SQArray* arr,
+ rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>* obj,
+ rapidjson::MemoryPoolAllocator<SourceAllocator>& allocator)
+{
+ for (int i = 0; i < arr->_usedSlots; i++)
+ {
+ SQObject* node = &arr->_values[i];
+
+ rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>> newObj(rapidjson::kObjectType);
+ rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>> newArray(rapidjson::kArrayType);
+
+ switch (node->_Type)
+ {
+ case OT_STRING:
+ obj->PushBack(rapidjson::StringRef(node->_VAL.asString->_val), allocator);
+ break;
+ case OT_INTEGER:
+ obj->PushBack(node->_VAL.asInteger, allocator);
+ break;
+ case OT_FLOAT:
+ obj->PushBack(node->_VAL.asFloat, allocator);
+ break;
+ case OT_BOOL:
+ if (node->_VAL.asInteger)
+ obj->PushBack(rapidjson::StringRef("true"), allocator);
+ else
+ obj->PushBack(rapidjson::StringRef("false"), allocator);
+ break;
+ case OT_TABLE:
+ EncodeJSONTable<context>(node->_VAL.asTable, &newObj, allocator);
+ obj->PushBack(newObj, allocator);
+ break;
+ case OT_ARRAY:
+ EncodeJSONArray<context>(node->_VAL.asArray, &newArray, allocator);
+ obj->PushBack(newArray, allocator);
+ break;
+ default:
+ spdlog::info("SQ encode Json type {} not supported", SQTypeNameFromID(node->_Type));
+ }
+ }
+}
+
+ADD_SQFUNC(
+ "table",
+ DecodeJSON,
+ "string json, bool fatalParseErrors = false",
+ "converts a json string to a squirrel table",
+ ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)
+{
+ const char* pJson = g_pSquirrel<context>->getstring(sqvm, 1);
+ const bool bFatalParseErrors = g_pSquirrel<context>->getbool(sqvm, 2);
+
+ rapidjson_document doc;
+ doc.Parse(pJson);
+ if (doc.HasParseError())
+ {
+ g_pSquirrel<context>->newtable(sqvm);
+
+ std::string sErrorString = fmt::format(
+ "Failed parsing json file: encountered parse error \"{}\" at offset {}",
+ GetParseError_En(doc.GetParseError()),
+ doc.GetErrorOffset());
+
+ if (bFatalParseErrors)
+ g_pSquirrel<context>->raiseerror(sqvm, sErrorString.c_str());
+ else
+ spdlog::warn(sErrorString);
+
+ return SQRESULT_NOTNULL;
+ }
+
+ DecodeJsonTable<context>(sqvm, (rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>*)&doc);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC(
+ "string",
+ EncodeJSON,
+ "table data",
+ "converts a squirrel table to a json string",
+ ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)
+{
+ rapidjson_document doc;
+ doc.SetObject();
+
+ // temp until this is just the func parameter type
+ HSquirrelVM* vm = (HSquirrelVM*)sqvm;
+ SQTable* table = vm->_stackOfCurrentFunction[1]._VAL.asTable;
+ EncodeJSONTable<context>(table, &doc, doc.GetAllocator());
+
+ rapidjson::StringBuffer buffer;
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+ doc.Accept(writer);
+ const char* pJsonString = buffer.GetString();
+
+ g_pSquirrel<context>->pushstring(sqvm, pJsonString, -1);
+ return SQRESULT_NOTNULL;
+}
diff --git a/NorthstarDLL/scripts/scriptutility.cpp b/NorthstarDLL/scripts/scriptutility.cpp
new file mode 100644
index 00000000..fa35df7b
--- /dev/null
+++ b/NorthstarDLL/scripts/scriptutility.cpp
@@ -0,0 +1,29 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+#include "client/r2client.h"
+#include "engine/r2engine.h"
+
+// asset function StringToAsset( string assetName )
+ADD_SQFUNC(
+ "asset",
+ StringToAsset,
+ "string assetName",
+ "converts a given string to an asset",
+ ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)
+{
+ g_pSquirrel<context>->pushasset(sqvm, g_pSquirrel<context>->getstring(sqvm, 1), -1);
+ return SQRESULT_NOTNULL;
+}
+
+// string function NSGetLocalPlayerUID()
+ADD_SQFUNC(
+ "string", NSGetLocalPlayerUID, "", "Returns the local player's uid.", ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)
+{
+ if (R2::g_pLocalPlayerUserID)
+ {
+ g_pSquirrel<context>->pushstring(sqvm, R2::g_pLocalPlayerUserID);
+ return SQRESULT_NOTNULL;
+ }
+
+ return SQRESULT_NULL;
+}
diff --git a/NorthstarDLL/scripts/server/miscserverfixes.cpp b/NorthstarDLL/scripts/server/miscserverfixes.cpp
new file mode 100644
index 00000000..4feca505
--- /dev/null
+++ b/NorthstarDLL/scripts/server/miscserverfixes.cpp
@@ -0,0 +1,7 @@
+#include "pch.h"
+
+ON_DLL_LOAD("server.dll", MiscServerFixes, (CModule module))
+{
+ // nop out call to VGUI shutdown since it crashes the game when quitting from the console
+ module.Offset(0x154A96).NOP(5);
+}
diff --git a/NorthstarDLL/scripts/server/miscserverscript.cpp b/NorthstarDLL/scripts/server/miscserverscript.cpp
new file mode 100644
index 00000000..b58bdfda
--- /dev/null
+++ b/NorthstarDLL/scripts/server/miscserverscript.cpp
@@ -0,0 +1,101 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+#include "masterserver/masterserver.h"
+#include "server/auth/serverauthentication.h"
+#include "dedicated/dedicated.h"
+#include "client/r2client.h"
+#include "server/r2server.h"
+
+#include <filesystem>
+
+ADD_SQFUNC("void", NSEarlyWritePlayerPersistenceForLeave, "entity player", "", ScriptContext::SERVER)
+{
+ const R2::CBasePlayer* pPlayer = g_pSquirrel<context>->getentity<R2::CBasePlayer>(sqvm, 1);
+ if (!pPlayer)
+ {
+ spdlog::warn("NSEarlyWritePlayerPersistenceForLeave got null player");
+
+ g_pSquirrel<context>->pushbool(sqvm, false);
+ return SQRESULT_NOTNULL;
+ }
+
+ R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1];
+ if (g_pServerAuthentication->m_PlayerAuthenticationData.find(pClient) == g_pServerAuthentication->m_PlayerAuthenticationData.end())
+ {
+ g_pSquirrel<context>->pushbool(sqvm, false);
+ return SQRESULT_NOTNULL;
+ }
+
+ g_pServerAuthentication->m_PlayerAuthenticationData[pClient].needPersistenceWriteOnLeave = false;
+ g_pServerAuthentication->WritePersistentData(pClient);
+ return SQRESULT_NULL;
+}
+
+ADD_SQFUNC("bool", NSIsWritingPlayerPersistence, "", "", ScriptContext::SERVER)
+{
+ g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_bSavingPersistentData);
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("bool", NSIsPlayerLocalPlayer, "entity player", "", ScriptContext::SERVER)
+{
+ const R2::CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<R2::CBasePlayer>(sqvm, 1);
+ if (!pPlayer)
+ {
+ spdlog::warn("NSIsPlayerLocalPlayer got null player");
+
+ g_pSquirrel<context>->pushbool(sqvm, false);
+ return SQRESULT_NOTNULL;
+ }
+
+ R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1];
+ g_pSquirrel<context>->pushbool(sqvm, !strcmp(R2::g_pLocalPlayerUserID, pClient->m_UID));
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC("bool", NSIsDedicated, "", "", ScriptContext::SERVER)
+{
+ g_pSquirrel<context>->pushbool(sqvm, IsDedicatedServer());
+ return SQRESULT_NOTNULL;
+}
+
+ADD_SQFUNC(
+ "bool",
+ NSDisconnectPlayer,
+ "entity player, string reason",
+ "Disconnects the player from the server with the given reason",
+ ScriptContext::SERVER)
+{
+ const R2::CBasePlayer* pPlayer = g_pSquirrel<context>->getentity<R2::CBasePlayer>(sqvm, 1);
+ const char* reason = g_pSquirrel<context>->getstring(sqvm, 2);
+
+ if (!pPlayer)
+ {
+ spdlog::warn("Attempted to call NSDisconnectPlayer() with null player.");
+
+ g_pSquirrel<context>->pushbool(sqvm, false);
+ return SQRESULT_NOTNULL;
+ }
+
+ // Shouldn't happen but I like sanity checks.
+ R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1];
+ if (!pClient)
+ {
+ spdlog::warn("NSDisconnectPlayer(): player entity has null CBaseClient!");
+
+ g_pSquirrel<context>->pushbool(sqvm, false);
+ return SQRESULT_NOTNULL;
+ }
+
+ if (reason)
+ {
+ R2::CBaseClient__Disconnect(pClient, 1, reason);
+ }
+ else
+ {
+ R2::CBaseClient__Disconnect(pClient, 1, "Disconnected by the server.");
+ }
+
+ g_pSquirrel<context>->pushbool(sqvm, true);
+ return SQRESULT_NOTNULL;
+}
diff --git a/NorthstarDLL/scripts/server/scriptuserinfo.cpp b/NorthstarDLL/scripts/server/scriptuserinfo.cpp
new file mode 100644
index 00000000..68baac0e
--- /dev/null
+++ b/NorthstarDLL/scripts/server/scriptuserinfo.cpp
@@ -0,0 +1,105 @@
+#include "pch.h"
+#include "squirrel/squirrel.h"
+#include "engine/r2engine.h"
+#include "server/r2server.h"
+
+// clang-format off
+ADD_SQFUNC("string", GetUserInfoKVString_Internal, "entity player, string key, string defaultValue = \"\"",
+ "Gets the string value of a given player's userinfo convar by name", ScriptContext::SERVER)
+// clang-format on
+{
+ const R2::CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<R2::CBasePlayer>(sqvm, 1);
+ if (!pPlayer)
+ {
+ g_pSquirrel<ScriptContext::SERVER>->raiseerror(sqvm, "player is null");
+ return SQRESULT_ERROR;
+ }
+
+ const char* pKey = g_pSquirrel<ScriptContext::SERVER>->getstring(sqvm, 2);
+ const char* pDefaultValue = g_pSquirrel<ScriptContext::SERVER>->getstring(sqvm, 3);
+
+ const char* pResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetString(pKey, pDefaultValue);
+ g_pSquirrel<ScriptContext::SERVER>->pushstring(sqvm, pResult);
+ return SQRESULT_NOTNULL;
+}
+
+// clang-format off
+ADD_SQFUNC("asset", GetUserInfoKVAsset_Internal, "entity player, string key, asset defaultValue = $\"\"",
+ "Gets the asset value of a given player's userinfo convar by name", ScriptContext::SERVER)
+// clang-format on
+{
+ const R2::CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<R2::CBasePlayer>(sqvm, 1);
+ if (!pPlayer)
+ {
+ g_pSquirrel<ScriptContext::SERVER>->raiseerror(sqvm, "player is null");
+ return SQRESULT_ERROR;
+ }
+
+ const char* pKey = g_pSquirrel<ScriptContext::SERVER>->getstring(sqvm, 2);
+ const char* pDefaultValue;
+ g_pSquirrel<ScriptContext::SERVER>->getasset(sqvm, 3, &pDefaultValue);
+
+ const char* pResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetString(pKey, pDefaultValue);
+ g_pSquirrel<ScriptContext::SERVER>->pushasset(sqvm, pResult);
+ return SQRESULT_NOTNULL;
+}
+
+// clang-format off
+ADD_SQFUNC("int", GetUserInfoKVInt_Internal, "entity player, string key, int defaultValue = 0",
+ "Gets the int value of a given player's userinfo convar by name", ScriptContext::SERVER)
+// clang-format on
+{
+ const R2::CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<R2::CBasePlayer>(sqvm, 1);
+ if (!pPlayer)
+ {
+ g_pSquirrel<ScriptContext::SERVER>->raiseerror(sqvm, "player is null");
+ return SQRESULT_ERROR;
+ }
+
+ const char* pKey = g_pSquirrel<ScriptContext::SERVER>->getstring(sqvm, 2);
+ const int iDefaultValue = g_pSquirrel<ScriptContext::SERVER>->getinteger(sqvm, 3);
+
+ const int iResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetInt(pKey, iDefaultValue);
+ g_pSquirrel<ScriptContext::SERVER>->pushinteger(sqvm, iResult);
+ return SQRESULT_NOTNULL;
+}
+
+// clang-format off
+ADD_SQFUNC("float", GetUserInfoKVFloat_Internal, "entity player, string key, float defaultValue = 0",
+ "Gets the float value of a given player's userinfo convar by name", ScriptContext::SERVER)
+// clang-format on
+{
+ const R2::CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<R2::CBasePlayer>(sqvm, 1);
+ if (!pPlayer)
+ {
+ g_pSquirrel<ScriptContext::SERVER>->raiseerror(sqvm, "player is null");
+ return SQRESULT_ERROR;
+ }
+
+ const char* pKey = g_pSquirrel<ScriptContext::SERVER>->getstring(sqvm, 2);
+ const float flDefaultValue = g_pSquirrel<ScriptContext::SERVER>->getfloat(sqvm, 3);
+
+ const float flResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetFloat(pKey, flDefaultValue);
+ g_pSquirrel<ScriptContext::SERVER>->pushfloat(sqvm, flResult);
+ return SQRESULT_NOTNULL;
+}
+
+// clang-format off
+ADD_SQFUNC("bool", GetUserInfoKVBool_Internal, "entity player, string key, bool defaultValue = false",
+ "Gets the bool value of a given player's userinfo convar by name", ScriptContext::SERVER)
+// clang-format on
+{
+ const R2::CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<R2::CBasePlayer>(sqvm, 1);
+ if (!pPlayer)
+ {
+ g_pSquirrel<ScriptContext::SERVER>->raiseerror(sqvm, "player is null");
+ return SQRESULT_ERROR;
+ }
+
+ const char* pKey = g_pSquirrel<ScriptContext::SERVER>->getstring(sqvm, 2);
+ const bool bDefaultValue = g_pSquirrel<ScriptContext::SERVER>->getbool(sqvm, 3);
+
+ const bool bResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetInt(pKey, bDefaultValue);
+ g_pSquirrel<ScriptContext::SERVER>->pushbool(sqvm, bResult);
+ return SQRESULT_NOTNULL;
+}