aboutsummaryrefslogtreecommitdiff
path: root/NorthstarDLL/scripts
diff options
context:
space:
mode:
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/scriptjson.cpp249
-rw-r--r--NorthstarDLL/scripts/scriptutility.cpp14
-rw-r--r--NorthstarDLL/scripts/server/miscserverfixes.cpp7
-rw-r--r--NorthstarDLL/scripts/server/miscserverscript.cpp60
-rw-r--r--NorthstarDLL/scripts/server/scriptuserinfo.cpp105
12 files changed, 2159 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/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..8dae49cf
--- /dev/null
+++ b/NorthstarDLL/scripts/scriptutility.cpp
@@ -0,0 +1,14 @@
+#include "pch.h"
+#include "squirrel/squirrel.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;
+}
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..0d865388
--- /dev/null
+++ b/NorthstarDLL/scripts/server/miscserverscript.cpp
@@ -0,0 +1,60 @@
+#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;
+}
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;
+}