Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Mod versioning #828

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 200 additions & 111 deletions primedev/mods/modmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,8 @@ void Mod::ParseInitScript(rapidjson_document& json)

ModManager::ModManager()
{
cfgPath = GetNorthstarPrefix() + "/enabledmods.json";

// precaculated string hashes
// note: use backslashes for these, since we use lexically_normal for file paths which uses them
m_hScriptsRsonHash = STR_HASH("scripts\\vscripts\\scripts.rson");
Expand Down Expand Up @@ -621,136 +623,98 @@ void ModManager::LoadMods()

m_DependencyConstants.clear();

// File format checks
bool isUsingOldFormat = false;
rapidjson_document oldEnabledModsCfg;

// read enabled mods cfg
std::ifstream enabledModsStream(GetNorthstarPrefix() + "/enabledmods.json");
std::ifstream enabledModsStream(cfgPath);
std::stringstream enabledModsStringStream;

if (!enabledModsStream.fail())
// create configuration file if does not exist
if (enabledModsStream.fail())
{
m_EnabledModsCfg.SetObject();
}
else
{
while (enabledModsStream.peek() != EOF)
enabledModsStringStream << (char)enabledModsStream.get();

enabledModsStream.close();
m_EnabledModsCfg.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(
enabledModsStringStream.str().c_str());

m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject();
}

// get mod directories
std::filesystem::directory_iterator classicModsDir = fs::directory_iterator(GetModFolderPath());
std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath());
std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath());

for (fs::directory_entry dir : classicModsDir)
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+))");
// Check file format, and rename file if it is not using new format
bool isUsingUnknownFormat = !m_EnabledModsCfg.IsObject() || !m_EnabledModsCfg.HasMember("Northstar.Client");
isUsingOldFormat =
m_EnabledModsCfg.IsObject() && m_EnabledModsCfg.HasMember("Northstar.Client") && m_EnabledModsCfg["Northstar.Client"].IsBool();

for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir})
{
for (fs::directory_entry dir : dirIterator)
if (isUsingUnknownFormat || isUsingOldFormat)
{
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::info(
"==> {} manifesto format detected, renaming it to enabledmods.old.json.", isUsingUnknownFormat ? "Unknown" : "Old");
int ret = rename(cfgPath.c_str(), (GetNorthstarPrefix() + "/enabledmods.old.json").c_str());
if (ret)
{
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());
}
}
spdlog::error("Failed renaming manifesto (error code: {}).", ret);
return;
}
}
}

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)
// Copy old configuration to migrate manifesto to new format
if (isUsingOldFormat)
{
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;
oldEnabledModsCfg.CopyFrom(m_EnabledModsCfg, oldEnabledModsCfg.GetAllocator());
}
if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end())
m_DependencyConstants.emplace(pair);
}

for (std::string& dependency : mod.PluginDependencyConstants)
{
m_PluginDependencyConstants.insert(dependency);
// Reset current configuration
m_EnabledModsCfg.SetObject();
}

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

// This is used to check if some mods have a folder but no entry in enabledmods.json
bool newModsDetected = false;

for (Mod& mod : m_LoadedMods)
{
if (!mod.m_bEnabled)
continue;

// Add mod entry to enabledmods.json if it doesn't exist
if (!mod.m_bIsRemote && m_bHasEnabledModsCfg && !m_EnabledModsCfg.HasMember(mod.Name.c_str()))
bool isModRemote = mod.m_bIsRemote;
bool modEntryExists = m_EnabledModsCfg.HasMember(mod.Name.c_str());
bool modEntryHasCorrectFormat = modEntryExists && m_EnabledModsCfg[mod.Name.c_str()].IsObject();
bool modVersionEntryExists = modEntryExists && m_EnabledModsCfg[mod.Name.c_str()].HasMember(mod.Version.c_str());

if (!isModRemote && (!modEntryExists || !modVersionEntryExists))
{
m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), true, m_EnabledModsCfg.GetAllocator());
// Creating mod key (with name)
if (!modEntryHasCorrectFormat)
{
// Adjust wrong format (string instead of object)
if (modEntryExists)
{
m_EnabledModsCfg.RemoveMember(mod.Name.c_str());
}
m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), false, m_EnabledModsCfg.GetAllocator());
m_EnabledModsCfg[mod.Name.c_str()].SetObject();
}

