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 00000000..e1678b0d --- /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 72efea21..095acb78 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 00000000..67e8f1be --- /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 00000000..76cecd8e --- /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 5ead99f7..9eaf5781 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 9810ff92..c46eaa17 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 00000000..242278fc --- /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 0dcd7cb4..406c90cb 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 00000000..4b0b70cd --- /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 11fafbec..59cfcf7b 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 00000000..d45b5651 --- /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 00000000..11edef84 --- /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 00000000..5b8a8862 --- /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 00000000..a79c3674 --- /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 00000000..b1aaa798 --- /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 00000000..4d8aca58 --- /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 00000000..464f0b99 --- /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 00000000..6b010e3a --- /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 00000000..fb808697 --- /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 00000000..31978630 --- /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 00000000..b12821c1 --- /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 00000000..2f4d0651 --- /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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

(K5xa?nf(NsHjXsjP?q$x84M; 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 B$OHfY 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 00000000..9d40bb2f --- /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 00000000..321f22dc --- /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 00000000..e2c497f1 --- /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 00000000..fb29948a --- /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 00000000..1fa6880b --- /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 00000000..b75aaaf6 --- /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 00000000..e9bd92b0 --- /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 00000000..826015e5 --- /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 00000000..2239126b --- /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 00000000..02a1c664 --- /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 00000000..4b7ffea0 --- /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 00000000..b37e6bc6 --- /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 00000000..7c625ff4 --- /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 00000000..30600c7e --- /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 00000000..aec2a01a --- /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 00000000..5c54ebeb --- /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 00000000..e2434f75 --- /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 00000000..e775f203 --- /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 00000000..9c780ec6 --- /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 00000000..8a444af1 --- /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 00000000..522d66ed --- /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 00000000..62474f05 --- /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 00000000..858bbb07 --- /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 00000000..8b87fbce --- /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 00000000..c467911d --- /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 00000000..3964923e --- /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 00000000..b428bcda --- /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 00000000..1e6d1a65 --- /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 00000000..8c984f26 --- /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 55a23c11..2c9f843f 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)