diff --git a/.ci/deploy-mac.sh b/.ci/deploy-mac.sh index 819e0725bc..930a87eee2 100755 --- a/.ci/deploy-mac.sh +++ b/.ci/deploy-mac.sh @@ -4,11 +4,15 @@ cd build || exit 1 cd bin +git clone --revision=32dceb35e2c95b46cec501033cbc3a1ddf32d6e8 https://github.com/KhronosGroup/MoltenVK.git +cd MoltenVK +./fetchDependencies --macos +make macos MVK_USE_METAL_PRIVATE_API=1 +cd ../ + mkdir -p "rpcs3.app/Contents/Resources/vulkan/icd.d" || true -wget https://github.com/KhronosGroup/MoltenVK/releases/download/v1.4.1/MoltenVK-macos-privateapi.tar -tar -xvf MoltenVK-macos-privateapi.tar -cp "MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib" "rpcs3.app/Contents/Frameworks/libMoltenVK.dylib" -cp "MoltenVK/MoltenVK/dynamic/dylib/macOS/MoltenVK_icd.json" "rpcs3.app/Contents/Resources/vulkan/icd.d/MoltenVK_icd.json" +cp "MoltenVK/Package/Latest/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib" "rpcs3.app/Contents/Frameworks/libMoltenVK.dylib" +cp "MoltenVK/Package/Latest/MoltenVK/dynamic/dylib/macOS/MoltenVK_icd.json" "rpcs3.app/Contents/Resources/vulkan/icd.d/MoltenVK_icd.json" sed -i '' "s/.\//..\/..\/..\/Frameworks\//g" "rpcs3.app/Contents/Resources/vulkan/icd.d/MoltenVK_icd.json" cp "$(realpath $BREW_PATH/opt/llvm@$LLVM_COMPILER_VER/lib/c++/libc++abi.1.0.dylib)" "rpcs3.app/Contents/Frameworks/libc++abi.1.dylib" diff --git a/.ci/deploy-windows-clang.sh b/.ci/deploy-windows-clang.sh index 07b4866fc4..04ba1bb20a 100644 --- a/.ci/deploy-windows-clang.sh +++ b/.ci/deploy-windows-clang.sh @@ -24,6 +24,7 @@ mkdir ./bin/config mkdir ./bin/config/input_configs curl -fsSL 'https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt' 1> ./bin/config/input_configs/gamecontrollerdb.txt curl -fsSL 'https://rpcs3.net/compatibility?api=v1&export' | iconv -f ISO-8859-1 -t UTF-8 1> ./bin/GuiConfigs/compat_database.dat +curl -fsSL 'https://api.rpcs3.net/config/?api=v1' | iconv -f ISO-8859-1 -t UTF-8 1> ./bin/GuiConfigs/config_database.dat # Download translations mkdir -p ./bin/share/qt6/translations diff --git a/.ci/deploy-windows.sh b/.ci/deploy-windows.sh index 069f8fb637..3c59391a66 100755 --- a/.ci/deploy-windows.sh +++ b/.ci/deploy-windows.sh @@ -14,6 +14,7 @@ mkdir ./bin/config mkdir ./bin/config/input_configs curl -fsSL 'https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt' 1> ./bin/config/input_configs/gamecontrollerdb.txt curl -fsSL 'https://rpcs3.net/compatibility?api=v1&export' | iconv -t UTF-8 1> ./bin/GuiConfigs/compat_database.dat +curl -fsSL 'https://api.rpcs3.net/config/?api=v1' | iconv -t UTF-8 1> ./bin/GuiConfigs/config_database.dat # Download translations mkdir -p ./bin/qt6/translations diff --git a/3rdparty/curl/CMakeLists.txt b/3rdparty/curl/CMakeLists.txt index 2b725169f9..0079315267 100644 --- a/3rdparty/curl/CMakeLists.txt +++ b/3rdparty/curl/CMakeLists.txt @@ -7,24 +7,24 @@ if(USE_SYSTEM_CURL) target_link_libraries(3rdparty_libcurl INTERFACE CURL::libcurl) else() message(STATUS "RPCS3: building libcurl + wolfssl submodules") - set(BUILD_CURL_EXE OFF CACHE BOOL "Set to ON to build curl executable.") - set(BUILD_STATIC_CURL OFF CACHE BOOL "Set to ON to build curl executable with static libcurl.") - set(BUILD_STATIC_LIBS ON CACHE BOOL "Set to ON to build static libcurl.") - set(BUILD_SHARED_LIBS OFF CACHE BOOL "Set to ON to build shared libcurl.") + set(BUILD_CURL_EXE OFF CACHE INTERNAL "") + set(BUILD_STATIC_CURL OFF CACHE INTERNAL "") + set(BUILD_STATIC_LIBS ON CACHE INTERNAL "") + set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "") find_package(WolfSSL REQUIRED) - set(CURL_USE_WOLFSSL ON CACHE BOOL "enable wolfSSL for SSL/TLS") - set(CURL_USE_OPENSSL OFF CACHE BOOL "Use OpenSSL code. Experimental") - set(HTTP_ONLY ON CACHE BOOL "disables all protocols except HTTP (This overrides all CURL_DISABLE_* options)") - set(USE_LIBIDN2 OFF CACHE BOOL "Use libidn2 for IDN support") # Disabled because MacOS CI doesn't work otherwise - set(CURL_CA_PATH "none" CACHE STRING "Location of default CA path. Set 'none' to disable or 'auto' for auto-detection. Defaults to 'auto'.") - option(CURL_DISABLE_INSTALL "Disable installation targets" ON) + set(CURL_USE_WOLFSSL ON CACHE INTERNAL "") + set(CURL_USE_OPENSSL OFF CACHE INTERNAL "") + set(HTTP_ONLY ON CACHE INTERNAL "") + set(USE_LIBIDN2 OFF CACHE INTERNAL "") # Disabled because MacOS CI doesn't work otherwise + set(CURL_CA_PATH "none" CACHE INTERNAL "") + set(CURL_DISABLE_INSTALL ON CACHE INTERNAL "") if(WIN32) - set(ENABLE_UNICODE ON CACHE BOOL "enable Unicode") + set(ENABLE_UNICODE ON CACHE INTERNAL "") endif() - set(CURL_USE_LIBSSH2 OFF CACHE BOOL "Use libSSH2") - set(CURL_USE_LIBPSL OFF CACHE BOOL "Use libPSL") - option(BUILD_TESTING "Build tests" OFF) - option(BUILD_EXAMPLES "Build libcurl examples" OFF) + set(CURL_USE_LIBSSH2 OFF CACHE INTERNAL "") + set(CURL_USE_LIBPSL OFF CACHE INTERNAL "") + set(BUILD_TESTING OFF CACHE INTERNAL "") + set(BUILD_EXAMPLES OFF CACHE INTERNAL "") add_subdirectory(curl EXCLUDE_FROM_ALL) diff --git a/3rdparty/libpng/libpng b/3rdparty/libpng/libpng index d5515b5b8b..3061454d98 160000 --- a/3rdparty/libpng/libpng +++ b/3rdparty/libpng/libpng @@ -1 +1 @@ -Subproject commit d5515b5b8be3901aac04e5bd8bd5c89f287bcd33 +Subproject commit 3061454d980de7d53608f594194cfac722721d2a diff --git a/3rdparty/version_check.sh b/3rdparty/version_check.sh index e0632da3e7..7947a006c1 100644 --- a/3rdparty/version_check.sh +++ b/3rdparty/version_check.sh @@ -119,7 +119,8 @@ echo -e "\n\nResult:\n" # Find the max length of the paths (before '->') max_len=0 -while IFS='->' read -r left _; do +while read -r line; do + left="${line%%->*}" len=$(echo -n "$left" | wc -c) if (( len > max_len )); then max_len=$len @@ -127,8 +128,10 @@ while IFS='->' read -r left _; do done < "$resultfile" # Print with padding so '->' lines up -while IFS='->' read -r left right; do - right=$(echo "$right" | sed 's/^[[:space:]]*>*[[:space:]]*//') +while read -r line; do + left="${line%%->*}" + right="${line#*->}" + right=$(echo "$right" | sed 's/^[[:space:]]*//') printf "%-${max_len}s -> %s\n" "$left" "$right" done < "$resultfile" diff --git a/3rdparty/wolfssl/CMakeLists.txt b/3rdparty/wolfssl/CMakeLists.txt index cf1a66a1f5..2c19c37d2f 100644 --- a/3rdparty/wolfssl/CMakeLists.txt +++ b/3rdparty/wolfssl/CMakeLists.txt @@ -4,23 +4,22 @@ if(USE_SYSTEM_WOLFSSL) add_library(wolfssl INTERFACE) target_link_libraries(wolfssl INTERFACE PkgConfig::WolfSSL) else() - option(WOLFSSL_TLS13 "Enable wolfSSL TLS v1.3 (default: enabled)" OFF) - set(WOLFSSL_SHA3 ON CACHE STRING "Enable wolfSSL SHA-3 support (default: enabled on x86_64/aarch64)") - set(WOLFSSL_SHAKE256 ON CACHE STRING "Enable wolfSSL SHAKE256 support (default: enabled on x86_64/aarch64)") - option(WOLFSSL_BASE64_ENCODE "Enable Base64 encoding (default: enabled on x86_64)" OFF) - option(WOLFSSL_DES3 "Enable DES3 (default: disabled)" ON) - option(WOLFSSL_PWDBASED "Enable PWDBASED (default: disabled)" ON) - option(WOLFSSL_FAST_MATH "Enable fast math ops (default: disabled)" ON) - option(WOLFSSL_EXAMPLES "Enable examples (default: enabled)" OFF) - option(WOLFSSL_CRYPT_TESTS "Enable Crypt Bench/Test (default: enabled)" OFF) - option(WOLFSSL_ASYNC_THREADS "Enable Asynchronous Threading (default: enabled)" OFF) - option(WOLFSSL_BUILD_OUT_OF_TREE "Don't generate files in the source tree (default: yes)" ON) - option(WOLFSSL_SNI "Enable SNI (default: disabled)" ON) - option(WOLFSSL_OPENSSLEXTRA "Enable extra OpenSSL API, size+ (default: disabled)" ON) - option(WOLFSSL_HARDEN "Enable Hardened build, Enables Timing Resistance and Blinding (default: enabled)" OFF) - option(WOLFSSL_ALT_CERT_CHAINS "Enable support for Alternate certification chains (default: disabled)" ON) + set(WOLFSSL_TLS13 OFF CACHE INTERNAL "") + set(WOLFSSL_SHA3 ON CACHE INTERNAL "") + set(WOLFSSL_SHAKE256 ON CACHE INTERNAL "") + set(WOLFSSL_BASE64_ENCODE OFF CACHE INTERNAL "") + set(WOLFSSL_DES3 ON CACHE INTERNAL "") + set(WOLFSSL_PWDBASED ON CACHE INTERNAL "") + set(WOLFSSL_FAST_MATH ON CACHE INTERNAL "") + set(WOLFSSL_EXAMPLES OFF CACHE INTERNAL "") + set(WOLFSSL_CRYPT_TESTS OFF CACHE INTERNAL "") + set(WOLFSSL_ASYNC_THREADS OFF CACHE INTERNAL "") + set(WOLFSSL_BUILD_OUT_OF_TREE ON CACHE INTERNAL "") + set(WOLFSSL_SNI ON CACHE INTERNAL "") + set(WOLFSSL_OPENSSLEXTRA ON CACHE INTERNAL "") + set(WOLFSSL_ALT_CERT_CHAINS ON CACHE INTERNAL "") add_subdirectory(wolfssl EXCLUDE_FROM_ALL) - target_compile_definitions(wolfssl PUBLIC WOLFSSL_DES_ECB HAVE_WRITE_DUP FP_MAX_BITS=8192 WOLFSSL_NO_OPTIONS_H) + target_compile_definitions(wolfssl PUBLIC WOLFSSL_DES_ECB HAVE_WRITE_DUP FP_MAX_BITS=8192 WOLFSSL_USE_OPTIONS_H) endif() diff --git a/3rdparty/wolfssl/wolfssl b/3rdparty/wolfssl/wolfssl index 922d04b356..1d363f3adc 160000 --- a/3rdparty/wolfssl/wolfssl +++ b/3rdparty/wolfssl/wolfssl @@ -1 +1 @@ -Subproject commit 922d04b3568c6428a9fb905ddee3ef5a68db3108 +Subproject commit 1d363f3adceba9d1478230ede476a37b0dcdef24 diff --git a/Utilities/Config.cpp b/Utilities/Config.cpp index bd0fe7a8b8..f242bd6172 100644 --- a/Utilities/Config.cpp +++ b/Utilities/Config.cpp @@ -40,7 +40,7 @@ namespace cfg owner->m_nodes.emplace_back(this); } - bool _base::from_string(std::string_view, bool) + bool _base::from_string(std::string_view /*value*/, bool /*dynamic*/) { cfg_log.fatal("cfg::_base::from_string() purecall"); return false; @@ -68,7 +68,7 @@ namespace cfg // Incrementally load config entries from YAML::Node. // The config value is preserved if the corresponding YAML node doesn't exist. - static void decode(const YAML::Node& data, class _base& rhs, bool dynamic = false); + [[nodiscard]] static bool decode(const YAML::Node& data, class _base& rhs, bool dynamic, bool strict); } std::vector cfg::make_int_range(s64 min, s64 max) @@ -76,11 +76,11 @@ std::vector cfg::make_int_range(s64 min, s64 max) return {std::to_string(min), std::to_string(max)}; } -bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max) +bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max, std::string_view name) { if (value.empty()) { - if (out) cfg_log.error("cfg::try_to_int64(): called with an empty string"); + if (out) cfg_log.error("cfg::try_to_int64('%s'): called with an empty string", name); return false; } @@ -107,7 +107,7 @@ bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max) if (ret.ec != std::errc() || ret.ptr != end || (start[0] == '-' && sign < 0)) { - if (out) cfg_log.error("cfg::try_to_int64('%s'): invalid integer", value); + if (out) cfg_log.error("cfg::try_to_int64('%s', '%s'): invalid integer", value, name); return false; } @@ -115,7 +115,7 @@ bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max) if (result < min || result > max) { - if (out) cfg_log.error("cfg::try_to_int64('%s'): out of bounds (val=%d, min=%d, max=%d)", value, result, min, max); + if (out) cfg_log.error("cfg::try_to_int64('%s', '%s'): out of bounds (val=%d, min=%d, max=%d)", value, name, result, min, max); return false; } @@ -128,11 +128,11 @@ std::vector cfg::make_uint_range(u64 min, u64 max) return {std::to_string(min), std::to_string(max)}; } -bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max) +bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max, std::string_view name) { if (value.empty()) { - if (out) cfg_log.error("cfg::try_to_uint64(): called with an empty string"); + if (out) cfg_log.error("cfg::try_to_uint64('%s'): called with an empty string", name); return false; } @@ -152,13 +152,13 @@ bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max) if (ret.ec != std::errc() || ret.ptr != end) { - if (out) cfg_log.error("cfg::try_to_uint64('%s'): invalid integer", value); + if (out) cfg_log.error("cfg::try_to_uint64('%s', '%s'): invalid integer", value, name); return false; } if (result < min || result > max) { - if (out) cfg_log.error("cfg::try_to_uint64('%s'): out of bounds (val=%u, min=%u, max=%u)", value, result, min, max); + if (out) cfg_log.error("cfg::try_to_uint64('%s', '%s'): out of bounds (val=%u, min=%u, max=%u)", value, name, result, min, max); return false; } @@ -166,11 +166,11 @@ bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max) return true; } -bool try_to_uint128(u128* out, std::string_view value) +bool try_to_uint128(u128* out, std::string_view value, std::string_view name) { if (value.empty()) { - if (out) cfg_log.error("cfg::try_to_uint128(): called with an empty string"); + if (out) cfg_log.error("cfg::try_to_uint128('%s'): called with an empty string", name); return false; } @@ -193,7 +193,7 @@ bool try_to_uint128(u128* out, std::string_view value) if (ret.ec != std::errc() || ret.ptr != end) { - if (out) cfg_log.error("cfg::try_to_uint128('%s'): invalid integer", value); + if (out) cfg_log.error("cfg::try_to_uint128('%s', '%s'): invalid integer", value, name); return false; } @@ -207,7 +207,7 @@ bool try_to_uint128(u128* out, std::string_view value) if (ret.ec != std::errc() || ret.ptr != start_low64) { - if (out) cfg_log.error("cfg::try_to_uint128('%s'): invalid integer", value); + if (out) cfg_log.error("cfg::try_to_uint128('%s', '%s'): invalid integer", value, name); return false; } @@ -220,11 +220,11 @@ std::vector cfg::make_float_range(f64 min, f64 max) return {std::to_string(min), std::to_string(max)}; } -bool try_to_float(f64* out, std::string_view value, f64 min, f64 max) +bool try_to_float(f64* out, std::string_view value, f64 min, f64 max, std::string_view name) { if (value.empty()) { - if (out) cfg_log.error("cfg::try_to_float(): called with an empty string"); + if (out) cfg_log.error("cfg::try_to_float('%s'): called with an empty string", name); return false; } @@ -237,13 +237,13 @@ bool try_to_float(f64* out, std::string_view value, f64 min, f64 max) if (end_check != str.data() + str.size()) { - if (out) cfg_log.error("cfg::try_to_float('%s'): invalid float", value); + if (out) cfg_log.error("cfg::try_to_float('%s', '%s'): invalid float", value, name); return false; } if (result < min || result > max) { - if (out) cfg_log.error("cfg::try_to_float('%s'): out of bounds (val=%f, min=%f, max=%f)", value, result, min, max); + if (out) cfg_log.error("cfg::try_to_float('%s', '%s'): out of bounds (val=%f, min=%f, max=%f)", value, name, result, min, max); return false; } @@ -251,7 +251,7 @@ bool try_to_float(f64* out, std::string_view value, f64 min, f64 max) return true; } -bool try_to_string(std::string* out, const f64& value) +bool try_to_string(std::string* out, f64 value, std::string_view name) { #ifdef __APPLE__ if (out) *out = std::to_string(value); @@ -266,13 +266,13 @@ bool try_to_string(std::string* out, const f64& value) } else { - if (out) cfg_log.error("cfg::try_to_string(): could not convert value '%f' to string. error='%s'", value, std::make_error_code(ec).message()); + if (out) cfg_log.error("cfg::try_to_string('%s'): could not convert value '%f' to string. error='%s'", name, value, std::make_error_code(ec).message()); return false; } #endif } -bool cfg::try_to_enum_value(u64* out, decltype(&fmt_class_string::format) func, std::string_view value) +bool cfg::try_to_enum_value(u64* out, decltype(&fmt_class_string::format) func, std::string_view value, std::string_view name) { u64 max = umax; @@ -313,13 +313,13 @@ bool cfg::try_to_enum_value(u64* out, decltype(&fmt_class_string::format) f if (ret.ec != std::errc() || ret.ptr != end) { - if (out) cfg_log.error("cfg::try_to_enum_value('%s'): invalid enum or integer", value); + if (out) cfg_log.error("cfg::try_to_enum_value('%s', '%s'): invalid enum or integer", value, name); return false; } if (result > max) { - if (out) cfg_log.error("cfg::try_to_enum_value('%s'): out of bounds(val=%u, min=0, max=%u)", value, result, max); + if (out) cfg_log.error("cfg::try_to_enum_value('%s', '%s'): out of bounds(val=%u, min=0, max=%u)", value, name, result, max); return false; } @@ -468,37 +468,50 @@ void cfg::encode(YAML::Emitter& out, const cfg::_base& rhs) } } -void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) +bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool strict) { if (dynamic && !rhs.get_is_dynamic()) { - return; + return true; } switch (rhs.get_type()) { case type::node: { - if (data.IsScalar() || data.IsSequence()) + if (!data.IsMap()) { - return; // ??? + cfg_log.error("node node is not a map"); + return false; } + bool success = true; + for (const auto& pair : data) { if (!pair.first.IsScalar()) continue; // Find the key among existing nodes - for (const auto& node : static_cast(rhs).get_nodes()) + const auto& nodes = static_cast(rhs).get_nodes(); + const auto it = std::find_if(nodes.cbegin(), nodes.cend(), [&pair](const auto& node) { return ensure(node)->get_name() == pair.first.Scalar(); }); + + if (it == nodes.cend()) { - if (node->get_name() == pair.first.Scalar()) + if (strict) { - decode(pair.second, *node, dynamic); + cfg_log.error("Unknown key found: '%s'", pair.first.Scalar()); + success = false; } + continue; + } + + if (!decode(pair.second, *ensure(*it), dynamic, strict) && strict) + { + success = false; } } - break; + return success; } case type::set: { @@ -506,7 +519,10 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) if (YAML::convert::decode(data, values)) { - rhs.from_list(std::move(values)); + if (!rhs.from_list(std::move(values)) && strict) + { + return false; + } } break; @@ -516,7 +532,8 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) { if (!data.IsMap()) { - return; + cfg_log.error("map node is not a map"); + return false; } map_of_type values; @@ -533,22 +550,36 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) } case type::log: { - if (data.IsScalar() || data.IsSequence()) + if (!data.IsMap()) { - return; // ??? + cfg_log.error("log node is not a map"); + return false; } map_of_type values; for (const auto& pair : data) { - if (!pair.first.IsScalar() || !pair.second.IsScalar()) continue; + if (!pair.first.IsScalar() || !pair.second.IsScalar()) + { + if (strict) + { + if (!pair.first.IsScalar()) + cfg_log.error("Key in map is not a scalar"); + else + cfg_log.error("Value in map is not a scalar. key='%s'", pair.first.Scalar()); + return false; + } + continue; + } u64 value; - if (cfg::try_to_enum_value(&value, &fmt_class_string::format, pair.second.Scalar())) + if (!cfg::try_to_enum_value(&value, &fmt_class_string::format, pair.second.Scalar(), pair.first.Scalar()) && strict) { - values.emplace(pair.first.Scalar(), static_cast(static_cast(value))); + return false; } + + values.emplace(pair.first.Scalar(), static_cast(static_cast(value))); } static_cast(rhs).set_map(std::move(values)); @@ -558,20 +589,43 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) { if (!data.IsMap()) { - return; // ??? + cfg_log.error("device node is not a map"); + return false; } map_of_type values; for (const auto& pair : data) { - if (!pair.first.IsScalar() || !pair.second.IsMap()) continue; + if (!pair.first.IsScalar() || !pair.second.IsMap()) + { + if (strict) + { + if (!pair.first.IsScalar()) + cfg_log.error("Key in device map is not a scalar"); + else + cfg_log.error("Value in device map is not a map. key='%s'", pair.first.Scalar()); + return false; + } + continue; + } device_info info{}; for (const auto& key_value : pair.second) { - if (!key_value.first.IsScalar() || !key_value.second.IsScalar()) continue; + if (!key_value.first.IsScalar() || !key_value.second.IsScalar()) + { + if (strict) + { + if (!key_value.first.IsScalar()) + cfg_log.error("Key in device info map is not a scalar"); + else + cfg_log.error("Value in device map is not a scalar. key='%s'", key_value.first.Scalar()); + return false; + } + continue; + } if (key_value.first.Scalar() == "Path") info.path = key_value.second.Scalar(); @@ -598,12 +652,17 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) if (YAML::convert::decode(data, value)) { - rhs.from_string(value, dynamic); + if (!rhs.from_string(value, dynamic) && strict) + { + return false; + } } break; // ??? } } + + return true; } std::string cfg::node::to_string() const @@ -620,8 +679,7 @@ bool cfg::node::from_string(std::string_view value, bool dynamic) if (error.empty()) { - cfg::decode(result, *this, dynamic); - return true; + return cfg::decode(result, *this, dynamic, false); } cfg_log.error("Failed to load node: %s", error); @@ -644,6 +702,19 @@ void cfg::node::restore_defaults() } } +bool cfg::node::validate(std::string_view value) +{ + auto [result, error] = yaml_load(std::string(value)); + + if (error.empty()) + { + return cfg::decode(result, *this, false, true); + } + + cfg_log.error("Failed to load node: %s", error); + return false; +} + std::string cfg::map_entry::get_value(std::string_view key) { if (auto it = m_map.find(key); it != m_map.end()) diff --git a/Utilities/Config.h b/Utilities/Config.h index 4c79cbf31c..4f8e578fde 100644 --- a/Utilities/Config.h +++ b/Utilities/Config.h @@ -25,7 +25,7 @@ namespace cfg std::vector make_float_range(f64 min, f64 max); // Internal hack - bool try_to_enum_value(u64* out, decltype(&fmt_class_string::format) func, std::string_view); + bool try_to_enum_value(u64* out, decltype(&fmt_class_string::format) func, std::string_view value, std::string_view name = {}); // Internal hack std::vector try_to_enum_list(decltype(&fmt_class_string::format) func); @@ -110,7 +110,7 @@ namespace cfg } // Try to convert from string (optional) - virtual bool from_string(std::string_view, bool /*dynamic*/ = false); + virtual bool from_string(std::string_view value, bool dynamic = false); // Get string list (optional) virtual std::vector to_list() const @@ -161,6 +161,9 @@ namespace cfg // Restore default members void restore_defaults() override; + + // Try to convert from string and validate + bool validate(std::string_view value); }; class _bool final : public _base @@ -301,7 +304,7 @@ namespace cfg { u64 result; - if (try_to_enum_value(&result, &fmt_class_string::format, value)) + if (try_to_enum_value(&result, &fmt_class_string::format, value, m_name)) { // No narrowing check, it's hard to do right there m_value = static_cast(static_cast>(result)); @@ -382,7 +385,7 @@ namespace cfg bool from_string(std::string_view value, bool /*dynamic*/ = false) override { s64 result; - if (try_to_int64(&result, value, Min, Max)) + if (try_to_int64(&result, value, Min, Max, m_name)) { m_value = static_cast(result); return true; @@ -451,7 +454,7 @@ namespace cfg std::string to_string() const override { std::string result; - if (try_to_string(&result, m_value)) + if (try_to_string(&result, m_value, m_name)) { return result; } @@ -462,7 +465,7 @@ namespace cfg std::string def_to_string() const override { std::string result; - if (try_to_string(&result, def)) + if (try_to_string(&result, def, m_name)) { return result; } @@ -473,7 +476,7 @@ namespace cfg bool from_string(std::string_view value, bool /*dynamic*/ = false) override { f64 result; - if (try_to_float(&result, value, Min, Max)) + if (try_to_float(&result, value, Min, Max, m_name)) { m_value = static_cast(result); return true; @@ -560,7 +563,7 @@ namespace cfg bool from_string(std::string_view value, bool /*dynamic*/ = false) override { u64 result; - if (try_to_uint64(&result, value, Min, Max)) + if (try_to_uint64(&result, value, Min, Max, m_name)) { m_value = static_cast(result); return true; @@ -646,7 +649,7 @@ namespace cfg bool from_string(std::string_view value, bool /*dynamic*/ = false) override { u128 result; - if (try_to_uint128(&result, value)) + if (try_to_uint128(&result, value, m_name)) { m_value = result; return true; diff --git a/Utilities/StrUtil.h b/Utilities/StrUtil.h index 66c351d60f..3fcfe98a8a 100644 --- a/Utilities/StrUtil.h +++ b/Utilities/StrUtil.h @@ -23,19 +23,19 @@ inline void strcpy_trunc(D&& dst, const T& src) } // Convert string to signed integer -bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max); +bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max, std::string_view name = {}); // Convert string to unsigned integer -bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max); +bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max, std::string_view name = {}); // Convert string to unsigned int128_t -bool try_to_uint128(u128* out, std::string_view value); +bool try_to_uint128(u128* out, std::string_view value, std::string_view name = {}); // Convert string to float -bool try_to_float(f64* out, std::string_view value, f64 min, f64 max); +bool try_to_float(f64* out, std::string_view value, f64 min, f64 max, std::string_view name = {}); // Convert float to string locale independent -bool try_to_string(std::string* out, const f64& value); +bool try_to_string(std::string* out, f64 value, std::string_view name = {}); // Get the file extension of a file path ("png", "jpg", etc.) std::string get_file_extension(const std::string& file_path); diff --git a/Utilities/Thread.cpp b/Utilities/Thread.cpp index 4ec70b58bb..57d7446daf 100644 --- a/Utilities/Thread.cpp +++ b/Utilities/Thread.cpp @@ -2883,6 +2883,13 @@ void thread_base::exec() } } +void thread_ctrl::set_name(std::string name) +{ + ensure(g_tls_this_thread); + g_tls_this_thread->m_tname.store(make_single(name)); + g_tls_this_thread->set_name(std::move(name)); +} + [[noreturn]] void thread_ctrl::emergency_exit(std::string_view reason) { // Print stacktrace diff --git a/Utilities/Thread.h b/Utilities/Thread.h index 7cd9a7c7ea..bafcea0b9f 100644 --- a/Utilities/Thread.h +++ b/Utilities/Thread.h @@ -129,7 +129,7 @@ public: const native_entry entry_point; // Set name for debugger - static void set_name(std::string); + static void set_name(std::string name); private: // Thread handle (platform-specific) @@ -232,11 +232,7 @@ public: } // Set current thread name (not recommended) - static void set_name(std::string name) - { - g_tls_this_thread->m_tname.store(make_single(name)); - g_tls_this_thread->set_name(std::move(name)); - } + static void set_name(std::string name); // Set thread name (not recommended) template diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index cfc2495f15..ba65a16eaf 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -187,6 +187,7 @@ if(BUILD_RPCS3_TESTS) tests/test_tuple.cpp tests/test_simple_array.cpp tests/test_address_range.cpp + tests/test_sys_fs.cpp tests/test_rsx_cfg.cpp tests/test_rsx_fp_asm.cpp tests/test_dmux_pamf.cpp diff --git a/rpcs3/Emu/Cell/Modules/cellGame.cpp b/rpcs3/Emu/Cell/Modules/cellGame.cpp index 51e5ed6a33..d7073a4e47 100644 --- a/rpcs3/Emu/Cell/Modules/cellGame.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGame.cpp @@ -520,10 +520,11 @@ error_code cellHddGameCheck(ppu_thread& ppu, u32 version, vm::cptr dirName return CELL_GAMEDATA_ERROR_PARAM; } - if (!fs::create_path(vfs::get(usrdir))) - { - return {CELL_GAME_ERROR_ACCESS_ERROR, usrdir}; - } + // Nuked until correctly reversed engineered + //if (!fs::create_path(vfs::get(usrdir))) + //{ + // return {CELL_GAME_ERROR_ACCESS_ERROR, usrdir}; + //} } // Nuked until correctly reversed engineered diff --git a/rpcs3/Emu/Cell/Modules/cellVdec.cpp b/rpcs3/Emu/Cell/Modules/cellVdec.cpp index 3205afd786..1850416ba3 100644 --- a/rpcs3/Emu/Cell/Modules/cellVdec.cpp +++ b/rpcs3/Emu/Cell/Modules/cellVdec.cpp @@ -1385,8 +1385,8 @@ error_code cellVdecGetPictureExt(ppu_thread& ppu, u32 handle, vm::cptrsws = sws_getCachedContext(vdec->sws, w, h, in_f, w, h, out_f, SWS_POINT, nullptr, nullptr, nullptr); - u8* in_data[4] = { frame->data[0], frame->data[1], frame->data[2], alpha_plane.get() }; - int in_line[4] = { frame->linesize[0], frame->linesize[1], frame->linesize[2], w * 1 }; + const u8* in_data[4] = { frame->data[0], frame->data[1], frame->data[2], alpha_plane.get() }; + const int in_line[4] = { frame->linesize[0], frame->linesize[1], frame->linesize[2], w * 1 }; u8* out_data[4] = { outBuff.get_ptr() }; int out_line[4] = { w * 4 }; // RGBA32 or ARGB32 diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.cpp b/rpcs3/Emu/Cell/lv2/sys_fs.cpp index 2534f6a8c1..cfbab23419 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_fs.cpp @@ -14,7 +14,6 @@ #include "Emu/system_utils.hpp" #include "Emu/Cell/lv2/sys_process.h" -#include #include #include @@ -93,15 +92,22 @@ void fmt_class_string::format(std::string& out, u64 arg) bool has_fs_write_rights(std::string_view vpath) { // VSH has access to everything - if (g_ps3_process_info.has_root_perm()) - return true; + const bool has_root_perm = g_ps3_process_info.has_root_perm(); - const auto norm_vpath = lv2_fs_object::get_normalized_path(vpath); - const auto parent_dir = fs::get_parent_dir_view(norm_vpath); + const auto parent_dir = fs::get_parent_dir_view(vpath); + const auto [dev_root, trail] = lv2_fs_object::get_path_root_and_trail(parent_dir); // This is not exhaustive, PS3 has a unix filesystem with rights for each directory and files - // This is mostly meant to protect against games doing insane things(ie NPUB30003 => NPUB30008) - if (parent_dir == "/dev_hdd0" || parent_dir == "/dev_hdd0/game") + // This is mostly meant to protect against games doing insane things (ie NPUB30003 => NPUB30008) + if (dev_root == "dev_hdd0"sv && (trail.empty() || trail == "game"sv)) + return has_root_perm; + + // This is read-only for games + if (dev_root.starts_with("dev_flash"sv)) + return has_root_perm; + + // Technically should not reach here, but handle it anyways + if (dev_root == "dev_bdvd"sv || dev_root == "dev_ps2disc"sv || dev_root.empty()) return false; return true; @@ -205,27 +211,29 @@ bool lv2_fs_mount_info_map::remove(std::string_view path) const lv2_fs_mount_info& lv2_fs_mount_info_map::lookup(std::string_view path, bool no_cell_fs_path, std::string* mount_path) const { - if (path.starts_with("/"sv)) + const auto [dev_root, trail] = lv2_fs_object::get_path_root_and_trail(path); + + if (dev_root.empty()) + { + if (trail.empty()) + { + return map.find("/")->second; + } + + return g_mi_sys_not_found; + } + + if (const auto iterator = map.find("/" + std::string{dev_root}); iterator != map.end()) { constexpr std::string_view cell_fs_path = "CELL_FS_PATH:"sv; - const std::string normalized_path = lv2_fs_object::get_normalized_path(path); - std::string_view parent_dir; - u32 parent_level = 0; - do - { - parent_dir = fs::get_parent_dir_view(normalized_path, parent_level++); - if (const auto iterator = map.find(parent_dir); iterator != map.end()) - { - if (iterator->second == &g_mp_sys_dev_root && parent_level > 1) - break; - if (no_cell_fs_path && iterator->second.device.starts_with(cell_fs_path)) - return lookup(iterator->second.device.substr(cell_fs_path.size()), no_cell_fs_path, mount_path); // Recursively look up the parent mount info - if (mount_path) - *mount_path = iterator->first; - return iterator->second; - } - } while (parent_dir.length() > 1); // Exit the loop when parent_dir == "/" or empty + if (no_cell_fs_path && iterator->second.device.starts_with(cell_fs_path)) + return lookup(iterator->second.device.substr(cell_fs_path.size()), no_cell_fs_path, mount_path); // Recursively look up the parent mount info + + if (mount_path) + *mount_path = iterator->first; + + return iterator->second; } return g_mi_sys_not_found; @@ -287,36 +295,89 @@ bool lv2_fs_mount_info_map::vfs_unmount(std::string_view vpath, bool remove_from return result; } -std::string lv2_fs_object::get_normalized_path(std::string_view path) +std::pair lv2_fs_object::get_path_root_and_trail(std::string_view filename) { - std::string normalized_path = std::filesystem::path(path).lexically_normal().string(); - -#ifdef _WIN32 - std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/'); -#endif - - if (normalized_path.ends_with('/')) - normalized_path.pop_back(); - - return normalized_path.empty() ? "/" : normalized_path; -} - -std::string lv2_fs_object::get_device_root(std::string_view filename) -{ - std::string path = get_normalized_path(filename); // Prevent getting fooled by ".." trick such as "/dev_usb000/../dev_flash" - - if (const auto first = path.find_first_not_of("/"sv); first != umax) + if (filename.empty()) { - if (const auto pos = path.substr(first).find_first_of("/"sv); pos != umax) - path = path.substr(0, first + pos); - path = path.substr(std::max>(0, first - 1)); // Remove duplicate leading '/' while keeping only one - } - else - { - path = path.substr(0, 1); + // Should CELL_ENOENT later - root cannot have a trail + return {""sv, "ENOENT"}; } - return path; + std::string_view root; + std::string trail; + + usz level = 0; + usz pos = 0; + + while (pos != umax) + { + const usz ndl_pos = filename.find_first_not_of("/", pos); + + if (ndl_pos == pos) + { + // Should CELL_ENOENT later - root cannot have a trail + return {""sv, "ENOENT"}; + } + + if (ndl_pos == umax) + { + break; + } + + const usz dl_pos = ndl_pos == umax ? usz{umax} : filename.find_first_of("/", ndl_pos); + std::string_view component = filename.substr(ndl_pos, dl_pos - ndl_pos); + + if (component == "."sv) + { + // No change + // level += 0; + pos = dl_pos; + continue; + } + + if (component == ".."sv) + { + if (level > 1) + { + ensure(!trail.empty()); + trail.resize(trail.find_last_of("/") + 1); + trail.resize(trail.find_last_not_of("/") + 1); + } + else if (level == 1) + { + // Reset root + root = {}; + } + else//if (level == 0) + { + // Should CELL_ENOENT later - root cannot have a trail + return {""sv, "ENOENT"}; + } + + ensure(level)--; + pos = dl_pos; + continue; + } + + if (level == 0) + { + root = component; + } + else if (trail.empty()) + { + trail = std::string{component}; + } + else + { + trail += "/"; + trail.append(component); + } + + level++; + pos = dl_pos; + } + + return { root, std::move(trail) }; } lv2_fs_mount_point* lv2_fs_object::get_mp(std::string_view filename, std::string* vfs_path) @@ -328,7 +389,7 @@ lv2_fs_mount_point* lv2_fs_object::get_mp(std::string_view filename, std::string filename.remove_prefix(cell_fs_path.size()); const bool is_path = filename.starts_with("/"sv); - std::string mp_name = is_path ? get_device_root(filename) : std::string(filename); + std::string mp_name = is_path ? std::string{get_path_root_and_trail(filename).first} : std::string(filename); const auto check_mp = [&]() { @@ -1405,6 +1466,10 @@ error_code sys_fs_opendir(ppu_thread& ppu, vm::cptr path, vm::ptr fd) break; } + case fs::error::notdir: + { + return { CELL_ENOTDIR, path }; + } default: { if (has_non_directory_components(local_path)) @@ -3398,7 +3463,7 @@ error_code sys_fs_mount(ppu_thread& ppu, vm::cptr dev_name, vm::cptr return {path_error, path_sv}; } - const std::string vpath = lv2_fs_object::get_normalized_path(path_sv); + const auto [root_name, trail] = lv2_fs_object::get_path_root_and_trail(path_sv); std::string vfs_path; const auto mp = lv2_fs_object::get_mp(device_name, &vfs_path); @@ -3416,8 +3481,8 @@ error_code sys_fs_mount(ppu_thread& ppu, vm::cptr dev_name, vm::cptr if (vfs_path.empty()) return {CELL_ENOTSUP, device_name}; - if (vpath.find_first_not_of('/') == umax || !vfs::get(vpath).empty()) - return {CELL_EEXIST, vpath}; + if (root_name.empty() || !vfs::get(path_sv).empty()) + return {CELL_EEXIST, path_sv}; if (mp == &g_mp_sys_dev_hdd1) { @@ -3452,7 +3517,7 @@ error_code sys_fs_mount(ppu_thread& ppu, vm::cptr dev_name, vm::cptr } } - if (!vfs::mount(vpath, vfs_path, !is_simplefs)) + if (!vfs::mount("/" + std::string{root_name}, vfs_path, !is_simplefs)) { if (is_simplefs) { @@ -3469,7 +3534,7 @@ error_code sys_fs_mount(ppu_thread& ppu, vm::cptr dev_name, vm::cptr return CELL_EIO; } - g_fxo->get().add(vpath, mp, device_name, filesystem, prot); + g_fxo->get().add("/" + std::string{root_name}, mp, device_name, filesystem, prot); return CELL_OK; } diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.h b/rpcs3/Emu/Cell/lv2/sys_fs.h index e64a2b4edb..c78ad7b5a2 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.h +++ b/rpcs3/Emu/Cell/lv2/sys_fs.h @@ -4,6 +4,7 @@ #include "Emu/Cell/ErrorCodes.h" #include "Utilities/File.h" #include "Utilities/StrUtil.h" +#include "Utilities/mutex.h" #include @@ -245,11 +246,15 @@ public: lv2_fs_object& operator=(const lv2_fs_object&) = delete; - // Normalize a virtual path - static std::string get_normalized_path(std::string_view path); + // Get the device's root path (e.g. "/dev_hdd0") from a given path + // Cut the trail and return it in seccond argument + static std::pair get_path_root_and_trail(std::string_view path); // Get the device's root path (e.g. "/dev_hdd0") from a given path - static std::string get_device_root(std::string_view filename); + static std::string get_device_root(std::string_view filename) + { + return std::string{get_path_root_and_trail(filename).first}; + } // Filename can be either a path starting with '/' or a CELL_FS device name // This should be used only when handling devices that are not mounted diff --git a/rpcs3/Emu/Cell/lv2/sys_process.cpp b/rpcs3/Emu/Cell/lv2/sys_process.cpp index b914408ec9..4ddf3720fd 100644 --- a/rpcs3/Emu/Cell/lv2/sys_process.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_process.cpp @@ -473,7 +473,7 @@ void lv2_exitspawn(ppu_thread& ppu, std::vector& argv, std::vector< }; Emu.after_kill_callback = [func = std::move(func), argv = std::move(argv), envp = std::move(envp), data = std::move(data), - disc = std::move(disc), path = std::move(path), hdd1 = std::move(hdd1), old_config = Emu.GetUsedConfig(), klic]() mutable + disc = std::move(disc), path = std::move(path), hdd1 = std::move(hdd1), old_config = Emu.GetUsedConfig(), old_db_config = Emu.GetUsedDatabaseConfig(), klic]() mutable { Emu.argv = std::move(argv); Emu.envp = std::move(envp); @@ -489,7 +489,7 @@ void lv2_exitspawn(ppu_thread& ppu, std::vector& argv, std::vector< Emu.SetForceBoot(true); - auto res = Emu.BootGame(path, "", true, cfg_mode::continuous, old_config); + auto res = Emu.BootGame(path, "", true, cfg_mode::continuous, old_config, old_db_config); if (res != game_boot_result::no_errors) { diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index b79eb5a52a..520f4c1be9 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -159,6 +159,7 @@ void fmt_class_string::format(std::string& out, u64 arg) case game_boot_result::still_running: return "Game is still running"; case game_boot_result::already_added: return "Game was already added"; case game_boot_result::currently_restricted: return "Booting is restricted at the time being"; + case game_boot_result::database_config_missing: return "Could not find config in database"; } return unknown; }); @@ -173,7 +174,7 @@ void fmt_class_string::format(std::string& out, u64 arg) { case cfg_mode::custom: return "custom config"; case cfg_mode::custom_selection: return "custom config selection"; - case cfg_mode::global: return "global config"; + case cfg_mode::database_config: return "database config"; case cfg_mode::config_override: return "config override"; case cfg_mode::continuous: return "continuous config"; case cfg_mode::default_config: return "default config"; @@ -932,14 +933,14 @@ game_boot_result Emulator::GetElfPathFromDir(std::string& elf_path, const std::s return game_boot_result::invalid_file_or_folder; } -game_boot_result Emulator::BootGame(const std::string& path, const std::string& title_id, bool direct, cfg_mode config_mode, const std::string& config_path) +game_boot_result Emulator::BootGame(const std::string& path, const std::string& title_id, bool direct, cfg_mode config_mode, const std::string& config_path, const std::optional& db_config) { if (m_restrict_emu_state_change) { return game_boot_result::currently_restricted; } - auto save_args = std::make_tuple(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path); + auto save_args = std::make_tuple(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config); auto restore_on_no_boot = [&](game_boot_result result) { @@ -949,7 +950,7 @@ game_boot_result Emulator::BootGame(const std::string& path, const std::string& if (m_state == system_state::stopped) { - std::tie(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path) = std::move(save_args); + std::tie(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config) = std::move(save_args); if (result != game_boot_result::no_errors) { @@ -964,7 +965,7 @@ game_boot_result Emulator::BootGame(const std::string& path, const std::string& // Execute after Kill() is done Emu.after_kill_callback = [this, result, save_args = std::move(save_args)]() mutable { - std::tie(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path) = std::move(save_args); + std::tie(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config) = std::move(save_args); if (result != game_boot_result::no_errors) { @@ -981,6 +982,7 @@ game_boot_result Emulator::BootGame(const std::string& path, const std::string& m_config_mode = config_mode; m_config_path = config_path; + m_db_config = db_config; // Handle files and special paths inside Load unmodified if (direct || !fs::is_dir(path)) @@ -1563,6 +1565,24 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, sys_log.notice("Version: APP_VER=%s VERSION=%s", version_app, version_disc); { + if (m_config_mode == cfg_mode::database_config || m_config_mode == cfg_mode::custom) + { + if (!m_db_config) + { + // Get database config if possible. This only happens if the database config hasn't been set by the UI (e.g. if booted with no-gui). + // We only know the title_id for sure at this point, so it doesn't make sense to retrieve it earlier. + m_db_config = Emu.GetCallbacks().get_database_config(m_title_id); + } + + // We add the database configuration if it is set, unless we are using a mode that specifically selects a different configuration. + m_add_database_config = m_db_config && !m_db_config->empty(); + } + else if (m_config_mode != cfg_mode::continuous) + { + // Reset flag unless in continuous mode + m_add_database_config = false; + } + if (m_config_mode == cfg_mode::custom_selection || (m_config_mode == cfg_mode::continuous && !m_config_path.empty())) { if (fs::file cfg_file{ m_config_path }) @@ -1605,6 +1625,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, { g_cfg.name = config_path; m_config_path = config_path; + m_add_database_config = false; // A custom config exists. Do not add the database config. break; } @@ -1613,6 +1634,21 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } } + if (m_add_database_config && m_db_config && !m_db_config->empty()) + { + // Add database config + sys_log.notice("Applying database config"); + + if (g_cfg.from_string(*m_db_config)) + { + g_cfg.name = "database_config"; + } + else + { + sys_log.error("Failed to apply database config"); + } + } + // Disable incompatible settings fixup_settings(&_psf); @@ -3342,6 +3378,25 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s return; } + const auto reset_emu_state = [this]() + { + m_ar.reset(); + argv.clear(); + envp.clear(); + data.clear(); + disc.clear(); + klic.clear(); + hdd1.clear(); + init_mem_containers = nullptr; + m_db_config = std::nullopt; + m_config_path.clear(); + m_config_mode = cfg_mode::custom; + read_used_savestate_versions(); + m_savestate_extension_flags1 = {}; + m_emu_state_close_pending = false; + m_precompilation_option = {}; + }; + if (system_state old_state = m_state.fetch_op([](system_state& state) { if (state == system_state::stopping || state == system_state::stopped) @@ -3360,21 +3415,8 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s } // Ensure clean state - m_ar.reset(); - argv.clear(); - envp.clear(); - data.clear(); - disc.clear(); - klic.clear(); - hdd1.clear(); - init_mem_containers = nullptr; + reset_emu_state(); after_kill_callback = nullptr; - m_config_path.clear(); - m_config_mode = cfg_mode::custom; - read_used_savestate_versions(); - m_savestate_extension_flags1 = {}; - m_emu_state_close_pending = false; - m_precompilation_option = {}; // Enable logging rpcs3::utils::configure_logs(true); @@ -3422,7 +3464,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s // There is no race condition because it is only accessed by the same thread std::shared_ptr> join_thread = std::make_shared>(); - *join_thread = make_ptr(new named_thread("Emulation Join Thread"sv, [join_thread, savestate, allow_autoexit, save_stage = save_stage ? *save_stage : savestate_stage{}, this]() mutable + *join_thread = make_ptr(new named_thread("Emulation Join Thread"sv, [join_thread, reset_emu_state, savestate, allow_autoexit, save_stage = save_stage ? *save_stage : savestate_stage{}, this]() mutable { fs::pending_file file; @@ -3921,7 +3963,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s set_progress_message("Resetting Objects"); // Final termination from main thread (move the last ownership of join thread in order to destroy it) - CallFromMainThread([join_thread = std::move(join_thread), verbose_message, stop_watchdog, init_mtx, allow_autoexit, this]() + CallFromMainThread([join_thread = std::move(join_thread), reset_emu_state, verbose_message, stop_watchdog, init_mtx, allow_autoexit, this]() { cpu_thread::cleanup(); @@ -3967,20 +4009,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s m_stop_ctr.notify_all(); // Boot arg cleanup (preserved in the case restarting) - argv.clear(); - envp.clear(); - data.clear(); - disc.clear(); - klic.clear(); - hdd1.clear(); - init_mem_containers = nullptr; - m_config_path.clear(); - m_config_mode = cfg_mode::custom; - m_ar.reset(); - read_used_savestate_versions(); - m_savestate_extension_flags1 = {}; - m_emu_state_close_pending = false; - m_precompilation_option = {}; + reset_emu_state(); if (!m_continuous_mode) { @@ -4004,7 +4033,9 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s if (allow_autoexit) { - Quit(g_cfg.misc.autoexit.get()); + const bool autoexit = g_cfg.misc.autoexit.get(); + sys_log.notice("Quit with main_window::closeEvent. (autoexit=%d)", autoexit); + Quit(autoexit); } if (after_kill_callback) @@ -4055,14 +4086,14 @@ game_boot_result Emulator::Restart(bool graceful, bool reset_path) if (!IsStopped()) { - auto save_args = std::make_tuple(argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path); + auto save_args = std::make_tuple(argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config); if (graceful) GracefulShutdown(false, false); else Kill(false); - std::tie(argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path) = std::move(save_args); + std::tie(argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config) = std::move(save_args); } else { diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 58d9e66ef4..99c570b809 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -58,6 +58,7 @@ enum class game_boot_result : u32 still_running, already_added, currently_restricted, + database_config_missing, }; constexpr bool is_error(game_boot_result res) @@ -114,6 +115,7 @@ struct EmuCallbacks std::function check_microphone_permissions; std::function()> make_video_source; std::function enable_gamemode; + std::function get_database_config; }; namespace utils @@ -145,6 +147,7 @@ class Emulator final cfg_mode m_config_mode = cfg_mode::custom; std::string m_config_path; + std::optional m_db_config; // std::nullopt means it has not been retrieved yet std::string m_path; std::string m_path_old; std::string m_path_original; @@ -169,6 +172,7 @@ class Emulator final bool m_continuous_mode = false; bool m_has_gui = true; + bool m_add_database_config = false; bool m_state_inspection_savestate = false; @@ -368,6 +372,12 @@ public: return m_config_path; } + const std::string& GetUsedDatabaseConfig() const + { + static std::string empty_db_config; + return m_db_config ? *m_db_config : empty_db_config; + } + bool IsChildProcess() const { return m_config_mode == cfg_mode::continuous; @@ -417,7 +427,7 @@ public: return emulation_state_guard_t{this}; } - game_boot_result BootGame(const std::string& path, const std::string& title_id = "", bool direct = false, cfg_mode config_mode = cfg_mode::custom, const std::string& config_path = ""); + game_boot_result BootGame(const std::string& path, const std::string& title_id = "", bool direct = false, cfg_mode config_mode = cfg_mode::custom, const std::string& config_path = "", const std::optional& db_config = std::nullopt); bool BootRsxCapture(const std::string& path); void SetForceBoot(bool force_boot); diff --git a/rpcs3/Emu/config_mode.h b/rpcs3/Emu/config_mode.h index 0ca006e0c8..1918def27c 100644 --- a/rpcs3/Emu/config_mode.h +++ b/rpcs3/Emu/config_mode.h @@ -4,7 +4,7 @@ enum class cfg_mode { custom, // Prefer regular custom config. Fall back to global config. custom_selection, // Use user-selected custom config. Fall back to global config. - global, // Use global config. + database_config, // Use database config. Fall back to global config. config_override, // Use config override. This does not use the global VFS settings! Fall back to global config. continuous, // Use same config as on last boot. Fall back to global config. default_config // Use the default values of the config entries. diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index befb64f282..ba52f14d5d 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -282,7 +282,7 @@ struct cfg_root : cfg::node cfg::_bool paint_move_spheres{this, "Paint move spheres", false, true}; cfg::_bool allow_move_hue_set_by_game{this, "Allow move hue set by game", false, true}; cfg::_bool lock_overlay_input_to_player_one{this, "Lock overlay input to player one", false, true}; - cfg::string midi_devices{this, "Emulated Midi devices", "ßßß@@@ßßß@@@ßßß@@@"}; + cfg::string midi_devices{this, "Emulated Midi devices", "Keyboardßßß@@@Keyboardßßß@@@Keyboardßßß@@@"}; cfg::_bool load_sdl_mappings{ this, "Load SDL GameController Mappings", true }; cfg::_bool pad_debug_overlay{ this, "IO Debug overlay", false, true }; cfg::_bool mouse_debug_overlay{ this, "Mouse Debug overlay", false, true }; diff --git a/rpcs3/Input/keyboard_pad_handler.cpp b/rpcs3/Input/keyboard_pad_handler.cpp index 5e93ce64bd..d48b857ccf 100644 --- a/rpcs3/Input/keyboard_pad_handler.cpp +++ b/rpcs3/Input/keyboard_pad_handler.cpp @@ -788,7 +788,7 @@ std::vector keyboard_pad_handler::list_devices() std::string keyboard_pad_handler::GetMouseName(const QMouseEvent* event) { - return GetMouseName(event->button()); + return GetMouseName(static_cast(mouse::button) + static_cast(event->button())); } std::string keyboard_pad_handler::GetMouseName(u32 button) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 8ec93ec8c7..0dd50501b6 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -199,7 +199,7 @@ bool iso_file_decryption::init(const std::string& path) const u32 region_count = char_arr_BE_to_uint(sec0_sec1.data()); // Ensure the region count is a proper value - if (region_count < 1 || region_count > 31) // It's non-PS3ISO + if (region_count < 1 || region_count > 127) // It's non-PS3ISO { iso_log.error("init: Failed to read region information: '%s' (region_count=%d)", path, region_count); return false; diff --git a/rpcs3/headless_application.cpp b/rpcs3/headless_application.cpp index 5d9240254d..cd8e027f57 100644 --- a/rpcs3/headless_application.cpp +++ b/rpcs3/headless_application.cpp @@ -58,7 +58,7 @@ void headless_application::InitializeCallbacks() on_exit(); } - sys_log.notice("Quitting headless application"); + sys_log.notice("Quitting headless application (force_quit=%d)", force_quit); quit(); return true; } diff --git a/rpcs3/main_application.cpp b/rpcs3/main_application.cpp index b5d4401b7e..8c067f3975 100644 --- a/rpcs3/main_application.cpp +++ b/rpcs3/main_application.cpp @@ -2,6 +2,8 @@ #include "main_application.h" #include "display_sleep_control.h" #include "gamemode_control.h" +#include "rpcs3qt/gui_settings.h" +#include "rpcs3qt/config_database.h" #include "util/types.hpp" #include "util/logs.hpp" @@ -409,5 +411,28 @@ EmuCallbacks main_application::CreateCallbacks() return path + suffix; }; + callbacks.get_database_config = [](const std::string& title_id) + { + if (title_id.empty()) + return std::string(); + + sys_log.notice("Trying to retrieve database config for: '%s'", title_id); + + const auto settings = std::make_shared(); + config_database config_db(settings, nullptr); + config_db.request_config_database(false); + + if (!config_db.has_config(title_id)) + return std::string(); + + if (const auto config = config_db.get_config(title_id)) + { + sys_log.notice("Found database config for: '%s'", title_id); + return config.value(); + } + + return std::string(); + }; + return callbacks; } diff --git a/rpcs3/rpcs3.plist.in b/rpcs3/rpcs3.plist.in index 93a4f2c186..137c76e087 100644 --- a/rpcs3/rpcs3.plist.in +++ b/rpcs3/rpcs3.plist.in @@ -28,8 +28,6 @@ Licensed under GPLv2 NSHighResolutionCapable - LSApplicationCategoryType - public.app-category.games LSMinimumSystemVersion 14.4 NSCameraUsageDescription diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index d25ca3905c..0181ce8c84 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -296,6 +296,9 @@ true + + true + true @@ -602,6 +605,9 @@ true + + true + true @@ -912,6 +918,7 @@ + @@ -1855,6 +1862,16 @@ "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing config_database.h... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing config_database.h... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) Moc%27ing game_compatibility.h... diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 28af783415..793e695d77 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -519,6 +519,15 @@ Gui\misc dialogs + + Generated Files\Debug + + + Generated Files\Release + + + Gui\game list + Generated Files\Debug @@ -1651,6 +1660,9 @@ Gui\save + + Gui\game list + Gui\game list diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index b0a75d9bfa..fdfb437fe7 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(rpcs3_ui STATIC clans_settings_dialog.cpp config_adapter.cpp config_checker.cpp + config_database.cpp curl_handle.cpp custom_dialog.cpp custom_table_widget_item.cpp diff --git a/rpcs3/rpcs3qt/config_checker.cpp b/rpcs3/rpcs3qt/config_checker.cpp index 6a0851362a..f8ff600205 100644 --- a/rpcs3/rpcs3qt/config_checker.cpp +++ b/rpcs3/rpcs3qt/config_checker.cpp @@ -1,76 +1,127 @@ #include "stdafx.h" #include "config_checker.h" +#include "midi_creator.h" +#include "microphone_creator.h" #include "Emu/system_config.h" +#include "Emu/system_utils.hpp" +#include #include #include #include -#include #include -#include LOG_CHANNEL(gui_log, "GUI"); -config_checker::config_checker(QWidget* parent, const QString& content, bool is_log) : QDialog(parent) +config_checker::config_checker(QWidget* parent, const QString& content_or_serial, checker_mode mode, const std::string& db_config) + : QDialog(parent) + , m_checker_mode(mode) + , m_content_or_serial(content_or_serial) + , m_db_config(db_config) { setObjectName("config_checker"); + setWindowTitle(tr("Config Checker")); setAttribute(Qt::WA_DeleteOnClose); QVBoxLayout* layout = new QVBoxLayout(); - QLabel* label = new QLabel(this); - layout->addWidget(label); + QComboBox* combo = nullptr; - QString result; - - if (check_config(content, result, is_log)) + if (mode == checker_mode::gamelist) { - setWindowTitle(tr("Interesting!")); + m_serial = content_or_serial.toStdString(); - if (result.isEmpty()) + combo = new QComboBox(this); + + std::string custom_config_path; + if (std::string config_path = rpcs3::utils::get_custom_config_path(m_serial); fs::is_file(config_path)) { - label->setText(tr("Found config.\nIt seems to match the default config.")); + custom_config_path = std::move(config_path); + combo->addItem(tr("Custom Configuration"), static_cast(cfg_mode::custom)); } - else + + combo->addItem(tr("Database + Global Configuration"), static_cast(cfg_mode::database_config)); + combo->setCurrentIndex(combo->findData(static_cast(custom_config_path.empty() ? cfg_mode::database_config : cfg_mode::custom))); + + connect(combo, &QComboBox::currentIndexChanged, this, [this, combo]() { - label->setText(tr("Found config.\nSome settings seem to deviate from the default config:")); + check_config(static_cast(combo->currentData().toInt())); + }); - QTextEdit* text_box = new QTextEdit(); - text_box->setReadOnly(true); - text_box->setHtml(result); - layout->addWidget(text_box); + layout->addWidget(combo); + } - resize(400, 600); - } - } - else - { - setWindowTitle(tr("Ooops!")); - label->setText(result); - } + m_label = new QLabel(this); + layout->addWidget(m_label); + + m_text_box = new QTextEdit(); + m_text_box->setReadOnly(true); + layout->addWidget(m_text_box); QDialogButtonBox* box = new QDialogButtonBox(QDialogButtonBox::Close); connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(box); setLayout(layout); + resize(400, 600); + + check_config(combo ? static_cast(combo->currentData().toInt()) : cfg_mode::database_config); } -bool config_checker::check_config(QString content, QString& result, bool is_log) +void config_checker::check_config(cfg_mode mode) { - cfg_root config{}; + QString result; - if (is_log) + if (check_config(mode, m_content_or_serial, result)) + { + if (m_checker_mode == checker_mode::gamelist) + { + if (result.isEmpty()) + { + m_label->setText(tr("The configuration seems to match the default config.")); + } + else + { + m_label->setText(tr("Config database settings are marked with an * in front of the name.\nSome settings seem to deviate from the default config:")); + } + } + else + { + if (result.isEmpty()) + { + m_label->setText(tr("Found config.\nIt seems to match the default config.")); + } + else + { + m_label->setText(tr("Found config.\nSome settings seem to deviate from the default config:")); + } + } + + m_text_box->setVisible(!result.isEmpty()); + m_text_box->setHtml(result); + } + else + { + m_label->setText(result); + } +} + +bool config_checker::check_config(cfg_mode mode, QString content_or_serial, QString& result) +{ + std::unique_ptr config = std::make_unique(); + std::unique_ptr config_db_only; + + if (m_checker_mode == checker_mode::log) { const QString start_token = "SYS: Used configuration:\n"; const QString end_token = "\n·"; - qsizetype start = content.indexOf(start_token); + qsizetype start = content_or_serial.indexOf(start_token); qsizetype end = -1; if (start >= 0) { start += start_token.size(); - end = content.indexOf(end_token, start); + end = content_or_serial.indexOf(end_token, start); } if (end < 0) @@ -79,24 +130,93 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) return false; } - content = content.mid(start, end - start); + content_or_serial = content_or_serial.mid(start, end - start); } - if (!config.from_string(content.toStdString())) + if (m_checker_mode == checker_mode::gamelist) { - gui_log.error("log_viewer: Failed to parse config:\n%s", content); + config->from_default(); + + // Load global config + const std::string cfg_path = fs::get_config_dir(true) + "config.yml"; + if (const fs::file cfg_file{cfg_path}) + { + gui_log.notice("config_checker: Applying global config: %s", cfg_path); + + if (!config->from_string(cfg_file.to_string())) + { + gui_log.error("config_checker: Failed to apply global config: %s", cfg_path); + result = tr("Failed to apply global config!"); + return false; + } + } + + // Load custom config + const std::string custom_config_path = rpcs3::utils::get_custom_config_path(m_serial); + if (mode == cfg_mode::custom && !custom_config_path.empty()) + { + if (const fs::file cfg_file{custom_config_path}) + { + gui_log.notice("config_checker: Applying custom config: %s", custom_config_path); + + if (!config->from_string(cfg_file.to_string())) + { + gui_log.error("config_checker: Failed to apply custom config: %s", custom_config_path); + result = tr("Failed to apply custom config!"); + return false; + } + } + } + + if (mode == cfg_mode::database_config && !m_db_config.empty()) + { + gui_log.notice("config_checker: Applying database config: %s", custom_config_path); + + if (!config->from_string(m_db_config)) + { + gui_log.error("config_checker: Failed to apply database config:\n%s", m_db_config); + result = tr("Failed to apply database config!"); + return false; + } + + config_db_only = std::make_unique(); + config_db_only->from_default(); + if (!config_db_only->from_string(m_db_config)) + { + gui_log.error("config_checker: Failed to apply database config:\n%s", m_db_config); + result = tr("Failed to apply database config!"); + return false; + } + } + } + else if (!config->from_string(content_or_serial.toStdString())) + { + gui_log.error("config_checker: Failed to parse config:\n%s", content_or_serial); result = tr("Cannot find any config!"); return false; } - std::function print_diff_recursive; - print_diff_recursive = [&print_diff_recursive](const cfg::_base* base, std::string& diff, int indentation) -> void + std::function print_diff_recursive; + print_diff_recursive = [this, &print_diff_recursive, &config](const cfg::_base* base, const cfg::_base* base_db_only, std::string& diff, int indentation) -> void { if (!base) { return; } + // Ignore some irrelevant settings in gamelist mode + if (m_checker_mode == checker_mode::gamelist && base->get_type() != cfg::type::node) + { + const std::string key = base->get_name(); + + if (key == config->sys.console_psid.get_name() || + key == config->sys.system_name.get_name() || + key == config->video.vk.adapter.get_name()) + { + return; + } + } + const auto indent = [](std::string& str, int indentation) { for (int i = 0; i < indentation * 2; i++) @@ -105,6 +225,16 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) } }; + const auto base_name_db = [base](bool is_db_config) + { + if (is_db_config) + { + return "*" + base->get_name(); + } + + return base->get_name(); + }; + switch (base->get_type()) { case cfg::type::node: @@ -115,7 +245,20 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) for (const auto& n : node->get_nodes()) { - print_diff_recursive(n, diff_tmp, indentation + 1); + const cfg::_base* n_db_only = nullptr; + if (const auto& node_db_only = static_cast(base_db_only)) + { + for (const auto& n_db : node_db_only->get_nodes()) + { + if (n_db->get_name() == n->get_name()) + { + n_db_only = n_db; + break; + } + } + } + + print_diff_recursive(n, n_db_only, diff_tmp, indentation + 1); } if (!diff_tmp.empty()) @@ -142,19 +285,75 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) const std::string val = base->to_string(); const std::string def = base->def_to_string(); - if (val != def) - { - indent(diff, indentation); + if (val == def) + break; - if (def.empty()) + indent(diff, indentation); + + if (m_checker_mode == checker_mode::gamelist) + { + if (base->get_name() == config->io.midi_devices.get_name()) { - fmt::append(diff, "%s: %s
", base->get_name(), val); + fmt::append(diff, "%s:
", base->get_name()); + + midi_creator mc {}; + + mc.parse_devices(def); + const std::array def_devices = mc.get_selection_list(); + + mc.parse_devices(val); + const std::array devices = mc.get_selection_list(); + + for (usz i = 0; i < devices.size(); i++) + { + const midi_device& def_device = def_devices[i]; + const midi_device& device = devices[i]; + + if (device.name == def_device.name) + continue; + + indent(diff, indentation + 1); + fmt::append(diff, "Device %d: %s: %s
", i + 1, device.type, device.name); + } + break; } - else + else if (base->get_name() == config->audio.microphone_devices.get_name()) { - fmt::append(diff, "%s: %s default: %s
", base->get_name(), val, def); + fmt::append(diff, "%s:
", base->get_name()); + + microphone_creator mc {}; + + mc.parse_devices(def); + const std::array def_devices = mc.get_selection_list(); + + mc.parse_devices(val); + const std::array devices = mc.get_selection_list(); + + for (usz i = 0; i < devices.size(); i++) + { + const std::string& def_device = def_devices[i]; + const std::string& device = devices[i]; + + if (device == def_device) + continue; + + indent(diff, indentation + 1); + fmt::append(diff, "Device %d: %s
", i + 1, device); + } + break; } } + + const bool is_db_config = base_db_only && base_db_only->to_string() != def; + + if (def.empty()) + { + fmt::append(diff, "%s: %s
", base_name_db(is_db_config), val); + } + else + { + fmt::append(diff, "%s: %s default: %s
", base_name_db(is_db_config), val, def); + } break; } case cfg::type::set: @@ -208,7 +407,7 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) }; std::string diff; - print_diff_recursive(&config, diff, 0); + print_diff_recursive(config.get(), config_db_only.get(), diff, 0); result = QString::fromStdString(diff); return true; diff --git a/rpcs3/rpcs3qt/config_checker.h b/rpcs3/rpcs3qt/config_checker.h index 900fb30351..71b4a27de3 100644 --- a/rpcs3/rpcs3qt/config_checker.h +++ b/rpcs3/rpcs3qt/config_checker.h @@ -1,13 +1,34 @@ #pragma once +#include "Emu/config_mode.h" + #include +#include +#include class config_checker : public QDialog { Q_OBJECT public: - config_checker(QWidget* parent, const QString& path, bool is_log); + enum class checker_mode + { + config, + log, + gamelist + }; - bool check_config(QString content, QString& result, bool is_log); + config_checker(QWidget* parent, const QString& content_or_serial, checker_mode mode, const std::string& db_config = {}); + +private: + void check_config(cfg_mode mode); + bool check_config(cfg_mode mode, QString content_or_serial, QString& result); + + QLabel* m_label = nullptr; + QTextEdit* m_text_box = nullptr; + + checker_mode m_checker_mode = checker_mode::config; + QString m_content_or_serial; + std::string m_db_config; + std::string m_serial; }; diff --git a/rpcs3/rpcs3qt/config_database.cpp b/rpcs3/rpcs3qt/config_database.cpp new file mode 100644 index 0000000000..7e26252d3f --- /dev/null +++ b/rpcs3/rpcs3qt/config_database.cpp @@ -0,0 +1,228 @@ +#include "stdafx.h" +#include "config_database.h" +#include "gui_settings.h" +#include "downloader.h" +#include "Emu/system_config.h" + +LOG_CHANNEL(gui_log, "GUI"); + +config_database::config_database(std::shared_ptr settings, QWidget* parent) + : QObject(parent) + , m_gui_settings(std::move(settings)) +{ + m_filepath = m_gui_settings->GetSettingsDir() + "/config_database.dat"; + m_downloader = new downloader(parent); + request_config_database(); + + connect(m_downloader, &downloader::signal_download_error, this, &config_database::handle_download_error); + connect(m_downloader, &downloader::signal_download_finished, this, &config_database::handle_download_finished); + connect(m_downloader, &downloader::signal_download_canceled, this, &config_database::handle_download_canceled); +} + +config_database::~config_database() +{ +} + +bool config_database::has_config(const std::string& title_id) const +{ + return m_config_database.contains(title_id); +} + +std::optional config_database::get_config(const std::string& title_id) +{ + if (!m_config_database.contains(title_id)) + { + gui_log.error("Config database does not contain '%s'", title_id); + return std::nullopt; + } + + QFile file(m_filepath); + + if (!file.exists()) + { + gui_log.error("Config database file not found: %s", m_filepath); + return std::nullopt; + } + + if (!file.open(QIODevice::ReadOnly)) + { + gui_log.error("Config database error - Could not read database from file: %s", m_filepath); + return std::nullopt; + } + + const QByteArray content = file.readAll(); + file.close(); + + return read_json(content, false, title_id); +} + +void config_database::request_config_database(bool online) +{ + if (!online) + { + // Retrieve database from file + QFile file(m_filepath); + + if (!file.exists()) + { + gui_log.notice("Config database file not found: %s", m_filepath); + return; + } + + if (!file.open(QIODevice::ReadOnly)) + { + gui_log.error("Config database error - Could not read database from file: %s", m_filepath); + return; + } + + const QByteArray content = file.readAll(); + file.close(); + + gui_log.notice("Finished reading config database from file: %s", m_filepath); + + // Create new set from database + read_json(content, online); + + return; + } + + const std::string url = "https://api.rpcs3.net/config/?api=v1"; + gui_log.notice("Beginning config database download from: %s", url); + + m_downloader->start(url, true, true, true, tr("Downloading Config Database")); + + Q_EMIT download_started(); +} + +void config_database::handle_download_error(const QString& error) +{ + Q_EMIT download_error(error); +} + +void config_database::handle_download_finished(const QByteArray& content) +{ + gui_log.notice("Config database download finished"); + + // Create new map from database and write database to file if database was valid + if (read_json(content, true)) + { + // Write database to file + QFile file(m_filepath); + + if (file.exists()) + { + gui_log.notice("Config database file found: %s", m_filepath); + } + + if (!file.open(QIODevice::WriteOnly)) + { + gui_log.error("Config database error - Could not write database to file: %s", m_filepath); + return; + } + + file.write(content); + file.close(); + + gui_log.success("Wrote config database to file: %s", m_filepath); + } + + Q_EMIT download_finished(); +} + +void config_database::handle_download_canceled() +{ + Q_EMIT download_canceled(); +} + +std::optional config_database::read_json(const QByteArray& data, bool after_download, const std::string& serial) +{ + QJsonParseError error {}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + gui_log.error("Config database error - Invalid JSON: '%s'", error.errorString()); + return std::nullopt; + } + + const QJsonObject json_data = json_document.object(); + const int return_code = json_data["return_code"].toInt(-255); + + if (return_code < 0) + { + if (after_download) + { + std::string error_message; + switch (return_code) + { + case -1: error_message = "Server Error - Internal Error"; break; + case -2: error_message = "Server Error - Maintenance Mode"; break; + case -255: error_message = "Server Error - Return code not found"; break; + default: error_message = "Server Error - Unknown Error"; break; + } + gui_log.error("%s: return code %d", error_message, return_code); + Q_EMIT download_error(QString::fromStdString(error_message) + " " + QString::number(return_code)); + } + else + { + gui_log.error("Config database error - Invalid: return code %d", return_code); + } + return std::nullopt; + } + + if (!json_data["games"].isObject()) + { + gui_log.error("Config database error - No games found"); + return std::nullopt; + } + + std::unique_ptr config = std::make_unique(); + + const QJsonObject json_games = json_data["games"].toObject(); + + const auto validate = [&json_games, &config](const QString& serial) -> std::optional + { + if (!json_games[serial].isObject()) + { + gui_log.error("Config database error - Unusable object %s", serial); + return std::nullopt; + } + + const QJsonObject game = json_games[serial].toObject(); + if (!game["config"].isString()) + { + gui_log.error("Config database error - Unusable game string %s (config missing)", serial); + return std::nullopt; + } + + const std::string content = game["config"].toString().toStdString(); + + // Verify config + if (!config->validate(content)) + { + gui_log.error("Config database error - Invalid config for %s", serial); + return std::nullopt; + } + + return content; + }; + + if (serial.empty()) + { + m_config_database.clear(); + + // Retrieve status data for every valid entry + for (const QString& serial : json_games.keys()) + { + if (validate(serial)) + { + // Add title to set + m_config_database.insert(serial.toStdString()); + } + } + + return std::string(); + } + + return validate(QString::fromStdString(serial)); +} diff --git a/rpcs3/rpcs3qt/config_database.h b/rpcs3/rpcs3qt/config_database.h new file mode 100644 index 0000000000..e0283ddf7a --- /dev/null +++ b/rpcs3/rpcs3qt/config_database.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +class downloader; +class gui_settings; + +class config_database : public QObject +{ + Q_OBJECT + +public: + config_database(std::shared_ptr settings, QWidget* parent); + virtual ~config_database(); + + bool has_config(const std::string& title_id) const; + std::optional get_config(const std::string& title_id); + + /** Reads database. If online set to true: Downloads and writes the database to file */ + void request_config_database(bool online = false); + +Q_SIGNALS: + void download_started(); + void download_finished(); + void download_canceled(); + void download_error(const QString& error); + +private Q_SLOTS: + void handle_download_error(const QString& error); + void handle_download_finished(const QByteArray& content); + void handle_download_canceled(); + +private: + /** Creates new set from the database. Returns config for the optional serial. */ + std::optional read_json(const QByteArray& data, bool after_download, const std::string& serial = ""); + + std::shared_ptr m_gui_settings; + QString m_filepath; + downloader* m_downloader = nullptr; + + std::set m_config_database; +}; diff --git a/rpcs3/rpcs3qt/downloader.cpp b/rpcs3/rpcs3qt/downloader.cpp index 9a5038fcc1..9bcdbf495a 100644 --- a/rpcs3/rpcs3qt/downloader.cpp +++ b/rpcs3/rpcs3qt/downloader.cpp @@ -103,7 +103,7 @@ void downloader::start(const std::string& url, bool follow_location, bool show_p { if (m_curl_abort) { - network_log.notice("Download aborted"); + network_log.notice("Download aborted (url='%s')", url); return; } @@ -114,7 +114,7 @@ void downloader::start(const std::string& url, bool follow_location, bool show_p if (m_curl_success) { - network_log.notice("Download finished"); + network_log.notice("Download finished (url='%s')", url); if (check_return_code && m_download_attempts < 3) { diff --git a/rpcs3/rpcs3qt/emu_settings.cpp b/rpcs3/rpcs3qt/emu_settings.cpp index 0ec017a02c..a5bd055432 100644 --- a/rpcs3/rpcs3qt/emu_settings.cpp +++ b/rpcs3/rpcs3qt/emu_settings.cpp @@ -117,7 +117,7 @@ bool emu_settings::Init() return true; } -void emu_settings::LoadSettings(const std::string& title_id, bool create_config_from_global) +void emu_settings::LoadSettings(const std::string& title_id, bool create_config_from_global, const std::string& db_config) { m_title_id = title_id; @@ -159,6 +159,22 @@ void emu_settings::LoadSettings(const std::string& title_id, bool create_config_ .arg(QString::fromStdString(global_config_path)).arg(QString::fromStdString(global_error)), QMessageBox::Ok); } } + else if (!db_config.empty()) + { + // Add database config + auto [config, error] = yaml_load(db_config); + + if (config && error.empty()) + { + m_current_settings += config; + } + else + { + cfg_log.fatal("Failed to load database config for '%s':\n%s", title_id, error); + QMessageBox::critical(nullptr, tr("Config Error"), tr("Failed to load database config:\nError: %1") + .arg(QString::fromStdString(error)), QMessageBox::Ok); + } + } // Add game config if (!title_id.empty()) diff --git a/rpcs3/rpcs3qt/emu_settings.h b/rpcs3/rpcs3qt/emu_settings.h index c5a1e89252..4a7f8ee40d 100644 --- a/rpcs3/rpcs3qt/emu_settings.h +++ b/rpcs3/rpcs3qt/emu_settings.h @@ -103,7 +103,7 @@ public: midi_creator m_midi_creator; /** Loads the settings from path.*/ - void LoadSettings(const std::string& title_id = "", bool create_config_from_global = true); + void LoadSettings(const std::string& title_id = "", bool create_config_from_global = true, const std::string& db_config = ""); /** Fixes all registered invalid settings after asking the user for permission.*/ void OpenCorrectionDialog(QWidget* parent = Q_NULLPTR); diff --git a/rpcs3/rpcs3qt/game_compatibility.cpp b/rpcs3/rpcs3qt/game_compatibility.cpp index a7a4a4bc34..7f408d39d3 100644 --- a/rpcs3/rpcs3qt/game_compatibility.cpp +++ b/rpcs3/rpcs3qt/game_compatibility.cpp @@ -13,9 +13,9 @@ LOG_CHANNEL(compat_log, "Compat"); -game_compatibility::game_compatibility(std::shared_ptr gui_settings, QWidget* parent) +game_compatibility::game_compatibility(std::shared_ptr settings, QWidget* parent) : QObject(parent) - , m_gui_settings(std::move(gui_settings)) + , m_gui_settings(std::move(settings)) { m_filepath = m_gui_settings->GetSettingsDir() + "/compat_database.dat"; m_downloader = new downloader(parent); @@ -58,7 +58,7 @@ void game_compatibility::handle_download_finished(const QByteArray& content) compat_log.success("Wrote database to file: %s", m_filepath); } - // We have a new database in map, therefore refresh gamelist to new state + // We have a new database in map, therefore refresh game list to new state Q_EMIT DownloadFinished(); } @@ -69,7 +69,16 @@ void game_compatibility::handle_download_canceled() bool game_compatibility::handle_json(const QByteArray& data, bool after_download) { - const QJsonObject json_data = QJsonDocument::fromJson(data).object(); + QJsonParseError error {}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + compat_log.error("Database Error - Invalid JSON: '%s'", error.errorString()); + return false; + } + + const QJsonObject json_data = json_document.object(); const int return_code = json_data["return_code"].toInt(-255); if (return_code < 0) @@ -220,11 +229,11 @@ void game_compatibility::RequestCompatibility(bool online) m_downloader->start(url, true, true, true, tr("Downloading Database")); - // We want to retrieve a new database, therefore refresh gamelist and indicate that + // We want to retrieve a new database, therefore refresh game list and indicate that Q_EMIT DownloadStarted(); } -compat::status game_compatibility::GetCompatibility(const std::string& title_id) +compat::status game_compatibility::GetCompatibility(const std::string& title_id) const { if (m_compat_database.empty()) { @@ -244,7 +253,7 @@ compat::status game_compatibility::GetStatusData(const QString& status) const return ::at32(Status_Data, status); } -compat::package_info game_compatibility::GetPkgInfo(const QString& pkg_path, game_compatibility* compat) +compat::package_info game_compatibility::GetPkgInfo(const QString& pkg_path, const game_compatibility* compat) { compat::package_info info; diff --git a/rpcs3/rpcs3qt/game_compatibility.h b/rpcs3/rpcs3qt/game_compatibility.h index 8dc1ce5aa5..7c4d1a80a1 100644 --- a/rpcs3/rpcs3qt/game_compatibility.h +++ b/rpcs3/rpcs3qt/game_compatibility.h @@ -148,13 +148,13 @@ public: void RequestCompatibility(bool online = false); /** Returns the compatibility status for the requested title */ - compat::status GetCompatibility(const std::string& title_id); + compat::status GetCompatibility(const std::string& title_id) const; /** Returns the data for the requested status */ compat::status GetStatusData(const QString& status) const; /** Returns package information like title, version, changelog etc. */ - static compat::package_info GetPkgInfo(const QString& pkg_path, game_compatibility* compat); + static compat::package_info GetPkgInfo(const QString& pkg_path, const game_compatibility* compat); Q_SIGNALS: void DownloadStarted(); diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 30b099fb9a..e1a78feb11 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -374,7 +374,7 @@ void game_list_actions::ShowDiskUsageDialog() // so run it on a concurrent thread avoiding to block the entire GUI m_disk_usage_future = QtConcurrent::run([this]() { - thread_ctrl::set_name("Disk Usage"); + thread_base::set_name("Disk Usage"); const std::vector> vfs_disk_usage = rpcs3::utils::get_vfs_disk_usage(); const u64 cache_disk_usage = rpcs3::utils::get_cache_disk_usage(); diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 81f6f6e973..f8a26d3f80 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -11,6 +11,8 @@ #include "pad_settings_dialog.h" #include "patch_manager_dialog.h" #include "persistent_settings.h" +#include "config_database.h" +#include "config_checker.h" #include "Utilities/File.h" #include "Emu/system_utils.hpp" @@ -67,8 +69,8 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& // Make Actions QAction* boot = new QAction(gameinfo->has_custom_config ? (is_current_running_game - ? tr("&Reboot with Global Configuration") - : tr("&Boot with Global Configuration")) + ? (gameinfo->has_database_config ? tr("&Reboot with Database + Global Configuration") : tr("&Reboot with Global Configuration")) + : (gameinfo->has_database_config ? tr("&Boot with Database + Global Configuration") : tr("&Boot with Global Configuration"))) : (is_current_running_game ? tr("&Reboot") : tr("&Boot"))); @@ -156,6 +158,8 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& addSeparator(); + QAction* create_game_database_config = (gameinfo->has_custom_config || !gameinfo->has_database_config) ? nullptr + : addAction(tr("&Create Custom Configuration From Database Settings")); QAction* configure = addAction(gameinfo->has_custom_config ? tr("&Change Custom Configuration") : tr("&Create Custom Configuration From Global Settings")); @@ -164,6 +168,26 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& QAction* pad_configure = addAction(gameinfo->has_custom_pad_config ? tr("&Change Custom Gamepad Configuration") : tr("&Create Custom Gamepad Configuration")); + + QAction* compare_config = addAction(tr("&Compare Configurations")); + connect(compare_config, &QAction::triggered, this, [this, serial]() + { + std::string db_config; + if (config_database* db = m_game_list_frame->GetConfigDatabase(); db->has_config(serial)) + { + if (const std::optional config = db->get_config(serial)) + { + db_config = *config; + } + else + { + game_list_log.error("No database config found for '%s'", serial); + } + } + config_checker* dlg = new config_checker(m_game_list_frame, QString::fromStdString(serial), config_checker::checker_mode::gamelist, db_config); + dlg->open(); + }); + QAction* configure_patches = addAction(tr("&Manage Game Patches")); addSeparator(); @@ -578,6 +602,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& QAction* check_compat = addAction(tr("&Check Game Compatibility")); QAction* download_compat = addAction(tr("&Download Compatibility Database")); + QAction* download_config_db = addAction(tr("&Download Config Database")); addSeparator(); @@ -598,12 +623,13 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& connect(boot, &QAction::triggered, m_game_list_frame, [this, gameinfo]() { sys_log.notice("Booting from gamelist per context menu..."); - Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::global); + Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::database_config); }); - auto configure_l = [this, current_game, gameinfo](bool create_cfg_from_global_cfg) + const auto configure_game = [this, current_game, gameinfo](bool create_cfg_from_global_cfg, bool create_cfg_from_database) { - settings_dialog dlg(m_gui_settings, m_emu_settings, 0, m_game_list_frame, ¤t_game, create_cfg_from_global_cfg); + const std::optional db_config = create_cfg_from_database ? m_game_list_frame->GetConfigDatabase()->get_config(gameinfo->info.serial) : ""; + settings_dialog dlg(m_gui_settings, m_emu_settings, 0, m_game_list_frame, ¤t_game, create_cfg_from_global_cfg, db_config ? *db_config : ""); connect(&dlg, &settings_dialog::EmuSettingsApplied, [this, gameinfo]() { @@ -618,14 +644,16 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& dlg.exec(); }; + connect(configure, &QAction::triggered, this, [configure_game]() { configure_game(true, false); }); + if (create_game_default_config) { - connect(configure, &QAction::triggered, m_game_list_frame, [configure_l]() { configure_l(true); }); - connect(create_game_default_config, &QAction::triggered, m_game_list_frame, [configure_l = std::move(configure_l)]() { configure_l(false); }); + connect(create_game_default_config, &QAction::triggered, m_game_list_frame, [configure_game]() { configure_game(false, false); }); } - else + + if (create_game_database_config) { - connect(configure, &QAction::triggered, m_game_list_frame, [configure_l = std::move(configure_l)]() { configure_l(true); }); + connect(create_game_database_config, &QAction::triggered, m_game_list_frame, [configure_game]() { configure_game(false, true); }); } connect(pad_configure, &QAction::triggered, m_game_list_frame, [this, current_game, gameinfo]() @@ -671,7 +699,11 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& }); connect(download_compat, &QAction::triggered, m_game_list_frame, [this] { - ensure(m_game_list_frame->GetGameCompatibility())->RequestCompatibility(true); + m_game_list_frame->GetGameCompatibility()->RequestCompatibility(true); + }); + connect(download_config_db, &QAction::triggered, m_game_list_frame, [this] + { + m_game_list_frame->GetConfigDatabase()->request_config_database(true); }); connect(rename_title, &QAction::triggered, m_game_list_frame, [this, name, serial = QString::fromStdString(serial), global_pos] { @@ -935,7 +967,13 @@ void game_list_context_menu::show_multi_selection_context_menu(const std::vector QAction* download_compat = addAction(tr("&Download Compatibility Database")); connect(download_compat, &QAction::triggered, m_game_list_frame, [this] { - ensure(m_game_list_frame->GetGameCompatibility())->RequestCompatibility(true); + m_game_list_frame->GetGameCompatibility()->RequestCompatibility(true); + }); + + QAction* download_config_db = addAction(tr("&Download Config Database")); + connect(download_config_db, &QAction::triggered, m_game_list_frame, [this] + { + m_game_list_frame->GetConfigDatabase()->request_config_database(true); }); addSeparator(); diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 726ab5a5fe..e3d2ece920 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -10,6 +10,7 @@ #include "game_list_table.h" #include "game_list_grid.h" #include "game_list_grid_item.h" +#include "config_database.h" #include "Emu/System.h" #include "Emu/vfs_config.h" @@ -74,6 +75,7 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std m_game_list->verticalScrollBar()->installEventFilter(this); m_game_compat = new game_compatibility(m_gui_settings, this); + m_config_db = new config_database(m_gui_settings, this); m_central_widget = new QStackedWidget(this); m_central_widget->addWidget(m_game_list); @@ -200,6 +202,22 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std QMessageBox::warning(this, tr("Warning!"), tr("Failed to retrieve the online compatibility database!\nFalling back to local database.\n\n%0").arg(error)); }); + connect(m_config_db, &config_database::download_started, this, [this]() + { + for (const auto& game : m_game_data) + { + game->has_database_config = false; + } + Refresh(); + }); + connect(m_config_db, &config_database::download_finished, this, &game_list_frame::OnConfigDatabaseFinished); + connect(m_config_db, &config_database::download_canceled, this, &game_list_frame::OnConfigDatabaseFinished); + connect(m_config_db, &config_database::download_error, this, [this](const QString& error) + { + OnConfigDatabaseFinished(); + QMessageBox::warning(this, tr("Warning!"), tr("Failed to retrieve the online config database!\nFalling back to local database.\n\n%0").arg(error)); + }); + connect(m_game_list, &game_list::FocusToSearchBar, this, &game_list_frame::FocusToSearchBar); connect(m_game_grid, &game_list_grid::FocusToSearchBar, this, &game_list_frame::FocusToSearchBar); @@ -804,6 +822,7 @@ void game_list_frame::OnParsingFinished() game.localized_category = std::move(qt_cat); game.compat = m_game_compat->GetCompatibility(game.info.serial); + game.has_database_config = m_config_db->has_config(game.info.serial); game.has_custom_config = fs::is_file(rpcs3::utils::get_custom_config_path(game.info.serial)); game.has_custom_pad_config = fs::is_file(rpcs3::utils::get_custom_input_config_path(game.info.serial)); @@ -1058,6 +1077,15 @@ void game_list_frame::OnCompatFinished() Refresh(); } +void game_list_frame::OnConfigDatabaseFinished() +{ + for (const auto& game : m_game_data) + { + game->has_database_config = m_config_db->has_config(game->info.serial); + } + Refresh(); +} + void game_list_frame::ToggleCategoryFilter(const QStringList& categories, bool show) { QStringList& filters = m_is_list_layout ? m_category_filters : m_grid_category_filters; diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index b01dbd5a6e..b195cdf5c7 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -18,6 +18,7 @@ #include #include +class config_database; class game_list_table; class game_list_grid; class gui_settings; @@ -53,7 +54,8 @@ public: void SetShowHidden(bool show); - game_compatibility* GetGameCompatibility() const { return m_game_compat; } + game_compatibility* GetGameCompatibility() const { return ensure(m_game_compat); } + config_database* GetConfigDatabase() const { return ensure(m_config_db); } const std::vector& GetGameInfo() const { return m_game_data; } std::shared_ptr actions() const { return m_game_list_actions; } std::shared_ptr get_gui_settings() const { return m_gui_settings; } @@ -95,6 +97,7 @@ private Q_SLOTS: void OnParsingFinished(); void OnRefreshFinished(); void OnCompatFinished(); + void OnConfigDatabaseFinished(); void OnColClicked(int col); void ShowContextMenu(const QPoint& pos); void doubleClickedSlot(QTableWidgetItem* item); @@ -151,6 +154,7 @@ private: // Game List game_list_table* m_game_list = nullptr; game_compatibility* m_game_compat = nullptr; + config_database* m_config_db = nullptr; progress_dialog* m_progress_dialog = nullptr; std::map m_column_acts; Qt::SortOrder m_col_sort_order{}; diff --git a/rpcs3/rpcs3qt/gui_application.cpp b/rpcs3/rpcs3qt/gui_application.cpp index 8379e2f5e0..756dbe18ed 100644 --- a/rpcs3/rpcs3qt/gui_application.cpp +++ b/rpcs3/rpcs3qt/gui_application.cpp @@ -640,13 +640,15 @@ void gui_application::InitializeCallbacks() on_exit(); } + const bool no_gui = !m_main_window; + if (m_main_window) { // Close main window in order to save its window state m_main_window->close(); } - gui_log.notice("Quitting gui application"); + gui_log.notice("Quitting gui application (force_quit=%d, no-gui=%d)", force_quit, no_gui); quit(); return true; } diff --git a/rpcs3/rpcs3qt/gui_game_info.h b/rpcs3/rpcs3qt/gui_game_info.h index 693483dd6a..b06e97e8c2 100644 --- a/rpcs3/rpcs3qt/gui_game_info.h +++ b/rpcs3/rpcs3qt/gui_game_info.h @@ -13,9 +13,10 @@ struct gui_game_info { GameInfo info{}; QString localized_category; - compat::status compat; + compat::status compat{}; QPixmap icon; QPixmap pxmap; + bool has_database_config = false; bool has_custom_config = false; bool has_custom_pad_config = false; bool has_custom_icon = false; diff --git a/rpcs3/rpcs3qt/log_viewer.cpp b/rpcs3/rpcs3qt/log_viewer.cpp index b69c07901e..623a74b48a 100644 --- a/rpcs3/rpcs3qt/log_viewer.cpp +++ b/rpcs3/rpcs3qt/log_viewer.cpp @@ -201,7 +201,7 @@ void log_viewer::show_context_menu(const QPoint& pos) connect(config, &QAction::triggered, this, [this]() { - config_checker* dlg = new config_checker(this, m_full_log, true); + config_checker* dlg = new config_checker(this, m_full_log, config_checker::checker_mode::log); dlg->open(); }); diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 7ac04e5707..2c86daaefa 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -47,6 +47,7 @@ #include "music_player_dialog.h" #include "sound_effect_manager_dialog.h" #include "recording_settings_dialog.h" +#include "config_database.h" #include #include @@ -168,7 +169,7 @@ extern void qt_events_aware_op(int repeat_duration_ms, std::function wra } } -main_window::main_window(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget *parent) +main_window::main_window(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget* parent) : QMainWindow(parent) , ui(new Ui::main_window) , m_gui_settings(gui_settings) @@ -233,7 +234,7 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot) connect(ui->actionDownload_Update, &QAction::triggered, this, [this] { - m_updater.update(false); + m_updater.update(false, true); }); #ifdef _WIN32 @@ -259,6 +260,25 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot) #endif #ifdef RPCS3_UPDATE_SUPPORTED +#ifndef _WIN32 + connect(&m_updater, &update_manager::signal_download_additional_files, this, [this](bool auto_accept) + { + if (!m_game_list_frame) return; + + connect(m_game_list_frame->GetGameCompatibility(), &game_compatibility::DownloadFinished, this, [this, auto_accept]() + { + connect(m_game_list_frame->GetConfigDatabase(), &config_database::download_finished, this, [this, auto_accept]() + { + m_updater.update(auto_accept, false); + }, Qt::ConnectionType::SingleShotConnection); + + m_game_list_frame->GetConfigDatabase()->request_config_database(true); + }, Qt::ConnectionType::SingleShotConnection); + + m_game_list_frame->GetGameCompatibility()->RequestCompatibility(true); + }); +#endif + if (const auto update_value = m_gui_settings->GetValue(gui::m_check_upd_start).toString(); update_value != gui::update_off) { const bool in_background = with_cli_boot || update_value == gui::update_bkg; @@ -493,6 +513,9 @@ void main_window::show_boot_error(game_boot_result status) case game_boot_result::firmware_version: message = tr("The game or PS3 application needs a more recent firmware version."); break; + case game_boot_result::database_config_missing: + message = tr("Could not find any configuration for this game in the database."); + break; case game_boot_result::firmware_missing: // Handled elsewhere case game_boot_result::already_added: // Handled elsewhere case game_boot_result::currently_restricted: @@ -530,7 +553,25 @@ void main_window::Boot(const std::string& path, const std::string& title_id, boo m_app_icon = gui::utils::get_app_icon_from_path(path, title_id); - if (const auto error = Emu.BootGame(path, title_id, direct, config_mode, config_path); error != game_boot_result::no_errors) + std::string db_config; + + // Get database config if possible or if we are in database_config mode (to ensure we see an error on invalid use) + if (config_database* db = m_game_list_frame->GetConfigDatabase(); + db->has_config(title_id)) + { + const std::optional config = db->get_config(title_id); + + if (!config) + { + gui_log.error("Boot failed: reason: no database config found for '%s'", title_id); + show_boot_error(game_boot_result::database_config_missing); + return; + } + + db_config = *config; + } + + if (const auto error = Emu.BootGame(path, title_id, direct, config_mode, config_path, db_config); error != game_boot_result::no_errors) { gui_log.error("Boot failed: reason: %s, path: %s", error, path); show_boot_error(error); @@ -940,10 +981,10 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo bool precompile_caches = false; bool canceled = false; - game_compatibility* compat = m_game_list_frame ? m_game_list_frame->GetGameCompatibility() : nullptr; + const game_compatibility* compat = m_game_list_frame ? m_game_list_frame->GetGameCompatibility() : nullptr; // Let the user choose the packages to install and select the order in which they shall be installed. - pkg_install_dialog dlg(file_paths, compat, this); + pkg_install_dialog dlg(file_paths, from_boot, compat, this); connect(&dlg, &QDialog::finished, this, [&](int result) { if (result != QDialog::Accepted) @@ -968,7 +1009,8 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo if (canceled) { - return false; + // return "true" if installation of optional packages (requested by some games at first boot) is skipped + return from_boot; } if (!from_boot) @@ -3208,7 +3250,7 @@ void main_window::CreateConnects() m_gui_settings->SetValue(gui::fd_cfg_check, file_info.path()); - config_checker* dlg = new config_checker(this, content, file_path.endsWith(".log") || file_path.endsWith(".log.gz")); + config_checker* dlg = new config_checker(this, content, (file_path.endsWith(".log") || file_path.endsWith(".log.gz")) ? config_checker::checker_mode::log : config_checker::checker_mode::config); dlg->open(); }); @@ -3906,6 +3948,7 @@ void main_window::closeEvent(QCloseEvent* closeEvent) Q_EMIT NotifyWindowCloseEvent(true); + gui_log.notice("Quit with main_window::closeEvent"); Emu.Quit(true); } diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index e9206070cf..b1dc4847d1 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -70,7 +70,7 @@ class main_window : public QMainWindow }; public: - explicit main_window(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget *parent = nullptr); + explicit main_window(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget* parent = nullptr); ~main_window(); bool Init(bool with_cli_boot); QIcon GetAppIcon() const; diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.cpp b/rpcs3/rpcs3qt/patch_manager_dialog.cpp index 2e9d980d20..de6db736e4 100644 --- a/rpcs3/rpcs3qt/patch_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/patch_manager_dialog.cpp @@ -1168,7 +1168,16 @@ void patch_manager_dialog::download_update(bool automatic, bool auto_accept) bool patch_manager_dialog::handle_json(const QByteArray& data) { - const QJsonObject json_data = QJsonDocument::fromJson(data).object(); + QJsonParseError error {}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + patch_log.error("Patch download error - Invalid JSON: '%s'", error.errorString()); + return false; + } + + const QJsonObject json_data = json_document.object(); const int return_code = json_data["return_code"].toInt(-255); if (return_code < 0) diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.cpp b/rpcs3/rpcs3qt/pkg_install_dialog.cpp index bd17c4eaff..a1ebb9a8d1 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.cpp +++ b/rpcs3/rpcs3qt/pkg_install_dialog.cpp @@ -24,7 +24,7 @@ enum Roles DataSizeRole = Qt::UserRole + 5, }; -pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibility* compat, QWidget* parent) +pkg_install_dialog::pkg_install_dialog(const QStringList& paths, bool from_boot, const game_compatibility* compat, QWidget* parent) : QDialog(parent) { ensure(!paths.empty()); @@ -148,6 +148,11 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil buttons->button(QDialogButtonBox::Ok)->setText(tr("Install")); buttons->button(QDialogButtonBox::Ok)->setDefault(true); + if (from_boot) + { + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Skip")); + } + m_dir_list->sortItems(); m_dir_list->setCurrentRow(0); m_dir_list->setMinimumWidth((m_dir_list->sizeHintForColumn(0) * 125) / 100); diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.h b/rpcs3/rpcs3qt/pkg_install_dialog.h index b1f3dbeb9d..979d7f5682 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.h +++ b/rpcs3/rpcs3qt/pkg_install_dialog.h @@ -17,7 +17,7 @@ class pkg_install_dialog : public QDialog Q_OBJECT public: - explicit pkg_install_dialog(const QStringList& paths, game_compatibility* compat, QWidget* parent = nullptr); + explicit pkg_install_dialog(const QStringList& paths, bool from_boot, const game_compatibility* compat, QWidget* parent = nullptr); std::vector get_paths_to_install() const; bool precompile_caches() const { return m_precompile_caches; } bool create_desktop_shortcuts() const { return m_create_desktop_shortcuts; } diff --git a/rpcs3/rpcs3qt/settings_dialog.cpp b/rpcs3/rpcs3qt/settings_dialog.cpp index 6787caa665..26e062b67b 100644 --- a/rpcs3/rpcs3qt/settings_dialog.cpp +++ b/rpcs3/rpcs3qt/settings_dialog.cpp @@ -87,7 +87,7 @@ void remove_item(QComboBox* box, int data_value, int def_value) extern const std::map g_prx_list; -settings_dialog::settings_dialog(std::shared_ptr gui_settings, std::shared_ptr emu_settings, int tab_index, QWidget* parent, const GameInfo* game, bool create_cfg_from_global_cfg) +settings_dialog::settings_dialog(std::shared_ptr gui_settings, std::shared_ptr emu_settings, int tab_index, QWidget* parent, const GameInfo* game, bool create_cfg_from_global_cfg, const std::string& db_config) : QDialog(parent) , m_tab_index(tab_index) , ui(new Ui::settings_dialog) @@ -132,7 +132,7 @@ settings_dialog::settings_dialog(std::shared_ptr gui_settings, std if (game) { - m_emu_settings->LoadSettings(game->serial, create_cfg_from_global_cfg); + m_emu_settings->LoadSettings(game->serial, create_cfg_from_global_cfg, db_config); setWindowTitle(tr("Settings: [%0] %1", "Settings dialog").arg(QString::fromStdString(game->serial)).arg(QString::fromStdString(game->name))); } else diff --git a/rpcs3/rpcs3qt/settings_dialog.h b/rpcs3/rpcs3qt/settings_dialog.h index 0513227e80..a7402850a3 100644 --- a/rpcs3/rpcs3qt/settings_dialog.h +++ b/rpcs3/rpcs3qt/settings_dialog.h @@ -21,7 +21,7 @@ class settings_dialog : public QDialog Q_OBJECT public: - explicit settings_dialog(std::shared_ptr gui_settings, std::shared_ptr emu_settings, int tab_index = 0, QWidget* parent = nullptr, const GameInfo* game = nullptr, bool create_cfg_from_global_cfg = true); + explicit settings_dialog(std::shared_ptr gui_settings, std::shared_ptr emu_settings, int tab_index = 0, QWidget* parent = nullptr, const GameInfo* game = nullptr, bool create_cfg_from_global_cfg = true, const std::string& db_config = ""); ~settings_dialog(); void open() override; Q_SIGNALS: diff --git a/rpcs3/rpcs3qt/settings_dialog.ui b/rpcs3/rpcs3qt/settings_dialog.ui index 9e23f48f1a..77b3f26593 100644 --- a/rpcs3/rpcs3qt/settings_dialog.ui +++ b/rpcs3/rpcs3qt/settings_dialog.ui @@ -4519,11 +4519,11 @@ - + - Accurate PPU/SPU Double-Precision FMA + Accurate PPU/SPU Double-Precision FMA - + @@ -4575,7 +4575,7 @@ - + Qt::Orientation::Vertical diff --git a/rpcs3/rpcs3qt/update_manager.cpp b/rpcs3/rpcs3qt/update_manager.cpp index a34292ce0e..87d3b445b6 100644 --- a/rpcs3/rpcs3qt/update_manager.cpp +++ b/rpcs3/rpcs3qt/update_manager.cpp @@ -46,6 +46,8 @@ LOG_CHANNEL(update_log, "UPDATER"); +constexpr bool allow_local_auto_update = false; // Set true for debugging the auto updater locally + update_manager::update_manager(QObject* parent, std::shared_ptr gui_settings) : QObject(parent), m_gui_settings(std::move(gui_settings)) { @@ -60,7 +62,7 @@ void update_manager::check_for_updates(bool automatic, bool check_only, bool aut if (automatic) { // Don't check for updates on local builds - if (rpcs3::is_local_build()) + if (!allow_local_auto_update && rpcs3::is_local_build()) { update_log.notice("Skipped automatic update check: this is a local build"); return; @@ -116,7 +118,16 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce { update_log.notice("Download of update info finished. automatic=%d, check_only=%d, auto_accept=%d", automatic, check_only, auto_accept); - const QJsonObject json_data = QJsonDocument::fromJson(data).object(); + QJsonParseError error {}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + update_log.error("Update error - Invalid JSON: '%s'", error.errorString()); + return false; + } + + const QJsonObject json_data = json_document.object(); const int return_code = json_data["return_code"].toInt(-255); m_update_info.hash_found = true; @@ -126,7 +137,7 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce std::string error_message; switch (return_code) { - case -1: error_message = "Hash not found(Custom/PR build)"; break; + case -1: error_message = "Hash not found (Custom/PR build)"; break; case -2: error_message = "Server Error - Maintenance Mode"; break; case -3: error_message = "Server Error - Illegal Search"; break; case -255: error_message = "Server Error - Return code not found"; break; @@ -139,14 +150,12 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce update_log.warning("Update error: %s, return code: %d", error_message, return_code); // If a user clicks "Check for Updates" with a custom build ask him if he's sure he wants to update to latest version - if (!automatic && return_code == -1) - { - m_update_info.hash_found = false; - } - else + if (!allow_local_auto_update && (automatic || return_code != -1)) { return false; } + + m_update_info.hash_found = false; } const auto& current = json_data["current_build"]; @@ -302,17 +311,17 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce return true; } - update(auto_accept); + update(auto_accept, true); return true; } -void update_manager::update(bool auto_accept) +void update_manager::update(bool auto_accept, bool is_first_call) { update_log.notice("Updating with auto_accept=%d", auto_accept); ensure(m_downloader); - if (!auto_accept) + if (!auto_accept && is_first_call) { if (!m_update_info.update_found) { @@ -416,6 +425,14 @@ void update_manager::update(bool auto_accept) return; } +#ifndef _WIN32 + if (is_first_call) + { + Q_EMIT signal_download_additional_files(auto_accept); + return; + } +#endif + m_downloader->disconnect(); connect(m_downloader, &downloader::signal_download_error, this, [this](const QString& /*error*/) diff --git a/rpcs3/rpcs3qt/update_manager.h b/rpcs3/rpcs3qt/update_manager.h index 98ef5cf3f1..62986b796b 100644 --- a/rpcs3/rpcs3qt/update_manager.h +++ b/rpcs3/rpcs3qt/update_manager.h @@ -16,10 +16,11 @@ class update_manager final : public QObject public: update_manager(QObject* parent, std::shared_ptr gui_settings); void check_for_updates(bool automatic, bool check_only, bool auto_accept, QWidget* parent = nullptr); - void update(bool auto_accept); + void update(bool auto_accept, bool is_first_call); Q_SIGNALS: void signal_update_available(bool update_available); + void signal_download_additional_files(bool auto_accept); private: downloader* m_downloader = nullptr; diff --git a/rpcs3/tests/rpcs3_test.vcxproj b/rpcs3/tests/rpcs3_test.vcxproj index fb9d0d21d4..a60b150469 100644 --- a/rpcs3/tests/rpcs3_test.vcxproj +++ b/rpcs3/tests/rpcs3_test.vcxproj @@ -100,6 +100,7 @@ + diff --git a/rpcs3/tests/test_sys_fs.cpp b/rpcs3/tests/test_sys_fs.cpp new file mode 100644 index 0000000000..66db10a554 --- /dev/null +++ b/rpcs3/tests/test_sys_fs.cpp @@ -0,0 +1,51 @@ +#include + +#define private public +#include "Emu/Cell/lv2/sys_fs.h" +#undef private + +using namespace utils; + +namespace utils +{ + TEST(cellFs, PathRoot) + { + std::string path = "/."; + auto [root, trail] = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_TRUE(root.empty()); + EXPECT_TRUE(trail.empty()); + + path = "/./././dev_bdvd/./"; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_bdvd"sv); + EXPECT_TRUE(trail.empty()); + + path = "/../"; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_TRUE(root.empty()); + EXPECT_EQ(trail, "ENOENT"sv); + } + + TEST(cellFs, PathSimplify) + { + std::string path = "/dev_hdd0/"; + auto [root, trail] = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_hdd0"sv); + EXPECT_TRUE(trail.empty()); + + path = "/dev_hdd0/game"; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_hdd0"sv); + EXPECT_EQ(trail, "game"sv); + + path = "/dev_hdd0/game/NP1234567"; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_hdd0"sv); + EXPECT_EQ(trail, "game/NP1234567"sv); + + path = "/dev_hdd0/game/NP1234567/../../NP1234568/."; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_hdd0"sv); + EXPECT_EQ(trail, "NP1234568"sv); + } +}