/** * @file init.cpp * * Implementation of routines for initializing the environment, disable screen saver, load MPQ. */ #include "init.h" #include #include #include #include #include #if defined(_WIN32) && !defined(__UWP__) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) #include #endif #include "DiabloUI/diabloui.h" #include "engine/assets.hpp" #include "engine/backbuffer_state.hpp" #include "engine/dx.h" #include "engine/events.hpp" #include "hwcursor.hpp" #include "options.h" #include "pfile.h" #include "utils/file_util.h" #include "utils/language.h" #include "utils/log.hpp" #include "utils/paths.h" #include "utils/str_split.hpp" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" #ifndef UNPACKED_MPQS #include "mpq/mpq_common.hpp" #include "mpq/mpq_reader.hpp" #endif #ifdef __vita__ // increase default allowed heap size on Vita int _newlib_heap_size_user = 100 * 1024 * 1024; #endif namespace devilution { /** True if the game is the current active window */ bool gbActive; /** Indicate if we only have access to demo data */ bool gbIsSpawn; /** Indicate if we have loaded the Hellfire expansion data */ bool gbIsHellfire; /** Indicate if we want vanilla savefiles */ bool gbVanilla; /** Whether the Hellfire mode is required (forced). */ bool forceHellfire; #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::optional spawn_mpq; std::optional diabdat_mpq; std::optional hellfire_mpq; std::optional hfmonk_mpq; std::optional hfbard_mpq; std::optional hfbarb_mpq; std::optional hfmusic_mpq; std::optional hfvoice_mpq; std::optional devilutionx_mpq; std::optional lang_mpq; std::optional font_mpq; #endif namespace { constexpr char DevilutionXMpqVersion[] = "1\n"; constexpr char ExtraFontsVersion[] = "1\n"; #ifdef UNPACKED_MPQS std::optional FindUnpackedMpqData(const std::vector &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; } #else std::optional LoadMPQ(const std::vector &paths, std::string_view mpqName) { 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))) { LogVerbose(" Found: {} in {}", mpqName, path); return archive; } if (error != 0) { LogError("Error {}: {}", MpqArchive::ErrorMessage(error), mpqAbsPath); } } if (error == 0) { LogVerbose("Missing: {}", mpqName); } return std::nullopt; } #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; } bool AssetContentsEq(AssetRef &&ref, std::string_view expected) { const size_t size = ref.size(); AssetHandle handle = OpenAsset(std::move(ref), false); if (!handle.ok()) return false; std::unique_ptr contents { new char[size] }; if (!handle.read(contents.get(), size)) return false; return std::string_view { contents.get(), size } == expected; } bool CheckDevilutionXMpqVersion(AssetRef &&ref) { return !AssetContentsEq(std::move(ref), DevilutionXMpqVersion); } bool CheckExtraFontsVersion(AssetRef &&ref) { return !AssetContentsEq(std::move(ref), ExtraFontsVersion); } } // namespace #ifndef UNPACKED_MPQS bool IsDevilutionXMpqOutOfDate(MpqArchive &archive) { const char filename[] = "ASSETS_VERSION"; const MpqFileHash fileHash = CalculateMpqFileHash(filename); uint32_t fileNumber; if (!archive.GetFileNumber(fileHash, fileNumber)) return true; AssetRef ref; ref.archive = &archive; ref.fileNumber = fileNumber; ref.filename = filename; return CheckDevilutionXMpqVersion(std::move(ref)); } #endif #ifdef UNPACKED_MPQS bool AreExtraFontsOutOfDate(const std::string &path) { const std::string versionPath = path + "fonts" DIRECTORY_SEPARATOR_STR "VERSION"; if (versionPath.size() + 1 > AssetRef::PathBufSize) app_fatal("Path too long"); AssetRef ref; *BufCopy(ref.path, versionPath) = '\0'; return CheckExtraFontsVersion(std::move(ref)); } #else bool AreExtraFontsOutOfDate(MpqArchive &archive) { const char filename[] = "fonts\\VERSION"; const MpqFileHash fileHash = CalculateMpqFileHash(filename); uint32_t fileNumber; if (!archive.GetFileNumber(fileHash, fileNumber)) return true; AssetRef ref; ref.archive = &archive; ref.fileNumber = fileNumber; ref.filename = filename; return CheckExtraFontsVersion(std::move(ref)); } #endif void init_cleanup() { if (gbIsMultiplayer && gbRunGame) { pfile_write_hero(/*writeGameData=*/false); 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 spawn_mpq = std::nullopt; diabdat_mpq = std::nullopt; hellfire_mpq = std::nullopt; hfmonk_mpq = std::nullopt; hfbard_mpq = std::nullopt; hfbarb_mpq = std::nullopt; hfmusic_mpq = std::nullopt; hfvoice_mpq = std::nullopt; lang_mpq = std::nullopt; font_mpq = std::nullopt; devilutionx_mpq = std::nullopt; #endif NetClose(); } 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 devilutionx_mpq = LoadMPQ(paths, "devilutionx.mpq"); #endif font_mpq = LoadMPQ(paths, "fonts.mpq"); // Extra fonts #endif } void LoadLanguageArchive() { #ifdef UNPACKED_MPQS lang_data_path = std::nullopt; #else lang_mpq = std::nullopt; #endif 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"); lang_mpq = LoadMPQ(GetMPQSearchPaths(), langMpqName); #endif } } 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")); } } hellfire_data_path = FindUnpackedMpqData(paths, "hellfire"); if (hellfire_data_path) gbIsHellfire = true; if (forceHellfire && !hellfire_data_path) InsertCDDlg("hellfire"); 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"); // Bard and barbarian are not currently supported in unpacked mode // because they use the same paths as rogue and warrior. gbBard = false; gbBarbarian = false; if (gbIsHellfire && (!hasMonk || !hasMusic || !hasVoice)) { UiErrorOkDialog(_("Some Hellfire MPQs are missing"), _("Not all Hellfire MPQs were found.\nPlease copy all the hf*.mpq files.")); diablo_quit(1); } #else // !UNPACKED_MPQS diabdat_mpq = LoadMPQ(paths, "DIABDAT.MPQ"); if (!diabdat_mpq) { // DIABDAT.MPQ is uppercase on the original CD and the GOG version. diabdat_mpq = LoadMPQ(paths, "diabdat.mpq"); } if (!diabdat_mpq) { spawn_mpq = LoadMPQ(paths, "spawn.mpq"); if (spawn_mpq) gbIsSpawn = true; } if (!HeadlessMode) { AssetRef ref = FindAsset("ui_art\\title.pcx"); if (!ref.ok()) { LogError("{}", SDL_GetError()); InsertCDDlg(_("diabdat.mpq or spawn.mpq")); } } hellfire_mpq = LoadMPQ(paths, "hellfire.mpq"); if (hellfire_mpq) gbIsHellfire = true; if (forceHellfire && !hellfire_mpq) InsertCDDlg("hellfire.mpq"); hfmonk_mpq = LoadMPQ(paths, "hfmonk.mpq"); hfbard_mpq = LoadMPQ(paths, "hfbard.mpq"); if (hfbard_mpq) gbBard = true; hfbarb_mpq = LoadMPQ(paths, "hfbarb.mpq"); if (hfbarb_mpq) gbBarbarian = true; hfmusic_mpq = LoadMPQ(paths, "hfmusic.mpq"); hfvoice_mpq = LoadMPQ(paths, "hfvoice.mpq"); if (gbIsHellfire && (!hfmonk_mpq || !hfmusic_mpq || !hfvoice_mpq)) { UiErrorOkDialog(_("Some Hellfire MPQs are missing"), _("Not all Hellfire MPQs were found.\nPlease copy all the hf*.mpq files.")); diablo_quit(1); } #endif } void init_create_window() { if (!SpawnWindow(PROJECT_NAME)) app_fatal(_("Unable to create main window")); dx_init(); gbActive = true; #ifndef USE_SDL1 SDL_DisableScreenSaver(); #endif } void MainWndProc(const SDL_Event &event) { #ifndef USE_SDL1 if (event.type != SDL_WINDOWEVENT) return; switch (event.window.event) { case SDL_WINDOWEVENT_HIDDEN: case SDL_WINDOWEVENT_MINIMIZED: gbActive = false; break; case SDL_WINDOWEVENT_SHOWN: case SDL_WINDOWEVENT_EXPOSED: case SDL_WINDOWEVENT_RESTORED: gbActive = true; RedrawEverything(); break; case SDL_WINDOWEVENT_SIZE_CHANGED: ReinitializeHardwareCursor(); break; case SDL_WINDOWEVENT_LEAVE: sgbMouseDown = CLICK_NONE; LastMouseButtonAction = MouseActionType::None; RedrawEverything(); break; case SDL_WINDOWEVENT_CLOSE: diablo_quit(0); break; case SDL_WINDOWEVENT_FOCUS_LOST: if (*sgOptions.Gameplay.pauseOnFocusLoss) diablo_focus_pause(); break; case SDL_WINDOWEVENT_FOCUS_GAINED: if (*sgOptions.Gameplay.pauseOnFocusLoss) diablo_focus_unpause(); break; case SDL_WINDOWEVENT_MOVED: case SDL_WINDOWEVENT_RESIZED: case SDL_WINDOWEVENT_MAXIMIZED: case SDL_WINDOWEVENT_ENTER: case SDL_WINDOWEVENT_TAKE_FOCUS: break; default: LogVerbose("Unhandled SDL_WINDOWEVENT event: {:d}", event.window.event); break; } #else if (event.type != SDL_ACTIVEEVENT) return; if ((event.active.state & SDL_APPINPUTFOCUS) != 0) { if (event.active.gain == 0) diablo_focus_pause(); else diablo_focus_unpause(); } #endif } } // namespace devilution