From b890edfd5652c331b38b858ae2f0c845a28ba91d Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 17 May 2025 07:00:05 +0100 Subject: [PATCH] Support mods with UNPACKED_MPQS --- Source/CMakeLists.txt | 1 + Source/engine/assets.cpp | 173 +++++++++++++++++++-------------------- Source/engine/assets.hpp | 22 ++--- Source/init.cpp | 18 ++-- Source/init.h | 26 ++---- Source/menu.cpp | 1 + Source/options.cpp | 5 +- test/player_test.cpp | 1 + test/writehero_test.cpp | 1 + 9 files changed, 109 insertions(+), 139 deletions(-) diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 5392f9ed6..0acdb6931 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -316,6 +316,7 @@ add_devilutionx_object_library(libdevilutionx_init target_link_dependencies(libdevilutionx_init PUBLIC libdevilutionx_assets libdevilutionx_config + libdevilutionx_mpq libdevilutionx_options ) diff --git a/Source/engine/assets.cpp b/Source/engine/assets.cpp index c803fe631..4ec55eab3 100644 --- a/Source/engine/assets.cpp +++ b/Source/engine/assets.cpp @@ -24,37 +24,23 @@ namespace devilution { std::vector OverridePaths; - -#ifdef UNPACKED_MPQS -std::optional spawn_data_path; -std::optional diabdat_data_path; -std::optional hellfire_data_path; -std::optional font_data_path; -std::optional lang_data_path; -#else -std::map> MpqArchives; +std::map MpqArchives; bool HasHellfireMpq; -#endif namespace { #ifdef UNPACKED_MPQS char *FindUnpackedMpqFile(char *relativePath) { + // Iterate over archives in reverse order to prefer files from high priority archives char *path = nullptr; - const auto at = [&](const std::optional &unpackedDir) -> bool { - if (!unpackedDir) - return false; - path = relativePath - unpackedDir->size(); - std::memcpy(path, unpackedDir->data(), unpackedDir->size()); - if (FileExists(path)) - return true; + for (auto rit = MpqArchives.rbegin(); rit != MpqArchives.rend(); ++rit) { + const std::string_view unpackedDir = rit->second; + path = relativePath - unpackedDir.size(); + std::memcpy(path, unpackedDir.data(), unpackedDir.size()); + if (FileExists(path)) break; path = nullptr; - return false; - }; - at(font_data_path) || at(lang_data_path) - || (gbIsHellfire && at(hellfire_data_path)) - || at(spawn_data_path) || at(diabdat_data_path); + } return path; } #else @@ -78,8 +64,8 @@ bool FindMpqFile(std::string_view filename, MpqArchive **archive, uint32_t *file // Iterate over archives in reverse order to prefer files from high priority archives for (auto rit = MpqArchives.rbegin(); rit != MpqArchives.rend(); ++rit) { - if (rit->second->GetFileNumber(fileHash, *fileNumber)) { - *archive = rit->second.get(); + if (rit->second.GetFileNumber(fileHash, *fileNumber)) { + *archive = &rit->second; return true; } } @@ -262,7 +248,7 @@ std::string FailedToOpenFileErrorMessage(std::string_view path, std::string_view namespace { #ifdef UNPACKED_MPQS -std::optional FindUnpackedMpqData(const std::vector &paths, std::string_view mpqName) +std::optional FindUnpackedMpqData(std::span paths, std::string_view mpqName) { std::string targetPath; for (const std::string &path : paths) { @@ -276,12 +262,28 @@ std::optional FindUnpackedMpqData(const std::vector &p } return std::nullopt; } + +bool FindMPQ(std::span paths, std::string_view mpqName) +{ + return FindUnpackedMpqData(paths, mpqName).has_value(); +} + +bool LoadMPQ(std::span paths, std::string_view mpqName, int priority) +{ + std::optional mpqPath = FindUnpackedMpqData(paths, mpqName); + if (!mpqPath.has_value()) { + LogVerbose("Missing: {}", mpqName); + return false; + } + MpqArchives[priority] = *std::move(mpqPath); + return true; +} #else -bool FindMPQ(const std::vector &paths, std::string_view mpqName) +bool FindMPQ(std::span paths, std::string_view mpqName) { std::string mpqAbsPath; for (const auto &path : paths) { - mpqAbsPath = path + mpqName.data(); + mpqAbsPath = StrCat(path, mpqName, ".mpq"); if (FileExists(mpqAbsPath)) { LogVerbose(" Found: {} in {}", mpqName, path); return true; @@ -290,16 +292,21 @@ bool FindMPQ(const std::vector &paths, std::string_view mpqName) return false; } -bool LoadMPQ(const std::vector &paths, std::string_view mpqName, int priority) + +bool LoadMPQ(std::span paths, std::string_view mpqName, int priority, std::string_view ext = ".mpq") { std::optional archive; std::string mpqAbsPath; std::int32_t error = 0; for (const auto &path : paths) { - mpqAbsPath = path + mpqName.data(); - if ((archive = MpqArchive::Open(mpqAbsPath.c_str(), error))) { + mpqAbsPath = StrCat(path, mpqName, ext); + archive = MpqArchive::Open(mpqAbsPath.c_str(), error); + if (archive.has_value()) { LogVerbose(" Found: {} in {}", mpqName, path); - MpqArchives[priority] = std::make_unique(std::move(*archive)); + auto [it, inserted] = MpqArchives.emplace(priority, *std::move(archive)); + if (!inserted) { + LogError("MPQ with priority {} is already registered, skipping {}", priority, mpqName); + } return true; } if (error != 0) { @@ -376,92 +383,78 @@ void LoadCoreArchives() { auto paths = GetMPQSearchPaths(); -#ifdef UNPACKED_MPQS - font_data_path = FindUnpackedMpqData(paths, "fonts"); -#else // !UNPACKED_MPQS #if !defined(__ANDROID__) && !defined(__APPLE__) && !defined(__3DS__) && !defined(__SWITCH__) // Load devilutionx.mpq first to get the font file for error messages - LoadMPQ(paths, "devilutionx.mpq", DevilutionXMpqPriority); -#endif - LoadMPQ(paths, "fonts.mpq", FontMpqPriority); // Extra fonts - HasHellfireMpq = FindMPQ(paths, "hellfire.mpq"); + LoadMPQ(paths, "devilutionx", DevilutionXMpqPriority); #endif + LoadMPQ(paths, "fonts", FontMpqPriority); // Extra fonts + HasHellfireMpq = FindMPQ(paths, "hellfire"); } void LoadLanguageArchive() { -#ifdef UNPACKED_MPQS - lang_data_path = std::nullopt; -#endif - - std::string_view code = GetLanguageCode(); + const std::string_view code = GetLanguageCode(); if (code != "en") { - std::string langMpqName { code }; -#ifdef UNPACKED_MPQS - lang_data_path = FindUnpackedMpqData(GetMPQSearchPaths(), langMpqName); -#else - langMpqName.append(".mpq"); - LoadMPQ(GetMPQSearchPaths(), langMpqName, LangMpqPriority); -#endif + LoadMPQ(GetMPQSearchPaths(), code, LangMpqPriority); } } void LoadGameArchives() { - auto paths = GetMPQSearchPaths(); -#ifdef UNPACKED_MPQS - diabdat_data_path = FindUnpackedMpqData(paths, "diabdat"); - if (!diabdat_data_path) { - spawn_data_path = FindUnpackedMpqData(paths, "spawn"); - if (spawn_data_path) - gbIsSpawn = true; - } - if (!HeadlessMode) { - AssetRef ref = FindAsset("ui_art\\title.clx"); - if (!ref.ok()) { - LogError("{}", SDL_GetError()); - InsertCDDlg(_("diabdat.mpq or spawn.mpq")); + const std::vector paths = GetMPQSearchPaths(); + bool haveDiabdat = false; + bool haveSpawn = false; + +#ifndef UNPACKED_MPQS + // DIABDAT.MPQ is uppercase on the original CD and the GOG version. + haveDiabdat = LoadMPQ(paths, "DIABDAT", MainMpqPriority, ".MPQ"); +#endif + + if (!haveDiabdat) { + haveDiabdat = LoadMPQ(paths, "diabdat", MainMpqPriority); + if (!haveDiabdat) { + gbIsSpawn = haveSpawn = LoadMPQ(paths, "spawn", MainMpqPriority); } } - hellfire_data_path = FindUnpackedMpqData(paths, "hellfire"); - if (forceHellfire && !hellfire_data_path) - InsertCDDlg("hellfire"); -#else // !UNPACKED_MPQS - if (!LoadMPQ(paths, "DIABDAT.MPQ", MainMpqPriority)) { - // DIABDAT.MPQ is uppercase on the original CD and the GOG version. - if (!LoadMPQ(paths, "diabdat.mpq", MainMpqPriority)) - gbIsSpawn = LoadMPQ(paths, "spawn.mpq", MainMpqPriority); - } if (!HeadlessMode) { - AssetRef ref = FindAsset("ui_art\\title.pcx"); - if (!ref.ok()) { + if (!haveDiabdat && !haveSpawn) { LogError("{}", SDL_GetError()); InsertCDDlg(_("diabdat.mpq or spawn.mpq")); } } - if (forceHellfire && !HasHellfireMpq) + if (forceHellfire && !HasHellfireMpq) { +#ifdef UNPACKED_MPQS + InsertCDDlg("hellfire"); +#else InsertCDDlg("hellfire.mpq"); - LoadMPQ(paths, "hfbard.mpq", 8110); - LoadMPQ(paths, "hfbarb.mpq", 8120); +#endif + } + +#ifndef UNPACKED_MPQS + // In unpacked mode, all the hellfire data is in the hellfire directory. + LoadMPQ(paths, "hfbard", 8110); + LoadMPQ(paths, "hfbarb", 8120); #endif } void LoadHellfireArchives() { - auto paths = GetMPQSearchPaths(); + const std::vector paths = GetMPQSearchPaths(); + LoadMPQ(paths, "hellfire", 8000); + #ifdef UNPACKED_MPQS - const bool hasMonk = FileExists(*hellfire_data_path + "plrgfx/monk/mha/mhaas.clx"); - const bool hasMusic = FileExists(*hellfire_data_path + "music/dlvlf.wav") - || FileExists(*hellfire_data_path + "music/dlvlf.mp3"); - const bool hasVoice = FileExists(*hellfire_data_path + "sfx/hellfire/cowsut1.wav") - || FileExists(*hellfire_data_path + "sfx/hellfire/cowsut1.mp3"); -#else // !UNPACKED_MPQS - LoadMPQ(paths, "hellfire.mpq", 8000); - const bool hasMonk = LoadMPQ(paths, "hfmonk.mpq", 8100); - const bool hasMusic = LoadMPQ(paths, "hfmusic.mpq", 8200); - const bool hasVoice = LoadMPQ(paths, "hfvoice.mpq", 8500); + const std::string &hellfireDataPath = MpqArchives.at(8000); + const bool hasMonk = FileExists(hellfireDataPath + "plrgfx/monk/mha/mhaas.clx"); + const bool hasMusic = FileExists(hellfireDataPath + "music/dlvlf.wav") + || FileExists(hellfireDataPath + "music/dlvlf.mp3"); + const bool hasVoice = FileExists(hellfireDataPath + "sfx/hellfire/cowsut1.wav") + || FileExists(hellfireDataPath + "sfx/hellfire/cowsut1.mp3"); +#else + const bool hasMonk = LoadMPQ(paths, "hfmonk", 8100); + const bool hasMusic = LoadMPQ(paths, "hfmusic", 8200); + const bool hasVoice = LoadMPQ(paths, "hfvoice", 8500); #endif if (!hasMonk || !hasMusic || !hasVoice) @@ -498,14 +491,12 @@ void LoadModArchives(std::span modnames) } OverridePaths.emplace_back(paths::PrefPath()); -#ifndef UNPACKED_MPQS int priority = 10000; auto paths = GetMPQSearchPaths(); for (std::string_view modname : modnames) { - LoadMPQ(paths, StrCat("mods" DIRECTORY_SEPARATOR_STR, modname, ".mpq"), priority); + LoadMPQ(paths, StrCat("mods" DIRECTORY_SEPARATOR_STR, modname), priority); priority++; } -#endif } } // namespace devilution diff --git a/Source/engine/assets.hpp b/Source/engine/assets.hpp index f466ce770..287d058cd 100644 --- a/Source/engine/assets.hpp +++ b/Source/engine/assets.hpp @@ -280,19 +280,17 @@ struct AssetData { tl::expected LoadAsset(std::string_view path); #ifdef UNPACKED_MPQS -extern DVL_API_FOR_TEST std::optional spawn_data_path; -extern DVL_API_FOR_TEST std::optional diabdat_data_path; -extern std::optional hellfire_data_path; -extern std::optional font_data_path; -extern std::optional lang_data_path; +using MpqArchiveT = std::string; #else -extern DVL_API_FOR_TEST std::map> MpqArchives; +using MpqArchiveT = MpqArchive; +#endif + +extern DVL_API_FOR_TEST std::map MpqArchives; constexpr int MainMpqPriority = 1000; constexpr int DevilutionXMpqPriority = 9000; constexpr int LangMpqPriority = 9100; constexpr int FontMpqPriority = 9200; extern bool HasHellfireMpq; -#endif void LoadCoreArchives(); void LoadLanguageArchive(); @@ -301,19 +299,11 @@ void LoadHellfireArchives(); void UnloadModArchives(); void LoadModArchives(std::span modnames); -#ifdef UNPACKED_MPQS -#ifdef BUILD_TESTING -[[nodiscard]] inline bool HaveMainData() { return diabdat_data_path.has_value() || spawn_data_path.has_value(); } -#endif -[[nodiscard]] inline bool HaveHellfire() { return hellfire_data_path.has_value(); } -[[nodiscard]] inline bool HaveExtraFonts() { return font_data_path.has_value(); } -#else #ifdef BUILD_TESTING [[nodiscard]] inline bool HaveMainData() { return MpqArchives.find(MainMpqPriority) != MpqArchives.end(); } #endif -[[nodiscard]] inline bool HaveHellfire() { return HasHellfireMpq; } [[nodiscard]] inline bool HaveExtraFonts() { return MpqArchives.find(FontMpqPriority) != MpqArchives.end(); } -#endif +[[nodiscard]] inline bool HaveHellfire() { return HasHellfireMpq; } [[nodiscard]] inline bool HaveIntro() { return FindAsset("gendata\\diablo1.smk").ok(); } [[nodiscard]] inline bool HaveFullMusic() { return FindAsset("music\\dintro.wav").ok() || FindAsset("music\\dintro.mp3").ok(); } [[nodiscard]] inline bool HaveBardAssets() { return FindAsset("plrgfx\\bard\\bha\\bhaas.clx").ok(); } diff --git a/Source/init.cpp b/Source/init.cpp index 2df9a883d..1c5716e75 100644 --- a/Source/init.cpp +++ b/Source/init.cpp @@ -80,9 +80,9 @@ bool IsDevilutionXMpqOutOfDate() } #ifdef UNPACKED_MPQS -bool AreExtraFontsOutOfDate(const std::string &path) +bool AreExtraFontsOutOfDate(std::string_view path) { - const std::string versionPath = path + "fonts" DIRECTORY_SEPARATOR_STR "VERSION"; + const std::string versionPath = StrCat(path, "fonts" DIRECTORY_SEPARATOR_STR "VERSION"); if (versionPath.size() + 1 > AssetRef::PathBufSize) app_fatal("Path too long"); AssetRef ref; @@ -105,6 +105,12 @@ bool AreExtraFontsOutOfDate(MpqArchive &archive) } #endif +bool AreExtraFontsOutOfDate() +{ + const auto it = MpqArchives.find(FontMpqPriority); + return it != MpqArchives.end() && AreExtraFontsOutOfDate(it->second); +} + void init_cleanup() { if (gbIsMultiplayer && gbRunGame) { @@ -112,16 +118,8 @@ void init_cleanup() sfile_write_stash(); } -#ifdef UNPACKED_MPQS - lang_data_path = std::nullopt; - font_data_path = std::nullopt; - hellfire_data_path = std::nullopt; - diabdat_data_path = std::nullopt; - spawn_data_path = std::nullopt; -#else MpqArchives.clear(); HasHellfireMpq = false; -#endif NetClose(); } diff --git a/Source/init.h b/Source/init.h index aff0ac924..0eebd930a 100644 --- a/Source/init.h +++ b/Source/init.h @@ -5,13 +5,10 @@ */ #pragma once -#include - -#include "engine/assets.hpp" -#include "utils/attributes.h" +#include #ifdef UNPACKED_MPQS -#include +#include #else #include "mpq/mpq_reader.hpp" #endif @@ -21,27 +18,16 @@ namespace devilution { /** True if the game is the current active window */ extern bool gbActive; +[[nodiscard]] bool IsDevilutionXMpqOutOfDate(); + #ifdef UNPACKED_MPQS -bool AreExtraFontsOutOfDate(const std::string &path); +bool AreExtraFontsOutOfDate(std::string_view path); #else bool AreExtraFontsOutOfDate(MpqArchive &archive); #endif -inline bool AreExtraFontsOutOfDate() -{ -#ifdef UNPACKED_MPQS - return font_data_path && AreExtraFontsOutOfDate(*font_data_path); -#else - auto it = MpqArchives.find(FontMpqPriority); - if (it == MpqArchives.end()) { - return false; - } - - return AreExtraFontsOutOfDate(*it->second); -#endif -} +[[nodiscard]] bool AreExtraFontsOutOfDate(); -bool IsDevilutionXMpqOutOfDate(); void init_cleanup(); void init_create_window(); void MainWndProc(const SDL_Event &event); diff --git a/Source/menu.cpp b/Source/menu.cpp index 651ad92c9..7667b15f5 100644 --- a/Source/menu.cpp +++ b/Source/menu.cpp @@ -8,6 +8,7 @@ #include "DiabloUI/diabloui.h" #include "DiabloUI/settingsmenu.h" +#include "engine/assets.hpp" #include "engine/demomode.h" #include "game_mode.hpp" #include "init.h" diff --git a/Source/options.cpp b/Source/options.cpp index edbd11d69..597e401d2 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -949,6 +949,7 @@ void OptionEntryLanguageCode::CheckLanguagesAreInitialized() const { if (!languages.empty()) return; + const bool haveExtraFonts = HaveExtraFonts(); // Add well-known supported languages languages.emplace_back("bg", "Български"); @@ -964,7 +965,7 @@ void OptionEntryLanguageCode::CheckLanguagesAreInitialized() const languages.emplace_back("hu", "Magyar"); languages.emplace_back("it", "Italiano"); - if (HaveExtraFonts()) { + if (haveExtraFonts) { languages.emplace_back("ja", "日本語"); languages.emplace_back("ko", "한국어"); } @@ -977,7 +978,7 @@ void OptionEntryLanguageCode::CheckLanguagesAreInitialized() const languages.emplace_back("tr", "Türkçe"); languages.emplace_back("uk", "Українська"); - if (HaveExtraFonts()) { + if (haveExtraFonts) { languages.emplace_back("zh_CN", "汉语"); languages.emplace_back("zh_TW", "漢語"); } diff --git a/test/player_test.cpp b/test/player_test.cpp index bb8b64ba5..30c93472a 100644 --- a/test/player_test.cpp +++ b/test/player_test.cpp @@ -3,6 +3,7 @@ #include #include "cursor.h" +#include "engine/assets.hpp" #include "init.h" #include "playerdat.hpp" diff --git a/test/writehero_test.cpp b/test/writehero_test.cpp index 5c942d3de..f7ff40722 100644 --- a/test/writehero_test.cpp +++ b/test/writehero_test.cpp @@ -9,6 +9,7 @@ #include #include "cursor.h" +#include "engine/assets.hpp" #include "game_mode.hpp" #include "init.h" #include "loadsave.h"