// Creating version key
if (!modVersionEntryExists)
{
m_EnabledModsCfg[mod.Name.c_str()].AddMember(
rapidjson_document::StringRefType(mod.Version.c_str()), false, m_EnabledModsCfg.GetAllocator());
}

// Add mod entry
bool modIsEnabled = mod.m_bEnabled;
// Try to use old manifesto if currently migrating from old format
if (isUsingOldFormat && oldEnabledModsCfg.HasMember(mod.Name.c_str()) && oldEnabledModsCfg[mod.Name.c_str()].IsBool())
{
modIsEnabled = oldEnabledModsCfg[mod.Name.c_str()].GetBool();
mod.m_bEnabled = modIsEnabled;
}
m_EnabledModsCfg[mod.Name.c_str()][mod.Version.c_str()].SetBool(modIsEnabled);

newModsDetected = true;
}

Expand Down Expand Up @@ -1022,7 +986,7 @@ void ModManager::LoadMods()
// If there are new mods, we write entries accordingly in enabledmods.json
if (newModsDetected)
{
std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json");
std::ofstream writeStream(cfgPath);
rapidjson::OStreamWrapper writeStreamWrapper(writeStream);
rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(writeStreamWrapper);
m_EnabledModsCfg.Accept(writer);
Expand Down Expand Up @@ -1101,24 +1065,149 @@ void ModManager::UnloadMods()
fs::remove(GetCompiledAssetsPath() / fs::path(kvPaths.second).lexically_relative(mod.m_ModDirectory));

mod.KeyValues.clear();
}

// save mods configuration to disk
ExportModsConfigurationToFile();

// write to m_enabledModsCfg
// should we be doing this here or should scripts be doing this manually?
// main issue with doing this here is when we reload mods for connecting to a server, we write enabled mods, which isn't necessarily
// what we wanna do
// do we need to dealloc individual entries in m_loadedMods? idk, rework
m_LoadedMods.clear();
}

void ModManager::SearchFilesystemForMods()
{
std::vector<fs::path> modDirs;
m_LoadedMods.clear();

// get mod directories
std::filesystem::directory_iterator classicModsDir = fs::directory_iterator(GetModFolderPath());
std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath());
std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath());

for (fs::directory_entry dir : classicModsDir)
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_EnabledModsCfg.HasMember(mod.Name.c_str()) && m_EnabledModsCfg[mod.Name.c_str()].HasMember(mod.Version))
{
mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()][mod.Version.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; });
}

void ModManager::ExportModsConfigurationToFile()
{
m_EnabledModsCfg.SetObject();

for (Mod& mod : m_LoadedMods)
{
// Creating mod key (with name)
if (!m_EnabledModsCfg.HasMember(mod.Name.c_str()))
{
m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), false, m_EnabledModsCfg.GetAllocator());
m_EnabledModsCfg[mod.Name.c_str()].SetObject();
}

m_EnabledModsCfg[mod.Name.c_str()].SetBool(mod.m_bEnabled);
// Creating version key
if (!m_EnabledModsCfg[mod.Name.c_str()].HasMember(mod.Version.c_str()))
m_EnabledModsCfg[mod.Name.c_str()].AddMember(
rapidjson_document::StringRefType(mod.Version.c_str()), false, m_EnabledModsCfg.GetAllocator());
m_EnabledModsCfg[mod.Name.c_str()][mod.Version.c_str()].SetBool(mod.m_bEnabled);
}

std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json");
std::ofstream writeStream(cfgPath);
rapidjson::OStreamWrapper writeStreamWrapper(writeStream);
rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(writeStreamWrapper);
m_EnabledModsCfg.Accept(writer);

// do we need to dealloc individual entries in m_loadedMods? idk, rework
m_LoadedMods.clear();
}

std::string ModManager::NormaliseModFilePath(const fs::path path)
Expand Down
5 changes: 5 additions & 0 deletions primedev/mods/modmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ class ModManager
bool m_bHasLoadedMods = false;
bool m_bHasEnabledModsCfg;
rapidjson_document m_EnabledModsCfg;
std::string cfgPath;

// precalculated hashes
size_t m_hScriptsRsonHash;
Expand All @@ -184,6 +185,10 @@ class ModManager
std::unordered_map<std::string, std::string> m_DependencyConstants;
std::unordered_set<std::string> m_PluginDependencyConstants;

private:
void SearchFilesystemForMods();
void ExportModsConfigurationToFile();

public:
ModManager();
void LoadMods();
Expand Down
Loading