diff options
| author | GeckoEidechse <gecko.eidechse+git@pm.me> | 2025-03-01 23:09:32 +0100 |
|---|---|---|
| committer | GeckoEidechse <gecko.eidechse+git@pm.me> | 2025-03-01 23:09:32 +0100 |
| commit | c277e3caf6c1f180a2f0f7a6171286c929f1ad59 (patch) | |
| tree | a86a9294dcdc68cc6d6c2f23209046f5690e28f7 | |
| parent | d8ba5e4292dcc59a933baf03eacb8a8cb5281e6c (diff) | |
| parent | c7e2d7e593a30d716bb529a3496c85544279cbeb (diff) | |
| download | NorthstarLauncher-feat/overhaul-mod-loading-locations.tar.gz NorthstarLauncher-feat/overhaul-mod-loading-locations.zip | |
Merge branch 'main' into feat/overhaul-mod-loading-locationsfeat/overhaul-mod-loading-locations
| -rw-r--r-- | .github/workflows/release.yml | 8 | ||||
| -rw-r--r-- | primedev/mods/autodownload/moddownloader.cpp | 57 | ||||
| -rw-r--r-- | primedev/mods/autodownload/moddownloader.h | 14 | ||||
| -rw-r--r-- | primedev/mods/modmanager.cpp | 245 | ||||
| -rw-r--r-- | primedev/mods/modmanager.h | 12 | ||||
| -rw-r--r-- | primedev/plugins/pluginmanager.cpp | 12 | ||||
| -rw-r--r-- | primedev/plugins/plugins.cpp | 14 | ||||
| -rw-r--r-- | primedev/plugins/plugins.h | 2 | ||||
| -rw-r--r-- | primedev/scripts/client/scriptbrowserhooks.cpp | 13 | ||||
| -rw-r--r-- | primedev/shared/misccommands.cpp | 1 |
10 files changed, 211 insertions, 167 deletions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a440aea3..831b192e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: - name: Build run: cmake --build build/ - name: Upload launcher build as artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: northstar-launcher path: | @@ -40,7 +40,7 @@ jobs: build/game/*.dll build/game/bin/x64_retail/*.dll - name: Upload debug build artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: launcher-debug-files path: | @@ -53,12 +53,12 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Download compiled launcher - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: northstar-launcher path: northstar-launcher - name: Download compiled launcher - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: launcher-debug-files path: launcher-debug-files diff --git a/primedev/mods/autodownload/moddownloader.cpp b/primedev/mods/autodownload/moddownloader.cpp index 21b98942..de95a0c2 100644 --- a/primedev/mods/autodownload/moddownloader.cpp +++ b/primedev/mods/autodownload/moddownloader.cpp @@ -176,7 +176,7 @@ int ModDownloader::ModFetchingProgressCallback( return 0; } -std::optional<fs::path> ModDownloader::FetchModFromDistantStore(std::string_view modName, VerifiedModVersion version) +std::tuple<fs::path, bool> ModDownloader::FetchModFromDistantStore(std::string_view modName, VerifiedModVersion version) { std::string url = version.downloadLink; std::string archiveName = fs::path(url).filename().generic_string(); @@ -190,7 +190,7 @@ std::optional<fs::path> ModDownloader::FetchModFromDistantStore(std::string_view modState.state = DOWNLOADING; // Download the actual archive - bool failed = false; + bool success = false; FILE* fp = fopen(downloadPath.generic_string().c_str(), "wb"); CURLcode result; CURL* easyhandle; @@ -219,13 +219,14 @@ std::optional<fs::path> ModDownloader::FetchModFromDistantStore(std::string_view if (result == CURLcode::CURLE_OK) { spdlog::info("Mod archive successfully fetched."); - return std::optional<fs::path>(downloadPath); + success = true; } else { spdlog::error("Fetching mod archive failed: {}", curl_easy_strerror(result)); - return std::optional<fs::path>(); } + + return {downloadPath, success}; } bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecksum) @@ -399,11 +400,9 @@ int GetModArchiveSize(unzFile file, unz_global_info64 info) return totalSize; } -void ModDownloader::ExtractMod(fs::path modPath, VerifiedModPlatform platform) +void ModDownloader::ExtractMod(fs::path modPath, fs::path destinationPath, VerifiedModPlatform platform) { unzFile file; - std::string name; - fs::path modDirectory; file = unzOpen(modPath.generic_string().c_str()); ScopeGuard cleanup( @@ -445,11 +444,6 @@ void ModDownloader::ExtractMod(fs::path modPath, VerifiedModPlatform platform) return; } - // Mod directory name (removing the ".zip" fom the archive name) - name = modPath.filename().string(); - name = name.substr(0, name.length() - 4); - modDirectory = GetRemoteModFolderPath() / name; - for (int i = 0; i < gi.number_entry; i++) { char zipFilename[256]; @@ -459,7 +453,7 @@ void ModDownloader::ExtractMod(fs::path modPath, VerifiedModPlatform platform) // Extract file { std::error_code ec; - fs::path fileDestination = modDirectory / zipFilename; + fs::path fileDestination = destinationPath / zipFilename; spdlog::info("=> {}", fileDestination.generic_string()); // Create parent directory if needed @@ -589,6 +583,9 @@ void ModDownloader::ExtractMod(fs::path modPath, VerifiedModPlatform platform) } } } + + // Mod extraction went fine + modState.state = DONE; } void ModDownloader::DownloadMod(std::string modName, std::string modVersion) @@ -603,11 +600,14 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion) std::thread requestThread( [this, modName, modVersion]() { + std::string name; fs::path archiveLocation; + fs::path modDirectory; ScopeGuard cleanup( [&] { + // Remove downloaded archive try { remove(archiveLocation); @@ -617,14 +617,31 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion) spdlog::error("Error while removing downloaded archive: {}", a.what()); } + // Remove mod if auto-download process failed + if (modState.state != DONE) + { + try + { + remove_all(modDirectory); + } + catch (const std::exception& e) + { + spdlog::error("Error while removing downloaded mod: {}", e.what()); + } + } + spdlog::info("Done cleaning after downloading {}.", modName); }); // Download mod archive VerifiedModVersion fullVersion = verifiedMods[modName].versions[modVersion]; std::string expectedHash = fullVersion.checksum; - std::optional<fs::path> fetchingResult = FetchModFromDistantStore(std::string_view(modName), fullVersion); - if (!fetchingResult.has_value()) + + const std::tuple<fs::path, bool> downloadResult = FetchModFromDistantStore(std::string_view(modName), fullVersion); + archiveLocation = get<0>(downloadResult); + bool downloadSuccessful = get<1>(downloadResult); + + if (!downloadSuccessful) { spdlog::error("Something went wrong while fetching archive, aborting."); if (modState.state != ABORTED) @@ -633,7 +650,7 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion) } return; } - archiveLocation = fetchingResult.value(); + if (!IsModLegit(archiveLocation, std::string_view(expectedHash))) { spdlog::warn("Archive hash does not match expected checksum, aborting."); @@ -641,9 +658,13 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion) return; } + // Mod directory name (removing the ".zip" fom the archive name) + name = archiveLocation.filename().string(); + name = name.substr(0, name.length() - 4); + modDirectory = GetRemoteModFolderPath() / name; + // Extract downloaded mod archive - ExtractMod(archiveLocation, fullVersion.platform); - modState.state = DONE; + ExtractMod(archiveLocation, modDirectory, fullVersion.platform); }); requestThread.detach(); diff --git a/primedev/mods/autodownload/moddownloader.h b/primedev/mods/autodownload/moddownloader.h index 2ac72a48..f8c0c664 100644 --- a/primedev/mods/autodownload/moddownloader.h +++ b/primedev/mods/autodownload/moddownloader.h @@ -43,14 +43,12 @@ private: * input mod name as mod dependency string if bypass flag is set up; fetched * archive is then stored in a temporary location. * - * If something went wrong during archive download, this will return an empty - * optional object. * * @param modName name of the mod to be downloaded * @param modVersion version of the mod to be downloaded - * @returns location of the downloaded archive + * @returns tuple containing location of the downloaded archive and whether download completed successfully */ - std::optional<fs::path> FetchModFromDistantStore(std::string_view modName, VerifiedModVersion modVersion); + std::tuple<fs::path, bool> FetchModFromDistantStore(std::string_view modName, VerifiedModVersion modVersion); /** * Tells if a mod archive has not been corrupted. @@ -69,14 +67,16 @@ private: /** * Extracts a mod archive to the game folder. * - * This extracts a downloaded mod archive from its original location to the - * current game profile; the install folder is defined by the platform parameter. + * This extracts a downloaded mod archive from its original location, `modPath`, + * to the specified `destinationPath`; the way to decompress the archive is + * defined by the `platform` parameter. * * @param modPath location of the downloaded archive + * @param destinationPath destination of the extraction * @param platform origin of the downloaded archive * @returns nothing */ - void ExtractMod(fs::path modPath, VerifiedModPlatform platform); + void ExtractMod(fs::path modPath, fs::path destinationPath, VerifiedModPlatform platform); public: ModDownloader(); diff --git a/primedev/mods/modmanager.cpp b/primedev/mods/modmanager.cpp index 43ee53ee..0794c97f 100644 --- a/primedev/mods/modmanager.cpp +++ b/primedev/mods/modmanager.cpp @@ -111,8 +111,6 @@ void ModManager::LoadMods() if (m_bHasLoadedMods) UnloadMods(); - std::vector<fs::path> modDirs; - // ensure dirs exist fs::remove_all(GetCompiledAssetsPath()); fs::create_directories(GetCoreModFolderPath()); @@ -138,123 +136,8 @@ void ModManager::LoadMods() m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject(); } - // get mod directories - std::filesystem::directory_iterator coreModsDir = fs::directory_iterator(GetCoreModFolderPath()); - std::filesystem::directory_iterator manualModsDir = fs::directory_iterator(GetManualModFolderPath()); - std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath()); - std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreLegacyModFolderPath()); - - // Set up regex for `Northstar.*` pattern - std::regex northstar_pattern(R"(.*\\Northstar\..+)"); - for (fs::directory_entry dir : coreModsDir) - { - if (!std::regex_match(dir.path().string(), northstar_pattern)) - { - spdlog::warn( - "The following directory did not match 'Northstar.*' and is most likely an incorrectly manually installed mod: {}", - dir.path().string()); - continue; // skip loading mod that doesn't match - } - if (fs::exists(dir.path() / "mod.json")) - modDirs.push_back(dir.path()); - } - - for (fs::directory_entry dir : manualModsDir) - if (fs::exists(dir.path() / "mod.json")) - modDirs.push_back(dir.path()); - - // Special case for Thunderstore and remote mods directories - // Set up regex for `AUTHOR-MOD-VERSION` pattern - std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))"); - - for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir}) - { - for (fs::directory_entry dir : dirIterator) - { - fs::path modsDir = dir.path() / "mods"; // Check for mods folder in the Thunderstore mod - // Use regex to match `AUTHOR-MOD-VERSION` pattern - if (!std::regex_match(dir.path().string(), pattern)) - { - spdlog::warn("The following directory did not match 'AUTHOR-MOD-VERSION': {}", dir.path().string()); - continue; // skip loading mod that doesn't match - } - if (fs::exists(modsDir) && fs::is_directory(modsDir)) - { - for (fs::directory_entry subDir : fs::directory_iterator(modsDir)) - { - if (fs::exists(subDir.path() / "mod.json")) - { - modDirs.push_back(subDir.path()); - } - } - } - } - } - - for (fs::path modDir : modDirs) - { - // read mod json file - std::ifstream jsonStream(modDir / "mod.json"); - std::stringstream jsonStringStream; - - // fail if no mod json - if (jsonStream.fail()) - { - spdlog::warn( - "Mod file at '{}' does not exist or could not be read, is it installed correctly?", (modDir / "mod.json").string()); - continue; - } - - while (jsonStream.peek() != EOF) - jsonStringStream << (char)jsonStream.get(); - - jsonStream.close(); - - Mod mod(modDir, (char*)jsonStringStream.str().c_str()); - - for (auto& pair : mod.DependencyConstants) - { - if (m_DependencyConstants.find(pair.first) != m_DependencyConstants.end() && m_DependencyConstants[pair.first] != pair.second) - { - spdlog::error( - "'{}' attempted to register a dependency constant '{}' for '{}' that already exists for '{}'. " - "Change the constant name.", - mod.Name, - pair.first, - pair.second, - m_DependencyConstants[pair.first]); - mod.m_bWasReadSuccessfully = false; - break; - } - if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) - m_DependencyConstants.emplace(pair); - } - - for (std::string& dependency : mod.PluginDependencyConstants) - { - m_PluginDependencyConstants.insert(dependency); - } - - if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str())) - mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue(); - else - mod.m_bEnabled = true; - - if (mod.m_bWasReadSuccessfully) - { - if (mod.m_bEnabled) - spdlog::info("'{}' loaded successfully, version {}", mod.Name, mod.Version); - else - spdlog::info("'{}' loaded successfully, version {} (DISABLED)", mod.Name, mod.Version); - - m_LoadedMods.push_back(mod); - } - else - spdlog::warn("Mod file at '{}' failed to load", (modDir / "mod.json").string()); - } - - // sort by load prio, lowest-highest - std::sort(m_LoadedMods.begin(), m_LoadedMods.end(), [](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; }); + // Load mod info from filesystem into `m_LoadedMods` + SearchFilesystemForMods(); // This is used to check if some mods have a folder but no entry in enabledmods.json bool newModsDetected = false; @@ -638,6 +521,130 @@ void ModManager::UnloadMods() m_LoadedMods.clear(); } +void ModManager::SearchFilesystemForMods() +{ + std::vector<fs::path> modDirs; + m_LoadedMods.clear(); + + // get mod directories + std::filesystem::directory_iterator coreModsDir = fs::directory_iterator(GetCoreModFolderPath()); + std::filesystem::directory_iterator manualModsDir = fs::directory_iterator(GetManualModFolderPath()); + std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath()); + std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreLegacyModFolderPath()); + + // Set up regex for `Northstar.*` pattern + std::regex northstar_pattern(R"(.*\\Northstar\..+)"); + for (fs::directory_entry dir : coreModsDir) + { + if (!std::regex_match(dir.path().string(), northstar_pattern)) + { + spdlog::warn( + "The following directory did not match 'Northstar.*' and is most likely an incorrectly manually installed mod: {}", + dir.path().string()); + continue; // skip loading mod that doesn't match + } + if (fs::exists(dir.path() / "mod.json")) + modDirs.push_back(dir.path()); + } + + for (fs::directory_entry dir : manualModsDir) + if (fs::exists(dir.path() / "mod.json")) + modDirs.push_back(dir.path()); + + // Special case for Thunderstore and remote mods directories + // Set up regex for `AUTHOR-MOD-VERSION` pattern + std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))"); + + for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir}) + { + for (fs::directory_entry dir : dirIterator) + { + fs::path modsDir = dir.path() / "mods"; // Check for mods folder in the Thunderstore mod + // Use regex to match `AUTHOR-MOD-VERSION` pattern + if (!std::regex_match(dir.path().string(), pattern)) + { + spdlog::warn("The following directory did not match 'AUTHOR-MOD-VERSION': {}", dir.path().string()); + continue; // skip loading mod that doesn't match + } + if (fs::exists(modsDir) && fs::is_directory(modsDir)) + { + for (fs::directory_entry subDir : fs::directory_iterator(modsDir)) + { + if (fs::exists(subDir.path() / "mod.json")) + { + modDirs.push_back(subDir.path()); + } + } + } + } + } + + for (fs::path modDir : modDirs) + { + // read mod json file + std::ifstream jsonStream(modDir / "mod.json"); + std::stringstream jsonStringStream; + + // fail if no mod json + if (jsonStream.fail()) + { + spdlog::warn( + "Mod file at '{}' does not exist or could not be read, is it installed correctly?", (modDir / "mod.json").string()); + continue; + } + + while (jsonStream.peek() != EOF) + jsonStringStream << (char)jsonStream.get(); + + jsonStream.close(); + + Mod mod(modDir, (char*)jsonStringStream.str().c_str()); + + for (auto& pair : mod.DependencyConstants) + { + if (m_DependencyConstants.find(pair.first) != m_DependencyConstants.end() && m_DependencyConstants[pair.first] != pair.second) + { + spdlog::error( + "'{}' attempted to register a dependency constant '{}' for '{}' that already exists for '{}'. " + "Change the constant name.", + mod.Name, + pair.first, + pair.second, + m_DependencyConstants[pair.first]); + mod.m_bWasReadSuccessfully = false; + break; + } + if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) + m_DependencyConstants.emplace(pair); + } + + for (std::string& dependency : mod.PluginDependencyConstants) + { + m_PluginDependencyConstants.insert(dependency); + } + + if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str())) + mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue(); + else + mod.m_bEnabled = true; + + if (mod.m_bWasReadSuccessfully) + { + if (mod.m_bEnabled) + spdlog::info("'{}' loaded successfully, version {}", mod.Name, mod.Version); + else + spdlog::info("'{}' loaded successfully, version {} (DISABLED)", mod.Name, mod.Version); + + m_LoadedMods.push_back(mod); + } + else + spdlog::warn("Mod file at '{}' failed to load", (modDir / "mod.json").string()); + } + + // sort by load prio, lowest-highest + std::sort(m_LoadedMods.begin(), m_LoadedMods.end(), [](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; }); +} + std::string ModManager::NormaliseModFilePath(const fs::path path) { std::string str = path.lexically_normal().string(); diff --git a/primedev/mods/modmanager.h b/primedev/mods/modmanager.h index 2beb4a06..ee6a78fe 100644 --- a/primedev/mods/modmanager.h +++ b/primedev/mods/modmanager.h @@ -47,6 +47,18 @@ public: std::unordered_map<std::string, std::string> m_DependencyConstants; std::unordered_set<std::string> m_PluginDependencyConstants; +private: + /** + * Load information for all mods from filesystem. + * + * This looks for mods in several directories (expecting them to be formatted in + * some way); it then uses respective `mod.json` manifest files to create `Mod` + * instances, which are then stored in the `m_LoadedMods` variable. + * + * @returns nothing + **/ + void SearchFilesystemForMods(); + public: ModManager(); void LoadMods(); diff --git a/primedev/plugins/pluginmanager.cpp b/primedev/plugins/pluginmanager.cpp index 0b89f76e..aee3850e 100644 --- a/primedev/plugins/pluginmanager.cpp +++ b/primedev/plugins/pluginmanager.cpp @@ -1,6 +1,7 @@ #include "pluginmanager.h" #include <regex> +#include <ranges> #include "plugins.h" #include "config/profile.h" #include "core/convar/concommand.h" @@ -111,13 +112,14 @@ bool PluginManager::LoadPlugins(bool reloaded) void PluginManager::ReloadPlugins() { - for (const Plugin& plugin : this->GetLoadedPlugins()) + NS::log::PLUGINSYS->info("Reloading plugins"); + + for (const Plugin& plugin : this->plugins | std::views::reverse) { - plugin.Unload(); + std::string name = plugin.GetName(); + if (plugin.Reload()) + NS::log::PLUGINSYS->info("Reloaded {}", name); } - - this->plugins.clear(); - this->LoadPlugins(true); } void PluginManager::RemovePlugin(HMODULE handle) diff --git a/primedev/plugins/plugins.cpp b/primedev/plugins/plugins.cpp index 3e623167..8542f7ed 100644 --- a/primedev/plugins/plugins.cpp +++ b/primedev/plugins/plugins.cpp @@ -138,9 +138,9 @@ bool Plugin::Unload() const if (IsValid()) { - bool unloaded = m_callbacks->Unload(); + bool shouldUnload = m_callbacks->Unload(); - if (!unloaded) + if (!shouldUnload) return false; } @@ -154,14 +154,18 @@ bool Plugin::Unload() const return true; } -void Plugin::Reload() const +bool Plugin::Reload() const { + std::string location = m_location; + bool unloaded = Unload(); if (!unloaded) - return; + return false; - g_pPluginManager->LoadPlugin(fs::path(m_location), true); + g_pPluginManager->LoadPlugin(fs::path(location), true); + + return true; } void Plugin::Log(spdlog::level::level_enum level, char* msg) const diff --git a/primedev/plugins/plugins.h b/primedev/plugins/plugins.h index 95ec08b5..057c7438 100644 --- a/primedev/plugins/plugins.h +++ b/primedev/plugins/plugins.h @@ -28,7 +28,7 @@ public: Plugin(std::string path); bool Unload() const; - void Reload() const; + bool Reload() const; // sys void Log(spdlog::level::level_enum level, char* msg) const; diff --git a/primedev/scripts/client/scriptbrowserhooks.cpp b/primedev/scripts/client/scriptbrowserhooks.cpp index dcf051d2..3c4af1b5 100644 --- a/primedev/scripts/client/scriptbrowserhooks.cpp +++ b/primedev/scripts/client/scriptbrowserhooks.cpp @@ -1,25 +1,22 @@ -AUTOHOOK_INIT() - bool* bIsOriginOverlayEnabled; -// clang-format off -AUTOHOOK(OpenExternalWebBrowser, engine.dll + 0x184E40, -void, __fastcall, (char* pUrl, char flags)) -// clang-format on +static void(__fastcall* o_pOpenExternalWebBrowser)(char* pUrl, char flags) = nullptr; +static void __fastcall h_OpenExternalWebBrowser(char* pUrl, char flags) { bool bIsOriginOverlayEnabledOriginal = *bIsOriginOverlayEnabled; bool isHttp = !strncmp(pUrl, "http://", 7) || !strncmp(pUrl, "https://", 8); if (flags & 2 && isHttp) // custom force external browser flag *bIsOriginOverlayEnabled = false; // if this bool is false, game will use an external browser rather than the origin overlay one - OpenExternalWebBrowser(pUrl, flags); + o_pOpenExternalWebBrowser(pUrl, flags); *bIsOriginOverlayEnabled = bIsOriginOverlayEnabledOriginal; } ON_DLL_LOAD_CLIENT("engine.dll", ScriptExternalBrowserHooks, (CModule module)) { - AUTOHOOK_DISPATCH() + o_pOpenExternalWebBrowser = module.Offset(0x184E40).RCast<decltype(o_pOpenExternalWebBrowser)>(); + HookAttach(&(PVOID&)o_pOpenExternalWebBrowser, (PVOID)h_OpenExternalWebBrowser); bIsOriginOverlayEnabled = module.Offset(0x13978255).RCast<bool*>(); } diff --git a/primedev/shared/misccommands.cpp b/primedev/shared/misccommands.cpp index 29d92d2d..af936941 100644 --- a/primedev/shared/misccommands.cpp +++ b/primedev/shared/misccommands.cpp @@ -301,6 +301,7 @@ void FixupCvarFlags() // these 2 could be FCVAR_CHEAT, i guess? {"cl_draw_player_model", FCVAR_DEVELOPMENTONLY}, {"cl_always_draw_3p_player", FCVAR_DEVELOPMENTONLY}, + {"cl_gib_lifetime", FCVAR_CHEAT}, {"idcolor_neutral", FCVAR_DEVELOPMENTONLY}, {"idcolor_ally", FCVAR_DEVELOPMENTONLY}, {"idcolor_ally_cb1", FCVAR_DEVELOPMENTONLY}, |
