diff --git a/src/async_handler.cpp b/src/async_handler.cpp index 097416f6dd..744ceab6fc 100644 --- a/src/async_handler.cpp +++ b/src/async_handler.cpp @@ -243,6 +243,12 @@ FileRequestAsync* AsyncHandler::RequestFile(std::string_view file_name) { return RequestFile(".", file_name); } +FileRequestAsync* AsyncHandler::RequestFile(std::string_view preferred_dir, std::string_view fallback_dir, std::string_view file_name) { + auto* request = RequestFile(preferred_dir, file_name); + request->SetFallbackDirectory(fallback_dir); + return request; +} + bool AsyncHandler::IsFilePending(bool important, bool graphic) { for (auto& ap: async_requests) { FileRequestAsync& request = ap.second; @@ -439,6 +445,20 @@ void FileRequestAsync::DownloadDone(bool success) { success = state == State_DoneSuccess; } + if (!success && !fallback_directory.empty()) { + // Not found in the current directory. On platforms without + // synchronous file access (Emscripten) this is the only way to + // learn that, since the file isn't present locally until it has + // actually been downloaded. Retry once in the fallback directory + // before giving up. + directory = std::move(fallback_directory); + fallback_directory.clear(); + path = FileFinder::MakePath(directory, file); + state = State_WaitForStart; + Start(); + return; + } + if (success) { #ifdef __EMSCRIPTEN__ if (state == State_Pending) { diff --git a/src/async_handler.h b/src/async_handler.h index e96aa9a50c..0b3dd5da13 100644 --- a/src/async_handler.h +++ b/src/async_handler.h @@ -69,6 +69,25 @@ namespace AsyncHandler { */ FileRequestAsync* RequestFile(std::string_view file_name); + /** + * Creates a request to a file that may reside in one of two possible + * directories, e.g. because a project can store it in a preferred + * subfolder or (for compatibility) in the project root. + * The file is requested from preferred_dir first. On platforms with + * synchronous file access (i.e. everywhere except Emscripten) whether + * the file actually exists there is already known at this point. + * On platforms without synchronous access, existence can only be + * determined by attempting the download: if that attempt fails, a + * fallback request for fallback_dir is issued automatically, and the + * request is only considered finished once that also completes. + * + * @param preferred_dir directory to try first + * @param fallback_dir directory to fall back to when the file isn't in preferred_dir + * @param file_name Name of the requested file requested. + * @return The async request. + */ + FileRequestAsync* RequestFile(std::string_view preferred_dir, std::string_view fallback_dir, std::string_view file_name); + /** * Checks if any file with important-flag hasn't finished downloading yet. * @@ -159,6 +178,17 @@ class FileRequestAsync { */ void SetGraphicFile(bool graphic); + /** + * Sets a fallback directory to retry the request in when it fails in + * its current directory (see AsyncHandler::RequestFile with a + * preferred/fallback directory pair). Only one fallback attempt is + * made. + * This must be set before Start() is invoked. + * + * @param dir directory to retry the request in on failure. + */ + void SetFallbackDirectory(std::string_view dir); + /** * Starts the async requests. * When the request was already started earlier and is pending this call @@ -218,6 +248,7 @@ class FileRequestAsync { std::string directory; std::string file; std::string path; + std::string fallback_directory; int state = State_DoneFailure; bool important = false; bool graphic = false; @@ -255,6 +286,10 @@ inline void FileRequestAsync::SetImportantFile(bool important) { this->important = important; } +inline void FileRequestAsync::SetFallbackDirectory(std::string_view dir) { + fallback_directory = ToString(dir); +} + inline bool FileRequestAsync::IsGraphicFile() const { return graphic; } diff --git a/src/fileext_guesser.cpp b/src/fileext_guesser.cpp index 568e891960..558ed89dd6 100644 --- a/src/fileext_guesser.cpp +++ b/src/fileext_guesser.cpp @@ -67,17 +67,10 @@ FileExtGuesser::RPG2KNonStandardFilenameGuesser FileExtGuesser::GetRPG2kProjectW return RPG2KNonStandardFilenameGuesser(); } -void FileExtGuesser::GuessAndAddLmuExtension(const FilesystemView& fs, Meta const& meta, RPG2KFileExtRemap& mapping) -{ - // If metadata is provided, rely on that. - std::string metaLmu = meta.GetLmuAlias(); - if (!metaLmu.empty()) { - mapping.extMap[SUFFIX_LMU] = metaLmu; - Output::Debug("Metadata-provided non-standard extension for LMU({})", metaLmu); - } else { - // Try to rescue and determine file extensions. - // Without metadata, scan for matching files. Stop after you find a few; - // we can't just pick the first since there may be some backup files on disk. +namespace { + // Scans a single directory for "mapXXXX.???" files and guesses the LMU + // extension from whichever non-standard extension occurs most often. + void GuessLmuExtensionIn(const FilesystemView& fs, FileExtGuesser::RPG2KFileExtRemap& mapping) { std::unordered_map extCounts; // ext => count const auto* entries = fs.ListDirectory(); @@ -94,7 +87,7 @@ void FileExtGuesser::GuessAndAddLmuExtension(const FilesystemView& fs, Meta cons if (extCounts[ext] >= 5) { mapping.extMap[SUFFIX_LMU] = ext; Output::Debug("Guessing non-standard extension for LMU({})", ext); - break; + return; } } } @@ -102,6 +95,30 @@ void FileExtGuesser::GuessAndAddLmuExtension(const FilesystemView& fs, Meta cons } } +void FileExtGuesser::GuessAndAddLmuExtension(const FilesystemView& fs, Meta const& meta, RPG2KFileExtRemap& mapping) +{ + // If metadata is provided, rely on that. + std::string metaLmu = meta.GetLmuAlias(); + if (!metaLmu.empty()) { + mapping.extMap[SUFFIX_LMU] = metaLmu; + Output::Debug("Metadata-provided non-standard extension for LMU({})", metaLmu); + return; + } + + // Try to rescue and determine file extensions. + // Without metadata, scan for matching files. Stop after you find a few; + // we can't just pick the first since there may be some backup files on disk. + // Maps are preferably stored in the "Map" subfolder, but fall back to + // the project root for games that keep their maps there. + FilesystemView maps_fs = fs.Subtree(MAP_DIR_NAME); + if (maps_fs) { + GuessLmuExtensionIn(maps_fs, mapping); + } + if (mapping.extMap.find(SUFFIX_LMU) == mapping.extMap.end()) { + GuessLmuExtensionIn(fs, mapping); + } +} + FileExtGuesser::RPG2KFileExtRemap FileExtGuesser::RPG2KNonStandardFilenameGuesser::guessExtensions(Meta& meta) { RPG2KFileExtRemap res; diff --git a/src/game_map.cpp b/src/game_map.cpp index b4acc5b50b..d8c253e803 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -330,11 +330,11 @@ std::unique_ptr Game_Map::LoadMapFile(int map_id) { // FIXME: Assert map was cached for async platforms bool map_is_easyrpg_file = Player::player_config.prefer_easyrpg_map_files.Get(); std::string map_name = Game_Map::ConstructMapName(map_id, map_is_easyrpg_file); - std::string map_file = FileFinder::Game().FindFile(map_name); + std::string map_file = Game_Map::FindMapFile(map_name); if (map_file.empty()) { map_is_easyrpg_file = !map_is_easyrpg_file; map_name = Game_Map::ConstructMapName(map_id, map_is_easyrpg_file); - map_file = FileFinder::Game().FindFile(map_name); + map_file = Game_Map::FindMapFile(map_name); if (map_file.empty()) { Output::Error("Loading of Map {} failed.\nThe map was not found.", map_name); @@ -2017,12 +2017,31 @@ std::string Game_Map::ConstructMapName(int map_id, bool is_easyrpg) { } } +std::string Game_Map::FindMapFile(std::string_view map_name) { + // Maps are preferably stored in the "Map" subfolder, but fall back to + // the game's root directory for compatibility with games that keep + // their maps there. + std::string map_file = FileFinder::Game().FindFile(MAP_DIR_NAME, map_name); + if (map_file.empty()) { + map_file = FileFinder::Game().FindFile(map_name); + } + return map_file; +} + FileRequestAsync* Game_Map::RequestMap(int map_id) { #ifdef __EMSCRIPTEN__ Player::translation.RequestAndAddMap(map_id); #endif - auto* request = AsyncHandler::RequestFile(Game_Map::ConstructMapName(map_id, false)); + std::string map_name = Game_Map::ConstructMapName(map_id, false); + + // On platforms with synchronous file access, FindMapFile() above could + // tell us upfront which directory the map is in. But on platforms like + // Emscripten a file isn't available locally until it has actually been + // downloaded, so existence can't be probed in advance: request the + // preferred directory and let AsyncHandler fall back to the root + // directory automatically if that attempt fails. + auto* request = AsyncHandler::RequestFile(MAP_DIR_NAME, ".", map_name); request->SetImportantFile(true); return request; } diff --git a/src/game_map.h b/src/game_map.h index 81384edeb3..c413ae141a 100644 --- a/src/game_map.h +++ b/src/game_map.h @@ -710,6 +710,16 @@ namespace Game_Map { */ std::string ConstructMapName(int map_id, bool is_easyrpg); + /** + * Searches for a map file on disk. Maps are preferably stored in the + * "Map" subfolder, but the game's root directory is also searched for + * compatibility with existing games that were never reorganized. + * + * @param map_name The filename to search for, as returned by ConstructMapName + * @return Path to the found file, or an empty string when not found + */ + std::string FindMapFile(std::string_view map_name); + FileRequestAsync* RequestMap(int map_id); namespace Caching { diff --git a/src/options.h b/src/options.h index a20e88c994..4b1f7d941f 100644 --- a/src/options.h +++ b/src/options.h @@ -82,6 +82,10 @@ #define TREEMAP_NAME RPG_RT_PREFIX "." SUFFIX_LMT #define TREEMAP_NAME_EASYRPG EASY_RT_PREFIX "." SUFFIX_EMT +/** Subfolder that map files (.lmu/.emu) are preferably stored in. + * Games that still keep them in the project root are also supported. */ +#define MAP_DIR_NAME "Map" + /** File name for additional metadata, such as multi-game save imports. */ #define META_NAME "Meta.ini" diff --git a/src/player.cpp b/src/player.cpp index 1ff38f8c6c..9aabcb8762 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -995,8 +995,8 @@ static bool DefaultLmuStartFileExists(const FilesystemView& fs) { int map_id = Player::start_map_id == -1 ? lcf::Data::treemap.start.party_map_id : Player::start_map_id; std::string mapName = Game_Map::ConstructMapName(map_id, false); - // Now see if the file exists. - return !fs.FindFile(mapName).empty(); + // Now see if the file exists, preferring the "Map" subfolder. + return !fs.FindFile(MAP_DIR_NAME, mapName).empty() || !fs.FindFile(mapName).empty(); } void Player::GuessNonStandardExtensions() {