From 8198c8442e9484ab38d2a68aaa0e1ae582b1bd83 Mon Sep 17 00:00:00 2001 From: THandke Date: Fri, 10 Jul 2026 19:31:35 +0200 Subject: [PATCH 1/6] Look for map files in a "Maps" subfolder, with root fallback RPG Maker 2000/2003 games traditionally keep MapNNNN.lmu files directly in the project root. This adds support for storing them in a "Maps" subfolder instead, which existing games and tools can adopt for a tidier project layout. RPG_RT.lmt (TreeMap) itself needed no changes: it only stores map IDs, names and hierarchy/metadata, never file paths. The ID-to-filename mapping is a pure Player-side convention, so only the file lookup logic needed updating. To avoid breaking the large existing library of RPG Maker 2000/2003 games that keep maps in the root, lookup prefers "Maps/" but falls back to the root directory when not found there. This affects map loading (Game_Map::LoadMapFile), async map prefetching (Game_Map::RequestMap), the start-map existence check used to trigger non-standard-extension guessing, and the non-standard LMU extension guesser itself. --- src/fileext_guesser.cpp | 41 +++++++++++++++++++++++++++++------------ src/game_map.cpp | 20 +++++++++++++++++--- src/game_map.h | 10 ++++++++++ src/options.h | 4 ++++ src/player.cpp | 4 ++-- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/fileext_guesser.cpp b/src/fileext_guesser.cpp index 568e891960..616689e433 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 "Maps" subfolder, but fall back to + // the project root for games that keep their maps there. + FilesystemView maps_fs = fs.Subtree(MAPS_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..297d6713bb 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,26 @@ 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 "Maps" 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(MAPS_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); + bool in_maps_dir = !FileFinder::Game().FindFile(MAPS_DIR_NAME, map_name).empty(); + + auto* request = AsyncHandler::RequestFile(in_maps_dir ? MAPS_DIR_NAME : ".", map_name); request->SetImportantFile(true); return request; } diff --git a/src/game_map.h b/src/game_map.h index 81384edeb3..7883e6568b 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 + * "Maps" 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..b66e8622ed 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 MAPS_DIR_NAME "Maps" + /** 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..09c87f6815 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 "Maps" subfolder. + return !fs.FindFile(MAPS_DIR_NAME, mapName).empty() || !fs.FindFile(mapName).empty(); } void Player::GuessNonStandardExtensions() { From 2f6221f7b50ba8842b95b1d5c5b9c0bf02a167d7 Mon Sep 17 00:00:00 2001 From: THandke Date: Fri, 10 Jul 2026 19:38:08 +0200 Subject: [PATCH 2/6] refactor: changed map folder name from Maps to Map --- src/fileext_guesser.cpp | 2 +- src/game_map.cpp | 2 +- src/game_map.h | 2 +- src/options.h | 2 +- src/player.cpp | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/fileext_guesser.cpp b/src/fileext_guesser.cpp index 616689e433..0954c4036c 100644 --- a/src/fileext_guesser.cpp +++ b/src/fileext_guesser.cpp @@ -108,7 +108,7 @@ void FileExtGuesser::GuessAndAddLmuExtension(const FilesystemView& fs, Meta cons // 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 "Maps" subfolder, but fall back to + // 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(MAPS_DIR_NAME); if (maps_fs) { diff --git a/src/game_map.cpp b/src/game_map.cpp index 297d6713bb..61daa86569 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -2018,7 +2018,7 @@ 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 "Maps" subfolder, but fall back to + // 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(MAPS_DIR_NAME, map_name); diff --git a/src/game_map.h b/src/game_map.h index 7883e6568b..c413ae141a 100644 --- a/src/game_map.h +++ b/src/game_map.h @@ -712,7 +712,7 @@ namespace Game_Map { /** * Searches for a map file on disk. Maps are preferably stored in the - * "Maps" subfolder, but the game's root directory is also searched for + * "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 diff --git a/src/options.h b/src/options.h index b66e8622ed..c740d8f099 100644 --- a/src/options.h +++ b/src/options.h @@ -84,7 +84,7 @@ /** Subfolder that map files (.lmu/.emu) are preferably stored in. * Games that still keep them in the project root are also supported. */ -#define MAPS_DIR_NAME "Maps" +#define MAPS_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 09c87f6815..3ac18167e7 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -995,7 +995,7 @@ 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, preferring the "Maps" subfolder. + // Now see if the file exists, preferring the "Map" subfolder. return !fs.FindFile(MAPS_DIR_NAME, mapName).empty() || !fs.FindFile(mapName).empty(); } From 2d72c2663a1357ddca39cd4bc2a21689b1f99fc5 Mon Sep 17 00:00:00 2001 From: THandke Date: Fri, 10 Jul 2026 19:47:52 +0200 Subject: [PATCH 3/6] Fix map-in-Maps-folder detection on Emscripten RequestMap() previously probed FileFinder::Game() to decide whether to fetch a map from the "Maps" subfolder or the root directory. On Emscripten, files aren't present in the (virtual) filesystem until they've actually been downloaded, so that probe was always false and maps relocated to "Maps/" would never be fetched from there. Move the fallback logic into AsyncHandler instead: a request can now carry a fallback directory, and if the initial download attempt fails, FileRequestAsync retries once in the fallback directory before reporting failure to listeners. RequestMap() just requests "Maps" with "." as fallback and lets the actual download attempt determine existence, which works regardless of platform. Review feedback from EasyRPG/Player#3607. --- src/async_handler.cpp | 20 ++++++++++++++++++++ src/async_handler.h | 35 +++++++++++++++++++++++++++++++++++ src/game_map.cpp | 11 ++++++++--- 3 files changed, 63 insertions(+), 3 deletions(-) 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/game_map.cpp b/src/game_map.cpp index 61daa86569..3e259c6cba 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -2018,7 +2018,7 @@ 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 + // Maps are preferably stored in the "Maps" 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(MAPS_DIR_NAME, map_name); @@ -2034,9 +2034,14 @@ FileRequestAsync* Game_Map::RequestMap(int map_id) { #endif std::string map_name = Game_Map::ConstructMapName(map_id, false); - bool in_maps_dir = !FileFinder::Game().FindFile(MAPS_DIR_NAME, map_name).empty(); - auto* request = AsyncHandler::RequestFile(in_maps_dir ? MAPS_DIR_NAME : ".", map_name); + // 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(MAPS_DIR_NAME, ".", map_name); request->SetImportantFile(true); return request; } From 65f91a86398938800fcde725a0ac1f93dd83a7fd Mon Sep 17 00:00:00 2001 From: THandke Date: Fri, 10 Jul 2026 19:59:11 +0200 Subject: [PATCH 4/6] renamed MAPS_DIR_NAME to MAP_DIR_NAME --- src/fileext_guesser.cpp | 2 +- src/game_map.cpp | 6 +++--- src/options.h | 2 +- src/player.cpp | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/fileext_guesser.cpp b/src/fileext_guesser.cpp index 0954c4036c..558ed89dd6 100644 --- a/src/fileext_guesser.cpp +++ b/src/fileext_guesser.cpp @@ -110,7 +110,7 @@ void FileExtGuesser::GuessAndAddLmuExtension(const FilesystemView& fs, Meta cons // 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(MAPS_DIR_NAME); + FilesystemView maps_fs = fs.Subtree(MAP_DIR_NAME); if (maps_fs) { GuessLmuExtensionIn(maps_fs, mapping); } diff --git a/src/game_map.cpp b/src/game_map.cpp index 3e259c6cba..d8c253e803 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -2018,10 +2018,10 @@ 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 "Maps" subfolder, but fall back to + // 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(MAPS_DIR_NAME, map_name); + std::string map_file = FileFinder::Game().FindFile(MAP_DIR_NAME, map_name); if (map_file.empty()) { map_file = FileFinder::Game().FindFile(map_name); } @@ -2041,7 +2041,7 @@ FileRequestAsync* Game_Map::RequestMap(int map_id) { // 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(MAPS_DIR_NAME, ".", map_name); + auto* request = AsyncHandler::RequestFile(MAP_DIR_NAME, ".", map_name); request->SetImportantFile(true); return request; } diff --git a/src/options.h b/src/options.h index c740d8f099..4b1f7d941f 100644 --- a/src/options.h +++ b/src/options.h @@ -84,7 +84,7 @@ /** Subfolder that map files (.lmu/.emu) are preferably stored in. * Games that still keep them in the project root are also supported. */ -#define MAPS_DIR_NAME "Map" +#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 3ac18167e7..9aabcb8762 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -996,7 +996,7 @@ static bool DefaultLmuStartFileExists(const FilesystemView& fs) { std::string mapName = Game_Map::ConstructMapName(map_id, false); // Now see if the file exists, preferring the "Map" subfolder. - return !fs.FindFile(MAPS_DIR_NAME, mapName).empty() || !fs.FindFile(mapName).empty(); + return !fs.FindFile(MAP_DIR_NAME, mapName).empty() || !fs.FindFile(mapName).empty(); } void Player::GuessNonStandardExtensions() { From 27a454898c845d130b38db4edbb82c072ceb6dd4 Mon Sep 17 00:00:00 2001 From: THandke Date: Fri, 10 Jul 2026 20:09:07 +0200 Subject: [PATCH 5/6] fixed typo --- src/async_handler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async_handler.h b/src/async_handler.h index 0b3dd5da13..ecfb6f571e 100644 --- a/src/async_handler.h +++ b/src/async_handler.h @@ -76,7 +76,7 @@ namespace AsyncHandler { * 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 + * On platforms without asynchronous 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. From c1e63fba84b1308cb92edebe4337fdea737360bf Mon Sep 17 00:00:00 2001 From: THandke Date: Fri, 10 Jul 2026 20:11:15 +0200 Subject: [PATCH 6/6] Revert "fixed typo" This reverts commit 27a454898c845d130b38db4edbb82c072ceb6dd4. --- src/async_handler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async_handler.h b/src/async_handler.h index ecfb6f571e..0b3dd5da13 100644 --- a/src/async_handler.h +++ b/src/async_handler.h @@ -76,7 +76,7 @@ namespace AsyncHandler { * 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 asynchronous access, existence can only be + * 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.