#include "engine/assets.hpp" #include #include #include #include #include #include "appfat.h" #include "game_mode.hpp" #include "utils/file_util.h" #include "utils/log.hpp" #include "utils/paths.h" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" #if defined(_WIN32) && !defined(__UWP__) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) #include #endif #ifndef UNPACKED_MPQS #include "mpq/mpq_sdl_rwops.hpp" #endif namespace devilution { std::vector OverridePaths; std::map> MpqArchives; bool HasHellfireMpq; namespace { #ifdef UNPACKED_MPQS char *FindUnpackedMpqFile(char *relativePath) { char *path = nullptr; for (const auto &[_, unpackedDir] : MpqArchives) { path = relativePath - unpackedDir.size(); std::memcpy(path, unpackedDir.data(), unpackedDir.size()); if (FileExists(path)) break; path = nullptr; } return path; } #else bool IsDebugLogging() { return SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION) <= SDL_LOG_PRIORITY_DEBUG; } SDL_RWops *OpenOptionalRWops(const std::string &path) { // SDL always logs an error in Debug mode. // We check the file presence in Debug mode to avoid this. if (IsDebugLogging() && !FileExists(path.c_str())) return nullptr; return SDL_RWFromFile(path.c_str(), "rb"); }; bool FindMpqFile(std::string_view filename, MpqArchive **archive, uint32_t *fileNumber) { const MpqFileHash fileHash = CalculateMpqFileHash(filename); for (auto &[_, mpqArchive] : MpqArchives) { if (mpqArchive.GetFileNumber(fileHash, *fileNumber)) { *archive = &mpqArchive; return true; } } return false; } #endif } // namespace #ifdef UNPACKED_MPQS AssetRef FindAsset(std::string_view filename) { AssetRef result; if (filename.empty() || filename.back() == '\\') return result; result.path[0] = '\0'; char pathBuf[AssetRef::PathBufSize]; char *const pathEnd = pathBuf + AssetRef::PathBufSize; char *const relativePath = &pathBuf[AssetRef::PathBufSize - filename.size() - 1]; *BufCopy(relativePath, filename) = '\0'; #if !defined(_WIN32) && !defined(__DJGPP__) std::replace(relativePath, pathEnd, '\\', '/'); #endif // Absolute path: if (relativePath[0] == '/') { if (FileExists(relativePath)) { *BufCopy(result.path, std::string_view(relativePath, filename.size())) = '\0'; } return result; } // Unpacked MPQ file: char *const unpackedMpqPath = FindUnpackedMpqFile(relativePath); if (unpackedMpqPath != nullptr) { *BufCopy(result.path, std::string_view(unpackedMpqPath, pathEnd - unpackedMpqPath)) = '\0'; return result; } // The `/assets` directory next to the devilutionx binary. const std::string &assetsPathPrefix = paths::AssetsPath(); char *assetsPath = relativePath - assetsPathPrefix.size(); std::memcpy(assetsPath, assetsPathPrefix.data(), assetsPathPrefix.size()); if (FileExists(assetsPath)) { *BufCopy(result.path, std::string_view(assetsPath, pathEnd - assetsPath)) = '\0'; } return result; } #else AssetRef FindAsset(std::string_view filename) { AssetRef result; if (filename.empty() || filename.back() == '\\') return result; std::string relativePath { filename }; #ifndef _WIN32 std::replace(relativePath.begin(), relativePath.end(), '\\', '/'); #endif if (relativePath[0] == '/') { result.directHandle = SDL_RWFromFile(relativePath.c_str(), "rb"); if (result.directHandle != nullptr) { return result; } } // Files in the `PrefPath()` directory can override MPQ contents. { for (const auto &overridePath : OverridePaths) { const std::string path = overridePath + relativePath; result.directHandle = OpenOptionalRWops(path); if (result.directHandle != nullptr) { LogVerbose("Loaded MPQ file override: {}", path); return result; } } } // Look for the file in all the MPQ archives: if (FindMpqFile(filename, &result.archive, &result.fileNumber)) { result.filename = filename; return result; } // Load from the `/assets` directory next to the devilutionx binary. result.directHandle = OpenOptionalRWops(paths::AssetsPath() + relativePath); if (result.directHandle != nullptr) return result; #if defined(__ANDROID__) || defined(__APPLE__) // Fall back to the bundled assets on supported systems. // This is handled by SDL when we pass a relative path. if (!paths::AssetsPath().empty()) { result.directHandle = SDL_RWFromFile(relativePath.c_str(), "rb"); if (result.directHandle != nullptr) return result; } #endif return result; } #endif AssetHandle OpenAsset(AssetRef &&ref, bool threadsafe) { #if UNPACKED_MPQS return AssetHandle { OpenFile(ref.path, "rb") }; #else if (ref.archive != nullptr) return AssetHandle { SDL_RWops_FromMpqFile(*ref.archive, ref.fileNumber, ref.filename, threadsafe) }; if (ref.directHandle != nullptr) { // Transfer handle ownership: SDL_RWops *handle = ref.directHandle; ref.directHandle = nullptr; return AssetHandle { handle }; } return AssetHandle { nullptr }; #endif } AssetHandle OpenAsset(std::string_view filename, bool threadsafe) { AssetRef ref = FindAsset(filename); if (!ref.ok()) return AssetHandle {}; return OpenAsset(std::move(ref), threadsafe); } AssetHandle OpenAsset(std::string_view filename, size_t &fileSize, bool threadsafe) { AssetRef ref = FindAsset(filename); if (!ref.ok()) return AssetHandle {}; fileSize = ref.size(); return OpenAsset(std::move(ref), threadsafe); } SDL_RWops *OpenAssetAsSdlRwOps(std::string_view filename, bool threadsafe) { #ifdef UNPACKED_MPQS AssetRef ref = FindAsset(filename); if (!ref.ok()) return nullptr; return SDL_RWFromFile(ref.path, "rb"); #else return OpenAsset(filename, threadsafe).release(); #endif } tl::expected LoadAsset(std::string_view path) { AssetRef ref = FindAsset(path); if (!ref.ok()) { return tl::make_unexpected(StrCat("Asset not found: ", path)); } const size_t size = ref.size(); std::unique_ptr data { new char[size] }; AssetHandle handle = OpenAsset(std::move(ref)); if (!handle.ok()) { return tl::make_unexpected(StrCat("Failed to open asset: ", path, "\n", handle.error())); } if (size > 0 && !handle.read(data.get(), size)) { return tl::make_unexpected(StrCat("Read failed: ", path, "\n", handle.error())); } return AssetData { std::move(data), size }; } std::string FailedToOpenFileErrorMessage(std::string_view path, std::string_view error) { return fmt::format(fmt::runtime(_("Failed to open file:\n{:s}\n\n{:s}\n\nThe MPQ file(s) might be damaged. Please check the file integrity.")), path, error); } namespace { #ifdef UNPACKED_MPQS std::optional FindUnpackedMpqData(std::span paths, std::string_view mpqName) { std::string targetPath; for (const std::string &path : paths) { targetPath.clear(); targetPath.reserve(path.size() + mpqName.size() + 1); targetPath.append(path).append(mpqName) += DirectorySeparator; if (FileExists(targetPath)) { LogVerbose(" Found unpacked MPQ directory: {}", targetPath); return targetPath; } } 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(std::span paths, std::string_view mpqName) { std::string mpqAbsPath; for (const auto &path : paths) { mpqAbsPath = StrCat(path, mpqName, ".mpq"); if (FileExists(mpqAbsPath)) { LogVerbose(" Found: {} in {}", mpqName, path); return true; } } return false; } 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 = StrCat(path, mpqName, ext); archive = MpqArchive::Open(mpqAbsPath.c_str(), error); if (archive.has_value()) { LogVerbose(" Found: {} in {}", mpqName, path); 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) { LogError("Error {}: {}", MpqArchive::ErrorMessage(error), mpqAbsPath); } } if (error == 0) { LogVerbose("Missing: {}", mpqName); } return false; } #endif std::vector GetMPQSearchPaths() { std::vector paths; paths.push_back(paths::BasePath()); paths.push_back(paths::PrefPath()); if (paths[0] == paths[1]) paths.pop_back(); paths.push_back(paths::ConfigPath()); if (paths[0] == paths[1] || (paths.size() == 3 && (paths[0] == paths[2] || paths[1] == paths[2]))) paths.pop_back(); #if (defined(__unix__) || defined(__APPLE__)) && !defined(__ANDROID__) // `XDG_DATA_HOME` is usually the root path of `paths::PrefPath()`, so we only // add `XDG_DATA_DIRS`. const char *xdgDataDirs = std::getenv("XDG_DATA_DIRS"); if (xdgDataDirs != nullptr) { for (const std::string_view path : SplitByChar(xdgDataDirs, ':')) { std::string fullPath(path); if (!path.empty() && path.back() != '/') fullPath += '/'; fullPath.append("diasurgical/devilutionx/"); paths.push_back(std::move(fullPath)); } } else { paths.emplace_back("/usr/local/share/diasurgical/devilutionx/"); paths.emplace_back("/usr/share/diasurgical/devilutionx/"); } #elif defined(NXDK) paths.emplace_back("D:\\"); #elif defined(_WIN32) && !defined(__UWP__) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) char gogpath[_FSG_PATH_MAX]; fsg_get_gog_game_path(gogpath, "1412601690"); if (strlen(gogpath) > 0) { paths.emplace_back(std::string(gogpath) + "/"); paths.emplace_back(std::string(gogpath) + "/hellfire/"); } #endif if (paths.empty() || !paths.back().empty()) { paths.emplace_back(); // PWD } if (SDL_LOG_PRIORITY_VERBOSE >= SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION)) { LogVerbose("Paths:\n base: {}\n pref: {}\n config: {}\n assets: {}", paths::BasePath(), paths::PrefPath(), paths::ConfigPath(), paths::AssetsPath()); std::string message; for (std::size_t i = 0; i < paths.size(); ++i) { message.append(fmt::format("\n{:6d}. '{}'", i + 1, paths[i])); } LogVerbose("MPQ search paths:{}", message); } return paths; } } // namespace void LoadCoreArchives() { auto paths = GetMPQSearchPaths(); #if !defined(__ANDROID__) && !defined(__APPLE__) && !defined(__3DS__) && !defined(__SWITCH__) // Load devilutionx.mpq first to get the font file for error messages #ifdef __DJGPP__ LoadMPQ(paths, "devx", DevilutionXMpqPriority); #else LoadMPQ(paths, "devilutionx", DevilutionXMpqPriority); #endif #endif LoadMPQ(paths, "fonts", FontMpqPriority); // Extra fonts HasHellfireMpq = FindMPQ(paths, "hellfire"); } void LoadLanguageArchive() { MpqArchives.erase(LangMpqPriority); const std::string_view code = GetLanguageCode(); if (code != "en") { LoadMPQ(GetMPQSearchPaths(), code, LangMpqPriority); } } void LoadGameArchives() { 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); } } if (!HeadlessMode) { if (!haveDiabdat && !haveSpawn) { LogError("{}", SDL_GetError()); InsertCDDlg(_("diabdat.mpq or spawn.mpq")); } } if (forceHellfire && !HasHellfireMpq) { #ifdef UNPACKED_MPQS InsertCDDlg("hellfire"); #else InsertCDDlg("hellfire.mpq"); #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() { const std::vector paths = GetMPQSearchPaths(); LoadMPQ(paths, "hellfire", 8000); #ifdef UNPACKED_MPQS 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) DisplayFatalErrorAndExit(_("Some Hellfire MPQs are missing"), _("Not all Hellfire MPQs were found.\nPlease copy all the hf*.mpq files.")); } void UnloadModArchives() { OverridePaths.clear(); #ifndef UNPACKED_MPQS for (auto it = MpqArchives.begin(); it != MpqArchives.end();) { if ((it->first >= 8000 && it->first < 9000) || it->first >= 10000) { it = MpqArchives.erase(it); // erase returns the next valid iterator } else { ++it; } } #endif } void LoadModArchives(std::span modnames) { std::string targetPath; for (const std::string_view modname : modnames) { targetPath = StrCat(paths::PrefPath(), "mods" DIRECTORY_SEPARATOR_STR, modname, DIRECTORY_SEPARATOR_STR); if (FileExists(targetPath)) { OverridePaths.emplace_back(targetPath); } targetPath = StrCat(paths::BasePath(), "mods" DIRECTORY_SEPARATOR_STR, modname, DIRECTORY_SEPARATOR_STR); if (FileExists(targetPath)) { OverridePaths.emplace_back(targetPath); } } OverridePaths.emplace_back(paths::PrefPath()); int priority = 10000; auto paths = GetMPQSearchPaths(); for (const std::string_view modname : modnames) { LoadMPQ(paths, StrCat("mods" DIRECTORY_SEPARATOR_STR, modname), priority); priority++; } } } // namespace devilution