diff --git a/include/rex/filesystem.h b/include/rex/filesystem.h index d4c512e..8722cce 100644 --- a/include/rex/filesystem.h +++ b/include/rex/filesystem.h @@ -36,6 +36,14 @@ std::filesystem::path GetExecutablePath(); // Get executable folder. std::filesystem::path GetExecutableFolder(); +// Get the application's root folder: the executable folder, or, when the +// executable lives inside a macOS application bundle +// ("/.app/Contents/MacOS/"), the folder containing the +// bundle. User-facing files (config, game data, saves, DLC, logs) resolve +// against this so a bundled app keeps them beside the bundle rather than +// inside it. +std::filesystem::path GetAppRootFolder(); + // Get user folder. std::filesystem::path GetUserFolder(); diff --git a/src/core/filesystem_posix.cpp b/src/core/filesystem_posix.cpp index 87f8e67..6e65d8a 100644 --- a/src/core/filesystem_posix.cpp +++ b/src/core/filesystem_posix.cpp @@ -95,6 +95,21 @@ std::filesystem::path GetExecutableFolder() { return GetExecutablePath().parent_path(); } +std::filesystem::path GetAppRootFolder() { + auto folder = GetExecutableFolder(); +#if REX_PLATFORM_MAC + // /.app/Contents/MacOS/ -> + if (folder.filename() == "MacOS") { + const auto contents = folder.parent_path(); + const auto bundle = contents.parent_path(); + if (contents.filename() == "Contents" && bundle.extension() == ".app") { + return bundle.parent_path(); + } + } +#endif + return folder; +} + std::filesystem::path GetUserFolder() { // get preferred data home char* home = std::getenv("XDG_DATA_HOME"); diff --git a/src/core/filesystem_win.cpp b/src/core/filesystem_win.cpp index b30d4a6..7f3252c 100644 --- a/src/core/filesystem_win.cpp +++ b/src/core/filesystem_win.cpp @@ -65,6 +65,12 @@ std::filesystem::path GetExecutableFolder() { return GetExecutablePath().parent_path(); } +std::filesystem::path GetAppRootFolder() { + // Application bundles are a macOS concept; the executable folder is the + // application root everywhere else. + return GetExecutableFolder(); +} + std::filesystem::path GetUserFolder() { std::filesystem::path result; PWSTR path; diff --git a/src/input/sdl/sdl_input_driver.cpp b/src/input/sdl/sdl_input_driver.cpp index 1303219..99caf39 100644 --- a/src/input/sdl/sdl_input_driver.cpp +++ b/src/input/sdl/sdl_input_driver.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -96,18 +97,23 @@ void SDLInputDriver::OnWindowAvailable(rex::ui::Window* window) { } SDL_Gamepad_initialized_ = true; - // Load custom controller mappings if available + // Load custom controller mappings if available. A relative path is + // resolved against the application root rather than the working + // directory, which is undefined when launched from a bundle/Finder. if (!REXCVAR_GET(hid_mappings_file).empty()) { std::filesystem::path mappings_path(REXCVAR_GET(hid_mappings_file)); + if (mappings_path.is_relative()) { + mappings_path = rex::filesystem::GetAppRootFolder() / mappings_path; + } if (!std::filesystem::exists(mappings_path)) { REXLOG_WARN("SDL GameControllerDB: file '{}' does not exist.", - REXCVAR_GET(hid_mappings_file)); + mappings_path.string()); } else { auto mappings_result = - SDL_AddGamepadMappingsFromFile(REXCVAR_GET(hid_mappings_file).c_str()); + SDL_AddGamepadMappingsFromFile(mappings_path.string().c_str()); if (mappings_result < 0) { REXLOG_ERROR("SDL GameControllerDB: error loading file '{}': {}.", - REXCVAR_GET(hid_mappings_file), mappings_result); + mappings_path.string(), mappings_result); } else { REXLOG_INFO("SDL GameControllerDB: loaded {} mappings.", mappings_result); } diff --git a/src/ui/rex_app.cpp b/src/ui/rex_app.cpp index 092938b..c5b574e 100644 --- a/src/ui/rex_app.cpp +++ b/src/ui/rex_app.cpp @@ -312,7 +312,9 @@ bool ReXApp::OnInitialize() { } bool ReXApp::SetupEnvironment() { - auto exe_dir = rex::filesystem::GetExecutableFolder(); + // App root, not executable folder: inside a macOS .app bundle the config + // and logs stay next to the bundle, where users can find them. + auto exe_dir = rex::filesystem::GetAppRootFolder(); auto config_path = exe_dir / (std::string(GetName()) + ".toml"); // Load config before resolving cvar-backed paths such as game_data_root.