Merge branch 'main' into feat/overhaul-mod-loading-locations

This commit is contained in:
GeckoEidechse 2025-03-01 23:09:32 +01:00
commit c277e3caf6
10 changed files with 212 additions and 168 deletions

View file

@ -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

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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())
{
plugin.Unload();
}
NS::log::PLUGINSYS->info("Reloading plugins");
this->plugins.clear();
this->LoadPlugins(true);
for (const Plugin& plugin : this->plugins | std::views::reverse)
{
std::string name = plugin.GetName();
if (plugin.Reload())
NS::log::PLUGINSYS->info("Reloaded {}", name);
}
}
void PluginManager::RemovePlugin(HMODULE handle)

View file

@ -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

View file

@ -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;

View file

@ -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*>();
}

View file

@ -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},