diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c60596..853ef90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required (VERSION 3.20) include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) set(SWA_THIRDPARTY_ROOT ${CMAKE_SOURCE_DIR}/thirdparty) +set(SWA_TOOLS_ROOT ${CMAKE_SOURCE_DIR}/tools) set(CMAKE_CXX_STANDARD 23) set(BUILD_SHARED_LIBS OFF) @@ -14,9 +15,9 @@ endif() set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") add_subdirectory(${SWA_THIRDPARTY_ROOT}) -project("UnleashedRecomp-ALL") +add_subdirectory(${SWA_TOOLS_ROOT}) -include("thirdparty/PowerRecomp/cmake/bin2h.cmake") +project("UnleashedRecomp-ALL") # Include sub-projects. add_subdirectory("UnleashedRecompLib") diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 72975fd..cb8ec0f 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -3,6 +3,36 @@ set(TARGET_NAME "SWA") option(SWA_XAUDIO2 "Use XAudio2 for audio playback" OFF) +function(BIN2C) + cmake_parse_arguments(BIN2C_ARGS "" "TARGET_OBJ;SOURCE_FILE;DEST_FILE;ARRAY_NAME;COMPRESSION_TYPE" "" ${ARGN}) + + if(NOT BIN2C_ARGS_TARGET_OBJ) + message(FATAL_ERROR "TARGET_OBJ not specified.") + endif() + + if(NOT BIN2C_ARGS_SOURCE_FILE) + message(FATAL_ERROR "SOURCE_FILE not specified.") + endif() + + if(NOT BIN2C_ARGS_DEST_FILE) + set(BIN2C_ARGS_DEST_FILE "${BIN2C_ARGS_SOURCE_FILE}") + endif() + + if(NOT BIN2C_ARGS_COMPRESSION_TYPE) + set(BIN2C_ARGS_COMPRESSION_TYPE "none") + endif() + + add_custom_command(OUTPUT "${BIN2C_ARGS_DEST_FILE}.c" + COMMAND file_to_c "${BIN2C_ARGS_SOURCE_FILE}" "${BIN2C_ARGS_ARRAY_NAME}" "${BIN2C_ARGS_COMPRESSION_TYPE}" "${BIN2C_ARGS_DEST_FILE}.c" "${BIN2C_ARGS_DEST_FILE}.h" + DEPENDS "${BIN2C_ARGS_SOURCE_FILE}" file_to_c + BYPRODUCTS "${BIN2C_ARGS_DEST_FILE}.h" + COMMENT "Generating binary header for ${BIN2C_ARGS_SOURCE_FILE}..." + ) + + set_source_files_properties(${BIN2C_ARGS_DEST_FILE}.c PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + target_sources(${BIN2C_ARGS_TARGET_OBJ} PRIVATE ${BIN2C_ARGS_DEST_FILE}.c) +endfunction() + add_compile_options( /fp:strict -march=sandybridge @@ -23,19 +53,10 @@ add_compile_definitions( _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR # Microsoft wtf? _CRT_SECURE_NO_WARNINGS) -# Generate icon bitmap header for SDL surface. -BIN2H(SOURCE_FILE "../UnleashedRecompResources/images/game_icon.bmp" HEADER_FILE "res/icon.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_icon") -BIN2H(SOURCE_FILE "../UnleashedRecompResources/images/game_icon_night.bmp" HEADER_FILE "res/icon_night.h" ARRAY_TYPE "unsigned char" VARIABLE_NAME "g_iconNight") - set(SWA_PRECOMPILED_HEADERS "stdafx.h" ) -set(SWA_CFG_CXX_SOURCES - "cfg/config.cpp" - "cfg/config_detail.cpp" -) - set(SWA_KERNEL_CXX_SOURCES "kernel/imports.cpp" "kernel/xdm.cpp" @@ -53,6 +74,7 @@ set(SWA_CPU_CXX_SOURCES set(SWA_GPU_CXX_SOURCES "gpu/video.cpp" + "gpu/imgui_common.cpp" "gpu/imgui_snapshot.cpp" "gpu/rhi/plume_d3d12.cpp" "gpu/rhi/plume_vulkan.cpp" @@ -74,7 +96,11 @@ set(SWA_HID_CXX_SOURCES ) set(SWA_PATCHES_CXX_SOURCES + "patches/ui/CHudPause_patches.cpp" + "patches/ui/CTitleStateMenu_patches.cpp" "patches/ui/frontend_listener.cpp" + "patches/audio_patches.cpp" + "patches/camera_patches.cpp" "patches/fps_patches.cpp" "patches/misc_patches.cpp" "patches/object_patches.cpp" @@ -84,6 +110,13 @@ set(SWA_PATCHES_CXX_SOURCES ) set(SWA_UI_CXX_SOURCES + "ui/achievement_menu.cpp" + "ui/achievement_overlay.cpp" + "ui/installer_wizard.cpp" + "ui/button_guide.cpp" + "ui/message_window.cpp" + "ui/options_menu.cpp" + "ui/sdl_listener.cpp" "ui/window.cpp" ) @@ -113,13 +146,19 @@ set_source_files_properties(${LIBMSPACK_C_SOURCES} PROPERTIES SKIP_PRECOMPILE_HE set(SMOLV_SOURCE_DIR "${SWA_THIRDPARTY_ROOT}/ShaderRecomp/thirdparty/smol-v/source") +set(SWA_USER_CXX_SOURCES + "user/achievement_data.cpp" + "user/config.cpp" + "user/config_detail.cpp" +) + set(SWA_CXX_SOURCES "app.cpp" + "exports.cpp" "main.cpp" "misc_impl.cpp" "stdafx.cpp" - - ${SWA_CFG_CXX_SOURCES} + ${SWA_KERNEL_CXX_SOURCES} ${SWA_CPU_CXX_SOURCES} ${SWA_GPU_CXX_SOURCES} @@ -130,6 +169,7 @@ set(SWA_CXX_SOURCES ${SWA_INSTALL_CXX_SOURCES} ${LIBMSPACK_C_SOURCES} "${SMOLV_SOURCE_DIR}/smolv.cpp" + ${SWA_USER_CXX_SOURCES} ) if (WIN32) @@ -161,6 +201,7 @@ find_package(unofficial-concurrentqueue REQUIRED) find_package(imgui CONFIG REQUIRED) find_package(magic_enum CONFIG REQUIRED) find_package(unofficial-tiny-aes-c CONFIG REQUIRED) +find_package(nfd CONFIG REQUIRED) find_path(MINIAUDIO_INCLUDE_DIRS "miniaudio.h") file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12) @@ -199,6 +240,7 @@ target_link_libraries(UnleashedRecomp PRIVATE imgui::imgui magic_enum::magic_enum unofficial::tiny-aes-c::tiny-aes-c + nfd::nfd ) target_include_directories(UnleashedRecomp PRIVATE @@ -276,3 +318,29 @@ generate_aggregate_header( "${CMAKE_CURRENT_SOURCE_DIR}/api" "${CMAKE_CURRENT_SOURCE_DIR}/api/SWA.h" ) + +set(RESOURCES_SOURCE_PATH "${PROJECT_SOURCE_DIR}/../UnleashedRecompResources") +set(RESOURCES_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/res") + +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.bin" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.bin" ARRAY_NAME "g_im_font_atlas" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/font/im_font_atlas.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/font/im_font_atlas.dds" ARRAY_NAME "g_im_font_atlas_texture" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/achievements_menu/trophy.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/achievements_menu/trophy.dds" ARRAY_NAME "g_trophy" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/general_window.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/general_window.dds" ARRAY_NAME "g_general_window" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/left_mouse_button.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/left_mouse_button.dds" ARRAY_NAME "g_left_mouse_button" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/mat_comon_x360_001.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/mat_comon_x360_001.dds" ARRAY_NAME "g_mat_comon_x360_001" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/select_fade.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/select_fade.dds" ARRAY_NAME "g_select_fade" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/select_fill.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/select_fill.dds" ARRAY_NAME "g_select_fill" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/start_back.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/start_back.dds" ARRAY_NAME "g_start_back" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon.bmp" ARRAY_NAME "g_game_icon") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon_night.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon_night.bmp" ARRAY_NAME "g_game_icon_night") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/arrow_circle.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/arrow_circle.dds" ARRAY_NAME "g_arrow_circle" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_001.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_001.dds" ARRAY_NAME "g_install_001" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_002.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_002.dds" ARRAY_NAME "g_install_002" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_003.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_003.dds" ARRAY_NAME "g_install_003" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_004.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_004.dds" ARRAY_NAME "g_install_004" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_005.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_005.dds" ARRAY_NAME "g_install_005" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_006.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_006.dds" ARRAY_NAME "g_install_006" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_007.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_007.dds" ARRAY_NAME "g_install_007" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/install_008.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/install_008.dds" ARRAY_NAME "g_install_008" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/miles_electric_icon.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/miles_electric_icon.dds" ARRAY_NAME "g_miles_electric_icon" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/installer/pulse_install.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/installer/pulse_install.dds" ARRAY_NAME "g_pulse_install" COMPRESSION_TYPE "zstd") diff --git a/UnleashedRecomp/api/Hedgehog/Base/Type/hhSharedString.inl b/UnleashedRecomp/api/Hedgehog/Base/Type/hhSharedString.inl index 6701358..1e971f1 100644 --- a/UnleashedRecomp/api/Hedgehog/Base/Type/hhSharedString.inl +++ b/UnleashedRecomp/api/Hedgehog/Base/Type/hhSharedString.inl @@ -212,11 +212,6 @@ namespace Hedgehog::Base return find(str.c_str(), pos); } - inline size_t CSharedString::rfind(const CSharedString& str, size_t pos) const - { - return rfind(str.c_str(), pos); - } - inline size_t CSharedString::find_first_of(const CSharedString& str, size_t pos) const { return find_first_of(str.c_str(), pos); diff --git a/UnleashedRecomp/api/SWA.h b/UnleashedRecomp/api/SWA.h index f72f0e1..bc40f81 100644 --- a/UnleashedRecomp/api/SWA.h +++ b/UnleashedRecomp/api/SWA.h @@ -48,6 +48,8 @@ #include "Hedgehog/Universe/Engine/hhUpdateInfo.h" #include "Hedgehog/Universe/Engine/hhUpdateUnit.h" #include "Hedgehog/Universe/Thread/hhParallelJob.h" +#include "SWA/Achievement/AchievementManager.h" +#include "SWA/Achievement/AchievementTest.h" #include "SWA/CSD/CsdDatabaseWrapper.h" #include "SWA/CSD/CsdProject.h" #include "SWA/CSD/CsdTexListMirage.h" @@ -56,6 +58,7 @@ #include "SWA/HUD/GeneralWindow/GeneralWindow.h" #include "SWA/HUD/Loading/Loading.h" #include "SWA/HUD/Pause/HudPause.h" +#include "SWA/HUD/SaveIcon/SaveIcon.h" #include "SWA/HUD/Sonic/HudSonicStage.h" #include "SWA/Movie/MovieDisplayer.h" #include "SWA/Movie/MovieManager.h" diff --git a/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h b/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h new file mode 100644 index 0000000..60903bc --- /dev/null +++ b/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace SWA::Achievement +{ + class CManager : public Hedgehog::Universe::CUpdateUnit + { + public: + class CMember + { + public: + SWA_INSERT_PADDING(0x08); + be m_AchievementID; + }; + + SWA_INSERT_PADDING(0x98); + xpointer m_pMember; + be m_IsUnlocked; + SWA_INSERT_PADDING(0x10); + }; +} diff --git a/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h b/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h new file mode 100644 index 0000000..a21cbd9 --- /dev/null +++ b/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace SWA +{ + class CAchievementTest + { + public: + SWA_INSERT_PADDING(0x38); + be m_Unk1; + be m_AchievementID; + uint8_t m_Unk2; + }; +} diff --git a/UnleashedRecomp/api/SWA/HUD/SaveIcon/SaveIcon.h b/UnleashedRecomp/api/SWA/HUD/SaveIcon/SaveIcon.h new file mode 100644 index 0000000..61d5ec7 --- /dev/null +++ b/UnleashedRecomp/api/SWA/HUD/SaveIcon/SaveIcon.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace SWA +{ + class CSaveIcon : Hedgehog::Universe::CUpdateUnit + { + public: + SWA_INSERT_PADDING(0xD8); + bool m_IsVisible; + }; +} diff --git a/UnleashedRecomp/api/SWA/System/ApplicationDocument.h b/UnleashedRecomp/api/SWA/System/ApplicationDocument.h index 6dfdc7d..fb81b07 100644 --- a/UnleashedRecomp/api/SWA/System/ApplicationDocument.h +++ b/UnleashedRecomp/api/SWA/System/ApplicationDocument.h @@ -35,7 +35,9 @@ namespace SWA public: SWA_INSERT_PADDING(0x20); boost::shared_ptr m_pGame; - SWA_INSERT_PADDING(0x114); + SWA_INSERT_PADDING(0xD4); + xpointer m_pAchievementManager; + SWA_INSERT_PADDING(0x3C); xpointer m_spGameParameter; }; diff --git a/UnleashedRecomp/app.cpp b/UnleashedRecomp/app.cpp index 722429f..8d5e8b3 100644 --- a/UnleashedRecomp/app.cpp +++ b/UnleashedRecomp/app.cpp @@ -1,19 +1,23 @@ +#include #include #include -#include +#include +bool g_isGameLoaded = false; double g_deltaTime; // CApplication::Update PPC_FUNC_IMPL(__imp__sub_822C1130); PPC_FUNC(sub_822C1130) { + g_isGameLoaded = true; g_deltaTime = ctx.f1.f64; SDL_PumpEvents(); SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); Window::Update(); + AudioPatches::Update(g_deltaTime); __imp__sub_822C1130(ctx, base); } diff --git a/UnleashedRecomp/app.h b/UnleashedRecomp/app.h index 4e1d379..b9b1bf5 100644 --- a/UnleashedRecomp/app.h +++ b/UnleashedRecomp/app.h @@ -1,3 +1,4 @@ #pragma once +extern bool g_isGameLoaded; extern double g_deltaTime; diff --git a/UnleashedRecomp/cfg/config.h b/UnleashedRecomp/cfg/config.h deleted file mode 100644 index 7116f27..0000000 --- a/UnleashedRecomp/cfg/config.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#include "config_detail.h" - -class Config -{ -public: - inline static std::vector Definitions{}; - - CONFIG_DEFINE_ENUM("System", ELanguage, Language, ELanguage::English); - CONFIG_DEFINE("System", bool, Hints, true); - CONFIG_DEFINE("System", bool, ControlTutorial, true); - CONFIG_DEFINE_ENUM("System", EScoreBehaviour, ScoreBehaviour, EScoreBehaviour::CheckpointReset); - CONFIG_DEFINE("System", bool, UnleashOutOfControlDrain, true); - CONFIG_DEFINE("System", bool, WerehogHubTransformVideo, true); - CONFIG_DEFINE("System", bool, LogoSkip, false); - - CONFIG_DEFINE("Controls", bool, CameraXInvert, false); - CONFIG_DEFINE("Controls", bool, CameraYInvert, false); - CONFIG_DEFINE("Controls", bool, XButtonHoming, true); - CONFIG_DEFINE("Controls", bool, UnleashCancel, false); - - CONFIG_DEFINE("Audio", float, MusicVolume, 1.0f); - CONFIG_DEFINE("Audio", float, SEVolume, 1.0f); - CONFIG_DEFINE_ENUM("Audio", EVoiceLanguage, VoiceLanguage, EVoiceLanguage::English); - CONFIG_DEFINE("Audio", bool, Subtitles, true); - CONFIG_DEFINE("Audio", bool, WerehogBattleMusic, true); - - CONFIG_DEFINE_ENUM("Video", EGraphicsAPI, GraphicsAPI, EGraphicsAPI::D3D12); - CONFIG_DEFINE("Video", int32_t, WindowX, WINDOWPOS_CENTRED); - CONFIG_DEFINE("Video", int32_t, WindowY, WINDOWPOS_CENTRED); - CONFIG_DEFINE("Video", int32_t, WindowWidth, 1280); - CONFIG_DEFINE("Video", int32_t, WindowHeight, 720); - CONFIG_DEFINE_ENUM("Video", EWindowState, WindowState, EWindowState::Normal); - - CONFIG_DEFINE_CALLBACK("Video", float, ResolutionScale, 1.0f, - { - def->Value = std::clamp(def->Value, 0.25f, 2.0f); - }); - - CONFIG_DEFINE("Video", bool, Fullscreen, false); - CONFIG_DEFINE("Video", bool, VSync, true); - CONFIG_DEFINE("Video", bool, TripleBuffering, true); - CONFIG_DEFINE("Video", int32_t, FPS, 60); - CONFIG_DEFINE("Video", float, Brightness, 0.5f); - CONFIG_DEFINE("Video", size_t, MSAA, 4); - CONFIG_DEFINE("Video", size_t, AnisotropicFiltering, 16); - CONFIG_DEFINE_ENUM("Video", EShadowResolution, ShadowResolution, EShadowResolution::x4096); - CONFIG_DEFINE_ENUM("Video", EGITextureFiltering, GITextureFiltering, EGITextureFiltering::Bicubic); - CONFIG_DEFINE("Video", bool, AlphaToCoverage, true); - CONFIG_DEFINE("Video", bool, MotionBlur, true); - CONFIG_DEFINE("Video", bool, Xbox360ColourCorrection, false); - CONFIG_DEFINE_ENUM("Video", EMovieScaleMode, MovieScaleMode, EMovieScaleMode::Fit); - CONFIG_DEFINE_ENUM("Video", EUIScaleMode, UIScaleMode, EUIScaleMode::Centre); - - static std::filesystem::path GetUserPath() - { - if (std::filesystem::exists("portable.txt")) - return std::filesystem::current_path(); - - std::filesystem::path userPath{}; - - // TODO: handle platform-specific paths. - PWSTR knownPath = NULL; - if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &knownPath) == S_OK) - userPath = std::filesystem::path{ knownPath } / USER_DIRECTORY; - - CoTaskMemFree(knownPath); - - return userPath; - } - - static std::filesystem::path GetConfigPath() - { - return GetUserPath() / TOML_FILE; - } - - static std::filesystem::path GetSavePath() - { - return GetUserPath() / "save"; - } - - static void Load(); - static void Save(); -}; diff --git a/UnleashedRecomp/cfg/config_detail.cpp b/UnleashedRecomp/cfg/config_detail.cpp deleted file mode 100644 index d48bbec..0000000 --- a/UnleashedRecomp/cfg/config_detail.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "config.h" -#include "config_detail.h" - -// CONFIG_DEFINE -template -ConfigDef::ConfigDef(std::string section, std::string name, T defaultValue) : Section(section), Name(name), DefaultValue(defaultValue) -{ - Config::Definitions.emplace_back(this); -} - -// CONFIG_DEFINE_ENUM -template -ConfigDef::ConfigDef(std::string section, std::string name, T defaultValue, std::unordered_map enumTemplate) - : Section(section), Name(name), DefaultValue(defaultValue), EnumTemplate(enumTemplate) -{ - for (const auto& pair : EnumTemplate) - EnumTemplateReverse[pair.second] = pair.first; - - Config::Definitions.emplace_back(this); -} - -// CONFIG_DEFINE_CALLBACK -template -ConfigDef::ConfigDef(std::string section, std::string name, T defaultValue, std::function*)> readCallback) - : Section(section), Name(name), DefaultValue(defaultValue), ReadCallback(readCallback) -{ - Config::Definitions.emplace_back(this); -} diff --git a/UnleashedRecomp/cfg/config_detail.h b/UnleashedRecomp/cfg/config_detail.h deleted file mode 100644 index f2c883f..0000000 --- a/UnleashedRecomp/cfg/config_detail.h +++ /dev/null @@ -1,282 +0,0 @@ -#pragma once - -#define USER_DIRECTORY "SWA" - -#define TOML_FILE "config.toml" - -#define CONFIG_DEFINE(section, type, name, defaultValue) \ - inline static ConfigDef name{section, #name, defaultValue}; - -#define CONFIG_DEFINE_ENUM_TEMPLATE(type) \ - inline static std::unordered_map g_##type##_template = - -#define CONFIG_DEFINE_ENUM(section, type, name, defaultValue) \ - inline static ConfigDef name{section, #name, defaultValue, g_##type##_template}; - -#define CONFIG_DEFINE_CALLBACK(section, type, name, defaultValue, readCallback) \ - inline static ConfigDef name{section, #name, defaultValue, [](ConfigDef* def) readCallback}; - -#define CONFIG_GET_DEFAULT(name) Config::name.DefaultValue -#define CONFIG_SET_DEFAULT(name) Config::name.MakeDefault(); - -#define WINDOWPOS_CENTRED 0x2FFF0000 - -class IConfigDef -{ -public: - virtual ~IConfigDef() = default; - virtual void ReadValue(toml::v3::ex::parse_result& toml) = 0; - virtual void MakeDefault() = 0; - virtual std::string GetSection() const = 0; - virtual std::string GetName() const = 0; - virtual std::string GetDefinition(bool withSection = false) const = 0; - virtual std::string ToString() const = 0; -}; - -template -class ConfigDef : public IConfigDef -{ -public: - std::string Section{}; - std::string Name{}; - T DefaultValue{}; - T Value{ DefaultValue }; - std::unordered_map EnumTemplate{}; - std::unordered_map EnumTemplateReverse{}; - std::function*)> ReadCallback; - - // CONFIG_DEFINE - ConfigDef(std::string section, std::string name, T defaultValue); - - // CONFIG_DEFINE_ENUM - ConfigDef(std::string section, std::string name, T defaultValue, std::unordered_map enumTemplate); - - // CONFIG_DEFINE_CALLBACK - ConfigDef(std::string section, std::string name, T defaultValue, std::function*)> readCallback); - - void ReadValue(toml::v3::ex::parse_result& toml) override - { - if (auto pSection = toml[Section].as_table()) - { - const auto& section = *pSection; - - if constexpr (std::is_same::value) - { - Value = section[Name].value_or(DefaultValue); - } - else if constexpr (std::is_enum_v) - { - auto it = EnumTemplate.begin(); - - Value = EnumTemplate[section[Name].value_or(static_cast(it->first))]; - } - else - { - Value = section[Name].value_or(DefaultValue); - } - - if (ReadCallback) - ReadCallback(this); - } - } - - void MakeDefault() override - { - Value = DefaultValue; - } - - std::string GetSection() const override - { - return Section; - } - - std::string GetName() const override - { - return Name; - } - - std::string GetDefinition(bool withSection = false) const override - { - std::string result; - - if (withSection) - result += "[" + Section + "]\n"; - - result += Name + " = " + ToString(); - - return result; - } - - std::string ToString() const override - { - if constexpr (std::is_same::value) - { - return std::format("\"{}\"", Value); - } - else if constexpr (std::is_enum_v) - { - auto it = EnumTemplateReverse.find(Value); - - if (it != EnumTemplateReverse.end()) - { - return std::format("\"{}\"", it->second); - } - else - { - return "\"N/A\""; - } - } - else - { - return std::format("{}", Value); - } - } - - ConfigDef& operator=(const ConfigDef& other) - { - if (this != &other) - Value = other.Value; - - return *this; - } - - operator T() const - { - return Value; - } - - void operator=(const T& other) - { - Value = other; - } -}; - -enum class ELanguage : uint32_t -{ - English = 1, - Japanese, - German, - French, - Spanish, - Italian -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(ELanguage) -{ - { "English", ELanguage::English }, - { "Japanese", ELanguage::Japanese }, - { "German", ELanguage::German }, - { "French", ELanguage::French }, - { "Spanish", ELanguage::Spanish }, - { "Italian", ELanguage::Italian } -}; - -enum class EScoreBehaviour : uint32_t -{ - CheckpointReset, - CheckpointRetain -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(EScoreBehaviour) -{ - { "CheckpointReset", EScoreBehaviour::CheckpointReset }, - { "CheckpointRetain", EScoreBehaviour::CheckpointRetain } -}; - -enum class EVoiceLanguage : uint32_t -{ - English, - Japanese -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(EVoiceLanguage) -{ - { "English", EVoiceLanguage::English }, - { "Japanese", EVoiceLanguage::Japanese } -}; - -enum class EGraphicsAPI : uint32_t -{ - D3D12, - Vulkan -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(EGraphicsAPI) -{ - { "D3D12", EGraphicsAPI::D3D12 }, - { "Vulkan", EGraphicsAPI::Vulkan } -}; - -enum class EWindowState : uint32_t -{ - Normal, - Maximised -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(EWindowState) -{ - { "Normal", EWindowState::Normal }, - { "Maximised", EWindowState::Maximised }, - { "Maximized", EWindowState::Maximised } -}; - -enum class EShadowResolution : int32_t -{ - Original = -1, - x512 = 512, - x1024 = 1024, - x2048 = 2048, - x4096 = 4096, - x8192 = 8192 -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(EShadowResolution) -{ - { "Original", EShadowResolution::Original }, - { "512", EShadowResolution::x512 }, - { "1024", EShadowResolution::x1024 }, - { "2048", EShadowResolution::x2048 }, - { "4096", EShadowResolution::x4096 }, - { "8192", EShadowResolution::x8192 }, -}; - -enum class EGITextureFiltering : uint32_t -{ - Linear, - Bicubic -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(EGITextureFiltering) -{ - { "Linear", EGITextureFiltering::Linear }, - { "Bicubic", EGITextureFiltering::Bicubic } -}; - -enum class EMovieScaleMode : uint32_t -{ - Stretch, - Fit, - Fill -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(EMovieScaleMode) -{ - { "Stretch", EMovieScaleMode::Stretch }, - { "Fit", EMovieScaleMode::Fit }, - { "Fill", EMovieScaleMode::Fill } -}; - -enum class EUIScaleMode : uint32_t -{ - Stretch, - Edge, - Centre -}; - -CONFIG_DEFINE_ENUM_TEMPLATE(EUIScaleMode) -{ - { "Stretch", EUIScaleMode::Stretch }, - { "Edge", EUIScaleMode::Edge }, - { "Centre", EUIScaleMode::Centre }, - { "Center", EUIScaleMode::Centre } -}; diff --git a/UnleashedRecomp/decompressor.h b/UnleashedRecomp/decompressor.h new file mode 100644 index 0000000..4809f7e --- /dev/null +++ b/UnleashedRecomp/decompressor.h @@ -0,0 +1,9 @@ +#pragma once + +template +static std::unique_ptr decompressZstd(const uint8_t(&data)[N], size_t decompressedSize) +{ + auto decompressedData = std::make_unique(decompressedSize); + ZSTD_decompress(decompressedData.get(), decompressedSize, data, N); + return decompressedData; +} diff --git a/UnleashedRecomp/exports.cpp b/UnleashedRecomp/exports.cpp new file mode 100644 index 0000000..e47eaef --- /dev/null +++ b/UnleashedRecomp/exports.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include +#include +#include + +SWA_API void Game_PlaySound(const char* pName) +{ + // TODO: use own sound player. + if (InstallerWizard::s_isVisible) + return; + + guest_stack_var soundPlayer; + GuestToHostFunction(sub_82B4DF50, soundPlayer.get(), ((be*)g_memory.Translate(0x83367900))->get(), 7, 0, 0); + + auto soundPlayerVtable = (be*)g_memory.Translate(*(be*)soundPlayer->get()); + uint32_t virtualFunction = *(soundPlayerVtable + 1); + + size_t strLen = strlen(pName); + void* strAllocation = g_userHeap.Alloc(strLen + 1); + memcpy(strAllocation, pName, strLen + 1); + GuestToHostFunction(virtualFunction, soundPlayer->get(), strAllocation, 0); + g_userHeap.Free(strAllocation); +} + +SWA_API void Window_SetFullscreen(bool isEnabled) +{ + Window::SetFullscreen(isEnabled); +} diff --git a/UnleashedRecomp/exports.h b/UnleashedRecomp/exports.h new file mode 100644 index 0000000..8963c76 --- /dev/null +++ b/UnleashedRecomp/exports.h @@ -0,0 +1,4 @@ +#pragma once + +SWA_API void Game_PlaySound(const char* pName); +SWA_API void Window_SetFullscreen(bool isEnabled); diff --git a/UnleashedRecomp/framework.h b/UnleashedRecomp/framework.h index e870fec..fb8a53f 100644 --- a/UnleashedRecomp/framework.h +++ b/UnleashedRecomp/framework.h @@ -74,3 +74,23 @@ constexpr size_t FirstBitLow(TValue value) return 0; } + +static std::unique_ptr ReadAllBytes(const char* filePath, size_t& fileSize) +{ + FILE* file = fopen(filePath, "rb"); + + if (!file) + return std::make_unique(0); + + fseek(file, 0, SEEK_END); + + fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + auto data = std::make_unique(fileSize); + fread(data.get(), 1, fileSize, file); + + fclose(file); + + return data; +} diff --git a/UnleashedRecomp/gpu/imgui_common.cpp b/UnleashedRecomp/gpu/imgui_common.cpp new file mode 100644 index 0000000..c273ac7 --- /dev/null +++ b/UnleashedRecomp/gpu/imgui_common.cpp @@ -0,0 +1,22 @@ +#include "imgui_common.h" + +static std::vector> g_callbackData; +static uint32_t g_callbackDataIndex = 0; + +ImGuiCallbackData* AddImGuiCallback(ImGuiCallback callback) +{ + if (g_callbackDataIndex >= g_callbackData.size()) + g_callbackData.emplace_back(std::make_unique()); + + auto& callbackData = g_callbackData[g_callbackDataIndex]; + ++g_callbackDataIndex; + + ImGui::GetForegroundDrawList()->AddCallback(reinterpret_cast(callback), callbackData.get()); + + return callbackData.get(); +} + +void ResetImGuiCallbacks() +{ + g_callbackDataIndex = 0; +} diff --git a/UnleashedRecomp/gpu/imgui_common.h b/UnleashedRecomp/gpu/imgui_common.h new file mode 100644 index 0000000..1398110 --- /dev/null +++ b/UnleashedRecomp/gpu/imgui_common.h @@ -0,0 +1,48 @@ +#pragma once + +#define IMGUI_SHADER_MODIFIER_NONE 0 +#define IMGUI_SHADER_MODIFIER_SCANLINE 1 +#define IMGUI_SHADER_MODIFIER_CHECKERBOARD 2 +#define IMGUI_SHADER_MODIFIER_SCANLINE_BUTTON 3 + +#ifdef __cplusplus + +enum class ImGuiCallback : int32_t +{ + SetGradient = -1, + SetShaderModifier = -2, + SetOrigin = -3, + SetScale = -4, +}; + +union ImGuiCallbackData +{ + struct + { + float gradientMin[2]; + float gradientMax[2]; + uint32_t gradientTop; + uint32_t gradientBottom; + } setGradient; + + struct + { + uint32_t shaderModifier; + } setShaderModifier; + + struct + { + float origin[2]; + } setOrigin; + + struct + { + float scale[2]; + } setScale; +}; + +extern ImGuiCallbackData* AddImGuiCallback(ImGuiCallback callback); + +extern void ResetImGuiCallbacks(); + +#endif diff --git a/UnleashedRecomp/gpu/imgui_snapshot.cpp b/UnleashedRecomp/gpu/imgui_snapshot.cpp index 492b61f..0b375a2 100644 --- a/UnleashedRecomp/gpu/imgui_snapshot.cpp +++ b/UnleashedRecomp/gpu/imgui_snapshot.cpp @@ -1,5 +1,12 @@ #include "imgui_snapshot.h" +#include +#include +#include +#include +#include +#include + void ImDrawDataSnapshot::Clear() { for (int n = 0; n < Cache.GetMapSize(); n++) @@ -52,3 +59,214 @@ void ImDrawDataSnapshot::SnapUsingSwap(ImDrawData* src, double current_time) Cache.Remove(GetDrawListID(entry->SrcCopy), entry); } }; + +template +void ImFontAtlasSnapshot::SnapPointer(size_t offset, const T1& value, const T2& ptr, size_t count) +{ + if (ptr != nullptr && count != 0) + { + if (!objects.contains(ptr)) + { + constexpr size_t ALIGN = alignof(std::remove_pointer_t); + constexpr size_t SIZE = sizeof(std::remove_pointer_t); + + size_t ptrOffset = (data.size() + ALIGN - 1) & ~(ALIGN - 1); + data.resize(ptrOffset + SIZE * count); + memcpy(&data[ptrOffset], ptr, SIZE * count); + + for (size_t i = 0; i < count; i++) + { + size_t curPtrOffset = ptrOffset + SIZE * i; + objects[&ptr[i]] = curPtrOffset; + Traverse(curPtrOffset, ptr[i]); + } + } + + size_t fieldOffset = offset + (reinterpret_cast(&ptr) - reinterpret_cast(&value)); + *reinterpret_cast(&data[fieldOffset]) = objects[ptr]; + offsets.push_back(fieldOffset); + } +} + +template +void ImFontAtlasSnapshot::Traverse(size_t offset, const T& value) +{ + if constexpr (std::is_pointer_v) + { + SnapPointer(offset, value, value, 1); + } + else if constexpr (std::is_same_v) + { + SnapPointer(offset, value, value.ConfigData.Data, value.ConfigData.Size); + SnapPointer(offset, value, value.CustomRects.Data, value.CustomRects.Size); + SnapPointer(offset, value, value.Fonts.Data, value.Fonts.Size); + } + else if constexpr (std::is_same_v) + { + SnapPointer(offset, value, value.IndexAdvanceX.Data, value.IndexAdvanceX.Size); + SnapPointer(offset, value, value.IndexLookup.Data, value.IndexLookup.Size); + SnapPointer(offset, value, value.Glyphs.Data, value.Glyphs.Size); + SnapPointer(offset, value, value.FallbackGlyph, 1); + SnapPointer(offset, value, value.ContainerAtlas, 1); + SnapPointer(offset, value, value.ConfigData, value.ConfigDataCount); + } + else if constexpr (std::is_same_v) + { + SnapPointer(offset, value, value.Font, 1); + } + else if constexpr (std::is_same_v) + { + SnapPointer(offset, value, value.GlyphRanges, value.GlyphRanges != nullptr ? wcslen(reinterpret_cast(value.GlyphRanges)) + 1 : 0); + SnapPointer(offset, value, value.DstFont, 1); + } +} + +template +size_t ImFontAtlasSnapshot::Snap(const T& value) +{ + size_t offset = (data.size() + alignof(T) - 1) & ~(alignof(T) - 1); + data.resize(offset + sizeof(T)); + memcpy(&data[offset], &value, sizeof(T)); + objects[&value] = offset; + Traverse(offset, value); + return offset; +} + +struct ImFontAtlasSnapshotHeader +{ + uint32_t imguiVersion; + uint32_t dataOffset; + uint32_t offsetCount; + uint32_t offsetsOffset; +}; + +void ImFontAtlasSnapshot::Snap() +{ + data.resize(sizeof(ImFontAtlasSnapshotHeader)); + size_t dataOffset = Snap(*ImGui::GetIO().Fonts); + + size_t offsetsOffset = data.size(); + std::sort(offsets.begin(), offsets.end()); + + data.insert(data.end(), + reinterpret_cast(offsets.data()), + reinterpret_cast(offsets.data() + offsets.size())); + + auto header = reinterpret_cast(data.data()); + header->imguiVersion = IMGUI_VERSION_NUM; + header->dataOffset = dataOffset; + header->offsetCount = offsets.size(); + header->offsetsOffset = offsetsOffset; +} + +static std::unique_ptr g_imFontAtlas; + +ImFontAtlas* ImFontAtlasSnapshot::Load() +{ + g_imFontAtlas = decompressZstd(g_im_font_atlas, g_im_font_atlas_uncompressed_size); + + auto header = reinterpret_cast(g_imFontAtlas.get()); + assert(header->imguiVersion == IMGUI_VERSION_NUM && "ImGui version mismatch, the font atlas needs to be regenerated!"); + + auto offsetTable = reinterpret_cast(g_imFontAtlas.get() + header->offsetsOffset); + for (size_t i = 0; i < header->offsetCount; i++) + { + *reinterpret_cast(g_imFontAtlas.get() + (*offsetTable)) += reinterpret_cast(g_imFontAtlas.get()); + ++offsetTable; + } + + return reinterpret_cast(g_imFontAtlas.get() + header->dataOffset); +} + + +static void GetGlyphs(std::set& glyphs, const std::string_view& value) +{ + const char* cur = value.data(); + while (cur < value.data() + value.size()) + { + unsigned int c; + cur += ImTextCharFromUtf8(&c, cur, value.data() + value.size()); + glyphs.emplace(c); + } +} + +static std::vector g_glyphRanges; + +void ImFontAtlasSnapshot::GenerateGlyphRanges() +{ + std::vector localeStrings; + + for (auto& config : Config::Definitions) + config->GetLocaleStrings(localeStrings); + + std::set glyphs; + + for (size_t i = 0x20; i <= 0xFF; i++) + glyphs.emplace(i); + + for (auto& localeString : localeStrings) + GetGlyphs(glyphs, localeString); + + for (auto& [name, locale] : g_locale) + { + for (auto& [language, value] : locale) + GetGlyphs(glyphs, value); + } + + for (auto& [language, locale] : g_bool_locale) + { + for (auto& [value, nameAndDesc] : locale) + { + GetGlyphs(glyphs, std::get<0>(nameAndDesc)); + GetGlyphs(glyphs, std::get<1>(nameAndDesc)); + } + } + + if (g_isGameLoaded) + { + for (size_t i = XDBF_LANGUAGE_ENGLISH; i <= XDBF_LANGUAGE_ITALIAN; i++) + { + auto achievements = g_xdbfWrapper.GetAchievements(static_cast(i)); + for (auto& achievement : achievements) + { + GetGlyphs(glyphs, achievement.Name); + GetGlyphs(glyphs, achievement.UnlockedDesc); + GetGlyphs(glyphs, achievement.LockedDesc); + } + } + } + + for (auto glyph : glyphs) + { + if (g_glyphRanges.empty() || (g_glyphRanges.back() + 1) != glyph) + { + g_glyphRanges.push_back(glyph); + g_glyphRanges.push_back(glyph); + } + else + { + g_glyphRanges.back() = glyph; + } + } + + g_glyphRanges.push_back(0); +} + +ImFont* ImFontAtlasSnapshot::GetFont(const char* name, float size) +{ + auto fontAtlas = ImGui::GetIO().Fonts; + for (auto& configData : fontAtlas->ConfigData) + { + if (strstr(configData.Name, name) != nullptr && abs(configData.SizePixels - size) < 0.001f) + { + assert(configData.DstFont != nullptr); + return configData.DstFont; + } + } + +#ifdef ENABLE_IM_FONT_ATLAS_SNAPSHOT + assert(false && "Unable to locate equivalent font in the atlas file."); +#endif + + return fontAtlas->AddFontFromFileTTF(name, size, nullptr, g_glyphRanges.data()); +} diff --git a/UnleashedRecomp/gpu/imgui_snapshot.h b/UnleashedRecomp/gpu/imgui_snapshot.h index f08393b..26b60d0 100644 --- a/UnleashedRecomp/gpu/imgui_snapshot.h +++ b/UnleashedRecomp/gpu/imgui_snapshot.h @@ -30,3 +30,34 @@ struct ImDrawDataSnapshot ImGuiID GetDrawListID(ImDrawList* src_list) { return ImHashData(&src_list, sizeof(src_list)); } // Hash pointer ImDrawDataSnapshotEntry* GetOrAddEntry(ImDrawList* src_list) { return Cache.GetOrAddByKey(GetDrawListID(src_list)); } }; + +// Undefine this to generate a font atlas file in working directory. +// You also need to do this if you are testing localization, as only +// characters in the locale get added to the atlas. +// Don't forget to compress the generated atlas texture to BC4 with no mips! +#define ENABLE_IM_FONT_ATLAS_SNAPSHOT + +struct ImFontAtlasSnapshot +{ + std::vector data; + ankerl::unordered_dense::map objects; + std::vector offsets; + + template + void SnapPointer(size_t offset, const T1& value, const T2& ptr, size_t count); + + template + void Traverse(size_t offset, const T& value); + + template + size_t Snap(const T& value); + + void Snap(); + + static ImFontAtlas* Load(); + + static void GenerateGlyphRanges(); + + // When ENABLE_IM_FONT_ATLAS_SNAPSHOT is undefined, this creates the font runtime instead. + static ImFont* GetFont(const char* name, float size); +}; diff --git a/UnleashedRecomp/gpu/shader/imgui_common.hlsli b/UnleashedRecomp/gpu/shader/imgui_common.hlsli index d54f529..85a9ecc 100644 --- a/UnleashedRecomp/gpu/shader/imgui_common.hlsli +++ b/UnleashedRecomp/gpu/shader/imgui_common.hlsli @@ -2,8 +2,15 @@ struct PushConstants { + float2 GradientMin; + float2 GradientMax; + uint GradientTop; + uint GradientBottom; + uint ShaderModifier; uint Texture2DDescriptorIndex; float2 InverseDisplaySize; + float2 Origin; + float2 Scale; }; Texture2D g_Texture2DDescriptorHeap[] : register(t0, space0); diff --git a/UnleashedRecomp/gpu/shader/imgui_ps.hlsl b/UnleashedRecomp/gpu/shader/imgui_ps.hlsl index bc0cd76..2207a2b 100644 --- a/UnleashedRecomp/gpu/shader/imgui_ps.hlsl +++ b/UnleashedRecomp/gpu/shader/imgui_ps.hlsl @@ -1,11 +1,92 @@ #include "imgui_common.hlsli" +#include "../imgui_common.h" + +float4 DecodeColor(uint color) +{ + return float4(color & 0xFF, (color >> 8) & 0xFF, (color >> 16) & 0xFF, (color >> 24) & 0xFF) / 255.0; +} + +float4 SamplePoint(int2 position) +{ + switch (g_PushConstants.ShaderModifier) + { + case IMGUI_SHADER_MODIFIER_SCANLINE: + { + if (int(position.y) % 2 == 0) + return float4(1.0, 1.0, 1.0, 0.0); + + break; + } + case IMGUI_SHADER_MODIFIER_CHECKERBOARD: + { + int remnantX = int(position.x) % 9; + int remnantY = int(position.y) % 9; + + float4 color = 1.0; + + if (remnantX == 0 || remnantY == 0) + color.a = 0.0; + + if ((remnantY % 2) == 0) + color.rgb = 0.5; + + return color; + } + case IMGUI_SHADER_MODIFIER_SCANLINE_BUTTON: + { + if (int(position.y) % 2 == 0) + return float4(1.0, 1.0, 1.0, 0.5); + + break; + } + } + + return 1.0; +} + +float4 SampleLinear(float2 uvTexspace) +{ + int2 integerPart = floor(uvTexspace); + float2 fracPart = frac(uvTexspace); + + float4 topLeft = SamplePoint(integerPart + float2(0, 0)); + float4 topRight = SamplePoint(integerPart + float2(1, 0)); + float4 bottomLeft = SamplePoint(integerPart + float2(0, 1)); + float4 bottomRight = SamplePoint(integerPart + float2(1, 1)); + + float4 top = lerp(topLeft, topRight, fracPart.x); + float4 bottom = lerp(bottomLeft, bottomRight, fracPart.x); + + return lerp(top, bottom, fracPart.y); +} + +float4 PixelAntialiasing(float2 uvTexspace) +{ + float2 seam = floor(uvTexspace + 0.5); + uvTexspace = (uvTexspace - seam) / fwidth(uvTexspace) + seam; + uvTexspace = clamp(uvTexspace, seam - 0.5, seam + 0.5); + + if (g_PushConstants.InverseDisplaySize.x < g_PushConstants.InverseDisplaySize.y) + uvTexspace *= min(1.0, g_PushConstants.InverseDisplaySize.y * 720.0f); + else + uvTexspace *= min(1.0, g_PushConstants.InverseDisplaySize.x * 1280.0f); + + return SampleLinear(uvTexspace); +} float4 main(in Interpolators interpolators) : SV_Target { float4 color = interpolators.Color; + color *= PixelAntialiasing(interpolators.Position.xy - 0.5); if (g_PushConstants.Texture2DDescriptorIndex != 0) color *= g_Texture2DDescriptorHeap[g_PushConstants.Texture2DDescriptorIndex].Sample(g_SamplerDescriptorHeap[0], interpolators.UV); + if (any(g_PushConstants.GradientMin != g_PushConstants.GradientMax)) + { + float2 factor = saturate((interpolators.Position.xy - g_PushConstants.GradientMin) / (g_PushConstants.GradientMax - g_PushConstants.GradientMin)); + color *= lerp(DecodeColor(g_PushConstants.GradientTop), DecodeColor(g_PushConstants.GradientBottom), factor.y); + } + return color; } diff --git a/UnleashedRecomp/gpu/shader/imgui_vs.hlsl b/UnleashedRecomp/gpu/shader/imgui_vs.hlsl index 90481cd..5b7820d 100644 --- a/UnleashedRecomp/gpu/shader/imgui_vs.hlsl +++ b/UnleashedRecomp/gpu/shader/imgui_vs.hlsl @@ -2,7 +2,8 @@ void main(in float2 position : POSITION, in float2 uv : TEXCOORD, in float4 color : COLOR, out Interpolators interpolators) { - interpolators.Position = float4(position * g_PushConstants.InverseDisplaySize * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); + float2 correctedPosition = g_PushConstants.Origin + (position - g_PushConstants.Origin) * g_PushConstants.Scale; + interpolators.Position = float4(correctedPosition * g_PushConstants.InverseDisplaySize * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); interpolators.UV = uv; interpolators.Color = color; } diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index 5d55681..821923a 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -6,13 +6,25 @@ #include #include #include +#include #include #include +#include +#include +#include +#include +#include +#include #include "imgui_snapshot.h" +#include "imgui_common.h" #include "video.h" #include -#include +#include +#include + +#include +#include #include @@ -768,7 +780,7 @@ static void SetAlphaTestMode(bool enable) if (enable) { - enableAlphaToCoverage = Config::AlphaToCoverage && g_renderTarget != nullptr && g_renderTarget->sampleCount != RenderSampleCount::COUNT_1; + enableAlphaToCoverage = Config::TransparencyAntiAliasing && g_renderTarget != nullptr && g_renderTarget->sampleCount != RenderSampleCount::COUNT_1; if (enableAlphaToCoverage) specConstants = SPEC_CONSTANT_ALPHA_TO_COVERAGE; @@ -1020,10 +1032,7 @@ static bool DetectWine() static constexpr size_t TEXTURE_DESCRIPTOR_SIZE = 65536; static constexpr size_t SAMPLER_DESCRIPTOR_SIZE = 1024; -static std::unique_ptr g_imFontTexture; -static std::unique_ptr g_imFontTextureView; -static uint32_t g_imFontTextureDescriptorIndex; -static bool g_imPendingBarrier = true; +static std::unique_ptr g_imFontTexture; static std::unique_ptr g_imPipelineLayout; static std::unique_ptr g_imPipeline; static ImDrawDataSnapshot g_imSnapshot; @@ -1043,13 +1052,52 @@ static void ExecuteCopyCommandList(const T& function) static constexpr uint32_t PITCH_ALIGNMENT = 0x100; static constexpr uint32_t PLACEMENT_ALIGNMENT = 0x200; +struct ImGuiPushConstants +{ + ImVec2 gradientMin{}; + ImVec2 gradientMax{}; + ImU32 gradientTop{}; + ImU32 gradientBottom{}; + uint32_t shaderModifier{}; + uint32_t texture2DDescriptorIndex{}; + ImVec2 inverseDisplaySize{}; + ImVec2 origin{ 0.0f, 0.0f }; + ImVec2 scale{ 1.0f, 1.0f }; +}; + static void CreateImGuiBackend() { ImGuiIO& io = ImGui::GetIO(); + io.IniFilename = nullptr; io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; + io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; + +#ifdef ENABLE_IM_FONT_ATLAS_SNAPSHOT + IM_DELETE(io.Fonts); + io.Fonts = ImFontAtlasSnapshot::Load(); +#else + io.Fonts->TexDesiredWidth = 4096; + io.Fonts->AddFontDefault(); + ImFontAtlasSnapshot::GenerateGlyphRanges(); +#endif + + AchievementMenu::Init(); + AchievementOverlay::Init(); + ButtonGuide::Init(); + MessageWindow::Init(); + OptionsMenu::Init(); + InstallerWizard::Init(); ImGui_ImplSDL2_InitForOther(Window::s_pWindow); + RenderComponentMapping componentMapping(RenderSwizzle::ONE, RenderSwizzle::ONE, RenderSwizzle::ONE, RenderSwizzle::R); + +#ifdef ENABLE_IM_FONT_ATLAS_SNAPSHOT + g_imFontTexture = LoadTexture(decompressZstd(g_im_font_atlas_texture, g_im_font_atlas_texture_uncompressed_size).get(), + g_im_font_atlas_texture_uncompressed_size, componentMapping); +#else + g_imFontTexture = std::make_unique(ResourceType::Texture); + uint8_t* pixels; int width, height; io.Fonts->GetTexDataAsAlpha8(&pixels, &width, &height); @@ -1062,7 +1110,9 @@ static void CreateImGuiBackend() textureDesc.mipLevels = 1; textureDesc.arraySize = 1; textureDesc.format = RenderFormat::R8_UNORM; - g_imFontTexture = g_device->createTexture(textureDesc); + + g_imFontTexture->textureHolder = g_device->createTexture(textureDesc); + g_imFontTexture->texture = g_imFontTexture->textureHolder.get(); uint32_t rowPitch = (width + PITCH_ALIGNMENT - 1) & ~(PITCH_ALIGNMENT - 1); uint32_t slicePitch = (rowPitch * height + PLACEMENT_ALIGNMENT - 1) & ~(PLACEMENT_ALIGNMENT - 1); @@ -1087,24 +1137,27 @@ static void CreateImGuiBackend() ExecuteCopyCommandList([&] { - g_copyCommandList->barriers(RenderBarrierStage::COPY, RenderTextureBarrier(g_imFontTexture.get(), RenderTextureLayout::COPY_DEST)); + g_copyCommandList->barriers(RenderBarrierStage::COPY, RenderTextureBarrier(g_imFontTexture->texture, RenderTextureLayout::COPY_DEST)); g_copyCommandList->copyTextureRegion( - RenderTextureCopyLocation::Subresource(g_imFontTexture.get(), 0), + RenderTextureCopyLocation::Subresource(g_imFontTexture->texture, 0), RenderTextureCopyLocation::PlacedFootprint(uploadBuffer.get(), RenderFormat::R8_UNORM, width, height, 1, rowPitch, 0)); }); + g_imFontTexture->layout = RenderTextureLayout::COPY_DEST; + RenderTextureViewDesc textureViewDesc; textureViewDesc.format = textureDesc.format; textureViewDesc.dimension = RenderTextureViewDimension::TEXTURE_2D; textureViewDesc.mipLevels = 1; - textureViewDesc.componentMapping = RenderComponentMapping(RenderSwizzle::ONE, RenderSwizzle::ONE, RenderSwizzle::ONE, RenderSwizzle::R); - g_imFontTextureView = g_imFontTexture->createTextureView(textureViewDesc); + textureViewDesc.componentMapping = componentMapping; + g_imFontTexture->textureView = g_imFontTexture->texture->createTextureView(textureViewDesc); - g_imFontTextureDescriptorIndex = g_textureDescriptorAllocator.allocate(); - g_textureDescriptorSet->setTexture(g_imFontTextureDescriptorIndex, g_imFontTexture.get(), RenderTextureLayout::SHADER_READ, g_imFontTextureView.get()); + g_imFontTexture->descriptorIndex = g_textureDescriptorAllocator.allocate(); + g_textureDescriptorSet->setTexture(g_imFontTexture->descriptorIndex, g_imFontTexture->texture, RenderTextureLayout::SHADER_READ, g_imFontTexture->textureView.get()); +#endif - io.Fonts->SetTexID(ImTextureID(g_imFontTextureDescriptorIndex)); + io.Fonts->SetTexID(g_imFontTexture.get()); RenderPipelineLayoutBuilder pipelineLayoutBuilder; pipelineLayoutBuilder.begin(false, true); @@ -1120,7 +1173,7 @@ static void CreateImGuiBackend() descriptorSetBuilder.end(true, SAMPLER_DESCRIPTOR_SIZE); pipelineLayoutBuilder.addDescriptorSet(descriptorSetBuilder); - pipelineLayoutBuilder.addPushConstant(0, 2, 12, RenderShaderStageFlag::VERTEX | RenderShaderStageFlag::PIXEL); + pipelineLayoutBuilder.addPushConstant(0, 2, sizeof(ImGuiPushConstants), RenderShaderStageFlag::VERTEX | RenderShaderStageFlag::PIXEL); pipelineLayoutBuilder.end(); g_imPipelineLayout = pipelineLayoutBuilder.create(g_device.get()); @@ -1147,9 +1200,37 @@ static void CreateImGuiBackend() pipelineDesc.inputSlots = &inputSlot; pipelineDesc.inputSlotsCount = 1; g_imPipeline = g_device->createGraphicsPipeline(pipelineDesc); + +#ifndef ENABLE_IM_FONT_ATLAS_SNAPSHOT + ImFontAtlasSnapshot snapshot; + snapshot.Snap(); + + FILE* file = fopen("im_font_atlas.bin", "wb"); + if (file) + { + fwrite(snapshot.data.data(), 1, snapshot.data.size(), file); + fclose(file); + } + + ddspp::Header header; + ddspp::HeaderDXT10 headerDX10; + ddspp::encode_header(ddspp::R8_UNORM, width, height, 1, ddspp::Texture2D, 1, 1, header, headerDX10); + + file = fopen("im_font_atlas.dds", "wb"); + if (file) + { + fwrite(&ddspp::DDS_MAGIC, 4, 1, file); + fwrite(&header, sizeof(header), 1, file); + fwrite(&headerDX10, sizeof(headerDX10), 1, file); + fwrite(pixels, 1, width * height, file); + fclose(file); + } +#endif } -static void CreateHostDevice() +static void BeginCommandList(); + +void Video::CreateHostDevice() { for (uint32_t i = 0; i < 16; i++) g_inputSlots[i].index = i; @@ -1180,7 +1261,22 @@ static void CreateHostDevice() g_copyCommandList = g_device->createCommandList(RenderCommandListType::COPY); g_copyCommandFence = g_device->createCommandFence(); - g_swapChain = g_queue->createSwapChain(Window::s_handle, Config::TripleBuffering ? 3 : 2, BACKBUFFER_FORMAT); + uint32_t bufferCount = 2; + + switch (Config::TripleBuffering) + { + case ETripleBuffering::Auto: + bufferCount = g_vulkan ? 2 : 3; // Defaulting to 3 is fine on D3D12 thanks to flip discard model. + break; + case ETripleBuffering::On: + bufferCount = 3; + break; + case ETripleBuffering::Off: + bufferCount = 2; + break; + } + + g_swapChain = g_queue->createSwapChain(Window::s_handle, bufferCount, BACKBUFFER_FORMAT); g_swapChain->setVsyncEnabled(Config::VSync); g_swapChainValid = !g_swapChain->needsResize(); @@ -1323,9 +1419,23 @@ static void CreateHostDevice() desc.renderTargetBlend[0] = RenderBlendDesc::Copy(); desc.renderTargetCount = 1; g_gammaCorrectionPipeline = g_device->createGraphicsPipeline(desc); + + g_backBuffer = g_userHeap.AllocPhysical(ResourceType::RenderTarget); + g_backBuffer->width = 1280; + g_backBuffer->height = 720; + g_backBuffer->format = BACKBUFFER_FORMAT; + g_backBuffer->textureHolder = g_device->createTexture(RenderTextureDesc::Texture2D(1, 1, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET)); + + BeginCommandList(); + + RenderTextureBarrier blankTextureBarriers[TEXTURE_DESCRIPTOR_NULL_COUNT]; + for (size_t i = 0; i < TEXTURE_DESCRIPTOR_NULL_COUNT; i++) + blankTextureBarriers[i] = RenderTextureBarrier(g_blankTextures[i].get(), RenderTextureLayout::SHADER_READ); + + g_commandLists[g_frame]->barriers(RenderBarrierStage::NONE, blankTextureBarriers, std::size(blankTextureBarriers)); } -static void WaitForGPU() +void Video::WaitForGPU() { if (g_vulkan) { @@ -1346,12 +1456,11 @@ static void WaitForGPU() } } -static bool g_pendingRenderThread; +static std::atomic g_pendingRenderThread; static void WaitForRenderThread() { - while (g_pendingRenderThread) - Sleep(0); + g_pendingRenderThread.wait(true); } static void BeginCommandList() @@ -1363,11 +1472,12 @@ static void BeginCommandList() g_pipelineState.renderTargetFormat = BACKBUFFER_FORMAT; g_pipelineState.depthStencilFormat = RenderFormat::UNKNOWN; + g_swapChain->setVsyncEnabled(Config::VSync); g_swapChainValid &= !g_swapChain->needsResize(); if (!g_swapChainValid) { - WaitForGPU(); + Video::WaitForGPU(); g_backBuffer->framebuffers.clear(); g_swapChainValid = g_swapChain->resize(); g_needsResize = g_swapChainValid; @@ -1378,7 +1488,7 @@ static void BeginCommandList() if (g_swapChainValid) { - bool applyingGammaCorrection = Config::Xbox360ColourCorrection || abs(Config::Brightness - 0.5f) > 0.001f; + bool applyingGammaCorrection = Config::XboxColourCorrection || abs(Config::Brightness - 0.5f) > 0.001f; if (applyingGammaCorrection) { @@ -1391,7 +1501,7 @@ static void BeginCommandList() if (g_intermediaryBackBufferTextureDescriptorIndex == NULL) g_intermediaryBackBufferTextureDescriptorIndex = g_textureDescriptorAllocator.allocate(); - WaitForGPU(); // Fine to wait for GPU, this'll only happen during resize. + Video::WaitForGPU(); // Fine to wait for GPU, this'll only happen during resize. g_intermediaryBackBufferTexture = g_device->createTexture(RenderTextureDesc::Texture2D(width, height, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET)); g_textureDescriptorSet->setTexture(g_intermediaryBackBufferTextureDescriptorIndex, g_intermediaryBackBufferTexture.get(), RenderTextureLayout::SHADER_READ); @@ -1438,21 +1548,17 @@ static void BeginCommandList() static uint32_t CreateDevice(uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5, be* a6) { - CreateHostDevice(); + g_xdbfTextureCache = std::unordered_map(); - g_backBuffer = g_userHeap.AllocPhysical(ResourceType::RenderTarget); - g_backBuffer->width = 1280; - g_backBuffer->height = 720; - g_backBuffer->format = BACKBUFFER_FORMAT; - g_backBuffer->textureHolder = g_device->createTexture(RenderTextureDesc::Texture2D(1, 1, 1, BACKBUFFER_FORMAT, RenderTextureFlag::RENDER_TARGET)); + for (auto &achievement : g_xdbfWrapper.GetAchievements(XDBF_LANGUAGE_ENGLISH)) + { + // huh? + if (!achievement.pImageBuffer || !achievement.ImageBufferSize) + continue; - BeginCommandList(); - - RenderTextureBarrier blankTextureBarriers[TEXTURE_DESCRIPTOR_NULL_COUNT]; - for (size_t i = 0; i < TEXTURE_DESCRIPTOR_NULL_COUNT; i++) - blankTextureBarriers[i] = RenderTextureBarrier(g_blankTextures[i].get(), RenderTextureLayout::SHADER_READ); - - g_commandLists[g_frame]->barriers(RenderBarrierStage::NONE, blankTextureBarriers, std::size(blankTextureBarriers)); + g_xdbfTextureCache[achievement.ID] = + LoadTexture((uint8_t *)achievement.pImageBuffer, achievement.ImageBufferSize).release(); + } auto device = g_userHeap.AllocPhysical(); memset(device, 0, sizeof(*device)); @@ -1668,6 +1774,8 @@ static void DrawImGui() ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); + ResetImGuiCallbacks(); + #ifdef ASYNC_PSO_DEBUG if (ImGui::Begin("Async PSO Stats")) { @@ -1684,6 +1792,13 @@ static void DrawImGui() ImGui::End(); #endif + AchievementMenu::Draw(); + OptionsMenu::Draw(); + AchievementOverlay::Draw(); + InstallerWizard::Draw(); + MessageWindow::Draw(); + ButtonGuide::Draw(); + ImGui::Render(); auto drawData = ImGui::GetDrawData(); @@ -1697,15 +1812,16 @@ static void DrawImGui() } } +static void SetFramebuffer(GuestSurface *renderTarget, GuestSurface *depthStencil, bool settingForClear); + static void ProcDrawImGui(const RenderCommand& cmd) { - auto& commandList = g_commandLists[g_frame]; + // Make sure the backbuffer is the current target. + AddBarrier(g_backBuffer, RenderTextureLayout::COLOR_WRITE); + FlushBarriers(); + SetFramebuffer(g_backBuffer, nullptr, false); - if (g_imPendingBarrier) - { - commandList->barriers(RenderBarrierStage::GRAPHICS, RenderTextureBarrier(g_imFontTexture.get(), RenderTextureLayout::SHADER_READ)); - g_imPendingBarrier = false; - } + auto& commandList = g_commandLists[g_frame]; commandList->setGraphicsPipelineLayout(g_imPipelineLayout.get()); commandList->setPipeline(g_imPipeline.get()); @@ -1715,8 +1831,9 @@ static void ProcDrawImGui(const RenderCommand& cmd) auto& drawData = g_imSnapshot.DrawData; commandList->setViewports(RenderViewport(drawData.DisplayPos.x, drawData.DisplayPos.y, drawData.DisplaySize.x, drawData.DisplaySize.y)); - float inverseDisplaySize[] = { 1.0f / drawData.DisplaySize.x, 1.0f / drawData.DisplaySize.y }; - commandList->setGraphicsPushConstants(0, inverseDisplaySize, 4, 8); + ImGuiPushConstants pushConstants{}; + pushConstants.inverseDisplaySize = { 1.0f / drawData.DisplaySize.x, 1.0f / drawData.DisplaySize.y }; + commandList->setGraphicsPushConstants(0, &pushConstants); for (int i = 0; i < drawData.CmdListsCount; i++) { @@ -1735,33 +1852,69 @@ static void ProcDrawImGui(const RenderCommand& cmd) for (int j = 0; j < drawList->CmdBuffer.Size; j++) { auto& drawCmd = drawList->CmdBuffer[j]; + if (drawCmd.UserCallback != nullptr) + { + auto callbackData = reinterpret_cast(drawCmd.UserCallbackData); - if (drawCmd.ClipRect.z <= drawCmd.ClipRect.x || drawCmd.ClipRect.w <= drawCmd.ClipRect.y) - continue; + switch (static_cast(reinterpret_cast(drawCmd.UserCallback))) + { + case ImGuiCallback::SetGradient: + commandList->setGraphicsPushConstants(0, &callbackData->setGradient, offsetof(ImGuiPushConstants, gradientMin), sizeof(callbackData->setGradient)); + break; + case ImGuiCallback::SetShaderModifier: + commandList->setGraphicsPushConstants(0, &callbackData->setShaderModifier, offsetof(ImGuiPushConstants, shaderModifier), sizeof(callbackData->setShaderModifier)); + break; + case ImGuiCallback::SetOrigin: + commandList->setGraphicsPushConstants(0, &callbackData->setOrigin, offsetof(ImGuiPushConstants, origin), sizeof(callbackData->setOrigin)); + break; + case ImGuiCallback::SetScale: + commandList->setGraphicsPushConstants(0, &callbackData->setScale, offsetof(ImGuiPushConstants, scale), sizeof(callbackData->setScale)); + break; + } + } + else + { + if (drawCmd.ClipRect.z <= drawCmd.ClipRect.x || drawCmd.ClipRect.w <= drawCmd.ClipRect.y) + continue; - uint32_t descriptorIndex = uint32_t(drawCmd.GetTexID()); - commandList->setGraphicsPushConstants(0, &descriptorIndex, 0, 4); - commandList->setScissors(RenderRect(int32_t(drawCmd.ClipRect.x), int32_t(drawCmd.ClipRect.y), int32_t(drawCmd.ClipRect.z), int32_t(drawCmd.ClipRect.w))); - commandList->drawIndexedInstanced(drawCmd.ElemCount, 1, drawCmd.IdxOffset, drawCmd.VtxOffset, 0); + auto texture = reinterpret_cast(drawCmd.TextureId); + uint32_t descriptorIndex = TEXTURE_DESCRIPTOR_NULL_TEXTURE_2D; + if (texture != nullptr) + { + if (texture->layout != RenderTextureLayout::SHADER_READ) + { + commandList->barriers(RenderBarrierStage::GRAPHICS | RenderBarrierStage::COPY, + RenderTextureBarrier(texture->texture, RenderTextureLayout::SHADER_READ)); + + texture->layout = RenderTextureLayout::SHADER_READ; + } + + descriptorIndex = texture->descriptorIndex; + } + + commandList->setGraphicsPushConstants(0, &descriptorIndex, offsetof(ImGuiPushConstants, texture2DDescriptorIndex), sizeof(descriptorIndex)); + commandList->setScissors(RenderRect(int32_t(drawCmd.ClipRect.x), int32_t(drawCmd.ClipRect.y), int32_t(drawCmd.ClipRect.z), int32_t(drawCmd.ClipRect.w))); + commandList->drawIndexedInstanced(drawCmd.ElemCount, 1, drawCmd.IdxOffset, drawCmd.VtxOffset, 0); + } } } } -static bool g_precompiledPipelineStateCache = false; +static bool g_shouldPrecompilePipelines = false; -static void Present() +void Video::HostPresent() { - DrawImGui(); WaitForRenderThread(); + DrawImGui(); - g_pendingRenderThread = true; + g_pendingRenderThread.store(true); RenderCommand cmd; cmd.type = RenderCommandType::Present; g_renderQueue.enqueue(cmd); // All the shaders are available at this point. We can precompile embedded PSOs then. - if (!g_precompiledPipelineStateCache) + if (g_shouldPrecompilePipelines) { // This is all the model consumer thread needs to see. ++g_compilingDataCount; @@ -1769,10 +1922,20 @@ static void Present() if ((++g_pendingDataCount) == 1) g_pendingDataCount.notify_all(); - g_precompiledPipelineStateCache = true; + g_shouldPrecompilePipelines = false; } } +void Video::StartPipelinePrecompilation() +{ + g_shouldPrecompilePipelines = true; +} + +static void GuestPresent() +{ + Video::HostPresent(); +} + static void SetRootDescriptor(const UploadAllocation& allocation, size_t index) { auto& commandList = g_commandLists[g_frame]; @@ -1798,7 +1961,7 @@ static void ProcPresent(const RenderCommand& cmd) uint32_t textureDescriptorIndex; } constants; - if (Config::Xbox360ColourCorrection) + if (Config::XboxColourCorrection) { constants.gammaR = 1.2f; constants.gammaG = 1.17f; @@ -1818,11 +1981,11 @@ static void ProcPresent(const RenderCommand& cmd) constants.gammaB = 1.0f / std::clamp(constants.gammaB + offset, 0.1f, 4.0f); constants.textureDescriptorIndex = g_intermediaryBackBufferTextureDescriptorIndex; - auto& framebuffer = g_backBuffer->framebuffers[swapChainTexture]; + auto &framebuffer = g_backBuffer->framebuffers[swapChainTexture]; if (!framebuffer) { RenderFramebufferDesc desc; - desc.colorAttachments = const_cast(&swapChainTexture); + desc.colorAttachments = const_cast(&swapChainTexture); desc.colorAttachmentsCount = 1; framebuffer = g_device->createFramebuffer(desc); } @@ -1833,7 +1996,7 @@ static void ProcPresent(const RenderCommand& cmd) RenderTextureBarrier(swapChainTexture, RenderTextureLayout::COLOR_WRITE) }; - auto& commandList = g_commandLists[g_frame]; + auto &commandList = g_commandLists[g_frame]; commandList->barriers(RenderBarrierStage::GRAPHICS, srcBarriers, std::size(srcBarriers)); commandList->setGraphicsPipelineLayout(g_pipelineLayout.get()); commandList->setPipeline(g_gammaCorrectionPipeline.get()); @@ -1852,14 +2015,14 @@ static void ProcPresent(const RenderCommand& cmd) } } - auto& commandList = g_commandLists[g_frame]; + auto &commandList = g_commandLists[g_frame]; commandList->end(); if (g_swapChainValid) { - const RenderCommandList* commandLists[] = { commandList.get() }; - RenderCommandSemaphore* waitSemaphores[] = { g_acquireSemaphores[g_frame].get() }; - RenderCommandSemaphore* signalSemaphores[] = { g_renderSemaphores[g_frame].get() }; + const RenderCommandList *commandLists[] = { commandList.get() }; + RenderCommandSemaphore *waitSemaphores[] = { g_acquireSemaphores[g_frame].get() }; + RenderCommandSemaphore *signalSemaphores[] = { g_renderSemaphores[g_frame].get() }; g_queue->executeCommandLists( commandLists, std::size(commandLists), @@ -1893,7 +2056,8 @@ static void ProcPresent(const RenderCommand& cmd) BeginCommandList(); - g_pendingRenderThread = false; + g_pendingRenderThread.store(false); + g_pendingRenderThread.notify_all(); } static GuestSurface* GetBackBuffer() @@ -2019,7 +2183,7 @@ static GuestSurface* CreateSurface(uint32_t width, uint32_t height, uint32_t for desc.depth = 1; desc.mipLevels = 1; desc.arraySize = 1; - desc.multisampling.sampleCount = multiSample != 0 && Config::MSAA > 1 ? Config::MSAA : RenderSampleCount::COUNT_1; + desc.multisampling.sampleCount = multiSample != 0 && Config::AntiAliasing != EAntiAliasing::None ? int32_t(Config::AntiAliasing.Value) : RenderSampleCount::COUNT_1; desc.format = ConvertFormat(format); desc.flags = desc.format == RenderFormat::D32_FLOAT ? RenderTextureFlag::DEPTH_TARGET : RenderTextureFlag::RENDER_TARGET; @@ -4039,176 +4203,194 @@ static RenderFormat ConvertDXGIFormat(ddspp::DXGIFormat format) } } -static void MakePictureData(GuestPictureData* pictureData, uint8_t* data, uint32_t dataSize) +static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping) { - if ((pictureData->flags & 0x1) == 0 && data != nullptr) + ddspp::Descriptor ddsDesc; + if (ddspp::decode_header((unsigned char *)(data), ddsDesc) != ddspp::Error) { - ddspp::Descriptor ddsDesc; - if (ddspp::decode_header(data, ddsDesc) != ddspp::Error) + RenderTextureDesc desc; + desc.dimension = ConvertTextureDimension(ddsDesc.type); + desc.width = ddsDesc.width; + desc.height = ddsDesc.height; + desc.depth = ddsDesc.depth; + desc.mipLevels = ddsDesc.numMips; + desc.arraySize = ddsDesc.type == ddspp::TextureType::Cubemap ? ddsDesc.arraySize * 6 : ddsDesc.arraySize; + desc.format = ConvertDXGIFormat(ddsDesc.format); + desc.flags = ddsDesc.type == ddspp::TextureType::Cubemap ? RenderTextureFlag::CUBE : RenderTextureFlag::NONE; + + texture.textureHolder = g_device->createTexture(desc); + texture.texture = texture.textureHolder.get(); + texture.layout = RenderTextureLayout::COPY_DEST; + + RenderTextureViewDesc viewDesc; + viewDesc.format = desc.format; + viewDesc.dimension = ConvertTextureViewDimension(ddsDesc.type); + viewDesc.mipLevels = ddsDesc.numMips; + viewDesc.componentMapping = componentMapping; + texture.textureView = texture.texture->createTextureView(viewDesc); + texture.descriptorIndex = g_textureDescriptorAllocator.allocate(); + g_textureDescriptorSet->setTexture(texture.descriptorIndex, texture.texture, RenderTextureLayout::SHADER_READ, texture.textureView.get()); + + texture.viewDimension = viewDesc.dimension; + + struct Slice { - const auto texture = g_userHeap.AllocPhysical(ResourceType::Texture); + uint32_t width; + uint32_t height; + uint32_t depth; + uint32_t srcOffset; + uint32_t dstOffset; + uint32_t srcRowPitch; + uint32_t dstRowPitch; + uint32_t rowCount; + }; - RenderTextureDesc desc; - desc.dimension = ConvertTextureDimension(ddsDesc.type); - desc.width = ddsDesc.width; - desc.height = ddsDesc.height; - desc.depth = ddsDesc.depth; - desc.mipLevels = ddsDesc.numMips; - desc.arraySize = ddsDesc.type == ddspp::TextureType::Cubemap ? ddsDesc.arraySize * 6 : ddsDesc.arraySize; - desc.format = ConvertDXGIFormat(ddsDesc.format); - desc.flags = ddsDesc.type == ddspp::TextureType::Cubemap ? RenderTextureFlag::CUBE : RenderTextureFlag::NONE; + std::vector slices; + uint32_t curSrcOffset = 0; + uint32_t curDstOffset = 0; - texture->textureHolder = g_device->createTexture(desc); - texture->texture = texture->textureHolder.get(); - texture->layout = RenderTextureLayout::COPY_DEST; - -#ifdef _DEBUG - texture->texture->setName(reinterpret_cast(g_memory.Translate(pictureData->name + 2))); -#endif - - RenderTextureViewDesc viewDesc; - viewDesc.format = desc.format; - viewDesc.dimension = ConvertTextureViewDimension(ddsDesc.type); - viewDesc.mipLevels = ddsDesc.numMips; - texture->textureView = texture->texture->createTextureView(viewDesc); - texture->descriptorIndex = g_textureDescriptorAllocator.allocate(); - g_textureDescriptorSet->setTexture(texture->descriptorIndex, texture->texture, RenderTextureLayout::SHADER_READ, texture->textureView.get()); - - texture->viewDimension = viewDesc.dimension; - - struct Slice + for (uint32_t arraySlice = 0; arraySlice < desc.arraySize; arraySlice++) + { + for (uint32_t mipSlice = 0; mipSlice < ddsDesc.numMips; mipSlice++) { - uint32_t width; - uint32_t height; - uint32_t depth; - uint32_t srcOffset; - uint32_t dstOffset; - uint32_t srcRowPitch; - uint32_t dstRowPitch; - uint32_t rowCount; - }; + auto& slice = slices.emplace_back(); - std::vector slices; - uint32_t curSrcOffset = 0; - uint32_t curDstOffset = 0; + slice.width = std::max(1u, ddsDesc.width >> mipSlice); + slice.height = std::max(1u, ddsDesc.height >> mipSlice); + slice.depth = std::max(1u, ddsDesc.depth >> mipSlice); + slice.srcOffset = curSrcOffset; + slice.dstOffset = curDstOffset; + uint32_t rowPitch = ((slice.width + ddsDesc.blockWidth - 1) / ddsDesc.blockWidth) * ddsDesc.bitsPerPixelOrBlock; + slice.srcRowPitch = (rowPitch + 7) / 8; + slice.dstRowPitch = (slice.srcRowPitch + PITCH_ALIGNMENT - 1) & ~(PITCH_ALIGNMENT - 1); + slice.rowCount = (slice.height + ddsDesc.blockHeight - 1) / ddsDesc.blockHeight; - for (uint32_t arraySlice = 0; arraySlice < desc.arraySize; arraySlice++) + curSrcOffset += slice.srcRowPitch * slice.rowCount * slice.depth; + curDstOffset += (slice.dstRowPitch * slice.rowCount * slice.depth + PLACEMENT_ALIGNMENT - 1) & ~(PLACEMENT_ALIGNMENT - 1); + } + } + + auto uploadBuffer = g_device->createBuffer(RenderBufferDesc::UploadBuffer(curDstOffset)); + uint8_t* mappedMemory = reinterpret_cast(uploadBuffer->map()); + + for (auto& slice : slices) + { + const uint8_t* srcData = data + ddsDesc.headerSize + slice.srcOffset; + uint8_t* dstData = mappedMemory + slice.dstOffset; + + if (slice.srcRowPitch == slice.dstRowPitch) { - for (uint32_t mipSlice = 0; mipSlice < ddsDesc.numMips; mipSlice++) + memcpy(dstData, srcData, slice.srcRowPitch * slice.rowCount * slice.depth); + } + else + { + for (size_t i = 0; i < slice.rowCount * slice.depth; i++) { - auto& slice = slices.emplace_back(); - - slice.width = std::max(1u, ddsDesc.width >> mipSlice); - slice.height = std::max(1u, ddsDesc.height >> mipSlice); - slice.depth = std::max(1u, ddsDesc.depth >> mipSlice); - slice.srcOffset = curSrcOffset; - slice.dstOffset = curDstOffset; - uint32_t rowPitch = ((slice.width + ddsDesc.blockWidth - 1) / ddsDesc.blockWidth) * ddsDesc.bitsPerPixelOrBlock; - slice.srcRowPitch = (rowPitch + 7) / 8; - slice.dstRowPitch = (slice.srcRowPitch + PITCH_ALIGNMENT - 1) & ~(PITCH_ALIGNMENT - 1); - slice.rowCount = (slice.height + ddsDesc.blockHeight - 1) / ddsDesc.blockHeight; - - curSrcOffset += slice.srcRowPitch * slice.rowCount * slice.depth; - curDstOffset += (slice.dstRowPitch * slice.rowCount * slice.depth + PLACEMENT_ALIGNMENT - 1) & ~(PLACEMENT_ALIGNMENT - 1); + memcpy(dstData, srcData, slice.srcRowPitch); + srcData += slice.srcRowPitch; + dstData += slice.dstRowPitch; } } + } - auto uploadBuffer = g_device->createBuffer(RenderBufferDesc::UploadBuffer(curDstOffset)); + uploadBuffer->unmap(); + + ExecuteCopyCommandList([&] + { + g_copyCommandList->barriers(RenderBarrierStage::COPY, RenderTextureBarrier(texture.texture, RenderTextureLayout::COPY_DEST)); + + for (size_t i = 0; i < slices.size(); i++) + { + auto& slice = slices[i]; + + g_copyCommandList->copyTextureRegion( + RenderTextureCopyLocation::Subresource(texture.texture, i), + RenderTextureCopyLocation::PlacedFootprint(uploadBuffer.get(), desc.format, slice.width, slice.height, slice.depth, (slice.dstRowPitch * 8) / ddsDesc.bitsPerPixelOrBlock * ddsDesc.blockWidth, slice.dstOffset)); + } + }); + + return true; + } + else + { + int width, height; + void* stbImage = stbi_load_from_memory(data, dataSize, &width, &height, nullptr, 4); + + if (stbImage != nullptr) + { + texture.textureHolder = g_device->createTexture(RenderTextureDesc::Texture2D(width, height, 1, RenderFormat::R8G8B8A8_UNORM)); + texture.texture = texture.textureHolder.get(); + texture.viewDimension = RenderTextureViewDimension::TEXTURE_2D; + texture.layout = RenderTextureLayout::COPY_DEST; + + texture.descriptorIndex = g_textureDescriptorAllocator.allocate(); + g_textureDescriptorSet->setTexture(texture.descriptorIndex, texture.texture, RenderTextureLayout::SHADER_READ); + + uint32_t rowPitch = (width * 4 + PITCH_ALIGNMENT - 1) & ~(PITCH_ALIGNMENT - 1); + uint32_t slicePitch = rowPitch * height; + + auto uploadBuffer = g_device->createBuffer(RenderBufferDesc::UploadBuffer(slicePitch)); uint8_t* mappedMemory = reinterpret_cast(uploadBuffer->map()); - for (auto& slice : slices) + if (rowPitch == (width * 4)) { - uint8_t* srcData = data + ddsDesc.headerSize + slice.srcOffset; - uint8_t* dstData = mappedMemory + slice.dstOffset; + memcpy(mappedMemory, stbImage, slicePitch); + } + else + { + auto data = reinterpret_cast(stbImage); - if (slice.srcRowPitch == slice.dstRowPitch) + for (size_t i = 0; i < height; i++) { - memcpy(dstData, srcData, slice.srcRowPitch * slice.rowCount * slice.depth); - } - else - { - for (size_t i = 0; i < slice.rowCount * slice.depth; i++) - { - memcpy(dstData, srcData, slice.srcRowPitch); - srcData += slice.srcRowPitch; - dstData += slice.dstRowPitch; - } + memcpy(mappedMemory, data, width * 4); + data += width * 4; + mappedMemory += rowPitch; } } uploadBuffer->unmap(); + stbi_image_free(stbImage); + ExecuteCopyCommandList([&] { - g_copyCommandList->barriers(RenderBarrierStage::COPY, RenderTextureBarrier(texture->texture, RenderTextureLayout::COPY_DEST)); + g_copyCommandList->barriers(RenderBarrierStage::COPY, RenderTextureBarrier(texture.texture, RenderTextureLayout::COPY_DEST)); - for (size_t i = 0; i < slices.size(); i++) - { - auto& slice = slices[i]; - - g_copyCommandList->copyTextureRegion( - RenderTextureCopyLocation::Subresource(texture->texture, i), - RenderTextureCopyLocation::PlacedFootprint(uploadBuffer.get(), desc.format, slice.width, slice.height, slice.depth, (slice.dstRowPitch * 8) / ddsDesc.bitsPerPixelOrBlock * ddsDesc.blockWidth, slice.dstOffset)); - } + g_copyCommandList->copyTextureRegion( + RenderTextureCopyLocation::Subresource(texture.texture, 0), + RenderTextureCopyLocation::PlacedFootprint(uploadBuffer.get(), RenderFormat::R8G8B8A8_UNORM, width, height, 1, rowPitch / 4, 0)); }); - pictureData->texture = g_memory.MapVirtual(texture); - pictureData->type = 0; + return true; } - else + } + + return false; +} + +std::unique_ptr LoadTexture(const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping) +{ + GuestTexture texture(ResourceType::Texture); + + if (LoadTexture(texture, data, dataSize, componentMapping)) + return std::make_unique(std::move(texture)); + + return nullptr; +} + +static void MakePictureData(GuestPictureData* pictureData, uint8_t* data, uint32_t dataSize) +{ + if ((pictureData->flags & 0x1) == 0 && data != nullptr) + { + GuestTexture texture(ResourceType::Texture); + + if (LoadTexture(texture, data, dataSize, {})) { - int width, height; - void* stbImage = stbi_load_from_memory(data, dataSize, &width, &height, nullptr, 4); - - if (stbImage != nullptr) - { - const auto texture = g_userHeap.AllocPhysical(ResourceType::Texture); - texture->textureHolder = g_device->createTexture(RenderTextureDesc::Texture2D(width, height, 1, RenderFormat::R8G8B8A8_UNORM)); - texture->texture = texture->textureHolder.get(); - texture->viewDimension = RenderTextureViewDimension::TEXTURE_2D; - texture->layout = RenderTextureLayout::COPY_DEST; - - texture->descriptorIndex = g_textureDescriptorAllocator.allocate(); - g_textureDescriptorSet->setTexture(texture->descriptorIndex, texture->texture, RenderTextureLayout::SHADER_READ); - - uint32_t rowPitch = (width * 4 + PITCH_ALIGNMENT - 1) & ~(PITCH_ALIGNMENT - 1); - uint32_t slicePitch = rowPitch * height; - - auto uploadBuffer = g_device->createBuffer(RenderBufferDesc::UploadBuffer(slicePitch)); - uint8_t* mappedMemory = reinterpret_cast(uploadBuffer->map()); - - if (rowPitch == (width * 4)) - { - memcpy(mappedMemory, stbImage, slicePitch); - } - else - { - auto data = reinterpret_cast(stbImage); - - for (size_t i = 0; i < height; i++) - { - memcpy(mappedMemory, data, width * 4); - data += width * 4; - mappedMemory += rowPitch; - } - } - - uploadBuffer->unmap(); - - stbi_image_free(stbImage); - - ExecuteCopyCommandList([&] - { - g_copyCommandList->barriers(RenderBarrierStage::COPY, RenderTextureBarrier(texture->texture, RenderTextureLayout::COPY_DEST)); - - g_copyCommandList->copyTextureRegion( - RenderTextureCopyLocation::Subresource(texture->texture, 0), - RenderTextureCopyLocation::PlacedFootprint(uploadBuffer.get(), RenderFormat::R8G8B8A8_UNORM, width, height, 1, rowPitch / 4, 0)); - }); - - pictureData->texture = g_memory.MapVirtual(texture); - pictureData->type = 0; - } +#ifdef _DEBUG + texture.texture->setName(reinterpret_cast(g_memory.Translate(pictureData->name + 2))); +#endif + pictureData->texture = g_memory.MapVirtual(g_userHeap.AllocPhysical(std::move(texture))); + pictureData->type = 0; } } } @@ -4700,7 +4882,7 @@ static void CompileMeshPipeline(Hedgehog::Mirage::CMeshData* mesh, MeshLayer lay pipelineState.vertexStrides[2] = isFur ? 4 : 0; pipelineState.renderTargetFormat = RenderFormat::R16G16B16A16_FLOAT; pipelineState.depthStencilFormat = RenderFormat::D32_FLOAT; - pipelineState.sampleCount = Config::MSAA > 1 ? Config::MSAA : 1; + pipelineState.sampleCount = Config::AntiAliasing != EAntiAliasing::None ? int32_t(Config::AntiAliasing.Value) : 1; if (pipelineState.vertexDeclaration->hasR11G11B10Normal) pipelineState.specConstants |= SPEC_CONSTANT_R11G11B10_NORMAL; @@ -4710,7 +4892,7 @@ static void CompileMeshPipeline(Hedgehog::Mirage::CMeshData* mesh, MeshLayer lay if (layer == MeshLayer::PunchThrough) { - if (Config::MSAA > 1 && Config::AlphaToCoverage) + if (Config::AntiAliasing != EAntiAliasing::None && Config::TransparencyAntiAliasing) { pipelineState.enableAlphaToCoverage = true; pipelineState.specConstants |= SPEC_CONSTANT_ALPHA_TO_COVERAGE; @@ -4870,7 +5052,7 @@ static void CompileParticleMaterialPipeline(const Hedgehog::Sparkle::CParticleMa pipelineState.vertexStrides[0] = isMeshShader ? 104 : 28; pipelineState.renderTargetFormat = RenderFormat::R16G16B16A16_FLOAT; pipelineState.depthStencilFormat = RenderFormat::D32_FLOAT; - pipelineState.sampleCount = Config::MSAA > 1 ? Config::MSAA : 1; + pipelineState.sampleCount = Config::AntiAliasing != EAntiAliasing::None ? int32_t(Config::AntiAliasing.Value) : 1; pipelineState.specConstants = SPEC_CONSTANT_REVERSE_Z; if (pipelineState.vertexDeclaration->hasR11G11B10Normal) @@ -5147,14 +5329,14 @@ static void ModelConsumerThread() pipelineState.specConstants |= SPEC_CONSTANT_BICUBIC_GI_FILTER; // Compile both MSAA and non MSAA variants to work with reflection maps. The render formats are an assumption but it should hold true. - if (Config::MSAA > 1 && + if (Config::AntiAliasing != EAntiAliasing::None && pipelineState.renderTargetFormat == RenderFormat::R16G16B16A16_FLOAT && pipelineState.depthStencilFormat == RenderFormat::D32_FLOAT) { auto msaaPipelineState = pipelineState; - msaaPipelineState.sampleCount = Config::MSAA; + msaaPipelineState.sampleCount = int32_t(Config::AntiAliasing.Value); - if (Config::AlphaToCoverage && (msaaPipelineState.specConstants & SPEC_CONSTANT_ALPHA_TEST) != 0) + if (Config::TransparencyAntiAliasing && (msaaPipelineState.specConstants & SPEC_CONSTANT_ALPHA_TEST) != 0) { msaaPipelineState.enableAlphaToCoverage = true; msaaPipelineState.specConstants &= ~SPEC_CONSTANT_ALPHA_TEST; @@ -5404,6 +5586,15 @@ public: SDLEventListenerForPSOCaching g_sdlEventListenerForPSOCaching; #endif +void VideoConfigValueChangedCallback(IConfigDef* config) +{ + // Config options that require internal resolution resize + g_needsResize |= + config == &Config::ResolutionScale || + config == &Config::AntiAliasing || + config == &Config::ShadowResolution; +} + GUEST_FUNCTION_HOOK(sub_82BD99B0, CreateDevice); GUEST_FUNCTION_HOOK(sub_82BE6230, DestructResource); @@ -5424,7 +5615,7 @@ GUEST_FUNCTION_HOOK(sub_82BE96F0, GetSurfaceDesc); GUEST_FUNCTION_HOOK(sub_82BE04B0, GetVertexDeclaration); GUEST_FUNCTION_HOOK(sub_82BE0530, HashVertexDeclaration); -GUEST_FUNCTION_HOOK(sub_82BDA8C0, Present); +GUEST_FUNCTION_HOOK(sub_82BDA8C0, GuestPresent); GUEST_FUNCTION_HOOK(sub_82BDD330, GetBackBuffer); GUEST_FUNCTION_HOOK(sub_82BE9498, CreateTexture); diff --git a/UnleashedRecomp/gpu/video.h b/UnleashedRecomp/gpu/video.h index 35bedf4..bb636b2 100644 --- a/UnleashedRecomp/gpu/video.h +++ b/UnleashedRecomp/gpu/video.h @@ -10,6 +10,14 @@ using namespace plume; +struct Video +{ + static void CreateHostDevice(); + static void HostPresent(); + static void StartPipelinePrecompilation(); + static void WaitForGPU(); +}; + struct GuestSamplerState { be data[6]; @@ -378,3 +386,7 @@ enum GuestTextureAddress D3DTADDRESS_MIRRORONCE = 3, D3DTADDRESS_BORDER = 6 }; + +extern std::unique_ptr LoadTexture(const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping = RenderComponentMapping()); + +extern void VideoConfigValueChangedCallback(class IConfigDef* config); diff --git a/UnleashedRecomp/hid/driver/sdl_hid.cpp b/UnleashedRecomp/hid/driver/sdl_hid.cpp index bcd8949..ee57f9b 100644 --- a/UnleashedRecomp/hid/driver/sdl_hid.cpp +++ b/UnleashedRecomp/hid/driver/sdl_hid.cpp @@ -1,6 +1,10 @@ #include #include +#include #include +#include + +#define TRANSLATE_INPUT(S, X) SDL_GameControllerGetButton(controller, S) << FirstBitLow(X) #define VIBRATION_TIMEOUT_MS 5000 class Controller @@ -13,17 +17,12 @@ public: XAMINPUT_VIBRATION vibration{ 0, 0 }; Controller() = default; - explicit Controller(int index) : Controller(SDL_GameControllerOpen(index)) - { - - } + explicit Controller(int index) : Controller(SDL_GameControllerOpen(index)) {} Controller(SDL_GameController* controller) : controller(controller) { if (!controller) - { return; - } joystick = SDL_GameControllerGetJoystick(controller); id = SDL_JoystickInstanceID(joystick); @@ -31,20 +30,28 @@ public: void Close() { - if (controller == nullptr) - { + if (!controller) return; - } SDL_GameControllerClose(controller); + controller = nullptr; joystick = nullptr; id = -1; } + bool CanPoll() + { + return controller && (Window::s_isFocused || Config::AllowBackgroundInput); + } + void PollAxis() { + if (!CanPoll()) + return; + auto& pad = state; + pad.sThumbLX = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); pad.sThumbLY = ~SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); @@ -55,14 +62,13 @@ public: pad.bRightTrigger = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7; } - #define TRANSLATE_INPUT(S, X) SDL_GameControllerGetButton(controller, S) << FirstBitLow(X) void Poll() { - if (controller == nullptr) - { + if (!CanPoll()) return; - } + auto& pad = state; + pad.wButtons = 0; pad.wButtons |= TRANSLATE_INPUT(SDL_CONTROLLER_BUTTON_DPAD_UP, XAMINPUT_GAMEPAD_DPAD_UP); @@ -87,12 +93,11 @@ public: void SetVibration(const XAMINPUT_VIBRATION& vibration) { - if (controller == nullptr) - { + if (!CanPoll()) return; - } this->vibration = vibration; + SDL_GameControllerRumble(controller, vibration.wLeftMotorSpeed * 256, vibration.wRightMotorSpeed * 256, VIBRATION_TIMEOUT_MS); } }; @@ -102,9 +107,7 @@ std::array g_controllers; inline Controller* EnsureController(DWORD dwUserIndex) { if (!g_controllers[dwUserIndex].controller) - { return nullptr; - } return &g_controllers[dwUserIndex]; } @@ -114,9 +117,7 @@ inline size_t FindFreeController() for (size_t i = 0; i < g_controllers.size(); i++) { if (!g_controllers[i].controller) - { return i; - } } return -1; @@ -127,9 +128,7 @@ inline Controller* FindController(int which) for (auto& controller : g_controllers) { if (controller.id == which) - { return &controller; - } } return nullptr; @@ -142,22 +141,21 @@ int HID_OnSDLEvent(void*, SDL_Event* event) if (event->type == SDL_CONTROLLERDEVICEADDED) { const auto freeIndex = FindFreeController(); + if (freeIndex != -1) - { g_controllers[freeIndex] = Controller(event->cdevice.which); - } } if (event->type == SDL_CONTROLLERDEVICEREMOVED) { auto* controller = FindController(event->cdevice.which); + if (controller) - { controller->Close(); - } } else if (event->type == SDL_CONTROLLERBUTTONDOWN || event->type == SDL_CONTROLLERBUTTONUP || event->type == SDL_CONTROLLERAXISMOTION) { auto* controller = FindController(event->cdevice.which); + if (controller) { if (event->type == SDL_CONTROLLERAXISMOTION) @@ -181,6 +179,7 @@ void hid::detail::Init() SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "1"); SDL_InitSubSystem(SDL_INIT_EVENTS); @@ -193,62 +192,58 @@ void hid::detail::Init() uint32_t hid::detail::GetState(uint32_t dwUserIndex, XAMINPUT_STATE* pState) { static DWORD packet; + if (!pState) - { return ERROR_BAD_ARGUMENTS; - } memset(pState, 0, sizeof(*pState)); + pState->dwPacketNumber = packet++; SDL_JoystickUpdate(); - auto* controller = EnsureController(dwUserIndex); - if (controller == nullptr) - { + + if (!EnsureController(dwUserIndex)) return ERROR_DEVICE_NOT_CONNECTED; - } pState->Gamepad = g_controllers[dwUserIndex].state; + return ERROR_SUCCESS; } uint32_t hid::detail::SetState(uint32_t dwUserIndex, XAMINPUT_VIBRATION* pVibration) { if (!pVibration) - { return ERROR_BAD_ARGUMENTS; - } SDL_JoystickUpdate(); + auto* controller = EnsureController(dwUserIndex); - if (controller == nullptr) - { + + if (!controller) return ERROR_DEVICE_NOT_CONNECTED; - } controller->SetVibration(*pVibration); + return ERROR_SUCCESS; } uint32_t hid::detail::GetCapabilities(uint32_t dwUserIndex, XAMINPUT_CAPABILITIES* pCaps) { if (!pCaps) - { return ERROR_BAD_ARGUMENTS; - } SDL_JoystickUpdate(); + auto* controller = EnsureController(dwUserIndex); - if (controller == nullptr) - { + + if (!controller) return ERROR_DEVICE_NOT_CONNECTED; - } memset(pCaps, 0, sizeof(*pCaps)); + pCaps->Type = XAMINPUT_DEVTYPE_GAMEPAD; pCaps->SubType = XAMINPUT_DEVSUBTYPE_GAMEPAD; // TODO: other types? pCaps->Flags = 0; - pCaps->Gamepad = controller->state; pCaps->Vibration = controller->vibration; diff --git a/UnleashedRecomp/install/installer.cpp b/UnleashedRecomp/install/installer.cpp index 0058b2e..c28783b 100644 --- a/UnleashedRecomp/install/installer.cpp +++ b/UnleashedRecomp/install/installer.cpp @@ -66,7 +66,7 @@ static std::unique_ptr createFileSystemFromPath(const std::fi } } -static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector &fileData, Journal &journal, const std::function &progressCallback) { +static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector &fileData, Journal &journal, const std::function &progressCallback) { const std::string filename(pair.first); const uint32_t hashCount = pair.second; if (!sourceVfs.exists(filename)) @@ -136,7 +136,8 @@ static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFi return false; } - progressCallback(++journal.progressCounter); + journal.progressCounter += fileData.size(); + progressCallback(); return true; } @@ -200,7 +201,46 @@ bool Installer::checkGameInstall(const std::filesystem::path &baseDirectory) return std::filesystem::exists(baseDirectory / GameDirectory / GameExecutableFile); } -bool Installer::copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback) +bool Installer::checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc) +{ + switch (dlc) + { + case DLC::Spagonia: + return std::filesystem::exists(baseDirectory / SpagoniaDirectory / DLCValidationFile); + case DLC::Chunnan: + return std::filesystem::exists(baseDirectory / ChunnanDirectory / DLCValidationFile); + case DLC::Mazuri: + return std::filesystem::exists(baseDirectory / MazuriDirectory / DLCValidationFile); + case DLC::Holoska: + return std::filesystem::exists(baseDirectory / HoloskaDirectory / DLCValidationFile); + case DLC::ApotosShamar: + return std::filesystem::exists(baseDirectory / ApotosShamarDirectory / DLCValidationFile); + case DLC::EmpireCityAdabat: + return std::filesystem::exists(baseDirectory / EmpireCityAdabatDirectory / DLCValidationFile); + default: + return false; + } +} + +bool Installer::computeTotalSize(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize) +{ + for (FilePair pair : filePairs) + { + const std::string filename(pair.first); + if (!sourceVfs.exists(filename)) + { + journal.lastResult = Journal::Result::FileMissing; + journal.lastErrorMessage = std::format("File {} does not exist in the file system.", filename); + return false; + } + + totalSize += sourceVfs.getSize(filename); + } + + return true; +} + +bool Installer::copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback) { if (!std::filesystem::exists(targetDirectory) && !std::filesystem::create_directories(targetDirectory)) { @@ -265,45 +305,49 @@ bool Installer::parseContent(const std::filesystem::path &sourcePath, std::uniqu } } -bool Installer::install(const Input &input, const std::filesystem::path &targetDirectory, Journal &journal, const std::function &progressCallback) +constexpr uint32_t PatcherContribution = 512 * 1024 * 1024; + +bool Installer::parseSources(const Input &input, Journal &journal, Sources &sources) { + journal = Journal(); + sources = Sources(); + // Parse the contents of the base game. - std::unique_ptr gameSource; if (!input.gameSource.empty()) { - if (!parseContent(input.gameSource, gameSource, journal)) + if (!parseContent(input.gameSource, sources.game, journal)) { return false; } - journal.progressTotal += GameFilesSize; + if (!computeTotalSize({ GameFiles, GameFilesSize }, GameHashes, *sources.game, journal, sources.totalSize)) + { + return false; + } } // Parse the contents of Update. - std::unique_ptr updateSource; if (!input.updateSource.empty()) { - if (!parseContent(input.updateSource, updateSource, journal)) + // Add an arbitrary progress size for the patching process. + journal.progressTotal += PatcherContribution; + + if (!parseContent(input.updateSource, sources.update, journal)) { return false; } - journal.progressTotal += UpdateFilesSize; + if (!computeTotalSize({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *sources.update, journal, sources.totalSize)) + { + return false; + } } // Parse the contents of the DLC Packs. - struct DLCSource { - std::unique_ptr sourceVfs; - std::span filePairs; - const uint64_t *fileHashes = nullptr; - std::string targetSubDirectory; - }; - - std::vector dlcSources; for (const auto &path : input.dlcSources) { - dlcSources.emplace_back(); - DLCSource &dlcSource = dlcSources.back(); + sources.dlc.emplace_back(); + DLCSource &dlcSource = sources.dlc.back(); if (!parseContent(path, dlcSource.sourceVfs, journal)) { return false; @@ -346,17 +390,51 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD return false; } - journal.progressTotal += dlcSource.filePairs.size(); + if (!computeTotalSize(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, journal, sources.totalSize)) + { + return false; + } } - // Install the base game. - if (!copyFiles({ GameFiles, GameFilesSize }, GameHashes, *gameSource, targetDirectory / GameDirectory, GameExecutableFile, input.skipHashChecks, journal, progressCallback)) + // Add the total size in bytes as the journal progress. + journal.progressTotal += sources.totalSize; + + return true; +} + +bool Installer::install(const Sources &sources, const std::filesystem::path &targetDirectory, bool skipHashChecks, Journal &journal, const std::function &progressCallback) +{ + // Install files in reverse order of importance. In case of a process crash or power outage, this will increase the likelihood of the installation + // missing critical files required for the game to run. These files are used as the way to detect if the game is installed. + + // Install the DLC. + if (!sources.dlc.empty()) + { + journal.createdDirectories.insert(targetDirectory / DLCDirectory); + } + + for (const DLCSource &dlcSource : sources.dlc) + { + if (!copyFiles(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, targetDirectory / dlcSource.targetSubDirectory, DLCValidationFile, skipHashChecks, journal, progressCallback)) + { + return false; + } + } + + // If no game or update was specified, we're finished. This means the user was only installing the DLC. + if ((sources.game == nullptr) && (sources.update == nullptr)) + { + return true; + } + + // Install the update. + if (!copyFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *sources.update, targetDirectory / UpdateDirectory, UpdateExecutablePatchFile, skipHashChecks, journal, progressCallback)) { return false; } - // Install the update. - if (!copyFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, *updateSource, targetDirectory / UpdateDirectory, UpdateExecutablePatchFile, input.skipHashChecks, journal, progressCallback)) + // Install the base game. + if (!copyFiles({ GameFiles, GameFilesSize }, GameHashes, *sources.game, targetDirectory / GameDirectory, GameExecutableFile, skipHashChecks, journal, progressCallback)) { return false; } @@ -374,6 +452,10 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD return false; } + // Update the progress with the artificial amount attributed to the patching. + journal.progressCounter += PatcherContribution; + progressCallback(); + // Replace the executable by renaming and deleting in a safe way. std::error_code ec; std::filesystem::path oldXexPath = targetDirectory / GameDirectory / (GameExecutableFile + OldExtension); @@ -396,20 +478,6 @@ bool Installer::install(const Input &input, const std::filesystem::path &targetD std::filesystem::remove(oldXexPath); - // Install the DLC. - if (!dlcSources.empty()) - { - journal.createdDirectories.insert(targetDirectory / DLCDirectory); - } - - for (const DLCSource &dlcSource : dlcSources) - { - if (!copyFiles(dlcSource.filePairs, dlcSource.fileHashes, *dlcSource.sourceVfs, targetDirectory / dlcSource.targetSubDirectory, DLCValidationFile, input.skipHashChecks, journal, progressCallback)) - { - return false; - } - } - return true; } diff --git a/UnleashedRecomp/install/installer.h b/UnleashedRecomp/install/installer.h index 4300614..fae798e 100644 --- a/UnleashedRecomp/install/installer.h +++ b/UnleashedRecomp/install/installer.h @@ -13,7 +13,8 @@ enum class DLC { Mazuri, Holoska, ApotosShamar, - EmpireCityAdabat + EmpireCityAdabat, + Count = EmpireCityAdabat }; struct Journal @@ -35,8 +36,8 @@ struct Journal UnknownDLCType }; - uint32_t progressCounter = 0; - uint32_t progressTotal = 0; + uint64_t progressCounter = 0; + uint64_t progressTotal = 0; std::list createdFiles; std::set createdDirectories; Result lastResult = Result::Success; @@ -53,13 +54,30 @@ struct Installer std::filesystem::path gameSource; std::filesystem::path updateSource; std::list dlcSources; - bool skipHashChecks = false; + }; + + struct DLCSource { + std::unique_ptr sourceVfs; + std::span filePairs; + const uint64_t *fileHashes = nullptr; + std::string targetSubDirectory; + }; + + struct Sources + { + std::unique_ptr game; + std::unique_ptr update; + std::vector dlc; + uint64_t totalSize = 0; }; static bool checkGameInstall(const std::filesystem::path &baseDirectory); - static bool copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback); + static bool checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc); + static bool computeTotalSize(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize); + static bool copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback); static bool parseContent(const std::filesystem::path &sourcePath, std::unique_ptr &targetVfs, Journal &journal); - static bool install(const Input &input, const std::filesystem::path &targetDirectory, Journal &journal, const std::function &progressCallback); + static bool parseSources(const Input &input, Journal &journal, Sources &sources); + static bool install(const Sources &sources, const std::filesystem::path &targetDirectory, bool skipHashChecks, Journal &journal, const std::function &progressCallback); static void rollback(Journal &journal); // Convenience method for checking if the specified file contains the game. This should be used when the user selects the file. diff --git a/UnleashedRecomp/kernel/imports.cpp b/UnleashedRecomp/kernel/imports.cpp index 277cee8..9f83e41 100644 --- a/UnleashedRecomp/kernel/imports.cpp +++ b/UnleashedRecomp/kernel/imports.cpp @@ -11,7 +11,7 @@ #include "xam.h" #include "xdm.h" #include -#include +#include #include diff --git a/UnleashedRecomp/kernel/xam.cpp b/UnleashedRecomp/kernel/xam.cpp index bc9842d..12f61fe 100644 --- a/UnleashedRecomp/kernel/xam.cpp +++ b/UnleashedRecomp/kernel/xam.cpp @@ -8,6 +8,7 @@ #include #include #include "xxHashMap.h" +#include // Needed for commctrl #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") @@ -245,7 +246,7 @@ SWA_API uint32_t XamContentCreateEx(DWORD dwUserIndex, LPCSTR szRootName, const if (pContentData->dwContentType == XCONTENTTYPE_SAVEDATA) { - root = Config::GetSavePath().string(); + root = GetSavePath().string(); } else if (pContentData->dwContentType == XCONTENTTYPE_DLC) { @@ -333,9 +334,6 @@ SWA_API uint32_t XamInputGetState(uint32_t userIndex, uint32_t flags, XAMINPUT_S { //printf("!!! STUB !!! XamInputGetState\n"); - if (!Window::s_isFocused) - return 0; - uint32_t result = hid::GetState(userIndex, state); if (result == ERROR_SUCCESS) @@ -349,6 +347,9 @@ SWA_API uint32_t XamInputGetState(uint32_t userIndex, uint32_t flags, XAMINPUT_S } else if (userIndex == 0) { + if (!Window::s_isFocused) + return ERROR_SUCCESS; + memset(state, 0, sizeof(*state)); if (GetAsyncKeyState('W') & 0x8000) state->Gamepad.wButtons |= XAMINPUT_GAMEPAD_Y; diff --git a/UnleashedRecomp/kernel/xdbf.h b/UnleashedRecomp/kernel/xdbf.h new file mode 100644 index 0000000..7f23976 --- /dev/null +++ b/UnleashedRecomp/kernel/xdbf.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +extern XDBFWrapper g_xdbfWrapper; +extern std::unordered_map g_xdbfTextureCache; diff --git a/UnleashedRecomp/locale/config_locale.h b/UnleashedRecomp/locale/config_locale.h new file mode 100644 index 0000000..5ac29f1 --- /dev/null +++ b/UnleashedRecomp/locale/config_locale.h @@ -0,0 +1,338 @@ +#pragma once + +#include + +#define CONFIG_DEFINE_LOCALE(name) \ + inline static std::unordered_map> g_##name##_locale = + +#define CONFIG_DEFINE_ENUM_LOCALE(type) \ + inline static std::unordered_map>> g_##type##_locale = + +CONFIG_DEFINE_ENUM_LOCALE(bool) +{ + { + ELanguage::English, + { + { true, { "ON", "" } }, + { false, { "OFF", "" } } + } + }, + { + ELanguage::Japanese, + { + { true, { "オン", "" } }, + { false, { "オフ", "" } } + } + }, + { + ELanguage::German, + { + { true, { "EIN", "" } }, + { false, { "AUS", "" } } + } + }, + { + ELanguage::French, + { + { true, { "OUI", "" } }, + { false, { "NON", "" } } + } + }, + { + ELanguage::Spanish, + { + { true, { "SÍ", "" } }, + { false, { "NO", "" } } + } + }, + { + ELanguage::Italian, + { + { true, { "SÌ", "" } }, + { false, { "NO", "" } } + } + } +}; + +CONFIG_DEFINE_LOCALE(Language) +{ + { ELanguage::English, { "Language", "Change the language used for text and logos." } }, + { ELanguage::Japanese, { "言語", "[PLACEHOLDER]" } }, + { ELanguage::German, { "Sprache", "[PLACEHOLDER]" } }, + { ELanguage::French, { "Langue", "[PLACEHOLDER]" } }, + { ELanguage::Spanish, { "Idioma", "[PLACEHOLDER]" } }, + { ELanguage::Italian, { "Lingua", "[PLACEHOLDER]" } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(ELanguage) +{ + { + ELanguage::English, + { + { ELanguage::English, { "ENGLISH", "" } }, + { ELanguage::Japanese, { "日本語", "" } }, + { ELanguage::German, { "DEUTSCH", "" } }, + { ELanguage::French, { "FRANÇAIS", "" } }, + { ELanguage::Spanish, { "ESPAÑOL", "" } }, + { ELanguage::Italian, { "ITALIANO", "" } } + } + } +}; + +CONFIG_DEFINE_LOCALE(Hints) +{ + { ELanguage::English, { "Hints", "Show hint rings in stages." } } +}; + +CONFIG_DEFINE_LOCALE(ControlTutorial) +{ + { ELanguage::English, { "Control Tutorial", "Show controller hints in stages." } } +}; + +CONFIG_DEFINE_LOCALE(AchievementNotifications) +{ + { ELanguage::English, { "Achievement Notifications", "Show notifications for unlocking achievements.\n\nAchievements will still be rewarded with notifications disabled." } } +}; + +CONFIG_DEFINE_LOCALE(SaveScoreAtCheckpoints) +{ + { ELanguage::English, { "Save Score at Checkpoints", "Keep your score from the last checkpoint upon respawning.\n\n[TO BE REMOVED]" } } +}; + +CONFIG_DEFINE_LOCALE(UnleashGaugeBehaviour) +{ + { ELanguage::English, { "Unleash Gauge Behavior", "Change how the Unleash gauge behaves.\n\n[TO BE REMOVED]" } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(EUnleashGaugeBehaviour) +{ + { + ELanguage::English, + { + { EUnleashGaugeBehaviour::Original, { "ORIGINAL", "Original: the gauge will drain at all times regardless." } }, + { EUnleashGaugeBehaviour::Revised, { "REVISED", "Revised: the gauge will only drain when the player can move." } } + } + } +}; + +CONFIG_DEFINE_LOCALE(TimeOfDayTransition) +{ + { ELanguage::English, { "Time of Day Transition", "Change how the loading screen appears when switching time of day in the hub areas." } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(ETimeOfDayTransition) +{ + { + ELanguage::English, + { + { ETimeOfDayTransition::Xbox, { "XBOX", "Xbox: the transformation cutscene will play with artificial loading times." } }, + { ETimeOfDayTransition::PlayStation, { "PLAYSTATION", "PlayStation: a spinning medal loading screen will be used instead." } } + } + } +}; + +CONFIG_DEFINE_LOCALE(SkipIntroLogos) +{ + { ELanguage::English, { "Skip Intro Logos", "Skip the logos during the game's boot sequence.\n\n[TO BE REMOVED]" } } +}; + +CONFIG_DEFINE_LOCALE(InvertCameraX) +{ + { ELanguage::English, { "Invert Camera X", "Toggle between inverted left and right camera movement." } } +}; + +CONFIG_DEFINE_LOCALE(InvertCameraY) +{ + { ELanguage::English, { "Invert Camera Y", "Toggle between inverted up and down camera movement." } } +}; + +CONFIG_DEFINE_LOCALE(XButtonHoming) +{ + { ELanguage::English, { "Homing Attack on Boost", "Toggle between using the boost button or the jump button for the homing attack.\n\n[TO BE REMOVED]" } } +}; + +CONFIG_DEFINE_LOCALE(AllowCancellingUnleash) +{ + { ELanguage::English, { "Allow Cancelling Unleash", "Allow Unleash to be cancelled at the cost of some energy by pressing the input again.\n\n[TO BE REMOVED]" } } +}; + +CONFIG_DEFINE_LOCALE(AllowBackgroundInput) +{ + { ELanguage::English, { "Allow Background Input", "Accept controller input whilst the game window is unfocused." } } +}; + +CONFIG_DEFINE_LOCALE(MusicVolume) +{ + { ELanguage::English, { "Music Volume", "Adjust the volume for the music." } } +}; + +CONFIG_DEFINE_LOCALE(EffectsVolume) +{ + { ELanguage::English, { "Effects Volume", "Adjust the volume for sound effects." } } +}; + +CONFIG_DEFINE_LOCALE(MusicAttenuation) +{ + { ELanguage::English, { "Music Attenuation", "Fade out the game's music when external media is playing." } } +}; + +CONFIG_DEFINE_LOCALE(VoiceLanguage) +{ + { ELanguage::English, { "Voice Language", "Change the language used for character voices." } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(EVoiceLanguage) +{ + { + ELanguage::English, + { + { EVoiceLanguage::English, { "ENGLISH", "" } }, + { EVoiceLanguage::Japanese, { "日本語", "" } } + } + } +}; + +CONFIG_DEFINE_LOCALE(Subtitles) +{ + { ELanguage::English, { "Subtitles", "Show subtitles during dialogue." } } +}; + +CONFIG_DEFINE_LOCALE(BattleTheme) +{ + { ELanguage::English, { "Battle Theme", "Play the Werehog battle theme during combat.\n\nThis option will apply the next time you're in combat." } } +}; + +CONFIG_DEFINE_LOCALE(AspectRatio) +{ + { ELanguage::English, { "Aspect Ratio", "Change the aspect ratio." } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(EAspectRatio) +{ + { + ELanguage::English, + { + { EAspectRatio::Auto, { "AUTO", "Auto: the aspect ratio will dynamically adjust to the window size." } } + } + } +}; + +CONFIG_DEFINE_LOCALE(ResolutionScale) +{ + { ELanguage::English, { "Resolution Scale", "Adjust the internal resolution of the game.\n\n%dx%d" } } +}; + +CONFIG_DEFINE_LOCALE(Fullscreen) +{ + { ELanguage::English, { "Fullscreen", "Toggle between borderless fullscreen or windowed mode." } } +}; + +CONFIG_DEFINE_LOCALE(VSync) +{ + { ELanguage::English, { "V-Sync", "[PLACEHOLDER]" } } +}; + +CONFIG_DEFINE_LOCALE(FPS) +{ + { ELanguage::English, { "FPS", "[PLACEHOLDER]" } } +}; + +CONFIG_DEFINE_LOCALE(Brightness) +{ + { ELanguage::English, { "Brightness", "Adjust the brightness level of the game." } } +}; + +CONFIG_DEFINE_LOCALE(AntiAliasing) +{ + { ELanguage::English, { "Anti-Aliasing", "Adjust the amount of smoothing applied to jagged edges." } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(EAntiAliasing) +{ + { + ELanguage::English, + { + { EAntiAliasing::None, { "NONE", "" } }, + } + } +}; + +CONFIG_DEFINE_LOCALE(TransparencyAntiAliasing) +{ + { ELanguage::English, { "Transparency Anti-Aliasing", "Apply anti-aliasing to alpha transparent textures." } } +}; + +CONFIG_DEFINE_LOCALE(ShadowResolution) +{ + { ELanguage::English, { "Shadow Resolution", "[PLACEHOLDER]" } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(EShadowResolution) +{ + { + ELanguage::English, + { + { EShadowResolution::Original, { "ORIGINAL", "Original: the game will automatically determine the resolution of the shadows." } }, + } + } +}; + +CONFIG_DEFINE_LOCALE(GITextureFiltering) +{ + { ELanguage::English, { "GI Texture Filtering", "[PLACEHOLDER]" } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(EGITextureFiltering) +{ + { + ELanguage::English, + { + { EGITextureFiltering::Bilinear, { "BILINEAR", "" } }, + { EGITextureFiltering::Bicubic, { "BICUBIC", "" } }, + } + } +}; + +CONFIG_DEFINE_LOCALE(MotionBlur) +{ + { ELanguage::English, { "Motion Blur", "Use per-object motion blur and radial blur." } } +}; + +CONFIG_DEFINE_LOCALE(XboxColourCorrection) +{ + { ELanguage::English, { "Xbox Color Correction", "Use the warm tint from the Xbox version of the game." } } +}; + +CONFIG_DEFINE_LOCALE(MovieScaleMode) +{ + { ELanguage::English, { "Movie Scale Mode", "Change how the movie player scales to the display." } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(EMovieScaleMode) +{ + { + ELanguage::English, + { + { EMovieScaleMode::Stretch, { "STRETCH", "Stretch: the movie will stretch to the display." } }, + { EMovieScaleMode::Fit, { "FIT", "Fit: the movie will maintain its aspect ratio and fit to the display." } }, + { EMovieScaleMode::Fill, { "FILL", "Fill: the movie will scale past the bounds of the display if it doesn't match the aspect ratio." } }, + } + } +}; + +CONFIG_DEFINE_LOCALE(UIScaleMode) +{ + { ELanguage::English, { "UI Scale Mode", "Change how the UI scales to the display." } } +}; + +CONFIG_DEFINE_ENUM_LOCALE(EUIScaleMode) +{ + { + ELanguage::English, + { + { EUIScaleMode::Stretch, { "STRETCH", "Stretch: the UI will stretch to the display." } }, + { EUIScaleMode::Edge, { "EDGE", "Edge: the UI will anchor to the edges of the display." } }, + { EUIScaleMode::Centre, { "CENTER", "Center: the UI will anchor to the center of the display." } }, + } + } +}; diff --git a/UnleashedRecomp/locale/locale.h b/UnleashedRecomp/locale/locale.h new file mode 100644 index 0000000..90b3d36 --- /dev/null +++ b/UnleashedRecomp/locale/locale.h @@ -0,0 +1,279 @@ +#pragma once + +#include + +inline static std::string g_localeMissing = ""; + +inline static std::unordered_map> g_locale +{ + { + "Options_Category_System", + { + { ELanguage::English, "SYSTEM" } + } + }, + { + "Options_Category_Input", + { + { ELanguage::English, "INPUT" } + } + }, + { + "Options_Category_Audio", + { + { ELanguage::English, "AUDIO" } + } + }, + { + "Options_Category_Video", + { + { ELanguage::English, "VIDEO" } + } + }, + { + "Options_Value_Max", + { + { ELanguage::English, "MAX" } + } + }, + { + "Options_Name_WindowSize", + { + { ELanguage::English, "Window Size" } + } + }, + { + "Options_Desc_WindowSize", + { + { ELanguage::English, "Adjust the size of the game window in windowed mode." } + } + }, + { + "Options_Desc_NotAvailable", + { + { ELanguage::English, "This option is not available at this location." } + } + }, + { + "Options_Desc_NotAvailableMSAA", + { + { ELanguage::English, "This option is not available without MSAA." } + } + }, + { + "Options_Desc_OSNotSupported", + { + { ELanguage::English, "This option is not supported by your operating system." } + } + }, + { + "Achievements_Name", + { + { ELanguage::English, "Achievements" } + } + }, + { + "Achievements_Name_Uppercase", + { + { ELanguage::English, "ACHIEVEMENTS" } + } + }, + { + "Achievements_Unlock", + { + { ELanguage::English, "Achievement Unlocked!" } + } + }, + { + "Installer_Header_Installer", + { + { ELanguage::English, "INSTALLER" }, + { ELanguage::Spanish, "INSTALADOR" }, + }, + }, + { + "Installer_Header_Installing", + { + { ELanguage::English, "INSTALLING" }, + { ELanguage::Spanish, "INSTALANDO" }, + } + }, + { + "Installer_Page_SelectLanguage", + { + { ELanguage::English, "Please select a language." } + } + }, + { + "Installer_Page_Introduction", + { + { ELanguage::English, "Welcome to Unleashed Recompiled!\n\nYou'll need an Xbox 360 copy\nof Sonic Unleashed in order to proceed with the installation." } + } + }, + { + "Installer_Page_SelectGameAndUpdate", + { + { ELanguage::English, "Add the files for the game and its title update. You can use digital dumps or pre-extracted folders containing the necessary files." } + } + }, + { + "Installer_Page_SelectDLC", + { + { ELanguage::English, "Add the files for the DLC. You can use digital dumps or pre-extracted folders containing the necessary files." } + } + }, + { + "Installer_Page_CheckSpace", + { + { ELanguage::English, "The content will be installed to the program's folder. Please confirm you have enough free space.\n\n" } + } + }, + { + "Installer_Page_Installing", + { + { ELanguage::English, "Please wait while the content is being installed..." } + } + }, + { + "Installer_Page_InstallSucceeded", + { + { ELanguage::English, "Installation complete.\n\nThis project is brought to you by:\n\n" } + } + }, + { + "Installer_Page_InstallFailed", + { + { ELanguage::English, "Installation failed.\n\nError:\n\n" } + } + }, + { + "Installer_Step_Game", + { + { ELanguage::English, "GAME" } + } + }, + { + "Installer_Step_Update", + { + { ELanguage::English, "UPDATE" } + } + }, + { + "Installer_Step_RequiredSpace", + { + { ELanguage::English, "Required space:" } + } + }, + { + "Installer_Step_AvailableSpace", + { + { ELanguage::English, "Available space:" } + } + }, + { + "Installer_Button_Next", + { + { ELanguage::English, "NEXT" }, + { ELanguage::Spanish, "SIGUIENTE" }, + { ELanguage::German, "WEITER" }, + } + }, + { + "Installer_Button_Skip", + { + { ELanguage::English, "SKIP" }, + { ELanguage::Spanish, "SALTAR" }, + { ELanguage::German, "ÜBERSPRINGEN" }, + } + }, + { + "Installer_Button_AddFiles", + { + { ELanguage::English, "ADD FILES" }, + { ELanguage::Spanish, "AÑADIR ARCHIVOS" }, + { ELanguage::German, "DATEIEN HINZUFÜGEN" }, + } + }, + { + "Installer_Button_AddFolder", + { + { ELanguage::English, "ADD FOLDER" }, + { ELanguage::Spanish, "AÑADIR CARPETA" }, + { ELanguage::German, "ORDNER HINZUFÜGEN" }, + } + }, + { + "Installer_Message_InvalidFilesList", + { + { ELanguage::English, "The following selected files are invalid:" } + } + }, + { + "Installer_Message_InvalidFiles", + { + { ELanguage::English, "Some of the files that have\nbeen provided are not valid.\n\nPlease make sure all the\nspecified files are correct\nand try again." } + } + }, + { + "Installer_Message_IncompatibleGameData", + { + { ELanguage::English, "The specified game and\nupdate file are incompatible.\n\nPlease ensure the files are\nfor the same version and\nregion and try again." } + } + }, + { + "Installer_Message_DLCWarning", + { + { ELanguage::English, "It is highly recommended\nthat you install all of the\nDLC, as it includes high\nquality lighting textures\nfor the base game.\n\nAre you sure you want to\nskip this step?" } + } + }, + { + "Common_Next", + { + { ELanguage::English, "Next" } + } + }, + { + "Common_Select", + { + { ELanguage::English, "Select" } + } + }, + { + "Common_Back", + { + { ELanguage::English, "Back" } + } + }, + { + "Common_Reset", + { + { ELanguage::English, "Reset" } + } + }, + { + "Common_Switch", + { + { ELanguage::English, "Switch" } + } + } +}; + +static std::string& Localise(const char* key) +{ + if (!g_locale.count(key)) + return g_localeMissing; + + if (!g_locale[key].count(Config::Language)) + { + if (g_locale[key].count(ELanguage::English)) + { + return g_locale[key][ELanguage::English]; + } + else + { + return g_localeMissing; + } + } + + return g_locale[key][Config::Language]; +} diff --git a/UnleashedRecomp/main.cpp b/UnleashedRecomp/main.cpp index 7ac1e4f..42b394f 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -11,8 +11,12 @@ #include #include #include -#include +#include +#include +#include +#include #include +#include #define GAME_XEX_PATH "game:\\default.xex" @@ -22,9 +26,10 @@ const size_t XMAIOEnd = XMAIOBegin + 0x0000FFFF; Memory g_memory{ reinterpret_cast(0x100000000), 0x100000000 }; Heap g_userHeap; CodeCache g_codeCache; +XDBFWrapper g_xdbfWrapper; +std::unordered_map g_xdbfTextureCache; -// Name inspired from nt's entry point -void KiSystemStartup() +void HostStartup() { #ifdef _WIN32 CoInitializeEx(nullptr, COINIT_MULTITHREADED); @@ -36,12 +41,18 @@ void KiSystemStartup() g_memory.Alloc(XMAIOBegin, 0xFFFF, MEM_COMMIT); + hid::Init(); +} + +// Name inspired from nt's entry point +void KiSystemStartup() +{ const auto gameContent = XamMakeContent(XCONTENTTYPE_RESERVED, "Game"); const auto updateContent = XamMakeContent(XCONTENTTYPE_RESERVED, "Update"); XamRegisterContent(gameContent, DirectoryExists(".\\game") ? ".\\game" : "."); XamRegisterContent(updateContent, ".\\update"); - const auto savePath = Config::GetSavePath(); + const auto savePath = GetSavePath(); const auto saveName = "SYS-DATA"; // TODO: implement save slots? @@ -77,7 +88,6 @@ void KiSystemStartup() } XAudioInitializeSystem(); - hid::Init(); } uint32_t LdrLoadModule(const char* path) @@ -125,17 +135,46 @@ uint32_t LdrLoadModule(const char* path) assert(false && "Unknown compression type."); } + auto res = Xex2FindOptionalHeader(xex, XEX_HEADER_RESOURCE_INFO); + + g_xdbfWrapper = XDBFWrapper((uint8_t*)g_memory.Translate(res->Offset.get()), res->SizeOfData); + return entry; } -int main() +int main(int argc, char *argv[]) { + bool forceInstaller = false; + bool forceDLCInstaller = false; + for (uint32_t i = 1; i < argc; i++) + { + forceInstaller = forceInstaller || (strcmp(argv[i], "--install") == 0); + forceDLCInstaller = forceDLCInstaller || (strcmp(argv[i], "--install-dlc") == 0); + } + Config::Load(); + HostStartup(); + + Video::CreateHostDevice(); + + bool isGameInstalled = Installer::checkGameInstall("."); + if (forceInstaller || forceDLCInstaller || !isGameInstalled) + { + if (!InstallerWizard::Run(isGameInstalled && forceDLCInstaller)) + { + return 1; + } + } + + AchievementData::Load(); + KiSystemStartup(); uint32_t entry = LdrLoadModule(FileSystem::TransformPath(GAME_XEX_PATH)); + Video::StartPipelinePrecompilation(); + GuestThread::Start(entry); return 0; diff --git a/UnleashedRecomp/patches/audio_patches.cpp b/UnleashedRecomp/patches/audio_patches.cpp new file mode 100644 index 0000000..e42d37a --- /dev/null +++ b/UnleashedRecomp/patches/audio_patches.cpp @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include + +#if _WIN32 +#include +#include + +using namespace winrt; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Media::Control; + +GlobalSystemMediaTransportControlsSessionManager m_sessionManager = nullptr; + +GlobalSystemMediaTransportControlsSessionManager GetSessionManager() +{ + if (m_sessionManager) + return m_sessionManager; + + init_apartment(); + + return m_sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); +} + +GlobalSystemMediaTransportControlsSession GetCurrentSession() +{ + return GetSessionManager().GetCurrentSession(); +} + +bool IsExternalAudioPlaying() +{ + auto session = GetCurrentSession(); + + if (!session) + return false; + + return session.GetPlaybackInfo().PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing; +} + +int AudioPatches::m_isAttenuationSupported = -1; +#endif + +be* GetVolume(bool isMusic = true) +{ + auto ppUnkClass = (be*)g_memory.Translate(0x83362FFC); + + if (!ppUnkClass->get()) + return nullptr; + + // NOTE (Hyper): This is fine, trust me. See 0x82E58728. + return (be*)g_memory.Translate(4 * ((int)isMusic + 0x1C) + ((be*)g_memory.Translate(ppUnkClass->get() + 4))->get()); +} + +bool AudioPatches::CanAttenuate() +{ +#if _WIN32 + if (m_isAttenuationSupported >= 0) + return m_isAttenuationSupported; + + auto version = GetPlatformVersion(); + + m_isAttenuationSupported = version.Major >= 10 && version.Build >= 17763; + + return m_isAttenuationSupported; +#else + return false; +#endif +} + +void AudioPatches::Update(float deltaTime) +{ + auto pMusicVolume = GetVolume(); + auto pEffectsVolume = GetVolume(false); + + if (!pMusicVolume || !pEffectsVolume) + return; + +#if _WIN32 + if (Config::MusicAttenuation && CanAttenuate()) + { + auto time = 1.0f - expf(2.5f * -deltaTime); + + if (IsExternalAudioPlaying()) + { + *pMusicVolume = std::lerp(*pMusicVolume, 0.0f, time); + } + else + { + *pMusicVolume = std::lerp(*pMusicVolume, Config::MusicVolume, time); + } + } + else +#endif + { + *pMusicVolume = Config::MusicVolume; + } + + *pEffectsVolume = Config::EffectsVolume; +} + +// Stub volume setter. +GUEST_FUNCTION_STUB(sub_82E58728); diff --git a/UnleashedRecomp/patches/audio_patches.h b/UnleashedRecomp/patches/audio_patches.h new file mode 100644 index 0000000..e28ec90 --- /dev/null +++ b/UnleashedRecomp/patches/audio_patches.h @@ -0,0 +1,11 @@ +#pragma once + +class AudioPatches +{ +protected: + static int m_isAttenuationSupported; + +public: + static bool CanAttenuate(); + static void Update(float deltaTime); +}; diff --git a/UnleashedRecomp/patches/camera_patches.cpp b/UnleashedRecomp/patches/camera_patches.cpp new file mode 100644 index 0000000..9b70e52 --- /dev/null +++ b/UnleashedRecomp/patches/camera_patches.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include + +constexpr float m_baseAspectRatio = 16.0f / 9.0f; + +bool CameraAspectRatioMidAsmHook(PPCRegister& r31) +{ + auto pCamera = (SWA::CCamera*)g_memory.Translate(r31.u32); + auto newAspectRatio = (float)Window::s_width / (float)Window::s_height; + + // Dynamically adjust horizontal aspect ratio to window dimensions. + pCamera->m_HorzAspectRatio = newAspectRatio; + + if (auto s_pVertAspectRatio = (be*)g_memory.Translate(0x82028FE0)) + { + // Dynamically adjust vertical aspect ratio for VERT+. + *s_pVertAspectRatio = 2.0f * atan(tan(45.0f / 2.0f) * (m_baseAspectRatio / newAspectRatio)); + } + + // Jump to 4:3 code for VERT+ adjustments if using a narrow aspect ratio. + return newAspectRatio < m_baseAspectRatio; +} + +bool CameraBoostAspectRatioMidAsmHook(PPCRegister& r31, PPCRegister& f0, PPCRegister& f10, PPCRegister& f12) +{ + auto pCamera = (SWA::CCamera*)g_memory.Translate(r31.u32); + + if (Window::s_width < Window::s_height) + { + pCamera->m_VertFieldOfView = pCamera->m_HorzFieldOfView + f10.f64; + } + else + { + pCamera->m_VertFieldOfView = (f12.f64 / f0.f64) + f10.f64; + } + + return true; +} + +PPC_FUNC_IMPL(__imp__sub_824697B0); +PPC_FUNC(sub_824697B0) +{ + auto pCamera = (SWA::CCamera*)g_memory.Translate(ctx.r3.u32); + + pCamera->m_InvertX = Config::InvertCameraX; + pCamera->m_InvertY = Config::InvertCameraY; + + __imp__sub_824697B0(ctx, base); +} diff --git a/UnleashedRecomp/patches/fps_patches.cpp b/UnleashedRecomp/patches/fps_patches.cpp index 1ad63b1..289fea5 100644 --- a/UnleashedRecomp/patches/fps_patches.cpp +++ b/UnleashedRecomp/patches/fps_patches.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include float m_lastLoadingFrameDelta = 0.0f; diff --git a/UnleashedRecomp/patches/misc_patches.cpp b/UnleashedRecomp/patches/misc_patches.cpp index 1caac4b..3c69e3b 100644 --- a/UnleashedRecomp/patches/misc_patches.cpp +++ b/UnleashedRecomp/patches/misc_patches.cpp @@ -1,7 +1,13 @@ #include #include #include -#include +#include +#include + +void AchievementManagerUnlockMidAsmHook(PPCRegister& id) +{ + AchievementData::Unlock(id.u32); +} bool DisableHintsMidAsmHook() { @@ -22,9 +28,16 @@ bool DisableEvilControlTutorialMidAsmHook(PPCRegister& r4, PPCRegister& r5) return r4.u32 == 1 && r5.u32 == 1; } +void ToggleSubtitlesMidAsmHook(PPCRegister& r27) +{ + auto pApplicationDocument = (SWA::CApplicationDocument*)g_memory.Translate(r27.u32); + + pApplicationDocument->m_InspireSubtitles = Config::Subtitles; +} + void WerehogBattleMusicMidAsmHook(PPCRegister& r11) { - if (Config::WerehogBattleMusic) + if (Config::BattleTheme) return; // Swap CStateBattle for CStateNormal. @@ -53,7 +66,7 @@ PPC_FUNC(sub_825197C0) PPC_FUNC_IMPL(__imp__sub_82547DF0); PPC_FUNC(sub_82547DF0) { - if (Config::LogoSkip) + if (Config::SkipIntroLogos) { ctx.r4.u64 = 0; ctx.r5.u64 = 0; diff --git a/UnleashedRecomp/patches/player_patches.cpp b/UnleashedRecomp/patches/player_patches.cpp index 6fe6bda..8003ae3 100644 --- a/UnleashedRecomp/patches/player_patches.cpp +++ b/UnleashedRecomp/patches/player_patches.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include uint32_t m_lastCheckpointScore = 0; float m_lastDarkGaiaEnergy = 0.0f; @@ -15,7 +15,7 @@ PPC_FUNC(sub_82624308) { __imp__sub_82624308(ctx, base); - if (Config::ScoreBehaviour != EScoreBehaviour::CheckpointRetain) + if (!Config::SaveScoreAtCheckpoints) return; auto pGameDocument = SWA::CGameDocument::GetInstance(); @@ -24,6 +24,8 @@ PPC_FUNC(sub_82624308) return; m_lastCheckpointScore = pGameDocument->m_pMember->m_Score; + + printf("[*] Score: %d\n", m_lastCheckpointScore); } /* Hook function that resets the score @@ -33,7 +35,7 @@ PPC_FUNC(sub_8245F048) { __imp__sub_8245F048(ctx, base); - if (Config::ScoreBehaviour != EScoreBehaviour::CheckpointRetain) + if (!Config::SaveScoreAtCheckpoints) return; auto pGameDocument = SWA::CGameDocument::GetInstance(); @@ -41,7 +43,7 @@ PPC_FUNC(sub_8245F048) if (!pGameDocument) return; - printf("[*] Resetting score to %d\n", m_lastCheckpointScore); + printf("[*] Score: %d\n", m_lastCheckpointScore); pGameDocument->m_pMember->m_Score = m_lastCheckpointScore; } @@ -60,12 +62,12 @@ PPC_FUNC(sub_823AF7A8) m_lastDarkGaiaEnergy = pEvilSonicContext->m_DarkGaiaEnergy; // Don't drain energy if out of control. - if (!Config::UnleashOutOfControlDrain && pEvilSonicContext->m_OutOfControlCount && ctx.f1.f64 < 0.0) + if (Config::UnleashGaugeBehaviour == EUnleashGaugeBehaviour::Revised && pEvilSonicContext->m_OutOfControlCount && ctx.f1.f64 < 0.0) return; __imp__sub_823AF7A8(ctx, base); - if (!Config::UnleashCancel) + if (!Config::AllowCancellingUnleash) return; auto pInputState = SWA::CInputState::GetInstance(); @@ -86,7 +88,7 @@ void PostUnleashMidAsmHook(PPCRegister& r30) if (m_isUnleashCancelled) { if (auto pEvilSonicContext = (SWA::Player::CEvilSonicContext*)g_memory.Translate(r30.u32)) - pEvilSonicContext->m_DarkGaiaEnergy = m_lastDarkGaiaEnergy; + pEvilSonicContext->m_DarkGaiaEnergy = std::max(0.0f, m_lastDarkGaiaEnergy - 35.0f); m_isUnleashCancelled = false; } diff --git a/UnleashedRecomp/patches/resident_patches.cpp b/UnleashedRecomp/patches/resident_patches.cpp index f1606b6..94efda9 100644 --- a/UnleashedRecomp/patches/resident_patches.cpp +++ b/UnleashedRecomp/patches/resident_patches.cpp @@ -1,8 +1,12 @@ #include -#include +#include +#include +#include const char* m_pStageID; +bool m_isSavedAchievementData = false; + void GetStageIDMidAsmHook(PPCRegister& r5) { m_pStageID = *(xpointer*)g_memory.Translate(r5.u32); @@ -12,43 +16,67 @@ void GetStageIDMidAsmHook(PPCRegister& r5) PPC_FUNC_IMPL(__imp__sub_824DCF38); PPC_FUNC(sub_824DCF38) { - /* Force the Werehog transition ID - to use a different transition. */ - if (!Config::WerehogHubTransformVideo) + // TODO: use the actual PS3 loading screen ("n_2_d"). + if (Config::TimeOfDayTransition == ETimeOfDayTransition::PlayStation) { - /* - 0 - Tails Electric NOW LOADING - 1 - No Transition - 2 - Werehog Transition - 3 - Tails Electric NOW LOADING w/ Info (requires context) - 4 - Arrows In/Out - 5 - NOW LOADING - 6 - Event Gallery - 7 - NOW LOADING - 8 - Black Screen - */ - if (ctx.r4.u32 == 2) - ctx.r4.u32 = 4; + if (ctx.r4.u32 == SWA::eLoadingDisplayType_WerehogMovie) + ctx.r4.u32 = SWA::eLoadingDisplayType_Arrows; } if (m_pStageID) { /* Fix restarting Eggmanland as the Werehog erroneously using the Event Gallery transition. */ - if (ctx.r4.u32 == 6 && !strcmp(m_pStageID, "Act_EggmanLand")) - ctx.r4.u32 = 5; + if (ctx.r4.u32 == SWA::eLoadingDisplayType_EventGallery && !strcmp(m_pStageID, "Act_EggmanLand")) + ctx.r4.u32 = SWA::eLoadingDisplayType_NowLoading; } __imp__sub_824DCF38(ctx, base); } -// CApplicationDocument::LoadArchiveDatabases +// Load voice language files. +PPC_FUNC_IMPL(__imp__sub_824EB9B0); +PPC_FUNC(sub_824EB9B0) +{ + auto pApplicationDocument = (SWA::CApplicationDocument*)g_memory.Translate(ctx.r4.u32); + + pApplicationDocument->m_VoiceLanguage = (SWA::EVoiceLanguage)Config::VoiceLanguage.Value; + + __imp__sub_824EB9B0(ctx, base); +} + +// SWA::CSaveIcon::Update +PPC_FUNC_IMPL(__imp__sub_824E5170); +PPC_FUNC(sub_824E5170) +{ + auto pSaveIcon = (SWA::CSaveIcon*)g_memory.Translate(ctx.r3.u32); + + __imp__sub_824E5170(ctx, base); + + if (pSaveIcon->m_IsVisible) + { + if (!m_isSavedAchievementData) + { + printf("[*] Saving achievements...\n"); + + AchievementData::Save(); + + m_isSavedAchievementData = true; + } + } + else + { + m_isSavedAchievementData = false; + } +} + +// SWA::CApplicationDocument::LoadArchiveDatabases PPC_FUNC_IMPL(__imp__sub_824EFD28); PPC_FUNC(sub_824EFD28) { auto r3 = ctx.r3; - // CSigninXenon::InitializeDLC + // SWA::CSigninXenon::InitializeDLC ctx.r3.u64 = PPC_LOAD_U32(r3.u32 + 4) + 200; ctx.r4.u64 = 0; sub_822C57D8(ctx, base); @@ -57,7 +85,7 @@ PPC_FUNC(sub_824EFD28) __imp__sub_824EFD28(ctx, base); } -// CFileReaderXenon_DLC::InitializeParallel +// SWA::CFileReaderXenon_DLC::InitializeParallel PPC_FUNC(sub_822C3778) { if (!PPC_LOAD_U8(0x83361F10)) // ms_DLCInitialized diff --git a/UnleashedRecomp/patches/ui/CHudPause_patches.cpp b/UnleashedRecomp/patches/ui/CHudPause_patches.cpp new file mode 100644 index 0000000..97948d2 --- /dev/null +++ b/UnleashedRecomp/patches/ui/CHudPause_patches.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include +#include +#include + +float g_achievementMenuIntroTime = 0.0f; +constexpr float g_achievementMenuIntroThreshold = 3.0f; +float g_achievementMenuOutroTime = 0.0f; +constexpr float g_achievementMenuOutroThreshold = 0.32f; +bool g_isAchievementMenuOutro = false; + +void CHudPauseAddOptionsItemMidAsmHook(PPCRegister& pThis) +{ + guest_stack_var menu("TopMenu"); + guest_stack_var name("option"); + + GuestToHostFunction(0x824AE690, pThis.u32, menu.get(), name.get()); +} + +bool InjectMenuBehaviour(uint32_t pThis, uint32_t count) +{ + auto pHudPause = (SWA::CHudPause*)g_memory.Translate(pThis); + auto cursorIndex = *(be*)g_memory.Translate(4 * (*(be*)g_memory.Translate(pThis + 0x19C) + 0x68) + pThis); + + auto actionType = SWA::eActionType_Undefined; + auto transitionType = SWA::eTransitionType_Undefined; + + switch (pHudPause->m_Menu) + { + case SWA::eMenuType_WorldMap: + case SWA::eMenuType_Stage: + case SWA::eMenuType_Misc: + actionType = SWA::eActionType_Return; + transitionType = SWA::eTransitionType_Quit; + break; + + case SWA::eMenuType_Village: + case SWA::eMenuType_Hub: + actionType = SWA::eActionType_Return; + transitionType = SWA::eTransitionType_Hide; + break; + } + + if (auto pInputState = SWA::CInputState::GetInstance()) + { + if (pInputState->GetPadState().IsTapped(SWA::eKeyState_Select)) + { + AchievementMenu::Open(); + + pHudPause->m_Action = SWA::eActionType_Undefined; + pHudPause->m_Transition = SWA::eTransitionType_SubMenu; + + return false; + } + } + + if (pHudPause->m_Status == SWA::eStatusType_Accept) + { + if (cursorIndex == count - 2) + { + OptionsMenu::Open(true, pHudPause->m_Menu); + + pHudPause->m_Action = SWA::eActionType_Undefined; + pHudPause->m_Transition = SWA::eTransitionType_Hide; + + return true; + } + else if (cursorIndex == count - 1) + { + pHudPause->m_Action = actionType; + pHudPause->m_Transition = transitionType; + + return true; + } + } + + return false; +} + +bool CHudPauseItemCountMidAsmHook(PPCRegister& pThis, PPCRegister& count) +{ + count.u32 += 1; + + return InjectMenuBehaviour(pThis.u32, count.u32); +} + +void CHudPauseVillageItemCountMidAsmHook(PPCRegister& pThis, PPCRegister& count) +{ + count.u32 += 1; + + InjectMenuBehaviour(pThis.u32, count.u32); +} + +bool CHudPauseMiscItemCountMidAsmHook(PPCRegister& count) +{ + if (count.u32 < 3) + return true; + + return false; +} + +bool CHudPauseMiscInjectOptionsMidAsmHook(PPCRegister& pThis) +{ + return InjectMenuBehaviour(pThis.u32, 3); +} + +// SWA::CHudPause::Update +PPC_FUNC_IMPL(__imp__sub_824B0930); +PPC_FUNC(sub_824B0930) +{ + auto pHudPause = (SWA::CHudPause*)g_memory.Translate(ctx.r3.u32); + auto pInputState = SWA::CInputState::GetInstance(); + + g_achievementMenuIntroTime += g_deltaTime; + + if (g_isAchievementMenuOutro) + { + g_achievementMenuOutroTime += g_deltaTime; + + // Re-open pause menu after achievement menu closes with delay. + if (g_achievementMenuOutroTime >= g_achievementMenuOutroThreshold) + { + GuestToHostFunction(0x824AFD28, pHudPause, 0, 1, 0, 0); + + g_achievementMenuOutroTime = 0; + g_isAchievementMenuOutro = false; + } + } + + // TODO: disable Start button closing menu. + if (AchievementMenu::s_isVisible) + { + // HACK: wait for transition to finish before restoring control. + if (g_achievementMenuIntroThreshold >= g_achievementMenuIntroTime) + __imp__sub_824B0930(ctx, base); + + if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B) && !g_isAchievementMenuOutro) + { + AchievementMenu::Close(); + + g_isAchievementMenuOutro = true; + } + } + else if (OptionsMenu::s_isVisible && OptionsMenu::s_isPause) + { + if (OptionsMenu::CanClose() && pInputState->GetPadState().IsTapped(SWA::eKeyState_B)) + { + OptionsMenu::Close(); + + GuestToHostFunction(0x824AFD28, pHudPause, 0, 0, 0, 1); + } + } + else + { + g_achievementMenuIntroTime = 0; + + __imp__sub_824B0930(ctx, base); + } +} diff --git a/UnleashedRecomp/patches/ui/CTitleStateMenu_patches.cpp b/UnleashedRecomp/patches/ui/CTitleStateMenu_patches.cpp new file mode 100644 index 0000000..dfb4338 --- /dev/null +++ b/UnleashedRecomp/patches/ui/CTitleStateMenu_patches.cpp @@ -0,0 +1,38 @@ +#include +#include +#include +#include + +// SWA::CTitleStateMenu::Update +PPC_FUNC_IMPL(__imp__sub_825882B8); +PPC_FUNC(sub_825882B8) +{ + auto pTitleState = (SWA::CTitleStateBase*)g_memory.Translate(ctx.r3.u32); + auto pInputState = SWA::CInputState::GetInstance(); + auto& pPadState = pInputState->GetPadState(); + auto isOptionsIndex = pTitleState->m_pMember->m_pTitleMenu->m_CursorIndex == 2; + + if (!OptionsMenu::s_isVisible && pInputState && isOptionsIndex) + { + if (pPadState.IsTapped(SWA::eKeyState_A) || pPadState.IsTapped(SWA::eKeyState_Start)) + { + Game_PlaySound("sys_worldmap_window"); + Game_PlaySound("sys_worldmap_decide"); + + OptionsMenu::Open(); + } + } + + if (!OptionsMenu::s_isVisible) + __imp__sub_825882B8(ctx, base); + + if (pInputState && isOptionsIndex) + { + if (OptionsMenu::CanClose() && pPadState.IsTapped(SWA::eKeyState_B)) + { + Game_PlaySound("sys_worldmap_cansel"); + + OptionsMenu::Close(); + } + } +} diff --git a/UnleashedRecomp/patches/ui/frontend_listener.h b/UnleashedRecomp/patches/ui/frontend_listener.h index da89c73..eb149dd 100644 --- a/UnleashedRecomp/patches/ui/frontend_listener.h +++ b/UnleashedRecomp/patches/ui/frontend_listener.h @@ -2,6 +2,7 @@ #include "kernel/memory.h" #include "ui/sdl_listener.h" +#include "ui/options_menu.h" class FrontendListener : public SDLEventListener { @@ -10,6 +11,9 @@ class FrontendListener : public SDLEventListener public: void OnSDLEvent(SDL_Event* event) override { + if (OptionsMenu::s_isVisible) + return; + switch (event->type) { case SDL_KEYDOWN: diff --git a/UnleashedRecomp/patches/video_patches.cpp b/UnleashedRecomp/patches/video_patches.cpp index d3e1f0d..39622eb 100644 --- a/UnleashedRecomp/patches/video_patches.cpp +++ b/UnleashedRecomp/patches/video_patches.cpp @@ -1,44 +1,12 @@ #include +#include #include #include -#include +// TODO: to be removed. constexpr float m_baseAspectRatio = 16.0f / 9.0f; -bool CameraAspectRatioMidAsmHook(PPCRegister& r31) -{ - auto pCamera = (SWA::CCamera*)g_memory.Translate(r31.u32); - auto newAspectRatio = (float)Window::s_width / (float)Window::s_height; - - // Dynamically adjust horizontal aspect ratio to window dimensions. - pCamera->m_HorzAspectRatio = newAspectRatio; - - if (auto s_pVertAspectRatio = (be*)g_memory.Translate(0x82028FE0)) - { - // Dynamically adjust vertical aspect ratio for VERT+. - *s_pVertAspectRatio = 2.0f * atan(tan(45.0f / 2.0f) * (m_baseAspectRatio / newAspectRatio)); - } - - // Jump to 4:3 code for VERT+ adjustments if using a narrow aspect ratio. - return newAspectRatio < m_baseAspectRatio; -} - -void CameraBoostAspectRatioMidAsmHook(PPCRegister& r31, PPCRegister& f0) -{ - auto pCamera = (SWA::CCamera*)g_memory.Translate(r31.u32); - - if (Window::s_width < Window::s_height) - { - // Use horizontal FOV for narrow aspect ratios. - f0.f32 = pCamera->m_HorzFieldOfView; - } - else - { - // Use vertical FOV for wide aspect ratios. - f0.f32 = pCamera->m_VertFieldOfView; - } -} - +// TODO: to be removed. void CSDAspectRatioMidAsmHook(PPCRegister& f1, PPCRegister& f2) { if (Config::UIScaleMode == EUIScaleMode::Stretch) diff --git a/UnleashedRecomp/res/.gitignore b/UnleashedRecomp/res/.gitignore index e328559..435cdea 100644 --- a/UnleashedRecomp/res/.gitignore +++ b/UnleashedRecomp/res/.gitignore @@ -1,2 +1,3 @@ ![Ww][Ii][Nn]32/ +*.c *.h \ No newline at end of file diff --git a/UnleashedRecomp/stdafx.h b/UnleashedRecomp/stdafx.h index 8fd5bb2..011046d 100644 --- a/UnleashedRecomp/stdafx.h +++ b/UnleashedRecomp/stdafx.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ #include #include #include +#include using Microsoft::WRL::ComPtr; diff --git a/UnleashedRecomp/ui/achievement_menu.cpp b/UnleashedRecomp/ui/achievement_menu.cpp new file mode 100644 index 0000000..143b969 --- /dev/null +++ b/UnleashedRecomp/ui/achievement_menu.cpp @@ -0,0 +1,678 @@ +#include "achievement_menu.h" +#include "imgui_utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr double HEADER_CONTAINER_INTRO_MOTION_START = 0; +constexpr double HEADER_CONTAINER_INTRO_MOTION_END = 15; +constexpr double HEADER_CONTAINER_OUTRO_MOTION_START = 0; +constexpr double HEADER_CONTAINER_OUTRO_MOTION_END = 40; +constexpr double HEADER_CONTAINER_INTRO_FADE_START = 5; +constexpr double HEADER_CONTAINER_INTRO_FADE_END = 14; +constexpr double HEADER_CONTAINER_OUTRO_FADE_START = 0; +constexpr double HEADER_CONTAINER_OUTRO_FADE_END = 7; + +constexpr double CONTENT_CONTAINER_COMMON_MOTION_START = 11; +constexpr double CONTENT_CONTAINER_COMMON_MOTION_END = 12; + +constexpr double COUNTER_INTRO_FADE_START = 15; +constexpr double COUNTER_INTRO_FADE_END = 16; + +constexpr double SELECTION_CONTAINER_BREATHE = 30; + +static bool g_isClosing = false; + +static double g_appearTime; + +static std::vector> g_achievements; + +static ImFont* g_fntSeurat; +static ImFont* g_fntNewRodinDB; +static ImFont* g_fntNewRodinUB; + +static std::unique_ptr g_upTrophyIcon; +static std::unique_ptr g_upSelectionCursor; +static std::unique_ptr g_upWindow; + +static int g_firstVisibleRowIndex; +static int g_selectedRowIndex; +static double g_rowSelectionTime; + +static bool g_upWasHeld; +static bool g_downWasHeld; +static bool g_leftWasHeld; +static bool g_rightWasHeld; +static bool g_upRSWasHeld; +static bool g_downRSWasHeld; + +static void ResetSelection() +{ + g_firstVisibleRowIndex = 0; + g_selectedRowIndex = 0; + g_rowSelectionTime = ImGui::GetTime(); + g_upWasHeld = false; + g_downWasHeld = false; +} + +static void DrawContainer(ImVec2 min, ImVec2 max, ImU32 gradientTop, ImU32 gradientBottom, float alpha = 1, float cornerRadius = 25) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + DrawPauseContainer(g_upWindow.get(), min, max, alpha); + + drawList->PushClipRect({ min.x, min.y + Scale(20) }, { max.x, max.y - Scale(5) }); +} + +static void DrawSelectionContainer(ImVec2 min, ImVec2 max) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + static auto breatheStart = ImGui::GetTime(); + auto alpha = Lerp(1.0f, 0.75f, (sin((ImGui::GetTime() - breatheStart) * (2.0f * M_PI / (55.0f / 60.0f))) + 1.0f) / 2.0f); + auto colour = IM_COL32(255, 255, 255, 255 * alpha); + + auto commonWidth = Scale(11); + auto commonHeight = Scale(24); + + auto tl = PIXELS_TO_UV_COORDS(64, 64, 0, 0, 11, 24); + auto tc = PIXELS_TO_UV_COORDS(64, 64, 11, 0, 8, 24); + auto tr = PIXELS_TO_UV_COORDS(64, 64, 19, 0, 11, 24); + auto cl = PIXELS_TO_UV_COORDS(64, 64, 0, 24, 11, 2); + auto cc = PIXELS_TO_UV_COORDS(64, 64, 11, 24, 8, 2); + auto cr = PIXELS_TO_UV_COORDS(64, 64, 19, 24, 11, 2); + auto bl = PIXELS_TO_UV_COORDS(64, 64, 0, 26, 11, 24); + auto bc = PIXELS_TO_UV_COORDS(64, 64, 11, 26, 8, 24); + auto br = PIXELS_TO_UV_COORDS(64, 64, 19, 26, 11, 24); + + drawList->AddImage(g_upSelectionCursor.get(), min, { min.x + commonWidth, min.y + commonHeight }, GET_UV_COORDS(tl), colour); + drawList->AddImage(g_upSelectionCursor.get(), { min.x + commonWidth, min.y }, { max.x - commonWidth, min.y + commonHeight }, GET_UV_COORDS(tc), colour); + drawList->AddImage(g_upSelectionCursor.get(), { max.x - commonWidth, min.y }, { max.x, min.y + commonHeight }, GET_UV_COORDS(tr), colour); + drawList->AddImage(g_upSelectionCursor.get(), { min.x, min.y + commonHeight }, { min.x + commonWidth, max.y - commonHeight }, GET_UV_COORDS(cl), colour); + drawList->AddImage(g_upSelectionCursor.get(), { min.x + commonWidth, min.y + commonHeight }, { max.x - commonWidth, max.y - commonHeight }, GET_UV_COORDS(cc), colour); + drawList->AddImage(g_upSelectionCursor.get(), { max.x - commonWidth, min.y + commonHeight }, { max.x, max.y - commonHeight }, GET_UV_COORDS(cr), colour); + drawList->AddImage(g_upSelectionCursor.get(), { min.x, max.y - commonHeight }, { min.x + commonWidth, max.y }, GET_UV_COORDS(bl), colour); + drawList->AddImage(g_upSelectionCursor.get(), { min.x + commonWidth, max.y - commonHeight }, { max.x - commonWidth, max.y }, GET_UV_COORDS(bc), colour); + drawList->AddImage(g_upSelectionCursor.get(), { max.x - commonWidth, max.y - commonHeight }, { max.x, max.y }, GET_UV_COORDS(br), colour); +} + +static void DrawHeaderContainer(const char* text) +{ + auto drawList = ImGui::GetForegroundDrawList(); + auto fontSize = Scale(24); + auto textSize = g_fntNewRodinUB->CalcTextSizeA(fontSize, FLT_MAX, 0, text); + auto cornerRadius = 23; + auto textMarginX = Scale(16) + (Scale(cornerRadius) / 2); + + auto containerMotion = g_isClosing + ? ComputeMotion(g_appearTime, HEADER_CONTAINER_OUTRO_MOTION_START, HEADER_CONTAINER_OUTRO_MOTION_END) + : ComputeMotion(g_appearTime, HEADER_CONTAINER_INTRO_MOTION_START, HEADER_CONTAINER_INTRO_MOTION_END); + + auto colourMotion = g_isClosing + ? ComputeMotion(g_appearTime, HEADER_CONTAINER_OUTRO_FADE_START, HEADER_CONTAINER_OUTRO_FADE_END) + : ComputeMotion(g_appearTime, HEADER_CONTAINER_INTRO_FADE_START, HEADER_CONTAINER_INTRO_FADE_END); + + // Slide animation. + auto containerMarginX = g_isClosing + ? Hermite(251, 151, containerMotion) + : Hermite(151, 251, containerMotion); + + // Transparency fade animation. + auto alpha = g_isClosing + ? Lerp(1, 0, colourMotion) + : Lerp(0, 1, colourMotion); + + ImVec2 min = { Scale(containerMarginX), Scale(136) }; + ImVec2 max = { min.x + textMarginX * 2 + textSize.x + Scale(5), Scale(196) }; + + DrawPauseHeaderContainer(g_upWindow.get(), min, max, alpha); + + // TODO: skew this text and apply bevel. + DrawTextWithOutline + ( + g_fntNewRodinUB, + fontSize, + { /* X */ min.x + textMarginX, /* Y */ CENTRE_TEXT_VERT(min, max, textSize) - Scale(5) }, + IM_COL32(255, 255, 255, 255 * alpha), + text, + 3, + IM_COL32(0, 0, 0, 255 * alpha) + ); +} + +static void DrawAchievement(int rowIndex, float yOffset, Achievement& achievement, bool isUnlocked) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + auto clipRectMin = drawList->GetClipRectMin(); + auto clipRectMax = drawList->GetClipRectMax(); + + auto itemWidth = Scale(700); + auto itemHeight = Scale(94); + auto itemMarginX = Scale(18); + auto imageMarginX = Scale(25); + auto imageMarginY = Scale(18); + auto imageSize = Scale(60); + + ImVec2 min = { itemMarginX + clipRectMin.x, clipRectMin.y + itemHeight * rowIndex + yOffset }; + ImVec2 max = { itemMarginX + min.x + itemWidth, min.y + itemHeight }; + + auto icon = g_xdbfTextureCache[achievement.ID]; + auto isSelected = rowIndex == g_selectedRowIndex; + + if (isSelected) + DrawSelectionContainer(min, max); + + auto desc = isUnlocked ? achievement.UnlockedDesc.c_str() : achievement.LockedDesc.c_str(); + auto fontSize = Scale(24); + auto textSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0, desc); + auto textX = min.x + imageMarginX + imageSize + itemMarginX * 2; + auto textMarqueeX = min.x + imageMarginX + imageSize; + auto titleTextY = Scale(20); + auto descTextY = Scale(52); + + // Draw achievement icon. + // TODO: make icon greyscale if locked? + drawList->AddImage + ( + icon, + { /* X */ min.x + imageMarginX, /* Y */ min.y + imageMarginY }, + { /* X */ min.x + imageMarginX + imageSize, /* Y */ min.y + imageMarginY + imageSize }, + { /* U */ 0, /* V */ 0 }, + { /* U */ 1, /* V */ 1 }, + IM_COL32(255, 255, 255, 255 * (isUnlocked ? 1 : 0.5f)) + ); + + drawList->PushClipRect(min, max, true); + + auto colLockedText = IM_COL32(60, 60, 60, 29); + + auto colTextShadow = isUnlocked + ? IM_COL32(0, 0, 0, 255) + : IM_COL32(60, 60, 60, 28); + + auto shadowOffset = isUnlocked ? 2 : 1; + + // Draw achievement name. + DrawTextWithShadow + ( + g_fntSeurat, + fontSize, + { textX, min.y + titleTextY }, + isUnlocked ? IM_COL32(252, 243, 5, 255) : colLockedText, + achievement.Name.c_str(), + shadowOffset, + 0.4f, + colTextShadow + ); + + if (isSelected && textX + textSize.x >= max.x - Scale(10)) + { + // Draw achievement description with marquee. + DrawTextWithMarqueeShadow + ( + g_fntSeurat, + fontSize, + { textX, min.y + descTextY }, + { textMarqueeX, min.y }, + max, + isUnlocked ? IM_COL32(255, 255, 255, 255) : colLockedText, + desc, + g_rowSelectionTime, + 0.9, + 250.0, + shadowOffset, + 0.4f, + colTextShadow + ); + } + else + { + // Draw achievement description. + DrawTextWithShadow + ( + g_fntSeurat, + fontSize, + { textX, min.y + descTextY }, + isUnlocked ? IM_COL32(255, 255, 255, 255) : colLockedText, + desc, + shadowOffset, + 0.4f, + colTextShadow + ); + } + + drawList->PopClipRect(); + + if (!isUnlocked) + return; + + auto timestamp = AchievementData::GetTimestamp(achievement.ID); + + if (!timestamp) + return; + + char buffer[32]; + struct tm time; + localtime_s(&time, ×tamp); + strftime(buffer, sizeof(buffer), "%Y/%m/%d %H:%M", &time); + + fontSize = Scale(12); + textSize = g_fntNewRodinDB->CalcTextSizeA(fontSize, FLT_MAX, 0, buffer); + + auto containerMarginX = Scale(10); + auto textMarginX = Scale(8); + + ImVec2 timestampMin = { max.x - containerMarginX - textSize.x - (textMarginX * 2), min.y + titleTextY }; + ImVec2 timestampMax = { max.x - containerMarginX, min.y + Scale(46) }; + + drawList->PushClipRect(min, max, true); + + auto bevelOffset = Scale(6); + + // Left + drawList->AddRectFilledMultiColor + ( + timestampMin, + { timestampMin.x + bevelOffset, timestampMax.y }, + IM_COL32(255, 255, 255, 255), + IM_COL32(149, 149, 149, 40), + IM_COL32(149, 149, 149, 40), + IM_COL32(255, 255, 255, 255) + ); + + // Right + drawList->AddRectFilledMultiColor + ( + { timestampMax.x - bevelOffset, timestampMin.y }, + { timestampMax.x, timestampMax.y }, + IM_COL32(149, 149, 149, 40), + IM_COL32(255, 255, 255, 255), + IM_COL32(255, 255, 255, 255), + IM_COL32(149, 149, 149, 40) + ); + + // Centre + drawList->AddRectFilled + ( + { timestampMin.x, timestampMin.y + bevelOffset }, + { timestampMax.x, timestampMax.y - bevelOffset }, + IM_COL32(38, 38, 38, 172) + ); + + // Top + drawList->AddRectFilledMultiColor + ( + timestampMin, + { timestampMax.x, timestampMin.y + bevelOffset }, + IM_COL32(16, 16, 16, 192), + IM_COL32(16, 16, 16, 192), + IM_COL32(38, 38, 38, 172), + IM_COL32(38, 38, 38, 172) + ); + + // Bottom + drawList->AddRectFilledMultiColor + ( + { timestampMin.x, timestampMax.y - bevelOffset }, + { timestampMax.x, timestampMax.y }, + IM_COL32(38, 40, 38, 169), + IM_COL32(38, 40, 38, 169), + IM_COL32(16, 16, 16, 192), + IM_COL32(16, 16, 16, 192) + ); + + // Draw timestamp text. + DrawTextWithOutline + ( + g_fntNewRodinDB, + fontSize, + { /* X */ CENTRE_TEXT_HORZ(timestampMin, timestampMax, textSize), /* Y */ CENTRE_TEXT_VERT(timestampMin, timestampMax, textSize) }, + IM_COL32(255, 255, 255, 255), + buffer, + 2, + IM_COL32(8, 8, 8, 255) + ); + + drawList->PopClipRect(); +} + +static void DrawAchievementTotal(ImVec2 min, ImVec2 max) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + // Transparency fade animation. + auto alpha = Cubic(0, 1, ComputeMotion(g_appearTime, COUNTER_INTRO_FADE_START, COUNTER_INTRO_FADE_END)); + + auto imageMarginX = Scale(5); + auto imageMarginY = Scale(5); + auto imageSize = Scale(45); + + ImVec2 imageMin = { max.x - imageSize - imageMarginX, min.y - imageSize - imageMarginY }; + ImVec2 imageMax = { imageMin.x + imageSize, imageMin.y + imageSize }; + + constexpr auto columns = 8; + constexpr auto rows = 4; + constexpr auto spriteSize = 256.0f; + constexpr auto textureWidth = 2048.0f; + constexpr auto textureHeight = 1024.0f; + auto frameIndex = int32_t(floor(ImGui::GetTime() * 30.0f)) % 30; + auto columnIndex = frameIndex % columns; + auto rowIndex = frameIndex / columns; + auto uv0 = ImVec2(columnIndex * spriteSize / textureWidth, rowIndex * spriteSize / textureHeight); + auto uv1 = ImVec2((columnIndex + 1) * spriteSize / textureWidth, (rowIndex + 1) * spriteSize / textureHeight); + + drawList->AddImage(g_upTrophyIcon.get(), imageMin, imageMax, uv0, uv1, IM_COL32(255, 255, 255, 255 * alpha)); + + auto str = std::format("{} / {}", AchievementData::GetTotalRecords(), ACH_RECORDS); + auto fontSize = Scale(20); + auto textSize = g_fntNewRodinDB->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()); + + DrawTextWithOutline + ( + g_fntNewRodinDB, + fontSize, + { /* X */ imageMin.x - textSize.x - Scale(6), /* Y */ CENTRE_TEXT_VERT(imageMin, imageMax, textSize) }, + IM_COL32(255, 255, 255, 255 * alpha), + str.c_str(), + 2, + IM_COL32(0, 0, 0, 255 * alpha) + ); +} + +static void DrawContentContainer() +{ + auto drawList = ImGui::GetForegroundDrawList(); + + // Expand/retract animation. + auto motion = g_isClosing + ? ComputeMotion(g_appearTime, 0, CONTENT_CONTAINER_COMMON_MOTION_START) + : ComputeMotion(g_appearTime, CONTENT_CONTAINER_COMMON_MOTION_START, CONTENT_CONTAINER_COMMON_MOTION_END); + + auto minX = g_isClosing + ? Hermite(251, 301, motion) + : Hermite(301, 251, motion); + + auto minY = g_isClosing + ? Hermite(189, 206, motion) + : Hermite(206, 189, motion); + + auto maxX = g_isClosing + ? Hermite(1031, 978, motion) + : Hermite(978, 1031, motion); + + auto maxY = g_isClosing + ? Hermite(604, 573, motion) + : Hermite(573, 604, motion); + + ImVec2 min = { Scale(minX), Scale(minY) }; + ImVec2 max = { Scale(maxX), Scale(maxY) }; + + // Transparency fade animation. + auto alpha = g_isClosing + ? Hermite(1, 0, motion) + : Hermite(0, 1, motion); + + DrawContainer(min, max, IM_COL32(197, 194, 197, 200), IM_COL32(115, 113, 115, 236), alpha); + + if (motion < 1.0f) + { + return; + } + else if (g_isClosing) + { + AchievementMenu::s_isVisible = false; + return; + } + + auto clipRectMin = drawList->GetClipRectMin(); + auto clipRectMax = drawList->GetClipRectMax(); + + auto itemHeight = Scale(94); + auto yOffset = -g_firstVisibleRowIndex * itemHeight + Scale(2); + auto rowCount = 0; + + // Draw separators. + for (int i = 1; i <= 3; i++) + { + auto lineMarginLeft = Scale(35); + auto lineMarginRight = Scale(55); + auto lineMarginY = Scale(2); + + ImVec2 lineMin = { clipRectMin.x + lineMarginLeft, clipRectMin.y + itemHeight * i + lineMarginY }; + ImVec2 lineMax = { clipRectMax.x - lineMarginRight, clipRectMin.y + itemHeight * i + lineMarginY }; + + drawList->AddLine(lineMin, lineMax, IM_COL32(163, 163, 163, 255)); + drawList->AddLine({ lineMin.x, lineMin.y + Scale(1) }, { lineMax.x, lineMax.y + Scale(1) }, IM_COL32(143, 148, 143, 255)); + } + + for (auto& tpl : g_achievements) + { + auto achievement = std::get<0>(tpl); + + if (AchievementData::IsUnlocked(achievement.ID)) + DrawAchievement(rowCount++, yOffset, achievement, true); + } + + for (auto& tpl : g_achievements) + { + auto achievement = std::get<0>(tpl); + + if (!AchievementData::IsUnlocked(achievement.ID)) + DrawAchievement(rowCount++, yOffset, achievement, false); + } + + auto inputState = SWA::CInputState::GetInstance(); + + bool upIsHeld = inputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) || + inputState->GetPadState().LeftStickVertical > 0.5f; + + bool downIsHeld = inputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) || + inputState->GetPadState().LeftStickVertical < -0.5f; + + bool leftIsHeld = inputState->GetPadState().IsDown(SWA::eKeyState_DpadLeft) || + inputState->GetPadState().LeftStickHorizontal < -0.5f; + + bool rightIsHeld = inputState->GetPadState().IsDown(SWA::eKeyState_DpadRight) || + inputState->GetPadState().LeftStickHorizontal > 0.5f; + + bool upRSIsHeld = inputState->GetPadState().RightStickVertical > 0.5f; + bool downRSIsHeld = inputState->GetPadState().RightStickVertical < -0.5f; + + bool isReachedTop = g_selectedRowIndex == 0; + bool isReachedBottom = g_selectedRowIndex == rowCount - 1; + + bool scrollUp = !g_upWasHeld && upIsHeld; + bool scrollDown = !g_downWasHeld && downIsHeld; + bool scrollPageUp = !g_leftWasHeld && leftIsHeld && !isReachedTop; + bool scrollPageDown = !g_rightWasHeld && rightIsHeld && !isReachedBottom; + bool jumpToTop = !g_upRSWasHeld && upRSIsHeld && !isReachedTop; + bool jumpToBottom = !g_downRSWasHeld && downRSIsHeld && !isReachedBottom; + + int prevSelectedRowIndex = g_selectedRowIndex; + + if (scrollUp) + { + --g_selectedRowIndex; + if (g_selectedRowIndex < 0) + g_selectedRowIndex = rowCount - 1; + } + else if (scrollDown) + { + ++g_selectedRowIndex; + if (g_selectedRowIndex >= rowCount) + g_selectedRowIndex = 0; + } + else if (scrollPageUp) + { + g_selectedRowIndex -= 3; + if (g_selectedRowIndex < 0) + g_selectedRowIndex = 0; + } + else if (scrollPageDown) + { + g_selectedRowIndex += 3; + if (g_selectedRowIndex >= rowCount) + g_selectedRowIndex = rowCount - 1; + } + else if (jumpToTop) + { + g_selectedRowIndex = 0; + } + else if (jumpToBottom) + { + g_selectedRowIndex = rowCount - 1; + } + + // lol + if (scrollUp || scrollDown || scrollPageUp || scrollPageDown || jumpToTop || jumpToBottom) + { + g_rowSelectionTime = ImGui::GetTime(); + Game_PlaySound("sys_actstg_pausecursor"); + } + + g_upWasHeld = upIsHeld; + g_downWasHeld = downIsHeld; + g_leftWasHeld = leftIsHeld; + g_rightWasHeld = rightIsHeld; + g_upRSWasHeld = upRSIsHeld; + g_downRSWasHeld = downRSIsHeld; + + int visibleRowCount = int(floor((clipRectMax.y - clipRectMin.y) / itemHeight)); + + if (g_firstVisibleRowIndex > g_selectedRowIndex) + g_firstVisibleRowIndex = g_selectedRowIndex; + + if (g_firstVisibleRowIndex + visibleRowCount - 1 < g_selectedRowIndex) + g_firstVisibleRowIndex = std::max(0, g_selectedRowIndex - visibleRowCount + 1); + + // Pop clip rect from DrawContentContainer + drawList->PopClipRect(); + + DrawAchievementTotal(min, max); + + // Draw scroll bar + if (rowCount > visibleRowCount) + { + float cornerRadius = Scale(25); + float totalHeight = (clipRectMax.y - clipRectMin.y - cornerRadius) - Scale(5); + float heightRatio = float(visibleRowCount) / float(rowCount); + float offsetRatio = float(g_firstVisibleRowIndex) / float(rowCount); + float offsetX = clipRectMax.x - Scale(39); + float offsetY = offsetRatio * totalHeight + clipRectMin.y + Scale(4); + float maxY = max.y - cornerRadius - Scale(3); + float lineThickness = Scale(1); + float innerMarginX = Scale(2); + float outerMarginX = Scale(24); + + // Outline + drawList->AddRect + ( + { /* X */ offsetX - lineThickness, /* Y */ clipRectMin.y - lineThickness }, + { /* X */ clipRectMax.x - outerMarginX + lineThickness, /* Y */ maxY + lineThickness }, + IM_COL32(255, 255, 255, 155), + Scale(1) + ); + + // Background + drawList->AddRectFilledMultiColor + ( + { /* X */ offsetX, /* Y */ clipRectMin.y }, + { /* X */ clipRectMax.x - outerMarginX, /* Y */ maxY }, + IM_COL32(123, 125, 123, 255), + IM_COL32(123, 125, 123, 255), + IM_COL32(97, 99, 97, 255), + IM_COL32(97, 99, 97, 255) + ); + + // Scroll Bar Outline + drawList->AddRectFilledMultiColor + ( + { /* X */ offsetX + innerMarginX, /* Y */ offsetY - lineThickness }, + { /* X */ clipRectMax.x - outerMarginX - innerMarginX, /* Y */ offsetY + lineThickness + totalHeight * heightRatio }, + IM_COL32(185, 185, 185, 255), + IM_COL32(185, 185, 185, 255), + IM_COL32(172, 172, 172, 255), + IM_COL32(172, 172, 172, 255) + ); + + // Scroll Bar + drawList->AddRectFilled + ( + { /* X */ offsetX + innerMarginX + lineThickness, /* Y */ offsetY }, + { /* X */ clipRectMax.x - outerMarginX - innerMarginX - lineThickness, /* Y */ offsetY + totalHeight * heightRatio }, + IM_COL32(255, 255, 255, 255) + ); + } +} + +void AchievementMenu::Init() +{ + auto& io = ImGui::GetIO(); + + constexpr float FONT_SCALE = 2.0f; + + g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE); + g_fntNewRodinDB = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-DB.otf", 20.0f * FONT_SCALE); + g_fntNewRodinUB = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-UB.otf", 20.0f * FONT_SCALE); + + g_upTrophyIcon = LoadTexture(decompressZstd(g_trophy, g_trophy_uncompressed_size).get(), g_trophy_uncompressed_size); + g_upSelectionCursor = LoadTexture(decompressZstd(g_select_fill, g_select_fill_uncompressed_size).get(), g_select_fill_uncompressed_size); + g_upWindow = LoadTexture(decompressZstd(g_general_window, g_general_window_uncompressed_size).get(), g_general_window_uncompressed_size); +} + +void AchievementMenu::Draw() +{ + if (!s_isVisible) + return; + + DrawHeaderContainer(Localise("Achievements_Name_Uppercase").c_str()); + DrawContentContainer(); +} + +void AchievementMenu::Open() +{ + s_isVisible = true; + g_isClosing = false; + g_appearTime = ImGui::GetTime(); + + g_achievements.clear(); + + for (auto& achievement : g_xdbfWrapper.GetAchievements((EXDBFLanguage)Config::Language.Value)) + g_achievements.push_back(std::make_tuple(achievement, AchievementData::GetTimestamp(achievement.ID))); + + std::sort(g_achievements.begin(), g_achievements.end(), [](const auto& a, const auto& b) + { + return std::get<1>(a) > std::get<1>(b); + }); + + ButtonGuide::Open({ Button(Localise("Common_Back"), EButtonIcon::B) }); + + ResetSelection(); + Game_PlaySound("sys_actstg_pausewinopen"); +} + +void AchievementMenu::Close() +{ + if (!g_isClosing) + { + g_appearTime = ImGui::GetTime(); + g_isClosing = true; + } + + ButtonGuide::Close(); + + Game_PlaySound("sys_actstg_pausewinclose"); + Game_PlaySound("sys_actstg_pausecansel"); +} diff --git a/UnleashedRecomp/ui/achievement_menu.h b/UnleashedRecomp/ui/achievement_menu.h new file mode 100644 index 0000000..db8d6d4 --- /dev/null +++ b/UnleashedRecomp/ui/achievement_menu.h @@ -0,0 +1,12 @@ +#pragma once + +class AchievementMenu +{ +public: + inline static bool s_isVisible = false; + + static void Init(); + static void Draw(); + static void Open(); + static void Close(); +}; diff --git a/UnleashedRecomp/ui/achievement_overlay.cpp b/UnleashedRecomp/ui/achievement_overlay.cpp new file mode 100644 index 0000000..f18331f --- /dev/null +++ b/UnleashedRecomp/ui/achievement_overlay.cpp @@ -0,0 +1,200 @@ +#include "achievement_overlay.h" +#include "imgui_utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr double OVERLAY_CONTAINER_COMMON_MOTION_START = 0; +constexpr double OVERLAY_CONTAINER_COMMON_MOTION_END = 11; +constexpr double OVERLAY_CONTAINER_INTRO_FADE_START = 5; +constexpr double OVERLAY_CONTAINER_INTRO_FADE_END = 9; +constexpr double OVERLAY_CONTAINER_OUTRO_FADE_START = 0; +constexpr double OVERLAY_CONTAINER_OUTRO_FADE_END = 4; + +constexpr double OVERLAY_DURATION = 3; + +static bool g_isClosing = false; + +static double g_appearTime = 0; + +static Achievement g_achievement; + +static ImFont* g_fntSeurat; + +static std::unique_ptr g_upWindow; + +static bool DrawContainer(ImVec2 min, ImVec2 max, float cornerRadius = 25) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + // Expand/retract animation. + auto containerMotion = ComputeMotion(g_appearTime, OVERLAY_CONTAINER_COMMON_MOTION_START, OVERLAY_CONTAINER_COMMON_MOTION_END); + + auto centreX = (min.x + max.x) / 2; + auto centreY = (min.y + max.y) / 2; + + if (g_isClosing) + { + min.x = Hermite(min.x, centreX, containerMotion); + max.x = Hermite(max.x, centreX, containerMotion); + min.y = Hermite(min.y, centreY, containerMotion); + max.y = Hermite(max.y, centreY, containerMotion); + } + else + { + min.x = Hermite(centreX, min.x, containerMotion); + max.x = Hermite(centreX, max.x, containerMotion); + min.y = Hermite(centreY, min.y, containerMotion); + max.y = Hermite(centreY, max.y, containerMotion); + } + + // Transparency fade animation. + auto colourMotion = g_isClosing + ? ComputeMotion(g_appearTime, OVERLAY_CONTAINER_OUTRO_FADE_START, OVERLAY_CONTAINER_OUTRO_FADE_END) + : ComputeMotion(g_appearTime, OVERLAY_CONTAINER_INTRO_FADE_START, OVERLAY_CONTAINER_INTRO_FADE_END); + + auto alpha = g_isClosing + ? Hermite(1, 0, colourMotion) + : Hermite(0, 1, colourMotion); + + DrawPauseContainer(g_upWindow.get(), min, max, alpha); + + drawList->PushClipRect(min, max); + + return containerMotion >= 1.0f; +} + +void AchievementOverlay::Init() +{ + auto& io = ImGui::GetIO(); + + constexpr float FONT_SCALE = 2.0f; + + g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE); + + g_upWindow = LoadTexture(decompressZstd(g_general_window, g_general_window_uncompressed_size).get(), g_general_window_uncompressed_size); +} + +void AchievementOverlay::Draw() +{ + if (!s_isVisible) + return; + + if (ImGui::GetTime() - g_appearTime >= OVERLAY_DURATION) + AchievementOverlay::Close(); + + auto drawList = ImGui::GetForegroundDrawList(); + auto& res = ImGui::GetIO().DisplaySize; + + auto strAchievementUnlocked = Localise("Achievements_Unlock").c_str(); + auto strAchievementName = g_achievement.Name.c_str(); + + // Calculate text sizes. + auto fontSize = Scale(24); + auto headerSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0, strAchievementUnlocked); + auto bodySize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0, strAchievementName); + auto maxSize = std::max(headerSize.x, bodySize.x) + Scale(5); + + // Calculate image margins. + auto imageMarginX = Scale(25); + auto imageMarginY = Scale(22.5f); + auto imageSize = Scale(60); + + // Calculate text margins. + auto textMarginX = imageMarginX * 2 + imageSize - Scale(5); + auto textMarginY = imageMarginY + Scale(2); + + auto containerWidth = imageMarginX + textMarginX + maxSize; + + ImVec2 min = { (res.x / 2) - (containerWidth / 2), Scale(55) }; + ImVec2 max = { min.x + containerWidth, min.y + Scale(105) }; + + if (DrawContainer(min, max)) + { + if (g_isClosing) + { + s_isVisible = false; + return; + } + + // Draw achievement icon. + drawList->AddImage + ( + g_xdbfTextureCache[g_achievement.ID], // user_texture_id + { /* X */ min.x + imageMarginX, /* Y */ min.y + imageMarginY }, // p_min + { /* X */ min.x + imageMarginX + imageSize, /* Y */ min.y + imageMarginY + imageSize }, // p_max + { 0, 0 }, // uv_min + { 1, 1 }, // uv_max + IM_COL32(255, 255, 255, 255) // col + ); + + // Draw header text. + DrawTextWithShadow + ( + g_fntSeurat, // font + fontSize, // fontSize + { /* X */ min.x + textMarginX + (maxSize - headerSize.x) / 2, /* Y */ min.y + textMarginY }, // pos + IM_COL32(252, 243, 5, 255), // colour + strAchievementUnlocked, // text + 2, // offset + 0.4f, // radius + IM_COL32(0, 0, 0, 255) // shadowColour + ); + + // Draw achievement name. + DrawTextWithShadow + ( + g_fntSeurat, // font + fontSize, // fontSize + { /* X */ min.x + textMarginX + (maxSize - bodySize.x) / 2, /* Y */ min.y + textMarginY + bodySize.y + Scale(6) }, // pos + IM_COL32(255, 255, 255, 255), // colour + strAchievementName, // text + 2, // offset + 0.4f, // radius + IM_COL32(0, 0, 0, 255) // shadowColour + ); + + // Pop clip rect from DrawContainer. + drawList->PopClipRect(); + } +} + +void AchievementOverlay::Open(int id) +{ + if (s_isVisible) + { + s_queue.emplace(id); + return; + } + + s_isVisible = true; + g_isClosing = false; + g_appearTime = ImGui::GetTime(); + g_achievement = g_xdbfWrapper.GetAchievement((EXDBFLanguage)Config::Language.Value, id); + + Game_PlaySound("obj_navi_appear"); +} + +void AchievementOverlay::Close() +{ + if (!g_isClosing) + { + g_appearTime = ImGui::GetTime(); + g_isClosing = true; + } + + if (s_queue.size()) + { + s_isVisible = false; + AchievementOverlay::Open(s_queue.front()); + s_queue.pop(); + } +} diff --git a/UnleashedRecomp/ui/achievement_overlay.h b/UnleashedRecomp/ui/achievement_overlay.h new file mode 100644 index 0000000..5312605 --- /dev/null +++ b/UnleashedRecomp/ui/achievement_overlay.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class AchievementOverlay +{ +public: + inline static bool s_isVisible = false; + + inline static std::queue s_queue{}; + + static void Init(); + static void Draw(); + static void Open(int id); + static void Close(); +}; diff --git a/UnleashedRecomp/ui/button_guide.cpp b/UnleashedRecomp/ui/button_guide.cpp new file mode 100644 index 0000000..213ea5f --- /dev/null +++ b/UnleashedRecomp/ui/button_guide.cpp @@ -0,0 +1,305 @@ +#include "button_guide.h" +#include "imgui_utils.h" +#include +#include +#include +#include +#include +#include + +constexpr float DEFAULT_SIDE_MARGINS = 379; + +ImFont* g_fntNewRodin; +ImFont* g_fntNewRodinLQ; + +std::unique_ptr g_upIcons; +std::unique_ptr g_upLMBIcon; +std::unique_ptr g_upStartBackIcons; + +float g_sideMargins = DEFAULT_SIDE_MARGINS; + +std::vector