feat: Add ModWorkshop support to MAD (#873)

* feat: add ModWorkshop support for MAD
This commit is contained in:
Rémy Raes 2025-11-15 21:58:36 +01:00 committed by GitHub
parent f5bd7c9b95
commit c0ed69f795
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 77 additions and 14 deletions

View file

@ -129,8 +129,7 @@ void ModDownloader::FetchModsListFromAPI()
std::string checksum = attribute["Checksum"].GetString(); std::string checksum = attribute["Checksum"].GetString();
std::string downloadLink = attribute["DownloadLink"].GetString(); std::string downloadLink = attribute["DownloadLink"].GetString();
std::string platformValue = attribute["Platform"].GetString(); std::string platformValue = attribute["Platform"].GetString();
VerifiedModPlatform platform = VerifiedModPlatform platform = resolvePlatform(platformValue);
platformValue.compare("thunderstore") == 0 ? VerifiedModPlatform::Thunderstore : VerifiedModPlatform::Unknown;
modVersions.insert({version, {.checksum = checksum, .downloadLink = downloadLink, .platform = platform}}); modVersions.insert({version, {.checksum = checksum, .downloadLink = downloadLink, .platform = platform}});
} }
@ -176,13 +175,28 @@ int ModDownloader::ModFetchingProgressCallback(
return 0; return 0;
} }
std::string ModDownloader::GetModArchiveName(std::string url)
{
std::string name = fs::path(url).filename().generic_string();
std::string::size_type charIndex = name.find("?");
// Thunderstore format
if (std::string::npos == charIndex)
{
return name;
}
// ModWorkshop format (removing the "?filename=" part)
return name.substr(charIndex + 10);
}
std::tuple<fs::path, bool> 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 url = version.downloadLink;
std::string archiveName = fs::path(url).filename().generic_string();
spdlog::info(std::format("Fetching mod archive from {}", url));
// Download destination // Download destination
spdlog::info(std::format("Fetching mod archive from {}", url));
std::string archiveName = ModDownloader::GetModArchiveName(url);
std::filesystem::path downloadPath = std::filesystem::temp_directory_path() / archiveName; std::filesystem::path downloadPath = std::filesystem::temp_directory_path() / archiveName;
spdlog::info(std::format("Downloading archive to {}", downloadPath.generic_string())); spdlog::info(std::format("Downloading archive to {}", downloadPath.generic_string()));
@ -436,10 +450,10 @@ void ModDownloader::ExtractMod(fs::path modPath, fs::path destinationPath, Verif
modState.total = GetModArchiveSize(file, gi); modState.total = GetModArchiveSize(file, gi);
modState.progress = 0; modState.progress = 0;
// Right now, we only know how to extract Thunderstore mods // We don't know how to extract mods from unknown platforms
if (platform != VerifiedModPlatform::Thunderstore) if (platform == VerifiedModPlatform::Unknown)
{ {
spdlog::error("Failed extracting mod from unknown platform (value: {}).", platform); spdlog::error("Failed extracting mod from unknown platform.");
modState.state = UNKNOWN_PLATFORM; modState.state = UNKNOWN_PLATFORM;
return; return;
} }
@ -658,10 +672,19 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion)
return; return;
} }
// Mod directory name (removing the ".zip" fom the archive name) // Mod directory name
name = archiveLocation.filename().string(); /// Don't use archive name as destination with ModWorkshop
name = name.substr(0, name.length() - 4); if (fullVersion.platform == VerifiedModPlatform::ModWorkshop)
modDirectory = GetRemoteModFolderPath() / name; {
modDirectory = GetRemoteModFolderPath();
}
else
/// Removes the ".zip" fom the archive name and use it as parent directory
{
name = archiveLocation.filename().string();
name = name.substr(0, name.length() - 4);
modDirectory = GetRemoteModFolderPath() / name;
}
// Extract downloaded mod archive // Extract downloaded mod archive
ExtractMod(archiveLocation, modDirectory, fullVersion.platform); ExtractMod(archiveLocation, modDirectory, fullVersion.platform);

View file

@ -11,8 +11,21 @@ private:
enum class VerifiedModPlatform enum class VerifiedModPlatform
{ {
Unknown, Unknown,
ModWorkshop,
Thunderstore Thunderstore
}; };
VerifiedModPlatform resolvePlatform(std::string input)
{
if (input.compare("thunderstore") == 0)
{
return VerifiedModPlatform::Thunderstore;
}
if (input.compare("modworkshop") == 0)
{
return VerifiedModPlatform::ModWorkshop;
}
return VerifiedModPlatform::Unknown;
}
struct VerifiedModVersion struct VerifiedModVersion
{ {
std::string checksum; std::string checksum;
@ -78,6 +91,22 @@ private:
*/ */
void ExtractMod(fs::path modPath, fs::path destinationPath, VerifiedModPlatform platform); void ExtractMod(fs::path modPath, fs::path destinationPath, VerifiedModPlatform platform);
/**
* Retrieves archive name from input URL.
*
* Thunderstore and ModWorkshop do not format their URLs the same way:
* - Thunderstore: https://gcdn.thunderstore.io/live/repository/packages/Nyami11-mp_brick-1.0.2.zip
* - ModWorkshop:
* https://storage.modworkshop.net/mods/files/46563_156759_CSio1Cd3QVXYRZLd1h6iNu0IPuuaY9ePKGceQD31.zip?filename=em4v.zip
*
* This takes those differences into account and returns an archive name for
* both types of URLs.
*
* @param url URL from which to extract archive file name
* @returns name of the archive to be downloaded
*/
std::string GetModArchiveName(std::string url);
public: public:
ModDownloader(); ModDownloader();

View file

@ -448,19 +448,30 @@ void ModManager::SearchFilesystemForMods()
std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath()); std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath());
std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath()); std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath());
for (fs::directory_entry dir : classicModsDir) for (fs::directory_iterator dirIterator : {classicModsDir, remoteModsDir})
if (fs::exists(dir.path() / "mod.json")) for (fs::directory_entry dir : dirIterator)
modDirs.push_back(dir.path()); if (fs::exists(dir.path() / "mod.json"))
modDirs.push_back(dir.path());
// Special case for Thunderstore and remote mods directories // Special case for Thunderstore and remote mods directories
// Set up regex for `AUTHOR-MOD-VERSION` pattern // Set up regex for `AUTHOR-MOD-VERSION` pattern
std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))"); std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))");
// Reset directory iterator
remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath());
for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir}) for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir})
{ {
for (fs::directory_entry dir : dirIterator) for (fs::directory_entry dir : dirIterator)
{ {
fs::path modsDir = dir.path() / "mods"; // Check for mods folder in the Thunderstore mod fs::path modsDir = dir.path() / "mods"; // Check for mods folder in the Thunderstore mod
// Do not register ModWorkshop mods twice
if (std::find(modDirs.begin(), modDirs.end(), dir.path()) != modDirs.end())
{
continue;
}
// Use regex to match `AUTHOR-MOD-VERSION` pattern // Use regex to match `AUTHOR-MOD-VERSION` pattern
if (!std::regex_match(dir.path().string(), pattern)) if (!std::regex_match(dir.path().string(), pattern))
{ {