aboutsummaryrefslogtreecommitdiff
path: root/primedev/util
diff options
context:
space:
mode:
Diffstat (limited to 'primedev/util')
-rw-r--r--primedev/util/printcommands.cpp285
-rw-r--r--primedev/util/printcommands.h6
-rw-r--r--primedev/util/printmaps.cpp233
-rw-r--r--primedev/util/printmaps.h2
-rw-r--r--primedev/util/utils.cpp82
-rw-r--r--primedev/util/utils.h3
-rw-r--r--primedev/util/version.cpp95
-rw-r--r--primedev/util/version.h6
-rw-r--r--primedev/util/wininfo.cpp9
-rw-r--r--primedev/util/wininfo.h4
10 files changed, 725 insertions, 0 deletions
diff --git a/primedev/util/printcommands.cpp b/primedev/util/printcommands.cpp
new file mode 100644
index 00000000..34d56666
--- /dev/null
+++ b/primedev/util/printcommands.cpp
@@ -0,0 +1,285 @@
+#include "printcommands.h"
+#include "core/convar/cvar.h"
+#include "core/convar/convar.h"
+#include "core/convar/concommand.h"
+
+void PrintCommandHelpDialogue(const ConCommandBase* command, const char* name)
+{
+ if (!command)
+ {
+ spdlog::info("unknown command {}", name);
+ return;
+ }
+
+ // temp because command->IsCommand does not currently work
+ ConVar* cvar = g_pCVar->FindVar(command->m_pszName);
+
+ // build string for flags if not FCVAR_NONE
+ std::string flagString;
+ if (command->GetFlags() != FCVAR_NONE)
+ {
+ flagString = "( ";
+
+ for (auto& flagPair : g_PrintCommandFlags)
+ {
+ if (command->GetFlags() & flagPair.first)
+ {
+ // special case, slightly hacky: PRINTABLEONLY is for commands, GAMEDLL_FOR_REMOTE_CLIENTS is for concommands, both have the
+ // same value
+ if (flagPair.first == FCVAR_PRINTABLEONLY)
+ {
+ if (cvar && !strcmp(flagPair.second, "GAMEDLL_FOR_REMOTE_CLIENTS"))
+ continue;
+
+ if (!cvar && !strcmp(flagPair.second, "PRINTABLEONLY"))
+ continue;
+ }
+
+ flagString += flagPair.second;
+ flagString += " ";
+ }
+ }
+
+ flagString += ") ";
+ }
+
+ if (cvar)
+ spdlog::info("\"{}\" = \"{}\" {}- {}", cvar->GetBaseName(), cvar->GetString(), flagString, cvar->GetHelpText());
+ else
+ spdlog::info("\"{}\" {} - {}", command->m_pszName, flagString, command->GetHelpText());
+}
+
+void TryPrintCvarHelpForCommand(const char* pCommand)
+{
+ // try to display help text for an inputted command string from the console
+ int pCommandLen = strlen(pCommand);
+ char* pCvarStr = new char[pCommandLen];
+ strcpy(pCvarStr, pCommand);
+
+ // trim whitespace from right
+ for (int i = pCommandLen - 1; i; i--)
+ {
+ if (isspace(pCvarStr[i]))
+ pCvarStr[i] = '\0';
+ else
+ break;
+ }
+
+ // check if we're inputting a cvar, but not setting it at all
+ ConVar* cvar = g_pCVar->FindVar(pCvarStr);
+ if (cvar)
+ PrintCommandHelpDialogue(&cvar->m_ConCommandBase, pCvarStr);
+
+ delete[] pCvarStr;
+}
+
+void ConCommand_help(const CCommand& arg)
+{
+ if (arg.ArgC() < 2)
+ {
+ spdlog::info("Usage: help <cvarname>");
+ return;
+ }
+
+ PrintCommandHelpDialogue(g_pCVar->FindCommandBase(arg.Arg(1)), arg.Arg(1));
+}
+
+void ConCommand_find(const CCommand& arg)
+{
+ if (arg.ArgC() < 2)
+ {
+ spdlog::info("Usage: find <string> [<string>...]");
+ return;
+ }
+
+ char pTempName[256];
+ char pTempSearchTerm[256];
+
+ ConCommandBase* var;
+ CCVarIteratorInternal* itint = g_pCVar->FactoryInternalIterator();
+ std::map<std::string, ConCommandBase*> sorted;
+ for (itint->SetFirst(); itint->IsValid(); itint->Next())
+ {
+ var = itint->Get();
+ if (!var->IsFlagSet(FCVAR_DEVELOPMENTONLY) && !var->IsFlagSet(FCVAR_HIDDEN))
+ {
+ sorted.insert({var->m_pszName, var});
+ }
+ }
+ delete itint;
+
+ for (auto& map : sorted)
+ {
+ bool bPrintCommand = true;
+ for (int i = 0; i < arg.ArgC() - 1; i++)
+ {
+ // make lowercase to avoid case sensitivity
+ strncpy_s(pTempName, sizeof(pTempName), map.second->m_pszName, sizeof(pTempName) - 1);
+ strncpy_s(pTempSearchTerm, sizeof(pTempSearchTerm), arg.Arg(i + 1), sizeof(pTempSearchTerm) - 1);
+
+ for (int i = 0; pTempName[i]; i++)
+ pTempName[i] = tolower(pTempName[i]);
+
+ for (int i = 0; pTempSearchTerm[i]; i++)
+ pTempSearchTerm[i] = tolower(pTempSearchTerm[i]);
+
+ if (!strstr(pTempName, pTempSearchTerm))
+ {
+ bPrintCommand = false;
+ break;
+ }
+ }
+
+ if (bPrintCommand)
+ PrintCommandHelpDialogue(map.second, map.second->m_pszName);
+ }
+}
+
+void ConCommand_findflags(const CCommand& arg)
+{
+ if (arg.ArgC() < 2)
+ {
+ spdlog::info("Usage: findflags <string>");
+ for (auto& flagPair : g_PrintCommandFlags)
+ spdlog::info(" - {}", flagPair.second);
+
+ return;
+ }
+
+ // convert input flag to uppercase
+ char* upperFlag = new char[strlen(arg.Arg(1))];
+ strcpy(upperFlag, arg.Arg(1));
+
+ for (int i = 0; upperFlag[i]; i++)
+ upperFlag[i] = toupper(upperFlag[i]);
+
+ // resolve flag name => int flags
+ int resolvedFlag = FCVAR_NONE;
+ for (auto& flagPair : g_PrintCommandFlags)
+ {
+ if (!strcmp(flagPair.second, upperFlag))
+ {
+ resolvedFlag |= flagPair.first;
+ break;
+ }
+ }
+
+ ConCommandBase* var;
+ CCVarIteratorInternal* itint = g_pCVar->FactoryInternalIterator();
+ std::map<std::string, ConCommandBase*> sorted;
+ for (itint->SetFirst(); itint->IsValid(); itint->Next())
+ {
+ var = itint->Get();
+ if (!var->IsFlagSet(FCVAR_DEVELOPMENTONLY) && !var->IsFlagSet(FCVAR_HIDDEN))
+ {
+ sorted.insert({var->m_pszName, var});
+ }
+ }
+ delete itint;
+
+ for (auto& map : sorted)
+ {
+ if (map.second->m_nFlags & resolvedFlag)
+ PrintCommandHelpDialogue(map.second, map.second->m_pszName);
+ }
+
+ delete[] upperFlag;
+}
+
+void ConCommand_list(const CCommand& arg)
+{
+ ConCommandBase* var;
+ CCVarIteratorInternal* itint = g_pCVar->FactoryInternalIterator();
+ std::map<std::string, ConCommandBase*> sorted;
+ for (itint->SetFirst(); itint->IsValid(); itint->Next())
+ {
+ var = itint->Get();
+ if (!var->IsFlagSet(FCVAR_DEVELOPMENTONLY) && !var->IsFlagSet(FCVAR_HIDDEN))
+ {
+ sorted.insert({var->m_pszName, var});
+ }
+ }
+ delete itint;
+
+ for (auto& map : sorted)
+ {
+ PrintCommandHelpDialogue(map.second, map.second->m_pszName);
+ }
+ spdlog::info("{} total convars/concommands", sorted.size());
+}
+
+void ConCommand_differences(const CCommand& arg)
+{
+ CCVarIteratorInternal* itint = g_pCVar->FactoryInternalIterator();
+ std::map<std::string, ConCommandBase*> sorted;
+
+ for (itint->SetFirst(); itint->IsValid(); itint->Next())
+ {
+ ConCommandBase* var = itint->Get();
+ if (!var->IsFlagSet(FCVAR_DEVELOPMENTONLY) && !var->IsFlagSet(FCVAR_HIDDEN))
+ {
+ sorted.insert({var->m_pszName, var});
+ }
+ }
+ delete itint;
+
+ for (auto& map : sorted)
+ {
+ ConVar* cvar = g_pCVar->FindVar(map.second->m_pszName);
+
+ if (!cvar)
+ {
+ continue;
+ }
+
+ if (strcmp(cvar->GetString(), "FCVAR_NEVER_AS_STRING") == NULL)
+ {
+ continue;
+ }
+
+ if (strcmp(cvar->GetString(), cvar->m_pszDefaultValue) == NULL)
+ {
+ continue;
+ }
+
+ std::string formatted =
+ fmt::format("\"{}\" = \"{}\" ( def. \"{}\" )", cvar->GetBaseName(), cvar->GetString(), cvar->m_pszDefaultValue);
+
+ if (cvar->m_bHasMin)
+ {
+ formatted.append(fmt::format(" min. {}", cvar->m_fMinVal));
+ }
+
+ if (cvar->m_bHasMax)
+ {
+ formatted.append(fmt::format(" max. {}", cvar->m_fMaxVal));
+ }
+
+ formatted.append(fmt::format(" - {}", cvar->GetHelpText()));
+ spdlog::info(formatted);
+ }
+}
+
+void InitialiseCommandPrint()
+{
+ RegisterConCommand(
+ "convar_find", ConCommand_find, "Find convars/concommands with the specified string in their name/help text.", FCVAR_NONE);
+
+ // these commands already exist, so we need to modify the preexisting command to use our func instead
+ // and clear the flags also
+ ConCommand* helpCommand = g_pCVar->FindCommand("help");
+ helpCommand->m_nFlags = FCVAR_NONE;
+ helpCommand->m_pCommandCallback = ConCommand_help;
+
+ ConCommand* findCommand = g_pCVar->FindCommand("convar_findByFlags");
+ findCommand->m_nFlags = FCVAR_NONE;
+ findCommand->m_pCommandCallback = ConCommand_findflags;
+
+ ConCommand* listCommand = g_pCVar->FindCommand("convar_list");
+ listCommand->m_nFlags = FCVAR_NONE;
+ listCommand->m_pCommandCallback = ConCommand_list;
+
+ ConCommand* diffCommand = g_pCVar->FindCommand("convar_differences");
+ diffCommand->m_nFlags = FCVAR_NONE;
+ diffCommand->m_pCommandCallback = ConCommand_differences;
+}
diff --git a/primedev/util/printcommands.h b/primedev/util/printcommands.h
new file mode 100644
index 00000000..cb72e5cc
--- /dev/null
+++ b/primedev/util/printcommands.h
@@ -0,0 +1,6 @@
+#pragma once
+#include "core/convar/concommand.h"
+
+void PrintCommandHelpDialogue(const ConCommandBase* command, const char* name);
+void TryPrintCvarHelpForCommand(const char* pCommand);
+void InitialiseCommandPrint();
diff --git a/primedev/util/printmaps.cpp b/primedev/util/printmaps.cpp
new file mode 100644
index 00000000..d3253605
--- /dev/null
+++ b/primedev/util/printmaps.cpp
@@ -0,0 +1,233 @@
+#include "printmaps.h"
+#include "core/convar/convar.h"
+#include "core/convar/concommand.h"
+#include "mods/modmanager.h"
+#include "core/tier0.h"
+#include "engine/r2engine.h"
+#include "squirrel/squirrel.h"
+
+#include <filesystem>
+#include <regex>
+
+AUTOHOOK_INIT()
+
+enum class MapSource_t
+{
+ VPK,
+ GAMEDIR,
+ MOD
+};
+
+const std::unordered_map<MapSource_t, const char*> PrintMapSource = {
+ {MapSource_t::VPK, "VPK"}, {MapSource_t::MOD, "MOD"}, {MapSource_t::GAMEDIR, "R2"}};
+
+struct MapVPKInfo
+{
+ std::string name;
+ std::string parent;
+ MapSource_t source;
+};
+
+// our current list of maps in the game
+std::vector<MapVPKInfo> vMapList;
+
+typedef void (*Host_Map_helperType)(const CCommand&, void*);
+typedef void (*Host_Changelevel_fType)(const CCommand&);
+
+Host_Map_helperType Host_Map_helper;
+Host_Changelevel_fType Host_Changelevel_f;
+
+void RefreshMapList()
+{
+ // Only update the maps list every 10 seconds max to we avoid constantly reading fs
+ static double fLastRefresh = -999;
+
+ if (fLastRefresh + 10.0 > g_pGlobals->m_flRealTime)
+ return;
+
+ fLastRefresh = g_pGlobals->m_flRealTime;
+
+ // Rebuild map list
+ vMapList.clear();
+
+ // get modded maps
+ // TODO: could probably check mod vpks to get mapnames from there too?
+ for (auto& modFilePair : g_pModManager->m_ModFiles)
+ {
+ ModOverrideFile file = modFilePair.second;
+ if (file.m_Path.extension() == ".bsp" && file.m_Path.parent_path().string() == "maps") // only allow mod maps actually in /maps atm
+ {
+ MapVPKInfo& map = vMapList.emplace_back();
+ map.name = file.m_Path.stem().string();
+ map.parent = file.m_pOwningMod->Name;
+ map.source = MapSource_t::MOD;
+ }
+ }
+
+ // get maps in vpk
+ {
+ const int iNumRetailNonMapVpks = 1;
+ static const char* const ppRetailNonMapVpks[] = {
+ "englishclient_frontend.bsp.pak000_dir.vpk"}; // don't include mp_common here as it contains mp_lobby
+
+ // matches directory vpks, and captures their map name in the first group
+ static const std::regex rVpkMapRegex("englishclient_([a-zA-Z0-9_]+)\\.bsp\\.pak000_dir\\.vpk", std::regex::icase);
+
+ for (fs::directory_entry file : fs::directory_iterator("./vpk"))
+ {
+ std::string pathString = file.path().filename().string();
+
+ bool bIsValidMapVpk = true;
+ for (int i = 0; i < iNumRetailNonMapVpks; i++)
+ {
+ if (!pathString.compare(ppRetailNonMapVpks[i]))
+ {
+ bIsValidMapVpk = false;
+ break;
+ }
+ }
+
+ if (!bIsValidMapVpk)
+ continue;
+
+ // run our map vpk regex on the filename
+ std::smatch match;
+ std::regex_match(pathString, match, rVpkMapRegex);
+
+ if (match.length() < 2)
+ continue;
+
+ std::string mapName = match[1].str();
+ // special case: englishclient_mp_common contains mp_lobby, so hardcode the name here
+ if (mapName == "mp_common")
+ mapName = "mp_lobby";
+
+ MapVPKInfo& map = vMapList.emplace_back();
+ map.name = mapName;
+ map.parent = pathString;
+ map.source = MapSource_t::VPK;
+ }
+ }
+
+ // get maps in game dir
+ std::string gameDir = fmt::format("{}/maps", g_pModName);
+ if (!std::filesystem::exists(gameDir))
+ {
+ return;
+ }
+
+ for (fs::directory_entry file : fs::directory_iterator(gameDir))
+ {
+ if (file.path().extension() == ".bsp")
+ {
+ MapVPKInfo& map = vMapList.emplace_back();
+ map.name = file.path().stem().string();
+ map.parent = "R2";
+ map.source = MapSource_t::GAMEDIR;
+ }
+ }
+}
+
+// clang-format off
+AUTOHOOK(_Host_Map_f_CompletionFunc, engine.dll + 0x161AE0,
+int, __fastcall, (const char *const cmdname, const char *const partial, char commands[COMMAND_COMPLETION_MAXITEMS][COMMAND_COMPLETION_ITEM_LENGTH]))
+// clang-format on
+{
+ RefreshMapList();
+
+ // use a custom autocomplete func for all map loading commands
+ const int cmdLength = strlen(cmdname);
+ const char* query = partial + cmdLength;
+ const int queryLength = strlen(query);
+
+ int numMaps = 0;
+ for (int i = 0; i < vMapList.size() && numMaps < COMMAND_COMPLETION_MAXITEMS; i++)
+ {
+ if (!strncmp(query, vMapList[i].name.c_str(), queryLength))
+ {
+ strcpy(commands[numMaps], cmdname);
+ strncpy_s(
+ commands[numMaps++] + cmdLength,
+ COMMAND_COMPLETION_ITEM_LENGTH,
+ &vMapList[i].name[0],
+ COMMAND_COMPLETION_ITEM_LENGTH - cmdLength);
+ }
+ }
+
+ return numMaps;
+}
+
+ADD_SQFUNC(
+ "array<string>",
+ NSGetLoadedMapNames,
+ "",
+ "Returns a string array of loaded map file names",
+ ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)
+{
+ // Maybe we should call this on mods reload instead
+ RefreshMapList();
+
+ g_pSquirrel<context>->newarray(sqvm, 0);
+
+ for (MapVPKInfo& map : vMapList)
+ {
+ g_pSquirrel<context>->pushstring(sqvm, map.name.c_str());
+ g_pSquirrel<context>->arrayappend(sqvm, -2);
+ }
+
+ return SQRESULT_NOTNULL;
+}
+
+void ConCommand_maps(const CCommand& args)
+{
+ if (args.ArgC() < 2)
+ {
+ spdlog::info("Usage: maps <substring>");
+ spdlog::info("maps * for full listing");
+ return;
+ }
+
+ RefreshMapList();
+
+ for (MapVPKInfo& map : vMapList) // need to figure out a nice way to include parent path without making the formatting awful
+ if ((*args.Arg(1) == '*' && !args.Arg(1)[1]) || strstr(map.name.c_str(), args.Arg(1)))
+ spdlog::info("({}) {}", PrintMapSource.at(map.source), map.name);
+}
+
+// clang-format off
+AUTOHOOK(Host_Map_f, engine.dll + 0x15B340, void, __fastcall, (const CCommand& args))
+// clang-format on
+{
+ RefreshMapList();
+
+ if (args.ArgC() > 1 &&
+ std::find_if(vMapList.begin(), vMapList.end(), [&](MapVPKInfo map) -> bool { return map.name == args.Arg(1); }) == vMapList.end())
+ {
+ spdlog::warn("Map load failed: {} not found or invalid", args.Arg(1));
+ return;
+ }
+ else if (args.ArgC() == 1)
+ {
+ spdlog::warn("Map load failed: no map name provided");
+ return;
+ }
+
+ if (*g_pServerState >= server_state_t::ss_active)
+ return Host_Changelevel_f(args);
+ else
+ return Host_Map_helper(args, nullptr);
+}
+
+void InitialiseMapsPrint()
+{
+ AUTOHOOK_DISPATCH()
+
+ ConCommand* mapsCommand = g_pCVar->FindCommand("maps");
+ mapsCommand->m_pCommandCallback = ConCommand_maps;
+}
+
+ON_DLL_LOAD("engine.dll", Host_Map_f, (CModule module))
+{
+ Host_Map_helper = module.Offset(0x15AEF0).RCast<Host_Map_helperType>();
+ Host_Changelevel_f = module.Offset(0x15AAD0).RCast<Host_Changelevel_fType>();
+}
diff --git a/primedev/util/printmaps.h b/primedev/util/printmaps.h
new file mode 100644
index 00000000..b01761c0
--- /dev/null
+++ b/primedev/util/printmaps.h
@@ -0,0 +1,2 @@
+#pragma once
+void InitialiseMapsPrint();
diff --git a/primedev/util/utils.cpp b/primedev/util/utils.cpp
new file mode 100644
index 00000000..c3f90cfa
--- /dev/null
+++ b/primedev/util/utils.cpp
@@ -0,0 +1,82 @@
+#include <ctype.h>
+#include "utils.h"
+
+bool skip_valid_ansi_csi_sgr(char*& str)
+{
+ if (*str++ != '\x1B')
+ return false;
+ if (*str++ != '[') // CSI
+ return false;
+ for (char* c = str; *c; c++)
+ {
+ if (*c >= '0' && *c <= '9')
+ continue;
+ if (*c == ';' || *c == ':')
+ continue;
+ if (*c == 'm') // SGR
+ break;
+ return false;
+ }
+ return true;
+}
+
+void RemoveAsciiControlSequences(char* str, bool allow_color_codes)
+{
+ for (char *pc = str, c = *pc; c = *pc; pc++)
+ {
+ // skip UTF-8 characters
+ int bytesToSkip = 0;
+ if ((c & 0xE0) == 0xC0)
+ bytesToSkip = 1; // skip 2-byte UTF-8 sequence
+ else if ((c & 0xF0) == 0xE0)
+ bytesToSkip = 2; // skip 3-byte UTF-8 sequence
+ else if ((c & 0xF8) == 0xF0)
+ bytesToSkip = 3; // skip 4-byte UTF-8 sequence
+ else if ((c & 0xFC) == 0xF8)
+ bytesToSkip = 4; // skip 5-byte UTF-8 sequence
+ else if ((c & 0xFE) == 0xFC)
+ bytesToSkip = 5; // skip 6-byte UTF-8 sequence
+
+ bool invalid = false;
+ char* orgpc = pc;
+ for (int i = 0; i < bytesToSkip; i++)
+ {
+ char next = pc[1];
+
+ // valid UTF-8 part
+ if ((next & 0xC0) == 0x80)
+ {
+ pc++;
+ continue;
+ }
+
+ // invalid UTF-8 part or encountered \0
+ invalid = true;
+ break;
+ }
+ if (invalid)
+ {
+ // erase the whole "UTF-8" sequence
+ for (char* x = orgpc; x <= pc; x++)
+ if (*x != '\0')
+ *x = ' ';
+ else
+ break;
+ }
+ if (bytesToSkip > 0)
+ continue; // this byte was already handled as UTF-8
+
+ // an invalid control character or an UTF-8 part outside of UTF-8 sequence
+ if ((iscntrl(c) && c != '\n' && c != '\r' && c != '\x1B') || (c & 0x80) != 0)
+ {
+ *pc = ' ';
+ continue;
+ }
+
+ if (c == '\x1B') // separate handling for this escape sequence...
+ if (allow_color_codes && skip_valid_ansi_csi_sgr(pc)) // ...which we allow for color codes...
+ pc--;
+ else // ...but remove it otherwise
+ *pc = ' ';
+ }
+}
diff --git a/primedev/util/utils.h b/primedev/util/utils.h
new file mode 100644
index 00000000..85922692
--- /dev/null
+++ b/primedev/util/utils.h
@@ -0,0 +1,3 @@
+#pragma once
+
+void RemoveAsciiControlSequences(char* str, bool allow_color_codes);
diff --git a/primedev/util/version.cpp b/primedev/util/version.cpp
new file mode 100644
index 00000000..a947cde1
--- /dev/null
+++ b/primedev/util/version.cpp
@@ -0,0 +1,95 @@
+#include "util/version.h"
+#include "ns_version.h"
+#include "dedicated/dedicated.h"
+
+char version[16];
+char NSUserAgent[256];
+
+void InitialiseVersion()
+{
+ constexpr int northstar_version[4] {NORTHSTAR_VERSION};
+ int ua_len = 0;
+
+ // We actually use the rightmost integer do determine whether or not we're a debug/dev build
+ // If it is set to a non-zero value, we are a dev build
+ // On github CI, we set this to a 0 automatically as we replace the 0,0,0,1 with the real version number
+ if (northstar_version[3])
+ {
+ sprintf(version, "%d.%d.%d.%d+dev", northstar_version[0], northstar_version[1], northstar_version[2], northstar_version[3]);
+ ua_len += snprintf(
+ NSUserAgent + ua_len,
+ sizeof(NSUserAgent) - ua_len,
+ "R2Northstar/%d.%d.%d+dev",
+ northstar_version[0],
+ northstar_version[1],
+ northstar_version[2]);
+ }
+ else
+ {
+ sprintf(version, "%d.%d.%d.%d", northstar_version[0], northstar_version[1], northstar_version[2], northstar_version[3]);
+ ua_len += snprintf(
+ NSUserAgent + ua_len,
+ sizeof(NSUserAgent) - ua_len,
+ "R2Northstar/%d.%d.%d",
+ northstar_version[0],
+ northstar_version[1],
+ northstar_version[2]);
+ }
+
+ if (IsDedicatedServer())
+ ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " (Dedicated)");
+
+ // Add the host platform info to the user agent.
+ //
+ // note: ntdll will always be loaded
+ HMODULE ntdll = GetModuleHandleA("ntdll");
+ if (ntdll)
+ {
+ // real win32 version info (i.e., ignore manifest)
+ DWORD(WINAPI * RtlGetVersion)(LPOSVERSIONINFOEXW);
+ *(FARPROC*)(&RtlGetVersion) = GetProcAddress(ntdll, "RtlGetVersion");
+
+ // wine version (7.0-rc1, 7.0, 7.11, etc)
+ const char*(CDECL * wine_get_version)(void);
+ *(FARPROC*)(&wine_get_version) = GetProcAddress(ntdll, "wine_get_version");
+
+ // human-readable build string (e.g., "wine-7.22 (Staging)")
+ const char*(CDECL * wine_get_build_id)(void);
+ *(FARPROC*)(&wine_get_build_id) = GetProcAddress(ntdll, "wine_get_build_id");
+
+ // uname sysname (Darwin, Linux, etc) and release (kernel version string)
+ void(CDECL * wine_get_host_version)(const char** sysname, const char** release);
+ *(FARPROC*)(&wine_get_host_version) = GetProcAddress(ntdll, "wine_get_host_version");
+
+ OSVERSIONINFOEXW osvi = {};
+ const char *wine_version = NULL, *wine_build_id = NULL, *wine_sysname = NULL, *wine_release = NULL;
+ if (RtlGetVersion)
+ RtlGetVersion(&osvi);
+ if (wine_get_version)
+ wine_version = wine_get_version();
+ if (wine_get_build_id)
+ wine_build_id = wine_get_build_id();
+ if (wine_get_host_version)
+ wine_get_host_version(&wine_sysname, &wine_release);
+
+ // windows version
+ if (osvi.dwMajorVersion)
+ ua_len += snprintf(
+ NSUserAgent + ua_len,
+ sizeof(NSUserAgent) - ua_len,
+ " Windows/%d.%d.%d",
+ osvi.dwMajorVersion,
+ osvi.dwMinorVersion,
+ osvi.dwBuildNumber);
+
+ // wine version
+ if (wine_version && wine_build_id)
+ ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " Wine/%s (%s)", wine_version, wine_build_id);
+
+ // wine host system version
+ if (wine_sysname && wine_release)
+ ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " %s/%s", wine_sysname, wine_release);
+ }
+
+ return;
+}
diff --git a/primedev/util/version.h b/primedev/util/version.h
new file mode 100644
index 00000000..a3dcf8c7
--- /dev/null
+++ b/primedev/util/version.h
@@ -0,0 +1,6 @@
+#pragma once
+
+extern char version[16];
+extern char NSUserAgent[256];
+
+void InitialiseVersion();
diff --git a/primedev/util/wininfo.cpp b/primedev/util/wininfo.cpp
new file mode 100644
index 00000000..4fd64369
--- /dev/null
+++ b/primedev/util/wininfo.cpp
@@ -0,0 +1,9 @@
+AUTOHOOK_INIT()
+
+HWND* g_gameHWND;
+HMODULE g_NorthstarModule = 0;
+
+ON_DLL_LOAD("engine.dll", WinInfo, (CModule module))
+{
+ g_gameHWND = module.Offset(0x7d88a0).RCast<HWND*>();
+}
diff --git a/primedev/util/wininfo.h b/primedev/util/wininfo.h
new file mode 100644
index 00000000..c56f7b87
--- /dev/null
+++ b/primedev/util/wininfo.h
@@ -0,0 +1,4 @@
+#pragma once
+
+extern HWND* g_gameHWND;
+extern HMODULE g_NorthstarModule;