From e5c9e5d699fa5bf8fbf69426511f4b697edc15d2 Mon Sep 17 00:00:00 2001 From: GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com> Date: Wed, 4 Sep 2024 23:00:04 +0200 Subject: [PATCH 01/17] Add dependabot config to check for GitHub Actions updates (#814) --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..786ba2130 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +# Configures dependabot + +version: 2 +updates: + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" From 68d374dbac1e0fa6fefa67db1e54d7d1eb9dc433 Mon Sep 17 00:00:00 2001 From: F1F7Y <64418963+F1F7Y@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:10:07 +0200 Subject: [PATCH 02/17] game: Add more ways to crash to the `crash_test` concommand (#807) Adds more types of crashes to the `crash_test` concommand --- primedev/Northstar.cmake | 1 + primedev/game/client/clientmode_shared.cpp | 66 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 primedev/game/client/clientmode_shared.cpp diff --git a/primedev/Northstar.cmake b/primedev/Northstar.cmake index 05c3c72af..4e8ec973b 100644 --- a/primedev/Northstar.cmake +++ b/primedev/Northstar.cmake @@ -69,6 +69,7 @@ add_library( "engine/r2engine.cpp" "engine/r2engine.h" "engine/runframe.cpp" + "game/client/clientmode_shared.cpp" "logging/crashhandler.cpp" "logging/crashhandler.h" "logging/logging.cpp" diff --git a/primedev/game/client/clientmode_shared.cpp b/primedev/game/client/clientmode_shared.cpp new file mode 100644 index 000000000..e5793261a --- /dev/null +++ b/primedev/game/client/clientmode_shared.cpp @@ -0,0 +1,66 @@ + +//----------------------------------------------------------------------------- +// Some explanation might be needed for this. The crash is caused by +// us calling a pure virtual function in the constructor. +// The order goes like this: +// ctor +// -> vftable = IPureCall::vftable +// -> IPureCall::Ok() +// -> IPureCall::CallMeIDareYou() +// -> purecall_handler +// -> crash :( +class IPureCall +{ +public: + IPureCall() { Ok(); } + + virtual void CallMeIDareYou() = 0; + + void Ok() { CallMeIDareYou(); } +}; + +class CPureCall : IPureCall +{ + virtual void CallMeIDareYou() {} +}; + +static void (*o_pCC_crash_test_f)(const CCommand& args); +static void h_CC_crash_test_f(const CCommand& args) +{ + int crashtype = 0; + int dummy; + if (args.ArgC() > 1) + { + crashtype = atoi(args.Arg(1)); + } + switch (crashtype) + { + case 0: + dummy = *((int*)NULL); + spdlog::info("Crashed! {}", dummy); + break; + case 1: + *((int*)NULL) = 24122021; + break; + case 2: + throw std::exception("Crashed!"); + break; + case 3: + RaiseException(7, 0, 0, NULL); + break; + case 4: + { + CPureCall PureCall; + break; + } + default: + spdlog::info("Unknown variety of crash. You have now failed to crash. I hope you're happy."); + break; + } +} + +ON_DLL_LOAD("engine.dll", ClientModeShared, (CModule module)) +{ + o_pCC_crash_test_f = module.Offset(0x15BEE0).RCast(); + HookAttach(&(PVOID&)o_pCC_crash_test_f, (PVOID)h_CC_crash_test_f); +} From 27f478e7a296bca381a3bd78b82798863cac749c Mon Sep 17 00:00:00 2001 From: F1F7Y <64418963+F1F7Y@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:22:52 +0200 Subject: [PATCH 03/17] core: Remove use of `SourceInterface` for `IFileSystem` (#805) `SourceInteface` class goes back to icepick and is not good. We have a replacement, let's use it. --- primedev/core/filesystem/filesystem.cpp | 22 +++++++++++----------- primedev/core/filesystem/filesystem.h | 3 +-- primedev/mods/modmanager.cpp | 2 +- primedev/scripts/scriptdatatables.cpp | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/primedev/core/filesystem/filesystem.cpp b/primedev/core/filesystem/filesystem.cpp index 2c3a50a1d..3c711e8e3 100644 --- a/primedev/core/filesystem/filesystem.cpp +++ b/primedev/core/filesystem/filesystem.cpp @@ -1,5 +1,5 @@ #include "filesystem.h" -#include "core/sourceinterface.h" +#include "core/tier1.h" #include "mods/modmanager.h" #include @@ -10,23 +10,23 @@ std::string sCurrentModPath; ConVar* Cvar_ns_fs_log_reads; -SourceInterface* g_pFilesystem; +IFileSystem* g_pFilesystem; std::string ReadVPKFile(const char* path) { // read scripts.rson file, todo: check if this can be overwritten - FileHandle_t fileHandle = (*g_pFilesystem)->m_vtable2->Open(&(*g_pFilesystem)->m_vtable2, path, "rb", "GAME", 0); + FileHandle_t fileHandle = g_pFilesystem->m_vtable2->Open(&g_pFilesystem->m_vtable2, path, "rb", "GAME", 0); std::stringstream fileStream; int bytesRead = 0; char data[4096]; do { - bytesRead = (*g_pFilesystem)->m_vtable2->Read(&(*g_pFilesystem)->m_vtable2, data, (int)std::size(data), fileHandle); + bytesRead = g_pFilesystem->m_vtable2->Read(&g_pFilesystem->m_vtable2, data, (int)std::size(data), fileHandle); fileStream.write(data, bytesRead); } while (bytesRead == std::size(data)); - (*g_pFilesystem)->m_vtable2->Close(*g_pFilesystem, fileHandle); + g_pFilesystem->m_vtable2->Close(g_pFilesystem, fileHandle); return fileStream.str(); } @@ -65,12 +65,12 @@ void SetNewModSearchPaths(Mod* mod) if ((fs::absolute(mod->m_ModDirectory) / MOD_OVERRIDE_DIR).string().compare(sCurrentModPath)) { o_pAddSearchPath( - &*(*g_pFilesystem), (fs::absolute(mod->m_ModDirectory) / MOD_OVERRIDE_DIR).string().c_str(), "GAME", PATH_ADD_TO_HEAD); + g_pFilesystem, (fs::absolute(mod->m_ModDirectory) / MOD_OVERRIDE_DIR).string().c_str(), "GAME", PATH_ADD_TO_HEAD); sCurrentModPath = (fs::absolute(mod->m_ModDirectory) / MOD_OVERRIDE_DIR).string(); } } else // push compiled to head - o_pAddSearchPath(&*(*g_pFilesystem), fs::absolute(GetCompiledAssetsPath()).string().c_str(), "GAME", PATH_ADD_TO_HEAD); + o_pAddSearchPath(g_pFilesystem, fs::absolute(GetCompiledAssetsPath()).string().c_str(), "GAME", PATH_ADD_TO_HEAD); } bool TryReplaceFile(const char* pPath, bool shouldCompile) @@ -167,12 +167,12 @@ ON_DLL_LOAD("filesystem_stdio.dll", Filesystem, (CModule module)) o_pCBaseFileSystem__OpenEx = module.Offset(0x15F50).RCast(); HookAttach(&(PVOID&)o_pCBaseFileSystem__OpenEx, (PVOID)h_CBaseFileSystem__OpenEx); - g_pFilesystem = new SourceInterface("filesystem_stdio.dll", "VFileSystem017"); + g_pFilesystem = Sys_GetFactoryPtr("filesystem_stdio.dll", "VFileSystem017").RCast(); - o_pAddSearchPath = reinterpret_cast((*g_pFilesystem)->m_vtable->AddSearchPath); + o_pAddSearchPath = reinterpret_cast(g_pFilesystem->m_vtable->AddSearchPath); HookAttach(&(PVOID&)o_pAddSearchPath, (PVOID)h_AddSearchPath); - o_pReadFromCache = reinterpret_cast((*g_pFilesystem)->m_vtable->ReadFromCache); + o_pReadFromCache = reinterpret_cast(g_pFilesystem->m_vtable->ReadFromCache); HookAttach(&(PVOID&)o_pReadFromCache, (PVOID)h_ReadFromCache); - o_pMountVPK = reinterpret_cast((*g_pFilesystem)->m_vtable->MountVPK); + o_pMountVPK = reinterpret_cast(g_pFilesystem->m_vtable->MountVPK); HookAttach(&(PVOID&)o_pMountVPK, (PVOID)h_MountVPK); } diff --git a/primedev/core/filesystem/filesystem.h b/primedev/core/filesystem/filesystem.h index 4e2c18d9b..2c592b3f6 100644 --- a/primedev/core/filesystem/filesystem.h +++ b/primedev/core/filesystem/filesystem.h @@ -1,5 +1,4 @@ #pragma once -#include "core/sourceinterface.h" // taken from ttf2sdk typedef void* FileHandle_t; @@ -48,7 +47,7 @@ class IFileSystem VTable2* m_vtable2; }; -extern SourceInterface* g_pFilesystem; +extern IFileSystem* g_pFilesystem; std::string ReadVPKFile(const char* path); std::string ReadVPKOriginalFile(const char* path); diff --git a/primedev/mods/modmanager.cpp b/primedev/mods/modmanager.cpp index 45eddd3ee..a3e0a5f57 100644 --- a/primedev/mods/modmanager.cpp +++ b/primedev/mods/modmanager.cpp @@ -819,7 +819,7 @@ void ModManager::LoadMods() modVpk.m_sVpkPath = (file.path().parent_path() / vpkName).string(); if (m_bHasLoadedMods && modVpk.m_bAutoLoad) - (*g_pFilesystem)->m_vtable->MountVPK(*g_pFilesystem, vpkName.c_str()); + g_pFilesystem->m_vtable->MountVPK(g_pFilesystem, vpkName.c_str()); } } } diff --git a/primedev/scripts/scriptdatatables.cpp b/primedev/scripts/scriptdatatables.cpp index 76e57b956..c91e16ffb 100644 --- a/primedev/scripts/scriptdatatables.cpp +++ b/primedev/scripts/scriptdatatables.cpp @@ -96,7 +96,7 @@ REPLACE_SQFUNC(GetDataTable, (ScriptContext::UI | ScriptContext::CLIENT | Script diskAssetPath /= fs::path(pAssetName); std::string sDiskAssetPath(diskAssetPath.string()); - if ((*g_pFilesystem)->m_vtable2->FileExists(&(*g_pFilesystem)->m_vtable2, sDiskAssetPath.c_str(), "GAME")) + if (g_pFilesystem->m_vtable2->FileExists(&g_pFilesystem->m_vtable2, sDiskAssetPath.c_str(), "GAME")) { std::string sTableCSV = ReadVPKFile(sDiskAssetPath.c_str()); if (!sTableCSV.size()) From dab57649caef0f2bea82d5cd2a7d4729e4b0bd19 Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:47:34 +0100 Subject: [PATCH 04/17] Remove uses of Autohook from `logging.cpp` (#811) Remove AUTOHOOK_INIT --- primedev/logging/logging.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/primedev/logging/logging.cpp b/primedev/logging/logging.cpp index 6d71eea0a..e1298553c 100644 --- a/primedev/logging/logging.cpp +++ b/primedev/logging/logging.cpp @@ -11,8 +11,6 @@ #include #include -AUTOHOOK_INIT() - std::vector> loggers {}; namespace NS::log From 160f503bc81bffdef6dbaa16eec7c73fccef0eee Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:10:28 +0100 Subject: [PATCH 05/17] Big rpak loading refactor (#766) This reworks how rpaks are loaded, unloaded and tracked. It allows for rpak reloading between map loads, meaning that skins and map overhauls could be enabled and disabled on the fly. Previous methods of loading rpaks still work. --- primedev/core/filesystem/rpakfilesystem.cpp | 543 ++++++++++++++------ primedev/core/filesystem/rpakfilesystem.h | 86 +++- primedev/mods/modmanager.cpp | 49 +- primedev/mods/modmanager.h | 29 +- primedev/scripts/scriptdatatables.cpp | 11 +- 5 files changed, 524 insertions(+), 194 deletions(-) diff --git a/primedev/core/filesystem/rpakfilesystem.cpp b/primedev/core/filesystem/rpakfilesystem.cpp index da72646b9..ebb9085a7 100644 --- a/primedev/core/filesystem/rpakfilesystem.cpp +++ b/primedev/core/filesystem/rpakfilesystem.cpp @@ -2,206 +2,447 @@ #include "mods/modmanager.h" #include "dedicated/dedicated.h" #include "core/tier0.h" +#include "util/utils.h" -AUTOHOOK_INIT() - -// there are more i'm just too lazy to add +#pragma pack(push, 1) struct PakLoadFuncs { - void* unk0[3]; - int (*LoadPakAsync)(const char* pPath, void* unknownSingleton, int flags, void* callback0, void* callback1); - void* unk1[2]; - void* (*UnloadPak)(int iPakHandle, void* callback); - void* unk2[6]; - void* (*LoadFile)(const char* path); // unsure - void* unk3[10]; - void* (*ReadFileAsync)(const char* pPath, void* a2); + void (*InitRpakSystem)(); + void (*AddAssetLoaderWithJobDetails)(/*assetTypeHeader*/ void*, uint32_t, int); + void (*AddAssetLoader)(/*assetTypeHeader*/ void*); + PakHandle (*LoadRpakFileAsync)(const char* pPath, void* allocator, int flags); + void (*LoadRpakFile)(const char*, __int64(__fastcall*)(), __int64, void(__cdecl*)()); + __int64 qword28; + void (*UnloadPak)(PakHandle iPakHandle, void* callback); + __int64 qword38; + __int64 qword40; + __int64 qword48; + __int64 qword50; + FARPROC (*GetDllCallback)(__int16 a1, const CHAR* a2); + __int64 (*GetAssetByHash)(__int64 hash); + __int64 (*GetAssetByName)(const char* a1); + __int64 qword70; + __int64 qword78; + __int64 qword80; + __int64 qword88; + __int64 qword90; + __int64 qword98; + __int64 qwordA0; + __int64 qwordA8; + __int64 qwordB0; + __int64 qwordBB; + void* (*OpenFile)(const char* pPath); + __int64 CloseFile; + __int64 qwordD0; + __int64 FileReadAsync; + __int64 ComplexFileReadAsync; + __int64 GetReadJobState; + __int64 WaitForFileReadJobComplete; + __int64 CancelFileReadJob; + __int64 CancelFileReadJobAsync; + __int64 qword108; }; +static_assert(sizeof(PakLoadFuncs) == 0x110); +#pragma pack(pop) PakLoadFuncs* g_pakLoadApi; - PakLoadManager* g_pPakLoadManager; -void** pUnknownPakLoadSingleton; -int PakLoadManager::LoadPakAsync(const char* pPath, const ePakLoadSource nLoadSource) +static char* pszCurrentMapRpakPath = nullptr; +static PakHandle* piCurrentMapRpakHandle = nullptr; +static PakHandle* piCurrentMapPatchRpakHandle = nullptr; +static /*CModelLoader*/ void** ppModelLoader = nullptr; +static void** rpakMemoryAllocator = nullptr; + +static __int64 (*o_pLoadGametypeSpecificRpaks)(const char* levelName) = nullptr; +static __int64 (**o_pCleanMaterialSystemStuff)() = nullptr; +static __int64 (**o_pCModelLoader_UnreferenceAllModels)(/*CModelLoader*/ void* a1) = nullptr; +static char* (*o_pLoadlevelLoadscreen)(const char* levelName) = nullptr; +static unsigned int (*o_pGetPakPatchNumber)(const char* pPakPath) = nullptr; + +// Marks all mod Paks to be unloaded on next map load. +// Also cleans up any mod Paks that are already unloaded. +void PakLoadManager::UnloadAllModPaks() { - int nHandle = g_pakLoadApi->LoadPakAsync(pPath, *pUnknownPakLoadSingleton, 2, nullptr, nullptr); - - // set the load source of the pak we just loaded - if (nHandle != -1) - GetPakInfo(nHandle)->m_nLoadSource = nLoadSource; - - return nHandle; + NS::log::rpak->info("Reloading RPaks on next map load..."); + for (auto& modPak : m_modPaks) + { + modPak.m_markedForDelete = true; + } + // clean up any paks that are both marked for unload and already unloaded + CleanUpUnloadedPaks(); + SetForceReloadOnMapLoad(true); } -void PakLoadManager::UnloadPak(const int nPakHandle) +// Tracks all Paks related to a mod. +void PakLoadManager::TrackModPaks(Mod& mod) { - g_pakLoadApi->UnloadPak(nPakHandle, nullptr); + const fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); + + for (auto& modRpakEntry : mod.Rpaks) + { + ModPak_t pak; + pak.m_modName = mod.Name; + pak.m_path = (modPakPath / modRpakEntry.m_pakName).string(); + pak.m_pathHash = STR_HASH(pak.m_path); + + pak.m_preload = modRpakEntry.m_preload; + pak.m_dependentPakHash = modRpakEntry.m_dependentPakHash; + pak.m_mapRegex = modRpakEntry.m_loadRegex; + + m_modPaks.push_back(pak); + } } -void PakLoadManager::UnloadMapPaks() +// Untracks all paks that aren't currently loaded and are marked for unload. +void PakLoadManager::CleanUpUnloadedPaks() { - for (auto& pair : m_vLoadedPaks) - if (pair.second.m_nLoadSource == ePakLoadSource::MAP) - UnloadPak(pair.first); + auto fnRemovePredicate = [](ModPak_t& pak) -> bool { return pak.m_markedForDelete && pak.m_handle == PakHandle::INVALID; }; + + m_modPaks.erase(std::remove_if(m_modPaks.begin(), m_modPaks.end(), fnRemovePredicate), m_modPaks.end()); } -LoadedPak* PakLoadManager::TrackLoadedPak(ePakLoadSource nLoadSource, int nPakHandle, size_t nPakNameHash) +// Unloads all paks that are marked for unload. +void PakLoadManager::UnloadMarkedPaks() { - LoadedPak pak; - pak.m_nLoadSource = nLoadSource; - pak.m_nPakHandle = nPakHandle; - pak.m_nPakNameHash = nPakNameHash; + ++m_reentranceCounter; + const ScopeGuard guard([&]() { --m_reentranceCounter; }); - m_vLoadedPaks.insert(std::make_pair(nPakHandle, pak)); - return &m_vLoadedPaks.at(nPakHandle); + (*o_pCModelLoader_UnreferenceAllModels)(*ppModelLoader); + (*o_pCleanMaterialSystemStuff)(); + + for (auto& modPak : m_modPaks) + { + if (modPak.m_handle == PakHandle::INVALID || !modPak.m_markedForDelete) + continue; + + g_pakLoadApi->UnloadPak(modPak.m_handle, *o_pCleanMaterialSystemStuff); + modPak.m_handle = PakHandle::INVALID; + } } -void PakLoadManager::RemoveLoadedPak(int nPakHandle) +// Loads all modded paks for the given map. +void PakLoadManager::LoadModPaksForMap(const char* mapName) { - m_vLoadedPaks.erase(nPakHandle); + ++m_reentranceCounter; + const ScopeGuard guard([&]() { --m_reentranceCounter; }); + + for (auto& modPak : m_modPaks) + { + // don't load paks that are already loaded + if (modPak.m_handle != PakHandle::INVALID) + continue; + std::cmatch matches; + if (!std::regex_match(mapName, matches, modPak.m_mapRegex)) + continue; + + modPak.m_handle = g_pakLoadApi->LoadRpakFileAsync(modPak.m_path.c_str(), *rpakMemoryAllocator, 7); + m_mapPaks.push_back(modPak.m_pathHash); + } } -LoadedPak* PakLoadManager::GetPakInfo(const int nPakHandle) +// Unloads all modded map paks. +void PakLoadManager::UnloadModPaks() { - return &m_vLoadedPaks.at(nPakHandle); + ++m_reentranceCounter; + const ScopeGuard guard([&]() { --m_reentranceCounter; }); + + (*o_pCModelLoader_UnreferenceAllModels)(*ppModelLoader); + (*o_pCleanMaterialSystemStuff)(); + + for (auto& modPak : m_modPaks) + { + for (auto it = m_mapPaks.begin(); it != m_mapPaks.end(); ++it) + { + if (*it != modPak.m_pathHash) + continue; + + m_mapPaks.erase(it, it + 1); + g_pakLoadApi->UnloadPak(modPak.m_handle, *o_pCleanMaterialSystemStuff); + modPak.m_handle = PakHandle::INVALID; + break; + } + } + + // If this has happened, we may have leaked a pak? + // It basically means that none of the entries in m_modPaks matched the hash in m_mapPaks so we didn't end up unloading it + assert_msg(m_mapPaks.size() == 0, "Not all map paks were unloaded?"); } -int PakLoadManager::GetPakHandle(const size_t nPakNameHash) +// Called after a Pak was loaded. +void PakLoadManager::OnPakLoaded(std::string& originalPath, std::string& resultingPath, PakHandle resultingHandle) { - for (auto& pair : m_vLoadedPaks) - if (pair.second.m_nPakNameHash == nPakNameHash) - return pair.first; + if (IsVanillaCall()) + { + // add entry to loaded vanilla rpaks + m_vanillaPaks.emplace_back(originalPath, resultingHandle); + } - return -1; + LoadDependentPaks(resultingPath, resultingHandle); } -int PakLoadManager::GetPakHandle(const char* pPath) +// Called before a Pak was unloaded. +void PakLoadManager::OnPakUnloading(PakHandle handle) { - return GetPakHandle(STR_HASH(pPath)); + UnloadDependentPaks(handle); + + if (IsVanillaCall()) + { + // remove entry from loaded vanilla rpaks + auto fnRemovePredicate = [handle](std::pair& pair) -> bool { return pair.second == handle; }; + + m_vanillaPaks.erase(std::remove_if(m_vanillaPaks.begin(), m_vanillaPaks.end(), fnRemovePredicate), m_vanillaPaks.end()); + + // no need to handle aliasing here, if vanilla wants it gone, it's gone + } + else + { + // note: aliasing is handled the old way, long term todo: move it over to the PakLoadManager + // handle the potential unloading of an aliased vanilla rpak (we aliased it, and we are now unloading the alias, so we should load + // the vanilla one again) + // for (auto& [path, resultingHandle] : m_vanillaPaks) + //{ + // if (resultingHandle != handle) + // continue; + + // // load vanilla rpak + //} + } + + // set handle of the mod pak (if any) that has this handle for proper tracking + for (auto& modPak : m_modPaks) + { + if (modPak.m_handle == handle) + modPak.m_handle = PakHandle::INVALID; + } } -void* PakLoadManager::LoadFile(const char* path) +// Whether the vanilla game has this rpak +static bool VanillaHasPak(const char* pakName) { - return g_pakLoadApi->LoadFile(path); + fs::path originalPath = fs::path("./r2/paks/Win64") / pakName; + unsigned int highestPatch = o_pGetPakPatchNumber(pakName); + if (highestPatch) + { + // add the patch path to the extension + char buf[16]; + snprintf(buf, sizeof(buf), "(%02u).rpak", highestPatch); + // remove the .rpak and add the new suffix + originalPath = originalPath.replace_extension().string() + buf; + } + else + { + originalPath /= pakName; + } + + return fs::exists(originalPath); } -void HandlePakAliases(char** map) +// If vanilla doesn't have an rpak for this path, tries to map it to a modded rpak of the same name. +void PakLoadManager::FixupPakPath(std::string& pakPath) { - // convert the pak being loaded to it's aliased one, e.g. aliasing mp_hub_timeshift => sp_hub_timeshift - for (int64_t i = g_pModManager->m_LoadedMods.size() - 1; i > -1; i--) + if (VanillaHasPak(pakPath.c_str())) + return; + + for (ModPak_t& modPak : m_modPaks) { - Mod* mod = &g_pModManager->m_LoadedMods[i]; - if (!mod->m_bEnabled) + if (modPak.m_markedForDelete) continue; - if (mod->RpakAliases.find(*map) != mod->RpakAliases.end()) + fs::path modPakFilename = fs::path(modPak.m_path).filename(); + if (pakPath == modPakFilename.string()) { - *map = &mod->RpakAliases[*map][0]; + pakPath = modPak.m_path; return; } } } -void LoadPreloadPaks() +// Loads all "Preload" Paks. todo: deprecate Preload. +void PakLoadManager::LoadPreloadPaks() { - // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks - for (Mod& mod : g_pModManager->m_LoadedMods) + ++m_reentranceCounter; + const ScopeGuard guard([&]() { --m_reentranceCounter; }); + + for (auto& modPak : m_modPaks) { - if (!mod.m_bEnabled) + if (modPak.m_markedForDelete || modPak.m_handle != PakHandle::INVALID || !modPak.m_preload) continue; - // need to get a relative path of mod to mod folder - fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); + modPak.m_handle = g_pakLoadApi->LoadRpakFileAsync(modPak.m_path.c_str(), *rpakMemoryAllocator, 7); + } +} - for (ModRpakEntry& pak : mod.Rpaks) - if (pak.m_bAutoLoad) - g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::CONSTANT); +// Causes all "Postload" paks to be loaded again. +void PakLoadManager::ReloadPostloadPaks() +{ + ++m_reentranceCounter; + const ScopeGuard guard([&]() { --m_reentranceCounter; }); + + // pretend that we just loaded all of these vanilla paks + for (auto& [path, handle] : m_vanillaPaks) + { + LoadDependentPaks(path, handle); } } -void LoadPostloadPaks(const char* pPath) +// Wrapper for Pak load API. +void* PakLoadManager::OpenFile(const char* path) { - // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks - for (Mod& mod : g_pModManager->m_LoadedMods) + return g_pakLoadApi->OpenFile(path); +} + +// Loads Paks that depend on this Pak. +void PakLoadManager::LoadDependentPaks(std::string& path, PakHandle handle) +{ + ++m_reentranceCounter; + const ScopeGuard guard([&]() { --m_reentranceCounter; }); + + const size_t hash = STR_HASH(path); + for (auto& modPak : m_modPaks) { - if (!mod.m_bEnabled) + if (modPak.m_handle != PakHandle::INVALID) + continue; + if (modPak.m_dependentPakHash != hash) continue; - // need to get a relative path of mod to mod folder - fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); - - for (ModRpakEntry& pak : mod.Rpaks) - if (pak.m_sLoadAfterPak == pPath) - g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::CONSTANT); + // load pak + modPak.m_handle = g_pakLoadApi->LoadRpakFileAsync(modPak.m_path.c_str(), *rpakMemoryAllocator, 7); + m_dependentPaks.emplace_back(handle, hash); } } -void LoadCustomMapPaks(char** pakName, bool* bNeedToFreePakName) +// Unloads Paks that depend on this Pak. +void PakLoadManager::UnloadDependentPaks(PakHandle handle) { - // whether the vanilla game has this rpak - bool bHasOriginalPak = fs::exists(fs::path("r2/paks/Win64/") / *pakName); + ++m_reentranceCounter; + const ScopeGuard guard([&]() { --m_reentranceCounter; }); - // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks - for (Mod& mod : g_pModManager->m_LoadedMods) + auto fnRemovePredicate = [&](std::pair& pair) -> bool { - if (!mod.m_bEnabled) - continue; + if (pair.first != handle) + return false; - // need to get a relative path of mod to mod folder - fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); + for (auto& modPak : m_modPaks) + { + if (modPak.m_pathHash != pair.second || modPak.m_handle == PakHandle::INVALID) + continue; - for (ModRpakEntry& pak : mod.Rpaks) + // unload pak + g_pakLoadApi->UnloadPak(modPak.m_handle, *o_pCleanMaterialSystemStuff); + modPak.m_handle = PakHandle::INVALID; + } + + return true; + }; + m_dependentPaks.erase(std::remove_if(m_dependentPaks.begin(), m_dependentPaks.end(), fnRemovePredicate), m_dependentPaks.end()); +} + +// Handles aliases for rpaks defined in rpak.json, effectively redirecting an rpak load to a different path. +static void HandlePakAliases(std::string& originalPath) +{ + // convert the pak being loaded to its aliased one, e.g. aliasing mp_hub_timeshift => sp_hub_timeshift + for (int64_t i = g_pModManager->m_LoadedMods.size() - 1; i > PakHandle::INVALID; i--) + { + Mod* mod = &g_pModManager->m_LoadedMods[i]; + if (!mod->m_bEnabled) + continue; + + if (mod->RpakAliases.find(originalPath) != mod->RpakAliases.end()) { - if (!pak.m_bAutoLoad && !pak.m_sPakName.compare(*pakName)) - { - // if the game doesn't have the original pak, let it handle loading this one as if it was the one it was loading originally - if (!bHasOriginalPak) - { - std::string path = (modPakPath / pak.m_sPakName).string(); - *pakName = new char[path.size() + 1]; - strcpy(*pakName, &path[0]); - (*pakName)[path.size()] = '\0'; - - bHasOriginalPak = true; - *bNeedToFreePakName = - true; // we can't free this memory until we're done with the pak, so let whatever's calling this deal with it - } - else - g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::MAP); - } + originalPath = (mod->m_ModDirectory / "paks" / mod->RpakAliases[originalPath]).string(); + return; } } } -// clang-format off -HOOK(LoadPakAsyncHook, LoadPakAsync, -int, __fastcall, (char* pPath, void* unknownSingleton, int flags, void* pCallback0, void* pCallback1)) -// clang-format on +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +static bool (*o_pLoadMapRpaks)(char* mapPath) = nullptr; +static bool h_LoadMapRpaks(char* mapPath) { - HandlePakAliases(&pPath); + // unload all mod rpaks that are marked for unload + g_pPakLoadManager->UnloadMarkedPaks(); + g_pPakLoadManager->CleanUpUnloadedPaks(); - // dont load the pak if it's currently loaded already - size_t nPathHash = STR_HASH(pPath); - if (g_pPakLoadManager->GetPakHandle(nPathHash) != -1) - return -1; + // strip file extension + const std::string mapName = fs::path(mapPath).replace_extension().string(); - bool bNeedToFreePakName = false; + // load mp_common, sp_common etc. + o_pLoadGametypeSpecificRpaks(mapName.c_str()); - static bool bShouldLoadPaks = true; - if (bShouldLoadPaks) + // unload old modded map paks + g_pPakLoadManager->UnloadModPaks(); + // load modded map paks + g_pPakLoadManager->LoadModPaksForMap(mapName.c_str()); + + // don't load/unload anything when going to the lobby, presumably to save load times when going back to the same map + if (!g_pPakLoadManager->GetForceReloadOnMapLoad() && !strcmp("mp_lobby", mapName.c_str())) + return false; + + if (g_pPakLoadManager->GetForceReloadOnMapLoad()) { - // make a copy of the path for comparing to determine whether we should load this pak on dedi, before it could get overwritten by - // LoadCustomMapPaks - std::string originalPath(pPath); + g_pPakLoadManager->LoadPreloadPaks(); + g_pPakLoadManager->ReloadPostloadPaks(); + } - // disable preloading while we're doing this - bShouldLoadPaks = false; + char mapRpakStr[272]; + snprintf(mapRpakStr, 272, "%s.rpak", mapName.c_str()); - LoadPreloadPaks(); - LoadCustomMapPaks(&pPath, &bNeedToFreePakName); + // if level being loaded is the same as current level, do nothing + if (!g_pPakLoadManager->GetForceReloadOnMapLoad() && !strcmp(mapRpakStr, pszCurrentMapRpakPath)) + return true; - bShouldLoadPaks = true; + strcpy(pszCurrentMapRpakPath, mapRpakStr); + + (*o_pCleanMaterialSystemStuff)(); + o_pLoadlevelLoadscreen(mapName.c_str()); + + // unload old map rpaks + PakHandle curHandle = *piCurrentMapRpakHandle; + PakHandle curPatchHandle = *piCurrentMapPatchRpakHandle; + if (curHandle != PakHandle::INVALID) + { + (*o_pCModelLoader_UnreferenceAllModels)(*ppModelLoader); + (*o_pCleanMaterialSystemStuff)(); + g_pakLoadApi->UnloadPak(curHandle, *o_pCleanMaterialSystemStuff); + *piCurrentMapRpakHandle = PakHandle::INVALID; + } + if (curPatchHandle != PakHandle::INVALID) + { + (*o_pCModelLoader_UnreferenceAllModels)(*ppModelLoader); + (*o_pCleanMaterialSystemStuff)(); + g_pakLoadApi->UnloadPak(curPatchHandle, *o_pCleanMaterialSystemStuff); + *piCurrentMapPatchRpakHandle = PakHandle::INVALID; + } + + *piCurrentMapRpakHandle = g_pakLoadApi->LoadRpakFileAsync(mapRpakStr, *rpakMemoryAllocator, 7); + + // load special _patch rpak (seemingly used for dev things?) + char levelPatchRpakStr[272]; + snprintf(levelPatchRpakStr, 272, "%s_patch.rpak", mapName.c_str()); + *piCurrentMapPatchRpakHandle = g_pakLoadApi->LoadRpakFileAsync(levelPatchRpakStr, *rpakMemoryAllocator, 7); + + // we just reloaded the paks, so we don't need to force it again + g_pPakLoadManager->SetForceReloadOnMapLoad(false); + return true; +} + +// clang-format off +HOOK(LoadPakAsyncHook, LoadPakAsync, +PakHandle, __fastcall, (const char* pPath, void* memoryAllocator, int flags)) +// clang-format on +{ + // make a copy of the path for comparing to determine whether we should load this pak on dedi, before it could get overwritten + std::string svOriginalPath(pPath); + + std::string resultingPath(pPath); + HandlePakAliases(resultingPath); + + if (g_pPakLoadManager->IsVanillaCall()) + { + g_pPakLoadManager->LoadPreloadPaks(); + g_pPakLoadManager->FixupPakPath(resultingPath); // do this after custom paks load and in bShouldLoadPaks so we only ever call this on the root pakload call // todo: could probably add some way to flag custom paks to not be loaded on dedicated servers in rpak.json @@ -210,44 +451,27 @@ int, __fastcall, (char* pPath, void* unknownSingleton, int flags, void* pCallbac // sp_ rpaks contain tutorial ghost data // sucks to have to load the entire rpak for that but sp was never meant to be done on dedi if (IsDedicatedServer() && - (CommandLine()->CheckParm("-nopakdedi") || strncmp(&originalPath[0], "common", 6) && strncmp(&originalPath[0], "sp_", 3))) + (CommandLine()->CheckParm("-nopakdedi") || strncmp(&svOriginalPath[0], "common", 6) && strncmp(&svOriginalPath[0], "sp_", 3))) { - if (bNeedToFreePakName) - delete[] pPath; - - NS::log::rpak->info("Not loading pak {} for dedicated server", originalPath); - return -1; + NS::log::rpak->info("Not loading pak {} for dedicated server", svOriginalPath); + return PakHandle::INVALID; } } - int iPakHandle = LoadPakAsync(pPath, unknownSingleton, flags, pCallback0, pCallback1); - NS::log::rpak->info("LoadPakAsync {} {}", pPath, iPakHandle); - - // trak the pak - g_pPakLoadManager->TrackLoadedPak(ePakLoadSource::UNTRACKED, iPakHandle, nPathHash); - LoadPostloadPaks(pPath); + PakHandle iPakHandle = LoadPakAsync(resultingPath.c_str(), memoryAllocator, flags); + NS::log::rpak->info("LoadPakAsync {} {}", resultingPath, iPakHandle); - if (bNeedToFreePakName) - delete[] pPath; + g_pPakLoadManager->OnPakLoaded(svOriginalPath, resultingPath, iPakHandle); return iPakHandle; } // clang-format off HOOK(UnloadPakHook, UnloadPak, -void*, __fastcall, (int nPakHandle, void* pCallback)) +void*, __fastcall, (PakHandle nPakHandle, void* pCallback)) // clang-format on { - // stop tracking the pak - g_pPakLoadManager->RemoveLoadedPak(nPakHandle); - - static bool bShouldUnloadPaks = true; - if (bShouldUnloadPaks) - { - bShouldUnloadPaks = false; - g_pPakLoadManager->UnloadMapPaks(); - bShouldUnloadPaks = true; - } + g_pPakLoadManager->OnPakUnloading(nPakHandle); NS::log::rpak->info("UnloadPak {}", nPakHandle); return UnloadPak(nPakHandle, pCallback); @@ -256,7 +480,7 @@ void*, __fastcall, (int nPakHandle, void* pCallback)) // we hook this exclusively for resolving stbsp paths, but seemingly it's also used for other stuff like vpk, rpak, mprj and starpak loads // tbh this actually might be for memory mapped files or something, would make sense i think // clang-format off -HOOK(ReadFileAsyncHook, ReadFileAsync, +HOOK(OpenFileHook, o_pOpenFile, void*, __fastcall, (const char* pPath, void* pCallback)) // clang-format on { @@ -329,19 +553,34 @@ void*, __fastcall, (const char* pPath, void* pCallback)) NS::log::rpak->info("LoadStreamPak: {}", filename.string()); } - return ReadFileAsync(pPath, pCallback); + return o_pOpenFile(pPath, pCallback); } ON_DLL_LOAD("engine.dll", RpakFilesystem, (CModule module)) { - AUTOHOOK_DISPATCH(); - g_pPakLoadManager = new PakLoadManager; g_pakLoadApi = module.Offset(0x5BED78).Deref().RCast(); - pUnknownPakLoadSingleton = module.Offset(0x7C5E20).RCast(); - LoadPakAsyncHook.Dispatch((LPVOID*)g_pakLoadApi->LoadPakAsync); + LoadPakAsyncHook.Dispatch((LPVOID*)g_pakLoadApi->LoadRpakFileAsync); UnloadPakHook.Dispatch((LPVOID*)g_pakLoadApi->UnloadPak); - ReadFileAsyncHook.Dispatch((LPVOID*)g_pakLoadApi->ReadFileAsync); + OpenFileHook.Dispatch((LPVOID*)g_pakLoadApi->OpenFile); + + pszCurrentMapRpakPath = module.Offset(0x1315C3E0).RCast(); + piCurrentMapRpakHandle = module.Offset(0x7CB5A0).RCast(); + piCurrentMapPatchRpakHandle = module.Offset(0x7CB5A4).RCast(); + ppModelLoader = module.Offset(0x7C4AC0).RCast(); + rpakMemoryAllocator = module.Offset(0x7C5E20).RCast(); + + o_pLoadGametypeSpecificRpaks = module.Offset(0x15AD20).RCast(); + o_pCleanMaterialSystemStuff = module.Offset(0x12A11F00).RCast(); + o_pCModelLoader_UnreferenceAllModels = module.Offset(0x5ED580).RCast(); + o_pLoadlevelLoadscreen = module.Offset(0x15A810).RCast(); + + o_pLoadMapRpaks = module.Offset(0x15A8C0).RCast(); + HookAttach(&(PVOID&)o_pLoadMapRpaks, (PVOID)h_LoadMapRpaks); + + // kinda bad, doing things in rtech in an engine callback but it seems fine for now + CModule rtechModule(GetModuleHandleA("rtech_game.dll")); + o_pGetPakPatchNumber = rtechModule.Offset(0x9A00).RCast(); } diff --git a/primedev/core/filesystem/rpakfilesystem.h b/primedev/core/filesystem/rpakfilesystem.h index bcd57a731..87a41e7b1 100644 --- a/primedev/core/filesystem/rpakfilesystem.h +++ b/primedev/core/filesystem/rpakfilesystem.h @@ -1,39 +1,81 @@ #pragma once -enum class ePakLoadSource -{ - UNTRACKED = -1, // not a pak we loaded, we shouldn't touch this one +#include - CONSTANT, // should be loaded at all times - MAP // loaded from a map, should be unloaded when the map is unloaded +enum PakHandle : int +{ + INVALID = -1, }; -struct LoadedPak +struct ModPak_t { - ePakLoadSource m_nLoadSource; - int m_nPakHandle; - size_t m_nPakNameHash; + std::string m_modName; + + std::string m_path; + size_t m_pathHash = 0; + + // If the map being loaded matches this regex, this pak will be loaded. + std::regex m_mapRegex; + // If a pak with a hash matching this is loaded, this pak will be loaded. + size_t m_dependentPakHash = 0; + // If this is set, this pak will be loaded whenever any other pak is loaded. + bool m_preload = false; + + // If this is set, the Pak will be unloaded on next map load + bool m_markedForDelete = false; + // The current rpak handle associated with this Pak + PakHandle m_handle = PakHandle::INVALID; }; class PakLoadManager { -private: - std::map m_vLoadedPaks {}; - std::unordered_map m_HashToPakHandle {}; - public: - int LoadPakAsync(const char* pPath, const ePakLoadSource nLoadSource); - void UnloadPak(const int nPakHandle); - void UnloadMapPaks(); - void* LoadFile(const char* path); // this is a guess + void UnloadAllModPaks(); + void TrackModPaks(Mod& mod); + + void CleanUpUnloadedPaks(); + void UnloadMarkedPaks(); + + void LoadModPaksForMap(const char* mapName); + void UnloadModPaks(); + + // Whether the current context is a vanilla call to a function, or a modded one + bool IsVanillaCall() const { return m_reentranceCounter == 0; } + // Whether paks will be forced to reload on the next map load + bool GetForceReloadOnMapLoad() const { return m_forceReloadOnMapLoad; } + void SetForceReloadOnMapLoad(bool value) { m_forceReloadOnMapLoad = value; } + + void OnPakLoaded(std::string& originalPath, std::string& resultingPath, PakHandle resultingHandle); + void OnPakUnloading(PakHandle handle); + + void FixupPakPath(std::string& path); + + void LoadPreloadPaks(); + void ReloadPostloadPaks(); + + void* OpenFile(const char* path); + +private: + void LoadDependentPaks(std::string& path, PakHandle handle); + void UnloadDependentPaks(PakHandle handle); - LoadedPak* TrackLoadedPak(ePakLoadSource nLoadSource, int nPakHandle, size_t nPakNameHash); - void RemoveLoadedPak(int nPakHandle); + // All paks that vanilla has attempted to load. (they may have been aliased away) + // Also known as a list of rpaks that the vanilla game would have loaded at this point in time. + std::vector> m_vanillaPaks; - LoadedPak* GetPakInfo(const int nPakHandle); + // All mod Paks that are currently tracked + std::vector m_modPaks; + // Hashes of the currently loaded map mod paks + std::vector m_mapPaks; + // Currently loaded Pak path hashes that depend on a handle to remain loaded (Postload) + std::vector> m_dependentPaks; - int GetPakHandle(const size_t nPakNameHash); - int GetPakHandle(const char* pPath); + // Used to force rpaks to be unloaded and reloaded on the next map load. + // Vanilla behaviour is to not do this when loading into mp_lobby, or loading into the same map you were last in. + bool m_forceReloadOnMapLoad = false; + // Used to track if the current hook call is a vanilla call or not. + // When loading/unloading a mod Pak, increment this before doing so, and decrement afterwards. + int m_reentranceCounter = 0; }; extern PakLoadManager* g_pPakLoadManager; diff --git a/primedev/mods/modmanager.cpp b/primedev/mods/modmanager.cpp index a3e0a5f57..52fc6e8b1 100644 --- a/primedev/mods/modmanager.cpp +++ b/primedev/mods/modmanager.cpp @@ -866,7 +866,9 @@ void ModManager::LoadMods() if (fs::is_regular_file(file) && file.path().extension() == ".rpak") { std::string pakName(file.path().filename().string()); - ModRpakEntry& modPak = mod.Rpaks.emplace_back(); + ModRpakEntry& modPak = mod.Rpaks.emplace_back(mod); + + modPak.m_pakName = pakName; if (!bUseRpakJson) { @@ -874,19 +876,47 @@ void ModManager::LoadMods() } else { - modPak.m_bAutoLoad = + modPak.m_preload = (dRpakJson.HasMember("Preload") && dRpakJson["Preload"].IsObject() && dRpakJson["Preload"].HasMember(pakName) && dRpakJson["Preload"][pakName].IsTrue()); + // only one load method can be used for an rpak. + if (modPak.m_preload) + goto REGISTER_STARPAK; + // postload things if (dRpakJson.HasMember("Postload") && dRpakJson["Postload"].IsObject() && dRpakJson["Postload"].HasMember(pakName)) { - modPak.m_sLoadAfterPak = dRpakJson["Postload"][pakName].GetString(); + modPak.m_dependentPakHash = STR_HASH(dRpakJson["Postload"][pakName].GetString()); + + // only one load method can be used for an rpak. + goto REGISTER_STARPAK; } - } - modPak.m_sPakName = pakName; + // this is the only bit of rpak.json that isn't really deprecated. Even so, it will be moved over to the mod.json + // eventually + if (dRpakJson.HasMember(pakName)) + { + if (!dRpakJson[pakName].IsString()) + { + spdlog::error("Mod {} has invalid rpak.json. Rpak entries must be strings.", mod.Name); + continue; + } + + std::string loadStr = dRpakJson[pakName].GetString(); + try + { + modPak.m_loadRegex = std::regex(loadStr); + } + catch (...) + { + spdlog::error("Mod {} has invalid rpak.json. Malformed regex \"{}\" for {}", mod.Name, loadStr, pakName); + return; + } + } + } + REGISTER_STARPAK: // read header of file and get the starpak paths // this is done here as opposed to on starpak load because multiple rpaks can load a starpak // and there is seemingly no good way to tell which rpak is causing the load of a starpak :/ @@ -926,12 +956,11 @@ void ModManager::LoadMods() } } } - - // not using atm because we need to resolve path to rpak - // if (m_hasLoadedMods && modPak.m_bAutoLoad) - // g_pPakLoadManager->LoadPakAsync(pakName.c_str()); } } + + if (g_pPakLoadManager != nullptr) + g_pPakLoadManager->TrackModPaks(mod); } // read keyvalues paths @@ -1059,6 +1088,8 @@ void ModManager::UnloadMods() fs::remove_all(GetCompiledAssetsPath()); g_CustomAudioManager.ClearAudioOverrides(); + if (g_pPakLoadManager != nullptr) + g_pPakLoadManager->UnloadAllModPaks(); if (!m_bHasEnabledModsCfg) m_EnabledModsCfg.SetObject(); diff --git a/primedev/mods/modmanager.h b/primedev/mods/modmanager.h index 95a8fe121..7859d6184 100644 --- a/primedev/mods/modmanager.h +++ b/primedev/mods/modmanager.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace fs = std::filesystem; @@ -19,6 +20,8 @@ const std::string COMPILED_ASSETS_SUFFIX = "\\runtime\\compiled"; const std::set MODS_BLACKLIST = {"Mod Settings"}; +class Mod; + struct ModConVar { public: @@ -71,9 +74,22 @@ struct ModVPKEntry struct ModRpakEntry { public: - bool m_bAutoLoad; - std::string m_sPakName; - std::string m_sLoadAfterPak; + ModRpakEntry(Mod& parent) + : m_parent(parent) + , m_loadRegex("^thisMatchesNothing^") // discord couldnt give me a funny string + { + } + + Mod& m_parent; + std::string m_pakName; + std::regex m_loadRegex; + + // these exist purely for backwards compatibility, i don't really like them anymore + + // Preload, loads before the first rpak is loaded + bool m_preload = false; + // Postload, this rpak depends on an rpak with this hash + size_t m_dependentPakHash; }; class Mod @@ -120,11 +136,12 @@ class Mod std::string Pdiff; // only need one per mod std::vector Rpaks; - std::unordered_map - RpakAliases; // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite - std::vector StarpakPaths; // starpaks that this mod contains + // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite + std::unordered_map RpakAliases; + // starpaks that this mod contains // there seems to be no nice way to get the rpak that is causing the load of a starpak? // hashed with STR_HASH + std::vector StarpakPaths; std::unordered_map DependencyConstants; std::vector PluginDependencyConstants; diff --git a/primedev/scripts/scriptdatatables.cpp b/primedev/scripts/scriptdatatables.cpp index c91e16ffb..b3c599212 100644 --- a/primedev/scripts/scriptdatatables.cpp +++ b/primedev/scripts/scriptdatatables.cpp @@ -70,10 +70,11 @@ REPLACE_SQFUNC(GetDataTable, (ScriptContext::UI | ScriptContext::CLIENT | Script g_pSquirrel->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)) + else if (!Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->OpenFile(pAssetName)) + { return g_pSquirrel->m_funcOriginals["GetDataTable"](sqvm); - // either we prefer disk datatables, or we're loading a datatable that wasn't found in rpak - else + } + else // either we prefer disk datatables, or we're loading a datatable that wasn't found in rpak { std::string sAssetPath(fmt::format("scripts/{}", pAssetName)); @@ -223,7 +224,7 @@ REPLACE_SQFUNC(GetDataTable, (ScriptContext::UI | ScriptContext::CLIENT | Script 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)) + else if (Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->OpenFile(pAssetName)) return g_pSquirrel->m_funcOriginals["GetDataTable"](sqvm); // the file doesn't exist at all, error else @@ -750,7 +751,7 @@ std::string DataTableToString(Datatable* datatable) void DumpDatatable(const char* pDatatablePath) { - Datatable* pDatatable = (Datatable*)g_pPakLoadManager->LoadFile(pDatatablePath); + Datatable* pDatatable = (Datatable*)g_pPakLoadManager->OpenFile(pDatatablePath); if (!pDatatable) { spdlog::error("couldn't load datatable {} (rpak containing it may not be loaded?)", pDatatablePath); From 8c546ed68c83b42cdff32d9b848b25ec2cba9c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Raes?= Date: Sun, 8 Sep 2024 00:27:44 +0200 Subject: [PATCH 06/17] Adjust for restructured `verified-mods.json` (#748) Updates the launcher code to deal with adjusted verified mods JSON structure from the default manifest source The idea here is to allow installing mods from other sources than Thunderstore. --- primedev/mods/autodownload/moddownloader.cpp | 42 ++++++++++++-------- primedev/mods/autodownload/moddownloader.h | 19 ++++++--- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/primedev/mods/autodownload/moddownloader.cpp b/primedev/mods/autodownload/moddownloader.cpp index 8e533decb..c20a3adbc 100644 --- a/primedev/mods/autodownload/moddownloader.cpp +++ b/primedev/mods/autodownload/moddownloader.cpp @@ -103,23 +103,23 @@ void ModDownloader::FetchModsListFromAPI() for (auto i = verifiedModsJson.MemberBegin(); i != verifiedModsJson.MemberEnd(); ++i) { // Format testing - if (!i->value.HasMember("DependencyPrefix") || !i->value.HasMember("Versions")) + if (!i->value.HasMember("Repository") || !i->value.HasMember("Versions")) { spdlog::warn("Verified mods manifesto format is unrecognized, skipping loading."); return; } std::string name = i->name.GetString(); - std::string dependency = i->value["DependencyPrefix"].GetString(); - std::unordered_map modVersions; + rapidjson::Value& versions = i->value["Versions"]; assert(versions.IsArray()); for (auto& attribute : versions.GetArray()) { assert(attribute.IsObject()); // Format testing - if (!attribute.HasMember("Version") || !attribute.HasMember("Checksum")) + if (!attribute.HasMember("Version") || !attribute.HasMember("Checksum") || !attribute.HasMember("DownloadLink") || + !attribute.HasMember("Platform")) { spdlog::warn("Verified mods manifesto format is unrecognized, skipping loading."); return; @@ -127,10 +127,14 @@ void ModDownloader::FetchModsListFromAPI() std::string version = attribute["Version"].GetString(); std::string checksum = attribute["Checksum"].GetString(); - modVersions.insert({version, {.checksum = checksum}}); + std::string downloadLink = attribute["DownloadLink"].GetString(); + std::string platformValue = attribute["Platform"].GetString(); + VerifiedModPlatform platform = + platformValue.compare("thunderstore") == 0 ? VerifiedModPlatform::Thunderstore : VerifiedModPlatform::Unknown; + modVersions.insert({version, {.checksum = checksum, .downloadLink = downloadLink, .platform = platform}}); } - VerifiedModDetails modConfig = {.dependencyPrefix = dependency, .versions = modVersions}; + VerifiedModDetails modConfig = {.versions = modVersions}; verifiedMods.insert({name, modConfig}); spdlog::info("==> Loaded configuration for mod \"" + name + "\""); } @@ -164,13 +168,10 @@ int ModDownloader::ModFetchingProgressCallback( return 0; } -std::optional ModDownloader::FetchModFromDistantStore(std::string_view modName, std::string_view modVersion) +std::optional ModDownloader::FetchModFromDistantStore(std::string_view modName, VerifiedModVersion version) { - // Retrieve mod prefix from local mods list, or use mod name as mod prefix if bypass flag is set - std::string modPrefix = strstr(GetCommandLineA(), VERIFICATION_FLAG) ? modName.data() : verifiedMods[modName.data()].dependencyPrefix; - // Build archive distant URI - std::string archiveName = std::format("{}-{}.zip", modPrefix, modVersion.data()); - std::string url = STORE_URL + archiveName; + std::string url = version.downloadLink; + std::string archiveName = fs::path(url).filename().generic_string(); spdlog::info(std::format("Fetching mod archive from {}", url)); // Download destination @@ -390,7 +391,7 @@ int GetModArchiveSize(unzFile file, unz_global_info64 info) return totalSize; } -void ModDownloader::ExtractMod(fs::path modPath) +void ModDownloader::ExtractMod(fs::path modPath, VerifiedModPlatform platform) { unzFile file; std::string name; @@ -428,6 +429,14 @@ void ModDownloader::ExtractMod(fs::path modPath) modState.total = GetModArchiveSize(file, gi); modState.progress = 0; + // Right now, we only know how to extract Thunderstore mods + if (platform != VerifiedModPlatform::Thunderstore) + { + spdlog::error("Failed extracting mod from unknown platform (value: {}).", platform); + modState.state = UNKNOWN_PLATFORM; + return; + } + // Mod directory name (removing the ".zip" fom the archive name) name = modPath.filename().string(); name = name.substr(0, name.length() - 4); @@ -598,8 +607,9 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion) }); // Download mod archive - std::string expectedHash = verifiedMods[modName].versions[modVersion].checksum; - std::optional fetchingResult = FetchModFromDistantStore(std::string_view(modName), std::string_view(modVersion)); + VerifiedModVersion fullVersion = verifiedMods[modName].versions[modVersion]; + std::string expectedHash = fullVersion.checksum; + std::optional fetchingResult = FetchModFromDistantStore(std::string_view(modName), fullVersion); if (!fetchingResult.has_value()) { spdlog::error("Something went wrong while fetching archive, aborting."); @@ -615,7 +625,7 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion) } // Extract downloaded mod archive - ExtractMod(archiveLocation); + ExtractMod(archiveLocation, fullVersion.platform); }); requestThread.detach(); diff --git a/primedev/mods/autodownload/moddownloader.h b/primedev/mods/autodownload/moddownloader.h index 98fc27ae3..c7a88c19a 100644 --- a/primedev/mods/autodownload/moddownloader.h +++ b/primedev/mods/autodownload/moddownloader.h @@ -5,17 +5,22 @@ class ModDownloader private: const char* VERIFICATION_FLAG = "-disablemodverification"; const char* CUSTOM_MODS_URL_FLAG = "-customverifiedurl="; - const char* STORE_URL = "https://gcdn.thunderstore.io/live/repository/packages/"; const char* DEFAULT_MODS_LIST_URL = "https://raw.githubusercontent.com/R2Northstar/VerifiedMods/main/verified-mods.json"; char* modsListUrl; + enum class VerifiedModPlatform + { + Unknown, + Thunderstore + }; struct VerifiedModVersion { std::string checksum; + std::string downloadLink; + VerifiedModPlatform platform; }; struct VerifiedModDetails { - std::string dependencyPrefix; std::unordered_map versions = {}; }; std::unordered_map verifiedMods = {}; @@ -45,7 +50,7 @@ class ModDownloader * @param modVersion version of the mod to be downloaded * @returns location of the downloaded archive */ - std::optional FetchModFromDistantStore(std::string_view modName, std::string_view modVersion); + std::optional FetchModFromDistantStore(std::string_view modName, VerifiedModVersion modVersion); /** * Tells if a mod archive has not been corrupted. @@ -65,12 +70,13 @@ class ModDownloader * Extracts a mod archive to the game folder. * * This extracts a downloaded mod archive from its original location to the - * current game profile, in the remote mods folder. + * current game profile; the install folder is defined by the platform parameter. * * @param modPath location of the downloaded archive + * @param platform origin of the downloaded archive * @returns nothing */ - void ExtractMod(fs::path modPath); + void ExtractMod(fs::path modPath, VerifiedModPlatform platform); public: ModDownloader(); @@ -131,7 +137,8 @@ class ModDownloader MOD_FETCHING_FAILED, MOD_CORRUPTED, // Downloaded archive checksum does not match verified hash NO_DISK_SPACE_AVAILABLE, - NOT_FOUND // Mod is not currently being auto-downloaded + NOT_FOUND, // Mod is not currently being auto-downloaded + UNKNOWN_PLATFORM }; struct MOD_STATE From 8824340644b0da9d30c35232789acdea93db2569 Mon Sep 17 00:00:00 2001 From: p0358 Date: Sun, 8 Sep 2024 00:48:12 +0200 Subject: [PATCH 07/17] Set thread names for game threads (#666) Adds nice thread names that can be visible in crash dumps, non-attachable debuggers and generally in all places where old method of throwing exceptions to attached debugger on game start wouldn't work --- primedev/client/audio.cpp | 40 +++++++++++++++++++++++++++++++++++++++ primedev/core/tier0.cpp | 29 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/primedev/client/audio.cpp b/primedev/client/audio.cpp index 15f5c2aec..4a759cdab 100644 --- a/primedev/client/audio.cpp +++ b/primedev/client/audio.cpp @@ -509,6 +509,40 @@ static void __fastcall h_MilesLog(int level, const char* string) spdlog::info("[MSS] {} - {}", level, string); } +static void(__fastcall* o_pSub_18003EBD0)(DWORD dwThreadID, const char* threadName) = nullptr; +static void __fastcall h_Sub_18003EBD0(DWORD dwThreadID, const char* threadName) +{ + HANDLE hThread = OpenThread(THREAD_SET_LIMITED_INFORMATION, FALSE, dwThreadID); + + if (hThread != NULL) + { + // TODO: This "method" of "charset conversion" from string to wstring is abhorrent. Change it to a proper one + // as soon as Northstar has some helper function to do proper charset conversions. + auto tmp = std::string(threadName); + HRESULT WINAPI _SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription); + _SetThreadDescription(hThread, std::wstring(tmp.begin(), tmp.end()).c_str()); + + CloseHandle(hThread); + } + + o_pSub_18003EBD0(dwThreadID, threadName); +} + +static char*(__fastcall* o_pSub_18003BC10)(void* a1, void* a2, void* a3, void* a4, void* a5, int a6) = nullptr; +static char* __fastcall h_Sub_18003BC10(void* a1, void* a2, void* a3, void* a4, void* a5, int a6) +{ + HANDLE hThread; + char* ret = o_pSub_18003BC10(a1, a2, a3, a4, a5, a6); + + if (ret != NULL && (hThread = reinterpret_cast(*((uint64_t*)ret + 55))) != NULL) + { + HRESULT WINAPI _SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription); + _SetThreadDescription(hThread, L"[Miles] WASAPI Service Thread"); + } + + return ret; +} + ON_DLL_LOAD("mileswin64.dll", MilesWin64_Audio, (CModule module)) { o_pLoadSampleMetadata = module.Offset(0xF110).RCast(); @@ -516,6 +550,12 @@ ON_DLL_LOAD("mileswin64.dll", MilesWin64_Audio, (CModule module)) o_pSub_1800294C0 = module.Offset(0x294C0).RCast(); HookAttach(&(PVOID&)o_pSub_1800294C0, (PVOID)h_Sub_1800294C0); + + o_pSub_18003EBD0 = module.Offset(0x3EBD0).RCast(); + HookAttach(&(PVOID&)o_pSub_18003EBD0, (PVOID)h_Sub_18003EBD0); + + o_pSub_18003BC10 = module.Offset(0x3BC10).RCast(); + HookAttach(&(PVOID&)o_pSub_18003BC10, (PVOID)h_Sub_18003BC10); } ON_DLL_LOAD_RELIESON("engine.dll", MilesLogFuncHooks, ConVar, (CModule module)) diff --git a/primedev/core/tier0.cpp b/primedev/core/tier0.cpp index dd5ac245e..639b3bf8c 100644 --- a/primedev/core/tier0.cpp +++ b/primedev/core/tier0.cpp @@ -18,11 +18,40 @@ void TryCreateGlobalMemAlloc() g_pMemAllocSingleton = CreateGlobalMemAlloc(); // if it already exists, this returns the preexisting IMemAlloc instance } +HRESULT WINAPI _SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription) +{ + // need to grab it dynamically as this function was only introduced at some point in Windows 10 + static decltype(&SetThreadDescription) _SetThreadDescription = + CModule("KernelBase.dll").GetExportedFunction("SetThreadDescription").RCast(); + + if (_SetThreadDescription) + return _SetThreadDescription(hThread, lpThreadDescription); + + return ERROR_OLD_WIN_VERSION; +} + +static void(__fastcall* o_pThreadSetDebugName)(HANDLE threadHandle, const char* name) = nullptr; +static void __fastcall h_ThreadSetDebugName(HANDLE threadHandle, const char* name) +{ + if (threadHandle == 0) + threadHandle = GetCurrentThread(); + + // TODO: This "method" of "charset conversion" from string to wstring is abhorrent. Change it to a proper one + // as soon as Northstar has some helper function to do proper charset conversions. + auto tmp = std::string(name); + _SetThreadDescription(threadHandle, std::wstring(tmp.begin(), tmp.end()).c_str()); + + o_pThreadSetDebugName(threadHandle, name); +} + ON_DLL_LOAD("tier0.dll", Tier0GameFuncs, (CModule module)) { // shouldn't be necessary, but do this just in case TryCreateGlobalMemAlloc(); + o_pThreadSetDebugName = module.GetExportedFunction("ThreadSetDebugName").RCast(); + HookAttach(&(PVOID&)o_pThreadSetDebugName, (PVOID)h_ThreadSetDebugName); + // setup tier0 funcs CommandLine = module.GetExportedFunction("CommandLine").RCast(); Plat_FloatTime = module.GetExportedFunction("Plat_FloatTime").RCast(); From f9a97985da5d30cb67243837726a5d853648b3d6 Mon Sep 17 00:00:00 2001 From: uniboi <64006268+uniboi@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:01:36 +0000 Subject: [PATCH 08/17] plugins: Remove duplicate sqvm destroy log (#822) --- primedev/plugins/plugins.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/primedev/plugins/plugins.cpp b/primedev/plugins/plugins.cpp index f0a9c3c5f..92be9d5cf 100644 --- a/primedev/plugins/plugins.cpp +++ b/primedev/plugins/plugins.cpp @@ -219,7 +219,6 @@ void Plugin::OnSqvmCreated(CSquirrelVM* sqvm) const void Plugin::OnSqvmDestroying(CSquirrelVM* sqvm) const { - NS::log::PLUGINSYS->info("destroying sqvm {}", sqvm->vmContext); m_callbacks->OnSqvmDestroying(sqvm); } From 1f4765d4a82d2ae3a17f66330f30d16f2eab2be7 Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:47:32 +0100 Subject: [PATCH 09/17] dedicated: Remove uses of Autohook from `dedicated.cpp` (#799) Removes use of AUTOHOOK macro from dedicated.cpp --- primedev/dedicated/dedicated.cpp | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/primedev/dedicated/dedicated.cpp b/primedev/dedicated/dedicated.cpp index eca9b9f1f..8b0604fcf 100644 --- a/primedev/dedicated/dedicated.cpp +++ b/primedev/dedicated/dedicated.cpp @@ -8,8 +8,6 @@ #include "masterserver/masterserver.h" #include "util/printcommands.h" -AUTOHOOK_INIT() - bool IsDedicatedServer() { static bool result = strstr(GetCommandLineA(), "-dedicated"); @@ -114,10 +112,8 @@ DWORD WINAPI ConsoleInputThread(PVOID pThreadParameter) return 0; } -// clang-format off -AUTOHOOK(IsGameActiveWindow, engine.dll + 0x1CDC80, -bool,, ()) -// clang-format on +static bool (*o_pIsGameActiveWindow)() = nullptr; +static bool h_IsGameActiveWindow() { return true; } @@ -126,7 +122,8 @@ ON_DLL_LOAD_DEDI_RELIESON("engine.dll", DedicatedServer, ServerPresence, (CModul { spdlog::info("InitialiseDedicated"); - AUTOHOOK_DISPATCH_MODULE(engine.dll) + o_pIsGameActiveWindow = module.Offset(0x1CDC80).RCast(); + HookAttach(&(PVOID&)o_pIsGameActiveWindow, (PVOID)h_IsGameActiveWindow); // Host_Init // prevent a particle init that relies on client dll @@ -270,12 +267,10 @@ ON_DLL_LOAD_DEDI("tier0.dll", DedicatedServerOrigin, (CModule module)) module.GetExportedFunction("Tier0_InitOrigin").Patch("C3"); } -// clang-format off -AUTOHOOK(PrintSquirrelError, server.dll + 0x794D0, -void, __fastcall, (void* sqvm)) -// clang-format on +static void(__fastcall* o_pPrintSquirrelError)(void* sqvm) = nullptr; +static void __fastcall h_PrintSquirrelError(void* sqvm) { - PrintSquirrelError(sqvm); + o_pPrintSquirrelError(sqvm); // close dedicated server if a fatal error is hit // atm, this will crash if not aborted, so this just closes more gracefully @@ -289,7 +284,8 @@ void, __fastcall, (void* sqvm)) ON_DLL_LOAD_DEDI("server.dll", DedicatedServerGameDLL, (CModule module)) { - AUTOHOOK_DISPATCH_MODULE(server.dll) + o_pPrintSquirrelError = module.Offset(0x794D0).RCast(); + HookAttach(&(PVOID&)o_pPrintSquirrelError, (PVOID)h_PrintSquirrelError); if (CommandLine()->CheckParm("-nopakdedi")) { From ba485e9826c13a37945e06f3c00101ea16089266 Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:08:47 +0100 Subject: [PATCH 10/17] dedicated: Remove uses of Autohook from `dedicatedmaterialsystem.cpp` (#800) Removes AUTOHOOK macro from dedicatedmaterialsystem.cpp --- .../dedicated/dedicatedmaterialsystem.cpp | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/primedev/dedicated/dedicatedmaterialsystem.cpp b/primedev/dedicated/dedicatedmaterialsystem.cpp index 010780862..f74cbfe39 100644 --- a/primedev/dedicated/dedicatedmaterialsystem.cpp +++ b/primedev/dedicated/dedicatedmaterialsystem.cpp @@ -1,11 +1,18 @@ #include "dedicated.h" #include "core/tier0.h" -AUTOHOOK_INIT() - -// clang-format off -AUTOHOOK(D3D11CreateDevice, materialsystem_dx11.dll + 0xD9A0E, -HRESULT, __stdcall, ( +static HRESULT(__stdcall* o_pD3D11CreateDevice)( + void* pAdapter, + int DriverType, + HMODULE Software, + UINT Flags, + int* pFeatureLevels, + UINT FeatureLevels, + UINT SDKVersion, + void** ppDevice, + int* pFeatureLevel, + void** ppImmediateContext) = nullptr; +static HRESULT __stdcall h_D3D11CreateDevice( void* pAdapter, int DriverType, HMODULE Software, @@ -15,8 +22,7 @@ HRESULT, __stdcall, ( UINT SDKVersion, void** ppDevice, int* pFeatureLevel, - void** ppImmediateContext)) -// clang-format on + void** ppImmediateContext) { // note: this is super duper temp pretty much just messing around with it // does run surprisingly well on dedi for a software driver tho if you ignore the +1gb ram usage at times, seems like dedi doesn't @@ -26,13 +32,14 @@ HRESULT, __stdcall, ( if (CommandLine()->CheckParm("-softwared3d11")) DriverType = 5; // D3D_DRIVER_TYPE_WARP - return D3D11CreateDevice( + return o_pD3D11CreateDevice( pAdapter, DriverType, Software, Flags, pFeatureLevels, FeatureLevels, SDKVersion, ppDevice, pFeatureLevel, ppImmediateContext); } ON_DLL_LOAD_DEDI("materialsystem_dx11.dll", DedicatedServerMaterialSystem, (CModule module)) { - AUTOHOOK_DISPATCH() + o_pD3D11CreateDevice = module.Offset(0xD9A0E).RCast(); + HookAttach(&(PVOID&)o_pD3D11CreateDevice, (PVOID)h_D3D11CreateDevice); // CMaterialSystem::FindMaterial // make the game always use the error material From 6737a344c012c0f7fd19cd593949dd3dbe5a0cb7 Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:26:05 +0100 Subject: [PATCH 11/17] engine: Remove uses of Autohook from `hoststate.cpp` (#806) Removes use of AUTOHOOK macro from hoststate.cpp. --- primedev/engine/hoststate.cpp | 64 +++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/primedev/engine/hoststate.cpp b/primedev/engine/hoststate.cpp index d5942551a..4a4d909da 100644 --- a/primedev/engine/hoststate.cpp +++ b/primedev/engine/hoststate.cpp @@ -9,14 +9,12 @@ #include "squirrel/squirrel.h" #include "plugins/pluginmanager.h" -AUTOHOOK_INIT() - CHostState* g_pHostState; std::string sLastMode; -VAR_AT(engine.dll + 0x13FA6070, ConVar*, Cvar_hostport); -FUNCTION_AT(engine.dll + 0x1232C0, void, __fastcall, _Cmd_Exec_f, (const CCommand& arg, bool bOnlyIfExists, bool bUseWhitelists)); +static ConVar* Cvar_hostport = nullptr; +static void(__fastcall* _Cmd_Exec_f)(const CCommand& arg, bool bOnlyIfExists, bool bUseWhitelists) = nullptr; void ServerStartingOrChangingMap() { @@ -53,10 +51,8 @@ void ServerStartingOrChangingMap() g_pServerAuthentication->m_bStartingLocalSPGame = false; } -// clang-format off -AUTOHOOK(CHostState__State_NewGame, engine.dll + 0x16E7D0, -void, __fastcall, (CHostState* self)) -// clang-format on +static void(__fastcall* o_pCHostState__State_NewGame)(CHostState* self) = nullptr; +static void __fastcall h_CHostState__State_NewGame(CHostState* self) { spdlog::info("HostState: NewGame"); @@ -70,7 +66,7 @@ void, __fastcall, (CHostState* self)) ServerStartingOrChangingMap(); double dStartTime = Plat_FloatTime(); - CHostState__State_NewGame(self); + o_pCHostState__State_NewGame(self); spdlog::info("loading took {}s", Plat_FloatTime() - dStartTime); // setup server presence @@ -82,10 +78,8 @@ void, __fastcall, (CHostState* self)) g_pServerAuthentication->m_bNeedLocalAuthForNewgame = false; } -// clang-format off -AUTOHOOK(CHostState__State_LoadGame, engine.dll + 0x16E730, -void, __fastcall, (CHostState* self)) -// clang-format on +static void(__fastcall* o_pCHostState__State_LoadGame)(CHostState* self) = nullptr; +static void __fastcall h_CHostState__State_LoadGame(CHostState* self) { // singleplayer server starting // useless in 99% of cases but without it things could potentially break very much @@ -100,7 +94,7 @@ void, __fastcall, (CHostState* self)) g_pServerAuthentication->m_bStartingLocalSPGame = true; double dStartTime = Plat_FloatTime(); - CHostState__State_LoadGame(self); + o_pCHostState__State_LoadGame(self); spdlog::info("loading took {}s", Plat_FloatTime() - dStartTime); // no server presence, can't do it because no map name in hoststate @@ -109,32 +103,28 @@ void, __fastcall, (CHostState* self)) g_pServerAuthentication->m_bNeedLocalAuthForNewgame = false; } -// clang-format off -AUTOHOOK(CHostState__State_ChangeLevelMP, engine.dll + 0x16E520, -void, __fastcall, (CHostState* self)) -// clang-format on +static void(__fastcall* o_pCHostState__State_ChangeLevelMP)(CHostState* self) = nullptr; +static void __fastcall h_CHostState__State_ChangeLevelMP(CHostState* self) { spdlog::info("HostState: ChangeLevelMP"); ServerStartingOrChangingMap(); double dStartTime = Plat_FloatTime(); - CHostState__State_ChangeLevelMP(self); + o_pCHostState__State_ChangeLevelMP(self); spdlog::info("loading took {}s", Plat_FloatTime() - dStartTime); g_pServerPresence->SetMap(g_pHostState->m_levelName); } -// clang-format off -AUTOHOOK(CHostState__State_GameShutdown, engine.dll + 0x16E640, -void, __fastcall, (CHostState* self)) -// clang-format on +static void(__fastcall* o_pCHostState__State_GameShutdown)(CHostState* self) = nullptr; +static void __fastcall h_CHostState__State_GameShutdown(CHostState* self) { spdlog::info("HostState: GameShutdown"); g_pServerPresence->DestroyPresence(); - CHostState__State_GameShutdown(self); + o_pCHostState__State_GameShutdown(self); // run gamemode cleanup cfg now instead of when we start next map if (sLastMode.length()) @@ -153,12 +143,10 @@ void, __fastcall, (CHostState* self)) } } -// clang-format off -AUTOHOOK(CHostState__FrameUpdate, engine.dll + 0x16DB00, -void, __fastcall, (CHostState* self, double flCurrentTime, float flFrameTime)) -// clang-format on +static void(__fastcall* o_pCHostState__FrameUpdate)(CHostState* self, double flCurrentTime, float flFrameTime) = nullptr; +static void __fastcall h_CHostState__FrameUpdate(CHostState* self, double flCurrentTime, float flFrameTime) { - CHostState__FrameUpdate(self, flCurrentTime, flFrameTime); + o_pCHostState__FrameUpdate(self, flCurrentTime, flFrameTime); if (*g_pServerState == server_state_t::ss_active) { @@ -184,7 +172,23 @@ void, __fastcall, (CHostState* self, double flCurrentTime, float flFrameTime)) ON_DLL_LOAD_RELIESON("engine.dll", HostState, ConVar, (CModule module)) { - AUTOHOOK_DISPATCH() + o_pCHostState__State_NewGame = module.Offset(0x16E7D0).RCast(); + HookAttach(&(PVOID&)o_pCHostState__State_NewGame, (PVOID)h_CHostState__State_NewGame); + + o_pCHostState__State_LoadGame = module.Offset(0x16E730).RCast(); + HookAttach(&(PVOID&)o_pCHostState__State_LoadGame, (PVOID)h_CHostState__State_LoadGame); + + o_pCHostState__State_ChangeLevelMP = module.Offset(0x16E520).RCast(); + HookAttach(&(PVOID&)o_pCHostState__State_ChangeLevelMP, (PVOID)h_CHostState__State_ChangeLevelMP); + + o_pCHostState__State_GameShutdown = module.Offset(0x16E640).RCast(); + HookAttach(&(PVOID&)o_pCHostState__State_GameShutdown, (PVOID)h_CHostState__State_GameShutdown); + + o_pCHostState__FrameUpdate = module.Offset(0x16DB00).RCast(); + HookAttach(&(PVOID&)o_pCHostState__FrameUpdate, (PVOID)h_CHostState__FrameUpdate); + + Cvar_hostport = module.Offset(0x13FA6070).RCast(); + _Cmd_Exec_f = module.Offset(0x1232C0).RCast(); g_pHostState = module.Offset(0x7CF180).RCast(); } From 71349f05b69923dbf091d27f8e256bcc3022e859 Mon Sep 17 00:00:00 2001 From: Gazyi Date: Tue, 1 Oct 2024 15:28:58 +0300 Subject: [PATCH 12/17] Force `_WIN32_WINNT` variable (#770) to avoid instant crashes on older Windows platforms. --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index a9646ae10..c9516e521 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,3 +50,7 @@ include_directories(primedev/thirdparty) # Targets add_subdirectory(primedev) +# Forces Minizip to not use functions that are not available in Win 7 libraries. +if(WIN32) + target_compile_definitions(minizip PUBLIC -D_WIN32_WINNT=0x0601) +endif() From 8a29c5bbd05cf52007a8aa3ff833c8fed237a625 Mon Sep 17 00:00:00 2001 From: F1F7Y <64418963+F1F7Y@users.noreply.github.com> Date: Sun, 6 Oct 2024 09:59:22 +0200 Subject: [PATCH 13/17] core: Remove unused SourceInterface class (#816) Removes unused `SourceInterface` class, moves `InterfaceStatus` enum to `tier1.h`. --- primedev/Northstar.cmake | 1 - primedev/core/convar/convar.h | 1 - primedev/core/sourceinterface.cpp | 1 - primedev/core/sourceinterface.h | 32 ----------------------- primedev/core/tier1.h | 6 +++++ primedev/logging/sourceconsole.h | 1 - primedev/plugins/interfaces/interface.cpp | 1 + primedev/plugins/interfaces/sys/ISys.cpp | 1 + primedev/plugins/plugins.cpp | 1 - primedev/plugins/plugins.h | 2 +- 10 files changed, 9 insertions(+), 38 deletions(-) delete mode 100644 primedev/core/sourceinterface.h diff --git a/primedev/Northstar.cmake b/primedev/Northstar.cmake index 4e8ec973b..35383e69c 100644 --- a/primedev/Northstar.cmake +++ b/primedev/Northstar.cmake @@ -52,7 +52,6 @@ add_library( "core/memalloc.cpp" "core/memalloc.h" "core/sourceinterface.cpp" - "core/sourceinterface.h" "core/tier0.cpp" "core/tier0.h" "core/tier1.cpp" diff --git a/primedev/core/convar/convar.h b/primedev/core/convar/convar.h index f0366b466..33a50c1c4 100644 --- a/primedev/core/convar/convar.h +++ b/primedev/core/convar/convar.h @@ -1,5 +1,4 @@ #pragma once -#include "core/sourceinterface.h" #include "core/math/color.h" #include "cvar.h" #include "concommand.h" diff --git a/primedev/core/sourceinterface.cpp b/primedev/core/sourceinterface.cpp index 74e4a9963..7ce339255 100644 --- a/primedev/core/sourceinterface.cpp +++ b/primedev/core/sourceinterface.cpp @@ -1,4 +1,3 @@ -#include "sourceinterface.h" #include "logging/sourceconsole.h" // really wanted to do a modular callback system here but honestly couldn't be bothered so hardcoding stuff for now: todo later diff --git a/primedev/core/sourceinterface.h b/primedev/core/sourceinterface.h deleted file mode 100644 index 730339daa..000000000 --- a/primedev/core/sourceinterface.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include - -// interface return status -enum class InterfaceStatus : int -{ - IFACE_OK = 0, - IFACE_FAILED, -}; - -// literally just copied from ttf2sdk definition -typedef void* (*CreateInterfaceFn)(const char* pName, int* pReturnCode); - -template class SourceInterface -{ -private: - T* m_interface; - -public: - SourceInterface(const std::string& moduleName, const std::string& interfaceName) - { - HMODULE handle = GetModuleHandleA(moduleName.c_str()); - CreateInterfaceFn createInterface = (CreateInterfaceFn)GetProcAddress(handle, "CreateInterface"); - m_interface = (T*)createInterface(interfaceName.c_str(), NULL); - if (m_interface == nullptr) - spdlog::error("Failed to call CreateInterface for %s in %s", interfaceName, moduleName); - } - - T* operator->() const { return m_interface; } - - operator T*() const { return m_interface; } -}; diff --git a/primedev/core/tier1.h b/primedev/core/tier1.h index d162e7c84..36f577cc2 100644 --- a/primedev/core/tier1.h +++ b/primedev/core/tier1.h @@ -7,6 +7,12 @@ #define CREATEINTERFACE_PROCNAME "CreateInterface" +enum class InterfaceStatus : int +{ + IFACE_OK = 0, + IFACE_FAILED, +}; + typedef void* (*CreateInterfaceFn)(const char* pName, int* pReturnCode); CMemory Sys_GetFactoryPtr(const std::string& svModuleName, const std::string& svFact); diff --git a/primedev/logging/sourceconsole.h b/primedev/logging/sourceconsole.h index 35cc1723c..8c647fb42 100644 --- a/primedev/logging/sourceconsole.h +++ b/primedev/logging/sourceconsole.h @@ -1,5 +1,4 @@ #pragma once -#include "core/sourceinterface.h" #include "spdlog/sinks/base_sink.h" #include diff --git a/primedev/plugins/interfaces/interface.cpp b/primedev/plugins/interfaces/interface.cpp index bc9005421..e82005604 100644 --- a/primedev/plugins/interfaces/interface.cpp +++ b/primedev/plugins/interfaces/interface.cpp @@ -1,4 +1,5 @@ #include +#include "core/tier1.h" #include "interface.h" InterfaceReg* s_pInterfaceRegs; diff --git a/primedev/plugins/interfaces/sys/ISys.cpp b/primedev/plugins/interfaces/sys/ISys.cpp index 6b0b41ddf..948e7d90b 100644 --- a/primedev/plugins/interfaces/sys/ISys.cpp +++ b/primedev/plugins/interfaces/sys/ISys.cpp @@ -1,3 +1,4 @@ +#include "core/tier1.h" #include "plugins/interfaces/interface.h" #include "ISys.h" #include "plugins/plugins.h" diff --git a/primedev/plugins/plugins.cpp b/primedev/plugins/plugins.cpp index 92be9d5cf..21169c061 100644 --- a/primedev/plugins/plugins.cpp +++ b/primedev/plugins/plugins.cpp @@ -2,7 +2,6 @@ #include "pluginmanager.h" #include "squirrel/squirrel.h" #include "util/wininfo.h" -#include "core/sourceinterface.h" #include "logging/logging.h" #include "dedicated/dedicated.h" diff --git a/primedev/plugins/plugins.h b/primedev/plugins/plugins.h index d004038c4..71e184c7c 100644 --- a/primedev/plugins/plugins.h +++ b/primedev/plugins/plugins.h @@ -1,5 +1,5 @@ #pragma once -#include "core/sourceinterface.h" +#include "core/tier1.h" #include "plugins/interfaces/interface.h" #include "plugins/interfaces/IPluginId.h" #include "plugins/interfaces/IPluginCallbacks.h" From 42d97028e1a474e7fecc1de7e76c5d92ecf2c28f Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Sun, 6 Oct 2024 17:01:50 +0100 Subject: [PATCH 14/17] Fix rpak aliasing not working when trying to alias towards a vanilla rpak (#825) Replace incorrect variable with hardcoded `-1` Don't account for the modded path in pak aliases --- primedev/core/filesystem/rpakfilesystem.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/primedev/core/filesystem/rpakfilesystem.cpp b/primedev/core/filesystem/rpakfilesystem.cpp index ebb9085a7..c3e5e74ee 100644 --- a/primedev/core/filesystem/rpakfilesystem.cpp +++ b/primedev/core/filesystem/rpakfilesystem.cpp @@ -342,7 +342,7 @@ void PakLoadManager::UnloadDependentPaks(PakHandle handle) static void HandlePakAliases(std::string& originalPath) { // convert the pak being loaded to its aliased one, e.g. aliasing mp_hub_timeshift => sp_hub_timeshift - for (int64_t i = g_pModManager->m_LoadedMods.size() - 1; i > PakHandle::INVALID; i--) + for (int64_t i = g_pModManager->m_LoadedMods.size() - 1; i > -1; i--) { Mod* mod = &g_pModManager->m_LoadedMods[i]; if (!mod->m_bEnabled) @@ -350,7 +350,7 @@ static void HandlePakAliases(std::string& originalPath) if (mod->RpakAliases.find(originalPath) != mod->RpakAliases.end()) { - originalPath = (mod->m_ModDirectory / "paks" / mod->RpakAliases[originalPath]).string(); + originalPath = mod->RpakAliases[originalPath]; return; } } From 27e17113161323b5a6269d395dbcc04162f50319 Mon Sep 17 00:00:00 2001 From: uniboi <64006268+uniboi@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:20:54 +0000 Subject: [PATCH 15/17] plugins: Allow plugins to customize their log colors (#823) Allows plugins to specify a custom colour for their indicator in the logs --- primedev/plugins/interfaces/IPluginId.h | 1 + primedev/plugins/plugins.cpp | 10 +++++++++- primedev/plugins/plugins.h | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/primedev/plugins/interfaces/IPluginId.h b/primedev/plugins/interfaces/IPluginId.h index dc4c548b3..0b025224b 100644 --- a/primedev/plugins/interfaces/IPluginId.h +++ b/primedev/plugins/interfaces/IPluginId.h @@ -18,6 +18,7 @@ enum class PluginString : int enum class PluginField : int { CONTEXT = 0, + COLOR = 1, }; // an interface that is required from every plugin to query data about it diff --git a/primedev/plugins/plugins.cpp b/primedev/plugins/plugins.cpp index 21169c061..3e6231676 100644 --- a/primedev/plugins/plugins.cpp +++ b/primedev/plugins/plugins.cpp @@ -23,6 +23,7 @@ bool isValidSquirrelIdentifier(std::string s) Plugin::Plugin(std::string path) : m_location(path) + , m_logColor(NS::Colors::PLUGIN) { HMODULE pluginModule = GetModuleHandleA(path.c_str()); @@ -69,6 +70,13 @@ Plugin::Plugin(std::string path) m_runOnServer = context & PluginContext::DEDICATED; m_runOnClient = context & PluginContext::CLIENT; + int64_t logColor = m_pluginId->GetField(PluginField::COLOR); + // Apply custom colour if plugin has specified one + if ((logColor & 0xFFFFFF) != 0) + { + m_logColor = Color((int)(logColor & 0xFF), (int)((logColor >> 8) & 0xFF), (int)((logColor >> 16) & 0xFF)); + } + if (!name) { NS::log::PLUGINSYS->error("Could not load name of plugin at '{}'", path); @@ -105,7 +113,7 @@ Plugin::Plugin(std::string path) return; } - m_logger = std::make_shared(m_logName, NS::Colors::PLUGIN); + m_logger = std::make_shared(m_logName, m_logColor); RegisterLogger(m_logger); if (IsDedicatedServer() && !m_runOnServer) diff --git a/primedev/plugins/plugins.h b/primedev/plugins/plugins.h index 71e184c7c..95ec08b5c 100644 --- a/primedev/plugins/plugins.h +++ b/primedev/plugins/plugins.h @@ -20,6 +20,7 @@ class Plugin std::string m_location; // path of the dll bool m_runOnServer; bool m_runOnClient; + Color m_logColor; public: HMODULE m_handle; From 41583e4f690686095e728d61bb13b4526b29a69e Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:15:40 +0000 Subject: [PATCH 16/17] server: Remove uses of Autohook from `serverauthentication.cpp` (#820) Manually hook - `CBaseServer__ConnectClient` - `CBaseClient__Connect` - `CBaseClient__ActivatePlayer` - `CBaseClient__Disconnect` Remove `AUTOHOOK_INIT` and `AUTOHOOK_DISPATCH` --- primedev/server/auth/serverauthentication.cpp | 67 ++++++++++++------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/primedev/server/auth/serverauthentication.cpp b/primedev/server/auth/serverauthentication.cpp index d0d4c698f..58268bcfa 100644 --- a/primedev/server/auth/serverauthentication.cpp +++ b/primedev/server/auth/serverauthentication.cpp @@ -19,8 +19,6 @@ #include #include -AUTOHOOK_INIT() - // global vars ServerAuthenticationManager* g_pServerAuthentication; CBaseServer__RejectConnectionType CBaseServer__RejectConnection; @@ -207,9 +205,25 @@ void ServerAuthenticationManager::WritePersistentData(CBaseClient* pPlayer) char* pNextPlayerToken; uint64_t iNextPlayerUid; -// clang-format off -AUTOHOOK(CBaseServer__ConnectClient, engine.dll + 0x114430, -void*,, ( +static void* (*o_pCBaseServer__ConnectClient)( + void* self, + void* addr, + void* a3, + uint32_t a4, + uint32_t a5, + int32_t a6, + void* a7, + char* playerName, + char* serverFilter, + void* a10, + char a11, + void* a12, + char a13, + char a14, + int64_t uid, + uint32_t a16, + uint32_t a17) = nullptr; +static void* h_CBaseServer__ConnectClient( void* self, void* addr, void* a3, @@ -226,22 +240,21 @@ void*,, ( char a14, int64_t uid, uint32_t a16, - uint32_t a17)) -// clang-format on + uint32_t a17) { // auth tokens are sent with serverfilter, can't be accessed from player struct to my knowledge, so have to do this here pNextPlayerToken = serverFilter; iNextPlayerUid = uid; - return CBaseServer__ConnectClient(self, addr, a3, a4, a5, a6, a7, playerName, serverFilter, a10, a11, a12, a13, a14, uid, a16, a17); + return o_pCBaseServer__ConnectClient(self, addr, a3, a4, a5, a6, a7, playerName, serverFilter, a10, a11, a12, a13, a14, uid, a16, a17); } ConVar* Cvar_ns_allowuserclantags; -// clang-format off -AUTOHOOK(CBaseClient__Connect, engine.dll + 0x101740, -bool,, (CBaseClient* self, char* pName, void* pNetChannel, char bFakePlayer, void* a5, char pDisconnectReason[256], void* a7)) -// clang-format on +static bool (*o_pCBaseClient__Connect)( + CBaseClient* self, char* pName, void* pNetChannel, char bFakePlayer, void* a5, char pDisconnectReason[256], void* a7) = nullptr; +static bool +h_CBaseClient__Connect(CBaseClient* self, char* pName, void* pNetChannel, char bFakePlayer, void* a5, char pDisconnectReason[256], void* a7) { const char* pAuthenticationFailure = nullptr; char pVerifiedName[64]; @@ -267,7 +280,7 @@ bool,, (CBaseClient* self, char* pName, void* pNetChannel, char bFakePlayer, voi } // try to actually connect the player - if (!CBaseClient__Connect(self, pVerifiedName, pNetChannel, bFakePlayer, a5, pDisconnectReason, a7)) + if (!o_pCBaseClient__Connect(self, pVerifiedName, pNetChannel, bFakePlayer, a5, pDisconnectReason, a7)) return false; // we already know this player's authentication data is legit, actually write it to them now @@ -279,10 +292,8 @@ bool,, (CBaseClient* self, char* pName, void* pNetChannel, char bFakePlayer, voi return true; } -// clang-format off -AUTOHOOK(CBaseClient__ActivatePlayer, engine.dll + 0x100F80, -void,, (CBaseClient* self)) -// clang-format on +static void (*o_pCBaseClient__ActivatePlayer)(CBaseClient* self) = nullptr; +static void h_CBaseClient__ActivatePlayer(CBaseClient* self) { // if we're authed, write our persistent data // RemovePlayerAuthData returns true if it removed successfully, i.e. on first call only, and we only want to write on >= second call @@ -294,13 +305,11 @@ void,, (CBaseClient* self)) g_pServerPresence->SetPlayerCount((int)g_pServerAuthentication->m_PlayerAuthenticationData.size()); } - CBaseClient__ActivatePlayer(self); + o_pCBaseClient__ActivatePlayer(self); } -// clang-format off -AUTOHOOK(_CBaseClient__Disconnect, engine.dll + 0x1012C0, -void,, (CBaseClient* self, uint32_t unknownButAlways1, const char* pReason, ...)) -// clang-format on +static void (*o_pCBaseClient__Disconnect)(CBaseClient* self, uint32_t unknownButAlways1, const char* pReason, ...) = nullptr; +static void h_CBaseClient__Disconnect(CBaseClient* self, uint32_t unknownButAlways1, const char* pReason, ...) { // have to manually format message because can't pass varargs to original func char buf[1024]; @@ -328,7 +337,7 @@ void,, (CBaseClient* self, uint32_t unknownButAlways1, const char* pReason, ...) g_pServerPresence->SetPlayerCount((int)g_pServerAuthentication->m_PlayerAuthenticationData.size()); - _CBaseClient__Disconnect(self, unknownButAlways1, buf); + o_pCBaseClient__Disconnect(self, unknownButAlways1, buf); } void ConCommand_ns_resetpersistence(const CCommand& args) @@ -346,7 +355,17 @@ void ConCommand_ns_resetpersistence(const CCommand& args) ON_DLL_LOAD_RELIESON("engine.dll", ServerAuthentication, (ConCommand, ConVar), (CModule module)) { - AUTOHOOK_DISPATCH() + o_pCBaseServer__ConnectClient = module.Offset(0x114430).RCast(); + HookAttach(&(PVOID&)o_pCBaseServer__ConnectClient, (PVOID)h_CBaseServer__ConnectClient); + + o_pCBaseClient__Connect = module.Offset(0x101740).RCast(); + HookAttach(&(PVOID&)o_pCBaseClient__Connect, (PVOID)h_CBaseClient__Connect); + + o_pCBaseClient__ActivatePlayer = module.Offset(0x100F80).RCast(); + HookAttach(&(PVOID&)o_pCBaseClient__ActivatePlayer, (PVOID)h_CBaseClient__ActivatePlayer); + + o_pCBaseClient__Disconnect = module.Offset(0x1012C0).RCast(); + HookAttach(&(PVOID&)o_pCBaseClient__Disconnect, (PVOID)h_CBaseClient__Disconnect); g_pServerAuthentication = new ServerAuthenticationManager; From 10041ca495aa35154671a4ab5a9737ddeee83dd0 Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:26:29 +0000 Subject: [PATCH 17/17] mods: Remove uses of Autohook from `modkeyvalues.cpp` (#818) Remove unused AUTOHOOK_INIT --- primedev/mods/compiled/modkeyvalues.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/primedev/mods/compiled/modkeyvalues.cpp b/primedev/mods/compiled/modkeyvalues.cpp index e44a81d31..dfff706d0 100644 --- a/primedev/mods/compiled/modkeyvalues.cpp +++ b/primedev/mods/compiled/modkeyvalues.cpp @@ -3,8 +3,6 @@ #include -AUTOHOOK_INIT() - void ModManager::TryBuildKeyValues(const char* filename) { spdlog::info("Building KeyValues for file {}", filename);