diff --git a/Source/options.cpp b/Source/options.cpp index e03351062..8c6f44198 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include @@ -58,6 +60,49 @@ namespace devilution { namespace { +void DiscoverMods() +{ + // Add mods available by default: + std::unordered_set modNames = { "clock" }; + + // Check if the mods directory exists. + const std::string modsPath = StrCat(paths::PrefPath(), "mods"); + if (DirectoryExists(modsPath.c_str())) { + // Find unpacked mods + for (const std::string &modFolder : ListDirectories(modsPath.c_str())) { + // Only consider this folder if the init.lua file exists. + std::string modScriptPath = modsPath + modFolder + DIRECTORY_SEPARATOR_STR + "init.lua"; + if (!FileExists(modScriptPath.c_str())) + continue; + + modNames.insert(modFolder); + } + + // Find packed mods + for (const std::string &modMpq : ListFiles(modsPath.c_str())) { + if (!modMpq.ends_with(".mpq")) + continue; + + modNames.insert(modMpq.substr(0, modMpq.size() - 4)); + } + } + + // Get the list of mods currently stored in the INI. + std::vector existingMods = GetOptions().Mods.GetModList(); + + // Add new mods. + for (const std::string &modName : modNames) { + if (std::find(existingMods.begin(), existingMods.end(), modName) == existingMods.end()) + GetOptions().Mods.AddModEntry(modName); + } + + // Remove mods that are no longer installed. + for (const std::string_view &modName : existingMods) { + if (modNames.find(std::string(modName)) == modNames.end()) + GetOptions().Mods.RemoveModEntry(std::string(modName)); + } +} + std::optional ini; #if defined(__ANDROID__) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) @@ -158,6 +203,7 @@ bool HardwareCursorSupported() void LoadOptions() { LoadIni(); + DiscoverMods(); Options &options = GetOptions(); for (OptionCategoryBase *pCategory : options.GetCategories()) { for (OptionEntryBase *pEntry : pCategory->GetEntries()) { @@ -1478,6 +1524,24 @@ std::vector ModOptions::GetEntries() return optionEntries; } +void ModOptions::AddModEntry(const std::string &modName) +{ + auto &entries = GetModEntries(); + entries.emplace_front(modName); +} + +void ModOptions::RemoveModEntry(const std::string &modName) +{ + if (!modEntries) { + return; + } + + auto &entries = *modEntries; + entries.remove_if([&](const ModEntry &entry) { + return entry.name == modName; + }); +} + std::forward_list &ModOptions::GetModEntries() { if (modEntries) @@ -1485,13 +1549,6 @@ std::forward_list &ModOptions::GetModEntries() std::vector modNames = ini->getKeys(key); - // Add mods available by default: - for (const std::string_view modName : { "clock" }) { - if (c_find(modNames, modName) != modNames.end()) continue; - ini->set(key, modName, false); - modNames.emplace_back(modName); - } - std::forward_list &newModEntries = modEntries.emplace(); for (auto &modName : modNames) { newModEntries.emplace_front(modName); diff --git a/Source/options.h b/Source/options.h index 0e70af79d..e022d97e4 100644 --- a/Source/options.h +++ b/Source/options.h @@ -830,6 +830,8 @@ struct ModOptions : OptionCategoryBase { std::vector GetActiveModList(); std::vector GetModList(); std::vector GetEntries() override; + void AddModEntry(const std::string &modName); + void RemoveModEntry(const std::string &modName); private: struct ModEntry { diff --git a/Source/utils/file_util.cpp b/Source/utils/file_util.cpp index e70918348..63884f071 100644 --- a/Source/utils/file_util.cpp +++ b/Source/utils/file_util.cpp @@ -5,9 +5,6 @@ #include #include #include -#include -#include -#include #include @@ -472,4 +469,70 @@ FILE *OpenFile(const char *path, const char *mode) #endif } +std::vector ListDirectories(const char *path) +{ + std::vector dirs; +#ifdef DVL_HAS_FILESYSTEM + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator(reinterpret_cast(path), ec)) { + if (!entry.is_directory()) + continue; + std::u8string filename = entry.path().filename().u8string(); + dirs.emplace_back(filename.begin(), filename.end()); + } +#elif defined(_WIN32) + WIN32_FIND_DATAA findData; + // Construct the search path by appending the directory separator and wildcard. + std::string searchPath = std::string(path) + DIRECTORY_SEPARATOR_STR + "*"; + HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData); + if (hFind == INVALID_HANDLE_VALUE) + return dirs; + do { + std::string folder = findData.cFileName; + // Skip the special entries "." and ".." + if (folder == "." || folder == "..") + continue; + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + dirs.push_back(folder); + } while (FindNextFileA(hFind, &findData)); + FindClose(hFind); +#else + static_assert(false, "ListDirectories not implemented for the current platform"); +#endif + return dirs; +} + +std::vector ListFiles(const char *path) +{ + std::vector files; +#ifdef DVL_HAS_FILESYSTEM + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator(reinterpret_cast(path), ec)) { + if (!entry.is_regular_file()) + continue; + std::u8string filename = entry.path().filename().u8string(); + files.emplace_back(filename.begin(), filename.end()); + } +#elif defined(_WIN32) + WIN32_FIND_DATAA findData; + // Construct the search path by appending the directory separator and wildcard. + std::string searchPath = std::string(path) + DIRECTORY_SEPARATOR_STR + "*"; + HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData); + if (hFind == INVALID_HANDLE_VALUE) + return files; + do { + std::string file = findData.cFileName; + // Skip the special entries "." and ".." + if (file == "." || file == "..") + continue; + if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + files.push_back(file); + } while (FindNextFileA(hFind, &findData)); + FindClose(hFind); +#else + static_assert(false, "ListFiles not implemented for the current platform"); +#endif + return files; +} + } // namespace devilution diff --git a/Source/utils/file_util.h b/Source/utils/file_util.h index ed3031c03..0e5d9e1f5 100644 --- a/Source/utils/file_util.h +++ b/Source/utils/file_util.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace devilution { @@ -46,4 +47,7 @@ FILE *OpenFile(const char *path, const char *mode); std::unique_ptr ToWideChar(std::string_view path); #endif +std::vector ListDirectories(const char *path); +std::vector ListFiles(const char *path); + } // namespace devilution