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 00000000..ecf6675a Binary files /dev/null and b/src/images/shadps4.ico differ 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)