diff --git a/extern/aurora b/extern/aurora index f1f6bc5b28..3643a369ad 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit f1f6bc5b28d4bd2fc225680ab08d9124328e7911 +Subproject commit 3643a369ad47d8acca8d73dba462eab2ee2bd150 diff --git a/include/dusk/main.h b/include/dusk/main.h index 240c132f40..0a2be8734d 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -10,6 +10,7 @@ extern bool IsShuttingDown; extern bool IsGameLaunched; extern bool RestartRequested; extern std::filesystem::path ConfigPath; +extern std::filesystem::path CachePath; #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ (defined(TARGET_OS_TV) && TARGET_OS_TV) diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 5c75388361..2c2a77079b 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -61,7 +61,7 @@ std::string release_name() { } std::filesystem::path sentry_database_path() { - return dusk::ConfigPath / "sentry"; + return dusk::CachePath / "sentry"; } std::filesystem::path log_attachment_path() { diff --git a/src/dusk/data.cpp b/src/dusk/data.cpp index 154083b365..3e4ce15853 100644 --- a/src/dusk/data.cpp +++ b/src/dusk/data.cpp @@ -6,6 +6,7 @@ #include "dusk/main.h" #include +#include #include #include #include @@ -30,6 +31,21 @@ constexpr auto kLocationDescriptorName = "data_location.json"; constexpr auto kPipelineCacheName = "pipeline_cache.db"; constexpr auto kInitialPipelineCacheName = "initial_pipeline_cache.db"; +constexpr std::array kUserDataDirectories = { + "texture_replacements", + "USA", + "EUR", + "JAP", +}; +constexpr std::array kUserDataFiles = { + "achievements.json", + "config.json", + "controller_ports.dat", + "imgui.ini", + "keyboard_bindings.dat", + "states.json", +}; + enum class LocationMode { Default, Portable, @@ -249,6 +265,62 @@ std::filesystem::path absolute_path(const std::filesystem::path& path) { return absolute.lexically_normal(); } +void rename_legacy_pref_path( + const std::filesystem::path& legacyPath, const std::filesystem::path& prefPath) { + if (legacyPath.empty() || prefPath.empty() || + normalized_path(legacyPath) == normalized_path(prefPath)) + { + return; + } + + std::error_code ec; + if (!std::filesystem::exists(legacyPath, ec)) { + if (ec) { + Log.warn("Failed to inspect legacy data directory '{}': {}", + io::fs_path_to_string(legacyPath), ec.message()); + } + return; + } + + const bool prefExists = std::filesystem::exists(prefPath, ec); + if (ec) { + Log.warn("Failed to inspect data directory '{}': {}", io::fs_path_to_string(prefPath), + ec.message()); + return; + } + if (prefExists) { + if (!std::filesystem::is_directory(prefPath, ec) || + !std::filesystem::is_empty(prefPath, ec)) + { + if (ec) { + Log.warn("Failed to inspect data directory '{}': {}", + io::fs_path_to_string(prefPath), ec.message()); + } else { + Log.info("Skipping legacy data directory rename because '{}' is not empty", + io::fs_path_to_string(prefPath)); + } + return; + } + + std::filesystem::remove(prefPath, ec); + if (ec) { + Log.warn("Failed to remove empty data directory '{}' before legacy rename: {}", + io::fs_path_to_string(prefPath), ec.message()); + return; + } + } + + std::filesystem::rename(legacyPath, prefPath, ec); + if (ec) { + Log.warn("Failed to rename legacy data directory '{}' to '{}': {}", + io::fs_path_to_string(legacyPath), io::fs_path_to_string(prefPath), ec.message()); + return; + } + + Log.info("Renamed legacy data directory '{}' to '{}'", io::fs_path_to_string(legacyPath), + io::fs_path_to_string(prefPath)); +} + bool is_same_or_inside(const std::filesystem::path& root, const std::filesystem::path& path) { const auto normalizedRoot = normalized_path(root); const auto normalizedPath = normalized_path(path); @@ -283,78 +355,39 @@ bool should_skip_migration_path(const std::filesystem::path& path, return false; } -bool has_location_descriptor(const std::filesystem::path& path) { - std::error_code ec; - return std::filesystem::exists(path / kLocationDescriptorName, ec); +bool matches_name(std::string_view name, const auto& names) { + return std::ranges::find(names, name) != names.end(); } -bool remove_empty_destination_for_rename(const std::filesystem::path& path) { - std::error_code ec; - const bool exists = std::filesystem::exists(path, ec); - if (ec) { - Log.debug("Could not inspect migration destination '{}': {}", io::fs_path_to_string(path), - ec.message()); +bool should_migrate_user_data_path( + const std::filesystem::path& sourcePath, const std::filesystem::path& from) { + const auto relativePath = sourcePath.lexically_relative(from); + if (relativePath.empty() || relativePath.is_absolute()) { return false; } - if (!exists) { + + auto it = relativePath.begin(); + if (it == relativePath.end() || *it == "..") { + return false; + } + + const auto first = io::fs_path_to_string(*it); + if (matches_name(first, kUserDataDirectories)) { return true; } - const bool canRemove = std::filesystem::is_directory(path, ec) && - std::filesystem::is_empty(path, ec) && !has_location_descriptor(path); - if (ec || !canRemove) { - if (ec) { - Log.debug("Could not inspect migration destination '{}': {}", - io::fs_path_to_string(path), ec.message()); - } + ++it; + if (it != relativePath.end()) { return false; } - std::filesystem::remove(path, ec); - if (ec) { - Log.debug("Could not remove empty migration destination '{}': {}", - io::fs_path_to_string(path), ec.message()); - return false; + const auto filename = io::fs_path_to_string(relativePath.filename()); + if (matches_name(filename, kUserDataFiles)) { + return true; } - return true; -} - -bool try_rename_directory_migration( - const std::filesystem::path& from, const std::filesystem::path& to) { - std::error_code ec; - if (!std::filesystem::is_directory(from, ec)) { - return false; - } - if (ec) { - Log.debug("Could not inspect migration source '{}': {}", io::fs_path_to_string(from), - ec.message()); - return false; - } - if (has_location_descriptor(from)) { - return false; - } - if (!remove_empty_destination_for_rename(to)) { - return false; - } - - std::filesystem::create_directories(to.parent_path(), ec); - if (ec) { - Log.debug("Could not create migration destination parent '{}': {}", - io::fs_path_to_string(to.parent_path()), ec.message()); - return false; - } - - std::filesystem::rename(from, to, ec); - if (ec) { - Log.debug("Could not rename data directory '{}' to '{}': {}", io::fs_path_to_string(from), - io::fs_path_to_string(to), ec.message()); - return false; - } - - Log.info("Renamed data directory '{}' to '{}'", io::fs_path_to_string(from), - io::fs_path_to_string(to)); - return true; + return relativePath.extension() == ".controller" || relativePath.extension() == ".gci" || + (filename.starts_with("MemoryCard") && filename.ends_with(".raw")); } std::filesystem::path current_data_path() { @@ -439,6 +472,61 @@ bool write_location_descriptor(LocationMode mode, const std::filesystem::path& t return false; } +void set_error(std::string* errorOut, std::string error) { + if (errorOut != nullptr) { + *errorOut = std::move(error); + } +} + +bool validate_writable_data_path(const std::filesystem::path& path, std::string* errorOut) { + if (path.empty()) { + set_error(errorOut, "Choose a folder."); + return false; + } + + std::error_code ec; + std::filesystem::create_directories(path, ec); + if (ec) { + set_error(errorOut, fmt::format("{} could not create the selected folder.", AppName)); + Log.warn("Failed to create custom data folder '{}': {}", io::fs_path_to_string(path), + ec.message()); + return false; + } + + if (!std::filesystem::is_directory(path, ec)) { + set_error(errorOut, "The selected path is not a folder."); + if (ec) { + Log.warn("Failed to inspect custom data folder '{}': {}", io::fs_path_to_string(path), + ec.message()); + } + return false; + } + + const auto probePath = path / fmt::format(".write-probe-{}.tmp", + std::chrono::steady_clock::now().time_since_epoch().count()); + try { + io::FileStream::WriteAllText(probePath, "dusk"); + } catch (const std::exception& e) { + set_error(errorOut, fmt::format("{} could not write to the selected folder.", AppName)); + Log.warn("Failed write probe for custom data folder '{}': {}", io::fs_path_to_string(path), + e.what()); + return false; + } + + std::filesystem::remove(probePath, ec); + if (ec) { + set_error( + errorOut, fmt::format("{} could write to the selected folder, but could not remove " + "the test file it created.", + AppName)); + Log.warn("Failed to remove custom data folder write probe '{}': {}", + io::fs_path_to_string(probePath), ec.message()); + return false; + } + + return true; +} + std::uintmax_t remove_empty_directories(const std::filesystem::path& root, bool includeRoot) { std::error_code ec; std::vector directories; @@ -647,14 +735,6 @@ void migrate_directory(const std::filesystem::path& from, const std::filesystem: return; } - if (try_rename_directory_migration(from, to)) { - return; - } - - if (try_rename_directory_migration(from, to)) { - return; - } - std::filesystem::create_directories(to, ec); if (ec) { ++stats.failures; @@ -700,6 +780,16 @@ void migrate_directory(const std::filesystem::path& from, const std::filesystem: continue; } + if (!should_migrate_user_data_path(sourcePath, from)) { + ++stats.skippedUnsupportedEntries; + if (std::filesystem::is_directory(status)) { + it.disable_recursion_pending(); + } + ec.clear(); + it.increment(ec); + continue; + } + const auto relativePath = sourcePath.lexically_relative(from); if (relativePath.empty() || relativePath.is_absolute()) { ++stats.failures; @@ -761,8 +851,6 @@ void migrate_data(const std::filesystem::path& prefPath, const std::filesystem:: const LocationDescriptor* descriptor) { if (descriptor && !descriptor->previousPath.empty()) { migrate_directory(descriptor->previousPath, dataPath, prefPath); - } else if (const auto legacyPath = get_legacy_path(); !legacyPath.empty()) { - migrate_directory(legacyPath, dataPath, prefPath); } } @@ -899,17 +987,25 @@ bool open_data_path() { #endif } -bool set_custom_data_path(const std::filesystem::path& path) { - if (path.empty()) { - Log.warn("Ignoring empty custom data path"); +bool set_custom_data_path(const std::filesystem::path& path, std::string* errorOut) { + if (!validate_writable_data_path(path, errorOut)) { return false; } - return write_location_descriptor(LocationMode::Custom, path); + if (!write_location_descriptor(LocationMode::Custom, path)) { + set_error(errorOut, fmt::format("{} could not save the data folder setting.", AppName)); + return false; + } + + return true; } -bool set_custom_data_path(const char* path) { - return set_custom_data_path(path_from_utf8(path)); +bool set_custom_data_path(const char* path, std::string* errorOut) { + if (path == nullptr) { + set_error(errorOut, "Choose a folder."); + return false; + } + return set_custom_data_path(path_from_utf8(path), errorOut); } bool set_portable_data_path() { @@ -941,6 +1037,13 @@ std::filesystem::path configured_data_path() { return *sConfiguredDataPath; } +std::filesystem::path cache_path() { + if (!CachePath.empty()) { + return CachePath; + } + return get_pref_path(); +} + bool is_data_path_restart_pending() { if (ConfigPath.empty()) { return false; @@ -949,8 +1052,10 @@ bool is_data_path_restart_pending() { return normalized_path(ConfigPath) != normalized_path(configured_data_path()); } -std::filesystem::path initialize_data() { +Paths initialize_data() { const auto prefPath = get_pref_path(); + rename_legacy_pref_path(get_legacy_path(), prefPath); + const auto descriptor = read_location_descriptor(prefPath); if (descriptor) { sActiveDescriptorPath = descriptor->path; @@ -963,9 +1068,13 @@ std::filesystem::path initialize_data() { migrate_data(prefPath, dataPath, descriptor ? &descriptor->descriptor : nullptr); ensure_data_directory(dataPath); - ensure_initial_pipeline_cache(dataPath); + ensure_data_directory(prefPath); + ensure_initial_pipeline_cache(prefPath); - return dataPath; + return Paths{ + .userPath = dataPath, + .cachePath = prefPath, + }; } } // namespace dusk::data diff --git a/src/dusk/data.hpp b/src/dusk/data.hpp index 85e9457bcb..c3ce495b46 100644 --- a/src/dusk/data.hpp +++ b/src/dusk/data.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #if defined(__APPLE__) #include @@ -14,7 +15,7 @@ #define DUSK_CAN_OPEN_DATA_FOLDER 0 #endif -#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#if (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || defined(__ANDROID__) #define DUSK_CAN_CHANGE_DATA_FOLDER 0 #else #define DUSK_CAN_CHANGE_DATA_FOLDER 1 @@ -22,11 +23,17 @@ namespace dusk::data { -std::filesystem::path initialize_data(); +struct Paths { + std::filesystem::path userPath; + std::filesystem::path cachePath; +}; + +Paths initialize_data(); std::filesystem::path configured_data_path(); +std::filesystem::path cache_path(); bool open_data_path(); -bool set_custom_data_path(const char* path); -bool set_custom_data_path(const std::filesystem::path& path); +bool set_custom_data_path(const char* path, std::string* errorOut); +bool set_custom_data_path(const std::filesystem::path& path, std::string* errorOut); bool set_portable_data_path(); bool reset_data_path(); bool is_default_data_path(); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 2ee51029ab..2a26d5113a 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -3,6 +3,7 @@ #include "aurora/gfx.h" #include "bool_button.hpp" #include "controller_config.hpp" +#include "dusk/app_info.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" #include "dusk/config.hpp" @@ -17,6 +18,7 @@ #include "graphics_tuner.hpp" #include "m_Do/m_Do_main.h" #include "menu_bar.hpp" +#include "modal.hpp" #include "number_button.hpp" #include "menu_bar.hpp" #include "pane.hpp" @@ -25,6 +27,7 @@ #include #include +#include #if DUSK_ENABLE_SENTRY_NATIVE #include "dusk/crash_reporting.h" @@ -297,13 +300,49 @@ private: Rml::String mCurrentRml; }; +void show_data_folder_error_modal(std::string_view message) { + auto dismiss = [](Modal& modal) { + mDoAud_seStartMenu(kSoundWindowClose); + modal.pop(); + }; + push_document(std::make_unique(Modal::Props{ + .title = "Data Folder Not Changed", + .bodyRml = escape(message), + .actions = + { + ModalAction{ + .label = "OK", + .onPressed = dismiss, + }, + }, + .onDismiss = dismiss, + .icon = "warning", + })); + if (auto* doc = top_document()) { + doc->focus(); + } +} + void data_folder_dialog_callback(void*, const char* path, const char* error) { - if (error != nullptr || path == nullptr) { + if (error != nullptr) { + show_data_folder_error_modal(error); return; } - if (data::set_custom_data_path(path)) { - mDoAud_seStartMenu(kSoundItemChange); + if (path == nullptr) { + return; } + + std::string dataPathError; + if (data::set_custom_data_path(path, &dataPathError)) { + mDoAud_seStartMenu(kSoundItemChange); + return; + } + + if (dataPathError.empty()) { + dataPathError = + fmt::format("{} could not use the selected folder as its data folder.", AppName); + } + show_data_folder_error_modal(dataPathError); } const Rml::String kInternalResolutionHelpText = diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index ca681b9257..5b62ae92ed 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -119,6 +119,7 @@ bool dusk::IsShuttingDown = false; bool dusk::IsGameLaunched = false; bool dusk::RestartRequested = false; std::filesystem::path dusk::ConfigPath; +std::filesystem::path dusk::CachePath; #endif void dusk::RequestRestart() noexcept { @@ -513,8 +514,10 @@ int game_main(int argc, char* argv[]) { const auto startupLogLevel = static_cast(parsed_arg_options["log-level"].as()); - dusk::ConfigPath = dusk::data::initialize_data(); - dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel); + const auto dataPaths = dusk::data::initialize_data(); + dusk::ConfigPath = dataPaths.userPath; + dusk::CachePath = dataPaths.cachePath; + dusk::InitializeFileLogging(dusk::CachePath, startupLogLevel); log_build_info(); @@ -536,10 +539,12 @@ int game_main(int argc, char* argv[]) { SDL_SetAppMetadata("Dusklight", DUSK_VERSION_STRING, "dev.twilitrealm.dusk"); { - const auto configPathString = dusk::ConfigPath.u8string(); + const auto userPathString = dusk::ConfigPath.u8string(); + const auto cachePathString = dusk::CachePath.u8string(); AuroraConfig config{}; config.appName = dusk::AppName; - config.configPath = reinterpret_cast(configPathString.c_str()); + config.userPath = reinterpret_cast(userPathString.c_str()); + config.cachePath = reinterpret_cast(cachePathString.c_str()); config.vsync = dusk::getSettings().video.enableVsync; config.startFullscreen = dusk::getSettings().video.enableFullscreen; config.windowPosX = -1;