diff options
author | Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> | 2023-12-27 00:32:01 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-27 01:32:01 +0100 |
commit | f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287 (patch) | |
tree | 90f2c6a4885dbd181799e2325cf33588697674e1 /primedev/scripts | |
parent | bb8ed59f6891b1196c5f5bbe7346cd171c8215fa (diff) | |
download | NorthstarLauncher-f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287.tar.gz NorthstarLauncher-f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287.zip |
Folder restructuring from primedev (#624)v1.21.2-rc3v1.21.2
Copies of over the primedev folder structure for easier cherry-picking of further changes
Co-authored-by: F1F7Y <filip.bartos07@proton.me>
Diffstat (limited to 'primedev/scripts')
-rw-r--r-- | primedev/scripts/client/clientchathooks.cpp | 72 | ||||
-rw-r--r-- | primedev/scripts/client/cursorposition.cpp | 22 | ||||
-rw-r--r-- | primedev/scripts/client/scriptbrowserhooks.cpp | 24 | ||||
-rw-r--r-- | primedev/scripts/client/scriptmainmenupromos.cpp | 123 | ||||
-rw-r--r-- | primedev/scripts/client/scriptmodmenu.cpp | 165 | ||||
-rw-r--r-- | primedev/scripts/client/scriptoriginauth.cpp | 35 | ||||
-rw-r--r-- | primedev/scripts/client/scriptserverbrowser.cpp | 209 | ||||
-rw-r--r-- | primedev/scripts/client/scriptservertoclientstringcommand.cpp | 18 | ||||
-rw-r--r-- | primedev/scripts/scriptdatatables.cpp | 909 | ||||
-rw-r--r-- | primedev/scripts/scripthttprequesthandler.cpp | 585 | ||||
-rw-r--r-- | primedev/scripts/scripthttprequesthandler.h | 130 | ||||
-rw-r--r-- | primedev/scripts/scriptjson.cpp | 250 | ||||
-rw-r--r-- | primedev/scripts/scriptjson.h | 13 | ||||
-rw-r--r-- | primedev/scripts/scriptutility.cpp | 28 | ||||
-rw-r--r-- | primedev/scripts/server/miscserverfixes.cpp | 6 | ||||
-rw-r--r-- | primedev/scripts/server/miscserverscript.cpp | 100 | ||||
-rw-r--r-- | primedev/scripts/server/scriptuserinfo.cpp | 104 |
17 files changed, 2793 insertions, 0 deletions
diff --git a/primedev/scripts/client/clientchathooks.cpp b/primedev/scripts/client/clientchathooks.cpp new file mode 100644 index 00000000..e084f47e --- /dev/null +++ b/primedev/scripts/client/clientchathooks.cpp @@ -0,0 +1,72 @@ +#include "squirrel/squirrel.h" +#include "util/utils.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; + } + + RemoveAsciiControlSequences(const_cast<char*>(message), true); + + 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/primedev/scripts/client/cursorposition.cpp b/primedev/scripts/client/cursorposition.cpp new file mode 100644 index 00000000..c0e8623c --- /dev/null +++ b/primedev/scripts/client/cursorposition.cpp @@ -0,0 +1,22 @@ +#include "squirrel/squirrel.h" +#include "util/wininfo.h" + +ADD_SQFUNC("vector ornull", NSGetCursorPosition, "", "", ScriptContext::UI) +{ + RECT rcClient; + POINT p; + if (GetCursorPos(&p) && ScreenToClient(*g_gameHWND, &p) && GetClientRect(*g_gameHWND, &rcClient)) + { + if (GetAncestor(GetForegroundWindow(), GA_ROOTOWNER) != *g_gameHWND) + return SQRESULT_NULL; + + g_pSquirrel<context>->pushvector( + sqvm, + {p.x > 0 ? p.x > rcClient.right ? rcClient.right : (float)p.x : 0, + p.y > 0 ? p.y > rcClient.bottom ? rcClient.bottom : (float)p.y : 0, + 0}); + return SQRESULT_NOTNULL; + } + g_pSquirrel<context>->raiseerror(sqvm, "Failed retrieving cursor position of game window"); + return SQRESULT_ERROR; +} diff --git a/primedev/scripts/client/scriptbrowserhooks.cpp b/primedev/scripts/client/scriptbrowserhooks.cpp new file mode 100644 index 00000000..86b4a356 --- /dev/null +++ b/primedev/scripts/client/scriptbrowserhooks.cpp @@ -0,0 +1,24 @@ + +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).RCast<bool*>(); +} diff --git a/primedev/scripts/client/scriptmainmenupromos.cpp b/primedev/scripts/client/scriptmainmenupromos.cpp new file mode 100644 index 00000000..ecb47af7 --- /dev/null +++ b/primedev/scripts/client/scriptmainmenupromos.cpp @@ -0,0 +1,123 @@ +#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/primedev/scripts/client/scriptmodmenu.cpp b/primedev/scripts/client/scriptmodmenu.cpp new file mode 100644 index 00000000..a88478fb --- /dev/null +++ b/primedev/scripts/client/scriptmodmenu.cpp @@ -0,0 +1,165 @@ +#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/primedev/scripts/client/scriptoriginauth.cpp b/primedev/scripts/client/scriptoriginauth.cpp new file mode 100644 index 00000000..420c4872 --- /dev/null +++ b/primedev/scripts/client/scriptoriginauth.cpp @@ -0,0 +1,35 @@ +#include "squirrel/squirrel.h" +#include "masterserver/masterserver.h" +#include "engine/r2engine.h" +#include "client/r2client.h" + +ADD_SQFUNC("bool", NSIsMasterServerAuthenticated, "", "", ScriptContext::UI) +{ + g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_bOriginAuthWithMasterServerDone); + return SQRESULT_NOTNULL; +} + +/* +global struct MasterServerAuthResult +{ + bool success + string errorCode + string errorMessage +} +*/ + +ADD_SQFUNC("MasterServerAuthResult", NSGetMasterServerAuthResult, "", "", ScriptContext::UI) +{ + g_pSquirrel<context>->pushnewstructinstance(sqvm, 3); + + g_pSquirrel<context>->pushbool(sqvm, g_pMasterServerManager->m_bOriginAuthWithMasterServerSuccessful); + g_pSquirrel<context>->sealstructslot(sqvm, 0); + + g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_sOriginAuthWithMasterServerErrorCode.c_str(), -1); + g_pSquirrel<context>->sealstructslot(sqvm, 1); + + g_pSquirrel<context>->pushstring(sqvm, g_pMasterServerManager->m_sOriginAuthWithMasterServerErrorMessage.c_str(), -1); + g_pSquirrel<context>->sealstructslot(sqvm, 2); + + return SQRESULT_NOTNULL; +} diff --git a/primedev/scripts/client/scriptserverbrowser.cpp b/primedev/scripts/client/scriptserverbrowser.cpp new file mode 100644 index 00000000..a142c3f4 --- /dev/null +++ b/primedev/scripts/client/scriptserverbrowser.cpp @@ -0,0 +1,209 @@ +#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("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("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( + g_pLocalPlayerUserID, + g_pMasterServerManager->m_sOwnClientAuthToken, + g_pMasterServerManager->m_vRemoteServers[serverIndex], + (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 + g_pCVar->FindVar("serverfilter")->SetValue(info.authToken); + Cbuf_AddText( + 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(), + cmd_source_t::kCommandSrcCode); + + g_pMasterServerManager->m_bHasPendingConnectionInfo = false; + return SQRESULT_NULL; +} + +ADD_SQFUNC("void", NSTryAuthWithLocalServer, "", "", ScriptContext::UI) +{ + // do auth request + g_pMasterServerManager->AuthenticateWithOwnServer(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()) + 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; +} + +ADD_SQFUNC("array<ServerInfo>", NSGetGameServers, "", "", ScriptContext::UI) +{ + g_pSquirrel<context>->newarray(sqvm, 0); + for (size_t i = 0; i < g_pMasterServerManager->m_vRemoteServers.size(); i++) + { + const RemoteServerInfo& remoteServer = g_pMasterServerManager->m_vRemoteServers[i]; + + g_pSquirrel<context>->pushnewstructinstance(sqvm, 11); + + // index + g_pSquirrel<context>->pushinteger(sqvm, i); + g_pSquirrel<context>->sealstructslot(sqvm, 0); + + // id + g_pSquirrel<context>->pushstring(sqvm, remoteServer.id, -1); + g_pSquirrel<context>->sealstructslot(sqvm, 1); + + // name + g_pSquirrel<context>->pushstring(sqvm, remoteServer.name, -1); + g_pSquirrel<context>->sealstructslot(sqvm, 2); + + // description + g_pSquirrel<context>->pushstring(sqvm, remoteServer.description.c_str(), -1); + g_pSquirrel<context>->sealstructslot(sqvm, 3); + + // map + g_pSquirrel<context>->pushstring(sqvm, remoteServer.map, -1); + g_pSquirrel<context>->sealstructslot(sqvm, 4); + + // playlist + g_pSquirrel<context>->pushstring(sqvm, remoteServer.playlist, -1); + g_pSquirrel<context>->sealstructslot(sqvm, 5); + + // playerCount + g_pSquirrel<context>->pushinteger(sqvm, remoteServer.playerCount); + g_pSquirrel<context>->sealstructslot(sqvm, 6); + + // maxPlayerCount + g_pSquirrel<context>->pushinteger(sqvm, remoteServer.maxPlayers); + g_pSquirrel<context>->sealstructslot(sqvm, 7); + + // requiresPassword + g_pSquirrel<context>->pushbool(sqvm, remoteServer.requiresPassword); + g_pSquirrel<context>->sealstructslot(sqvm, 8); + + // region + g_pSquirrel<context>->pushstring(sqvm, remoteServer.region, -1); + g_pSquirrel<context>->sealstructslot(sqvm, 9); + + // requiredMods + g_pSquirrel<context>->newarray(sqvm); + for (const RemoteModInfo& mod : remoteServer.requiredMods) + { + g_pSquirrel<context>->pushnewstructinstance(sqvm, 2); + + // name + g_pSquirrel<context>->pushstring(sqvm, mod.Name.c_str(), -1); + g_pSquirrel<context>->sealstructslot(sqvm, 0); + + // version + g_pSquirrel<context>->pushstring(sqvm, mod.Version.c_str(), -1); + g_pSquirrel<context>->sealstructslot(sqvm, 1); + + g_pSquirrel<context>->arrayappend(sqvm, -2); + } + g_pSquirrel<context>->sealstructslot(sqvm, 10); + + g_pSquirrel<context>->arrayappend(sqvm, -2); + } + return SQRESULT_NOTNULL; +} diff --git a/primedev/scripts/client/scriptservertoclientstringcommand.cpp b/primedev/scripts/client/scriptservertoclientstringcommand.cpp new file mode 100644 index 00000000..a3a81c8a --- /dev/null +++ b/primedev/scripts/client/scriptservertoclientstringcommand.cpp @@ -0,0 +1,18 @@ +#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/primedev/scripts/scriptdatatables.cpp b/primedev/scripts/scriptdatatables.cpp new file mode 100644 index 00000000..87a26dca --- /dev/null +++ b/primedev/scripts/scriptdatatables.cpp @@ -0,0 +1,909 @@ +#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>->template 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 ((*g_pFilesystem)->m_vtable2->FileExists(&(*g_pFilesystem)->m_vtable2, sDiskAssetPath.c_str(), "GAME")) + { + std::string sTableCSV = 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>->template 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 = (Vector3*)(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", 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).RCast<Datatable* (*)(HSquirrelVM*)>(); +} + +ON_DLL_LOAD_RELIESON("client.dll", ClientScriptDatatables, ClientSquirrel, (CModule module)) +{ + SQ_GetDatatableInternal<ScriptContext::CLIENT> = module.Offset(0x1C9070).RCast<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() && 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/primedev/scripts/scripthttprequesthandler.cpp b/primedev/scripts/scripthttprequesthandler.cpp new file mode 100644 index 00000000..aa75127a --- /dev/null +++ b/primedev/scripts/scripthttprequesthandler.cpp @@ -0,0 +1,585 @@ +#include "scripthttprequesthandler.h" +#include "util/version.h" +#include "squirrel/squirrel.h" +#include "core/tier0.h" + +HttpRequestHandler* g_httpRequestHandler; + +bool IsHttpDisabled() +{ + const static bool bIsHttpDisabled = CommandLine()->FindParm("-disablehttprequests"); + return bIsHttpDisabled; +} + +bool IsLocalHttpAllowed() +{ + const static bool bIsLocalHttpAllowed = CommandLine()->FindParm("-allowlocalhttp"); + return bIsLocalHttpAllowed; +} + +bool DisableHttpSsl() +{ + const static bool bDisableHttpSsl = 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, ¤tQuery, 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; +} + +// 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; +} + +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>); +} + +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/primedev/scripts/scripthttprequesthandler.h b/primedev/scripts/scripthttprequesthandler.h new file mode 100644 index 00000000..f3921f4e --- /dev/null +++ b/primedev/scripts/scripthttprequesthandler.h @@ -0,0 +1,130 @@ +#pragma once + +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/primedev/scripts/scriptjson.cpp b/primedev/scripts/scriptjson.cpp new file mode 100644 index 00000000..06bda6f4 --- /dev/null +++ b/primedev/scripts/scriptjson.cpp @@ -0,0 +1,250 @@ +#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()); + return SQRESULT_ERROR; + } + + 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/primedev/scripts/scriptjson.h b/primedev/scripts/scriptjson.h new file mode 100644 index 00000000..b747106b --- /dev/null +++ b/primedev/scripts/scriptjson.h @@ -0,0 +1,13 @@ +#pragma once + +#include "rapidjson/document.h" +#include "rapidjson/writer.h" +#include "rapidjson/stringbuffer.h" + +template <ScriptContext context> void EncodeJSONTable( + SQTable* table, + rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>* obj, + rapidjson::MemoryPoolAllocator<SourceAllocator>& allocator); + +template <ScriptContext context> void +DecodeJsonTable(HSquirrelVM* sqvm, rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<SourceAllocator>>* obj); diff --git a/primedev/scripts/scriptutility.cpp b/primedev/scripts/scriptutility.cpp new file mode 100644 index 00000000..4b92fa02 --- /dev/null +++ b/primedev/scripts/scriptutility.cpp @@ -0,0 +1,28 @@ +#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 (g_pLocalPlayerUserID) + { + g_pSquirrel<context>->pushstring(sqvm, g_pLocalPlayerUserID); + return SQRESULT_NOTNULL; + } + + return SQRESULT_NULL; +} diff --git a/primedev/scripts/server/miscserverfixes.cpp b/primedev/scripts/server/miscserverfixes.cpp new file mode 100644 index 00000000..48c2c111 --- /dev/null +++ b/primedev/scripts/server/miscserverfixes.cpp @@ -0,0 +1,6 @@ + +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/primedev/scripts/server/miscserverscript.cpp b/primedev/scripts/server/miscserverscript.cpp new file mode 100644 index 00000000..ed6e4800 --- /dev/null +++ b/primedev/scripts/server/miscserverscript.cpp @@ -0,0 +1,100 @@ +#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 CBasePlayer* pPlayer = g_pSquirrel<context>->template getentity<CBasePlayer>(sqvm, 1); + if (!pPlayer) + { + spdlog::warn("NSEarlyWritePlayerPersistenceForLeave got null player"); + + g_pSquirrel<context>->pushbool(sqvm, false); + return SQRESULT_NOTNULL; + } + + CBaseClient* pClient = &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 CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->template getentity<CBasePlayer>(sqvm, 1); + if (!pPlayer) + { + spdlog::warn("NSIsPlayerLocalPlayer got null player"); + + g_pSquirrel<context>->pushbool(sqvm, false); + return SQRESULT_NOTNULL; + } + + CBaseClient* pClient = &g_pClientArray[pPlayer->m_nPlayerIndex - 1]; + g_pSquirrel<context>->pushbool(sqvm, !strcmp(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 CBasePlayer* pPlayer = g_pSquirrel<context>->template getentity<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. + CBaseClient* pClient = &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) + { + CBaseClient__Disconnect(pClient, 1, reason); + } + else + { + CBaseClient__Disconnect(pClient, 1, "Disconnected by the server."); + } + + g_pSquirrel<context>->pushbool(sqvm, true); + return SQRESULT_NOTNULL; +} diff --git a/primedev/scripts/server/scriptuserinfo.cpp b/primedev/scripts/server/scriptuserinfo.cpp new file mode 100644 index 00000000..c53a9d22 --- /dev/null +++ b/primedev/scripts/server/scriptuserinfo.cpp @@ -0,0 +1,104 @@ +#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 CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->template getentity<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 = 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 CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->template getentity<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 = 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 CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->template getentity<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 = 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 CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<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 = 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 CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<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 = g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetInt(pKey, bDefaultValue); + g_pSquirrel<ScriptContext::SERVER>->pushbool(sqvm, bResult); + return SQRESULT_NOTNULL; +} |