From 02cbebbf78e9370e197b21028bd1fde9ee0340cd Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 1 Mar 2024 00:00:35 +0200 Subject: [PATCH] file formats and qt (#88) * added psf file format * clang format fix * crypto functions for pkg decryption * pkg decryption * initial add of qt gui , not yet usable * renamed ini for qt gui settings into shadps4qt.ini * file detection and loader support * option to build QT qui * clang format fix * fixed reuse * Update src/core/file_format/pkg.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/file_format/pkg.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/file_format/pkg.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/file_format/pkg.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/loader.h Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/loader.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * uppercase fix * clang format fix * small fixes * let's try windows qt build ci * some more fixes for ci * Update src/core/file_format/pkg.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/file_format/pkg.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/file_format/pkg.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/file_format/pkg.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update .github/workflows/windows-qt.yml Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/loader.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * Update src/core/file_format/psf.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * loader namespace * Update src/core/loader.cpp Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> * constexpr magic * linux qt ci by qurious * fix for linux qt * Make script executable * ci fix? --------- Co-authored-by: raziel1000 Co-authored-by: GPUCode <47210458+GPUCode@users.noreply.github.com> Co-authored-by: GPUCode --- .github/linux-appimage-qt.sh | 24 + .github/shadps4.desktop | 2 +- .github/workflows/linux-qt.yml | 60 ++ .github/workflows/windows-qt.yml | 54 ++ .reuse/dep5 | 2 + CMakeLists.txt | 203 ++++-- CONTRIBUTING.md | 128 ++++ externals/CMakeLists.txt | 23 + src/common/endian.h | 242 +++++++ src/common/io_file.h | 5 + src/core/crypto/crypto.cpp | 174 +++++ src/core/crypto/crypto.h | 63 ++ src/core/crypto/keys.h | 389 +++++++++++ src/core/file_format/pfs.h | 123 ++++ src/core/file_format/pkg.cpp | 375 ++++++++++ src/core/file_format/pkg.h | 137 ++++ src/core/file_format/pkg_type.cpp | 638 +++++++++++++++++ src/core/file_format/pkg_type.h | 10 + src/core/file_format/psf.cpp | 59 ++ src/core/file_format/psf.h | 48 ++ src/core/loader.cpp | 28 + src/core/loader.h | 18 + src/images/shadps4.ico | Bin 0 -> 157385 bytes src/qt_gui/custom_dock_widget.h | 62 ++ src/qt_gui/custom_table_widget_item.cpp | 67 ++ src/qt_gui/custom_table_widget_item.h | 26 + src/qt_gui/game_info.h | 20 + src/qt_gui/game_install_dialog.cpp | 83 +++ src/qt_gui/game_install_dialog.h | 27 + src/qt_gui/game_list_frame.cpp | 886 ++++++++++++++++++++++++ src/qt_gui/game_list_frame.h | 146 ++++ src/qt_gui/game_list_grid.cpp | 164 +++++ src/qt_gui/game_list_grid.h | 62 ++ src/qt_gui/game_list_grid_delegate.cpp | 67 ++ src/qt_gui/game_list_grid_delegate.h | 24 + src/qt_gui/game_list_item.h | 35 + src/qt_gui/game_list_table.cpp | 18 + src/qt_gui/game_list_table.h | 28 + src/qt_gui/game_list_utils.h | 109 +++ src/qt_gui/gui_save.h | 29 + src/qt_gui/gui_settings.cpp | 29 + src/qt_gui/gui_settings.h | 106 +++ src/qt_gui/main.cpp | 21 + src/qt_gui/main_window.cpp | 364 ++++++++++ src/qt_gui/main_window.h | 75 ++ src/qt_gui/main_window_themes.cpp | 120 ++++ src/qt_gui/main_window_themes.h | 21 + src/qt_gui/main_window_ui.h | 279 ++++++++ src/qt_gui/qt_utils.h | 20 + src/qt_gui/settings.cpp | 77 ++ src/qt_gui/settings.h | 50 ++ src/shadps4.rc | 1 + third-party/CMakeLists.txt | 2 + 53 files changed, 5781 insertions(+), 42 deletions(-) create mode 100755 .github/linux-appimage-qt.sh create mode 100644 .github/workflows/linux-qt.yml create mode 100644 .github/workflows/windows-qt.yml create mode 100644 CONTRIBUTING.md create mode 100644 src/common/endian.h create mode 100644 src/core/crypto/crypto.cpp create mode 100644 src/core/crypto/crypto.h create mode 100644 src/core/crypto/keys.h create mode 100644 src/core/file_format/pfs.h create mode 100644 src/core/file_format/pkg.cpp create mode 100644 src/core/file_format/pkg.h create mode 100644 src/core/file_format/pkg_type.cpp create mode 100644 src/core/file_format/pkg_type.h create mode 100644 src/core/file_format/psf.cpp create mode 100644 src/core/file_format/psf.h create mode 100644 src/core/loader.cpp create mode 100644 src/core/loader.h create mode 100644 src/images/shadps4.ico create mode 100644 src/qt_gui/custom_dock_widget.h create mode 100644 src/qt_gui/custom_table_widget_item.cpp create mode 100644 src/qt_gui/custom_table_widget_item.h create mode 100644 src/qt_gui/game_info.h create mode 100644 src/qt_gui/game_install_dialog.cpp create mode 100644 src/qt_gui/game_install_dialog.h create mode 100644 src/qt_gui/game_list_frame.cpp create mode 100644 src/qt_gui/game_list_frame.h create mode 100644 src/qt_gui/game_list_grid.cpp create mode 100644 src/qt_gui/game_list_grid.h create mode 100644 src/qt_gui/game_list_grid_delegate.cpp create mode 100644 src/qt_gui/game_list_grid_delegate.h create mode 100644 src/qt_gui/game_list_item.h create mode 100644 src/qt_gui/game_list_table.cpp create mode 100644 src/qt_gui/game_list_table.h create mode 100644 src/qt_gui/game_list_utils.h create mode 100644 src/qt_gui/gui_save.h create mode 100644 src/qt_gui/gui_settings.cpp create mode 100644 src/qt_gui/gui_settings.h create mode 100644 src/qt_gui/main.cpp create mode 100644 src/qt_gui/main_window.cpp create mode 100644 src/qt_gui/main_window.h create mode 100644 src/qt_gui/main_window_themes.cpp create mode 100644 src/qt_gui/main_window_themes.h create mode 100644 src/qt_gui/main_window_ui.h create mode 100644 src/qt_gui/qt_utils.h create mode 100644 src/qt_gui/settings.cpp create mode 100644 src/qt_gui/settings.h create mode 100644 src/shadps4.rc diff --git a/.github/linux-appimage-qt.sh b/.github/linux-appimage-qt.sh new file mode 100755 index 000000000..e1678b0d9 --- /dev/null +++ b/.github/linux-appimage-qt.sh @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2024 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +#!/bin/bash + +if [[ -z $GITHUB_WORKSPACE ]]; then + GITHUB_WORKSPACE="${PWD%/*}" +fi + +export PATH="$Qt6_DIR/bin:$PATH" + +# Prepare Tools for building the AppImage +wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage +wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage +wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-checkrt/releases/download/continuous/linuxdeploy-plugin-checkrt-x86_64.sh + +chmod a+x linuxdeploy-x86_64.AppImage +chmod a+x linuxdeploy-plugin-qt-x86_64.AppImage +chmod a+x linuxdeploy-plugin-checkrt-x86_64.sh + +# Build AppImage +./linuxdeploy-x86_64.AppImage --appdir AppDir +./linuxdeploy-plugin-checkrt-x86_64.sh --appdir AppDir +./linuxdeploy-x86_64.AppImage --appdir AppDir -d "$GITHUB_WORKSPACE"/.github/shadps4.desktop -e "$GITHUB_WORKSPACE"/build/shadps4 -i "$GITHUB_WORKSPACE"/.github/shadps4.png --plugin qt --output appimage diff --git a/.github/shadps4.desktop b/.github/shadps4.desktop index 72efea211..095acb787 100644 --- a/.github/shadps4.desktop +++ b/.github/shadps4.desktop @@ -4,6 +4,6 @@ Exec=shadps4 Terminal=false Type=Application Icon=shadps4 -Comment=gui for shadps4 +Comment=shadps4 emulator Categories=Game; StartupWMClass=shadps4; diff --git a/.github/workflows/linux-qt.yml b/.github/workflows/linux-qt.yml new file mode 100644 index 000000000..67e8f1bec --- /dev/null +++ b/.github/workflows/linux-qt.yml @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2024 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +name: Linux-Qt + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + CLANG_VER: 17 + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Install misc packages + run: | + sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 libwayland-dev libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-icccm4 libxcb-image0-dev libxcb-cursor-dev libxxhash-dev libvulkan-dev + + - name: Install newer Clang + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x ./llvm.sh + sudo ./llvm.sh ${{env.CLANG_VER}} + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: 6.6.1 + host: linux + target: desktop + #arch: clang++-17 + dir: ${{ runner.temp }} + #modules: qtcharts qt3d + setup-python: false + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-${{env.CLANG_VER}} -DCMAKE_CXX_COMPILER=clang++-${{env.CLANG_VER}} -DENABLE_QT_GUI=ON + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel + + - name: Run AppImage packaging script + run: ./.github/linux-appimage-qt.sh + + - name: Upload executable + uses: actions/upload-artifact@v4 + with: + name: shadps4-linux-qt + path: Shadps4-x86_64.AppImage diff --git a/.github/workflows/windows-qt.yml b/.github/workflows/windows-qt.yml new file mode 100644 index 000000000..76cecd8e2 --- /dev/null +++ b/.github/workflows/windows-qt.yml @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2024 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +name: Windows-Qt + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +permissions: + contents: read + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Setup Qt + uses: jurplel/install-qt-action@v3 + with: + arch: win64_msvc2019_64 + version: 6.6.1 + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -T ClangCL -DENABLE_QT_GUI=ON + + - name: Build + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel + + - name: Deploy + run: | + mkdir upload + move build/Release/shadps4.exe upload + move build/Release/zlib-ng2.dll upload + windeployqt --dir upload upload/shadps4.exe + + - name: Upload executable + uses: actions/upload-artifact@v2 + with: + name: shadps4-win64-qt + path: upload diff --git a/.reuse/dep5 b/.reuse/dep5 index 5ead99f72..9eaf57812 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -9,5 +9,7 @@ Files: CMakeSettings.json .github/shadps4.desktop .github/shadps4.png .gitmodules + src/images/shadps4.ico + src/shadps4.rc Copyright: shadPS4 Emulator Project License: GPL-2.0-or-later diff --git a/CMakeLists.txt b/CMakeLists.txt index 9810ff92c..c46eaa179 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16.3) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED True) if (NOT CMAKE_BUILD_TYPE) @@ -12,6 +12,13 @@ endif() project(shadps4) +option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses a minimal SDL-based UI instead" OFF) + +if(ENABLE_QT_GUI) + find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent) + qt_standard_project_setup() +endif() + # This function should be passed a list of all files in a target. It will automatically generate # file groups following the directory hierarchy, so that the layout of the files in IDEs matches the # one in the filesystem. @@ -127,44 +134,113 @@ set(HOST_SOURCES src/Emulator/Host/controller.cpp src/Emulator/Host/controller.h ) +# the above is shared in sdl and qt version (TODO share them all) + +if(ENABLE_QT_GUI) + set(QT_GUI + src/qt_gui/main_window_ui.h + src/qt_gui/main_window.cpp + src/qt_gui/main_window.h + src/qt_gui/gui_settings.cpp + src/qt_gui/gui_settings.h + src/qt_gui/settings.cpp + src/qt_gui/settings.h + src/qt_gui/gui_save.h + src/qt_gui/custom_dock_widget.h + src/qt_gui/custom_table_widget_item.cpp + src/qt_gui/custom_table_widget_item.h + src/qt_gui/game_list_item.h + src/qt_gui/game_list_table.cpp + src/qt_gui/game_list_table.h + src/qt_gui/game_list_utils.h + src/qt_gui/game_info.h + src/qt_gui/game_list_grid.cpp + src/qt_gui/game_list_grid.h + src/qt_gui/game_list_grid_delegate.cpp + src/qt_gui/game_list_grid_delegate.h + src/qt_gui/game_list_frame.cpp + src/qt_gui/game_list_frame.h + src/qt_gui/qt_utils.h + src/qt_gui/game_install_dialog.cpp + src/qt_gui/game_install_dialog.h + src/qt_gui/main_window_themes.cpp + src/qt_gui/main_window_themes.h + src/qt_gui/main.cpp + ) +endif() + +set(COMMON src/common/logging/backend.cpp + src/common/logging/backend.h + src/common/logging/filter.cpp + src/common/logging/filter.h + src/common/logging/formatter.h + src/common/logging/log_entry.h + src/common/logging/log.h + src/common/logging/text_formatter.cpp + src/common/logging/text_formatter.h + src/common/logging/types.h + src/common/assert.cpp + src/common/assert.h + src/common/bounded_threadsafe_queue.h + src/common/concepts.h + src/common/debug.h + src/common/disassembler.cpp + src/common/disassembler.h + src/common/discord.cpp + src/common/discord.h + src/common/endian.h + src/common/io_file.cpp + src/common/io_file.h + src/common/error.cpp + src/common/error.h + src/common/native_clock.cpp + src/common/native_clock.h + src/common/path_util.cpp + src/common/path_util.h + src/common/rdtsc.cpp + src/common/rdtsc.h + src/common/singleton.h + src/common/string_util.cpp + src/common/string_util.h + src/common/thread.cpp + src/common/thread.h + src/common/types.h + src/common/uint128.h + src/common/version.h +) + +set(CORE src/core/loader.cpp + src/core/loader.h +) + +set(CRYPTO src/core/crypto/crypto.cpp + src/core/crypto/crypto.h + src/core/crypto/keys.h +) +set(FILE_FORMAT src/core/file_format/pfs.h + src/core/file_format/pkg.cpp + src/core/file_format/pkg.h + src/core/file_format/pkg_type.cpp + src/core/file_format/pkg_type.h + src/core/file_format/psf.cpp + src/core/file_format/psf.h +) + +set(UTILITIES src/Util/config.cpp + src/Util/config.h +) + +if(ENABLE_QT_GUI) +qt_add_executable(shadps4 + ${QT_GUI} + ${COMMON} + ${CORE} + ${CRYPTO} + ${FILE_FORMAT} + ${UTILITIES} +) +else() add_executable(shadps4 - src/common/assert.cpp - src/common/assert.h - src/common/bounded_threadsafe_queue.h - src/common/concepts.h - src/common/debug.h - src/common/disassembler.cpp - src/common/disassembler.h - src/common/discord.cpp - src/common/discord.h - src/common/error.cpp - src/common/error.h - src/common/io_file.cpp - src/common/io_file.h - src/common/path_util.cpp - src/common/path_util.h - src/common/logging/backend.cpp - src/common/logging/backend.h - src/common/logging/filter.cpp - src/common/logging/filter.h - src/common/logging/formatter.h - src/common/logging/log_entry.h - src/common/logging/log.h - src/common/logging/text_formatter.cpp - src/common/logging/text_formatter.h - src/common/logging/types.h - src/common/native_clock.cpp - src/common/native_clock.h - src/common/rdtsc.cpp - src/common/rdtsc.h - src/common/singleton.h - src/common/string_util.cpp - src/common/string_util.h - src/common/thread.cpp - src/common/thread.h - src/common/types.h - src/common/uint128.h - src/common/version.h ${LIBC_SOURCES} ${USERSERVICE_SOURCES} ${PAD_SOURCES} @@ -175,8 +251,6 @@ add_executable(shadps4 src/main.cpp src/core/loader/elf.cpp src/core/loader/elf.h - src/Util/config.cpp - src/Util/config.h src/core/virtual_memory.cpp src/core/virtual_memory.h src/core/linker.cpp @@ -227,22 +301,69 @@ add_executable(shadps4 src/core/hle/libraries/libkernel/time_management.h src/core/tls.cpp src/core/tls.h + ${COMMON} + ${CORE} + ${CRYPTO} + ${FILE_FORMAT} + ${UTILITIES} ) +endif() create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11) -target_link_libraries(shadps4 PRIVATE discord-rpc SDL3-shared vulkan-1 xxhash Zydis) +target_link_libraries(shadps4 PRIVATE discord-rpc vulkan-1 xxhash Zydis) + +if(NOT ENABLE_QT_GUI) + target_link_libraries(shadps4 PRIVATE SDL3-shared) +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND MSVC) + target_link_libraries(shadps4 PRIVATE cryptoppwin zlib) +else() + target_link_libraries(shadps4 PRIVATE cryptopp::cryptopp zlib) +endif() + +if(ENABLE_QT_GUI) + target_link_libraries(shadps4 PRIVATE Qt6::Widgets Qt6::Concurrent) +endif() + if (WIN32) target_link_libraries(shadps4 PRIVATE mincore winpthread clang_rt.builtins-x86_64.lib) add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE -D_SCL_SECURE_NO_WARNINGS) add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN) endif() +if(WIN32) + target_sources(shadps4 PRIVATE src/shadps4.rc) +endif() + +target_include_directories(shadps4 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +if(ENABLE_QT_GUI) +set_target_properties(shadps4 PROPERTIES + WIN32_EXECUTABLE ON + MACOSX_BUNDLE ON +) +endif() + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") +add_custom_command(TARGET shadps4 POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${PROJECT_SOURCE_DIR}/externals/zlib-ng-win/bin/zlib-ngd2.dll" $) +else() +add_custom_command(TARGET shadps4 POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${PROJECT_SOURCE_DIR}/externals/zlib-ng-win/bin/zlib-ng2.dll" $) +endif() + +if(NOT ENABLE_QT_GUI) add_custom_command(TARGET shadps4 POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $) +endif() + if (WIN32) add_custom_command(TARGET shadps4 POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..242278fca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,128 @@ + + +# Style guidelines + +## General Rules + +* Line width is typically 100 characters. Please do not use 80-characters. +* Don't ever introduce new external dependencies into Core +* Don't use any platform specific code in Core +* Use namespaces often +* Avoid the use of C-style casts and instead prefer C++-style static_cast and reinterpret_cast. Try to avoid using dynamic_cast. Never use const_cast except for when dealing with external const-incorrect APIs. + +## Naming Rules + +* Functions: `PascalCase` +* Variables: `lower_case_underscored. Prefix with g_ if global.` +* Classes: `PascalCase` +* Files and Directories: `lower_case_underscored` +* Namespaces: `PascalCase`, `_` may also be used for clarity (e.g. `ARM_InitCore`) + +# Indentation/Whitespace Style + +Follow the indentation/whitespace style shown below. Do not use tabs, use 4-spaces instead. + +# Comments + +* For regular comments, use C++ style (//) comments, even for multi-line ones. +* For doc-comments (Doxygen comments), use /// if it's a single line, else use the /** */ style featured in the example. Start the text on the second line, not the first containing /**. +* For items that are both defined and declared in two separate files, put the doc-comment only next to the associated declaration. (In a header file, usually.) Otherwise, put it next to the implementation. Never duplicate doc-comments in both places. + +``` +// Includes should be sorted lexicographically +// STD includes first +#include +#include +#include + +// then, library includes +#include + +// finally, shadps4 includes +#include "common/math_util.h" +#include "common/vector_math.h" + +// each major module is separated +#include "video_core/pica.h" +#include "video_core/video_core.h" + +namespace Example { + +// Namespace contents are not indented + +// Declare globals at the top (better yet, don't use globals at all!) +int g_foo{}; // {} can be used to initialize types as 0, false, or nullptr +char* g_some_pointer{}; // Pointer * and reference & stick to the type name, and make sure to initialize as nullptr! + +/// A colorful enum. +enum class SomeEnum { + Red, ///< The color of fire. + Green, ///< The color of grass. + Blue, ///< Not actually the color of water. +}; + +/** + * Very important struct that does a lot of stuff. + * Note that the asterisks are indented by one space to align to the first line. + */ +struct Position { + // Always intitialize member variables! + int x{}; + int y{}; +}; + +// Use "typename" rather than "class" here +template +void FooBar() { + const std::string some_string{"prefer uniform initialization"}; + + const std::array some_array{ + 5, + 25, + 7, + 42, + }; + + if (note == the_space_after_the_if) { + CallAFunction(); + } else { + // Use a space after the // when commenting + } + + // Place a single space after the for loop semicolons, prefer pre-increment + for (int i = 0; i != 25; ++i) { + // This is how we write loops + } + + DoStuff(this, function, call, takes, up, multiple, + lines, like, this); + + if (this || condition_takes_up_multiple && + lines && like && this || everything || + alright || then) { + + // Leave a blank space before the if block body if the condition was continued across + // several lines. + } + + // No indentation for case labels + switch (var) { + case 1: { + const int case_var{var + 3}; + DoSomething(case_var); + break; + } + case 3: + DoSomething(var); + return; + default: + // Yes, even break for the last case + break; + } +} + +} // namespace Example +``` diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 0dcd7cb4b..406c90cb1 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -11,4 +11,27 @@ set(BUILD_EXAMPLES OFF CACHE BOOL "") add_subdirectory(discord-rpc EXCLUDE_FROM_ALL) target_include_directories(discord-rpc INTERFACE ./discord-rpc/include) +if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND MSVC) + # If it is clang and MSVC we will add a static lib + # CryptoPP + add_subdirectory(cryptoppwin EXCLUDE_FROM_ALL) + target_include_directories(cryptoppwin INTERFACE cryptoppwin/include) + + # Zlib-Ng + add_subdirectory(zlib-ng-win EXCLUDE_FROM_ALL) + target_include_directories(zlib INTERFACE zlib-ng-win/include) +else() + # CryptoPP + set(CRYPTOPP_BUILD_TESTING OFF) + set(CRYPTOPP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/cryptopp/) + add_subdirectory(cryptopp-cmake EXCLUDE_FROM_ALL) + + # Zlib-Ng + set(ZLIB_ENABLE_TESTS OFF) + set(WITH_GTEST OFF) + set(WITH_NEW_STRATEGIES ON) + set(WITH_NATIVE_INSTRUCTIONS ON) + add_subdirectory(zlib-ng) +endif() + diff --git a/src/common/endian.h b/src/common/endian.h new file mode 100644 index 000000000..4b0b70cdd --- /dev/null +++ b/src/common/endian.h @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * (c) 2014-2016 Alexandro Sanchez Bach. All rights reserved. + * Released under GPL v2 license. Read LICENSE for more details. + * Some modifications for using with shadps4 by georgemoralis + */ + +#pragma once + +#include +#include +#include "common/types.h" + +namespace Common { + +/** + * Native endianness + */ +template +using NativeEndian = T; + +template +class SwappedEndian { +public: + const T& Raw() const { + return data; + } + + T Swap() const { + return std::byteswap(data); + } + + void FromRaw(const T& value) { + data = value; + } + + void FromSwap(const T& value) { + data = std::byteswap(value); + } + + operator const T() const { + return Swap(); + } + + template + explicit operator const SwappedEndian() const { + SwappedEndian res; + if (sizeof(T1) < sizeof(T)) { + res.FromRaw(Raw() >> ((sizeof(T) - sizeof(T1)) * 8)); + } else if (sizeof(T1) > sizeof(T)) { + res.FromSwap(Swap()); + } else { + res.FromRaw(Raw()); + } + return res; + } + + SwappedEndian& operator=(const T& right) { + FromSwap(right); + return *this; + } + SwappedEndian& operator=(const SwappedEndian& right) = default; + + template + SwappedEndian& operator+=(T1 right) { + return *this = T(*this) + right; + } + template + SwappedEndian& operator-=(T1 right) { + return *this = T(*this) - right; + } + template + SwappedEndian& operator*=(T1 right) { + return *this = T(*this) * right; + } + template + SwappedEndian& operator/=(T1 right) { + return *this = T(*this) / right; + } + template + SwappedEndian& operator%=(T1 right) { + return *this = T(*this) % right; + } + template + SwappedEndian& operator&=(T1 right) { + return *this = T(*this) & right; + } + template + SwappedEndian& operator|=(T1 right) { + return *this = T(*this) | right; + } + template + SwappedEndian& operator^=(T1 right) { + return *this = T(*this) ^ right; + } + template + SwappedEndian& operator<<=(T1 right) { + return *this = T(*this) << right; + } + template + SwappedEndian& operator>>=(T1 right) { + return *this = T(*this) >> right; + } + + template + SwappedEndian& operator+=(const SwappedEndian& right) { + return *this = Swap() + right.Swap(); + } + template + SwappedEndian& operator-=(const SwappedEndian& right) { + return *this = Swap() - right.Swap(); + } + template + SwappedEndian& operator*=(const SwappedEndian& right) { + return *this = Swap() * right.Swap(); + } + template + SwappedEndian& operator/=(const SwappedEndian& right) { + return *this = Swap() / right.Swap(); + } + template + SwappedEndian& operator%=(const SwappedEndian& right) { + return *this = Swap() % right.Swap(); + } + template + SwappedEndian& operator&=(const SwappedEndian& right) { + return *this = Raw() & right.Raw(); + } + template + SwappedEndian& operator|=(const SwappedEndian& right) { + return *this = Raw() | right.Raw(); + } + template + SwappedEndian& operator^=(const SwappedEndian& right) { + return *this = Raw() ^ right.Raw(); + } + + template + SwappedEndian operator&(const SwappedEndian& right) const { + return SwappedEndian{Raw() & right.Raw()}; + } + template + SwappedEndian operator|(const SwappedEndian& right) const { + return SwappedEndian{Raw() | right.Raw()}; + } + template + SwappedEndian operator^(const SwappedEndian& right) const { + return SwappedEndian{Raw() ^ right.Raw()}; + } + + template + bool operator==(T1 right) const { + return (T1)Swap() == right; + } + template + bool operator!=(T1 right) const { + return !(*this == right); + } + template + bool operator>(T1 right) const { + return (T1)Swap() > right; + } + template + bool operator<(T1 right) const { + return (T1)Swap() < right; + } + template + bool operator>=(T1 right) const { + return (T1)Swap() >= right; + } + template + bool operator<=(T1 right) const { + return (T1)Swap() <= right; + } + + template + bool operator==(const SwappedEndian& right) const { + return Raw() == right.Raw(); + } + template + bool operator!=(const SwappedEndian& right) const { + return !(*this == right); + } + template + bool operator>(const SwappedEndian& right) const { + return (T1)Swap() > right.Swap(); + } + template + bool operator<(const SwappedEndian& right) const { + return (T1)Swap() < right.Swap(); + } + template + bool operator>=(const SwappedEndian& right) const { + return (T1)Swap() >= right.Swap(); + } + template + bool operator<=(const SwappedEndian& right) const { + return (T1)Swap() <= right.Swap(); + } + + SwappedEndian operator++(int) { + SwappedEndian res = *this; + *this += 1; + return res; + } + SwappedEndian operator--(int) { + SwappedEndian res = *this; + *this -= 1; + return res; + } + SwappedEndian& operator++() { + *this += 1; + return *this; + } + SwappedEndian& operator--() { + *this -= 1; + return *this; + } + +private: + T data; +}; + +template +using LittleEndian = std::conditional_t, + SwappedEndian>; + +template +using BigEndian = + std::conditional_t, SwappedEndian>; + +} // namespace Common + +using u16_be = Common::BigEndian; +using u32_be = Common::BigEndian; +using u64_be = Common::BigEndian; + +using u16_le = Common::LittleEndian; +using u32_le = Common::LittleEndian; +using u64_le = Common::LittleEndian; diff --git a/src/common/io_file.h b/src/common/io_file.h index 11fafbeca..59cfcf7b5 100644 --- a/src/common/io_file.h +++ b/src/common/io_file.h @@ -178,6 +178,11 @@ public: return std::fread(&object, sizeof(T), 1, file) == 1; } + template + size_t WriteRaw(void* data, size_t size) const { + return std::fwrite(data, sizeof(T), size, file); + } + template bool WriteObject(const T& object) const { static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); diff --git a/src/core/crypto/crypto.cpp b/src/core/crypto/crypto.cpp new file mode 100644 index 000000000..d45b56519 --- /dev/null +++ b/src/core/crypto/crypto.cpp @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "crypto.h" + +RSA::PrivateKey Crypto::key_pkg_derived_key3_keyset_init() { + InvertibleRSAFunction params; + params.SetPrime1(Integer(pkg_derived_key3_keyset.Prime1, 0x80)); + params.SetPrime2(Integer(pkg_derived_key3_keyset.Prime2, 0x80)); + + params.SetPublicExponent(Integer(pkg_derived_key3_keyset.PublicExponent, 4)); + params.SetPrivateExponent(Integer(pkg_derived_key3_keyset.PrivateExponent, 0x100)); + + params.SetModPrime1PrivateExponent(Integer(pkg_derived_key3_keyset.Exponent1, 0x80)); + params.SetModPrime2PrivateExponent(Integer(pkg_derived_key3_keyset.Exponent2, 0x80)); + + params.SetModulus(Integer(pkg_derived_key3_keyset.Modulus, 0x100)); + params.SetMultiplicativeInverseOfPrime2ModPrime1( + Integer(pkg_derived_key3_keyset.Coefficient, 0x80)); + + RSA::PrivateKey privateKey(params); + + return privateKey; +} + +RSA::PrivateKey Crypto::FakeKeyset_keyset_init() { + InvertibleRSAFunction params; + params.SetPrime1(Integer(FakeKeyset_keyset.Prime1, 0x80)); + params.SetPrime2(Integer(FakeKeyset_keyset.Prime2, 0x80)); + + params.SetPublicExponent(Integer(FakeKeyset_keyset.PublicExponent, 4)); + params.SetPrivateExponent(Integer(FakeKeyset_keyset.PrivateExponent, 0x100)); + + params.SetModPrime1PrivateExponent(Integer(FakeKeyset_keyset.Exponent1, 0x80)); + params.SetModPrime2PrivateExponent(Integer(FakeKeyset_keyset.Exponent2, 0x80)); + + params.SetModulus(Integer(FakeKeyset_keyset.Modulus, 0x100)); + params.SetMultiplicativeInverseOfPrime2ModPrime1(Integer(FakeKeyset_keyset.Coefficient, 0x80)); + + RSA::PrivateKey privateKey(params); + + return privateKey; +} + +RSA::PrivateKey Crypto::DebugRifKeyset_init() { + AutoSeededRandomPool rng; + InvertibleRSAFunction params; + params.SetPrime1(Integer(DebugRifKeyset_keyset.Prime1, sizeof(DebugRifKeyset_keyset.Prime1))); + params.SetPrime2(Integer(DebugRifKeyset_keyset.Prime2, sizeof(DebugRifKeyset_keyset.Prime2))); + + params.SetPublicExponent(Integer(DebugRifKeyset_keyset.PrivateExponent, + sizeof(DebugRifKeyset_keyset.PrivateExponent))); + params.SetPrivateExponent(Integer(DebugRifKeyset_keyset.PrivateExponent, + sizeof(DebugRifKeyset_keyset.PrivateExponent))); + + params.SetModPrime1PrivateExponent( + Integer(DebugRifKeyset_keyset.Exponent1, sizeof(DebugRifKeyset_keyset.Exponent1))); + params.SetModPrime2PrivateExponent( + Integer(DebugRifKeyset_keyset.Exponent2, sizeof(DebugRifKeyset_keyset.Exponent2))); + + params.SetModulus( + Integer(DebugRifKeyset_keyset.Modulus, sizeof(DebugRifKeyset_keyset.Modulus))); + params.SetMultiplicativeInverseOfPrime2ModPrime1( + Integer(DebugRifKeyset_keyset.Coefficient, sizeof(DebugRifKeyset_keyset.Coefficient))); + + RSA::PrivateKey privateKey(params); + + return privateKey; +} + +void Crypto::RSA2048Decrypt(std::span dec_key, + std::span ciphertext, + bool is_dk3) { // RSAES_PKCS1v15_ + // Create an RSA decryptor + RSA::PrivateKey privateKey; + if (is_dk3) { + privateKey = key_pkg_derived_key3_keyset_init(); + } else { + privateKey = FakeKeyset_keyset_init(); + } + + RSAES_PKCS1v15_Decryptor rsaDecryptor(privateKey); + + // Allocate memory for the decrypted data + std::array decrypted; + + // Perform the decryption + AutoSeededRandomPool rng; + DecodingResult result = + rsaDecryptor.Decrypt(rng, ciphertext.data(), decrypted.size(), decrypted.data()); + std::copy(decrypted.begin(), decrypted.begin() + dec_key.size(), dec_key.begin()); +} + +void Crypto::ivKeyHASH256(std::span cipher_input, + std::span ivkey_result) { + CryptoPP::SHA256 sha256; + std::array hashResult; + auto array_sink = new CryptoPP::ArraySink(hashResult.data(), CryptoPP::SHA256::DIGESTSIZE); + auto filter = new CryptoPP::HashFilter(sha256, array_sink); + CryptoPP::ArraySource r(cipher_input.data(), cipher_input.size(), true, filter); + std::copy(hashResult.begin(), hashResult.begin() + ivkey_result.size(), ivkey_result.begin()); +} + +void Crypto::aesCbcCfb128Decrypt(std::span ivkey, + std::span ciphertext, + std::span decrypted) { + std::array key; + std::array iv; + + std::copy(ivkey.begin() + 16, ivkey.begin() + 16 + key.size(), key.begin()); + std::copy(ivkey.begin(), ivkey.begin() + iv.size(), iv.begin()); + + CryptoPP::AES::Decryption aesDecryption(key.data(), CryptoPP::AES::DEFAULT_KEYLENGTH); + CryptoPP::CBC_Mode_ExternalCipher::Decryption cbcDecryption(aesDecryption, iv.data()); + + for (size_t i = 0; i < decrypted.size(); i += CryptoPP::AES::BLOCKSIZE) { + cbcDecryption.ProcessData(decrypted.data() + i, ciphertext.data() + i, + CryptoPP::AES::BLOCKSIZE); + } +} + +void Crypto::PfsGenCryptoKey(std::span ekpfs, + std::span seed, + std::span dataKey, + std::span tweakKey) { + CryptoPP::HMAC hmac(ekpfs.data(), ekpfs.size()); + + CryptoPP::SecByteBlock d(20); // Use Crypto++ SecByteBlock for better memory management + + // Copy the bytes of 'index' to the 'd' array + uint32_t index = 1; + std::memcpy(d, &index, sizeof(uint32_t)); + + // Copy the bytes of 'seed' to the 'd' array starting from index 4 + std::memcpy(d + sizeof(uint32_t), seed.data(), seed.size()); + + // Allocate memory for 'u64' using new + std::vector data_tweak_key(hmac.DigestSize()); + + // Calculate the HMAC + hmac.CalculateDigest(data_tweak_key.data(), d, d.size()); + std::copy(data_tweak_key.begin(), data_tweak_key.begin() + dataKey.size(), tweakKey.begin()); + std::copy(data_tweak_key.begin() + tweakKey.size(), + data_tweak_key.begin() + tweakKey.size() + dataKey.size(), dataKey.begin()); +} + +void Crypto::decryptPFS(std::span dataKey, + std::span tweakKey, std::span src_image, + std::span dst_image, u64 sector) { + // Start at 0x10000 to keep the header when decrypting the whole pfs_image. + for (int i = 0; i < src_image.size(); i += 0x1000) { + const u64 current_sector = sector + (i / 0x1000); + CryptoPP::ECB_Mode::Encryption encrypt(tweakKey.data(), tweakKey.size()); + CryptoPP::ECB_Mode::Decryption decrypt(dataKey.data(), dataKey.size()); + + std::array tweak{}; + std::array encryptedTweak; + std::array xorBuffer; + std::memcpy(tweak.data(), ¤t_sector, sizeof(u64)); + + // Encrypt the tweak for each sector. + encrypt.ProcessData(encryptedTweak.data(), tweak.data(), 16); + + for (int plaintextOffset = 0; plaintextOffset < 0x1000; plaintextOffset += 16) { + xtsXorBlock(xorBuffer.data(), src_image.data() + i + plaintextOffset, + encryptedTweak.data()); // x, c, t + decrypt.ProcessData(xorBuffer.data(), xorBuffer.data(), 16); // x, x + xtsXorBlock(dst_image.data() + i + plaintextOffset, xorBuffer.data(), + encryptedTweak.data()); //(p) c, x , t + xtsMult(encryptedTweak); + } + } +} diff --git a/src/core/crypto/crypto.h b/src/core/crypto/crypto.h new file mode 100644 index 000000000..11edef843 --- /dev/null +++ b/src/core/crypto/crypto.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/types.h" +#include "keys.h" + +using namespace CryptoPP; + +class Crypto { +public: + PkgDerivedKey3Keyset pkg_derived_key3_keyset; + FakeKeyset FakeKeyset_keyset; + DebugRifKeyset DebugRifKeyset_keyset; + + RSA::PrivateKey key_pkg_derived_key3_keyset_init(); + RSA::PrivateKey FakeKeyset_keyset_init(); + RSA::PrivateKey DebugRifKeyset_init(); + + void RSA2048Decrypt(std::span dk3, + std::span ciphertext, + bool is_dk3); // RSAES_PKCS1v15_ + void ivKeyHASH256(std::span cipher_input, + std::span ivkey_result); + void aesCbcCfb128Decrypt(std::span ivkey, + std::span ciphertext, + std::span decrypted); + void PfsGenCryptoKey(std::span ekpfs, + std::span seed, + std::span dataKey, + std::span tweakKey); + void decryptPFS(std::span dataKey, + std::span tweakKey, std::span src_image, + std::span dst_image, u64 sector); + + void xtsXorBlock(CryptoPP::byte* x, const CryptoPP::byte* a, const CryptoPP::byte* b) { + for (int i = 0; i < 16; i++) { + x[i] = a[i] ^ b[i]; + } + } + + void xtsMult(std::span encryptedTweak) { + int feedback = 0; + for (int k = 0; k < encryptedTweak.size(); k++) { + const auto tmp = (encryptedTweak[k] >> 7) & 1; + encryptedTweak[k] = ((encryptedTweak[k] << 1) + feedback) & 0xFF; + feedback = tmp; + } + if (feedback != 0) { + encryptedTweak[0] ^= 0x87; + } + } +}; diff --git a/src/core/crypto/keys.h b/src/core/crypto/keys.h new file mode 100644 index 000000000..5b8a88622 --- /dev/null +++ b/src/core/crypto/keys.h @@ -0,0 +1,389 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +class FakeKeyset { +public: + // Constructor + const CryptoPP::byte* Exponent1; + // exponent2 = d mod (q - 1) + const CryptoPP::byte* Exponent2; + // e + const CryptoPP::byte* PublicExponent; + // (InverseQ)(q) = 1 mod p + const CryptoPP::byte* Coefficient; + // n = p * q + const CryptoPP::byte* Modulus; + // p + const CryptoPP::byte* Prime1; + // q + const CryptoPP::byte* Prime2; + const CryptoPP::byte* PrivateExponent; + + // Constructor + FakeKeyset() { + // Initialize PrivateExponent + PrivateExponent = new CryptoPP::byte[0x100]{ + 0x7F, 0x76, 0xCD, 0x0E, 0xE2, 0xD4, 0xDE, 0x05, 0x1C, 0xC6, 0xD9, 0xA8, 0x0E, 0x8D, + 0xFA, 0x7B, 0xCA, 0x1E, 0xAA, 0x27, 0x1A, 0x40, 0xF8, 0xF1, 0x22, 0x87, 0x35, 0xDD, + 0xDB, 0xFD, 0xEE, 0xF8, 0xC2, 0xBC, 0xBD, 0x01, 0xFB, 0x8B, 0xE2, 0x3E, 0x63, 0xB2, + 0xB1, 0x22, 0x5C, 0x56, 0x49, 0x6E, 0x11, 0xBE, 0x07, 0x44, 0x0B, 0x9A, 0x26, 0x66, + 0xD1, 0x49, 0x2C, 0x8F, 0xD3, 0x1B, 0xCF, 0xA4, 0xA1, 0xB8, 0xD1, 0xFB, 0xA4, 0x9E, + 0xD2, 0x21, 0x28, 0x83, 0x09, 0x8A, 0xF6, 0xA0, 0x0B, 0xA3, 0xD6, 0x0F, 0x9B, 0x63, + 0x68, 0xCC, 0xBC, 0x0C, 0x4E, 0x14, 0x5B, 0x27, 0xA4, 0xA9, 0xF4, 0x2B, 0xB9, 0xB8, + 0x7B, 0xC0, 0xE6, 0x51, 0xAD, 0x1D, 0x77, 0xD4, 0x6B, 0xB9, 0xCE, 0x20, 0xD1, 0x26, + 0x66, 0x7E, 0x5E, 0x9E, 0xA2, 0xE9, 0x6B, 0x90, 0xF3, 0x73, 0xB8, 0x52, 0x8F, 0x44, + 0x11, 0x03, 0x0C, 0x13, 0x97, 0x39, 0x3D, 0x13, 0x22, 0x58, 0xD5, 0x43, 0x82, 0x49, + 0xDA, 0x6E, 0x7C, 0xA1, 0xC5, 0x8C, 0xA5, 0xB0, 0x09, 0xE0, 0xCE, 0x3D, 0xDF, 0xF4, + 0x9D, 0x3C, 0x97, 0x15, 0xE2, 0x6A, 0xC7, 0x2B, 0x3C, 0x50, 0x93, 0x23, 0xDB, 0xBA, + 0x4A, 0x22, 0x66, 0x44, 0xAC, 0x78, 0xBB, 0x0E, 0x1A, 0x27, 0x43, 0xB5, 0x71, 0x67, + 0xAF, 0xF4, 0xAB, 0x48, 0x46, 0x93, 0x73, 0xD0, 0x42, 0xAB, 0x93, 0x63, 0xE5, 0x6C, + 0x9A, 0xDE, 0x50, 0x24, 0xC0, 0x23, 0x7D, 0x99, 0x79, 0x3F, 0x22, 0x07, 0xE0, 0xC1, + 0x48, 0x56, 0x1B, 0xDF, 0x83, 0x09, 0x12, 0xB4, 0x2D, 0x45, 0x6B, 0xC9, 0xC0, 0x68, + 0x85, 0x99, 0x90, 0x79, 0x96, 0x1A, 0xD7, 0xF5, 0x4D, 0x1F, 0x37, 0x83, 0x40, 0x4A, + 0xEC, 0x39, 0x37, 0xA6, 0x80, 0x92, 0x7D, 0xC5, 0x80, 0xC7, 0xD6, 0x6F, 0xFE, 0x8A, + 0x79, 0x89, 0xC6, 0xB1}; + + // Initialize Exponent1 + Exponent1 = new CryptoPP::byte[0x80]{ + 0x6D, 0x48, 0xE0, 0x54, 0x40, 0x25, 0xC8, 0x41, 0x29, 0x52, 0x42, 0x27, 0xEB, + 0xD2, 0xC7, 0xAB, 0x6B, 0x9C, 0x27, 0x0A, 0xB4, 0x1F, 0x94, 0x4E, 0xFA, 0x42, + 0x1D, 0xB7, 0xBC, 0xB9, 0xAE, 0xBC, 0x04, 0x6F, 0x75, 0x8F, 0x10, 0x5F, 0x89, + 0xAC, 0xAB, 0x9C, 0xD2, 0xFA, 0xE6, 0xA4, 0x13, 0x83, 0x68, 0xD4, 0x56, 0x38, + 0xFE, 0xE5, 0x2B, 0x78, 0x44, 0x9C, 0x34, 0xE6, 0x5A, 0xA0, 0xBE, 0x05, 0x70, + 0xAD, 0x15, 0xC3, 0x2D, 0x31, 0xAC, 0x97, 0x5D, 0x88, 0xFC, 0xC1, 0x62, 0x3D, + 0xE2, 0xED, 0x11, 0xDB, 0xB6, 0x9E, 0xFC, 0x5A, 0x5A, 0x03, 0xF6, 0xCF, 0x08, + 0xD4, 0x5D, 0x90, 0xC9, 0x2A, 0xB9, 0x9B, 0xCF, 0xC8, 0x1A, 0x65, 0xF3, 0x5B, + 0xE8, 0x7F, 0xCF, 0xA5, 0xA6, 0x4C, 0x5C, 0x2A, 0x12, 0x0F, 0x92, 0xA5, 0xE3, + 0xF0, 0x17, 0x1E, 0x9A, 0x97, 0x45, 0x86, 0xFD, 0xDB, 0x54, 0x25 + + }; + + Exponent2 = new CryptoPP::byte[0x80]{ + 0x2A, 0x51, 0xCE, 0x02, 0x44, 0x28, 0x50, 0xE8, 0x30, 0x20, 0x7C, 0x9C, 0x55, + 0xBF, 0x60, 0x39, 0xBC, 0xD1, 0xF0, 0xE7, 0x68, 0xF8, 0x08, 0x5B, 0x61, 0x1F, + 0xA7, 0xBF, 0xD0, 0xE8, 0x8B, 0xB5, 0xB1, 0xD5, 0xD9, 0x16, 0xAC, 0x75, 0x0C, + 0x6D, 0xF2, 0xE0, 0xB5, 0x97, 0x75, 0xD2, 0x68, 0x16, 0x1F, 0x00, 0x7D, 0x8B, + 0x17, 0xE8, 0x78, 0x48, 0x41, 0x71, 0x2B, 0x18, 0x96, 0x80, 0x11, 0xDB, 0x68, + 0x39, 0x9C, 0xD6, 0xE0, 0x72, 0x42, 0x86, 0xF0, 0x1B, 0x16, 0x0D, 0x3E, 0x12, + 0x94, 0x3D, 0x25, 0xA8, 0xA9, 0x30, 0x9E, 0x54, 0x5A, 0xD6, 0x36, 0x6C, 0xD6, + 0x8C, 0x20, 0x62, 0x8F, 0xA1, 0x6B, 0x1F, 0x7C, 0x6D, 0xB2, 0xB1, 0xC1, 0x2E, + 0xAD, 0x36, 0x02, 0x9C, 0x3A, 0xCA, 0x2F, 0x09, 0xD2, 0x45, 0x9E, 0xEB, 0xF2, + 0xBC, 0x6C, 0xAA, 0x3B, 0x3E, 0x90, 0xBC, 0x38, 0x67, 0x35, 0x4D}; + + PublicExponent = new CryptoPP::byte[4]{0, 1, 0, 1}; + + Coefficient = new CryptoPP::byte[0x80]{ + 0x0B, 0x67, 0x1C, 0x0D, 0x6C, 0x57, 0xD3, 0xE7, 0x05, 0x65, 0x94, 0x31, 0x56, + 0x55, 0xFD, 0x28, 0x08, 0xFA, 0x05, 0x8A, 0xCC, 0x55, 0x39, 0x61, 0x97, 0x63, + 0xA0, 0x16, 0x27, 0x3D, 0xED, 0xC1, 0x16, 0x40, 0x2A, 0x12, 0xEA, 0x6F, 0xD9, + 0xD8, 0x58, 0x56, 0xA8, 0x56, 0x8B, 0x0D, 0x38, 0x5E, 0x1E, 0x80, 0x3B, 0x5F, + 0x40, 0x80, 0x6F, 0x62, 0x4F, 0x28, 0xA2, 0x69, 0xF3, 0xD3, 0xF7, 0xFD, 0xB2, + 0xC3, 0x52, 0x43, 0x20, 0x92, 0x9D, 0x97, 0x8D, 0xA0, 0x15, 0x07, 0x15, 0x6E, + 0xA4, 0x0D, 0x56, 0xD3, 0x37, 0x1A, 0xC4, 0x9E, 0xDF, 0x02, 0x49, 0xB8, 0x0A, + 0x84, 0x62, 0xF5, 0xFA, 0xB9, 0x3F, 0xA4, 0x09, 0x76, 0xCC, 0xAA, 0xB9, 0x9B, + 0xA6, 0x4F, 0xC1, 0x6A, 0x64, 0xCE, 0xD8, 0x77, 0xAB, 0x4B, 0xF9, 0xA0, 0xAE, + 0xDA, 0xF1, 0x67, 0x87, 0x7C, 0x98, 0x5C, 0x7E, 0xB8, 0x73, 0xF5}; + + Modulus = new CryptoPP::byte[0x100]{ + 0xC6, 0xCF, 0x71, 0xE7, 0xE5, 0x9A, 0xF0, 0xD1, 0x2A, 0x2C, 0x45, 0x8B, 0xF9, 0x2A, + 0x0E, 0xC1, 0x43, 0x05, 0x8B, 0xC3, 0x71, 0x17, 0x80, 0x1D, 0xCD, 0x49, 0x7D, 0xDE, + 0x35, 0x9D, 0x25, 0x9B, 0xA0, 0xD7, 0xA0, 0xF2, 0x7D, 0x6C, 0x08, 0x7E, 0xAA, 0x55, + 0x02, 0x68, 0x2B, 0x23, 0xC6, 0x44, 0xB8, 0x44, 0x18, 0xEB, 0x56, 0xCF, 0x16, 0xA2, + 0x48, 0x03, 0xC9, 0xE7, 0x4F, 0x87, 0xEB, 0x3D, 0x30, 0xC3, 0x15, 0x88, 0xBF, 0x20, + 0xE7, 0x9D, 0xFF, 0x77, 0x0C, 0xDE, 0x1D, 0x24, 0x1E, 0x63, 0xA9, 0x4F, 0x8A, 0xBF, + 0x5B, 0xBE, 0x60, 0x19, 0x68, 0x33, 0x3B, 0xFC, 0xED, 0x9F, 0x47, 0x4E, 0x5F, 0xF8, + 0xEA, 0xCB, 0x3D, 0x00, 0xBD, 0x67, 0x01, 0xF9, 0x2C, 0x6D, 0xC6, 0xAC, 0x13, 0x64, + 0xE7, 0x67, 0x14, 0xF3, 0xDC, 0x52, 0x69, 0x6A, 0xB9, 0x83, 0x2C, 0x42, 0x30, 0x13, + 0x1B, 0xB2, 0xD8, 0xA5, 0x02, 0x0D, 0x79, 0xED, 0x96, 0xB1, 0x0D, 0xF8, 0xCC, 0x0C, + 0xDF, 0x81, 0x95, 0x4F, 0x03, 0x58, 0x09, 0x57, 0x0E, 0x80, 0x69, 0x2E, 0xFE, 0xFF, + 0x52, 0x77, 0xEA, 0x75, 0x28, 0xA8, 0xFB, 0xC9, 0xBE, 0xBF, 0x9F, 0xBB, 0xB7, 0x79, + 0x8E, 0x18, 0x05, 0xE1, 0x80, 0xBD, 0x50, 0x34, 0x94, 0x81, 0xD3, 0x53, 0xC2, 0x69, + 0xA2, 0xD2, 0x4C, 0xCF, 0x6C, 0xF4, 0x57, 0x2C, 0x10, 0x4A, 0x3F, 0xFB, 0x22, 0xFD, + 0x8B, 0x97, 0xE2, 0xC9, 0x5B, 0xA6, 0x2B, 0xCD, 0xD6, 0x1B, 0x6B, 0xDB, 0x68, 0x7F, + 0x4B, 0xC2, 0xA0, 0x50, 0x34, 0xC0, 0x05, 0xE5, 0x8D, 0xEF, 0x24, 0x67, 0xFF, 0x93, + 0x40, 0xCF, 0x2D, 0x62, 0xA2, 0xA0, 0x50, 0xB1, 0xF1, 0x3A, 0xA8, 0x3D, 0xFD, 0x80, + 0xD1, 0xF9, 0xB8, 0x05, 0x22, 0xAF, 0xC8, 0x35, 0x45, 0x90, 0x58, 0x8E, 0xE3, 0x3A, + 0x7C, 0xBD, 0x3E, 0x27}; + + Prime1 = new CryptoPP::byte[0x80]{ + 0xFE, 0xF6, 0xBF, 0x1D, 0x69, 0xAB, 0x16, 0x25, 0x08, 0x47, 0x55, 0x6B, 0x86, + 0xE4, 0x35, 0x88, 0x72, 0x2A, 0xB1, 0x3D, 0xF8, 0xB6, 0x44, 0xCA, 0xB3, 0xAB, + 0x19, 0xD1, 0x04, 0x24, 0x28, 0x0A, 0x74, 0x55, 0xB8, 0x15, 0x45, 0x09, 0xCC, + 0x13, 0x1C, 0xF2, 0xBA, 0x37, 0xA9, 0x03, 0x90, 0x8F, 0x02, 0x10, 0xFF, 0x25, + 0x79, 0x86, 0xCC, 0x18, 0x50, 0x9A, 0x10, 0x5F, 0x5B, 0x4C, 0x1C, 0x4E, 0xB0, + 0xA7, 0xE3, 0x59, 0xB1, 0x2D, 0xA0, 0xC6, 0xB0, 0x20, 0x2C, 0x21, 0x33, 0x12, + 0xB3, 0xAF, 0x72, 0x34, 0x83, 0xCD, 0x52, 0x2F, 0xAF, 0x0F, 0x20, 0x5A, 0x1B, + 0xC0, 0xE2, 0xA3, 0x76, 0x34, 0x0F, 0xD7, 0xFC, 0xC1, 0x41, 0xC9, 0xF9, 0x79, + 0x40, 0x17, 0x42, 0x21, 0x3E, 0x9D, 0xFD, 0xC7, 0xC1, 0x50, 0xDE, 0x44, 0x5A, + 0xC9, 0x31, 0x89, 0x6A, 0x78, 0x05, 0xBE, 0x65, 0xB4, 0xE8, 0x2D}; + + Prime2 = new CryptoPP::byte[0x80]{ + 0xC7, 0x9E, 0x47, 0x58, 0x00, 0x7D, 0x62, 0x82, 0xB0, 0xD2, 0x22, 0x81, 0xD4, + 0xA8, 0x97, 0x1B, 0x79, 0x0C, 0x3A, 0xB0, 0xD7, 0xC9, 0x30, 0xE3, 0xC3, 0x53, + 0x8E, 0x57, 0xEF, 0xF0, 0x9B, 0x9F, 0xB3, 0x90, 0x52, 0xC6, 0x94, 0x22, 0x36, + 0xAA, 0xE6, 0x4A, 0x5F, 0x72, 0x1D, 0x70, 0xE8, 0x76, 0x58, 0xC8, 0xB2, 0x91, + 0xCE, 0x9C, 0xC3, 0xE9, 0x09, 0x7F, 0x2E, 0x47, 0x97, 0xCC, 0x90, 0x39, 0x15, + 0x35, 0x31, 0xDE, 0x1F, 0x0C, 0x8C, 0x0D, 0xC1, 0xC2, 0x92, 0xBE, 0x97, 0xBF, + 0x2F, 0x91, 0xA1, 0x8C, 0x7D, 0x50, 0xA8, 0x21, 0x2F, 0xD7, 0xA2, 0x9A, 0x7E, + 0xB5, 0xA7, 0x2A, 0x90, 0x02, 0xD9, 0xF3, 0x3D, 0xD1, 0xEB, 0xB8, 0xE0, 0x5A, + 0x79, 0x9E, 0x7D, 0x8D, 0xCA, 0x18, 0x6D, 0xBD, 0x9E, 0xA1, 0x80, 0x28, 0x6B, + 0x2A, 0xFE, 0x51, 0x24, 0x9B, 0x6F, 0x4D, 0x84, 0x77, 0x80, 0x23}; + }; +}; + +class DebugRifKeyset { +public: + // Constructor + // std::uint8_t* PrivateExponent; + const CryptoPP::byte* Exponent1; + // exponent2 = d mod (q - 1) + const CryptoPP::byte* Exponent2; + // e + const CryptoPP::byte* PublicExponent; + // (InverseQ)(q) = 1 mod p + const CryptoPP::byte* Coefficient; + // n = p * q + const CryptoPP::byte* Modulus; + // p + const CryptoPP::byte* Prime1; + // q + const CryptoPP::byte* Prime2; + const CryptoPP::byte* PrivateExponent; + + // Constructor + DebugRifKeyset() { + // Initialize PrivateExponent + PrivateExponent = new CryptoPP::byte[0x100]{ + 0x01, 0x61, 0xAD, 0xD8, 0x9C, 0x06, 0x89, 0xD0, 0x60, 0xC8, 0x41, 0xF0, 0xB3, 0x83, + 0x01, 0x5D, 0xE3, 0xA2, 0x6B, 0xA2, 0xBA, 0x9A, 0x0A, 0x58, 0xCD, 0x1A, 0xA0, 0x97, + 0x64, 0xEC, 0xD0, 0x31, 0x1F, 0xCA, 0x36, 0x0E, 0x69, 0xDD, 0x40, 0xF7, 0x4E, 0xC0, + 0xC6, 0xA3, 0x73, 0xF0, 0x69, 0x84, 0xB2, 0xF4, 0x4B, 0x29, 0x14, 0x2A, 0x6D, 0xB8, + 0x23, 0xD8, 0x1B, 0x61, 0xD4, 0x9E, 0x87, 0xB3, 0xBB, 0xA9, 0xC4, 0x85, 0x4A, 0xF8, + 0x03, 0x4A, 0xBF, 0xFE, 0xF9, 0xFE, 0x8B, 0xDD, 0x54, 0x83, 0xBA, 0xE0, 0x2F, 0x3F, + 0xB1, 0xEF, 0xA5, 0x05, 0x5D, 0x28, 0x8B, 0xAB, 0xB5, 0xD0, 0x23, 0x2F, 0x8A, 0xCF, + 0x48, 0x7C, 0xAA, 0xBB, 0xC8, 0x5B, 0x36, 0x27, 0xC5, 0x16, 0xA4, 0xB6, 0x61, 0xAC, + 0x0C, 0x28, 0x47, 0x79, 0x3F, 0x38, 0xAE, 0x5E, 0x25, 0xC6, 0xAF, 0x35, 0xAE, 0xBC, + 0xB0, 0xF3, 0xBC, 0xBD, 0xFD, 0xA4, 0x87, 0x0D, 0x14, 0x3D, 0x90, 0xE4, 0xDE, 0x5D, + 0x1D, 0x46, 0x81, 0xF1, 0x28, 0x6D, 0x2F, 0x2C, 0x5E, 0x97, 0x2D, 0x89, 0x2A, 0x51, + 0x72, 0x3C, 0x20, 0x02, 0x59, 0xB1, 0x98, 0x93, 0x05, 0x1E, 0x3F, 0xA1, 0x8A, 0x69, + 0x30, 0x0E, 0x70, 0x84, 0x8B, 0xAE, 0x97, 0xA1, 0x08, 0x95, 0x63, 0x4C, 0xC7, 0xE8, + 0x5D, 0x59, 0xCA, 0x78, 0x2A, 0x23, 0x87, 0xAC, 0x6F, 0x04, 0x33, 0xB1, 0x61, 0xB9, + 0xF0, 0x95, 0xDA, 0x33, 0xCC, 0xE0, 0x4C, 0x82, 0x68, 0x82, 0x14, 0x51, 0xBE, 0x49, + 0x1C, 0x58, 0xA2, 0x8B, 0x05, 0x4E, 0x98, 0x37, 0xEB, 0x94, 0x0B, 0x01, 0x22, 0xDC, + 0xB3, 0x19, 0xCA, 0x77, 0xA6, 0x6E, 0x97, 0xFF, 0x8A, 0x53, 0x5A, 0xC5, 0x24, 0xE4, + 0xAF, 0x6E, 0xA8, 0x2B, 0x53, 0xA4, 0xBE, 0x96, 0xA5, 0x7B, 0xCE, 0x22, 0x56, 0xA3, + 0xF1, 0xCF, 0x14, 0xA5}; + + // Initialize Exponent1 + Exponent1 = new CryptoPP::byte[0x80]{ + 0xCD, 0x9A, 0x61, 0xB0, 0xB8, 0xD5, 0xB4, 0xE4, 0xE4, 0xF6, 0xAB, 0xF7, 0x27, + 0xB7, 0x56, 0x59, 0x6B, 0xB9, 0x11, 0xE7, 0xF4, 0x83, 0xAF, 0xB9, 0x73, 0x99, + 0x7F, 0x49, 0xA2, 0x9C, 0xF0, 0xB5, 0x6D, 0x37, 0x82, 0x14, 0x15, 0xF1, 0x04, + 0x8A, 0xD4, 0x8E, 0xEB, 0x2E, 0x1F, 0xE2, 0x81, 0xA9, 0x62, 0x6E, 0xB1, 0x68, + 0x75, 0x62, 0xF3, 0x0F, 0xFE, 0xD4, 0x91, 0x87, 0x98, 0x78, 0xBF, 0x26, 0xB5, + 0x07, 0x58, 0xD0, 0xEE, 0x3F, 0x21, 0xE8, 0xC8, 0x0F, 0x5F, 0xFA, 0x1C, 0x64, + 0x74, 0x49, 0x52, 0xEB, 0xE7, 0xEE, 0xDE, 0xBA, 0x23, 0x26, 0x4A, 0xF6, 0x9C, + 0x1A, 0x09, 0x3F, 0xB9, 0x0B, 0x36, 0x26, 0x1A, 0xBE, 0xA9, 0x76, 0xE6, 0xF2, + 0x69, 0xDE, 0xFF, 0xAF, 0xCC, 0x0C, 0x9A, 0x66, 0x03, 0x86, 0x0A, 0x1F, 0x49, + 0xA4, 0x10, 0xB6, 0xBC, 0xC3, 0x7C, 0x88, 0xE8, 0xCE, 0x4B, 0xD9 + + }; + + Exponent2 = new CryptoPP::byte[0x80]{ + 0xB3, 0x73, 0xA3, 0x59, 0xE6, 0x97, 0xC0, 0xAB, 0x3B, 0x68, 0xFC, 0x39, 0xAC, + 0xDB, 0x44, 0xB1, 0xB4, 0x9E, 0x35, 0x4D, 0xBE, 0xC5, 0x36, 0x69, 0x6C, 0x3D, + 0xC5, 0xFC, 0xFE, 0x4B, 0x2F, 0xDC, 0x86, 0x80, 0x46, 0x96, 0x40, 0x1A, 0x0D, + 0x6E, 0xFA, 0x8C, 0xE0, 0x47, 0x91, 0xAC, 0xAD, 0x95, 0x2B, 0x8E, 0x1F, 0xF2, + 0x0A, 0x45, 0xF8, 0x29, 0x95, 0x70, 0xC6, 0x88, 0x5F, 0x71, 0x03, 0x99, 0x79, + 0xBC, 0x84, 0x71, 0xBD, 0xE8, 0x84, 0x8C, 0x0E, 0xD4, 0x7B, 0x30, 0x74, 0x57, + 0x1A, 0x95, 0xE7, 0x90, 0x19, 0x8D, 0xAD, 0x8B, 0x4C, 0x4E, 0xC3, 0xE7, 0x6B, + 0x23, 0x86, 0x01, 0xEE, 0x9B, 0xE0, 0x2F, 0x15, 0xA2, 0x2C, 0x4C, 0x39, 0xD3, + 0xDF, 0x9C, 0x39, 0x01, 0xF1, 0x8C, 0x44, 0x4A, 0x15, 0x44, 0xDC, 0x51, 0xF7, + 0x22, 0xD7, 0x7F, 0x41, 0x7F, 0x68, 0xFA, 0xEE, 0x56, 0xE8, 0x05}; + + PublicExponent = new CryptoPP::byte[4]{0x00, 0x01, 0x00, 0x01}; + + Coefficient = new CryptoPP::byte[0x80]{ + 0xC0, 0x32, 0x43, 0xD3, 0x8C, 0x3D, 0xB4, 0xD2, 0x48, 0x8C, 0x42, 0x41, 0x24, + 0x94, 0x6C, 0x80, 0xC9, 0xC1, 0x79, 0x36, 0x7F, 0xAC, 0xC3, 0xFF, 0x6A, 0x25, + 0xEB, 0x2C, 0xFB, 0xD4, 0x2B, 0xA0, 0xEB, 0xFE, 0x25, 0xE9, 0xC6, 0x77, 0xCE, + 0xFE, 0x2D, 0x23, 0xFE, 0xD0, 0xF4, 0x0F, 0xD9, 0x7E, 0xD5, 0xA5, 0x7D, 0x1F, + 0xC0, 0xE8, 0xE8, 0xEC, 0x80, 0x5B, 0xC7, 0xFD, 0xE2, 0xBD, 0x94, 0xA6, 0x2B, + 0xDD, 0x6A, 0x60, 0x45, 0x54, 0xAB, 0xCA, 0x42, 0x9C, 0x6A, 0x6C, 0xBF, 0x3C, + 0x84, 0xF9, 0xA5, 0x0E, 0x63, 0x0C, 0x51, 0x58, 0x62, 0x6D, 0x5A, 0xB7, 0x3C, + 0x3F, 0x49, 0x1A, 0xD0, 0x93, 0xB8, 0x4F, 0x1A, 0x6C, 0x5F, 0xC5, 0xE5, 0xA9, + 0x75, 0xD4, 0x86, 0x9E, 0xDF, 0x87, 0x0F, 0x27, 0xB0, 0x26, 0x78, 0x4E, 0xFB, + 0xC1, 0x8A, 0x4A, 0x24, 0x3F, 0x7F, 0x8F, 0x9A, 0x12, 0x51, 0xCB}; + + Modulus = new CryptoPP::byte[0x100]{ + 0xC2, 0xD2, 0x44, 0xBC, 0xDD, 0x84, 0x3F, 0xD9, 0xC5, 0x22, 0xAF, 0xF7, 0xFC, 0x88, + 0x8A, 0x33, 0x80, 0xED, 0x8E, 0xE2, 0xCC, 0x81, 0xF7, 0xEC, 0xF8, 0x1C, 0x79, 0xBF, + 0x02, 0xBB, 0x12, 0x8E, 0x61, 0x68, 0x29, 0x1B, 0x15, 0xB6, 0x5E, 0xC6, 0xF8, 0xBF, + 0x5A, 0xE0, 0x3B, 0x6A, 0x6C, 0xD9, 0xD6, 0xF5, 0x75, 0xAB, 0xA0, 0x6F, 0x34, 0x81, + 0x34, 0x9A, 0x5B, 0xAD, 0xED, 0x31, 0xE3, 0xC6, 0xEA, 0x1A, 0xD1, 0x13, 0x22, 0xBB, + 0xB3, 0xDA, 0xB3, 0xB2, 0x53, 0xBD, 0x45, 0x79, 0x87, 0xAD, 0x0A, 0x01, 0x72, 0x18, + 0x10, 0x29, 0x49, 0xF4, 0x41, 0x7F, 0xD6, 0x47, 0x0C, 0x72, 0x92, 0x9E, 0xE9, 0xBB, + 0x95, 0xA9, 0x5D, 0x79, 0xEB, 0xE4, 0x30, 0x76, 0x90, 0x45, 0x4B, 0x9D, 0x9C, 0xCF, + 0x92, 0x03, 0x60, 0x8C, 0x4B, 0x6C, 0xB3, 0x7A, 0x3A, 0x05, 0x39, 0xA0, 0x66, 0xA9, + 0x35, 0xCF, 0xB9, 0xFA, 0xAD, 0x9C, 0xAB, 0xEB, 0xE4, 0x6A, 0x8C, 0xE9, 0x3B, 0xCC, + 0x72, 0x12, 0x62, 0x63, 0xBD, 0x80, 0xC4, 0xEE, 0x37, 0x2B, 0x32, 0x03, 0xA3, 0x09, + 0xF7, 0xA0, 0x61, 0x57, 0xAD, 0x0D, 0xCF, 0x15, 0x98, 0x9E, 0x4E, 0x49, 0xF8, 0xB5, + 0xA3, 0x5C, 0x27, 0xEE, 0x45, 0x04, 0xEA, 0xE4, 0x4B, 0xBC, 0x8F, 0x87, 0xED, 0x19, + 0x1E, 0x46, 0x75, 0x63, 0xC4, 0x5B, 0xD5, 0xBC, 0x09, 0x2F, 0x02, 0x73, 0x19, 0x3C, + 0x58, 0x55, 0x49, 0x66, 0x4C, 0x11, 0xEC, 0x0F, 0x09, 0xFA, 0xA5, 0x56, 0x0A, 0x5A, + 0x63, 0x56, 0xAD, 0xA0, 0x0D, 0x86, 0x08, 0xC1, 0xE6, 0xB6, 0x13, 0x22, 0x49, 0x2F, + 0x7C, 0xDB, 0x4C, 0x56, 0x97, 0x0E, 0xC2, 0xD9, 0x2E, 0x87, 0xBC, 0x0E, 0x67, 0xC0, + 0x1B, 0x58, 0xBC, 0x64, 0x2B, 0xC2, 0x6E, 0xE2, 0x93, 0x2E, 0xB5, 0x6B, 0x70, 0xA4, + 0x42, 0x9F, 0x64, 0xC1}; + + Prime1 = new CryptoPP::byte[0x80]{ + 0xE5, 0x62, 0xE1, 0x7F, 0x9F, 0x86, 0x08, 0xE2, 0x61, 0xD3, 0xD0, 0x42, 0xE2, + 0xC4, 0xB6, 0xA8, 0x51, 0x09, 0x19, 0x14, 0xA4, 0x3A, 0x11, 0x4C, 0x33, 0xA5, + 0x9C, 0x01, 0x5E, 0x34, 0xB6, 0x3F, 0x02, 0x1A, 0xCA, 0x47, 0xF1, 0x4F, 0x3B, + 0x35, 0x2A, 0x07, 0x20, 0xEC, 0xD8, 0xC1, 0x15, 0xD9, 0xCA, 0x03, 0x4F, 0xB8, + 0xE8, 0x09, 0x73, 0x3F, 0x85, 0xB7, 0x41, 0xD5, 0x51, 0x3E, 0x7B, 0xE3, 0x53, + 0x2B, 0x48, 0x8B, 0x8E, 0xCB, 0xBA, 0xF7, 0xE0, 0x60, 0xF5, 0x35, 0x0E, 0x6F, + 0xB0, 0xD9, 0x2A, 0x99, 0xD0, 0xFF, 0x60, 0x14, 0xED, 0x40, 0xEA, 0xF8, 0xD7, + 0x0B, 0xC3, 0x8D, 0x8C, 0xE8, 0x81, 0xB3, 0x75, 0x93, 0x15, 0xB3, 0x7D, 0xF6, + 0x39, 0x60, 0x1A, 0x00, 0xE7, 0xC3, 0x27, 0xAD, 0xA4, 0x33, 0xD5, 0x3E, 0xA4, + 0x35, 0x48, 0x6F, 0x22, 0xEF, 0x5D, 0xDD, 0x7D, 0x7B, 0x61, 0x05}; + + Prime2 = new CryptoPP::byte[0x80]{ + 0xD9, 0x6C, 0xC2, 0x0C, 0xF7, 0xAE, 0xD1, 0xF3, 0x3B, 0x3B, 0x49, 0x1E, 0x9F, + 0x12, 0x9C, 0xA1, 0x78, 0x1F, 0x35, 0x1D, 0x98, 0x26, 0x13, 0x71, 0xF9, 0x09, + 0xFD, 0xF0, 0xAD, 0x38, 0x55, 0xB7, 0xEE, 0x61, 0x04, 0x72, 0x51, 0x87, 0x2E, + 0x05, 0x84, 0xB1, 0x1D, 0x0C, 0x0D, 0xDB, 0xD4, 0x25, 0x3E, 0x26, 0xED, 0xEA, + 0xB8, 0xF7, 0x49, 0xFE, 0xA2, 0x94, 0xE6, 0xF2, 0x08, 0x92, 0xA7, 0x85, 0xF5, + 0x30, 0xB9, 0x84, 0x22, 0xBF, 0xCA, 0xF0, 0x5F, 0xCB, 0x31, 0x20, 0x34, 0x49, + 0x16, 0x76, 0x34, 0xCC, 0x7A, 0xCB, 0x96, 0xFE, 0x78, 0x7A, 0x41, 0xFE, 0x9A, + 0xA2, 0x23, 0xF7, 0x68, 0x80, 0xD6, 0xCE, 0x4A, 0x78, 0xA5, 0xB7, 0x05, 0x77, + 0x81, 0x1F, 0xDE, 0x5E, 0xA8, 0x6E, 0x3E, 0x87, 0xEC, 0x44, 0xD2, 0x69, 0xC6, + 0x54, 0x91, 0x6B, 0x5E, 0x13, 0x8A, 0x03, 0x87, 0x05, 0x31, 0x8D}; + }; +}; + +class PkgDerivedKey3Keyset { +public: + // PkgDerivedKey3Keyset(); + //~PkgDerivedKey3Keyset(); + + // Constructor + // std::uint8_t* PrivateExponent; + const CryptoPP::byte* Exponent1; + // exponent2 = d mod (q - 1) + const CryptoPP::byte* Exponent2; + // e + const CryptoPP::byte* PublicExponent; + // (InverseQ)(q) = 1 mod p + const CryptoPP::byte* Coefficient; + // n = p * q + const CryptoPP::byte* Modulus; + // p + const CryptoPP::byte* Prime1; + // q + const CryptoPP::byte* Prime2; + const CryptoPP::byte* PrivateExponent; + + PkgDerivedKey3Keyset() { + + Prime1 = new CryptoPP::byte[0x80]{ + 0xF9, 0x67, 0xAD, 0x99, 0x12, 0x31, 0x0C, 0x56, 0xA2, 0x2E, 0x16, 0x1C, 0x46, + 0xB3, 0x4D, 0x5B, 0x43, 0xBE, 0x42, 0xA2, 0xF6, 0x86, 0x96, 0x80, 0x42, 0xC3, + 0xC7, 0x3F, 0xC3, 0x42, 0xF5, 0x87, 0x49, 0x33, 0x9F, 0x07, 0x5D, 0x6E, 0x2C, + 0x04, 0xFD, 0xE3, 0xE1, 0xB2, 0xAE, 0x0A, 0x0C, 0xF0, 0xC7, 0xA6, 0x1C, 0xA1, + 0x63, 0x50, 0xC8, 0x09, 0x9C, 0x51, 0x24, 0x52, 0x6C, 0x5E, 0x5E, 0xBD, 0x1E, + 0x27, 0x06, 0xBB, 0xBC, 0x9E, 0x94, 0xE1, 0x35, 0xD4, 0x6D, 0xB3, 0xCB, 0x3C, + 0x68, 0xDD, 0x68, 0xB3, 0xFE, 0x6C, 0xCB, 0x8D, 0x82, 0x20, 0x76, 0x23, 0x63, + 0xB7, 0xE9, 0x68, 0x10, 0x01, 0x4E, 0xDC, 0xBA, 0x27, 0x5D, 0x01, 0xC1, 0x2D, + 0x80, 0x5E, 0x2B, 0xAF, 0x82, 0x6B, 0xD8, 0x84, 0xB6, 0x10, 0x52, 0x86, 0xA7, + 0x89, 0x8E, 0xAE, 0x9A, 0xE2, 0x89, 0xC6, 0xF7, 0xD5, 0x87, 0xFB}; + + Prime2 = new CryptoPP::byte[0x80]{ + 0xD7, 0xA1, 0x0F, 0x9A, 0x8B, 0xF2, 0xC9, 0x11, 0x95, 0x32, 0x9A, 0x8C, 0xF0, + 0xD9, 0x40, 0x47, 0xF5, 0x68, 0xA0, 0x0D, 0xBD, 0xC1, 0xFC, 0x43, 0x2F, 0x65, + 0xF9, 0xC3, 0x61, 0x0F, 0x25, 0x77, 0x54, 0xAD, 0xD7, 0x58, 0xAC, 0x84, 0x40, + 0x60, 0x8D, 0x3F, 0xF3, 0x65, 0x89, 0x75, 0xB5, 0xC6, 0x2C, 0x51, 0x1A, 0x2F, + 0x1F, 0x22, 0xE4, 0x43, 0x11, 0x54, 0xBE, 0xC9, 0xB4, 0xC7, 0xB5, 0x1B, 0x05, + 0x0B, 0xBC, 0x56, 0x9A, 0xCD, 0x4A, 0xD9, 0x73, 0x68, 0x5E, 0x5C, 0xFB, 0x92, + 0xB7, 0x8B, 0x0D, 0xFF, 0xF5, 0x07, 0xCA, 0xB4, 0xC8, 0x9B, 0x96, 0x3C, 0x07, + 0x9E, 0x3E, 0x6B, 0x2A, 0x11, 0xF2, 0x8A, 0xB1, 0x8A, 0xD7, 0x2E, 0x1B, 0xA5, + 0x53, 0x24, 0x06, 0xED, 0x50, 0xB8, 0x90, 0x67, 0xB1, 0xE2, 0x41, 0xC6, 0x92, + 0x01, 0xEE, 0x10, 0xF0, 0x61, 0xBB, 0xFB, 0xB2, 0x7D, 0x4A, 0x73}; + PrivateExponent = new CryptoPP::byte[0x100]{ + 0x32, 0xD9, 0x03, 0x90, 0x8F, 0xBD, 0xB0, 0x8F, 0x57, 0x2B, 0x28, 0x5E, 0x0B, 0x8D, + 0xB3, 0xEA, 0x5C, 0xD1, 0x7E, 0xA8, 0x90, 0x88, 0x8C, 0xDD, 0x6A, 0x80, 0xBB, 0xB1, + 0xDF, 0xC1, 0xF7, 0x0D, 0xAA, 0x32, 0xF0, 0xB7, 0x7C, 0xCB, 0x88, 0x80, 0x0E, 0x8B, + 0x64, 0xB0, 0xBE, 0x4C, 0xD6, 0x0E, 0x9B, 0x8C, 0x1E, 0x2A, 0x64, 0xE1, 0xF3, 0x5C, + 0xD7, 0x76, 0x01, 0x41, 0x5E, 0x93, 0x5C, 0x94, 0xFE, 0xDD, 0x46, 0x62, 0xC3, 0x1B, + 0x5A, 0xE2, 0xA0, 0xBC, 0x2D, 0xEB, 0xC3, 0x98, 0x0A, 0xA7, 0xB7, 0x85, 0x69, 0x70, + 0x68, 0x2B, 0x64, 0x4A, 0xB3, 0x1F, 0xCC, 0x7D, 0xDC, 0x7C, 0x26, 0xF4, 0x77, 0xF6, + 0x5C, 0xF2, 0xAE, 0x5A, 0x44, 0x2D, 0xD3, 0xAB, 0x16, 0x62, 0x04, 0x19, 0xBA, 0xFB, + 0x90, 0xFF, 0xE2, 0x30, 0x50, 0x89, 0x6E, 0xCB, 0x56, 0xB2, 0xEB, 0xC0, 0x91, 0x16, + 0x92, 0x5E, 0x30, 0x8E, 0xAE, 0xC7, 0x94, 0x5D, 0xFD, 0x35, 0xE1, 0x20, 0xF8, 0xAD, + 0x3E, 0xBC, 0x08, 0xBF, 0xC0, 0x36, 0x74, 0x9F, 0xD5, 0xBB, 0x52, 0x08, 0xFD, 0x06, + 0x66, 0xF3, 0x7A, 0xB3, 0x04, 0xF4, 0x75, 0x29, 0x5D, 0xE9, 0x5F, 0xAA, 0x10, 0x30, + 0xB2, 0x0F, 0x5A, 0x1A, 0xC1, 0x2A, 0xB3, 0xFE, 0xCB, 0x21, 0xAD, 0x80, 0xEC, 0x8F, + 0x20, 0x09, 0x1C, 0xDB, 0xC5, 0x58, 0x94, 0xC2, 0x9C, 0xC6, 0xCE, 0x82, 0x65, 0x3E, + 0x57, 0x90, 0xBC, 0xA9, 0x8B, 0x06, 0xB4, 0xF0, 0x72, 0xF6, 0x77, 0xDF, 0x98, 0x64, + 0xF1, 0xEC, 0xFE, 0x37, 0x2D, 0xBC, 0xAE, 0x8C, 0x08, 0x81, 0x1F, 0xC3, 0xC9, 0x89, + 0x1A, 0xC7, 0x42, 0x82, 0x4B, 0x2E, 0xDC, 0x8E, 0x8D, 0x73, 0xCE, 0xB1, 0xCC, 0x01, + 0xD9, 0x08, 0x70, 0x87, 0x3C, 0x44, 0x08, 0xEC, 0x49, 0x8F, 0x81, 0x5A, 0xE2, 0x40, + 0xFF, 0x77, 0xFC, 0x0D}; + Exponent1 = new CryptoPP::byte[0x80]{ + 0x52, 0xCC, 0x2D, 0xA0, 0x9C, 0x9E, 0x75, 0xE7, 0x28, 0xEE, 0x3D, 0xDE, 0xE3, + 0x45, 0xD1, 0x4F, 0x94, 0x1C, 0xCC, 0xC8, 0x87, 0x29, 0x45, 0x3B, 0x8D, 0x6E, + 0xAB, 0x6E, 0x2A, 0xA7, 0xC7, 0x15, 0x43, 0xA3, 0x04, 0x8F, 0x90, 0x5F, 0xEB, + 0xF3, 0x38, 0x4A, 0x77, 0xFA, 0x36, 0xB7, 0x15, 0x76, 0xB6, 0x01, 0x1A, 0x8E, + 0x25, 0x87, 0x82, 0xF1, 0x55, 0xD8, 0xC6, 0x43, 0x2A, 0xC0, 0xE5, 0x98, 0xC9, + 0x32, 0xD1, 0x94, 0x6F, 0xD9, 0x01, 0xBA, 0x06, 0x81, 0xE0, 0x6D, 0x88, 0xF2, + 0x24, 0x2A, 0x25, 0x01, 0x64, 0x5C, 0xBF, 0xF2, 0xD9, 0x99, 0x67, 0x3E, 0xF6, + 0x72, 0xEE, 0xE4, 0xE2, 0x33, 0x5C, 0xF8, 0x00, 0x40, 0xE3, 0x2A, 0x9A, 0xF4, + 0x3D, 0x22, 0x86, 0x44, 0x3C, 0xFB, 0x0A, 0xA5, 0x7C, 0x3F, 0xCC, 0xF5, 0xF1, + 0x16, 0xC4, 0xAC, 0x88, 0xB4, 0xDE, 0x62, 0x94, 0x92, 0x6A, 0x13}; + Exponent2 = new CryptoPP::byte[0x80]{ + 0x7C, 0x9D, 0xAD, 0x39, 0xE0, 0xD5, 0x60, 0x14, 0x94, 0x48, 0x19, 0x7F, 0x88, + 0x95, 0xD5, 0x8B, 0x80, 0xAD, 0x85, 0x8A, 0x4B, 0x77, 0x37, 0x85, 0xD0, 0x77, + 0xBB, 0xBF, 0x89, 0x71, 0x4A, 0x72, 0xCB, 0x72, 0x68, 0x38, 0xEC, 0x02, 0xC6, + 0x7D, 0xC6, 0x44, 0x06, 0x33, 0x51, 0x1C, 0xC0, 0xFF, 0x95, 0x8F, 0x0D, 0x75, + 0xDC, 0x25, 0xBB, 0x0B, 0x73, 0x91, 0xA9, 0x6D, 0x42, 0xD8, 0x03, 0xB7, 0x68, + 0xD4, 0x1E, 0x75, 0x62, 0xA3, 0x70, 0x35, 0x79, 0x78, 0x00, 0xC8, 0xF5, 0xEF, + 0x15, 0xB9, 0xFC, 0x4E, 0x47, 0x5A, 0xC8, 0x70, 0x70, 0x5B, 0x52, 0x98, 0xC0, + 0xC2, 0x58, 0x4A, 0x70, 0x96, 0xCC, 0xB8, 0x10, 0xE1, 0x2F, 0x78, 0x8B, 0x2B, + 0xA1, 0x7F, 0xF9, 0xAC, 0xDE, 0xF0, 0xBB, 0x2B, 0xE2, 0x66, 0xE3, 0x22, 0x92, + 0x31, 0x21, 0x57, 0x92, 0xC4, 0xB8, 0xF2, 0x3E, 0x76, 0x20, 0x37}; + Coefficient = new CryptoPP::byte[0x80]{ + 0x45, 0x97, 0x55, 0xD4, 0x22, 0x08, 0x5E, 0xF3, 0x5C, 0xB4, 0x05, 0x7A, 0xFD, + 0xAA, 0x42, 0x42, 0xAD, 0x9A, 0x8C, 0xA0, 0x6C, 0xBB, 0x1D, 0x68, 0x54, 0x54, + 0x6E, 0x3E, 0x32, 0xE3, 0x53, 0x73, 0x76, 0xF1, 0x3E, 0x01, 0xEA, 0xD3, 0xCF, + 0xEB, 0xEB, 0x23, 0x3E, 0xC0, 0xBE, 0xCE, 0xEC, 0x2C, 0x89, 0x5F, 0xA8, 0x27, + 0x3A, 0x4C, 0xB7, 0xE6, 0x74, 0xBC, 0x45, 0x4C, 0x26, 0xC8, 0x25, 0xFF, 0x34, + 0x63, 0x25, 0x37, 0xE1, 0x48, 0x10, 0xC1, 0x93, 0xA6, 0xAF, 0xEB, 0xBA, 0xE3, + 0xA2, 0xF1, 0x3D, 0xEF, 0x63, 0xD8, 0xF4, 0xFD, 0xD3, 0xEE, 0xE2, 0x5D, 0xE9, + 0x33, 0xCC, 0xAD, 0xBA, 0x75, 0x5C, 0x85, 0xAF, 0xCE, 0xA9, 0x3D, 0xD1, 0xA2, + 0x17, 0xF3, 0xF6, 0x98, 0xB3, 0x50, 0x8E, 0x5E, 0xF6, 0xEB, 0x02, 0x8E, 0xA1, + 0x62, 0xA7, 0xD6, 0x2C, 0xEC, 0x91, 0xFF, 0x15, 0x40, 0xD2, 0xE3}; + Modulus = new CryptoPP::byte[0x100]{ + 0xd2, 0x12, 0xfc, 0x33, 0x5f, 0x6d, 0xdb, 0x83, 0x16, 0x09, 0x62, 0x8b, 0x03, 0x56, + 0x27, 0x37, 0x82, 0xd4, 0x77, 0x85, 0x35, 0x29, 0x39, 0x2d, 0x52, 0x6b, 0x8c, 0x4c, + 0x8c, 0xfb, 0x06, 0xc1, 0x84, 0x5b, 0xe7, 0xd4, 0xf7, 0xbc, 0xd2, 0x4e, 0x62, 0x45, + 0xcd, 0x2a, 0xbb, 0xd7, 0x77, 0x76, 0x45, 0x36, 0x55, 0x27, 0x3f, 0xb3, 0xf5, 0xf9, + 0x8e, 0xda, 0x4b, 0xef, 0xaa, 0x59, 0xae, 0xb3, 0x9b, 0xea, 0x54, 0x98, 0xd2, 0x06, + 0x32, 0x6a, 0x58, 0x31, 0x2a, 0xe0, 0xd4, 0x4f, 0x90, 0xb5, 0x0a, 0x7d, 0xec, 0xf4, + 0x3a, 0x9c, 0x52, 0x67, 0x2d, 0x99, 0x31, 0x8e, 0x0c, 0x43, 0xe6, 0x82, 0xfe, 0x07, + 0x46, 0xe1, 0x2e, 0x50, 0xd4, 0x1f, 0x2d, 0x2f, 0x7e, 0xd9, 0x08, 0xba, 0x06, 0xb3, + 0xbf, 0x2e, 0x20, 0x3f, 0x4e, 0x3f, 0xfe, 0x44, 0xff, 0xaa, 0x50, 0x43, 0x57, 0x91, + 0x69, 0x94, 0x49, 0x15, 0x82, 0x82, 0xe4, 0x0f, 0x4c, 0x8d, 0x9d, 0x2c, 0xc9, 0x5b, + 0x1d, 0x64, 0xbf, 0x88, 0x8b, 0xd4, 0xc5, 0x94, 0xe7, 0x65, 0x47, 0x84, 0x1e, 0xe5, + 0x79, 0x10, 0xfb, 0x98, 0x93, 0x47, 0xb9, 0x7d, 0x85, 0x12, 0xa6, 0x40, 0x98, 0x2c, + 0xf7, 0x92, 0xbc, 0x95, 0x19, 0x32, 0xed, 0xe8, 0x90, 0x56, 0x0d, 0x65, 0xc1, 0xaa, + 0x78, 0xc6, 0x2e, 0x54, 0xfd, 0x5f, 0x54, 0xa1, 0xf6, 0x7e, 0xe5, 0xe0, 0x5f, 0x61, + 0xc1, 0x20, 0xb4, 0xb9, 0xb4, 0x33, 0x08, 0x70, 0xe4, 0xdf, 0x89, 0x56, 0xed, 0x01, + 0x29, 0x46, 0x77, 0x5f, 0x8c, 0xb8, 0xa9, 0xf5, 0x1e, 0x2e, 0xb3, 0xb9, 0xbf, 0xe0, + 0x09, 0xb7, 0x8d, 0x28, 0xd4, 0xa6, 0xc3, 0xb8, 0x1e, 0x1f, 0x07, 0xeb, 0xb4, 0x12, + 0x0b, 0x95, 0xb8, 0x85, 0x30, 0xfd, 0xdc, 0x39, 0x13, 0xd0, 0x7c, 0xdc, 0x8f, 0xed, + 0xf9, 0xc9, 0xa3, 0xc1}; + PublicExponent = new CryptoPP::byte[4]{0, 1, 0, 1}; + }; +}; \ No newline at end of file diff --git a/src/core/file_format/pfs.h b/src/core/file_format/pfs.h new file mode 100644 index 000000000..a79c3674a --- /dev/null +++ b/src/core/file_format/pfs.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "common/types.h" + +#define PFS_FILE 2 +#define PFS_DIR 3 +#define PFS_CURRENT_DIR 4 +#define PFS_PARENT_DIR 5 + +enum PfsMode : unsigned short { + None = 0, + Signed = 0x1, + Is64Bit = 0x2, + Encrypted = 0x4, + UnknownFlagAlwaysSet = 0x8 +}; + +struct PSFHeader_ { + s64 version; + s64 magic; + s64 id; + u8 fmode; + u8 clean; + u8 read_only; + u8 rsv; + PfsMode mode; + s16 unk1; + s32 block_size; + s32 n_backup; + s64 n_block; + s64 dinode_count; + s64 nd_block; + s64 dinode_block_count; + s64 superroot_ino; +}; + +struct PFSCHdr { + s32 magic; + s32 unk4; + s32 unk8; + s32 block_sz; + s64 block_sz2; + s64 block_offsets; + u64 data_start; + s64 data_length; +}; + +enum InodeMode : u16 { + o_read = 1, + o_write = 2, + o_execute = 4, + g_read = 8, + g_write = 16, + g_execute = 32, + u_read = 64, + u_write = 128, + u_execute = 256, + dir = 16384, + file = 32768, +}; + +enum InodeFlags : u32 { + compressed = 0x1, + unk1 = 0x2, + unk2 = 0x4, + unk3 = 0x8, + readonly = 0x10, + unk4 = 0x20, + unk5 = 0x40, + unk6 = 0x80, + unk7 = 0x100, + unk8 = 0x200, + unk9 = 0x400, + unk10 = 0x800, + unk11 = 0x1000, + unk12 = 0x2000, + unk13 = 0x4000, + unk14 = 0x8000, + unk15 = 0x10000, + internal = 0x20000 +}; + +struct Inode { + u16 Mode; + u16 Nlink; + u32 Flags; + s64 Size; + s64 SizeCompressed; + s64 Time1_sec; + s64 Time2_sec; + s64 Time3_sec; + s64 Time4_sec; + u32 Time1_nsec; + u32 Time2_nsec; + u32 Time3_nsec; + u32 Time4_nsec; + u32 Uid; + u32 Gid; + u64 Unk1; + u64 Unk2; + u32 Blocks; + u32 loc; +}; + +struct pfs_fs_table { + std::string name; + u32 inode; + u32 type; +}; + +struct Dirent { + s32 ino; + s32 type; + s32 namelen; + s32 entsize; + char name[512]; +}; diff --git a/src/core/file_format/pkg.cpp b/src/core/file_format/pkg.cpp new file mode 100644 index 000000000..b1aaa7989 --- /dev/null +++ b/src/core/file_format/pkg.cpp @@ -0,0 +1,375 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "common/io_file.h" +#include "pkg.h" +#include "pkg_type.h" + +static void DecompressPFSC(std::span compressed_data, + std::span decompressed_data) { + zng_stream decompressStream; + decompressStream.zalloc = Z_NULL; + decompressStream.zfree = Z_NULL; + decompressStream.opaque = Z_NULL; + + if (zng_inflateInit(&decompressStream) != Z_OK) { + // std::cerr << "Error initializing zlib for deflation." << std::endl; + } + + decompressStream.avail_in = compressed_data.size(); + decompressStream.next_in = reinterpret_cast(compressed_data.data()); + decompressStream.avail_out = decompressed_data.size(); + decompressStream.next_out = reinterpret_cast(decompressed_data.data()); + + if (zng_inflate(&decompressStream, Z_FINISH)) { + } + if (zng_inflateEnd(&decompressStream) != Z_OK) { + // std::cerr << "Error ending zlib inflate" << std::endl; + } +} + +u32 GetPFSCOffset(std::span pfs_image) { + static constexpr u32 PfscMagic = 0x43534650; + u32 value; + for (u32 i = 0x20000; i < pfs_image.size(); i += 0x10000) { + std::memcpy(&value, &pfs_image[i], sizeof(u32)); + if (value == PfscMagic) + return i; + } + return -1; +} + +std::filesystem::path findDirectory(const std::filesystem::path& rootFolder, + const std::filesystem::path& targetDirectory) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(rootFolder)) { + if (std::filesystem::is_directory(entry) && + entry.path().filename() == targetDirectory.filename()) { + return entry.path(); + } + } + return std::filesystem::path(); // Return an empty path if not found +} + +PKG::PKG() = default; + +PKG::~PKG() = default; + +bool PKG::Open(const std::string& filepath) { + Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read); + if (!file.IsOpen()) { + return false; + } + pkgSize = file.GetSize(); + + PKGHeader pkgheader; + file.Read(pkgheader); + + // Find title id it is part of pkg_content_id starting at offset 0x40 + file.Seek(0x47); // skip first 7 characters of content_id + file.Read(pkgTitleID); + + file.Close(); + + return true; +} + +bool PKG::Extract(const std::string& filepath, const std::filesystem::path& extract, + std::string& failreason) { + extract_path = extract; + pkgpath = filepath; + Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read); + if (!file.IsOpen()) { + return false; + } + pkgSize = file.GetSize(); + file.ReadRaw(&pkgheader, sizeof(PKGHeader)); + + if (pkgheader.pkg_size > pkgSize) { + failreason = "PKG file size is different"; + return false; + } + if ((pkgheader.pkg_content_size + pkgheader.pkg_content_offset) > pkgheader.pkg_size) { + failreason = "Content size is bigger than pkg size"; + return false; + } + file.Seek(0); + pkg.resize(pkgheader.pkg_promote_size); + file.Read(pkg); + + u32 offset = pkgheader.pkg_table_entry_offset; + u32 n_files = pkgheader.pkg_table_entry_count; + + std::array seed_digest; + std::array, 7> digest1; + std::array, 7> key1; + std::array imgkeydata; + + for (int i = 0; i < n_files; i++) { + PKGEntry entry; + std::memcpy(&entry, &pkg[offset + i * 0x20], sizeof(entry)); + + // Try to figure out the name + const auto name = GetEntryNameByType(entry.id); + if (name.empty()) { + // Just print with id + Common::FS::IOFile out(extract_path / "sce_sys" / std::to_string(entry.id), + Common::FS::FileAccessMode::Write); + out.WriteRaw(pkg.data() + entry.offset, entry.size); + out.Close(); + continue; + } + + const auto filepath = extract_path / "sce_sys" / name; + std::filesystem::create_directories(filepath.parent_path()); + + if (entry.id == 0x1) { // DIGESTS, seek; + // file.Seek(entry.offset, fsSeekSet); + } else if (entry.id == 0x10) { // ENTRY_KEYS, seek; + file.Seek(entry.offset); + file.Read(seed_digest); + + for (int i = 0; i < 7; i++) { + file.Read(digest1[i]); + } + + for (int i = 0; i < 7; i++) { + file.Read(key1); + } + + PKG::crypto.RSA2048Decrypt(dk3_, key1[3], true); // decrypt DK3 + } else if (entry.id == 0x20) { // IMAGE_KEY, seek; IV_KEY + file.Seek(entry.offset); + file.Read(imgkeydata); + + // The Concatenated iv + dk3 imagekey for HASH256 + std::array concatenated_ivkey_dk3; + std::memcpy(concatenated_ivkey_dk3.data(), &entry, sizeof(entry)); + std::memcpy(concatenated_ivkey_dk3.data() + sizeof(entry), dk3_.data(), sizeof(dk3_)); + + PKG::crypto.ivKeyHASH256(concatenated_ivkey_dk3, ivKey); // ivkey_ + // imgkey_ to use for last step to get ekpfs + PKG::crypto.aesCbcCfb128Decrypt(ivKey, imgkeydata, imgKey); + // ekpfs key to get data and tweak keys. + PKG::crypto.RSA2048Decrypt(ekpfsKey, imgKey, false); + } else if (entry.id == 0x80) { + // GENERAL_DIGESTS, seek; + // file.Seek(entry.offset, fsSeekSet); + } + + Common::FS::IOFile out(extract_path / "sce_sys" / name, Common::FS::FileAccessMode::Write); + out.WriteRaw(pkg.data() + entry.offset, entry.size); + out.Close(); + } + + // Read the seed + std::array seed; + file.Seek(pkgheader.pfs_image_offset + 0x370); + file.Read(seed); + + // Get data and tweak keys. + PKG::crypto.PfsGenCryptoKey(ekpfsKey, seed, dataKey, tweakKey); + const u32 length = pkgheader.pfs_cache_size * 0x2; // Seems to be ok. + + // Read encrypted pfs_image + std::vector pfs_encrypted(length); + file.Seek(pkgheader.pfs_image_offset); + file.Read(pfs_encrypted); + + // Decrypt the pfs_image. + std::vector pfs_decrypted(length); + PKG::crypto.decryptPFS(dataKey, tweakKey, pfs_encrypted, pfs_decrypted, 0); + + // Retrieve PFSC from decrypted pfs_image. + pfsc_offset = GetPFSCOffset(pfs_decrypted); + std::vector pfsc(length); + std::memcpy(pfsc.data(), pfs_decrypted.data() + pfsc_offset, length - pfsc_offset); + + PFSCHdr pfsChdr; + std::memcpy(&pfsChdr, pfsc.data(), sizeof(pfsChdr)); + + const int num_blocks = (int)(pfsChdr.data_length / pfsChdr.block_sz2); + sectorMap.resize(num_blocks + 1); // 8 bytes, need extra 1 to get the last offset. + + for (int i = 0; i < num_blocks + 1; i++) { + std::memcpy(§orMap[i], pfsc.data() + pfsChdr.block_offsets + i * 8, 8); + } + + u32 ent_size = 0; + u32 ndinode = 0; + + std::vector compressedData; + std::vector decompressedData(0x10000); + bool dinode_reached = false; + // Get iNdoes. + for (int i = 0; i < num_blocks; i++) { + const u64 sectorOffset = sectorMap[i]; + const u64 sectorSize = sectorMap[i + 1] - sectorOffset; + + compressedData.resize(sectorSize); + std::memcpy(compressedData.data(), pfsc.data() + sectorOffset, sectorSize); + + if (sectorSize == 0x10000) // Uncompressed data + std::memcpy(decompressedData.data(), compressedData.data(), 0x10000); + else if (sectorSize < 0x10000) // Compressed data + DecompressPFSC(compressedData, decompressedData); + + if (i == 0) { + std::memcpy(&ndinode, decompressedData.data() + 0x30, 4); // number of folders and files + } + + int occupied_blocks = + (ndinode * 0xA8) / 0x10000; // how many blocks(0x10000) are taken by iNodes. + if (((ndinode * 0xA8) % 0x10000) != 0) + occupied_blocks += 1; + + if (i >= 1 && i <= occupied_blocks) { // Get all iNodes, gives type, file size and location. + for (int p = 0; p < 0x10000; p += 0xA8) { + Inode node; + std::memcpy(&node, &decompressedData[p], sizeof(node)); + if (node.Mode == 0) { + break; + } + iNodeBuf.push_back(node); + } + } + + const char dot = decompressedData[0x10]; + const std::string_view dotdot(&decompressedData[0x28], 2); + if (dot == '.' && dotdot == "..") { + dinode_reached = true; + } + + // Get folder and file names. + bool end_reached = false; + if (dinode_reached) { + for (int j = 0; j < 0x10000; j += ent_size) { // Skip the first parent and child. + Dirent dirent; + std::memcpy(&dirent, &decompressedData[j], sizeof(dirent)); + + // Stop here and continue the main loop + if (dirent.ino == 0) { + break; + } + + if (dot != '.' && dotdot != "..") { + end_reached = true; + } + + ent_size = dirent.entsize; + auto& table = fsTable.emplace_back(); + table.name = std::string(dirent.name, dirent.namelen); + table.inode = dirent.ino; + table.type = dirent.type; + + if (table.type == PFS_DIR) { + folderMap[table.inode] = table.name; + } + } + + // Seems to be the last entry, at least for the games I tested. To check as we go. + const std::string_view rightsprx(&decompressedData[0x40], 10); + if (rightsprx == "right.sprx" || end_reached) { + break; + } + } + } + + // Create Folders. + folderMap[2] = GetTitleID(); // Set up game path instead of calling it uroot + game_dir = extract_path.parent_path(); + title_dir = game_dir / GetTitleID(); + + for (int i = 0; i < fsTable.size(); i++) { + const u32 inode_number = fsTable[i].inode; + const u32 inode_type = fsTable[i].type; + const auto inode_name = fsTable[i].name; + + if (inode_type == PFS_CURRENT_DIR) { + current_dir = folderMap[inode_number]; + } else if (inode_type == PFS_PARENT_DIR) { + parent_dir = folderMap[inode_number]; + // Skip uroot folder. we create our own game uid folder + if (i > 1) { + const auto par_dir = inode_number == 2 ? findDirectory(game_dir, parent_dir) + : findDirectory(title_dir, parent_dir); + const auto cur_dir = findDirectory(par_dir, current_dir); + + if (cur_dir == "") { + extract_path = par_dir / current_dir; + std::filesystem::create_directories(extract_path); + } else { + extract_path = cur_dir; + } + } + } + extractMap[inode_number] = extract_path.string(); + } + return true; +} + +void PKG::ExtractFiles(const int& index) { + int inode_number = fsTable[index].inode; + int inode_type = fsTable[index].type; + std::string inode_name = fsTable[index].name; + + if (inode_type == PFS_FILE) { + int sector_loc = iNodeBuf[inode_number].loc; + int nblocks = iNodeBuf[inode_number].Blocks; + int bsize = iNodeBuf[inode_number].Size; + std::string file_extracted = extractMap[inode_number] + "/" + inode_name; + + Common::FS::IOFile inflated; + inflated.Open(file_extracted, Common::FS::FileAccessMode::Write); + + Common::FS::IOFile pkgFile; // Open the file for each iteration to avoid conflict. + pkgFile.Open(pkgpath, Common::FS::FileAccessMode::Read); + + int size_decompressed = 0; + std::vector compressedData; + std::vector decompressedData(0x10000); + + u64 pfsc_buf_size = 0x11000; // extra 0x1000 + std::vector pfsc(pfsc_buf_size); + std::vector pfs_decrypted(pfsc_buf_size); + + for (int j = 0; j < nblocks; j++) { + u64 sectorOffset = + sectorMap[sector_loc + j]; // offset into PFSC_image and not pfs_image. + u64 sectorSize = sectorMap[sector_loc + j + 1] - + sectorOffset; // indicates if data is compressed or not. + u64 fileOffset = (pkgheader.pfs_image_offset + pfsc_offset + sectorOffset); + u64 currentSector1 = + (pfsc_offset + sectorOffset) / 0x1000; // block size is 0x1000 for xts decryption. + + int sectorOffsetMask = (sectorOffset + pfsc_offset) & 0xFFFFF000; + int previousData = (sectorOffset + pfsc_offset) - sectorOffsetMask; + + pkgFile.Seek(fileOffset - previousData); + pkgFile.Read(pfsc); + + PKG::crypto.decryptPFS(dataKey, tweakKey, pfsc, pfs_decrypted, currentSector1); + + compressedData.resize(sectorSize); + std::memcpy(compressedData.data(), pfs_decrypted.data() + previousData, sectorSize); + + if (sectorSize == 0x10000) // Uncompressed data + std::memcpy(decompressedData.data(), compressedData.data(), 0x10000); + else if (sectorSize < 0x10000) // Compressed data + DecompressPFSC(compressedData, decompressedData); + + size_decompressed += 0x10000; + + if (j < nblocks - 1) { + inflated.WriteRaw(decompressedData.data(), decompressedData.size()); + } else { + // This is to remove the zeros at the end of the file. + const u32 write_size = decompressedData.size() - (size_decompressed - bsize); + inflated.WriteRaw(decompressedData.data(), write_size); + } + } + pkgFile.Close(); + inflated.Close(); + } +} diff --git a/src/core/file_format/pkg.h b/src/core/file_format/pkg.h new file mode 100644 index 000000000..4d8aca58d --- /dev/null +++ b/src/core/file_format/pkg.h @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "common/endian.h" +#include "core/crypto/crypto.h" +#include "pfs.h" + +struct PKGHeader { + u32_be magic; // Magic + u32_be pkg_type; + u32_be pkg_0x8; // unknown field + u32_be pkg_file_count; + u32_be pkg_table_entry_count; + u16_be pkg_sc_entry_count; + u16_be pkg_table_entry_count_2; // same as pkg_entry_count + u32_be pkg_table_entry_offset; // file table offset + u32_be pkg_sc_entry_data_size; + u64_be pkg_body_offset; // offset of PKG entries + u64_be pkg_body_size; // length of all PKG entries + u64_be pkg_content_offset; + u64_be pkg_content_size; + u8 pkg_content_id[0x24]; // packages' content ID as a 36-byte string + u8 pkg_padding[0xC]; // padding + u32_be pkg_drm_type; // DRM type + u32_be pkg_content_type; // Content type + u32_be pkg_content_flags; // Content flags + u32_be pkg_promote_size; + u32_be pkg_version_date; + u32_be pkg_version_hash; + u32_be pkg_0x088; + u32_be pkg_0x08C; + u32_be pkg_0x090; + u32_be pkg_0x094; + u32_be pkg_iro_tag; + u32_be pkg_drm_type_version; + + u8 pkg_zeroes_1[0x60]; + + /* Digest table */ + u8 digest_entries1[0x20]; // sha256 digest for main entry 1 + u8 digest_entries2[0x20]; // sha256 digest for main entry 2 + u8 digest_table_digest[0x20]; // sha256 digest for digest table + u8 digest_body_digest[0x20]; // sha256 digest for main table + + u8 pkg_zeroes_2[0x280]; + + u32_be pkg_0x400; + + u32_be pfs_image_count; // count of PFS images + u64_be pfs_image_flags; // PFS flags + u64_be pfs_image_offset; // offset to start of external PFS image + u64_be pfs_image_size; // size of external PFS image + u64_be mount_image_offset; + u64_be mount_image_size; + u64_be pkg_size; + u32_be pfs_signed_size; + u32_be pfs_cache_size; + u8 pfs_image_digest[0x20]; + u8 pfs_signed_digest[0x20]; + u64_be pfs_split_size_nth_0; + u64_be pfs_split_size_nth_1; + + u8 pkg_zeroes_3[0xB50]; + + u8 pkg_digest[0x20]; +}; + +struct PKGEntry { + u32_be id; // File ID, useful for files without a filename entry + u32_be filename_offset; // Offset into the filenames table (ID 0x200) where this file's name is + // located + u32_be flags1; // Flags including encrypted flag, etc + u32_be flags2; // Flags including encryption key index, etc + u32_be offset; // Offset into PKG to find the file + u32_be size; // Size of the file + u64_be padding; // blank padding +}; +static_assert(sizeof(PKGEntry) == 32); + +class PKG { +public: + PKG(); + ~PKG(); + + bool Open(const std::string& filepath); + void ExtractFiles(const int& index); + bool Extract(const std::string& filepath, const std::filesystem::path& extract, + std::string& failreason); + + u32 GetNumberOfFiles() { + return fsTable.size(); + } + + u64 GetPkgSize() { + return pkgSize; + } + + std::string_view GetTitleID() { + return std::string_view(pkgTitleID, 9); + } + +private: + Crypto crypto; + std::vector pkg; + u64 pkgSize = 0; + char pkgTitleID[9]; + PKGHeader pkgheader; + + std::unordered_map folderMap; + std::unordered_map extractMap; + std::vector fsTable; + std::vector iNodeBuf; + std::vector sectorMap; + u64 pfsc_offset; + + std::array dk3_; + std::array ivKey; + std::array imgKey; + std::array ekpfsKey; + std::array dataKey; + std::array tweakKey; + + std::filesystem::path pkgpath; + std::filesystem::path current_dir; + std::filesystem::path parent_dir; + std::filesystem::path extract_path; + std::filesystem::path game_dir; + std::filesystem::path title_dir; +}; diff --git a/src/core/file_format/pkg_type.cpp b/src/core/file_format/pkg_type.cpp new file mode 100644 index 000000000..464f0b993 --- /dev/null +++ b/src/core/file_format/pkg_type.cpp @@ -0,0 +1,638 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "pkg_type.h" + +struct PkgEntryValue { + u32 type; + std::string_view name; + + operator u32() const noexcept { + return type; + } +}; + +constexpr static std::array PkgEntries = {{ + {0x0001, "digests"}, + {0x0010, "entry_keys"}, + {0x0020, "image_key"}, + {0x0080, "general_digests"}, + {0x0100, "metas"}, + {0x0200, "entry_names"}, + {0x0400, "license.dat"}, + {0x0401, "license.info"}, + {0x0402, "nptitle.dat"}, + {0x0403, "npbind.dat"}, + {0x0404, "selfinfo.dat"}, + {0x0406, "imageinfo.dat"}, + {0x0407, "target-deltainfo.dat"}, + {0x0408, "origin-deltainfo.dat"}, + {0x0409, "psreserved.dat"}, + {0x1000, "param.sfo"}, + {0x1001, "playgo-chunk.dat"}, + {0x1002, "playgo-chunk.sha"}, + {0x1003, "playgo-manifest.xml"}, + {0x1004, "pronunciation.xml"}, + {0x1005, "pronunciation.sig"}, + {0x1006, "pic1.png"}, + {0x1007, "pubtoolinfo.dat"}, + {0x1008, "app/playgo-chunk.dat"}, + {0x1009, "app/playgo-chunk.sha"}, + {0x100A, "app/playgo-manifest.xml"}, + {0x100B, "shareparam.json"}, + {0x100C, "shareoverlayimage.png"}, + {0x100D, "save_data.png"}, + {0x100E, "shareprivacyguardimage.png"}, + {0x1200, "icon0.png"}, + {0x1201, "icon0_00.png"}, + {0x1202, "icon0_01.png"}, + {0x1203, "icon0_02.png"}, + {0x1204, "icon0_03.png"}, + {0x1205, "icon0_04.png"}, + {0x1206, "icon0_05.png"}, + {0x1207, "icon0_06.png"}, + {0x1208, "icon0_07.png"}, + {0x1209, "icon0_08.png"}, + {0x120A, "icon0_09.png"}, + {0x120B, "icon0_10.png"}, + {0x120C, "icon0_11.png"}, + {0x120D, "icon0_12.png"}, + {0x120E, "icon0_13.png"}, + {0x120F, "icon0_14.png"}, + {0x1210, "icon0_15.png"}, + {0x1211, "icon0_16.png"}, + {0x1212, "icon0_17.png"}, + {0x1213, "icon0_18.png"}, + {0x1214, "icon0_19.png"}, + {0x1215, "icon0_20.png"}, + {0x1216, "icon0_21.png"}, + {0x1217, "icon0_22.png"}, + {0x1218, "icon0_23.png"}, + {0x1219, "icon0_24.png"}, + {0x121A, "icon0_25.png"}, + {0x121B, "icon0_26.png"}, + {0x121C, "icon0_27.png"}, + {0x121D, "icon0_28.png"}, + {0x121E, "icon0_29.png"}, + {0x121F, "icon0_30.png"}, + {0x1220, "pic0.png"}, + {0x1240, "snd0.at9"}, + {0x1241, "pic1_00.png"}, + {0x1242, "pic1_01.png"}, + {0x1243, "pic1_02.png"}, + {0x1244, "pic1_03.png"}, + {0x1245, "pic1_04.png"}, + {0x1246, "pic1_05.png"}, + {0x1247, "pic1_06.png"}, + {0x1248, "pic1_07.png"}, + {0x1249, "pic1_08.png"}, + {0x124A, "pic1_09.png"}, + {0x124B, "pic1_10.png"}, + {0x124C, "pic1_11.png"}, + {0x124D, "pic1_12.png"}, + {0x124E, "pic1_13.png"}, + {0x124F, "pic1_14.png"}, + {0x1250, "pic1_15.png"}, + {0x1251, "pic1_16.png"}, + {0x1252, "pic1_17.png"}, + {0x1253, "pic1_18.png"}, + {0x1254, "pic1_19.png"}, + {0x1255, "pic1_20.png"}, + {0x1256, "pic1_21.png"}, + {0x1257, "pic1_22.png"}, + {0x1258, "pic1_23.png"}, + {0x1259, "pic1_24.png"}, + {0x125A, "pic1_25.png"}, + {0x125B, "pic1_26.png"}, + {0x125C, "pic1_27.png"}, + {0x125D, "pic1_28.png"}, + {0x125E, "pic1_29.png"}, + {0x125F, "pic1_30.png"}, + {0x1260, "changeinfo/changeinfo.xml"}, + {0x1261, "changeinfo/changeinfo_00.xml"}, + {0x1262, "changeinfo/changeinfo_01.xml"}, + {0x1263, "changeinfo/changeinfo_02.xml"}, + {0x1264, "changeinfo/changeinfo_03.xml"}, + {0x1265, "changeinfo/changeinfo_04.xml"}, + {0x1266, "changeinfo/changeinfo_05.xml"}, + {0x1267, "changeinfo/changeinfo_06.xml"}, + {0x1268, "changeinfo/changeinfo_07.xml"}, + {0x1269, "changeinfo/changeinfo_08.xml"}, + {0x126A, "changeinfo/changeinfo_09.xml"}, + {0x126B, "changeinfo/changeinfo_10.xml"}, + {0x126C, "changeinfo/changeinfo_11.xml"}, + {0x126D, "changeinfo/changeinfo_12.xml"}, + {0x126E, "changeinfo/changeinfo_13.xml"}, + {0x126F, "changeinfo/changeinfo_14.xml"}, + {0x1270, "changeinfo/changeinfo_15.xml"}, + {0x1271, "changeinfo/changeinfo_16.xml"}, + {0x1272, "changeinfo/changeinfo_17.xml"}, + {0x1273, "changeinfo/changeinfo_18.xml"}, + {0x1274, "changeinfo/changeinfo_19.xml"}, + {0x1275, "changeinfo/changeinfo_20.xml"}, + {0x1276, "changeinfo/changeinfo_21.xml"}, + {0x1277, "changeinfo/changeinfo_22.xml"}, + {0x1278, "changeinfo/changeinfo_23.xml"}, + {0x1279, "changeinfo/changeinfo_24.xml"}, + {0x127A, "changeinfo/changeinfo_25.xml"}, + {0x127B, "changeinfo/changeinfo_26.xml"}, + {0x127C, "changeinfo/changeinfo_27.xml"}, + {0x127D, "changeinfo/changeinfo_28.xml"}, + {0x127E, "changeinfo/changeinfo_29.xml"}, + {0x127F, "changeinfo/changeinfo_30.xml"}, + {0x1280, "icon0.dds"}, + {0x1281, "icon0_00.dds"}, + {0x1282, "icon0_01.dds"}, + {0x1283, "icon0_02.dds"}, + {0x1284, "icon0_03.dds"}, + {0x1285, "icon0_04.dds"}, + {0x1286, "icon0_05.dds"}, + {0x1287, "icon0_06.dds"}, + {0x1288, "icon0_07.dds"}, + {0x1289, "icon0_08.dds"}, + {0x128A, "icon0_09.dds"}, + {0x128B, "icon0_10.dds"}, + {0x128C, "icon0_11.dds"}, + {0x128D, "icon0_12.dds"}, + {0x128E, "icon0_13.dds"}, + {0x128F, "icon0_14.dds"}, + {0x1290, "icon0_15.dds"}, + {0x1291, "icon0_16.dds"}, + {0x1292, "icon0_17.dds"}, + {0x1293, "icon0_18.dds"}, + {0x1294, "icon0_19.dds"}, + {0x1295, "icon0_20.dds"}, + {0x1296, "icon0_21.dds"}, + {0x1297, "icon0_22.dds"}, + {0x1298, "icon0_23.dds"}, + {0x1299, "icon0_24.dds"}, + {0x129A, "icon0_25.dds"}, + {0x129B, "icon0_26.dds"}, + {0x129C, "icon0_27.dds"}, + {0x129D, "icon0_28.dds"}, + {0x129E, "icon0_29.dds"}, + {0x129F, "icon0_30.dds"}, + {0x12A0, "pic0.dds"}, + {0x12C0, "pic1.dds"}, + {0x12C1, "pic1_00.dds"}, + {0x12C2, "pic1_01.dds"}, + {0x12C3, "pic1_02.dds"}, + {0x12C4, "pic1_03.dds"}, + {0x12C5, "pic1_04.dds"}, + {0x12C6, "pic1_05.dds"}, + {0x12C7, "pic1_06.dds"}, + {0x12C8, "pic1_07.dds"}, + {0x12C9, "pic1_08.dds"}, + {0x12CA, "pic1_09.dds"}, + {0x12CB, "pic1_10.dds"}, + {0x12CC, "pic1_11.dds"}, + {0x12CD, "pic1_12.dds"}, + {0x12CE, "pic1_13.dds"}, + {0x12CF, "pic1_14.dds"}, + {0x12D0, "pic1_15.dds"}, + {0x12D1, "pic1_16.dds"}, + {0x12D2, "pic1_17.dds"}, + {0x12D3, "pic1_18.dds"}, + {0x12D4, "pic1_19.dds"}, + {0x12D5, "pic1_20.dds"}, + {0x12D6, "pic1_21.dds"}, + {0x12D7, "pic1_22.dds"}, + {0x12D8, "pic1_23.dds"}, + {0x12D9, "pic1_24.dds"}, + {0x12DA, "pic1_25.dds"}, + {0x12DB, "pic1_26.dds"}, + {0x12DC, "pic1_27.dds"}, + {0x12DD, "pic1_28.dds"}, + {0x12DE, "pic1_29.dds"}, + {0x12DF, "pic1_30.dds"}, + {0x1400, "trophy/trophy00.trp"}, + {0x1401, "trophy/trophy01.trp"}, + {0x1402, "trophy/trophy02.trp"}, + {0x1403, "trophy/trophy03.trp"}, + {0x1404, "trophy/trophy04.trp"}, + {0x1405, "trophy/trophy05.trp"}, + {0x1406, "trophy/trophy06.trp"}, + {0x1407, "trophy/trophy07.trp"}, + {0x1408, "trophy/trophy08.trp"}, + {0x1409, "trophy/trophy09.trp"}, + {0x140A, "trophy/trophy10.trp"}, + {0x140B, "trophy/trophy11.trp"}, + {0x140C, "trophy/trophy12.trp"}, + {0x140D, "trophy/trophy13.trp"}, + {0x140E, "trophy/trophy14.trp"}, + {0x140F, "trophy/trophy15.trp"}, + {0x1410, "trophy/trophy16.trp"}, + {0x1411, "trophy/trophy17.trp"}, + {0x1412, "trophy/trophy18.trp"}, + {0x1413, "trophy/trophy19.trp"}, + {0x1414, "trophy/trophy20.trp"}, + {0x1415, "trophy/trophy21.trp"}, + {0x1416, "trophy/trophy22.trp"}, + {0x1417, "trophy/trophy23.trp"}, + {0x1418, "trophy/trophy24.trp"}, + {0x1419, "trophy/trophy25.trp"}, + {0x141A, "trophy/trophy26.trp"}, + {0x141B, "trophy/trophy27.trp"}, + {0x141C, "trophy/trophy28.trp"}, + {0x141D, "trophy/trophy29.trp"}, + {0x141E, "trophy/trophy30.trp"}, + {0x141F, "trophy/trophy31.trp"}, + {0x1420, "trophy/trophy32.trp"}, + {0x1421, "trophy/trophy33.trp"}, + {0x1422, "trophy/trophy34.trp"}, + {0x1423, "trophy/trophy35.trp"}, + {0x1424, "trophy/trophy36.trp"}, + {0x1425, "trophy/trophy37.trp"}, + {0x1426, "trophy/trophy38.trp"}, + {0x1427, "trophy/trophy39.trp"}, + {0x1428, "trophy/trophy40.trp"}, + {0x1429, "trophy/trophy41.trp"}, + {0x142A, "trophy/trophy42.trp"}, + {0x142B, "trophy/trophy43.trp"}, + {0x142C, "trophy/trophy44.trp"}, + {0x142D, "trophy/trophy45.trp"}, + {0x142E, "trophy/trophy46.trp"}, + {0x142F, "trophy/trophy47.trp"}, + {0x1430, "trophy/trophy48.trp"}, + {0x1431, "trophy/trophy49.trp"}, + {0x1432, "trophy/trophy50.trp"}, + {0x1433, "trophy/trophy51.trp"}, + {0x1434, "trophy/trophy52.trp"}, + {0x1435, "trophy/trophy53.trp"}, + {0x1436, "trophy/trophy54.trp"}, + {0x1437, "trophy/trophy55.trp"}, + {0x1438, "trophy/trophy56.trp"}, + {0x1439, "trophy/trophy57.trp"}, + {0x143A, "trophy/trophy58.trp"}, + {0x143B, "trophy/trophy59.trp"}, + {0x143C, "trophy/trophy60.trp"}, + {0x143D, "trophy/trophy61.trp"}, + {0x143E, "trophy/trophy62.trp"}, + {0x143F, "trophy/trophy63.trp"}, + {0x1440, "trophy/trophy64.trp"}, + {0x1441, "trophy/trophy65.trp"}, + {0x1442, "trophy/trophy66.trp"}, + {0x1443, "trophy/trophy67.trp"}, + {0x1444, "trophy/trophy68.trp"}, + {0x1445, "trophy/trophy69.trp"}, + {0x1446, "trophy/trophy70.trp"}, + {0x1447, "trophy/trophy71.trp"}, + {0x1448, "trophy/trophy72.trp"}, + {0x1449, "trophy/trophy73.trp"}, + {0x144A, "trophy/trophy74.trp"}, + {0x144B, "trophy/trophy75.trp"}, + {0x144C, "trophy/trophy76.trp"}, + {0x144D, "trophy/trophy77.trp"}, + {0x144E, "trophy/trophy78.trp"}, + {0x144F, "trophy/trophy79.trp"}, + {0x1450, "trophy/trophy80.trp"}, + {0x1451, "trophy/trophy81.trp"}, + {0x1452, "trophy/trophy82.trp"}, + {0x1453, "trophy/trophy83.trp"}, + {0x1454, "trophy/trophy84.trp"}, + {0x1455, "trophy/trophy85.trp"}, + {0x1456, "trophy/trophy86.trp"}, + {0x1457, "trophy/trophy87.trp"}, + {0x1458, "trophy/trophy88.trp"}, + {0x1459, "trophy/trophy89.trp"}, + {0x145A, "trophy/trophy90.trp"}, + {0x145B, "trophy/trophy91.trp"}, + {0x145C, "trophy/trophy92.trp"}, + {0x145D, "trophy/trophy93.trp"}, + {0x145E, "trophy/trophy94.trp"}, + {0x145F, "trophy/trophy95.trp"}, + {0x1460, "trophy/trophy96.trp"}, + {0x1461, "trophy/trophy97.trp"}, + {0x1462, "trophy/trophy98.trp"}, + {0x1463, "trophy/trophy99.trp"}, + {0x1600, "keymap_rp/001.png"}, + {0x1601, "keymap_rp/002.png"}, + {0x1602, "keymap_rp/003.png"}, + {0x1603, "keymap_rp/004.png"}, + {0x1604, "keymap_rp/005.png"}, + {0x1605, "keymap_rp/006.png"}, + {0x1606, "keymap_rp/007.png"}, + {0x1607, "keymap_rp/008.png"}, + {0x1608, "keymap_rp/009.png"}, + {0x1609, "keymap_rp/010.png"}, + {0x1610, "keymap_rp/00/001.png"}, + {0x1611, "keymap_rp/00/002.png"}, + {0x1612, "keymap_rp/00/003.png"}, + {0x1613, "keymap_rp/00/004.png"}, + {0x1614, "keymap_rp/00/005.png"}, + {0x1615, "keymap_rp/00/006.png"}, + {0x1616, "keymap_rp/00/007.png"}, + {0x1617, "keymap_rp/00/008.png"}, + {0x1618, "keymap_rp/00/009.png"}, + {0x1619, "keymap_rp/00/010.png"}, + {0x1620, "keymap_rp/01/001.png"}, + {0x1621, "keymap_rp/01/002.png"}, + {0x1622, "keymap_rp/01/003.png"}, + {0x1623, "keymap_rp/01/004.png"}, + {0x1624, "keymap_rp/01/005.png"}, + {0x1625, "keymap_rp/01/006.png"}, + {0x1626, "keymap_rp/01/007.png"}, + {0x1627, "keymap_rp/01/008.png"}, + {0x1628, "keymap_rp/01/009.png"}, + {0x1629, "keymap_rp/01/010.png"}, + {0x1630, "keymap_rp/02/001.png"}, + {0x1631, "keymap_rp/02/002.png"}, + {0x1632, "keymap_rp/02/003.png"}, + {0x1633, "keymap_rp/02/004.png"}, + {0x1634, "keymap_rp/02/005.png"}, + {0x1635, "keymap_rp/02/006.png"}, + {0x1636, "keymap_rp/02/007.png"}, + {0x1637, "keymap_rp/02/008.png"}, + {0x1638, "keymap_rp/02/009.png"}, + {0x1639, "keymap_rp/02/010.png"}, + {0x1640, "keymap_rp/03/001.png"}, + {0x1641, "keymap_rp/03/002.png"}, + {0x1642, "keymap_rp/03/003.png"}, + {0x1643, "keymap_rp/03/004.png"}, + {0x1644, "keymap_rp/03/005.png"}, + {0x1645, "keymap_rp/03/006.png"}, + {0x1646, "keymap_rp/03/007.png"}, + {0x1647, "keymap_rp/03/008.png"}, + {0x1648, "keymap_rp/03/0010.png"}, + {0x1650, "keymap_rp/04/001.png"}, + {0x1651, "keymap_rp/04/002.png"}, + {0x1652, "keymap_rp/04/003.png"}, + {0x1653, "keymap_rp/04/004.png"}, + {0x1654, "keymap_rp/04/005.png"}, + {0x1655, "keymap_rp/04/006.png"}, + {0x1656, "keymap_rp/04/007.png"}, + {0x1657, "keymap_rp/04/008.png"}, + {0x1658, "keymap_rp/04/009.png"}, + {0x1659, "keymap_rp/04/010.png"}, + {0x1660, "keymap_rp/05/001.png"}, + {0x1661, "keymap_rp/05/002.png"}, + {0x1662, "keymap_rp/05/003.png"}, + {0x1663, "keymap_rp/05/004.png"}, + {0x1664, "keymap_rp/05/005.png"}, + {0x1665, "keymap_rp/05/006.png"}, + {0x1666, "keymap_rp/05/007.png"}, + {0x1667, "keymap_rp/05/008.png"}, + {0x1668, "keymap_rp/05/009.png"}, + {0x1669, "keymap_rp/05/010.png"}, + {0x1670, "keymap_rp/06/001.png"}, + {0x1671, "keymap_rp/06/002.png"}, + {0x1672, "keymap_rp/06/003.png"}, + {0x1673, "keymap_rp/06/004.png"}, + {0x1674, "keymap_rp/06/005.png"}, + {0x1675, "keymap_rp/06/006.png"}, + {0x1676, "keymap_rp/06/007.png"}, + {0x1677, "keymap_rp/06/008.png"}, + {0x1678, "keymap_rp/06/009.png"}, + {0x1679, "keymap_rp/06/010.png"}, + {0x1680, "keymap_rp/07/001.png"}, + {0x1681, "keymap_rp/07/002.png"}, + {0x1682, "keymap_rp/07/003.png"}, + {0x1683, "keymap_rp/07/004.png"}, + {0x1684, "keymap_rp/07/005.png"}, + {0x1685, "keymap_rp/07/006.png"}, + {0x1686, "keymap_rp/07/007.png"}, + {0x1687, "keymap_rp/07/008.png"}, + {0x1688, "keymap_rp/07/009.png"}, + {0x1689, "keymap_rp/07/010.png"}, + {0x1690, "keymap_rp/08/001.png"}, + {0x1691, "keymap_rp/08/002.png"}, + {0x1692, "keymap_rp/08/003.png"}, + {0x1693, "keymap_rp/08/004.png"}, + {0x1694, "keymap_rp/08/005.png"}, + {0x1695, "keymap_rp/08/006.png"}, + {0x1696, "keymap_rp/08/007.png"}, + {0x1697, "keymap_rp/08/008.png"}, + {0x1698, "keymap_rp/08/009.png"}, + {0x1699, "keymap_rp/08/010.png"}, + {0x16A0, "keymap_rp/09/001.png"}, + {0x16A1, "keymap_rp/09/002.png"}, + {0x16A2, "keymap_rp/09/003.png"}, + {0x16A3, "keymap_rp/09/004.png"}, + {0x16A4, "keymap_rp/09/005.png"}, + {0x16A5, "keymap_rp/09/006.png"}, + {0x16A6, "keymap_rp/09/007.png"}, + {0x16A7, "keymap_rp/09/008.png"}, + {0x16A8, "keymap_rp/09/009.png"}, + {0x16A9, "keymap_rp/09/010.png"}, + {0x16B0, "keymap_rp/10/001.png"}, + {0x16B1, "keymap_rp/10/002.png"}, + {0x16B2, "keymap_rp/10/003.png"}, + {0x16B3, "keymap_rp/10/004.png"}, + {0x16B4, "keymap_rp/10/005.png"}, + {0x16B5, "keymap_rp/10/006.png"}, + {0x16B6, "keymap_rp/10/007.png"}, + {0x16B7, "keymap_rp/10/008.png"}, + {0x16B8, "keymap_rp/10/009.png"}, + {0x16B9, "keymap_rp/10/010.png"}, + {0x16C0, "keymap_rp/11/001.png"}, + {0x16C1, "keymap_rp/11/002.png"}, + {0x16C2, "keymap_rp/11/003.png"}, + {0x16C3, "keymap_rp/11/004.png"}, + {0x16C4, "keymap_rp/11/005.png"}, + {0x16C5, "keymap_rp/11/006.png"}, + {0x16C6, "keymap_rp/11/007.png"}, + {0x16C7, "keymap_rp/11/008.png"}, + {0x16C8, "keymap_rp/11/009.png"}, + {0x16C9, "keymap_rp/11/010.png"}, + {0x16D0, "keymap_rp/12/001.png"}, + {0x16D1, "keymap_rp/12/002.png"}, + {0x16D2, "keymap_rp/12/003.png"}, + {0x16D3, "keymap_rp/12/004.png"}, + {0x16D4, "keymap_rp/12/005.png"}, + {0x16D5, "keymap_rp/12/006.png"}, + {0x16D6, "keymap_rp/12/007.png"}, + {0x16D7, "keymap_rp/12/008.png"}, + {0x16D8, "keymap_rp/12/009.png"}, + {0x16D9, "keymap_rp/12/010.png"}, + {0x16E0, "keymap_rp/13/001.png"}, + {0x16E1, "keymap_rp/13/002.png"}, + {0x16E2, "keymap_rp/13/003.png"}, + {0x16E3, "keymap_rp/13/004.png"}, + {0x16E4, "keymap_rp/13/005.png"}, + {0x16E5, "keymap_rp/13/006.png"}, + {0x16E6, "keymap_rp/13/007.png"}, + {0x16E7, "keymap_rp/13/008.png"}, + {0x16E8, "keymap_rp/13/009.png"}, + {0x16E9, "keymap_rp/13/010.png"}, + {0x16F0, "keymap_rp/14/001.png"}, + {0x16F1, "keymap_rp/14/002.png"}, + {0x16F2, "keymap_rp/14/003.png"}, + {0x16F3, "keymap_rp/14/004.png"}, + {0x16F4, "keymap_rp/14/005.png"}, + {0x16F5, "keymap_rp/14/006.png"}, + {0x16F6, "keymap_rp/14/007.png"}, + {0x16F7, "keymap_rp/14/008.png"}, + {0x16F8, "keymap_rp/14/009.png"}, + {0x16F9, "keymap_rp/14/010.png"}, + {0x1700, "keymap_rp/15/001.png"}, + {0x1701, "keymap_rp/15/002.png"}, + {0x1702, "keymap_rp/15/003.png"}, + {0x1703, "keymap_rp/15/004.png"}, + {0x1704, "keymap_rp/15/005.png"}, + {0x1705, "keymap_rp/15/006.png"}, + {0x1706, "keymap_rp/15/007.png"}, + {0x1707, "keymap_rp/15/008.png"}, + {0x1708, "keymap_rp/15/009.png"}, + {0x1709, "keymap_rp/15/010.png"}, + {0x1710, "keymap_rp/16/001.png"}, + {0x1711, "keymap_rp/16/002.png"}, + {0x1712, "keymap_rp/16/003.png"}, + {0x1713, "keymap_rp/16/004.png"}, + {0x1714, "keymap_rp/16/005.png"}, + {0x1715, "keymap_rp/16/006.png"}, + {0x1716, "keymap_rp/16/007.png"}, + {0x1717, "keymap_rp/16/008.png"}, + {0x1718, "keymap_rp/16/009.png"}, + {0x1719, "keymap_rp/16/010.png"}, + {0x1720, "keymap_rp/17/001.png"}, + {0x1721, "keymap_rp/17/002.png"}, + {0x1722, "keymap_rp/17/003.png"}, + {0x1723, "keymap_rp/17/004.png"}, + {0x1724, "keymap_rp/17/005.png"}, + {0x1725, "keymap_rp/17/006.png"}, + {0x1726, "keymap_rp/17/007.png"}, + {0x1727, "keymap_rp/17/008.png"}, + {0x1728, "keymap_rp/17/009.png"}, + {0x1729, "keymap_rp/17/010.png"}, + {0x1730, "keymap_rp/18/001.png"}, + {0x1731, "keymap_rp/18/002.png"}, + {0x1732, "keymap_rp/18/003.png"}, + {0x1733, "keymap_rp/18/004.png"}, + {0x1734, "keymap_rp/18/005.png"}, + {0x1735, "keymap_rp/18/006.png"}, + {0x1736, "keymap_rp/18/007.png"}, + {0x1737, "keymap_rp/18/008.png"}, + {0x1738, "keymap_rp/18/009.png"}, + {0x1739, "keymap_rp/18/010.png"}, + {0x1740, "keymap_rp/19/001.png"}, + {0x1741, "keymap_rp/19/002.png"}, + {0x1742, "keymap_rp/19/003.png"}, + {0x1743, "keymap_rp/19/004.png"}, + {0x1744, "keymap_rp/19/005.png"}, + {0x1745, "keymap_rp/19/006.png"}, + {0x1746, "keymap_rp/19/007.png"}, + {0x1747, "keymap_rp/19/008.png"}, + {0x1748, "keymap_rp/19/009.png"}, + {0x1749, "keymap_rp/19/010.png"}, + {0x1750, "keymap_rp/20/001.png"}, + {0x1751, "keymap_rp/20/002.png"}, + {0x1752, "keymap_rp/20/003.png"}, + {0x1753, "keymap_rp/20/004.png"}, + {0x1754, "keymap_rp/20/005.png"}, + {0x1755, "keymap_rp/20/006.png"}, + {0x1756, "keymap_rp/20/007.png"}, + {0x1757, "keymap_rp/20/008.png"}, + {0x1758, "keymap_rp/20/009.png"}, + {0x1759, "keymap_rp/20/010.png"}, + {0x1760, "keymap_rp/21/001.png"}, + {0x1761, "keymap_rp/21/002.png"}, + {0x1762, "keymap_rp/21/003.png"}, + {0x1763, "keymap_rp/21/004.png"}, + {0x1764, "keymap_rp/21/005.png"}, + {0x1765, "keymap_rp/21/006.png"}, + {0x1766, "keymap_rp/21/007.png"}, + {0x1767, "keymap_rp/21/008.png"}, + {0x1768, "keymap_rp/21/009.png"}, + {0x1769, "keymap_rp/21/010.png"}, + {0x1770, "keymap_rp/22/001.png"}, + {0x1771, "keymap_rp/22/002.png"}, + {0x1772, "keymap_rp/22/003.png"}, + {0x1773, "keymap_rp/22/004.png"}, + {0x1774, "keymap_rp/22/005.png"}, + {0x1775, "keymap_rp/22/006.png"}, + {0x1776, "keymap_rp/22/007.png"}, + {0x1777, "keymap_rp/22/008.png"}, + {0x1778, "keymap_rp/22/009.png"}, + {0x1779, "keymap_rp/22/010.png"}, + {0x1780, "keymap_rp/23/001.png"}, + {0x1781, "keymap_rp/23/002.png"}, + {0x1782, "keymap_rp/23/003.png"}, + {0x1783, "keymap_rp/23/004.png"}, + {0x1784, "keymap_rp/23/005.png"}, + {0x1785, "keymap_rp/23/006.png"}, + {0x1786, "keymap_rp/23/007.png"}, + {0x1787, "keymap_rp/23/008.png"}, + {0x1788, "keymap_rp/23/009.png"}, + {0x1789, "keymap_rp/23/010.png"}, + {0x1790, "keymap_rp/24/001.png"}, + {0x1791, "keymap_rp/24/002.png"}, + {0x1792, "keymap_rp/24/003.png"}, + {0x1793, "keymap_rp/24/004.png"}, + {0x1794, "keymap_rp/24/005.png"}, + {0x1795, "keymap_rp/24/006.png"}, + {0x1796, "keymap_rp/24/007.png"}, + {0x1797, "keymap_rp/24/008.png"}, + {0x1798, "keymap_rp/24/009.png"}, + {0x1799, "keymap_rp/24/010.png"}, + {0x17A0, "keymap_rp/25/001.png"}, + {0x17A1, "keymap_rp/25/002.png"}, + {0x17A2, "keymap_rp/25/003.png"}, + {0x17A3, "keymap_rp/25/004.png"}, + {0x17A4, "keymap_rp/25/005.png"}, + {0x17A5, "keymap_rp/25/006.png"}, + {0x17A6, "keymap_rp/25/007.png"}, + {0x17A7, "keymap_rp/25/008.png"}, + {0x17A8, "keymap_rp/25/009.png"}, + {0x17A9, "keymap_rp/25/010.png"}, + {0x17B0, "keymap_rp/26/001.png"}, + {0x17B1, "keymap_rp/26/002.png"}, + {0x17B2, "keymap_rp/26/003.png"}, + {0x17B3, "keymap_rp/26/004.png"}, + {0x17B4, "keymap_rp/26/005.png"}, + {0x17B5, "keymap_rp/26/006.png"}, + {0x17B6, "keymap_rp/26/007.png"}, + {0x17B7, "keymap_rp/26/008.png"}, + {0x17B8, "keymap_rp/26/009.png"}, + {0x17B9, "keymap_rp/26/010.png"}, + {0x17C0, "keymap_rp/27/001.png"}, + {0x17C1, "keymap_rp/27/002.png"}, + {0x17C2, "keymap_rp/27/003.png"}, + {0x17C3, "keymap_rp/27/004.png"}, + {0x17C4, "keymap_rp/27/005.png"}, + {0x17C5, "keymap_rp/27/006.png"}, + {0x17C6, "keymap_rp/27/007.png"}, + {0x17C7, "keymap_rp/27/008.png"}, + {0x17C8, "keymap_rp/27/009.png"}, + {0x17C9, "keymap_rp/27/010.png"}, + {0x17D0, "keymap_rp/28/001.png"}, + {0x17D1, "keymap_rp/28/002.png"}, + {0x17D2, "keymap_rp/28/003.png"}, + {0x17D3, "keymap_rp/28/004.png"}, + {0x17D4, "keymap_rp/28/005.png"}, + {0x17D5, "keymap_rp/28/006.png"}, + {0x17D6, "keymap_rp/28/007.png"}, + {0x17D7, "keymap_rp/28/008.png"}, + {0x17D8, "keymap_rp/28/009.png"}, + {0x17D9, "keymap_rp/28/010.png"}, + {0x17E0, "keymap_rp/29/001.png"}, + {0x17E1, "keymap_rp/29/002.png"}, + {0x17E2, "keymap_rp/29/003.png"}, + {0x17E3, "keymap_rp/29/004.png"}, + {0x17E4, "keymap_rp/29/005.png"}, + {0x17E5, "keymap_rp/29/006.png"}, + {0x17E6, "keymap_rp/29/007.png"}, + {0x17E7, "keymap_rp/29/008.png"}, + {0x17E8, "keymap_rp/29/009.png"}, + {0x17E9, "keymap_rp/29/010.png"}, + {0x17F0, "keymap_rp/30/001.png"}, + {0x17F1, "keymap_rp/30/002.png"}, + {0x17F2, "keymap_rp/30/003.png"}, + {0x17F3, "keymap_rp/30/004.png"}, + {0x17F4, "keymap_rp/30/005.png"}, + {0x17F5, "keymap_rp/30/006.png"}, + {0x17F6, "keymap_rp/30/007.png"}, + {0x17F7, "keymap_rp/30/008.png"}, + {0x17F8, "keymap_rp/30/009.png"}, + {0x17F9, "keymap_rp/30/010.png"}, +}}; + +std::string_view GetEntryNameByType(u32 type) { + const auto key = PkgEntryValue{type}; + const auto it = std::ranges::lower_bound(PkgEntries, key); + if (it != PkgEntries.end() && it->type == type) { + return it->name; + } + return ""; +} diff --git a/src/core/file_format/pkg_type.h b/src/core/file_format/pkg_type.h new file mode 100644 index 000000000..6b010e3a3 --- /dev/null +++ b/src/core/file_format/pkg_type.h @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "common/types.h" + +/// Retrieves the PKG entry name from its type identifier. +std::string_view GetEntryNameByType(u32 type); diff --git a/src/core/file_format/psf.cpp b/src/core/file_format/psf.cpp new file mode 100644 index 000000000..fb808697e --- /dev/null +++ b/src/core/file_format/psf.cpp @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "common/io_file.h" +#include "psf.h" + +PSF::PSF() = default; + +PSF::~PSF() = default; + +bool PSF::open(const std::string& filepath) { + Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read); + if (!file.IsOpen()) { + return false; + } + + const u64 psfSize = file.GetSize(); + psf.resize(psfSize); + file.Seek(0); + file.Read(psf); + + // Parse file contents + PSFHeader header; + std::memcpy(&header, psf.data(), sizeof(header)); + for (u32 i = 0; i < header.index_table_entries; i++) { + PSFEntry entry; + std::memcpy(&entry, &psf[sizeof(PSFHeader) + i * sizeof(PSFEntry)], sizeof(entry)); + + const std::string key = (char*)&psf[header.key_table_offset + entry.key_offset]; + if (entry.param_fmt == PSFEntry::Fmt::TextRaw || + entry.param_fmt == PSFEntry::Fmt::TextNormal) { + map_strings[key] = (char*)&psf[header.data_table_offset + entry.data_offset]; + } + if (entry.param_fmt == PSFEntry::Fmt::Integer) { + u32 value; + std::memcpy(&value, &psf[header.data_table_offset + entry.data_offset], sizeof(value)); + map_integers[key] = value; + } + } + return true; +} + +std::string PSF::GetString(const std::string& key) { + if (map_strings.find(key) != map_strings.end()) { + return map_strings.at(key); + } + return ""; +} + +u32 PSF::GetInteger(const std::string& key) { + if (map_integers.find(key) != map_integers.end()) { + return map_integers.at(key); // TODO std::invalid_argument exception if it fails? + } + return 0; +} diff --git a/src/core/file_format/psf.h b/src/core/file_format/psf.h new file mode 100644 index 000000000..319786300 --- /dev/null +++ b/src/core/file_format/psf.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include "common/endian.h" + +struct PSFHeader { + u32_be magic; + u32_le version; + u32_le key_table_offset; + u32_le data_table_offset; + u32_le index_table_entries; +}; + +struct PSFEntry { + enum Fmt : u16 { + TextRaw = 0x0400, // String in UTF-8 format and not NULL terminated + TextNormal = 0x0402, // String in UTF-8 format and NULL terminated + Integer = 0x0404, // Unsigned 32-bit integer + }; + + u16_le key_offset; + u16_be param_fmt; + u32_le param_len; + u32_le param_max_len; + u32_le data_offset; +}; + +class PSF { +public: + PSF(); + ~PSF(); + + bool open(const std::string& filepath); + + std::string GetString(const std::string& key); + u32 GetInteger(const std::string& key); + + std::unordered_map map_strings; + std::unordered_map map_integers; + +private: + std::vector psf; +}; diff --git a/src/core/loader.cpp b/src/core/loader.cpp new file mode 100644 index 000000000..b12821c1b --- /dev/null +++ b/src/core/loader.cpp @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/io_file.h" +#include "common/types.h" +#include "loader.h" + +namespace Loader { + +FileTypes DetectFileType(const std::string& filepath) { + // No file loaded + if (filepath.empty()) { + return FileTypes::Unknown; + } + Common::FS::IOFile file; + file.Open(filepath, Common::FS::FileAccessMode::Read); + file.Seek(0); + u32 magic; + file.Read(magic); + file.Close(); + switch (magic) { + case PkgMagic: + return FileTypes::Pkg; + } + return FileTypes::Unknown; +} + +} // namespace Loader diff --git a/src/core/loader.h b/src/core/loader.h new file mode 100644 index 000000000..2f4d06513 --- /dev/null +++ b/src/core/loader.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace Loader { + +constexpr static u32 PkgMagic = 0x544e437f; + +enum class FileTypes { + Unknown, + Pkg, +}; + +FileTypes DetectFileType(const std::string& filepath); +} // namespace Loader diff --git a/src/images/shadps4.ico b/src/images/shadps4.ico new file mode 100644 index 0000000000000000000000000000000000000000..ecf6675aa7d45182f9717d1159c1be5780943eb2 GIT binary patch literal 157385 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

-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 literal 0 HcmV?d00001 diff --git a/src/qt_gui/custom_dock_widget.h b/src/qt_gui/custom_dock_widget.h new file mode 100644 index 000000000..9d40bb2fc --- /dev/null +++ b/src/qt_gui/custom_dock_widget.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +class CustomDockWidget : public QDockWidget { +private: + std::shared_ptr m_title_bar_widget; + bool m_is_title_bar_visible = true; + +public: + explicit CustomDockWidget(const QString& title, QWidget* parent = Q_NULLPTR, + Qt::WindowFlags flags = Qt::WindowFlags()) + : QDockWidget(title, parent, flags) { + m_title_bar_widget.reset(titleBarWidget()); + + connect(this, &QDockWidget::topLevelChanged, [this](bool /* topLevel*/) { + SetTitleBarVisible(m_is_title_bar_visible); + style()->unpolish(this); + style()->polish(this); + }); + } + + void SetTitleBarVisible(bool visible) { + if (visible || isFloating()) { + if (m_title_bar_widget.get() != titleBarWidget()) { + setTitleBarWidget(m_title_bar_widget.get()); + QMargins margins = widget()->contentsMargins(); + margins.setTop(0); + widget()->setContentsMargins(margins); + } + } else { + setTitleBarWidget(new QWidget()); + QMargins margins = widget()->contentsMargins(); + margins.setTop(1); + widget()->setContentsMargins(margins); + } + + m_is_title_bar_visible = visible; + } + +protected: + void paintEvent(QPaintEvent* event) override { + // We need to repaint the dock widgets as plain widgets in floating mode. + // Source: + // https://stackoverflow.com/questions/10272091/cannot-add-a-background-image-to-a-qdockwidget + if (isFloating()) { + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + return; + } + + // Use inherited method for docked mode because otherwise the dock would lose the title etc. + QDockWidget::paintEvent(event); + } +}; diff --git a/src/qt_gui/custom_table_widget_item.cpp b/src/qt_gui/custom_table_widget_item.cpp new file mode 100644 index 000000000..321f22dc6 --- /dev/null +++ b/src/qt_gui/custom_table_widget_item.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "custom_table_widget_item.h" + +CustomTableWidgetItem::CustomTableWidgetItem(const std::string& text, int sort_role, + const QVariant& sort_value) + : GameListItem( + QString::fromStdString(text).simplified()) // simplified() forces single line text +{ + if (sort_role != Qt::DisplayRole) { + setData(sort_role, sort_value, true); + } +} + +CustomTableWidgetItem::CustomTableWidgetItem(const QString& text, int sort_role, + const QVariant& sort_value) + : GameListItem(text.simplified()) // simplified() forces single line text +{ + if (sort_role != Qt::DisplayRole) { + setData(sort_role, sort_value, true); + } +} + +bool CustomTableWidgetItem::operator<(const QTableWidgetItem& other) const { + if (m_sort_role == Qt::DisplayRole) { + return QTableWidgetItem::operator<(other); + } + + const QVariant data_l = data(m_sort_role); + const QVariant data_r = other.data(m_sort_role); + const QVariant::Type type_l = data_l.type(); + const QVariant::Type type_r = data_r.type(); + + switch (type_l) { + case QVariant::Type::Bool: + case QVariant::Type::Int: + return data_l.toInt() < data_r.toInt(); + case QVariant::Type::UInt: + return data_l.toUInt() < data_r.toUInt(); + case QVariant::Type::LongLong: + return data_l.toLongLong() < data_r.toLongLong(); + case QVariant::Type::ULongLong: + return data_l.toULongLong() < data_r.toULongLong(); + case QVariant::Type::Double: + return data_l.toDouble() < data_r.toDouble(); + case QVariant::Type::Date: + return data_l.toDate() < data_r.toDate(); + case QVariant::Type::Time: + return data_l.toTime() < data_r.toTime(); + case QVariant::Type::DateTime: + return data_l.toDateTime() < data_r.toDateTime(); + case QVariant::Type::Char: + case QVariant::Type::String: + return data_l.toString() < data_r.toString(); + default: + throw std::runtime_error("unsupported type"); + } +} + +void CustomTableWidgetItem::setData(int role, const QVariant& value, bool assign_sort_role) { + if (assign_sort_role) { + m_sort_role = role; + } + QTableWidgetItem::setData(role, value); +} diff --git a/src/qt_gui/custom_table_widget_item.h b/src/qt_gui/custom_table_widget_item.h new file mode 100644 index 000000000..e2c497f1e --- /dev/null +++ b/src/qt_gui/custom_table_widget_item.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "game_list_item.h" + +class CustomTableWidgetItem : public GameListItem { +private: + int m_sort_role = Qt::DisplayRole; + +public: + using QTableWidgetItem::setData; + + CustomTableWidgetItem() = default; + CustomTableWidgetItem(const std::string& text, int sort_role = Qt::DisplayRole, + const QVariant& sort_value = 0); + CustomTableWidgetItem(const QString& text, int sort_role = Qt::DisplayRole, + const QVariant& sort_value = 0); + + bool operator<(const QTableWidgetItem& other) const override; + + void setData(int role, const QVariant& value, bool assign_sort_role); +}; diff --git a/src/qt_gui/game_info.h b/src/qt_gui/game_info.h new file mode 100644 index 000000000..fb29948ad --- /dev/null +++ b/src/qt_gui/game_info.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +struct GameInfo { + std::string path; // root path of game directory (normaly directory that contains eboot.bin) + std::string icon_path; // path of icon0.png + std::string pic_path; // path of pic1.png + + // variables extracted from param.sfo + std::string name = "Unknown"; + std::string serial = "Unknown"; + std::string app_ver = "Unknown"; + std::string version = "Unknown"; + std::string category = "Unknown"; + std::string fw = "Unknown"; +}; \ No newline at end of file diff --git a/src/qt_gui/game_install_dialog.cpp b/src/qt_gui/game_install_dialog.cpp new file mode 100644 index 000000000..1fa6880b5 --- /dev/null +++ b/src/qt_gui/game_install_dialog.cpp @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "game_install_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gui_settings.h" + +GameInstallDialog::GameInstallDialog(std::shared_ptr gui_settings) + : m_gamesDirectory(nullptr), m_gui_settings(std::move(gui_settings)) { + auto layout = new QVBoxLayout(this); + + layout->addWidget(SetupGamesDirectory()); + layout->addStretch(); + layout->addWidget(SetupDialogActions()); + + setWindowTitle("Shadps4 - Choose directory"); +} + +GameInstallDialog::~GameInstallDialog() {} + +void GameInstallDialog::Browse() { + auto path = QFileDialog::getExistingDirectory(this, "Directory to install games"); + + if (!path.isEmpty()) { + m_gamesDirectory->setText(QDir::toNativeSeparators(path)); + } +} + +QWidget* GameInstallDialog::SetupGamesDirectory() { + auto group = new QGroupBox("Directory to install games"); + auto layout = new QHBoxLayout(group); + + // Input. + m_gamesDirectory = new QLineEdit(); + m_gamesDirectory->setText(m_gui_settings->GetValue(gui::settings_install_dir).toString()); + m_gamesDirectory->setMinimumWidth(400); + + layout->addWidget(m_gamesDirectory); + + // Browse button. + auto browse = new QPushButton("..."); + + connect(browse, &QPushButton::clicked, this, &GameInstallDialog::Browse); + + layout->addWidget(browse); + + return group; +} + +QWidget* GameInstallDialog::SetupDialogActions() { + auto actions = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + connect(actions, &QDialogButtonBox::accepted, this, &GameInstallDialog::Save); + connect(actions, &QDialogButtonBox::rejected, this, &GameInstallDialog::reject); + + return actions; +} + +void GameInstallDialog::Save() { + // Check games directory. + auto gamesDirectory = m_gamesDirectory->text(); + + if (gamesDirectory.isEmpty() || !QDir(gamesDirectory).exists() || + !QDir::isAbsolutePath(gamesDirectory)) { + QMessageBox::critical(this, "Error", + "The value for location to install games is not valid."); + return; + } + + m_gui_settings->SetValue(gui::settings_install_dir, QDir::toNativeSeparators(gamesDirectory)); + + accept(); +} diff --git a/src/qt_gui/game_install_dialog.h b/src/qt_gui/game_install_dialog.h new file mode 100644 index 000000000..b75aaaf64 --- /dev/null +++ b/src/qt_gui/game_install_dialog.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "gui_settings.h" + +class QLineEdit; + +class GameInstallDialog final : public QDialog { +public: + GameInstallDialog(std::shared_ptr gui_settings); + ~GameInstallDialog(); + +private slots: + void Browse(); + +private: + QWidget* SetupGamesDirectory(); + QWidget* SetupDialogActions(); + void Save(); + +private: + QLineEdit* m_gamesDirectory; + std::shared_ptr m_gui_settings; +}; \ No newline at end of file diff --git a/src/qt_gui/game_list_frame.cpp b/src/qt_gui/game_list_frame.cpp new file mode 100644 index 000000000..e9bd92b08 --- /dev/null +++ b/src/qt_gui/game_list_frame.cpp @@ -0,0 +1,886 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include "core/file_format/psf.h" +#include "custom_table_widget_item.h" +#include "game_list_frame.h" +#include "gui_settings.h" +#include "qt_utils.h" + +GameListFrame::GameListFrame(std::shared_ptr gui_settings, QWidget* parent) + : CustomDockWidget(tr("Game List"), parent), m_gui_settings(std::move(gui_settings)) { + m_icon_size = gui::game_list_icon_size_min; // ensure a valid size + m_is_list_layout = m_gui_settings->GetValue(gui::game_list_listMode).toBool(); + m_margin_factor = m_gui_settings->GetValue(gui::game_list_marginFactor).toReal(); + m_text_factor = m_gui_settings->GetValue(gui::game_list_textFactor).toReal(); + m_icon_color = m_gui_settings->GetValue(gui::game_list_iconColor).value(); + m_col_sort_order = m_gui_settings->GetValue(gui::game_list_sortAsc).toBool() + ? Qt::AscendingOrder + : Qt::DescendingOrder; + m_sort_column = m_gui_settings->GetValue(gui::game_list_sortCol).toInt(); + + m_old_layout_is_list = m_is_list_layout; + + // Save factors for first setup + m_gui_settings->SetValue(gui::game_list_iconColor, m_icon_color); + m_gui_settings->SetValue(gui::game_list_marginFactor, m_margin_factor); + m_gui_settings->SetValue(gui::game_list_textFactor, m_text_factor); + + m_game_dock = new QMainWindow(this); + m_game_dock->setWindowFlags(Qt::Widget); + setWidget(m_game_dock); + + m_game_grid = new GameListGrid(QSize(), m_icon_color, m_margin_factor, m_text_factor, false); + + m_game_list = new GameListTable(); + m_game_list->setShowGrid(false); + m_game_list->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_game_list->setSelectionBehavior(QAbstractItemView::SelectRows); + m_game_list->setSelectionMode(QAbstractItemView::SingleSelection); + m_game_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_game_list->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + m_game_list->verticalScrollBar()->installEventFilter(this); + m_game_list->verticalScrollBar()->setSingleStep(20); + m_game_list->horizontalScrollBar()->setSingleStep(20); + m_game_list->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + m_game_list->verticalHeader()->setVisible(false); + m_game_list->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + m_game_list->horizontalHeader()->setHighlightSections(false); + m_game_list->horizontalHeader()->setSortIndicatorShown(true); + m_game_list->horizontalHeader()->setStretchLastSection(true); + m_game_list->setContextMenuPolicy(Qt::CustomContextMenu); + m_game_list->installEventFilter(this); + m_game_list->setColumnCount(gui::column_count); + m_game_list->setColumnWidth(1, 250); + m_game_list->setColumnWidth(2, 110); + m_game_list->setColumnWidth(3, 80); + m_game_list->setColumnWidth(4, 90); + m_game_list->setColumnWidth(5, 80); + m_game_list->setColumnWidth(6, 80); + QPalette palette; + palette.setColor(QPalette::Base, QColor(230, 230, 230, 80)); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + m_game_list->setPalette(palette); + m_central_widget = new QStackedWidget(this); + m_central_widget->addWidget(m_game_list); + m_central_widget->addWidget(m_game_grid); + m_central_widget->setCurrentWidget(m_is_list_layout ? m_game_list : m_game_grid); + + m_game_dock->setCentralWidget(m_central_widget); + + // Actions regarding showing/hiding columns + auto add_column = [this](gui::game_list_columns col, const QString& header_text, + const QString& action_text) { + QTableWidgetItem* item_ = new QTableWidgetItem(header_text); + item_->setTextAlignment(Qt::AlignCenter); // Center-align text + m_game_list->setHorizontalHeaderItem(col, item_); + m_columnActs.append(new QAction(action_text, this)); + }; + + add_column(gui::column_icon, tr("Icon"), tr("Show Icons")); + add_column(gui::column_name, tr("Name"), tr("Show Names")); + add_column(gui::column_serial, tr("Serial"), tr("Show Serials")); + add_column(gui::column_firmware, tr("Firmware"), tr("Show Firmwares")); + add_column(gui::column_size, tr("Size"), tr("Show Size")); + add_column(gui::column_version, tr("Version"), tr("Show Versions")); + add_column(gui::column_category, tr("Category"), tr("Show Categories")); + add_column(gui::column_path, tr("Path"), tr("Show Paths")); + + for (int col = 0; col < m_columnActs.count(); ++col) { + m_columnActs[col]->setCheckable(true); + + connect(m_columnActs[col], &QAction::triggered, this, [this, col](bool checked) { + if (!checked) // be sure to have at least one column left so you can call the context + // menu at all time + { + int c = 0; + for (int i = 0; i < m_columnActs.count(); ++i) { + if (m_gui_settings->GetGamelistColVisibility(i) && ++c > 1) + break; + } + if (c < 2) { + m_columnActs[col]->setChecked( + true); // re-enable the checkbox if we don't change the actual state + return; + } + } + m_game_list->setColumnHidden( + col, !checked); // Negate because it's a set col hidden and we have menu say show. + m_gui_settings->SetGamelistColVisibility(col, checked); + + if (checked) // handle hidden columns that have zero width after showing them (stuck + // between others) + { + FixNarrowColumns(); + } + }); + } + + // events + connect(m_game_list->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, + [this](const QPoint& pos) { + QMenu* configure = new QMenu(this); + configure->addActions(m_columnActs); + configure->exec(m_game_list->horizontalHeader()->viewport()->mapToGlobal(pos)); + }); + connect(m_game_list->horizontalHeader(), &QHeaderView::sectionClicked, this, + &GameListFrame::OnHeaderColumnClicked); + connect(&m_repaint_watcher, &QFutureWatcher::resultReadyAt, this, + [this](int index) { + if (!m_is_list_layout) + return; + if (GameListItem* item = m_repaint_watcher.resultAt(index)) { + item->call_icon_func(); + } + }); + connect(&m_repaint_watcher, &QFutureWatcher::finished, this, + &GameListFrame::OnRepaintFinished); + + connect(&m_refresh_watcher, &QFutureWatcher::finished, this, + &GameListFrame::OnRefreshFinished); + connect(&m_refresh_watcher, &QFutureWatcher::canceled, this, [this]() { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + + m_path_list.clear(); + m_game_data.clear(); + m_games.clear(); + }); + connect(m_game_list, &QTableWidget::customContextMenuRequested, this, + &GameListFrame::RequestGameMenu); + connect(m_game_grid, &QTableWidget::customContextMenuRequested, this, + &GameListFrame::RequestGameMenu); + + connect(m_game_list, &QTableWidget::itemClicked, this, &GameListFrame::SetListBackgroundImage); + connect(this, &GameListFrame::ResizedWindow, this, &GameListFrame::SetListBackgroundImage); + connect(m_game_list->verticalScrollBar(), &QScrollBar::valueChanged, this, + &GameListFrame::RefreshListBackgroundImage); + connect(m_game_list->horizontalScrollBar(), &QScrollBar::valueChanged, this, + &GameListFrame::RefreshListBackgroundImage); +} + +GameListFrame::~GameListFrame() { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + gui::utils::stop_future_watcher(m_refresh_watcher, true); + SaveSettings(); +} + +void GameListFrame::OnRefreshFinished() { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + for (auto&& g : m_games) { + m_game_data.push_back(g); + } + m_games.clear(); + // Sort by name at the very least. + std::sort(m_game_data.begin(), m_game_data.end(), + [&](const game_info& game1, const game_info& game2) { + const QString title1 = m_titles.value(QString::fromStdString(game1->info.serial), + QString::fromStdString(game1->info.name)); + const QString title2 = m_titles.value(QString::fromStdString(game2->info.serial), + QString::fromStdString(game2->info.name)); + return title1.toLower() < title2.toLower(); + }); + + m_path_list.clear(); + + Refresh(); +} + +void GameListFrame::RequestGameMenu(const QPoint& pos) { + + QPoint global_pos; + game_info gameinfo; + + if (m_is_list_layout) { + QTableWidgetItem* item = m_game_list->item( + m_game_list->indexAt(pos).row(), static_cast(gui::game_list_columns::column_icon)); + global_pos = m_game_list->viewport()->mapToGlobal(pos); + gameinfo = GetGameInfoFromItem(item); + } else { + const QModelIndex mi = m_game_grid->indexAt(pos); + QTableWidgetItem* item = m_game_grid->item(mi.row(), mi.column()); + global_pos = m_game_grid->viewport()->mapToGlobal(pos); + gameinfo = GetGameInfoFromItem(item); + } + + if (!gameinfo) { + return; + } + + // Setup menu. + QMenu menu(this); + QAction openFolder("Open Game Folder", this); + QAction openSfoViewer("SFO Viewer", this); + + menu.addAction(&openFolder); + menu.addAction(&openSfoViewer); + // Show menu. + auto selected = menu.exec(global_pos); + if (!selected) { + return; + } + + if (selected == &openFolder) { + QString folderPath = QString::fromStdString(gameinfo->info.path); + QDesktopServices::openUrl(QUrl::fromLocalFile(folderPath)); + } + + if (selected == &openSfoViewer) { + PSF psf; + if (psf.open(gameinfo->info.path + "/sce_sys/param.sfo")) { + int rows = psf.map_strings.size() + psf.map_integers.size(); + QTableWidget* tableWidget = new QTableWidget(rows, 2); + tableWidget->verticalHeader()->setVisible(false); // Hide vertical header + int row = 0; + + for (const auto& pair : psf.map_strings) { + QTableWidgetItem* keyItem = + new QTableWidgetItem(QString::fromStdString(pair.first)); + QTableWidgetItem* valueItem = + new QTableWidgetItem(QString::fromStdString(pair.second)); + + tableWidget->setItem(row, 0, keyItem); + tableWidget->setItem(row, 1, valueItem); + keyItem->setFlags(keyItem->flags() & ~Qt::ItemIsEditable); + valueItem->setFlags(valueItem->flags() & ~Qt::ItemIsEditable); + row++; + } + for (const auto& pair : psf.map_integers) { + QTableWidgetItem* keyItem = + new QTableWidgetItem(QString::fromStdString(pair.first)); + QTableWidgetItem* valueItem = new QTableWidgetItem(QString::number(pair.second)); + + tableWidget->setItem(row, 0, keyItem); + tableWidget->setItem(row, 1, valueItem); + keyItem->setFlags(keyItem->flags() & ~Qt::ItemIsEditable); + valueItem->setFlags(valueItem->flags() & ~Qt::ItemIsEditable); + row++; + } + tableWidget->resizeColumnsToContents(); + tableWidget->resizeRowsToContents(); + + int width = tableWidget->horizontalHeader()->sectionSize(0) + + tableWidget->horizontalHeader()->sectionSize(1) + 2; + int height = (rows + 1) * (tableWidget->rowHeight(0)); + tableWidget->setFixedSize(width, height); + tableWidget->sortItems(0, Qt::AscendingOrder); + tableWidget->horizontalHeader()->setVisible(false); + + tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed); + tableWidget->setWindowTitle("SFO Viewer"); + tableWidget->show(); + } + } +} + +void GameListFrame::RefreshListBackgroundImage() { + QPixmap blurredPixmap = QPixmap::fromImage(backgroundImage); + QPalette palette; + palette.setBrush(QPalette::Base, QBrush(blurredPixmap.scaled(size(), Qt::IgnoreAspectRatio))); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + m_game_list->setPalette(palette); +} + +void GameListFrame::SetListBackgroundImage(QTableWidgetItem* item) { + if (!item) { + // handle case where no item was clicked + return; + } + QTableWidgetItem* iconItem = + m_game_list->item(item->row(), static_cast(gui::game_list_columns::column_icon)); + + if (!iconItem) { + // handle case where icon item does not exist + return; + } + game_info gameinfo = GetGameInfoFromItem(iconItem); + QString pic1Path = QString::fromStdString(gameinfo->info.pic_path); + QString blurredPic1Path = + qApp->applicationDirPath() + + QString::fromStdString("/game_data/" + gameinfo->info.serial + "/pic1.png"); + + backgroundImage = QImage(blurredPic1Path); + if (backgroundImage.isNull()) { + QImage image(pic1Path); + backgroundImage = m_game_list_utils.BlurImage(image, image.rect(), 18); + + std::filesystem::path img_path = + std::filesystem::path("game_data/") / gameinfo->info.serial; + std::filesystem::create_directories(img_path); + if (!backgroundImage.save(blurredPic1Path, "PNG")) { + // qDebug() << "Error: Unable to save image."; + } + } + QPixmap blurredPixmap = QPixmap::fromImage(backgroundImage); + QPalette palette; + palette.setBrush(QPalette::Base, QBrush(blurredPixmap.scaled(size(), Qt::IgnoreAspectRatio))); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + m_game_list->setPalette(palette); +} + +void GameListFrame::OnRepaintFinished() { + if (m_is_list_layout) { + // Fixate vertical header and row height + m_game_list->verticalHeader()->setMinimumSectionSize(m_icon_size.height()); + m_game_list->verticalHeader()->setMaximumSectionSize(m_icon_size.height()); + + // Resize the icon column + m_game_list->resizeColumnToContents(gui::column_icon); + + // Shorten the last section to remove horizontal scrollbar if possible + m_game_list->resizeColumnToContents(gui::column_count - 1); + } else { + // The game grid needs to be recreated from scratch + int games_per_row = 0; + + if (m_icon_size.width() > 0 && m_icon_size.height() > 0) { + games_per_row = width() / (m_icon_size.width() + + m_icon_size.width() * m_game_grid->getMarginFactor() * 2); + } + + const int scroll_position = m_game_grid->verticalScrollBar()->value(); + // TODO add connections + PopulateGameGrid(games_per_row, m_icon_size, m_icon_color); + m_central_widget->addWidget(m_game_grid); + m_central_widget->setCurrentWidget(m_game_grid); + m_game_grid->verticalScrollBar()->setValue(scroll_position); + + connect(m_game_grid, &QTableWidget::customContextMenuRequested, this, + &GameListFrame::RequestGameMenu); + } +} + +bool GameListFrame::IsEntryVisible(const game_info& game) { + const QString serial = QString::fromStdString(game->info.serial); + return SearchMatchesApp(QString::fromStdString(game->info.name), serial); +} + +game_info GameListFrame::GetGameInfoFromItem(const QTableWidgetItem* item) { + if (!item) { + return nullptr; + } + + const QVariant var = item->data(gui::game_role); + if (!var.canConvert()) { + return nullptr; + } + + return var.value(); +} + +void GameListFrame::PopulateGameGrid(int maxCols, const QSize& image_size, + const QColor& image_color) { + int r = 0; + int c = 0; + + const std::string selected_item = CurrentSelectionPath(); + + // Release old data + m_game_list->clear_list(); + m_game_grid->deleteLater(); + + const bool show_text = m_icon_size_index > gui::game_list_max_slider_pos * 2 / 5; + + if (m_icon_size_index < gui::game_list_max_slider_pos * 2 / 3) { + m_game_grid = new GameListGrid(image_size, image_color, m_margin_factor, m_text_factor * 2, + show_text); + } else { + m_game_grid = + new GameListGrid(image_size, image_color, m_margin_factor, m_text_factor, show_text); + } + + // Get list of matching apps + QList matching_apps; + + for (const auto& app : m_game_data) { + if (IsEntryVisible(app)) { + matching_apps.push_back(app); + } + } + + const int entries = matching_apps.count(); + + // Edge cases! + if (entries == 0) { // For whatever reason, 0%x is division by zero. Absolute nonsense by + // definition of modulus. But, I'll acquiesce. + return; + } + + maxCols = std::clamp(maxCols, 1, entries); + + const int needs_extra_row = (entries % maxCols) != 0; + const int max_rows = needs_extra_row + entries / maxCols; + m_game_grid->setRowCount(max_rows); + m_game_grid->setColumnCount(maxCols); + + for (const auto& app : matching_apps) { + const QString serial = QString::fromStdString(app->info.serial); + const QString title = m_titles.value(serial, QString::fromStdString(app->info.name)); + + GameListItem* item = m_game_grid->addItem(app, title, r, c); + app->item = item; + item->setData(gui::game_role, QVariant::fromValue(app)); + + item->setToolTip(tr("%0 [%1]").arg(title).arg(serial)); + + if (selected_item == app->info.path + app->info.icon_path) { + m_game_grid->setCurrentItem(item); + } + + if (++c >= maxCols) { + c = 0; + r++; + } + } + + if (c != 0) { // if left over games exist -- if empty entries exist + for (int col = c; col < maxCols; ++col) { + GameListItem* empty_item = new GameListItem(); + empty_item->setFlags(Qt::NoItemFlags); + m_game_grid->setItem(r, col, empty_item); + } + } + + m_game_grid->resizeColumnsToContents(); + m_game_grid->resizeRowsToContents(); + m_game_grid->installEventFilter(this); + m_game_grid->verticalScrollBar()->installEventFilter(this); +} +void GameListFrame::Refresh(const bool from_drive, const bool scroll_after) { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + gui::utils::stop_future_watcher(m_refresh_watcher, from_drive); + + if (from_drive) { + m_path_list.clear(); + m_game_data.clear(); + m_games.clear(); + + // TODO better ATM manually add path from 1 dir to m_paths_list + QDir parent_folder(m_gui_settings->GetValue(gui::settings_install_dir).toString() + '/'); + QFileInfoList fList = + parent_folder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::DirsFirst); + foreach (QFileInfo item, fList) { + m_path_list.emplace_back(item.absoluteFilePath().toStdString()); + } + + m_refresh_watcher.setFuture(QtConcurrent::map(m_path_list, [this](const std::string& dir) { + GameInfo game{}; + game.path = dir; + PSF psf; + if (psf.open(game.path + "/sce_sys/param.sfo")) { + QString iconpath(QString::fromStdString(game.path) + "/sce_sys/icon0.png"); + QString picpath(QString::fromStdString(game.path) + "/sce_sys/pic1.png"); + game.icon_path = iconpath.toStdString(); + game.pic_path = picpath.toStdString(); + game.name = psf.GetString("TITLE"); + game.serial = psf.GetString("TITLE_ID"); + game.fw = (QString("%1").arg(psf.GetInteger("SYSTEM_VER"), 8, 16, QLatin1Char('0'))) + .mid(1, 3) + .insert(1, '.') + .toStdString(); + game.version = psf.GetString("APP_VER"); + game.category = psf.GetString("CATEGORY"); + + m_titles.insert(QString::fromStdString(game.serial), + QString::fromStdString(game.name)); + + GuiGameInfo info{}; + info.info = game; + + m_games.push_back(std::make_shared(std::move(info))); + } + })); + return; + } + // Fill Game List / Game Grid + + if (m_is_list_layout) { + const int scroll_position = m_game_list->verticalScrollBar()->value(); + PopulateGameList(); + SortGameList(); + RepaintIcons(); + + if (scroll_after) { + m_game_list->scrollTo(m_game_list->currentIndex(), QAbstractItemView::PositionAtCenter); + } else { + m_game_list->verticalScrollBar()->setValue(scroll_position); + } + } else { + RepaintIcons(); + } +} +/** + Cleans and readds entries to table widget in UI. +*/ +void GameListFrame::PopulateGameList() { + int selected_row = -1; + + const std::string selected_item = CurrentSelectionPath(); + + // Release old data + m_game_grid->clear_list(); + m_game_list->clear_list(); + + m_game_list->setRowCount(m_game_data.size()); + + int row = 0; + int index = -1; + for (const auto& game : m_game_data) { + index++; + + if (!IsEntryVisible(game)) { + game->item = nullptr; + continue; + } + + // Icon + CustomTableWidgetItem* icon_item = new CustomTableWidgetItem; + game->item = icon_item; + icon_item->set_icon_func([this, icon_item, game](int) { + icon_item->setData(Qt::DecorationRole, game->pxmap); + game->pxmap = {}; + }); + + icon_item->setData(Qt::UserRole, index, true); + icon_item->setData(gui::custom_roles::game_role, QVariant::fromValue(game)); + + m_game_list->setItem(row, gui::column_icon, icon_item); + SetTableItem(m_game_list, row, gui::column_name, QString::fromStdString(game->info.name)); + SetTableItem(m_game_list, row, gui::column_serial, + QString::fromStdString(game->info.serial)); + SetTableItem(m_game_list, row, gui::column_firmware, QString::fromStdString(game->info.fw)); + SetTableItem( + m_game_list, row, gui::column_size, + m_game_list_utils.GetFolderSize(QDir(QString::fromStdString(game->info.path)))); + SetTableItem(m_game_list, row, gui::column_version, + QString::fromStdString(game->info.version)); + SetTableItem(m_game_list, row, gui::column_category, + QString::fromStdString(game->info.category)); + SetTableItem(m_game_list, row, gui::column_path, QString::fromStdString(game->info.path)); + + if (selected_item == game->info.path + game->info.icon_path) { + selected_row = row; + } + + row++; + } + m_game_list->setRowCount(row); + m_game_list->selectRow(selected_row); +} + +std::string GameListFrame::CurrentSelectionPath() { + std::string selection; + + QTableWidgetItem* item = nullptr; + + if (m_old_layout_is_list) { + if (!m_game_list->selectedItems().isEmpty()) { + item = m_game_list->item(m_game_list->currentRow(), 0); + } + } else if (m_game_grid) { + if (!m_game_grid->selectedItems().isEmpty()) { + item = m_game_grid->currentItem(); + } + } + + if (item) { + if (const QVariant var = item->data(gui::game_role); var.canConvert()) { + if (const game_info game = var.value()) { + selection = game->info.path + game->info.icon_path; + } + } + } + + m_old_layout_is_list = m_is_list_layout; + + return selection; +} + +void GameListFrame::RepaintIcons(const bool& from_settings) { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + + if (from_settings) { + // TODO m_icon_color = gui::utils::get_label_color("gamelist_icon_background_color"); + } + + if (m_is_list_layout) { + QPixmap placeholder(m_icon_size); + placeholder.fill(Qt::transparent); + + for (auto& game : m_game_data) { + game->pxmap = placeholder; + } + + // Fixate vertical header and row height + m_game_list->verticalHeader()->setMinimumSectionSize(m_icon_size.height()); + m_game_list->verticalHeader()->setMaximumSectionSize(m_icon_size.height()); + + // Resize the icon column + m_game_list->resizeColumnToContents(gui::column_icon); + + // Shorten the last section to remove horizontal scrollbar if possible + m_game_list->resizeColumnToContents(gui::column_count - 1); + } + + const std::function func = [this](const game_info& game) -> GameListItem* { + if (game->icon.isNull() && + (game->info.icon_path.empty() || + !game->icon.load(QString::fromStdString(game->info.icon_path)))) { + // TODO added warning message if no found + } + game->pxmap = PaintedPixmap(game->icon); + return game->item; + }; + m_repaint_watcher.setFuture(QtConcurrent::mapped(m_game_data, func)); +} + +void GameListFrame::FixNarrowColumns() const { + qApp->processEvents(); + + // handle columns (other than the icon column) that have zero width after showing them (stuck + // between others) + for (int col = 1; col < m_columnActs.count(); ++col) { + if (m_game_list->isColumnHidden(col)) { + continue; + } + + if (m_game_list->columnWidth(col) <= + m_game_list->horizontalHeader()->minimumSectionSize()) { + m_game_list->setColumnWidth(col, m_game_list->horizontalHeader()->minimumSectionSize()); + } + } +} + +void GameListFrame::ResizeColumnsToContents(int spacing) const { + if (!m_game_list) { + return; + } + + m_game_list->verticalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); + m_game_list->horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); + + // Make non-icon columns slighty bigger for better visuals + for (int i = 1; i < m_game_list->columnCount(); i++) { + if (m_game_list->isColumnHidden(i)) { + continue; + } + + const int size = m_game_list->horizontalHeader()->sectionSize(i) + spacing; + m_game_list->horizontalHeader()->resizeSection(i, size); + } +} + +void GameListFrame::OnHeaderColumnClicked(int col) { + if (col == 0) + return; // Don't "sort" icons. + + if (col == m_sort_column) { + m_col_sort_order = + (m_col_sort_order == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; + } else { + m_col_sort_order = Qt::AscendingOrder; + } + m_sort_column = col; + + m_gui_settings->SetValue(gui::game_list_sortAsc, m_col_sort_order == Qt::AscendingOrder); + m_gui_settings->SetValue(gui::game_list_sortCol, col); + + SortGameList(); +} + +void GameListFrame::SortGameList() const { + // Back-up old header sizes to handle unwanted column resize in case of zero search results + QList column_widths; + const int old_row_count = m_game_list->rowCount(); + const int old_game_count = m_game_data.count(); + + for (int i = 0; i < m_game_list->columnCount(); i++) { + column_widths.append(m_game_list->columnWidth(i)); + } + + // Sorting resizes hidden columns, so unhide them as a workaround + QList columns_to_hide; + + for (int i = 0; i < m_game_list->columnCount(); i++) { + if (m_game_list->isColumnHidden(i)) { + m_game_list->setColumnHidden(i, false); + columns_to_hide << i; + } + } + + // Sort the list by column and sort order + m_game_list->sortByColumn(m_sort_column, m_col_sort_order); + + // Hide columns again + for (auto i : columns_to_hide) { + m_game_list->setColumnHidden(i, true); + } + + // Don't resize the columns if no game is shown to preserve the header settings + if (!m_game_list->rowCount()) { + for (int i = 0; i < m_game_list->columnCount(); i++) { + m_game_list->setColumnWidth(i, column_widths[i]); + } + + m_game_list->horizontalHeader()->setSectionResizeMode(gui::column_icon, QHeaderView::Fixed); + return; + } + + // Fixate vertical header and row height + m_game_list->verticalHeader()->setMinimumSectionSize(m_icon_size.height()); + m_game_list->verticalHeader()->setMaximumSectionSize(m_icon_size.height()); + m_game_list->resizeRowsToContents(); + + // Resize columns if the game list was empty before + if (!old_row_count && !old_game_count) { + ResizeColumnsToContents(); + } else { + m_game_list->resizeColumnToContents(gui::column_icon); + } + + // Fixate icon column + m_game_list->horizontalHeader()->setSectionResizeMode(gui::column_icon, QHeaderView::Fixed); + + // Shorten the last section to remove horizontal scrollbar if possible + m_game_list->resizeColumnToContents(gui::column_count - 1); +} + +QPixmap GameListFrame::PaintedPixmap(const QPixmap& icon) const { + const qreal device_pixel_ratio = devicePixelRatioF(); + QSize canvas_size(512, 512); + QSize icon_size(icon.size()); + QPoint target_pos; + + if (!icon.isNull()) { + // Let's upscale the original icon to at least fit into the outer rect of the size of PS4's + // ICON0.PNG + if (icon_size.width() < 512 || icon_size.height() < 512) { + icon_size.scale(512, 512, Qt::KeepAspectRatio); + } + + canvas_size = icon_size; + + // Calculate the centered size and position of the icon on our canvas. not needed I believe. + if (icon_size.width() != 512 || icon_size.height() != 512) { + constexpr double target_ratio = 1.0; // aspect ratio 20:11 + + if ((icon_size.width() / static_cast(icon_size.height())) > target_ratio) { + canvas_size.setHeight(std::ceil(icon_size.width() / target_ratio)); + } else { + canvas_size.setWidth(std::ceil(icon_size.height() * target_ratio)); + } + + target_pos.setX(std::max(0, (canvas_size.width() - icon_size.width()) / 2.0)); + target_pos.setY(std::max(0, (canvas_size.height() - icon_size.height()) / 2.0)); + } + } + + // Create a canvas large enough to fit our entire scaled icon + QPixmap canvas(canvas_size * device_pixel_ratio); + canvas.setDevicePixelRatio(device_pixel_ratio); + canvas.fill(m_icon_color); + + // Create a painter for our canvas + QPainter painter(&canvas); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // Draw the icon onto our canvas + if (!icon.isNull()) { + painter.drawPixmap(target_pos.x(), target_pos.y(), icon_size.width(), icon_size.height(), + icon); + } + + // Finish the painting + painter.end(); + + // Scale and return our final image + return canvas.scaled(m_icon_size * device_pixel_ratio, Qt::KeepAspectRatio, + Qt::TransformationMode::SmoothTransformation); +} +void GameListFrame::SetListMode(const bool& is_list) { + m_old_layout_is_list = m_is_list_layout; + m_is_list_layout = is_list; + + m_gui_settings->SetValue(gui::game_list_listMode, is_list); + + Refresh(true); + + m_central_widget->setCurrentWidget(m_is_list_layout ? m_game_list : m_game_grid); +} +void GameListFrame::SetSearchText(const QString& text) { + m_search_text = text; + Refresh(); +} +void GameListFrame::closeEvent(QCloseEvent* event) { + QDockWidget::closeEvent(event); + Q_EMIT GameListFrameClosed(); +} + +void GameListFrame::resizeEvent(QResizeEvent* event) { + if (!m_is_list_layout) { + Refresh(false, m_game_grid->selectedItems().count()); + } + Q_EMIT ResizedWindow(m_game_list->currentItem()); + QDockWidget::resizeEvent(event); +} +void GameListFrame::ResizeIcons(const int& slider_pos) { + m_icon_size_index = slider_pos; + m_icon_size = GuiSettings::SizeFromSlider(slider_pos); + + RepaintIcons(); +} + +void GameListFrame::LoadSettings() { + m_col_sort_order = m_gui_settings->GetValue(gui::game_list_sortAsc).toBool() + ? Qt::AscendingOrder + : Qt::DescendingOrder; + m_sort_column = m_gui_settings->GetValue(gui::game_list_sortCol).toInt(); + + Refresh(true); + + const QByteArray state = m_gui_settings->GetValue(gui::game_list_state).toByteArray(); + if (!m_game_list->horizontalHeader()->restoreState(state) && m_game_list->rowCount()) { + // If no settings exist, resize to contents. + ResizeColumnsToContents(); + } + + for (int col = 0; col < m_columnActs.count(); ++col) { + const bool vis = m_gui_settings->GetGamelistColVisibility(col); + m_columnActs[col]->setChecked(vis); + m_game_list->setColumnHidden(col, !vis); + } + SortGameList(); + FixNarrowColumns(); + + m_game_list->horizontalHeader()->restoreState(m_game_list->horizontalHeader()->saveState()); +} + +void GameListFrame::SaveSettings() { + for (int col = 0; col < m_columnActs.count(); ++col) { + m_gui_settings->SetGamelistColVisibility(col, m_columnActs[col]->isChecked()); + } + m_gui_settings->SetValue(gui::game_list_sortCol, m_sort_column); + m_gui_settings->SetValue(gui::game_list_sortAsc, m_col_sort_order == Qt::AscendingOrder); + m_gui_settings->SetValue(gui::game_list_state, m_game_list->horizontalHeader()->saveState()); +} + +/** + * Returns false if the game should be hidden because it doesn't match search term in toolbar. + */ +bool GameListFrame::SearchMatchesApp(const QString& name, const QString& serial) const { + if (!m_search_text.isEmpty()) { + const QString search_text = m_search_text.toLower(); + return m_titles.value(serial, name).toLower().contains(search_text) || + serial.toLower().contains(search_text); + } + return true; +} diff --git a/src/qt_gui/game_list_frame.h b/src/qt_gui/game_list_frame.h new file mode 100644 index 000000000..826015e5f --- /dev/null +++ b/src/qt_gui/game_list_frame.h @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "custom_dock_widget.h" +#include "game_list_grid.h" +#include "game_list_item.h" +#include "game_list_table.h" +#include "game_list_utils.h" +#include "gui_settings.h" + +class GameListFrame : public CustomDockWidget { + Q_OBJECT +public: + explicit GameListFrame(std::shared_ptr gui_settings, QWidget* parent = nullptr); + ~GameListFrame(); + /** Fix columns with width smaller than the minimal section size */ + void FixNarrowColumns() const; + + /** Loads from settings. Public so that main frame can easily reset these settings if needed. */ + void LoadSettings(); + + /** Saves settings. Public so that main frame can save this when a caching of column widths is + * needed for settings backup */ + void SaveSettings(); + + /** Resizes the columns to their contents and adds a small spacing */ + void ResizeColumnsToContents(int spacing = 20) const; + + /** Refresh the gamelist with/without loading game data from files. Public so that main frame + * can refresh after vfs or install */ + void Refresh(const bool from_drive = false, const bool scroll_after = true); + + /** Repaint Gamelist Icons with new background color */ + void RepaintIcons(const bool& from_settings = false); + + /** Resize Gamelist Icons to size given by slider position */ + void ResizeIcons(const int& slider_pos); + +public Q_SLOTS: + void SetSearchText(const QString& text); + void SetListMode(const bool& is_list); +private Q_SLOTS: + void OnHeaderColumnClicked(int col); + void OnRepaintFinished(); + void OnRefreshFinished(); + void RequestGameMenu(const QPoint& pos); + void SetListBackgroundImage(QTableWidgetItem* item); + void RefreshListBackgroundImage(); + +Q_SIGNALS: + void GameListFrameClosed(); + void RequestIconSizeChange(const int& val); + void ResizedWindow(QTableWidgetItem* item); + +protected: + void closeEvent(QCloseEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + +private: + QPixmap PaintedPixmap(const QPixmap& icon) const; + void SortGameList() const; + std::string CurrentSelectionPath(); + void PopulateGameList(); + void PopulateGameGrid(int maxCols, const QSize& image_size, const QColor& image_color); + bool SearchMatchesApp(const QString& name, const QString& serial) const; + bool IsEntryVisible(const game_info& game); + static game_info GetGameInfoFromItem(const QTableWidgetItem* item); + + // Which widget we are displaying depends on if we are in grid or list mode. + QMainWindow* m_game_dock = nullptr; + QStackedWidget* m_central_widget = nullptr; + + // Game Grid + GameListGrid* m_game_grid = nullptr; + + // Game List + GameListTable* m_game_list = nullptr; + QList m_columnActs; + Qt::SortOrder m_col_sort_order; + int m_sort_column; + QMap m_titles; + + // Game List Utils + GameListUtils m_game_list_utils; + + // List Mode + bool m_is_list_layout = true; + bool m_old_layout_is_list = true; + + // data + std::shared_ptr m_gui_settings; + QList m_game_data; + std::vector m_path_list; + std::vector m_games; + QFutureWatcher m_repaint_watcher; + QFutureWatcher m_refresh_watcher; + + // Search + QString m_search_text; + + // Icon Size + int m_icon_size_index = 0; + + // Icons + QSize m_icon_size; + QColor m_icon_color; + qreal m_margin_factor; + qreal m_text_factor; + + // Background Image + QImage backgroundImage; + + void SetTableItem(GameListTable* game_list, int row, int column, QString itemStr) { + QWidget* widget = new QWidget(); + QVBoxLayout* layout = new QVBoxLayout(); + QLabel* label = new QLabel(itemStr); + QTableWidgetItem* item = new QTableWidgetItem(); + + label->setStyleSheet("color: white; font-size: 15px; font-weight: bold;"); + + // Create shadow effect + QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(); + shadowEffect->setBlurRadius(5); // Set the blur radius of the shadow + shadowEffect->setColor(QColor(0, 0, 0, 160)); // Set the color and opacity of the shadow + shadowEffect->setOffset(2, 2); // Set the offset of the shadow + + label->setGraphicsEffect(shadowEffect); // Apply shadow effect to the QLabel + + layout->addWidget(label); + if (column != 7 && column != 1) + layout->setAlignment(Qt::AlignCenter); + widget->setLayout(layout); + game_list->setItem(row, column, item); + game_list->setCellWidget(row, column, widget); + } +}; diff --git a/src/qt_gui/game_list_grid.cpp b/src/qt_gui/game_list_grid.cpp new file mode 100644 index 000000000..2239126bd --- /dev/null +++ b/src/qt_gui/game_list_grid.cpp @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "game_list_grid.h" +#include "game_list_grid_delegate.h" +#include "game_list_item.h" + +GameListGrid::GameListGrid(const QSize& icon_size, QColor icon_color, const qreal& margin_factor, + const qreal& text_factor, const bool& showText) + : m_icon_size(icon_size), m_icon_color(std::move(icon_color)), m_margin_factor(margin_factor), + m_text_factor(text_factor), m_text_enabled(showText) { + setObjectName("game_grid"); + + QSize item_size; + if (m_text_enabled) { + item_size = + m_icon_size + QSize(m_icon_size.width() * m_margin_factor * 2, + m_icon_size.height() * m_margin_factor * (m_text_factor + 1)); + } else { + item_size = m_icon_size + m_icon_size * m_margin_factor * 2; + } + + grid_item_delegate = new GameListGridDelegate(item_size, m_margin_factor, m_text_factor, this); + setItemDelegate(grid_item_delegate); + setEditTriggers(QAbstractItemView::NoEditTriggers); + setSelectionBehavior(QAbstractItemView::SelectItems); + setSelectionMode(QAbstractItemView::SingleSelection); + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + verticalScrollBar()->setSingleStep(20); + horizontalScrollBar()->setSingleStep(20); + setContextMenuPolicy(Qt::CustomContextMenu); + verticalHeader()->setVisible(false); + horizontalHeader()->setVisible(false); + setShowGrid(false); + QPalette palette; + palette.setColor(QPalette::Base, QColor(230, 230, 230, 80)); + setPalette(palette); + + connect(this, &GameListTable::itemClicked, this, &GameListGrid::SetGridBackgroundImage); + connect(this, &GameListGrid::ResizedWindowGrid, this, &GameListGrid::SetGridBackgroundImage); + connect(this->verticalScrollBar(), &QScrollBar::valueChanged, this, + &GameListGrid::RefreshBackgroundImage); + connect(this->horizontalScrollBar(), &QScrollBar::valueChanged, this, + &GameListGrid::RefreshBackgroundImage); +} + +void GameListGrid::enableText(const bool& enabled) { + m_text_enabled = enabled; +} + +void GameListGrid::setIconSize(const QSize& size) const { + if (m_text_enabled) { + grid_item_delegate->setItemSize( + size + QSize(size.width() * m_margin_factor * 2, + size.height() * m_margin_factor * (m_text_factor + 1))); + } else { + grid_item_delegate->setItemSize(size + size * m_margin_factor * 2); + } +} + +GameListItem* GameListGrid::addItem(const game_info& app, const QString& name, const int& row, + const int& col) { + GameListItem* item = new GameListItem; + item->set_icon_func([this, app, item](int) { + const qreal device_pixel_ratio = devicePixelRatioF(); + + // define size of expanded image, which is raw image size + margins + QSizeF exp_size_f; + if (m_text_enabled) { + exp_size_f = + m_icon_size + QSizeF(m_icon_size.width() * m_margin_factor * 2, + m_icon_size.height() * m_margin_factor * (m_text_factor + 1)); + } else { + exp_size_f = m_icon_size + m_icon_size * m_margin_factor * 2; + } + + // define offset for raw image placement + QPoint offset(m_icon_size.width() * m_margin_factor, + m_icon_size.height() * m_margin_factor); + const QSize exp_size = (exp_size_f * device_pixel_ratio).toSize(); + + // create empty canvas for expanded image + QImage exp_img(exp_size, QImage::Format_ARGB32); + exp_img.setDevicePixelRatio(device_pixel_ratio); + exp_img.fill(Qt::transparent); + + // create background for image + QImage bg_img(app->pxmap.size(), QImage::Format_ARGB32); + bg_img.setDevicePixelRatio(device_pixel_ratio); + bg_img.fill(m_icon_color); + + // place raw image inside expanded image + QPainter painter(&exp_img); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + painter.drawImage(offset, bg_img); + painter.drawPixmap(offset, app->pxmap); + app->pxmap = {}; + painter.end(); + + // create item with expanded image, title and position + item->setData(Qt::ItemDataRole::DecorationRole, QPixmap::fromImage(exp_img)); + }); + if (m_text_enabled) { + item->setData(Qt::ItemDataRole::DisplayRole, name); + } + + setItem(row, col, item); + return item; +} + +qreal GameListGrid::getMarginFactor() const { + return m_margin_factor; +} +void GameListGrid::RefreshBackgroundImage() { + QPixmap blurredPixmap = QPixmap::fromImage(backgroundImage); + QPalette palette; + palette.setBrush(QPalette::Base, QBrush(blurredPixmap.scaled(size(), Qt::IgnoreAspectRatio))); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + this->setPalette(palette); +} +void GameListGrid::SetGridBackgroundImage(QTableWidgetItem* item) { + if (!item) { + // handle case where icon item does not exist + return; + } + QTableWidgetItem* iconItem = this->item(item->row(), item->column()); + + if (!iconItem) { + // handle case where icon item does not exist + return; + } + game_info gameinfo = GetGameInfoFromItem(iconItem); + QString pic1Path = QString::fromStdString(gameinfo->info.pic_path); + QString blurredPic1Path = + qApp->applicationDirPath() + + QString::fromStdString("/game_data/" + gameinfo->info.serial + "/pic1.png"); + + backgroundImage = QImage(blurredPic1Path); + if (backgroundImage.isNull()) { + QImage image(pic1Path); + backgroundImage = m_game_list_utils.BlurImage(image, image.rect(), 18); + + std::filesystem::path img_path = + std::filesystem::path("game_data/") / gameinfo->info.serial; + std::filesystem::create_directories(img_path); + if (!backgroundImage.save(blurredPic1Path, "PNG")) { + // qDebug() << "Error: Unable to save image."; + } + } + QPixmap blurredPixmap = QPixmap::fromImage(backgroundImage); + QPalette palette; + palette.setBrush(QPalette::Base, QBrush(blurredPixmap.scaled(size(), Qt::IgnoreAspectRatio))); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + this->setPalette(palette); +} + +void GameListGrid::resizeEvent(QResizeEvent* event) { + Q_EMIT ResizedWindowGrid(this->currentItem()); +} \ No newline at end of file diff --git a/src/qt_gui/game_list_grid.h b/src/qt_gui/game_list_grid.h new file mode 100644 index 000000000..02a1c6648 --- /dev/null +++ b/src/qt_gui/game_list_grid.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "custom_dock_widget.h" +#include "game_list_table.h" +#include "game_list_utils.h" +#include "gui_settings.h" + +class GameListGridDelegate; + +class GameListGrid : public GameListTable { + Q_OBJECT + + QSize m_icon_size; + QColor m_icon_color; + qreal m_margin_factor; + qreal m_text_factor; + bool m_text_enabled = true; + +Q_SIGNALS: + void ResizedWindowGrid(QTableWidgetItem* item); + +protected: + void resizeEvent(QResizeEvent* event) override; + +public: + explicit GameListGrid(const QSize& icon_size, QColor icon_color, const qreal& margin_factor, + const qreal& text_factor, const bool& showText); + + void enableText(const bool& enabled); + void setIconSize(const QSize& size) const; + GameListItem* addItem(const game_info& app, const QString& name, const int& row, + const int& col); + + [[nodiscard]] qreal getMarginFactor() const; + + game_info GetGameInfoFromItem(const QTableWidgetItem* item) { + if (!item) { + return nullptr; + } + + const QVariant var = item->data(gui::game_role); + if (!var.canConvert()) { + return nullptr; + } + + return var.value(); + } + +private: + void SetGridBackgroundImage(QTableWidgetItem* item); + void RefreshBackgroundImage(); + + GameListGridDelegate* grid_item_delegate; + GameListUtils m_game_list_utils; + + // Background Image + QImage backgroundImage; +}; diff --git a/src/qt_gui/game_list_grid_delegate.cpp b/src/qt_gui/game_list_grid_delegate.cpp new file mode 100644 index 000000000..4b7ffea02 --- /dev/null +++ b/src/qt_gui/game_list_grid_delegate.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "game_list_grid_delegate.h" + +GameListGridDelegate::GameListGridDelegate(const QSize& size, const qreal& margin_factor, + const qreal& text_factor, QObject* parent) + : QStyledItemDelegate(parent), m_size(size), m_margin_factor(margin_factor), + m_text_factor(text_factor) {} + +void GameListGridDelegate::initStyleOption(QStyleOptionViewItem* option, + const QModelIndex& index) const { + Q_UNUSED(index) + + // Remove the focus frame around selected items + option->state &= ~QStyle::State_HasFocus; + + // Call initStyleOption without a model index, since we want to paint the relevant data + // ourselves + QStyledItemDelegate::initStyleOption(option, QModelIndex()); +} + +void GameListGridDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const { + const QRect r = option.rect; + + painter->setRenderHints(QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); + painter->eraseRect(r); + + // Get title and image + const QPixmap image = qvariant_cast(index.data(Qt::DecorationRole)); + const QString title = index.data(Qt::DisplayRole).toString(); + + // Paint from our stylesheet + QStyledItemDelegate::paint(painter, option, index); + + // image + if (image.isNull() == false) { + painter->drawPixmap(option.rect, image); + } + + const int h = r.height() / (1 + m_margin_factor + m_margin_factor * m_text_factor); + const int height = r.height() - h - h * m_margin_factor; + const int top = r.bottom() - height; + + // title + if (option.state & QStyle::State_Selected) { + painter->setPen(QPen(option.palette.color(QPalette::HighlightedText), 1, Qt::SolidLine)); + } else { + painter->setPen(QPen(option.palette.color(QPalette::WindowText), 1, Qt::SolidLine)); + } + + painter->setFont(option.font); + painter->drawText(QRect(r.left(), top, r.width(), height), +Qt::TextWordWrap | +Qt::AlignCenter, + title); +} + +QSize GameListGridDelegate::sizeHint(const QStyleOptionViewItem& option, + const QModelIndex& index) const { + Q_UNUSED(option) + Q_UNUSED(index) + return m_size; +} + +void GameListGridDelegate::setItemSize(const QSize& size) { + m_size = size; +} diff --git a/src/qt_gui/game_list_grid_delegate.h b/src/qt_gui/game_list_grid_delegate.h new file mode 100644 index 000000000..b37e6bc6a --- /dev/null +++ b/src/qt_gui/game_list_grid_delegate.h @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class GameListGridDelegate : public QStyledItemDelegate { +public: + GameListGridDelegate(const QSize& imageSize, const qreal& margin_factor, + const qreal& margin_ratio, QObject* parent = nullptr); + + void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void setItemSize(const QSize& size); + +private: + QSize m_size; + qreal m_margin_factor; + qreal m_text_factor; +}; diff --git a/src/qt_gui/game_list_item.h b/src/qt_gui/game_list_item.h new file mode 100644 index 000000000..7c625ff4e --- /dev/null +++ b/src/qt_gui/game_list_item.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include + +using icon_callback_t = std::function; + +class GameListItem : public QTableWidgetItem { +public: + GameListItem() : QTableWidgetItem() {} + GameListItem(const QString& text, int type = Type) : QTableWidgetItem(text, type) {} + GameListItem(const QIcon& icon, const QString& text, int type = Type) + : QTableWidgetItem(icon, text, type) {} + + ~GameListItem() {} + + void call_icon_func() const { + if (m_icon_callback) { + m_icon_callback(0); + } + } + + void set_icon_func(const icon_callback_t& func) { + m_icon_callback = func; + call_icon_func(); + } + +private: + icon_callback_t m_icon_callback = nullptr; +}; diff --git a/src/qt_gui/game_list_table.cpp b/src/qt_gui/game_list_table.cpp new file mode 100644 index 000000000..30600c7e3 --- /dev/null +++ b/src/qt_gui/game_list_table.cpp @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "game_list_table.h" + +void GameListTable::clear_list() { + clearSelection(); + clearContents(); +} + +void GameListTable::mousePressEvent(QMouseEvent* event) { + if (QTableWidgetItem* item = itemAt(event->pos()); + !item || !item->data(Qt::UserRole).isValid()) { + clearSelection(); + setCurrentItem(nullptr); // Needed for currentItemChanged + } + QTableWidget::mousePressEvent(event); +} \ No newline at end of file diff --git a/src/qt_gui/game_list_table.h b/src/qt_gui/game_list_table.h new file mode 100644 index 000000000..aec2a01a8 --- /dev/null +++ b/src/qt_gui/game_list_table.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "game_info.h" +#include "game_list_item.h" + +struct GuiGameInfo { + GameInfo info{}; + QPixmap icon; + QPixmap pxmap; + GameListItem* item = nullptr; +}; + +typedef std::shared_ptr game_info; +Q_DECLARE_METATYPE(game_info) + +class GameListTable : public QTableWidget { +public: + void clear_list(); + +protected: + void mousePressEvent(QMouseEvent* event) override; +}; \ No newline at end of file diff --git a/src/qt_gui/game_list_utils.h b/src/qt_gui/game_list_utils.h new file mode 100644 index 000000000..5c54ebeb5 --- /dev/null +++ b/src/qt_gui/game_list_utils.h @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +class GameListUtils { +public: + static QString FormatSize(qint64 size) { + static const QStringList suffixes = {"B", "KB", "MB", "GB", "TB"}; + int suffixIndex = 0; + + while (size >= 1024 && suffixIndex < suffixes.size() - 1) { + size /= 1024; + ++suffixIndex; + } + + return QString("%1 %2").arg(size).arg(suffixes[suffixIndex]); + } + + static QString GetFolderSize(const QDir& dir) { + + QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories); + qint64 total = 0; + + while (it.hasNext()) { + // check if entry is file + if (it.fileInfo().isFile()) { + total += it.fileInfo().size(); + } + it.next(); + } + + // if there is a file left "at the end" get it's size + if (it.fileInfo().isFile()) { + total += it.fileInfo().size(); + } + + return FormatSize(total); + } + + QImage BlurImage(const QImage& image, const QRect& rect, int radius) { + int tab[] = {14, 10, 8, 6, 5, 5, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2}; + int alpha = (radius < 1) ? 16 : (radius > 17) ? 1 : tab[radius - 1]; + + QImage result = image.convertToFormat(QImage::Format_ARGB32); + int r1 = rect.top(); + int r2 = rect.bottom(); + int c1 = rect.left(); + int c2 = rect.right(); + + int bpl = result.bytesPerLine(); + int rgba[4]; + unsigned char* p; + + int i1 = 0; + int i2 = 3; + + for (int col = c1; col <= c2; col++) { + p = result.scanLine(r1) + col * 4; + for (int i = i1; i <= i2; i++) + rgba[i] = p[i] << 4; + + p += bpl; + for (int j = r1; j < r2; j++, p += bpl) + for (int i = i1; i <= i2; i++) + p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4; + } + + for (int row = r1; row <= r2; row++) { + p = result.scanLine(row) + c1 * 4; + for (int i = i1; i <= i2; i++) + rgba[i] = p[i] << 4; + + p += 4; + for (int j = c1; j < c2; j++, p += 4) + for (int i = i1; i <= i2; i++) + p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4; + } + + for (int col = c1; col <= c2; col++) { + p = result.scanLine(r2) + col * 4; + for (int i = i1; i <= i2; i++) + rgba[i] = p[i] << 4; + + p -= bpl; + for (int j = r1; j < r2; j++, p -= bpl) + for (int i = i1; i <= i2; i++) + p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4; + } + + for (int row = r1; row <= r2; row++) { + p = result.scanLine(row) + c2 * 4; + for (int i = i1; i <= i2; i++) + rgba[i] = p[i] << 4; + + p -= 4; + for (int j = c1; j < c2; j++, p -= 4) + for (int i = i1; i <= i2; i++) + p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4; + } + + return result; + } +}; diff --git a/src/qt_gui/gui_save.h b/src/qt_gui/gui_save.h new file mode 100644 index 000000000..e2434f752 --- /dev/null +++ b/src/qt_gui/gui_save.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +struct GuiSave { + QString key; + QString name; + QVariant def; + + GuiSave() { + key = ""; + name = ""; + def = QVariant(); + } + + GuiSave(const QString& k, const QString& n, const QVariant& d) { + key = k; + name = n; + def = d; + } + + bool operator==(const GuiSave& rhs) const noexcept { + return key == rhs.key && name == rhs.name && def == rhs.def; + } +}; diff --git a/src/qt_gui/gui_settings.cpp b/src/qt_gui/gui_settings.cpp new file mode 100644 index 000000000..e775f2038 --- /dev/null +++ b/src/qt_gui/gui_settings.cpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "gui_settings.h" + +GuiSettings::GuiSettings(QObject* parent) { + m_settings.reset(new QSettings("shadps4qt.ini", QSettings::Format::IniFormat, + parent)); // TODO make the path configurable +} + +void GuiSettings::SetGamelistColVisibility(int col, bool val) const { + SetValue(GetGuiSaveForColumn(col), val); +} + +bool GuiSettings::GetGamelistColVisibility(int col) const { + return GetValue(GetGuiSaveForColumn(col)).toBool(); +} + +GuiSave GuiSettings::GetGuiSaveForColumn(int col) { + return GuiSave{gui::game_list, + "visibility_" + + gui::get_game_list_column_name(static_cast(col)), + true}; +} +QSize GuiSettings::SizeFromSlider(int pos) { + return gui::game_list_icon_size_min + + (gui::game_list_icon_size_max - gui::game_list_icon_size_min) * + (1.f * pos / gui::game_list_max_slider_pos); +} \ No newline at end of file diff --git a/src/qt_gui/gui_settings.h b/src/qt_gui/gui_settings.h new file mode 100644 index 000000000..9c780ec6c --- /dev/null +++ b/src/qt_gui/gui_settings.h @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "settings.h" + +namespace gui { +enum custom_roles { + game_role = Qt::UserRole + 1337, +}; + +enum game_list_columns { + column_icon, + column_name, + column_serial, + column_firmware, + column_size, + column_version, + column_category, + column_path, + column_count +}; + +inline QString get_game_list_column_name(game_list_columns col) { + switch (col) { + case column_icon: + return "column_icon"; + case column_name: + return "column_name"; + case column_serial: + return "column_serial"; + case column_firmware: + return "column_firmware"; + case column_size: + return "column_size"; + case column_version: + return "column_version"; + case column_category: + return "column_category"; + case column_path: + return "column_path"; + case column_count: + return ""; + } + + throw std::runtime_error("get_game_list_column_name: Invalid column"); +} + +const QSize game_list_icon_size_min = QSize(28, 28); +const QSize game_list_icon_size_small = QSize(56, 56); +const QSize game_list_icon_size_medium = QSize(128, 128); +const QSize game_list_icon_size_max = + QSize(256, 256); // let's do 256, 512 is too big (that's what she said) + +const int game_list_max_slider_pos = 100; + +inline int get_Index(const QSize& current) { + const int size_delta = game_list_icon_size_max.width() - game_list_icon_size_min.width(); + const int current_delta = current.width() - game_list_icon_size_min.width(); + return game_list_max_slider_pos * current_delta / size_delta; +} + +const QString main_window = "main_window"; +const QString game_list = "GameList"; +const QString settings = "Settings"; +const QString themes = "Themes"; + +const QColor game_list_icon_color = QColor(240, 240, 240, 255); + +const GuiSave main_window_gamelist_visible = GuiSave(main_window, "gamelistVisible", true); +const GuiSave main_window_geometry = GuiSave(main_window, "geometry", QByteArray()); +const GuiSave main_window_windowState = GuiSave(main_window, "windowState", QByteArray()); +const GuiSave main_window_mwState = GuiSave(main_window, "mwState", QByteArray()); + +const GuiSave game_list_sortAsc = GuiSave(game_list, "sortAsc", true); +const GuiSave game_list_sortCol = GuiSave(game_list, "sortCol", 1); +const GuiSave game_list_state = GuiSave(game_list, "state", QByteArray()); +const GuiSave game_list_iconSize = + GuiSave(game_list, "iconSize", get_Index(game_list_icon_size_small)); +const GuiSave game_list_iconSizeGrid = + GuiSave(game_list, "iconSizeGrid", get_Index(game_list_icon_size_small)); +const GuiSave game_list_iconColor = GuiSave(game_list, "iconColor", game_list_icon_color); +const GuiSave game_list_listMode = GuiSave(game_list, "listMode", true); +const GuiSave game_list_textFactor = GuiSave(game_list, "textFactor", qreal{2.0}); +const GuiSave game_list_marginFactor = GuiSave(game_list, "marginFactor", qreal{0.09}); +const GuiSave settings_install_dir = GuiSave(settings, "installDirectory", ""); +const GuiSave mw_themes = GuiSave(themes, "Themes", 0); + +} // namespace gui + +class GuiSettings : public Settings { + Q_OBJECT + +public: + explicit GuiSettings(QObject* parent = nullptr); + + bool GetGamelistColVisibility(int col) const; + +public Q_SLOTS: + void SetGamelistColVisibility(int col, bool val) const; + static GuiSave GetGuiSaveForColumn(int col); + static QSize SizeFromSlider(int pos); +}; diff --git a/src/qt_gui/main.cpp b/src/qt_gui/main.cpp new file mode 100644 index 000000000..8a444af1d --- /dev/null +++ b/src/qt_gui/main.cpp @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "qt_gui/game_install_dialog.h" +#include "qt_gui/gui_settings.h" +#include "qt_gui/main_window.h" + +int main(int argc, char* argv[]) { + QApplication a(argc, argv); + auto m_gui_settings = std::make_shared(); + if (m_gui_settings->GetValue(gui::settings_install_dir) == "") { + GameInstallDialog dlg(m_gui_settings); + dlg.exec(); + } + MainWindow* m_main_window = new MainWindow(m_gui_settings, nullptr); + m_main_window->Init(); + + return a.exec(); +} \ No newline at end of file diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp new file mode 100644 index 000000000..522d66ed9 --- /dev/null +++ b/src/qt_gui/main_window.cpp @@ -0,0 +1,364 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include + +#include "common/io_file.h" +#include "core/file_format/pkg.h" +#include "core/loader.h" +#include "game_install_dialog.h" +#include "game_list_frame.h" +#include "gui_settings.h" +#include "main_window.h" + +MainWindow::MainWindow(std::shared_ptr gui_settings, QWidget* parent) + : QMainWindow(parent), ui(new Ui::MainWindow), m_gui_settings(std::move(gui_settings)) { + + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose); +} + +MainWindow::~MainWindow() { + SaveWindowState(); +} + +bool MainWindow::Init() { + // add toolbar widgets + QApplication::setStyle("Fusion"); + ui->toolBar->setObjectName("mw_toolbar"); + ui->sizeSlider->setRange(0, gui::game_list_max_slider_pos); + ui->toolBar->addWidget(ui->sizeSliderContainer); + ui->toolBar->addWidget(ui->mw_searchbar); + + CreateActions(); + CreateDockWindows(); + CreateConnects(); + SetLastUsedTheme(); + + setMinimumSize(350, minimumSizeHint().height()); + setWindowTitle(QString::fromStdString("ShadPS4 v0.0.2")); + + ConfigureGuiFromSettings(); + + show(); + + // Fix possible hidden game list columns. The game list has to be visible already. Use this + // after show() + m_game_list_frame->FixNarrowColumns(); + + return true; +} + +void MainWindow::CreateActions() { + // create action group for icon size + m_icon_size_act_group = new QActionGroup(this); + m_icon_size_act_group->addAction(ui->setIconSizeTinyAct); + m_icon_size_act_group->addAction(ui->setIconSizeSmallAct); + m_icon_size_act_group->addAction(ui->setIconSizeMediumAct); + m_icon_size_act_group->addAction(ui->setIconSizeLargeAct); + + // create action group for list mode + m_list_mode_act_group = new QActionGroup(this); + m_list_mode_act_group->addAction(ui->setlistModeListAct); + m_list_mode_act_group->addAction(ui->setlistModeGridAct); + + // create action group for themes + m_theme_act_group = new QActionGroup(this); + m_theme_act_group->addAction(ui->setThemeLight); + m_theme_act_group->addAction(ui->setThemeDark); + m_theme_act_group->addAction(ui->setThemeGreen); + m_theme_act_group->addAction(ui->setThemeBlue); + m_theme_act_group->addAction(ui->setThemeViolet); +} + +void MainWindow::CreateDockWindows() { + m_main_window = new QMainWindow(); + m_main_window->setContextMenuPolicy(Qt::PreventContextMenu); + + m_game_list_frame = new GameListFrame(m_gui_settings, m_main_window); + m_game_list_frame->setObjectName("gamelist"); + + m_main_window->addDockWidget(Qt::LeftDockWidgetArea, m_game_list_frame); + + m_main_window->setDockNestingEnabled(true); + + setCentralWidget(m_main_window); + + connect(m_game_list_frame, &GameListFrame::GameListFrameClosed, this, [this]() { + if (ui->showGameListAct->isChecked()) { + ui->showGameListAct->setChecked(false); + m_gui_settings->SetValue(gui::main_window_gamelist_visible, false); + } + }); +} +void MainWindow::CreateConnects() { + connect(ui->exitAct, &QAction::triggered, this, &QWidget::close); + + connect(ui->showGameListAct, &QAction::triggered, this, [this](bool checked) { + checked ? m_game_list_frame->show() : m_game_list_frame->hide(); + m_gui_settings->SetValue(gui::main_window_gamelist_visible, checked); + }); + connect(ui->refreshGameListAct, &QAction::triggered, this, + [this] { m_game_list_frame->Refresh(true); }); + + connect(m_icon_size_act_group, &QActionGroup::triggered, this, [this](QAction* act) { + static const int index_small = gui::get_Index(gui::game_list_icon_size_small); + static const int index_medium = gui::get_Index(gui::game_list_icon_size_medium); + + int index; + + if (act == ui->setIconSizeTinyAct) + index = 0; + else if (act == ui->setIconSizeSmallAct) + index = index_small; + else if (act == ui->setIconSizeMediumAct) + index = index_medium; + else + index = gui::game_list_max_slider_pos; + + m_save_slider_pos = true; + ResizeIcons(index); + }); + connect(m_game_list_frame, &GameListFrame::RequestIconSizeChange, this, [this](const int& val) { + const int idx = ui->sizeSlider->value() + val; + m_save_slider_pos = true; + ResizeIcons(idx); + }); + + connect(m_list_mode_act_group, &QActionGroup::triggered, this, [this](QAction* act) { + const bool is_list_act = act == ui->setlistModeListAct; + if (is_list_act == m_is_list_mode) + return; + + const int slider_pos = ui->sizeSlider->sliderPosition(); + ui->sizeSlider->setSliderPosition(m_other_slider_pos); + SetIconSizeActions(m_other_slider_pos); + m_other_slider_pos = slider_pos; + + m_is_list_mode = is_list_act; + m_game_list_frame->SetListMode(m_is_list_mode); + }); + connect(ui->sizeSlider, &QSlider::valueChanged, this, &MainWindow::ResizeIcons); + connect(ui->sizeSlider, &QSlider::sliderReleased, this, [this] { + const int index = ui->sizeSlider->value(); + m_gui_settings->SetValue( + m_is_list_mode ? gui::game_list_iconSize : gui::game_list_iconSizeGrid, index); + SetIconSizeActions(index); + }); + connect(ui->sizeSlider, &QSlider::actionTriggered, this, [this](int action) { + if (action != QAbstractSlider::SliderNoAction && + action != + QAbstractSlider::SliderMove) { // we only want to save on mouseclicks or slider + // release (the other connect handles this) + m_save_slider_pos = true; // actionTriggered happens before the value was changed + } + }); + + connect(ui->mw_searchbar, &QLineEdit::textChanged, m_game_list_frame, + &GameListFrame::SetSearchText); + connect(ui->bootInstallPkgAct, &QAction::triggered, this, [this] { InstallPkg(); }); + connect(ui->gameInstallPathAct, &QAction::triggered, this, [this] { InstallDirectory(); }); + + // Themes + connect(ui->setThemeLight, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Light, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Light)); + }); + connect(ui->setThemeDark, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Dark, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Dark)); + }); + connect(ui->setThemeGreen, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Green, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Green)); + }); + connect(ui->setThemeBlue, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Blue, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Blue)); + }); + connect(ui->setThemeViolet, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Violet, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Violet)); + }); +} + +void MainWindow::SetIconSizeActions(int idx) const { + static const int threshold_tiny = + gui::get_Index((gui::game_list_icon_size_small + gui::game_list_icon_size_min) / 2); + static const int threshold_small = + gui::get_Index((gui::game_list_icon_size_medium + gui::game_list_icon_size_small) / 2); + static const int threshold_medium = + gui::get_Index((gui::game_list_icon_size_max + gui::game_list_icon_size_medium) / 2); + + if (idx < threshold_tiny) + ui->setIconSizeTinyAct->setChecked(true); + else if (idx < threshold_small) + ui->setIconSizeSmallAct->setChecked(true); + else if (idx < threshold_medium) + ui->setIconSizeMediumAct->setChecked(true); + else + ui->setIconSizeLargeAct->setChecked(true); +} +void MainWindow::ResizeIcons(int index) { + if (ui->sizeSlider->value() != index) { + ui->sizeSlider->setSliderPosition(index); + return; // ResizeIcons will be triggered again by setSliderPosition, so return here + } + + if (m_save_slider_pos) { + m_save_slider_pos = false; + m_gui_settings->SetValue( + m_is_list_mode ? gui::game_list_iconSize : gui::game_list_iconSizeGrid, index); + + // this will also fire when we used the actions, but i didn't want to add another boolean + // member + SetIconSizeActions(index); + } + + m_game_list_frame->ResizeIcons(index); +} +void MainWindow::ConfigureGuiFromSettings() { + // Restore GUI state if needed. We need to if they exist. + if (!restoreGeometry(m_gui_settings->GetValue(gui::main_window_geometry).toByteArray())) { + resize(QGuiApplication::primaryScreen()->availableSize() * 0.7); + } + + restoreState(m_gui_settings->GetValue(gui::main_window_windowState).toByteArray()); + m_main_window->restoreState(m_gui_settings->GetValue(gui::main_window_mwState).toByteArray()); + + ui->showGameListAct->setChecked( + m_gui_settings->GetValue(gui::main_window_gamelist_visible).toBool()); + + m_game_list_frame->setVisible(ui->showGameListAct->isChecked()); + + // handle icon size options + m_is_list_mode = m_gui_settings->GetValue(gui::game_list_listMode).toBool(); + if (m_is_list_mode) + ui->setlistModeListAct->setChecked(true); + else + ui->setlistModeGridAct->setChecked(true); + + const int icon_size_index = + m_gui_settings + ->GetValue(m_is_list_mode ? gui::game_list_iconSize : gui::game_list_iconSizeGrid) + .toInt(); + m_other_slider_pos = + m_gui_settings + ->GetValue(!m_is_list_mode ? gui::game_list_iconSize : gui::game_list_iconSizeGrid) + .toInt(); + ui->sizeSlider->setSliderPosition(icon_size_index); + SetIconSizeActions(icon_size_index); + + // Gamelist + m_game_list_frame->LoadSettings(); +} + +void MainWindow::SaveWindowState() const { + // Save gui settings + m_gui_settings->SetValue(gui::main_window_geometry, saveGeometry()); + m_gui_settings->SetValue(gui::main_window_windowState, saveState()); + m_gui_settings->SetValue(gui::main_window_mwState, m_main_window->saveState()); + + // Save column settings + m_game_list_frame->SaveSettings(); +} + +void MainWindow::InstallPkg() { + QStringList fileNames = QFileDialog::getOpenFileNames( + this, tr("Install PKG Files"), QDir::currentPath(), tr("PKG File (*.PKG)")); + int nPkg = fileNames.size(); + int pkgNum = 0; + for (const QString& file : fileNames) { + pkgNum++; + MainWindow::InstallDragDropPkg(file.toStdString(), pkgNum, nPkg); + } +} + +void MainWindow::InstallDragDropPkg(std::string file, int pkgNum, int nPkg) { + + if (Loader::DetectFileType(file) == Loader::FileTypes::Pkg) { + PKG pkg; + pkg.Open(file); + std::string failreason; + const auto extract_path = + std::filesystem::path( + m_gui_settings->GetValue(gui::settings_install_dir).toString().toStdString()) / + pkg.GetTitleID(); + if (!pkg.Extract(file, extract_path, failreason)) { + QMessageBox::critical(this, "PKG ERROR", QString::fromStdString(failreason), + QMessageBox::Ok, 0); + } else { + int nfiles = pkg.GetNumberOfFiles(); + + QList indices; + for (int i = 0; i < nfiles; i++) { + indices.append(i); + } + + QProgressDialog dialog; + dialog.setWindowTitle("PKG Extraction"); + QString extractmsg = QString("Extracting PKG %1/%2").arg(pkgNum).arg(nPkg); + dialog.setLabelText(extractmsg); + + // Create a QFutureWatcher and connect signals and slots. + QFutureWatcher futureWatcher; + QObject::connect(&futureWatcher, SIGNAL(finished()), &dialog, SLOT(reset())); + QObject::connect(&dialog, SIGNAL(canceled()), &futureWatcher, SLOT(cancel())); + QObject::connect(&futureWatcher, SIGNAL(progressRangeChanged(int, int)), &dialog, + SLOT(setRange(int, int))); + QObject::connect(&futureWatcher, SIGNAL(progressValueChanged(int)), &dialog, + SLOT(setValue(int))); + + futureWatcher.setFuture(QtConcurrent::map( + indices, std::bind(&PKG::ExtractFiles, pkg, std::placeholders::_1))); + + // Display the dialog and start the event loop. + dialog.exec(); + futureWatcher.waitForFinished(); + + auto path = m_gui_settings->GetValue(gui::settings_install_dir).toString(); + if (pkgNum == nPkg) { + QMessageBox::information(this, "Extraction Finished", + "Game successfully installed at " + path, QMessageBox::Ok, + 0); + m_game_list_frame->Refresh(true); + } + } + } else { + QMessageBox::critical(this, "PKG ERROR", "File doesn't appear to be a valid PKG file", + QMessageBox::Ok, 0); + } +} + +void MainWindow::InstallDirectory() { + GameInstallDialog dlg(m_gui_settings); + dlg.exec(); +} + +void MainWindow::SetLastUsedTheme() { + + Theme lastTheme = static_cast(m_gui_settings->GetValue(gui::mw_themes).toInt()); + m_window_themes.SetWindowTheme(lastTheme, ui->mw_searchbar); + + switch (lastTheme) { + case Theme::Light: + ui->setThemeLight->setChecked(true); + break; + case Theme::Dark: + ui->setThemeDark->setChecked(true); + break; + case Theme::Green: + ui->setThemeGreen->setChecked(true); + break; + case Theme::Blue: + ui->setThemeBlue->setChecked(true); + break; + case Theme::Violet: + ui->setThemeViolet->setChecked(true); + break; + } +} \ No newline at end of file diff --git a/src/qt_gui/main_window.h b/src/qt_gui/main_window.h new file mode 100644 index 000000000..62474f050 --- /dev/null +++ b/src/qt_gui/main_window.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include "main_window_themes.h" +#include "main_window_ui.h" + +class GuiSettings; +class GameListFrame; + +class MainWindow : public QMainWindow { + Q_OBJECT + + std::unique_ptr ui; + + bool m_is_list_mode = true; + bool m_save_slider_pos = false; + int m_other_slider_pos = 0; + +public: + explicit MainWindow(std::shared_ptr gui_settings, QWidget* parent = nullptr); + ~MainWindow(); + bool Init(); + void InstallPkg(); + void InstallDragDropPkg(std::string file, int pkgNum, int nPkg); + void InstallDirectory(); + +private Q_SLOTS: + void ConfigureGuiFromSettings(); + void SetIconSizeActions(int idx) const; + void ResizeIcons(int index); + void SaveWindowState() const; + +private: + void CreateActions(); + void CreateDockWindows(); + void CreateConnects(); + void SetLastUsedTheme(); + + QActionGroup* m_icon_size_act_group = nullptr; + QActionGroup* m_list_mode_act_group = nullptr; + QActionGroup* m_theme_act_group = nullptr; + + // Dockable widget frames + QMainWindow* m_main_window = nullptr; + GameListFrame* m_game_list_frame = nullptr; + WindowThemes m_window_themes; + + std::shared_ptr m_gui_settings; + +protected: + void dragEnterEvent(QDragEnterEvent* event1) override { + if (event1->mimeData()->hasUrls()) { + event1->acceptProposedAction(); + } + } + + void dropEvent(QDropEvent* event1) override { + const QMimeData* mimeData = event1->mimeData(); + if (mimeData->hasUrls()) { + QList urlList = mimeData->urls(); + int pkgNum = 0; + int nPkg = urlList.size(); + for (const QUrl& url : urlList) { + pkgNum++; + InstallDragDropPkg(url.toLocalFile().toStdString(), pkgNum, nPkg); + } + } + } +}; diff --git a/src/qt_gui/main_window_themes.cpp b/src/qt_gui/main_window_themes.cpp new file mode 100644 index 000000000..858bbb07a --- /dev/null +++ b/src/qt_gui/main_window_themes.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "main_window_themes.h" + +void WindowThemes::SetWindowTheme(Theme theme, QLineEdit* mw_searchbar) { + QPalette themePalette; + + switch (theme) { + case Theme::Light: + mw_searchbar->setStyleSheet("background-color: #ffffff; /* Light gray background */" + "color: #000000; /* Black text */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(240, 240, 240)); // Light gray + themePalette.setColor(QPalette::WindowText, Qt::black); // Black + themePalette.setColor(QPalette::Base, QColor(230, 230, 230, 80)); // Grayish + themePalette.setColor(QPalette::ToolTipBase, Qt::black); // Black + themePalette.setColor(QPalette::ToolTipText, Qt::black); // Black + themePalette.setColor(QPalette::Text, Qt::black); // Black + themePalette.setColor(QPalette::Button, QColor(240, 240, 240)); // Light gray + themePalette.setColor(QPalette::ButtonText, Qt::black); // Black + themePalette.setColor(QPalette::BrightText, Qt::red); // Red + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Blue + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Blue + themePalette.setColor(QPalette::HighlightedText, Qt::white); // White + qApp->setPalette(themePalette); + break; + + case Theme::Dark: + mw_searchbar->setStyleSheet("background-color: #1e1e1e; /* Dark background */" + "color: #ffffff; /* White text */" + "border: 1px solid #ffffff; /* White border */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(53, 53, 53)); + themePalette.setColor(QPalette::WindowText, Qt::white); + themePalette.setColor(QPalette::Base, QColor(25, 25, 25)); + themePalette.setColor(QPalette::AlternateBase, QColor(25, 25, 25)); + themePalette.setColor(QPalette::AlternateBase, QColor(53, 53, 53)); + themePalette.setColor(QPalette::ToolTipBase, Qt::white); + themePalette.setColor(QPalette::ToolTipText, Qt::white); + themePalette.setColor(QPalette::Text, Qt::white); + themePalette.setColor(QPalette::Button, QColor(53, 53, 53)); + themePalette.setColor(QPalette::ButtonText, Qt::white); + themePalette.setColor(QPalette::BrightText, Qt::red); + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); + themePalette.setColor(QPalette::HighlightedText, Qt::black); + qApp->setPalette(themePalette); + break; + + case Theme::Green: + mw_searchbar->setStyleSheet("background-color: #354535; /* Dark green background */" + "color: #ffffff; /* White text */" + "border: 1px solid #ffffff; /* White border */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(53, 69, 53)); // Dark green background + themePalette.setColor(QPalette::WindowText, Qt::white); // White text + themePalette.setColor(QPalette::Base, QColor(25, 40, 25)); // Darker green base + themePalette.setColor(QPalette::AlternateBase, + QColor(53, 69, 53)); // Dark green alternate base + themePalette.setColor(QPalette::ToolTipBase, Qt::white); // White tooltip background + themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text + themePalette.setColor(QPalette::Text, Qt::white); // White text + themePalette.setColor(QPalette::Button, QColor(53, 69, 53)); // Dark green button + themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text + themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight + themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text + + qApp->setPalette(themePalette); + break; + + case Theme::Blue: + mw_searchbar->setStyleSheet("background-color: #283c5a; /* Dark blue background */" + "color: #ffffff; /* White text */" + "border: 1px solid #ffffff; /* White border */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(40, 60, 90)); // Dark blue background + themePalette.setColor(QPalette::WindowText, Qt::white); // White text + themePalette.setColor(QPalette::Base, QColor(20, 40, 60)); // Darker blue base + themePalette.setColor(QPalette::AlternateBase, + QColor(40, 60, 90)); // Dark blue alternate base + themePalette.setColor(QPalette::ToolTipBase, Qt::white); // White tooltip background + themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text + themePalette.setColor(QPalette::Text, Qt::white); // White text + themePalette.setColor(QPalette::Button, QColor(40, 60, 90)); // Dark blue button + themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text + themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight + themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text + + qApp->setPalette(themePalette); + break; + + case Theme::Violet: + mw_searchbar->setStyleSheet("background-color: #643278; /* Violet background */" + "color: #ffffff; /* White text */" + "border: 1px solid #ffffff; /* White border */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(100, 50, 120)); // Violet background + themePalette.setColor(QPalette::WindowText, Qt::white); // White text + themePalette.setColor(QPalette::Base, QColor(80, 30, 90)); // Darker violet base + themePalette.setColor(QPalette::AlternateBase, + QColor(100, 50, 120)); // Violet alternate base + themePalette.setColor(QPalette::ToolTipBase, Qt::white); // White tooltip background + themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text + themePalette.setColor(QPalette::Text, Qt::white); // White text + themePalette.setColor(QPalette::Button, QColor(100, 50, 120)); // Violet button + themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text + themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight + themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text + + qApp->setPalette(themePalette); + break; + } +} \ No newline at end of file diff --git a/src/qt_gui/main_window_themes.h b/src/qt_gui/main_window_themes.h new file mode 100644 index 000000000..8b87fbce5 --- /dev/null +++ b/src/qt_gui/main_window_themes.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include + +enum class Theme : int { + Light, + Dark, + Green, + Blue, + Violet, +}; + +class WindowThemes : public QObject { + Q_OBJECT +public Q_SLOTS: + void SetWindowTheme(Theme theme, QLineEdit* mw_searchbar); +}; diff --git a/src/qt_gui/main_window_ui.h b/src/qt_gui/main_window_ui.h new file mode 100644 index 000000000..c467911d4 --- /dev/null +++ b/src/qt_gui/main_window_ui.h @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +/******************************************************************************** +** Form generated from reading UI file 'main_window.ui' +** +** Created by: Qt User Interface Compiler version 6.6.1 +** +** WARNING! All changes made in this file will be lost when recompiling UI file! +********************************************************************************/ + +#ifndef MAIN_WINDOW_UI_H +#define MAIN_WINDOW_UI_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class Ui_MainWindow { +public: + QAction* bootInstallPkgAct; + QAction* exitAct; + QAction* showGameListAct; + QAction* refreshGameListAct; + QAction* setIconSizeTinyAct; + QAction* setIconSizeSmallAct; + QAction* setIconSizeMediumAct; + QAction* setIconSizeLargeAct; + QAction* setlistModeListAct; + QAction* setlistModeGridAct; + QAction* gameInstallPathAct; + QAction* setThemeLight; + QAction* setThemeDark; + QAction* setThemeGreen; + QAction* setThemeBlue; + QAction* setThemeViolet; + QWidget* centralWidget; + QLineEdit* mw_searchbar; + + QWidget* sizeSliderContainer; + QHBoxLayout* sizeSliderContainer_layout; + QSlider* sizeSlider; + QMenuBar* menuBar; + QMenu* menuFile; + QMenu* menuView; + QMenu* menuGame_List_Icons; + QMenu* menuGame_List_Mode; + QMenu* menuSettings; + QMenu* menuThemes; + QToolBar* toolBar; + + void setupUi(QMainWindow* MainWindow) { + if (MainWindow->objectName().isEmpty()) + MainWindow->setObjectName("MainWindow"); + MainWindow->resize(1058, 580); + QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + sizePolicy.setHorizontalStretch(0); + sizePolicy.setVerticalStretch(0); + sizePolicy.setHeightForWidth(MainWindow->sizePolicy().hasHeightForWidth()); + MainWindow->setSizePolicy(sizePolicy); + MainWindow->setMinimumSize(QSize(4, 0)); + MainWindow->setAutoFillBackground(false); + MainWindow->setAnimated(true); + MainWindow->setDockNestingEnabled(true); + MainWindow->setDockOptions(QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks | + QMainWindow::AnimatedDocks | QMainWindow::GroupedDragging); + bootInstallPkgAct = new QAction(MainWindow); + bootInstallPkgAct->setObjectName("bootInstallPkgAct"); + exitAct = new QAction(MainWindow); + exitAct->setObjectName("exitAct"); + showGameListAct = new QAction(MainWindow); + showGameListAct->setObjectName("showGameListAct"); + showGameListAct->setCheckable(true); + refreshGameListAct = new QAction(MainWindow); + refreshGameListAct->setObjectName("refreshGameListAct"); + setIconSizeTinyAct = new QAction(MainWindow); + setIconSizeTinyAct->setObjectName("setIconSizeTinyAct"); + setIconSizeTinyAct->setCheckable(true); + setIconSizeSmallAct = new QAction(MainWindow); + setIconSizeSmallAct->setObjectName("setIconSizeSmallAct"); + setIconSizeSmallAct->setCheckable(true); + setIconSizeSmallAct->setChecked(true); + setIconSizeMediumAct = new QAction(MainWindow); + setIconSizeMediumAct->setObjectName("setIconSizeMediumAct"); + setIconSizeMediumAct->setCheckable(true); + setIconSizeLargeAct = new QAction(MainWindow); + setIconSizeLargeAct->setObjectName("setIconSizeLargeAct"); + setIconSizeLargeAct->setCheckable(true); + setlistModeListAct = new QAction(MainWindow); + setlistModeListAct->setObjectName("setlistModeListAct"); + setlistModeListAct->setCheckable(true); + setlistModeListAct->setChecked(true); + setlistModeGridAct = new QAction(MainWindow); + setlistModeGridAct->setObjectName("setlistModeGridAct"); + setlistModeGridAct->setCheckable(true); + gameInstallPathAct = new QAction(MainWindow); + gameInstallPathAct->setObjectName("gameInstallPathAct"); + setThemeLight = new QAction(MainWindow); + setThemeLight->setObjectName("setThemeLight"); + setThemeLight->setCheckable(true); + setThemeLight->setChecked(true); + setThemeDark = new QAction(MainWindow); + setThemeDark->setObjectName("setThemeDark"); + setThemeDark->setCheckable(true); + setThemeGreen = new QAction(MainWindow); + setThemeGreen->setObjectName("setThemeGreen"); + setThemeGreen->setCheckable(true); + setThemeBlue = new QAction(MainWindow); + setThemeBlue->setObjectName("setThemeBlue"); + setThemeBlue->setCheckable(true); + setThemeViolet = new QAction(MainWindow); + setThemeViolet->setObjectName("setThemeViolet"); + setThemeViolet->setCheckable(true); + centralWidget = new QWidget(MainWindow); + centralWidget->setObjectName("centralWidget"); + sizePolicy.setHeightForWidth(centralWidget->sizePolicy().hasHeightForWidth()); + centralWidget->setSizePolicy(sizePolicy); + mw_searchbar = new QLineEdit(centralWidget); + mw_searchbar->setObjectName("mw_searchbar"); + mw_searchbar->setGeometry(QRect(480, 10, 150, 31)); + sizePolicy.setHeightForWidth(mw_searchbar->sizePolicy().hasHeightForWidth()); + mw_searchbar->setSizePolicy(sizePolicy); + mw_searchbar->setMaximumWidth(250); + QFont font; + font.setPointSize(10); + font.setBold(false); + mw_searchbar->setFont(font); + mw_searchbar->setFocusPolicy(Qt::ClickFocus); + mw_searchbar->setFrame(false); + mw_searchbar->setClearButtonEnabled(false); + + sizeSliderContainer = new QWidget(centralWidget); + sizeSliderContainer->setObjectName("sizeSliderContainer"); + sizeSliderContainer->setGeometry(QRect(280, 10, 181, 31)); + QSizePolicy sizePolicy1(QSizePolicy::Fixed, QSizePolicy::Expanding); + sizePolicy1.setHorizontalStretch(0); + sizePolicy1.setVerticalStretch(0); + sizePolicy1.setHeightForWidth(sizeSliderContainer->sizePolicy().hasHeightForWidth()); + sizeSliderContainer->setSizePolicy(sizePolicy1); + sizeSliderContainer_layout = new QHBoxLayout(sizeSliderContainer); + sizeSliderContainer_layout->setSpacing(0); + sizeSliderContainer_layout->setContentsMargins(11, 11, 11, 11); + sizeSliderContainer_layout->setObjectName("sizeSliderContainer_layout"); + sizeSliderContainer_layout->setContentsMargins(14, 0, 14, 0); + sizeSlider = new QSlider(sizeSliderContainer); + sizeSlider->setObjectName("sizeSlider"); + QSizePolicy sizePolicy2(QSizePolicy::Expanding, QSizePolicy::Preferred); + sizePolicy2.setHorizontalStretch(0); + sizePolicy2.setVerticalStretch(0); + sizePolicy2.setHeightForWidth(sizeSlider->sizePolicy().hasHeightForWidth()); + sizeSlider->setSizePolicy(sizePolicy2); + sizeSlider->setFocusPolicy(Qt::ClickFocus); + sizeSlider->setAutoFillBackground(false); + sizeSlider->setOrientation(Qt::Horizontal); + sizeSlider->setTickPosition(QSlider::NoTicks); + + sizeSliderContainer_layout->addWidget(sizeSlider); + + MainWindow->setCentralWidget(centralWidget); + menuBar = new QMenuBar(MainWindow); + menuBar->setObjectName("menuBar"); + menuBar->setGeometry(QRect(0, 0, 1058, 22)); + menuBar->setContextMenuPolicy(Qt::PreventContextMenu); + menuFile = new QMenu(menuBar); + menuFile->setObjectName("menuFile"); + menuView = new QMenu(menuBar); + menuView->setObjectName("menuView"); + menuGame_List_Icons = new QMenu(menuView); + menuGame_List_Icons->setObjectName("menuGame_List_Icons"); + menuGame_List_Mode = new QMenu(menuView); + menuGame_List_Mode->setObjectName("menuGame_List_Mode"); + menuSettings = new QMenu(menuBar); + menuSettings->setObjectName("menuSettings"); + menuThemes = new QMenu(menuView); + menuThemes->setObjectName("menuThemes"); + MainWindow->setMenuBar(menuBar); + toolBar = new QToolBar(MainWindow); + toolBar->setObjectName("toolBar"); + MainWindow->addToolBar(Qt::TopToolBarArea, toolBar); + + menuBar->addAction(menuFile->menuAction()); + menuBar->addAction(menuView->menuAction()); + menuBar->addAction(menuSettings->menuAction()); + menuFile->addAction(bootInstallPkgAct); + menuFile->addSeparator(); + menuFile->addAction(exitAct); + menuView->addAction(showGameListAct); + menuView->addSeparator(); + menuView->addAction(refreshGameListAct); + menuView->addAction(menuGame_List_Mode->menuAction()); + menuView->addAction(menuGame_List_Icons->menuAction()); + menuView->addAction(menuThemes->menuAction()); + menuThemes->addAction(setThemeLight); + menuThemes->addAction(setThemeDark); + menuThemes->addAction(setThemeGreen); + menuThemes->addAction(setThemeBlue); + menuThemes->addAction(setThemeViolet); + menuGame_List_Icons->addAction(setIconSizeTinyAct); + menuGame_List_Icons->addAction(setIconSizeSmallAct); + menuGame_List_Icons->addAction(setIconSizeMediumAct); + menuGame_List_Icons->addAction(setIconSizeLargeAct); + menuGame_List_Mode->addAction(setlistModeListAct); + menuGame_List_Mode->addAction(setlistModeGridAct); + menuSettings->addAction(gameInstallPathAct); + + retranslateUi(MainWindow); + + QMetaObject::connectSlotsByName(MainWindow); + } // setupUi + + void retranslateUi(QMainWindow* MainWindow) { + MainWindow->setWindowTitle(QCoreApplication::translate("MainWindow", "Shadps4", nullptr)); + bootInstallPkgAct->setText( + QCoreApplication::translate("MainWindow", "Install Packages (PKG)", nullptr)); +#if QT_CONFIG(tooltip) + bootInstallPkgAct->setToolTip(QCoreApplication::translate( + "MainWindow", "Install application from a .pkg file", nullptr)); +#endif // QT_CONFIG(tooltip) + exitAct->setText(QCoreApplication::translate("MainWindow", "Exit", nullptr)); +#if QT_CONFIG(tooltip) + exitAct->setToolTip(QCoreApplication::translate("MainWindow", "Exit Shadps4", nullptr)); +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(statustip) + exitAct->setStatusTip( + QCoreApplication::translate("MainWindow", "Exit the application.", nullptr)); +#endif // QT_CONFIG(statustip) + showGameListAct->setText( + QCoreApplication::translate("MainWindow", "Show Game List", nullptr)); + refreshGameListAct->setText( + QCoreApplication::translate("MainWindow", "Game List Refresh", nullptr)); + setIconSizeTinyAct->setText(QCoreApplication::translate("MainWindow", "Tiny", nullptr)); + setIconSizeSmallAct->setText(QCoreApplication::translate("MainWindow", "Small", nullptr)); + setIconSizeMediumAct->setText(QCoreApplication::translate("MainWindow", "Medium", nullptr)); + setIconSizeLargeAct->setText(QCoreApplication::translate("MainWindow", "Large", nullptr)); + setlistModeListAct->setText( + QCoreApplication::translate("MainWindow", "List View", nullptr)); + setlistModeGridAct->setText( + QCoreApplication::translate("MainWindow", "Grid View", nullptr)); + gameInstallPathAct->setText( + QCoreApplication::translate("MainWindow", "Game Install Directory", nullptr)); + mw_searchbar->setPlaceholderText( + QCoreApplication::translate("MainWindow", "Search...", nullptr)); + // darkModeSwitch->setText( + // QCoreApplication::translate("MainWindow", "Game", nullptr)); + menuFile->setTitle(QCoreApplication::translate("MainWindow", "File", nullptr)); + menuView->setTitle(QCoreApplication::translate("MainWindow", "View", nullptr)); + menuGame_List_Icons->setTitle( + QCoreApplication::translate("MainWindow", "Game List Icons", nullptr)); + menuGame_List_Mode->setTitle( + QCoreApplication::translate("MainWindow", "Game List Mode", nullptr)); + menuSettings->setTitle(QCoreApplication::translate("MainWindow", "Settings", nullptr)); + menuThemes->setTitle(QCoreApplication::translate("MainWindow", "Themes", nullptr)); + setThemeLight->setText(QCoreApplication::translate("MainWindow", "Light", nullptr)); + setThemeDark->setText(QCoreApplication::translate("MainWindow", "Dark", nullptr)); + setThemeGreen->setText(QCoreApplication::translate("MainWindow", "Green", nullptr)); + setThemeBlue->setText(QCoreApplication::translate("MainWindow", "Blue", nullptr)); + setThemeViolet->setText(QCoreApplication::translate("MainWindow", "Violet", nullptr)); + toolBar->setWindowTitle(QCoreApplication::translate("MainWindow", "toolBar", nullptr)); + } // retranslateUi +}; + +namespace Ui { +class MainWindow : public Ui_MainWindow {}; +} // namespace Ui + +QT_END_NAMESPACE + +#endif // MAIN_WINDOW_UI_H diff --git a/src/qt_gui/qt_utils.h b/src/qt_gui/qt_utils.h new file mode 100644 index 000000000..3964923e9 --- /dev/null +++ b/src/qt_gui/qt_utils.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace gui { +namespace utils { +template +void stop_future_watcher(QFutureWatcher& watcher, bool cancel) { + if (watcher.isStarted() || watcher.isRunning()) { + if (cancel) { + watcher.cancel(); + } + watcher.waitForFinished(); + } +} +} // namespace utils +} // namespace gui diff --git a/src/qt_gui/settings.cpp b/src/qt_gui/settings.cpp new file mode 100644 index 000000000..b428bcdab --- /dev/null +++ b/src/qt_gui/settings.cpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "settings.h" + +Settings::Settings(QObject* parent) : QObject(parent), m_settings_dir(ComputeSettingsDir()) {} + +Settings::~Settings() { + if (m_settings) { + m_settings->sync(); + } +} + +QString Settings::GetSettingsDir() const { + return m_settings_dir.absolutePath(); +} + +QString Settings::ComputeSettingsDir() { + return ""; // TODO currently we configure same dir , make it configurable +} + +void Settings::RemoveValue(const QString& key, const QString& name) const { + if (m_settings) { + m_settings->beginGroup(key); + m_settings->remove(name); + m_settings->endGroup(); + } +} + +void Settings::RemoveValue(const GuiSave& entry) const { + RemoveValue(entry.key, entry.name); +} + +QVariant Settings::GetValue(const QString& key, const QString& name, const QVariant& def) const { + return m_settings ? m_settings->value(key + "/" + name, def) : def; +} + +QVariant Settings::GetValue(const GuiSave& entry) const { + return GetValue(entry.key, entry.name, entry.def); +} + +QVariant Settings::List2Var(const q_pair_list& list) { + QByteArray ba; + QDataStream stream(&ba, QIODevice::WriteOnly); + stream << list; + return QVariant(ba); +} + +q_pair_list Settings::Var2List(const QVariant& var) { + q_pair_list list; + QByteArray ba = var.toByteArray(); + QDataStream stream(&ba, QIODevice::ReadOnly); + stream >> list; + return list; +} + +void Settings::SetValue(const GuiSave& entry, const QVariant& value) const { + if (m_settings) { + m_settings->beginGroup(entry.key); + m_settings->setValue(entry.name, value); + m_settings->endGroup(); + } +} + +void Settings::SetValue(const QString& key, const QVariant& value) const { + if (m_settings) { + m_settings->setValue(key, value); + } +} + +void Settings::SetValue(const QString& key, const QString& name, const QVariant& value) const { + if (m_settings) { + m_settings->beginGroup(key); + m_settings->setValue(name, value); + m_settings->endGroup(); + } +} diff --git a/src/qt_gui/settings.h b/src/qt_gui/settings.h new file mode 100644 index 000000000..1e6d1a651 --- /dev/null +++ b/src/qt_gui/settings.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include + +#include "gui_save.h" + +typedef QPair q_string_pair; +typedef QPair q_size_pair; +typedef QList q_pair_list; +typedef QList q_size_list; + +// Parent Class for GUI settings +class Settings : public QObject { + Q_OBJECT + +public: + explicit Settings(QObject* parent = nullptr); + ~Settings(); + + QString GetSettingsDir() const; + + QVariant GetValue(const QString& key, const QString& name, const QVariant& def) const; + QVariant GetValue(const GuiSave& entry) const; + static QVariant List2Var(const q_pair_list& list); + static q_pair_list Var2List(const QVariant& var); + +public Q_SLOTS: + /** Remove entry */ + void RemoveValue(const QString& key, const QString& name) const; + void RemoveValue(const GuiSave& entry) const; + + /** Write value to entry */ + void SetValue(const GuiSave& entry, const QVariant& value) const; + void SetValue(const QString& key, const QVariant& value) const; + void SetValue(const QString& key, const QString& name, const QVariant& value) const; + +protected: + static QString ComputeSettingsDir(); + + std::unique_ptr m_settings; + QDir m_settings_dir; +}; \ No newline at end of file diff --git a/src/shadps4.rc b/src/shadps4.rc new file mode 100644 index 000000000..8c984f260 --- /dev/null +++ b/src/shadps4.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "images/shadps4.ico" \ No newline at end of file diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index 55a23c110..2c9f843fb 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -12,8 +12,10 @@ add_subdirectory(fmt EXCLUDE_FROM_ALL) # MagicEnum add_subdirectory(magic_enum EXCLUDE_FROM_ALL) +if(NOT ENABLE_QT_GUI) # SDL3 add_subdirectory(SDL EXCLUDE_FROM_ALL) +endif() # Toml11 add_subdirectory(toml11 EXCLUDE_FROM_ALL)