Options menu, achievements, and installer wizard. (#19)

* Implemented guest-to-host function pointers (WIP)

Co-Authored-By: Skyth (Asilkan) <19259897+blueskythlikesclouds@users.noreply.github.com>

* function: support more types for function pointers

* Initial options menu implementation.

* Improve options menu visuals.

* Draw fade on borders, center tabs better.

* Adjust line sizes, fix tab text centering.

* Adjust padding & text sizes.

* Fix bar dark gradient effect.

* api: ported BlueBlur headers and misc. research

* Fix config name padding not getting scaled at different resolutions.

* config: use string_view, added method to get value pointer

* config: use std::map for reverse enum template

* Draw config options manually instead of looping through them.

* config: implemented name and enum localisation

* config_detail: move implementation to cpp, relocate sources

* Implemented accessing options menu via pause and title screen

* config: replace MSAA with AntiAliasing enum

* options_menu: implemented info panel and text marquee (see TODOs)

* Draw selection triangles.

* Supersample fonts to 2K.

* Implement options menu navigation.

* Fix duplicate triangles when selecting options.

* Draw scroll bar.

* Adjust scroll bar padding.

* Further scroll bar padding adjustments.

* Draw outer container as an outline.

* Improve marquee text scrolling.

* CTitleMenu: fix options menu re-entering on A press whilst visible

* Make procedural grid pattern more accurate.

* Add enum & bool editing.

* Update English localisation

* Fix input state mapping.

* options_menu: hide menu on Y hold

* CHudPause: fix crash when opening options menu from village/lab

* Implement float slider.

* options_menu: round res scale description resolution

* options_menu: use config callbacks after setting items

* api: fix GameObject layout

* camera_patches: implemented camera X/Y invert

* options_menu: fix buffered A press selecting first option upon entry

* config_locale: update description for Battle Music

* config: added Allow Background Input option

* options_menu: move ATOC option below Anti-Aliasing

* options_menu: only draw header/footer fade in stages

* Handle real-time modifications of some video config values.

* Converge increments only when holding the left/right button.

* Add sound effects to options menu.

* Change some sounds used in options menu.

* Give the final decide sound to bool toggling.

* Add option select animation.

* options_menu: only play slider sound between min/max range

* Apply category select animation.

* config: rename Controls category to Input

* Implement intro transition animation for options menu.

* audio_patches: implemented music volume

* Implement FPS slider.

* Prevent ImGui from displaying OS cursor.

* Fade container brackets during intro transition.

* player_patches: added penalty to Unleash Cancel

* config_locale: update English localisation

* player_patches: ensure Unleash gauge penalty doesn't dip into negatives

* options_menu: fix being unable to press A at least once after opening the menu

* CTitleMenu: added open/close sounds to the options menu

* audio_patches: implemented Music and SE volume

* api: update research

* Implemented music volume attenuation for Windows 10+

* api: fix score offset

* Add an interval between consecutive playbacks of the slider sound effect in fastIncrement mode

* config: implemented enum descriptions

* options_menu: fit thumbnail rect to grid, remove menu hide input

* options_menu: fix description wrap width

* camera_patches: fix FOV at narrow aspect ratios

mobile gaming is back on the menu

* options_menu: implemented greyed out options and localisation

* options_menu: allow providing reasons for greyed out options

* audio_patches: check if Windows version is supported

* Update PowerRecomp submodule

* api: more research

* options_menu: forget selected item upon opening

* options_menu: restrict XButtonHoming to title and world map

* window: always hide mouse cursor

The options menu doesn't accept mouse input, so there's not really any point to showing the cursor at all.

* Animate category tab background  from the center.

* Fix clip rect in info panel not getting popped at all times.

* Expose texture loader in "video.h".

* config: use final names and descriptions, label options to be moved to exports

* options_menu: implemented Voice Language (and some misc. clean-up)

* Move Voice Language patch to resident_patches

* config: added Aspect Ratio option (to be implemented)

* options_menu: implemented Subtitles

* Remove triple buffering from options menu, turn it to an enum.

* window: hide mouse cursor on controller input for windowed mode

* window: show window dimensions on title bar when resizing window

* api: update research

* Accept functions directly in GuestToHostFunction & add memory range asserts.

* Add guest_stack_var, improve shared_ptr implementation.

* Handle float/double arguments properly in GuestToHostFunction.

* CHudPause_patches: allocate options strings on stack

* api: update research

* guest_stack_var: allow creation without constructing underlying type

* memory: make assertions lenient towards nullptr

* api: include guest_stack_var in SWA.inl

* audio_patches: don't worry about it

* Implemented achievement overlay (WIP)

* Implemented achievements menu (WIP)

* Clean-up, improved animation and layouts

* options_menu: fix naming convention

* achievements_overlay: implemented queue and hermite interpolation

* achievements_menu: implemented animations and improved navigation

* achievements_menu: improve animation accuracy

* achievements_menu: added timestamps

* achievement_data: added checksum and format verification

* achievement_menu: improved outro animation

* achievement_menu: added total unlocked achievements

* achievement_menu: update sprite animation

* Update resources submodule

* Add installer wizard.

* Skip drawing scanlines when height is 0.

* Tweak install screen to better match the original

* Added arrow circle to installer's header

* Move icon header generation to resources submodule

* Added missing animations and tweaked other ones for installer

* Improve detection for DLC only mode. Add template for message prompts.

* Add language picker.

* window: update icon resources

* Added file_to_c

* Fixes to conversion.

* Implemented message window

* achievement_menu: use selection cursor texture

* Update embedded resources

* Implemented message window

* Merge branch 'bin2c' into options-menu

* Update embedded resources

* Framework for max width for buttons.

* Update embedded resources

* Use textures for pause menu containers

* audio_patches: check if Windows major version is >=10

Just in case.

* installer_wizard: use integer outline for button text

* Added arrow circle spinning animation during installation screen

* achievement_menu: fix timestamp and scroll bar padding

* achievement_overlay: fix achievement name padding

* installer_wizard: fix arrow circle spinning animation misaligning

* Add Scale and Origin to ImGui shaders. Change text to be squashed.

* message_window: implemented mouse input

* installer_wizard: implemented message windows

* achievement_menu: start marquee before timestamp margin

* Fix message box flow.

* message_window: use pause container texture

* Add extra condition for starting the installer.

* message_window: only accept mouse click if option is selected

* Implemented safer way to check if the game is loaded

* Add queued update when using files pickers.

* installer_wizard: implement localisation

* installer_wizard: use enum for localisation

* message_window: fix visibility persisting after window closes

* Fix arrow circle animation and added pulse animation

* Come back check space.

* Implement ZSTD compression in file_to_c.

* Add fade-in/out to installation icons and sleep after hitting 100%

* Implement ImGui font atlas caching.

* Controller navigation.

* Implemented button guide

* CTitleStateMenu: fix start button opening old options menu

* Update resources submodule

* imgui_snapshot: check if game is loaded before accessing XDBF

* message_window: added button guide

* options_menu: increase button guide side margins

* video: disable imgui.ini creation

* Use IM_DELETE for deleting the existing font atlas.

* Remove redundant FlushViewport call.

* Fix ImGui callbacks leaking memory.

* Replace unique_ptr reference arguments with raw pointers.

* Specialize description for resolution scale by reference.

---------

Co-authored-by: Hyper <34012267+hyperbx@users.noreply.github.com>
Co-authored-by: PTKay <jp_moura99@outlook.com>
Co-authored-by: Dario <dariosamo@gmail.com>
This commit is contained in:
Skyth (Asilkan) 2024-12-06 18:52:06 +03:00 committed by GitHub
parent b0562b4360
commit c0897dd507
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 8069 additions and 827 deletions

View file

@ -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$<$<CONFIG:Debug>: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")

View file

@ -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")

View file

@ -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);

View file

@ -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"

View file

@ -0,0 +1,22 @@
#pragma once
#include <SWA.inl>
namespace SWA::Achievement
{
class CManager : public Hedgehog::Universe::CUpdateUnit
{
public:
class CMember
{
public:
SWA_INSERT_PADDING(0x08);
be<uint32_t> m_AchievementID;
};
SWA_INSERT_PADDING(0x98);
xpointer<CMember> m_pMember;
be<uint32_t> m_IsUnlocked;
SWA_INSERT_PADDING(0x10);
};
}

View file

@ -0,0 +1,15 @@
#pragma once
#include <SWA.inl>
namespace SWA
{
class CAchievementTest
{
public:
SWA_INSERT_PADDING(0x38);
be<uint32_t> m_Unk1;
be<uint32_t> m_AchievementID;
uint8_t m_Unk2;
};
}

View file

@ -0,0 +1,13 @@
#pragma once
#include <SWA.inl>
namespace SWA
{
class CSaveIcon : Hedgehog::Universe::CUpdateUnit
{
public:
SWA_INSERT_PADDING(0xD8);
bool m_IsVisible;
};
}

View file

@ -35,7 +35,9 @@ namespace SWA
public:
SWA_INSERT_PADDING(0x20);
boost::shared_ptr<CGame> m_pGame;
SWA_INSERT_PADDING(0x114);
SWA_INSERT_PADDING(0xD4);
xpointer<Achievement::CManager> m_pAchievementManager;
SWA_INSERT_PADDING(0x3C);
xpointer<void> m_spGameParameter;
};

View file

@ -1,19 +1,23 @@
#include <app.h>
#include <kernel/function.h>
#include <ui/window.h>
#include <app.h>
#include <patches/audio_patches.h>
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);
}

View file

@ -1,3 +1,4 @@
#pragma once
extern bool g_isGameLoaded;
extern double g_deltaTime;

View file

@ -1,85 +0,0 @@
#pragma once
#include "config_detail.h"
class Config
{
public:
inline static std::vector<IConfigDef*> 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();
};

View file

@ -1,28 +0,0 @@
#include "config.h"
#include "config_detail.h"
// CONFIG_DEFINE
template<typename T>
ConfigDef<T>::ConfigDef(std::string section, std::string name, T defaultValue) : Section(section), Name(name), DefaultValue(defaultValue)
{
Config::Definitions.emplace_back(this);
}
// CONFIG_DEFINE_ENUM
template<typename T>
ConfigDef<T>::ConfigDef(std::string section, std::string name, T defaultValue, std::unordered_map<std::string, T> 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<typename T>
ConfigDef<T>::ConfigDef(std::string section, std::string name, T defaultValue, std::function<void(ConfigDef<T>*)> readCallback)
: Section(section), Name(name), DefaultValue(defaultValue), ReadCallback(readCallback)
{
Config::Definitions.emplace_back(this);
}

View file

@ -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<type> name{section, #name, defaultValue};
#define CONFIG_DEFINE_ENUM_TEMPLATE(type) \
inline static std::unordered_map<std::string, type> g_##type##_template =
#define CONFIG_DEFINE_ENUM(section, type, name, defaultValue) \
inline static ConfigDef<type> name{section, #name, defaultValue, g_##type##_template};
#define CONFIG_DEFINE_CALLBACK(section, type, name, defaultValue, readCallback) \
inline static ConfigDef<type> name{section, #name, defaultValue, [](ConfigDef<type>* 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<typename T>
class ConfigDef : public IConfigDef
{
public:
std::string Section{};
std::string Name{};
T DefaultValue{};
T Value{ DefaultValue };
std::unordered_map<std::string, T> EnumTemplate{};
std::unordered_map<T, std::string> EnumTemplateReverse{};
std::function<void(ConfigDef<T>*)> 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<std::string, T> enumTemplate);
// CONFIG_DEFINE_CALLBACK
ConfigDef(std::string section, std::string name, T defaultValue, std::function<void(ConfigDef<T>*)> 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<T, std::string>::value)
{
Value = section[Name].value_or<std::string>(DefaultValue);
}
else if constexpr (std::is_enum_v<T>)
{
auto it = EnumTemplate.begin();
Value = EnumTemplate[section[Name].value_or<std::string>(static_cast<std::string>(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<T, std::string>::value)
{
return std::format("\"{}\"", Value);
}
else if constexpr (std::is_enum_v<T>)
{
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 }
};

View file

@ -0,0 +1,9 @@
#pragma once
template<size_t N>
static std::unique_ptr<uint8_t[]> decompressZstd(const uint8_t(&data)[N], size_t decompressedSize)
{
auto decompressedData = std::make_unique<uint8_t[]>(decompressedSize);
ZSTD_decompress(decompressedData.get(), decompressedSize, data, N);
return decompressedData;
}

View file

@ -0,0 +1,31 @@
#include <kernel/function.h>
#include <kernel/heap.h>
#include <kernel/memory.h>
#include <cpu/guest_stack_var.h>
#include <ui/installer_wizard.h>
#include <ui/window.h>
#include <api/boost/smart_ptr/shared_ptr.h>
SWA_API void Game_PlaySound(const char* pName)
{
// TODO: use own sound player.
if (InstallerWizard::s_isVisible)
return;
guest_stack_var<boost::anonymous_shared_ptr> soundPlayer;
GuestToHostFunction<void>(sub_82B4DF50, soundPlayer.get(), ((be<uint32_t>*)g_memory.Translate(0x83367900))->get(), 7, 0, 0);
auto soundPlayerVtable = (be<uint32_t>*)g_memory.Translate(*(be<uint32_t>*)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<void>(virtualFunction, soundPlayer->get(), strAllocation, 0);
g_userHeap.Free(strAllocation);
}
SWA_API void Window_SetFullscreen(bool isEnabled)
{
Window::SetFullscreen(isEnabled);
}

View file

@ -0,0 +1,4 @@
#pragma once
SWA_API void Game_PlaySound(const char* pName);
SWA_API void Window_SetFullscreen(bool isEnabled);

View file

@ -74,3 +74,23 @@ constexpr size_t FirstBitLow(TValue value)
return 0;
}
static std::unique_ptr<uint8_t[]> ReadAllBytes(const char* filePath, size_t& fileSize)
{
FILE* file = fopen(filePath, "rb");
if (!file)
return std::make_unique<uint8_t[]>(0);
fseek(file, 0, SEEK_END);
fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
auto data = std::make_unique<uint8_t[]>(fileSize);
fread(data.get(), 1, fileSize, file);
fclose(file);
return data;
}

View file

@ -0,0 +1,22 @@
#include "imgui_common.h"
static std::vector<std::unique_ptr<ImGuiCallbackData>> 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<ImGuiCallbackData>());
auto& callbackData = g_callbackData[g_callbackDataIndex];
++g_callbackDataIndex;
ImGui::GetForegroundDrawList()->AddCallback(reinterpret_cast<ImDrawCallback>(callback), callbackData.get());
return callbackData.get();
}
void ResetImGuiCallbacks()
{
g_callbackDataIndex = 0;
}

View file

@ -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

View file

@ -1,5 +1,12 @@
#include "imgui_snapshot.h"
#include <locale/locale.h>
#include <res/font/im_font_atlas.bin.h>
#include <user/config.h>
#include <decompressor.h>
#include <kernel/xdbf.h>
#include <app.h>
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<typename T1, typename T2>
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<T2>);
constexpr size_t SIZE = sizeof(std::remove_pointer_t<T2>);
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<ptrdiff_t>(&ptr) - reinterpret_cast<ptrdiff_t>(&value));
*reinterpret_cast<size_t*>(&data[fieldOffset]) = objects[ptr];
offsets.push_back(fieldOffset);
}
}
template<typename T>
void ImFontAtlasSnapshot::Traverse(size_t offset, const T& value)
{
if constexpr (std::is_pointer_v<T>)
{
SnapPointer(offset, value, value, 1);
}
else if constexpr (std::is_same_v<T, ImFontAtlas>)
{
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<T, ImFont>)
{
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<T, ImFontAtlasCustomRect>)
{
SnapPointer(offset, value, value.Font, 1);
}
else if constexpr (std::is_same_v<T, ImFontConfig>)
{
SnapPointer(offset, value, value.GlyphRanges, value.GlyphRanges != nullptr ? wcslen(reinterpret_cast<const wchar_t*>(value.GlyphRanges)) + 1 : 0);
SnapPointer(offset, value, value.DstFont, 1);
}
}
template<typename T>
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<uint8_t*>(offsets.data()),
reinterpret_cast<uint8_t*>(offsets.data() + offsets.size()));
auto header = reinterpret_cast<ImFontAtlasSnapshotHeader*>(data.data());
header->imguiVersion = IMGUI_VERSION_NUM;
header->dataOffset = dataOffset;
header->offsetCount = offsets.size();
header->offsetsOffset = offsetsOffset;
}
static std::unique_ptr<uint8_t[]> g_imFontAtlas;
ImFontAtlas* ImFontAtlasSnapshot::Load()
{
g_imFontAtlas = decompressZstd(g_im_font_atlas, g_im_font_atlas_uncompressed_size);
auto header = reinterpret_cast<ImFontAtlasSnapshotHeader*>(g_imFontAtlas.get());
assert(header->imguiVersion == IMGUI_VERSION_NUM && "ImGui version mismatch, the font atlas needs to be regenerated!");
auto offsetTable = reinterpret_cast<uint32_t*>(g_imFontAtlas.get() + header->offsetsOffset);
for (size_t i = 0; i < header->offsetCount; i++)
{
*reinterpret_cast<size_t*>(g_imFontAtlas.get() + (*offsetTable)) += reinterpret_cast<size_t>(g_imFontAtlas.get());
++offsetTable;
}
return reinterpret_cast<ImFontAtlas*>(g_imFontAtlas.get() + header->dataOffset);
}
static void GetGlyphs(std::set<ImWchar>& 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<ImWchar> g_glyphRanges;
void ImFontAtlasSnapshot::GenerateGlyphRanges()
{
std::vector<std::string_view> localeStrings;
for (auto& config : Config::Definitions)
config->GetLocaleStrings(localeStrings);
std::set<ImWchar> 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<EXDBFLanguage>(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());
}

View file

@ -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<uint8_t> data;
ankerl::unordered_dense::map<const void*, size_t> objects;
std::vector<uint32_t> offsets;
template<typename T1, typename T2>
void SnapPointer(size_t offset, const T1& value, const T2& ptr, size_t count);
template<typename T>
void Traverse(size_t offset, const T& value);
template<typename T>
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);
};

View file

@ -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<float4> g_Texture2DDescriptorHeap[] : register(t0, space0);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -6,13 +6,25 @@
#include <cpu/guest_code.h>
#include <cpu/guest_thread.h>
#include <kernel/memory.h>
#include <kernel/xdbf.h>
#include <xxHashMap.h>
#include <shader/shader_cache.h>
#include <ui/achievement_menu.h>
#include <ui/achievement_overlay.h>
#include <ui/button_guide.h>
#include <ui/message_window.h>
#include <ui/options_menu.h>
#include <ui/installer_wizard.h>
#include "imgui_snapshot.h"
#include "imgui_common.h"
#include "video.h"
#include <ui/sdl_listener.h>
#include <cfg/config.h>
#include <ui/window.h>
#include <user/config.h>
#include <res/font/im_font_atlas.dds.h>
#include <decompressor.h>
#include <SWA.h>
@ -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<RenderTexture> g_imFontTexture;
static std::unique_ptr<RenderTextureView> g_imFontTextureView;
static uint32_t g_imFontTextureDescriptorIndex;
static bool g_imPendingBarrier = true;
static std::unique_ptr<GuestTexture> g_imFontTexture;
static std::unique_ptr<RenderPipelineLayout> g_imPipelineLayout;
static std::unique_ptr<RenderPipeline> 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<GuestTexture>(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<GuestSurface>(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<bool> 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<uint32_t>* a6)
{
CreateHostDevice();
g_xdbfTextureCache = std::unordered_map<uint16_t, GuestTexture *>();
g_backBuffer = g_userHeap.AllocPhysical<GuestSurface>(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<GuestDevice>();
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<const ImGuiCallbackData*>(drawCmd.UserCallbackData);
if (drawCmd.ClipRect.z <= drawCmd.ClipRect.x || drawCmd.ClipRect.w <= drawCmd.ClipRect.y)
continue;
switch (static_cast<ImGuiCallback>(reinterpret_cast<size_t>(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<GuestTexture*>(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<const RenderTexture**>(&swapChainTexture);
desc.colorAttachments = const_cast<const RenderTexture **>(&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<GuestTexture>(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<Slice> 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<char*>(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<Slice> 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<uint8_t*>(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<uint8_t*>(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<const uint8_t*>(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<GuestTexture> LoadTexture(const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping)
{
GuestTexture texture(ResourceType::Texture);
if (LoadTexture(texture, data, dataSize, componentMapping))
return std::make_unique<GuestTexture>(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<GuestTexture>(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<uint8_t*>(uploadBuffer->map());
if (rowPitch == (width * 4))
{
memcpy(mappedMemory, stbImage, slicePitch);
}
else
{
auto data = reinterpret_cast<const uint8_t*>(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<char*>(g_memory.Translate(pictureData->name + 2)));
#endif
pictureData->texture = g_memory.MapVirtual(g_userHeap.AllocPhysical<GuestTexture>(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);

View file

@ -10,6 +10,14 @@
using namespace plume;
struct Video
{
static void CreateHostDevice();
static void HostPresent();
static void StartPipelinePrecompilation();
static void WaitForGPU();
};
struct GuestSamplerState
{
be<uint32_t> data[6];
@ -378,3 +386,7 @@ enum GuestTextureAddress
D3DTADDRESS_MIRRORONCE = 3,
D3DTADDRESS_BORDER = 6
};
extern std::unique_ptr<GuestTexture> LoadTexture(const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping = RenderComponentMapping());
extern void VideoConfigValueChangedCallback(class IConfigDef* config);

View file

@ -1,6 +1,10 @@
#include <stdafx.h>
#include <SDL.h>
#include <user/config.h>
#include <hid/hid_detail.h>
#include <ui/window.h>
#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<Controller, 4> 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;

View file

@ -66,7 +66,7 @@ static std::unique_ptr<VirtualFileSystem> 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<uint8_t> &fileData, Journal &journal, const std::function<void(uint32_t)> &progressCallback) {
static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector<uint8_t> &fileData, Journal &journal, const std::function<void()> &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<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<void(uint32_t)> &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<const FilePair> 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<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<void()> &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<void(uint32_t)> &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<VirtualFileSystem> 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<VirtualFileSystem> 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<VirtualFileSystem> sourceVfs;
std::span<const FilePair> filePairs;
const uint64_t *fileHashes = nullptr;
std::string targetSubDirectory;
};
std::vector<DLCSource> 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<void()> &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;
}

View file

@ -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<std::filesystem::path> createdFiles;
std::set<std::filesystem::path> createdDirectories;
Result lastResult = Result::Success;
@ -53,13 +54,30 @@ struct Installer
std::filesystem::path gameSource;
std::filesystem::path updateSource;
std::list<std::filesystem::path> dlcSources;
bool skipHashChecks = false;
};
struct DLCSource {
std::unique_ptr<VirtualFileSystem> sourceVfs;
std::span<const FilePair> filePairs;
const uint64_t *fileHashes = nullptr;
std::string targetSubDirectory;
};
struct Sources
{
std::unique_ptr<VirtualFileSystem> game;
std::unique_ptr<VirtualFileSystem> update;
std::vector<DLCSource> dlc;
uint64_t totalSize = 0;
};
static bool checkGameInstall(const std::filesystem::path &baseDirectory);
static bool copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<void(uint32_t)> &progressCallback);
static bool checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc);
static bool computeTotalSize(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize);
static bool copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<void()> &progressCallback);
static bool parseContent(const std::filesystem::path &sourcePath, std::unique_ptr<VirtualFileSystem> &targetVfs, Journal &journal);
static bool install(const Input &input, const std::filesystem::path &targetDirectory, Journal &journal, const std::function<void(uint32_t)> &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<void()> &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.

View file

@ -11,7 +11,7 @@
#include "xam.h"
#include "xdm.h"
#include <timeapi.h>
#include <cfg/config.h>
#include <user/config.h>
#include <ntstatus.h>

View file

@ -8,6 +8,7 @@
#include <unordered_set>
#include <CommCtrl.h>
#include "xxHashMap.h"
#include <user/paths.h>
// 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;

View file

@ -0,0 +1,7 @@
#pragma once
#include <gpu/video.h>
#include <xdbf_wrapper.h>
extern XDBFWrapper g_xdbfWrapper;
extern std::unordered_map<uint16_t, GuestTexture*> g_xdbfTextureCache;

View file

@ -0,0 +1,338 @@
#pragma once
#include <user/config_detail.h>
#define CONFIG_DEFINE_LOCALE(name) \
inline static std::unordered_map<ELanguage, std::tuple<std::string, std::string>> g_##name##_locale =
#define CONFIG_DEFINE_ENUM_LOCALE(type) \
inline static std::unordered_map<ELanguage, std::unordered_map<type, std::tuple<std::string, std::string>>> 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, { "", "" } },
{ false, { "NO", "" } }
}
},
{
ELanguage::Italian,
{
{ true, { "", "" } },
{ 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." } },
}
}
};

View file

@ -0,0 +1,279 @@
#pragma once
#include <user/config.h>
inline static std::string g_localeMissing = "<missing string>";
inline static std::unordered_map<std::string, std::unordered_map<ELanguage, std::string>> 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];
}

View file

@ -11,8 +11,12 @@
#include <xex.h>
#include <apu/audio.h>
#include <hid/hid.h>
#include <cfg/config.h>
#include <user/achievement_data.h>
#include <user/config.h>
#include <user/paths.h>
#include <kernel/xdbf.h>
#include <install/installer.h>
#include <ui/installer_wizard.h>
#define GAME_XEX_PATH "game:\\default.xex"
@ -22,9 +26,10 @@ const size_t XMAIOEnd = XMAIOBegin + 0x0000FFFF;
Memory g_memory{ reinterpret_cast<void*>(0x100000000), 0x100000000 };
Heap g_userHeap;
CodeCache g_codeCache;
XDBFWrapper g_xdbfWrapper;
std::unordered_map<uint16_t, GuestTexture*> 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_RESOURCE_INFO>(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;

View file

@ -0,0 +1,105 @@
#include <cpu/guest_code.h>
#include <user/config.h>
#include <kernel/function.h>
#include <kernel/platform.h>
#include <patches/audio_patches.h>
#include <api/SWA.h>
#if _WIN32
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Media.Control.h>
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<float>* GetVolume(bool isMusic = true)
{
auto ppUnkClass = (be<uint32_t>*)g_memory.Translate(0x83362FFC);
if (!ppUnkClass->get())
return nullptr;
// NOTE (Hyper): This is fine, trust me. See 0x82E58728.
return (be<float>*)g_memory.Translate(4 * ((int)isMusic + 0x1C) + ((be<uint32_t>*)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);

View file

@ -0,0 +1,11 @@
#pragma once
class AudioPatches
{
protected:
static int m_isAttenuationSupported;
public:
static bool CanAttenuate();
static void Update(float deltaTime);
};

View file

@ -0,0 +1,51 @@
#include <cpu/guest_code.h>
#include <api/SWA.h>
#include <ui/window.h>
#include <user/config.h>
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<float>*)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);
}

View file

@ -1,7 +1,7 @@
#include <cpu/guest_code.h>
#include <api/SWA.h>
#include <ui/window.h>
#include <cfg/config.h>
#include <user/config.h>
#include <app.h>
float m_lastLoadingFrameDelta = 0.0f;

View file

@ -1,7 +1,13 @@
#include <cpu/guest_code.h>
#include <api/SWA.h>
#include <ui/window.h>
#include <cfg/config.h>
#include <user/achievement_data.h>
#include <user/config.h>
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;

View file

@ -2,7 +2,7 @@
#include <api/SWA.h>
#include <ui/window.h>
#include <ui/window_events.h>
#include <cfg/config.h>
#include <user/config.h>
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;
}

View file

@ -1,8 +1,12 @@
#include <cpu/guest_code.h>
#include <cfg/config.h>
#include <user/achievement_data.h>
#include <user/config.h>
#include <api/SWA.h>
const char* m_pStageID;
bool m_isSavedAchievementData = false;
void GetStageIDMidAsmHook(PPCRegister& r5)
{
m_pStageID = *(xpointer<const char>*)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

View file

@ -0,0 +1,161 @@
#include <cpu/guest_code.h>
#include <kernel/function.h>
#include <api/SWA.h>
#include <ui/achievement_menu.h>
#include <ui/options_menu.h>
#include <app.h>
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<Hedgehog::Base::CSharedString> menu("TopMenu");
guest_stack_var<Hedgehog::Base::CSharedString> name("option");
GuestToHostFunction<int>(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<uint32_t>*)g_memory.Translate(4 * (*(be<uint32_t>*)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<int>(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<int>(0x824AFD28, pHudPause, 0, 0, 0, 1);
}
}
else
{
g_achievementMenuIntroTime = 0;
__imp__sub_824B0930(ctx, base);
}
}

View file

@ -0,0 +1,38 @@
#include <cpu/guest_code.h>
#include <api/SWA.h>
#include <ui/options_menu.h>
#include <exports.h>
// 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();
}
}
}

View file

@ -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:

View file

@ -1,44 +1,12 @@
#include <cpu/guest_code.h>
#include <user/config.h>
#include <api/SWA.h>
#include <ui/window.h>
#include <cfg/config.h>
// 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<float>*)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)

View file

@ -1,2 +1,3 @@
![Ww][Ii][Nn]32/
*.c
*.h

View file

@ -14,6 +14,7 @@
#include <string>
#include <cassert>
#include <chrono>
#include <span>
#include <xbox.h>
#include <xxhash.h>
#include <ankerl/unordered_dense.h>
@ -32,6 +33,7 @@
#include <wrl/client.h>
#include <smolv.h>
#include <print>
#include <set>
using Microsoft::WRL::ComPtr;

View file

@ -0,0 +1,678 @@
#include "achievement_menu.h"
#include "imgui_utils.h"
#include <api/SWA.h>
#include <gpu/video.h>
#include <kernel/xdbf.h>
#include <locale/locale.h>
#include <ui/button_guide.h>
#include <user/achievement_data.h>
#include <user/config.h>
#include <app.h>
#include <exports.h>
#include <decompressor.h>
#include <res/images/achievements_menu/trophy.dds.h>
#include <res/images/common/general_window.dds.h>
#include <res/images/common/select_fill.dds.h>
#include <gpu/imgui_snapshot.h>
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<std::tuple<Achievement, time_t>> g_achievements;
static ImFont* g_fntSeurat;
static ImFont* g_fntNewRodinDB;
static ImFont* g_fntNewRodinUB;
static std::unique_ptr<GuestTexture> g_upTrophyIcon;
static std::unique_ptr<GuestTexture> g_upSelectionCursor;
static std::unique_ptr<GuestTexture> 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<int>
(
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, &timestamp);
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<int>
(
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<int>
(
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");
}

View file

@ -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();
};

View file

@ -0,0 +1,200 @@
#include "achievement_overlay.h"
#include "imgui_utils.h"
#include <gpu/video.h>
#include <kernel/memory.h>
#include <kernel/xdbf.h>
#include <locale/locale.h>
#include <user/config.h>
#include <user/achievement_data.h>
#include <app.h>
#include <exports.h>
#include <decompressor.h>
#include <res/images/common/general_window.dds.h>
#include <gpu/imgui_snapshot.h>
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<GuestTexture> 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();
}
}

View file

@ -0,0 +1,16 @@
#pragma once
#include <queue>
class AchievementOverlay
{
public:
inline static bool s_isVisible = false;
inline static std::queue<uint16_t> s_queue{};
static void Init();
static void Draw();
static void Open(int id);
static void Close();
};

View file

@ -0,0 +1,305 @@
#include "button_guide.h"
#include "imgui_utils.h"
#include <gpu/imgui_snapshot.h>
#include <gpu/video.h>
#include <decompressor.h>
#include <res/images/common/left_mouse_button.dds.h>
#include <res/images/common/mat_comon_x360_001.dds.h>
#include <res/images/common/start_back.dds.h>
constexpr float DEFAULT_SIDE_MARGINS = 379;
ImFont* g_fntNewRodin;
ImFont* g_fntNewRodinLQ;
std::unique_ptr<GuestTexture> g_upIcons;
std::unique_ptr<GuestTexture> g_upLMBIcon;
std::unique_ptr<GuestTexture> g_upStartBackIcons;
float g_sideMargins = DEFAULT_SIDE_MARGINS;
std::vector<Button> g_buttons;
std::unordered_map<EButtonIcon, float> g_iconWidths =
{
{ EButtonIcon::A, 40 },
{ EButtonIcon::B, 40 },
{ EButtonIcon::X, 40 },
{ EButtonIcon::Y, 40 },
{ EButtonIcon::LB, 70 },
{ EButtonIcon::RB, 70 },
{ EButtonIcon::LBRB, 70 },
{ EButtonIcon::LT, 42 },
{ EButtonIcon::RT, 42 },
{ EButtonIcon::LTRT, 42 },
{ EButtonIcon::Start, 40 },
{ EButtonIcon::Back, 40 },
{ EButtonIcon::LMB, 40 }
};
std::unordered_map<EButtonIcon, float> g_iconHeights =
{
{ EButtonIcon::A, 40 },
{ EButtonIcon::B, 40 },
{ EButtonIcon::X, 40 },
{ EButtonIcon::Y, 40 },
{ EButtonIcon::LB, 40 },
{ EButtonIcon::RB, 40 },
{ EButtonIcon::LBRB, 40 },
{ EButtonIcon::LT, 42 },
{ EButtonIcon::RT, 42 },
{ EButtonIcon::LTRT, 42 },
{ EButtonIcon::Start, 40 },
{ EButtonIcon::Back, 40 },
{ EButtonIcon::LMB, 40 }
};
std::tuple<std::tuple<ImVec2, ImVec2>, GuestTexture*> GetButtonIcon(EButtonIcon icon)
{
std::tuple<ImVec2, ImVec2> btn;
GuestTexture* texture;
switch (icon)
{
case EButtonIcon::A:
btn = PIXELS_TO_UV_COORDS(512, 512, 0, 0, 40, 40);
texture = g_upIcons.get();
break;
case EButtonIcon::B:
btn = PIXELS_TO_UV_COORDS(512, 512, 40, 0, 40, 40);
texture = g_upIcons.get();
break;
case EButtonIcon::X:
btn = PIXELS_TO_UV_COORDS(512, 512, 80, 0, 40, 40);
texture = g_upIcons.get();
break;
case EButtonIcon::Y:
btn = PIXELS_TO_UV_COORDS(512, 512, 120, 0, 40, 40);
texture = g_upIcons.get();
break;
case EButtonIcon::LB:
btn = PIXELS_TO_UV_COORDS(512, 512, 166, 0, 70, 40);
texture = g_upIcons.get();
break;
case EButtonIcon::RB:
btn = PIXELS_TO_UV_COORDS(512, 512, 246, 0, 70, 40);
texture = g_upIcons.get();
break;
case EButtonIcon::LT:
btn = PIXELS_TO_UV_COORDS(512, 512, 319, 0, 42, 42);
texture = g_upIcons.get();
break;
case EButtonIcon::RT:
btn = PIXELS_TO_UV_COORDS(512, 512, 359, 0, 42, 42);
texture = g_upIcons.get();
break;
case EButtonIcon::Start:
btn = PIXELS_TO_UV_COORDS(256, 256, 0, 0, 128, 128);
texture = g_upStartBackIcons.get();
break;
case EButtonIcon::Back:
btn = PIXELS_TO_UV_COORDS(256, 256, 0, 128, 128, 128);
texture = g_upStartBackIcons.get();
break;
case EButtonIcon::LMB:
btn = PIXELS_TO_UV_COORDS(128, 128, 0, 0, 128, 128);
texture = g_upLMBIcon.get();
break;
}
return std::make_tuple(btn, texture);
}
ImFont* GetFont(EFontQuality fontQuality)
{
return fontQuality == EFontQuality::Low
? g_fntNewRodinLQ
: g_fntNewRodin;
}
static void DrawGuide(float* offset, ImVec2 regionMin, ImVec2 regionMax, EButtonIcon icon,
EButtonAlignment alignment, ImVec2 iconMin, ImVec2 iconMax, EFontQuality fontQuality,
ImVec2 textSize, float fontSize, const char* text)
{
auto drawList = ImGui::GetForegroundDrawList();
auto _icon = icon;
auto iconWidth = Scale(g_iconWidths[icon]);
auto dualIconMarginX = Scale(25);
if (icon == EButtonIcon::LBRB)
{
_icon = EButtonIcon::LB;
}
else if (icon == EButtonIcon::LTRT)
{
_icon = EButtonIcon::LT;
}
else
{
dualIconMarginX = 0;
}
if (icon == EButtonIcon::LBRB || icon == EButtonIcon::LTRT)
{
iconMin = alignment == EButtonAlignment::Left
? ImVec2(/* X */ regionMin.x + *offset - dualIconMarginX * 3, /* Y */ iconMin.y)
: ImVec2(/* X */ regionMax.x - *offset - textSize.x - iconWidth, /* Y */ iconMin.y);
iconMax = alignment == EButtonAlignment::Left
? ImVec2(iconMin.x + iconWidth, iconMax.y)
: ImVec2(iconMin.x, iconMax.y);
}
auto btnIcon = GetButtonIcon(_icon);
drawList->AddImage(std::get<1>(btnIcon), iconMin, iconMax, GET_UV_COORDS(std::get<0>(btnIcon)));
auto textMarginX = alignment == EButtonAlignment::Left
? regionMin.x + *offset + dualIconMarginX
: regionMax.x - *offset - dualIconMarginX * 2;
DrawTextWithOutline<int>
(
GetFont(fontQuality),
fontSize,
{ /* X */ textMarginX, /* Y */ regionMin.y + Scale(8) },
IM_COL32(255, 255, 255, 255),
text,
2,
IM_COL32(0, 0, 0, 255)
);
if (icon == EButtonIcon::LBRB || icon == EButtonIcon::LTRT)
{
auto btnIcon = GetButtonIcon(icon == EButtonIcon::LBRB ? EButtonIcon::RB : EButtonIcon::RT);
auto dualIconMin = alignment == EButtonAlignment::Left
? ImVec2(/* X */ regionMin.x + *offset + textSize.x + dualIconMarginX * 2, /* Y */ iconMin.y)
: ImVec2(/* X */ regionMax.x - *offset + textSize.x - dualIconMarginX, /* Y */ iconMin.y);
auto dualIconMax = ImVec2(dualIconMin.x + iconWidth, iconMax.y);
drawList->AddImage(std::get<1>(btnIcon), dualIconMin, dualIconMax, GET_UV_COORDS(std::get<0>(btnIcon)));
*offset += dualIconMarginX + iconWidth;
}
}
void ButtonGuide::Init()
{
auto& io = ImGui::GetIO();
constexpr float FONT_SCALE = 2.0f;
g_fntNewRodin = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-M.otf", 24.0f * FONT_SCALE);
g_fntNewRodinLQ = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-M.otf", 19.0f);
g_upIcons = LoadTexture(
decompressZstd(g_mat_comon_x360_001, g_mat_comon_x360_001_uncompressed_size).get(),
g_mat_comon_x360_001_uncompressed_size);
g_upLMBIcon = LoadTexture(
decompressZstd(g_left_mouse_button, g_left_mouse_button_uncompressed_size).get(),
g_left_mouse_button_uncompressed_size);
g_upStartBackIcons = LoadTexture(
decompressZstd(g_start_back, g_start_back_uncompressed_size).get(),
g_start_back_uncompressed_size);
}
void ButtonGuide::Draw()
{
if (!s_isVisible)
return;
auto drawList = ImGui::GetForegroundDrawList();
auto& res = ImGui::GetIO().DisplaySize;
auto regionMarginX = Scale(g_sideMargins);
auto regionHeight = Scale(102);
ImVec2 regionMin = { regionMarginX, res.y - regionHeight };
ImVec2 regionMax = { res.x - regionMarginX, res.y };
auto textMarginX = Scale(57);
auto textMarginY = Scale(8);
auto iconMarginX = Scale(4);
auto fontSize = Scale(22.5f);
auto offsetLeft = 0.0f;
auto offsetRight = 0.0f;
// Draw left aligned icons.
for (int i = 0; i < g_buttons.size(); i++)
{
auto& btn = g_buttons[i];
if (btn.Alignment != EButtonAlignment::Left)
continue;
if (btn.Visibility && !*btn.Visibility)
continue;
auto iconWidth = Scale(g_iconWidths[btn.Icon]);
auto iconHeight = Scale(g_iconHeights[btn.Icon]);
auto textSize = g_fntNewRodin->CalcTextSizeA(fontSize, FLT_MAX, 0, btn.Name.c_str());
if (i > 0)
offsetLeft += textSize.x + iconWidth + textMarginX;
ImVec2 iconMin = { regionMin.x + offsetLeft - iconWidth - iconMarginX, regionMin.y };
ImVec2 iconMax = { regionMin.x + offsetLeft - iconMarginX, regionMin.y + iconHeight };
DrawGuide(&offsetLeft, regionMin, regionMax, btn.Icon, btn.Alignment, iconMin, iconMax, btn.FontQuality, textSize, fontSize, btn.Name.c_str());
}
// Draw right aligned icons.
for (int i = g_buttons.size() - 1; i >= 0; i--)
{
auto& btn = g_buttons[i];
if (btn.Alignment != EButtonAlignment::Right)
continue;
if (btn.Visibility && !*btn.Visibility)
continue;
auto iconWidth = Scale(g_iconWidths[btn.Icon]);
auto iconHeight = Scale(g_iconHeights[btn.Icon]);
auto textSize = g_fntNewRodin->CalcTextSizeA(fontSize, FLT_MAX, 0, btn.Name.c_str());
if (i < g_buttons.size() - 1)
offsetRight += textSize.x + iconWidth + textMarginX;
ImVec2 iconMin = { regionMax.x - offsetRight - iconWidth - iconMarginX, regionMin.y };
ImVec2 iconMax = { regionMax.x - offsetRight - iconMarginX, regionMin.y + iconHeight };
DrawGuide(&offsetRight, regionMin, regionMax, btn.Icon, btn.Alignment, iconMin, iconMax, btn.FontQuality, textSize, fontSize, btn.Name.c_str());
}
}
void ButtonGuide::Open(const std::vector<Button> buttons)
{
s_isVisible = true;
g_sideMargins = DEFAULT_SIDE_MARGINS;
g_buttons = buttons;
}
void ButtonGuide::SetSideMargins(float width = DEFAULT_SIDE_MARGINS)
{
g_sideMargins = width;
}
void ButtonGuide::Close()
{
s_isVisible = false;
}

View file

@ -0,0 +1,61 @@
#pragma once
#include <gpu/video.h>
enum class EButtonIcon
{
A,
B,
X,
Y,
LB,
RB,
LBRB,
LT,
RT,
LTRT,
Start,
Back,
LMB
};
enum class EButtonAlignment
{
Left,
Right
};
enum class EFontQuality
{
Low,
High
};
class Button
{
public:
std::string Name{};
EButtonIcon Icon{};
EButtonAlignment Alignment{ EButtonAlignment::Right };
EFontQuality FontQuality{ EFontQuality::High };
bool* Visibility{ nullptr };
Button(std::string name, EButtonIcon icon, EButtonAlignment alignment, EFontQuality fontQuality = EFontQuality::High, bool* visibility = nullptr)
: Name(name), Icon(icon), Alignment(alignment), FontQuality(fontQuality), Visibility(visibility) {}
Button(std::string name, EButtonIcon icon, bool* visibility) : Name(name), Icon(icon), Visibility(visibility) {}
Button(std::string name, EButtonIcon icon) : Name(name), Icon(icon) {}
};
class ButtonGuide
{
public:
inline static bool s_isVisible = false;
static void Init();
static void Draw();
static void Open(const std::vector<Button> buttons);
static void SetSideMargins(float width);
static void Close();
};

View file

@ -0,0 +1,362 @@
#pragma once
#include <gpu/imgui_common.h>
#include <gpu/video.h>
#include <app.h>
#define PIXELS_TO_UV_COORDS(textureWidth, textureHeight, x, y, width, height) \
std::make_tuple(ImVec2((float)x / (float)textureWidth, (float)y / (float)textureHeight), \
ImVec2(((float)x + (float)width) / (float)textureWidth, ((float)y + (float)height) / (float)textureHeight))
#define GET_UV_COORDS(tuple) std::get<0>(tuple), std::get<1>(tuple)
#define CENTRE_TEXT_HORZ(min, max, textSize) min.x + ((max.x - min.x) - textSize.x) / 2
#define CENTRE_TEXT_VERT(min, max, textSize) min.y + ((max.y - min.y) - textSize.y) / 2
static void SetGradient(const ImVec2& min, const ImVec2& max, ImU32 top, ImU32 bottom)
{
auto callbackData = AddImGuiCallback(ImGuiCallback::SetGradient);
callbackData->setGradient.gradientMin[0] = min.x;
callbackData->setGradient.gradientMin[1] = min.y;
callbackData->setGradient.gradientMax[0] = max.x;
callbackData->setGradient.gradientMax[1] = max.y;
callbackData->setGradient.gradientTop = top;
callbackData->setGradient.gradientBottom = bottom;
}
static void ResetGradient()
{
auto callbackData = AddImGuiCallback(ImGuiCallback::SetGradient);
memset(&callbackData->setGradient, 0, sizeof(callbackData->setGradient));
}
static void SetShaderModifier(uint32_t shaderModifier)
{
auto callbackData = AddImGuiCallback(ImGuiCallback::SetShaderModifier);
callbackData->setShaderModifier.shaderModifier = shaderModifier;
}
static void SetOrigin(ImVec2 origin)
{
auto callbackData = AddImGuiCallback(ImGuiCallback::SetOrigin);
callbackData->setOrigin.origin[0] = origin.x;
callbackData->setOrigin.origin[1] = origin.y;
}
static void SetScale(ImVec2 scale)
{
auto callbackData = AddImGuiCallback(ImGuiCallback::SetScale);
callbackData->setScale.scale[0] = scale.x;
callbackData->setScale.scale[1] = scale.y;
}
// Aspect ratio aware.
static float Scale(float size)
{
auto& io = ImGui::GetIO();
if (io.DisplaySize.x > io.DisplaySize.y)
return size * std::max(1.0f, io.DisplaySize.y / 720.0f);
else
return size * std::max(1.0f, io.DisplaySize.x / 1280.0f);
}
// Not aspect ratio aware. Will stretch.
static float ScaleX(float x)
{
auto& io = ImGui::GetIO();
return x * io.DisplaySize.x / 1280.0f;
}
// Not aspect ratio aware. Will stretch.
static float ScaleY(float y)
{
auto& io = ImGui::GetIO();
return y * io.DisplaySize.y / 720.0f;
}
static double ComputeMotion(double duration, double offset, double total)
{
return sqrt(std::clamp((ImGui::GetTime() - duration - offset / 60.0) / total * 60.0, 0.0, 1.0));
}
static void DrawPauseContainer(GuestTexture* texture, ImVec2 min, ImVec2 max, float alpha = 1)
{
auto drawList = ImGui::GetForegroundDrawList();
auto commonWidth = Scale(35);
auto commonHeight = Scale(35);
auto bottomHeight = Scale(5);
auto tl = PIXELS_TO_UV_COORDS(512, 512, 0, 0, 35, 35);
auto tc = PIXELS_TO_UV_COORDS(512, 512, 51, 0, 5, 35);
auto tr = PIXELS_TO_UV_COORDS(512, 512, 70, 0, 35, 35);
auto cl = PIXELS_TO_UV_COORDS(512, 512, 0, 35, 35, 235);
auto cc = PIXELS_TO_UV_COORDS(512, 512, 51, 35, 5, 235);
auto cr = PIXELS_TO_UV_COORDS(512, 512, 70, 35, 35, 235);
auto bl = PIXELS_TO_UV_COORDS(512, 512, 0, 270, 35, 40);
auto bc = PIXELS_TO_UV_COORDS(512, 512, 51, 270, 5, 40);
auto br = PIXELS_TO_UV_COORDS(512, 512, 70, 270, 35, 40);
auto colour = IM_COL32(255, 255, 255, 255 * alpha);
drawList->AddImage(texture, min, { min.x + commonWidth, min.y + commonHeight }, GET_UV_COORDS(tl), colour);
drawList->AddImage(texture, { min.x + commonWidth, min.y }, { max.x - commonWidth, min.y + commonHeight }, GET_UV_COORDS(tc), colour);
drawList->AddImage(texture, { max.x - commonWidth, min.y }, { max.x, min.y + commonHeight }, GET_UV_COORDS(tr), colour);
drawList->AddImage(texture, { min.x, min.y + commonHeight }, { min.x + commonWidth, max.y - commonHeight }, GET_UV_COORDS(cl), colour);
drawList->AddImage(texture, { min.x + commonWidth, min.y + commonHeight }, { max.x - commonWidth, max.y - commonHeight }, GET_UV_COORDS(cc), colour);
drawList->AddImage(texture, { max.x - commonWidth, min.y + commonHeight }, { max.x, max.y - commonHeight }, GET_UV_COORDS(cr), colour);
drawList->AddImage(texture, { min.x, max.y - commonHeight }, { min.x + commonWidth, max.y + bottomHeight }, GET_UV_COORDS(bl), colour);
drawList->AddImage(texture, { min.x + commonWidth, max.y - commonHeight }, { max.x - commonWidth, max.y + bottomHeight }, GET_UV_COORDS(bc), colour);
drawList->AddImage(texture, { max.x - commonWidth, max.y - commonHeight }, { max.x, max.y + bottomHeight }, GET_UV_COORDS(br), colour);
}
static void DrawPauseHeaderContainer(GuestTexture* texture, ImVec2 min, ImVec2 max, float alpha = 1)
{
auto drawList = ImGui::GetForegroundDrawList();
auto commonWidth = Scale(35);
auto left = PIXELS_TO_UV_COORDS(512, 512, 0, 314, 35, 60);
auto centre = PIXELS_TO_UV_COORDS(512, 512, 51, 314, 5, 60);
auto right = PIXELS_TO_UV_COORDS(512, 512, 70, 314, 35, 60);
auto colour = IM_COL32(255, 255, 255, 255 * alpha);
drawList->AddImage(texture, min, { min.x + commonWidth, max.y }, GET_UV_COORDS(left), colour);
drawList->AddImage(texture, { min.x + commonWidth, min.y }, { max.x - commonWidth, max.y }, GET_UV_COORDS(centre), colour);
drawList->AddImage(texture, { max.x - commonWidth, min.y }, max, GET_UV_COORDS(right), colour);
}
static void DrawTextWithMarquee(const ImFont* font, float fontSize, const ImVec2& pos, const ImVec2& min, const ImVec2& max, ImU32 color, const char* text, double time, double delay, double speed)
{
auto drawList = ImGui::GetForegroundDrawList();
auto rectWidth = max.x - min.x;
auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, text);
auto textX = pos.x - fmodf(std::max(0.0, ImGui::GetTime() - (time + delay)) * speed, textSize.x + rectWidth);
drawList->PushClipRect(min, max, true);
if (textX <= pos.x)
drawList->AddText(font, fontSize, { textX, pos.y }, color, text);
if (textX + textSize.x < pos.x)
drawList->AddText(font, fontSize, { textX + textSize.x + rectWidth, pos.y }, color, text);
drawList->PopClipRect();
}
template<typename T>
static void DrawTextWithOutline(const ImFont* font, float fontSize, const ImVec2& pos, ImU32 color, const char* text, T outlineSize, ImU32 outlineColor)
{
auto drawList = ImGui::GetForegroundDrawList();
outlineSize = Scale(outlineSize);
if constexpr (std::is_same_v<float, T> || std::is_same_v<double, T>)
{
// TODO: This is still very inefficient!
for (float i = -outlineSize; i <= outlineSize; i += 0.5f)
{
for (float j = -outlineSize; j <= outlineSize; j += 0.5f)
{
if (i == 0.0f && j == 0.0f)
continue;
drawList->AddText(font, fontSize, { pos.x + i, pos.y + j }, outlineColor, text);
}
}
}
else if constexpr (std::is_integral_v<T>)
{
// TODO: This is very inefficient!
for (int32_t i = -outlineSize + 1; i < outlineSize; i++)
{
for (int32_t j = -outlineSize + 1; j < outlineSize; j++)
drawList->AddText(font, fontSize, { pos.x + i, pos.y + j }, outlineColor, text);
}
}
drawList->AddText(font, fontSize, pos, color, text);
}
static void DrawTextWithShadow(const ImFont* font, float fontSize, const ImVec2& pos, ImU32 colour, const char* text, float offset = 2.0f, float radius = 0.4f, ImU32 shadowColour = IM_COL32(0, 0, 0, 255))
{
auto drawList = ImGui::GetForegroundDrawList();
offset = Scale(offset);
radius = Scale(radius);
DrawTextWithOutline<float>(font, fontSize, { pos.x + offset, pos.y + offset }, shadowColour, text, radius, shadowColour);
drawList->AddText(font, fontSize, pos, colour, text, nullptr);
}
static float CalcWidestTextSize(const ImFont* font, float fontSize, std::span<std::string> strs)
{
auto result = 0.0f;
for (auto& str : strs)
result = std::max(result, font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()).x);
return result;
}
static std::string Truncate(const std::string& input, size_t maxLength, bool useEllipsis = true, bool usePrefixEllipsis = false)
{
const std::string ellipsis = "...";
if (input.length() > maxLength)
{
if (useEllipsis && maxLength > ellipsis.length())
{
if (usePrefixEllipsis)
{
return ellipsis + input.substr(0, maxLength - ellipsis.length());
}
else
{
return input.substr(0, maxLength - ellipsis.length()) + ellipsis;
}
}
else
{
return input.substr(0, maxLength);
}
}
return input;
}
static std::vector<std::string> Split(const char* str, char delimiter)
{
std::vector<std::string> result;
if (!str)
return result;
const char* start = str;
const char* current = str;
while (*current)
{
if (*current == delimiter)
{
result.emplace_back(start, current - start);
start = current + 1;
}
current++;
}
result.emplace_back(start);
return result;
}
static ImVec2 MeasureCentredParagraph(const ImFont* font, float fontSize, float lineMargin, std::vector<std::string> lines)
{
auto x = 0.0f;
auto y = 0.0f;
for (auto& str : lines)
{
auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str());
x = std::max(x, textSize.x);
y += textSize.y + Scale(lineMargin);
}
return { x, y };
}
static ImVec2 MeasureCentredParagraph(const ImFont* font, float fontSize, float lineMargin, const char* text)
{
return MeasureCentredParagraph(font, fontSize, lineMargin, Split(text, '\n'));
}
static void DrawCentredParagraph(const ImFont* font, float fontSize, const ImVec2& centre, float lineMargin, const char* text, std::function<void(const char*, ImVec2)> drawMethod)
{
auto lines = Split(text, '\n');
auto paragraphSize = MeasureCentredParagraph(font, fontSize, lineMargin, lines);
auto offsetY = 0.0f;
auto hasList = std::strstr(text, "- ");
auto isList = false;
auto listOffsetX = 0.0f;
for (int i = 0; i < lines.size(); i++)
{
auto& str = lines[i];
auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str());
if (hasList)
{
if (!isList && str.starts_with("- ") && lines.size() > i + 1 && lines[i + 1].starts_with("- "))
{
isList = true;
listOffsetX = centre.x - textSize.x / 2;
}
else if (isList && !str.starts_with("- "))
{
isList = false;
}
}
drawMethod(str.c_str(), ImVec2(/* X */ isList ? listOffsetX : centre.x - textSize.x / 2, /* Y */ centre.y - paragraphSize.y / 2 + offsetY));
offsetY += textSize.y + Scale(lineMargin);
}
}
static void DrawTextWithMarqueeShadow(const ImFont* font, float fontSize, const ImVec2& pos, const ImVec2& min, const ImVec2& max, ImU32 colour, const char* text, double time, double delay, double speed, float offset = 2.0f, float radius = 0.4f, ImU32 shadowColour = IM_COL32(0, 0, 0, 255))
{
auto drawList = ImGui::GetForegroundDrawList();
auto rectWidth = max.x - min.x;
auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, text);
auto textX = pos.x - fmodf(std::max(0.0, ImGui::GetTime() - (time + delay)) * speed, textSize.x + rectWidth);
drawList->PushClipRect(min, max, true);
if (textX <= pos.x)
DrawTextWithShadow(font, fontSize, { textX, pos.y }, colour, text, offset, radius, shadowColour);
if (textX + textSize.x < pos.x)
DrawTextWithShadow(font, fontSize, { textX + textSize.x + rectWidth, pos.y }, colour, text, offset, radius, shadowColour);
drawList->PopClipRect();
}
static float Lerp(float a, float b, float t)
{
return a + (b - a) * t;
}
static float Cubic(float a, float b, float t)
{
return a + (b - a) * (t * t * t);
}
static float Hermite(float a, float b, float t)
{
return a + (b - a) * (t * t * (3 - 2 * t));
}
static ImVec2 Lerp(const ImVec2& a, const ImVec2& b, float t)
{
return { a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t };
}
static ImU32 ColourLerp(ImU32 c0, ImU32 c1, float t)
{
auto a = ImGui::ColorConvertU32ToFloat4(c0);
auto b = ImGui::ColorConvertU32ToFloat4(c1);
ImVec4 result;
result.x = a.x + (b.x - a.x) * t;
result.y = a.y + (b.y - a.y) * t;
result.z = a.z + (b.z - a.z) * t;
result.w = a.w + (b.w - a.w) * t;
return ImGui::ColorConvertFloat4ToU32(result);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
#pragma once
#include <api/SWA.h>
struct InstallerWizard
{
inline static bool s_isVisible = false;
static void Init();
static void Draw();
static void Shutdown();
static bool Run(bool skipGame);
};

View file

@ -0,0 +1,351 @@
#include "message_window.h"
#include "imgui_utils.h"
#include <api/SWA.h>
#include <gpu/video.h>
#include <locale/locale.h>
#include <ui/button_guide.h>
#include <app.h>
#include <exports.h>
#include <res/images/common/general_window.dds.h>
#include <decompressor.h>
#include <res/images/common/select_fade.dds.h>
#include <gpu/imgui_snapshot.h>
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;
static bool g_isAwaitingResult = false;
static bool g_isClosing = false;
static bool g_isControlsVisible = false;
static int g_selectedRowIndex;
static int g_foregroundCount;
static bool g_upWasHeld;
static bool g_downWasHeld;
static double g_appearTime;
static double g_controlsAppearTime;
static ImFont* g_fntSeurat;
static std::unique_ptr<GuestTexture> g_upSelectionCursor;
static std::unique_ptr<GuestTexture> g_upWindow;
std::string g_text;
int g_result;
std::vector<std::string> g_buttons;
int g_defaultButtonIndex;
bool DrawContainer(float appearTime, ImVec2 centre, ImVec2 max, bool isForeground = true)
{
auto drawList = ImGui::GetForegroundDrawList();
ImVec2 _min = { centre.x - max.x, centre.y - max.y };
ImVec2 _max = { centre.x + max.x, centre.y + max.y };
// Expand/retract animation.
auto containerMotion = ComputeMotion(appearTime, OVERLAY_CONTAINER_COMMON_MOTION_START, OVERLAY_CONTAINER_COMMON_MOTION_END);
if (g_isClosing)
{
_min.x = Hermite(_min.x, centre.x, containerMotion);
_max.x = Hermite(_max.x, centre.x, containerMotion);
_min.y = Hermite(_min.y, centre.y, containerMotion);
_max.y = Hermite(_max.y, centre.y, containerMotion);
}
else
{
_min.x = Hermite(centre.x, _min.x, containerMotion);
_max.x = Hermite(centre.x, _max.x, containerMotion);
_min.y = Hermite(centre.y, _min.y, containerMotion);
_max.y = Hermite(centre.y, _max.y, containerMotion);
}
// Transparency fade animation.
auto colourMotion = g_isClosing
? ComputeMotion(appearTime, OVERLAY_CONTAINER_OUTRO_FADE_START, OVERLAY_CONTAINER_OUTRO_FADE_END)
: ComputeMotion(appearTime, OVERLAY_CONTAINER_INTRO_FADE_START, OVERLAY_CONTAINER_INTRO_FADE_END);
auto alpha = g_isClosing
? Lerp(1, 0, colourMotion)
: Lerp(0, 1, colourMotion);
if (!isForeground)
g_foregroundCount++;
if (isForeground)
drawList->AddRectFilled({ 0.0f, 0.0f }, ImGui::GetIO().DisplaySize, IM_COL32(0, 0, 0, 223 * (g_foregroundCount ? 1 : alpha)));
DrawPauseContainer(g_upWindow.get(), _min, _max, alpha);
drawList->PushClipRect(_min, _max);
return containerMotion >= 1.0f && !g_isClosing;
}
void DrawButton(int rowIndex, float yOffset, float width, float height, std::string& text)
{
auto drawList = ImGui::GetForegroundDrawList();
auto clipRectMin = drawList->GetClipRectMin();
auto clipRectMax = drawList->GetClipRectMax();
ImVec2 min = { clipRectMin.x + ((clipRectMax.x - clipRectMin.x) - width) / 2, clipRectMin.y + height * rowIndex + yOffset };
ImVec2 max = { min.x + width, min.y + height };
bool isSelected = rowIndex == g_selectedRowIndex;
if (isSelected)
{
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 width = Scale(11);
auto left = PIXELS_TO_UV_COORDS(64, 64, 0, 0, 11, 50);
auto centre = PIXELS_TO_UV_COORDS(64, 64, 11, 0, 8, 50);
auto right = PIXELS_TO_UV_COORDS(64, 64, 19, 0, 11, 50);
drawList->AddImage(g_upSelectionCursor.get(), min, { min.x + width, max.y }, GET_UV_COORDS(left), colour);
drawList->AddImage(g_upSelectionCursor.get(), { min.x + width, min.y }, { max.x - width, max.y }, GET_UV_COORDS(centre), colour);
drawList->AddImage(g_upSelectionCursor.get(), { max.x - width, min.y }, max, GET_UV_COORDS(right), colour);
}
auto fontSize = Scale(28);
auto textSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0, text.c_str());
DrawTextWithShadow
(
g_fntSeurat,
fontSize,
{ /* X */ min.x + ((max.x - min.x) - textSize.x) / 2, /* Y */ min.y + ((max.y - min.y) - textSize.y) / 2 },
isSelected ? IM_COL32(255, 128, 0, 255) : IM_COL32(255, 255, 255, 255),
text.c_str()
);
}
static void ResetSelection()
{
g_selectedRowIndex = g_defaultButtonIndex;
g_upWasHeld = false;
g_downWasHeld = false;
}
void MessageWindow::Init()
{
auto& io = ImGui::GetIO();
constexpr float FONT_SCALE = 2.0f;
g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE);
g_upSelectionCursor = LoadTexture(decompressZstd(g_select_fade, g_select_fade_uncompressed_size).get(), g_select_fade_uncompressed_size);
g_upWindow = LoadTexture(decompressZstd(g_general_window, g_general_window_uncompressed_size).get(), g_general_window_uncompressed_size);
}
void MessageWindow::Draw()
{
if (!s_isVisible)
return;
auto pInputState = g_isGameLoaded ? SWA::CInputState::GetInstance() : nullptr;
auto drawList = ImGui::GetForegroundDrawList();
auto& res = ImGui::GetIO().DisplaySize;
ImVec2 centre = { res.x / 2, res.y / 2 };
auto fontSize = Scale(28);
auto textSize = MeasureCentredParagraph(g_fntSeurat, fontSize, 5, g_text.c_str());
auto textMarginX = Scale(37);
auto textMarginY = Scale(45);
if (DrawContainer(g_appearTime, centre, { textSize.x / 2 + textMarginX, textSize.y / 2 + textMarginY }, !g_isControlsVisible))
{
DrawCentredParagraph
(
g_fntSeurat,
fontSize,
{ centre.x, centre.y + Scale(3) },
5,
g_text.c_str(),
[=](const char* str, ImVec2 pos)
{
DrawTextWithShadow(g_fntSeurat, fontSize, pos, IM_COL32(255, 255, 255, 255), str);
}
);
drawList->PopClipRect();
bool isAccepted = pInputState
? pInputState->GetPadState().IsTapped(SWA::eKeyState_A)
: ImGui::IsMouseClicked(ImGuiMouseButton_Left);
if (g_buttons.size())
{
auto itemWidth = std::max(Scale(162), Scale(CalcWidestTextSize(g_fntSeurat, fontSize, g_buttons)));
auto itemHeight = Scale(57);
auto windowMarginX = Scale(23);
auto windowMarginY = Scale(30);
ImVec2 controlsMax = { /* X */ itemWidth / 2 + windowMarginX, /* Y */ itemHeight / 2 * g_buttons.size() + windowMarginY };
if (g_isControlsVisible && DrawContainer(g_controlsAppearTime, centre, controlsMax))
{
auto rowCount = 0;
for (auto& button : g_buttons)
DrawButton(rowCount++, windowMarginY, itemWidth, itemHeight, button);
if (pInputState)
{
bool upIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) ||
pInputState->GetPadState().LeftStickVertical > 0.5f;
bool downIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) ||
pInputState->GetPadState().LeftStickVertical < -0.5f;
bool scrollUp = !g_upWasHeld && upIsHeld;
bool scrollDown = !g_downWasHeld && downIsHeld;
if (scrollUp)
{
--g_selectedRowIndex;
if (g_selectedRowIndex < 0)
g_selectedRowIndex = rowCount - 1;
}
else if (scrollDown)
{
++g_selectedRowIndex;
if (g_selectedRowIndex >= rowCount)
g_selectedRowIndex = 0;
}
if (scrollUp || scrollDown)
Game_PlaySound("sys_actstg_pausecursor");
g_upWasHeld = upIsHeld;
g_downWasHeld = downIsHeld;
if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B))
{
g_result = -1;
Game_PlaySound("sys_actstg_pausecansel");
MessageWindow::Close();
}
ButtonGuide::Open
(
{
Button(Localise("Common_Select"), EButtonIcon::A),
Button(Localise("Common_Back"),EButtonIcon::B),
}
);
}
else
{
auto clipRectMin = drawList->GetClipRectMin();
auto clipRectMax = drawList->GetClipRectMax();
g_selectedRowIndex = -1;
for (int i = 0; i < rowCount; i++)
{
ImVec2 itemMin = { clipRectMin.x + windowMarginX, clipRectMin.y + windowMarginY + itemHeight * i };
ImVec2 itemMax = { clipRectMax.x - windowMarginX, clipRectMin.y + windowMarginY + itemHeight * i + itemHeight };
if (ImGui::IsMouseHoveringRect(itemMin, itemMax, false))
g_selectedRowIndex = i;
}
ButtonGuide::Open({ Button(Localise("Common_Select"), EButtonIcon::LMB) });
}
if (g_selectedRowIndex != -1 && isAccepted)
{
g_result = g_selectedRowIndex;
Game_PlaySound("sys_actstg_pausedecide");
MessageWindow::Close();
}
drawList->PopClipRect();
}
else
{
if (!g_isControlsVisible && isAccepted)
{
g_controlsAppearTime = ImGui::GetTime();
g_isControlsVisible = true;
Game_PlaySound("sys_actstg_pausewinopen");
}
}
}
else
{
if (isAccepted)
{
g_result = 0;
MessageWindow::Close();
}
}
}
else if (g_isClosing)
{
s_isVisible = false;
}
}
bool MessageWindow::Open(std::string text, int* result, std::span<std::string> buttons, int defaultButtonIndex)
{
if (!g_isAwaitingResult && *result == -1)
{
s_isVisible = true;
g_isClosing = false;
g_isControlsVisible = false;
g_foregroundCount = 0;
g_appearTime = ImGui::GetTime();
g_controlsAppearTime = ImGui::GetTime();
g_text = text;
g_buttons = std::vector(buttons.begin(), buttons.end());
g_defaultButtonIndex = g_isGameLoaded ? defaultButtonIndex : -1;
ResetSelection();
ButtonGuide::Open({ Button(Localise("Common_Next"), g_isGameLoaded ? EButtonIcon::A : EButtonIcon::LMB) });
Game_PlaySound("sys_actstg_pausewinopen");
g_isAwaitingResult = true;
}
*result = g_result;
return !g_isAwaitingResult;
}
void MessageWindow::Close()
{
if (!g_isClosing)
{
g_appearTime = ImGui::GetTime();
g_controlsAppearTime = ImGui::GetTime();
g_isClosing = true;
g_isControlsVisible = false;
g_foregroundCount = 0;
g_isAwaitingResult = false;
ButtonGuide::Close();
}
Game_PlaySound("sys_actstg_pausewinclose");
}

View file

@ -0,0 +1,12 @@
#pragma once
class MessageWindow
{
public:
inline static bool s_isVisible = false;
static void Init();
static void Draw();
static bool Open(std::string text, int* result, std::span<std::string> buttons = {}, int defaultButtonIndex = 0);
static void Close();
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
#pragma once
#include <api/SWA.h>
class OptionsMenu
{
public:
inline static bool s_isVisible = false;
inline static bool s_isPause = false;
inline static SWA::EMenuType s_pauseMenuType;
static void Init();
static void Draw();
static void Open(bool isPause = false, SWA::EMenuType pauseMenuType = SWA::eMenuType_WorldMap);
static void Close();
static bool CanClose();
};

View file

@ -0,0 +1,7 @@
#include "sdl_listener.h"
std::vector<ISDLEventListener*>& GetEventListeners()
{
static std::vector<ISDLEventListener*> g_eventListeners;
return g_eventListeners;
}

View file

@ -1,7 +1,5 @@
#pragma once
#include "ui/window.h"
class ISDLEventListener
{
public:
@ -9,12 +7,14 @@ public:
virtual void OnSDLEvent(SDL_Event* event) = 0;
};
std::vector<ISDLEventListener*>& GetEventListeners();
class SDLEventListener : public ISDLEventListener
{
public:
SDLEventListener()
{
Window::s_eventListeners.emplace_back(this);
GetEventListeners().emplace_back(this);
}
void OnSDLEvent(SDL_Event* event) override {}

View file

@ -1,16 +1,17 @@
#include "window.h"
#include "sdl_listener.h"
#include <cfg/config.h>
#include <user/config.h>
#include <SDL_syswm.h>
bool m_isFullscreenKeyReleased = true;
bool m_isResizing = false;
int Window_OnSDLEvent(void*, SDL_Event* event)
{
if (ImGui::GetIO().BackendPlatformUserData != nullptr)
ImGui_ImplSDL2_ProcessEvent(event);
for (auto listener : Window::s_eventListeners)
for (auto listener : GetEventListeners())
listener->OnSDLEvent(event);
switch (event->type)
@ -84,7 +85,7 @@ int Window_OnSDLEvent(void*, SDL_Event* event)
case SDL_WINDOWEVENT_FOCUS_GAINED:
Window::s_isFocused = true;
SDL_ShowCursor(Window::IsFullscreen() ? SDL_DISABLE : SDL_ENABLE);
SDL_ShowCursor(Window::IsFullscreen() && !Window::s_cursorAllowed ? SDL_DISABLE : SDL_ENABLE);
break;
case SDL_WINDOWEVENT_RESTORED:
@ -96,8 +97,10 @@ int Window_OnSDLEvent(void*, SDL_Event* event)
break;
case SDL_WINDOWEVENT_RESIZED:
m_isResizing = true;
Window::s_width = event->window.data1;
Window::s_height = event->window.data2;
Window::SetTitle(std::format("{} - [{}x{}]", Window::GetTitle(), Window::s_width, Window::s_height).c_str());
break;
case SDL_WINDOWEVENT_MOVED:
@ -117,6 +120,20 @@ int Window_OnSDLEvent(void*, SDL_Event* event)
}
}
if (!Window::IsFullscreen())
{
if (event->type == SDL_CONTROLLERBUTTONDOWN || event->type == SDL_CONTROLLERBUTTONUP || event->type == SDL_CONTROLLERAXISMOTION)
{
// Hide mouse cursor when controller input is detected.
SDL_ShowCursor(SDL_DISABLE);
}
else if (event->type == SDL_MOUSEMOTION)
{
// Restore mouse cursor when mouse input is detected.
SDL_ShowCursor(SDL_ENABLE);
}
}
return 0;
}
@ -177,4 +194,10 @@ void Window::Update()
Config::WindowWidth = Window::s_width;
Config::WindowHeight = Window::s_height;
}
if (m_isResizing)
{
SetTitle();
m_isResizing = false;
}
}

View file

@ -1,9 +1,9 @@
#pragma once
#include <res/icon.h>
#include <res/icon_night.h>
#include <res/images/game_icon.bmp.h>
#include <res/images/game_icon_night.bmp.h>
#include <ui/window_events.h>
#include <cfg/config.h>
#include <user/config.h>
#if _WIN32
#include <dwmapi.h>
@ -14,8 +14,6 @@
#define DEFAULT_WIDTH 1280
#define DEFAULT_HEIGHT 720
class SDLEventListener;
class Window
{
public:
@ -29,8 +27,7 @@ public:
inline static bool s_isFocused;
inline static bool s_isIconNight;
inline static std::vector<SDLEventListener*> s_eventListeners;
inline static bool s_cursorAllowed = false;
static SDL_Surface* GetIconSurface(void* pIconBmp, size_t iconSize)
{
@ -56,24 +53,24 @@ public:
{
if (isNight)
{
SetIcon((void*)g_iconNight, g_iconNight_size);
SetIcon(g_game_icon_night, sizeof(g_game_icon_night));
}
else
{
SetIcon((void*)g_icon, g_icon_size);
SetIcon(g_game_icon, sizeof(g_game_icon));
}
}
static const char* GetTitle()
{
return Config::Language == ELanguage::Japanese
? "SONIC WORLD ADVENTURE"
: "SONIC UNLEASHED";
}
static void SetTitle(const char* title = nullptr)
{
if (!title)
{
title = Config::Language == ELanguage::Japanese
? "SONIC WORLD ADVENTURE"
: "SONIC UNLEASHED";
}
SDL_SetWindowTitle(s_pWindow, title);
SDL_SetWindowTitle(s_pWindow, title ? title : GetTitle());
}
static void SetDarkTitleBar(bool isEnabled)
@ -103,7 +100,7 @@ public:
if (isEnabled)
{
SDL_SetWindowFullscreen(s_pWindow, SDL_WINDOW_FULLSCREEN_DESKTOP);
SDL_ShowCursor(SDL_DISABLE);
SDL_ShowCursor(s_cursorAllowed ? SDL_ENABLE : SDL_DISABLE);
}
else
{
@ -114,6 +111,14 @@ public:
return isEnabled;
}
static void SetCursorAllowed(bool isCursorAllowed)
{
s_cursorAllowed = isCursorAllowed;
// Refresh fullscreen state to enable the right cursor behavior.
SetFullscreen(IsFullscreen());
}
static bool IsMaximised()
{

View file

@ -0,0 +1,174 @@
#include "achievement_data.h"
#include <ui/achievement_overlay.h>
#include <user/config.h>
#define NUM_RECORDS sizeof(Data.Records) / sizeof(Record)
time_t AchievementData::GetTimestamp(uint16_t id)
{
for (int i = 0; i < NUM_RECORDS; i++)
{
if (!Data.Records[i].ID)
break;
if (Data.Records[i].ID == id)
return Data.Records[i].Timestamp;
}
return 0;
}
int AchievementData::GetTotalRecords()
{
auto result = 0;
for (int i = 0; i < NUM_RECORDS; i++)
{
if (!Data.Records[i].ID)
break;
result++;
}
return result;
}
bool AchievementData::IsUnlocked(uint16_t id)
{
for (int i = 0; i < NUM_RECORDS; i++)
{
if (!Data.Records[i].ID)
break;
if (Data.Records[i].ID == id)
return true;
}
return false;
}
void AchievementData::Unlock(uint16_t id)
{
if (IsUnlocked(id))
return;
for (int i = 0; i < NUM_RECORDS; i++)
{
if (Data.Records[i].ID == 0)
{
Data.Records[i].ID = id;
Data.Records[i].Timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
break;
}
}
if (Config::AchievementNotifications)
AchievementOverlay::Open(id);
}
uint32_t AchievementData::CalculateChecksum()
{
auto result = 0;
for (int i = 0; i < NUM_RECORDS; i++)
{
auto& record = Data.Records[i];
for (size_t j = 0; j < sizeof(Record); j++)
result ^= ((uint8_t*)(&record))[j];
}
return result;
}
bool AchievementData::VerifySignature()
{
char sig[4] = ACH_SIGNATURE;
return Data.Signature[0] == sig[0] &&
Data.Signature[1] == sig[1] &&
Data.Signature[2] == sig[2] &&
Data.Signature[3] == sig[3];
}
bool AchievementData::VerifyVersion()
{
return Data.Version == Version ACH_VERSION;
}
bool AchievementData::VerifyChecksum()
{
return Data.Checksum == CalculateChecksum();
}
void AchievementData::Load()
{
auto dataPath = GetDataPath();
if (!std::filesystem::exists(dataPath))
return;
std::ifstream file(dataPath, std::ios::binary);
if (!file)
{
printf("[*] ERROR: failed to read achievement data.\n");
return;
}
file.read((char*)&Data.Signature, sizeof(Data.Signature));
if (!VerifySignature())
{
printf("[*] ERROR: invalid achievement data signature.\n");
char sig[4] = ACH_SIGNATURE;
Data.Signature[0] = sig[0];
Data.Signature[1] = sig[1];
Data.Signature[2] = sig[2];
Data.Signature[3] = sig[3];
file.close();
return;
}
file.read((char*)&Data.Version, sizeof(Data.Version));
if (!VerifyVersion())
{
printf("[*] ERROR: unsupported achievement data version.\n");
Data.Version = ACH_VERSION;
file.close();
return;
}
file.seekg(0);
file.read((char*)&Data, sizeof(Data));
// TODO: display error message to user before wiping data?
if (!VerifyChecksum())
{
printf("[*] ERROR: achievement data checksum mismatch.\n");
memset(&Data.Records, 0, sizeof(Data.Records));
}
file.close();
}
void AchievementData::Save()
{
std::ofstream file(GetDataPath(), std::ios::binary);
if (!file)
{
printf("[*] ERROR: failed to write achievement data.\n");
return;
}
Data.Checksum = CalculateChecksum();
file.write((const char*)&Data, sizeof(Data));
file.close();
}

View file

@ -0,0 +1,63 @@
#pragma once
#include <user/paths.h>
#define ACH_SIGNATURE { 'A', 'C', 'H', ' ' }
#define ACH_VERSION { 1, 0, 0 }
#define ACH_RECORDS 50
class AchievementData
{
public:
#pragma pack(push, 1)
struct Record
{
uint16_t ID;
time_t Timestamp;
uint16_t Reserved[3];
};
#pragma pack(pop)
struct Version
{
uint8_t Major;
uint8_t Minor;
uint8_t Revision;
uint8_t Reserved;
bool operator==(const Version& other) const
{
return Major == other.Major &&
Minor == other.Minor &&
Revision == other.Revision;
}
};
class Data
{
public:
char Signature[4];
Version Version{};
uint32_t Checksum;
uint32_t Reserved;
Record Records[ACH_RECORDS];
};
inline static Data Data{ ACH_SIGNATURE, ACH_VERSION };
static std::filesystem::path GetDataPath()
{
return GetSavePath() / "ACH-DATA";
}
static time_t GetTimestamp(uint16_t id);
static int GetTotalRecords();
static bool IsUnlocked(uint16_t id);
static void Unlock(uint16_t id);
static uint32_t CalculateChecksum();
static bool VerifySignature();
static bool VerifyVersion();
static bool VerifyChecksum();
static void Load();
static void Save();
};

View file

@ -18,13 +18,13 @@ void Config::Load()
{
def->ReadValue(toml);
#if _DEBUG
printf("%s\n", def->GetDefinition().c_str());
printf("%s (0x%p)\n", def->GetDefinition().c_str(), def->GetValue());
#endif
}
}
catch (toml::parse_error& err)
{
printf("Failed to parse configuration: %s\n", err.what());
printf("[*] Failed to parse configuration: %s\n", err.what());
}
}
@ -62,6 +62,6 @@ void Config::Save()
}
else
{
printf("Failed to write configuration.\n");
printf("[*] Failed to write configuration.\n");
}
}

View file

@ -0,0 +1,77 @@
#pragma once
#include <user/config_detail.h>
#include <locale/config_locale.h>
#include <user/paths.h>
#include <exports.h>
class Config
{
public:
inline static std::vector<IConfigDef*> Definitions{};
CONFIG_DEFINE_ENUM_LOCALISED("System", ELanguage, Language, ELanguage::English);
CONFIG_DEFINE_LOCALISED("System", bool, Hints, true);
CONFIG_DEFINE_LOCALISED("System", bool, ControlTutorial, true);
CONFIG_DEFINE_LOCALISED("System", bool, AchievementNotifications, true);
CONFIG_DEFINE_LOCALISED("System", bool, SaveScoreAtCheckpoints, false);
CONFIG_DEFINE_ENUM_LOCALISED("System", EUnleashGaugeBehaviour, UnleashGaugeBehaviour, EUnleashGaugeBehaviour::Original);
CONFIG_DEFINE_ENUM_LOCALISED("System", ETimeOfDayTransition, TimeOfDayTransition, ETimeOfDayTransition::Xbox);
CONFIG_DEFINE_LOCALISED("System", bool, SkipIntroLogos, false);
CONFIG_DEFINE_LOCALISED("Input", bool, InvertCameraX, false);
CONFIG_DEFINE_LOCALISED("Input", bool, InvertCameraY, false);
CONFIG_DEFINE_LOCALISED("Input", bool, XButtonHoming, true);
CONFIG_DEFINE_LOCALISED("Input", bool, AllowCancellingUnleash, false);
CONFIG_DEFINE_LOCALISED("Input", bool, AllowBackgroundInput, false);
CONFIG_DEFINE_LOCALISED("Audio", float, MusicVolume, 1.0f);
CONFIG_DEFINE_LOCALISED("Audio", float, EffectsVolume, 1.0f);
CONFIG_DEFINE_ENUM_LOCALISED("Audio", EVoiceLanguage, VoiceLanguage, EVoiceLanguage::English);
CONFIG_DEFINE_LOCALISED("Audio", bool, Subtitles, true);
CONFIG_DEFINE_LOCALISED("Audio", bool, MusicAttenuation, false);
CONFIG_DEFINE_LOCALISED("Audio", bool, BattleTheme, 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_ENUM_LOCALISED("Video", EAspectRatio, AspectRatio, EAspectRatio::Auto);
CONFIG_DEFINE_CALLBACK("Video", float, ResolutionScale, 1.0f,
{
def->Locale = &g_ResolutionScale_locale;
def->Value = std::clamp(def->Value, 0.25f, 2.0f);
});
CONFIG_DEFINE_CALLBACK("Video", bool, Fullscreen, false,
{
def->Locale = &g_Fullscreen_locale;
Window_SetFullscreen(def->Value);
});
CONFIG_DEFINE_LOCALISED("Video", bool, VSync, true);
CONFIG_DEFINE_ENUM("Video", ETripleBuffering, TripleBuffering, ETripleBuffering::Auto);
CONFIG_DEFINE_LOCALISED("Video", int32_t, FPS, 60);
CONFIG_DEFINE_LOCALISED("Video", float, Brightness, 0.5f);
CONFIG_DEFINE_ENUM_LOCALISED("Video", EAntiAliasing, AntiAliasing, EAntiAliasing::MSAA4x);
CONFIG_DEFINE_LOCALISED("Video", bool, TransparencyAntiAliasing, true);
CONFIG_DEFINE("Video", size_t, AnisotropicFiltering, 16);
CONFIG_DEFINE_ENUM_LOCALISED("Video", EShadowResolution, ShadowResolution, EShadowResolution::x4096);
CONFIG_DEFINE_ENUM_LOCALISED("Video", EGITextureFiltering, GITextureFiltering, EGITextureFiltering::Bicubic);
CONFIG_DEFINE_LOCALISED("Video", bool, MotionBlur, true);
CONFIG_DEFINE_LOCALISED("Video", bool, XboxColourCorrection, false);
CONFIG_DEFINE_ENUM_LOCALISED("Video", EMovieScaleMode, MovieScaleMode, EMovieScaleMode::Fit);
CONFIG_DEFINE_ENUM_LOCALISED("Video", EUIScaleMode, UIScaleMode, EUIScaleMode::Centre);
static std::filesystem::path GetConfigPath()
{
return GetUserPath() / "config.toml";
}
static void Load();
static void Save();
};

View file

@ -0,0 +1,190 @@
#include "config.h"
#include "config_detail.h"
// CONFIG_DEFINE
template<typename T>
ConfigDef<T>::ConfigDef(std::string section, std::string name, T defaultValue) : Section(section), Name(name), DefaultValue(defaultValue)
{
Config::Definitions.emplace_back(this);
}
// CONFIG_DEFINE_LOCALISED
template<typename T>
ConfigDef<T>::ConfigDef(std::string section, std::string name, CONFIG_LOCALE* locale, T defaultValue)
: Section(section), Name(name), Locale(locale), DefaultValue(defaultValue)
{
Config::Definitions.emplace_back(this);
}
// CONFIG_DEFINE_ENUM
template<typename T>
ConfigDef<T>::ConfigDef(std::string section, std::string name, T defaultValue, std::unordered_map<std::string, T>* 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_ENUM_LOCALISED
template<typename T>
ConfigDef<T>::ConfigDef(std::string section, std::string name, CONFIG_LOCALE* locale, T defaultValue, std::unordered_map<std::string, T>* enumTemplate, CONFIG_ENUM_LOCALE(T)* enumLocale)
: Section(section), Name(name), Locale(locale), DefaultValue(defaultValue), EnumTemplate(enumTemplate), EnumLocale(enumLocale)
{
for (const auto& pair : *EnumTemplate)
EnumTemplateReverse[pair.second] = pair.first;
Config::Definitions.emplace_back(this);
}
// CONFIG_DEFINE_CALLBACK
template<typename T>
ConfigDef<T>::ConfigDef(std::string section, std::string name, T defaultValue, std::function<void(ConfigDef<T>*)> callback)
: Section(section), Name(name), DefaultValue(defaultValue), Callback(callback)
{
Config::Definitions.emplace_back(this);
}
template<typename T>
std::string ConfigDef<T>::GetNameLocalised() const
{
if (!Locale)
return Name;
if (!Locale->count(Config::Language))
{
if (Locale->count(ELanguage::English))
{
return std::get<0>(Locale->at(ELanguage::English));
}
else
{
return Name;
}
}
return std::get<0>(Locale->at(Config::Language));
}
template<typename T>
std::string ConfigDef<T>::GetDescription() const
{
if (!Locale)
return "";
if (!Locale->count(Config::Language))
{
if (Locale->count(ELanguage::English))
{
return std::get<1>(Locale->at(ELanguage::English));
}
else
{
return "";
}
}
return std::get<1>(Locale->at(Config::Language));
}
template<typename T>
std::string ConfigDef<T>::GetValueLocalised() const
{
auto language = Config::Language;
CONFIG_ENUM_LOCALE(T)* locale = nullptr;
if constexpr (std::is_enum_v<T>)
{
locale = EnumLocale;
}
else if constexpr (std::is_same_v<T, bool>)
{
locale = &g_bool_locale;
}
if (!locale)
return ToString(false);
if (!locale->count(language))
{
if (locale->count(ELanguage::English))
{
language = ELanguage::English;
}
else
{
return ToString(false);
}
}
auto strings = locale->at(language);
if (!strings.count(Value))
return ToString(false);
return std::get<0>(strings.at(Value));
}
template<typename T>
std::string ConfigDef<T>::GetValueDescription() const
{
auto language = Config::Language;
CONFIG_ENUM_LOCALE(T)* locale = nullptr;
if constexpr (std::is_enum_v<T>)
{
locale = EnumLocale;
}
else if constexpr (std::is_same_v<T, bool>)
{
locale = &g_bool_locale;
}
if (!locale)
return "";
if (!locale->count(language))
{
if (locale->count(ELanguage::English))
{
language = ELanguage::English;
}
else
{
return "";
}
}
auto strings = locale->at(language);
if (!strings.count(Value))
return "";
return std::get<1>(strings.at(Value));
}
template<typename T>
inline void ConfigDef<T>::GetLocaleStrings(std::vector<std::string_view>& localeStrings) const
{
if (Locale != nullptr)
{
for (auto& [language, nameAndDesc] : *Locale)
{
localeStrings.push_back(std::get<0>(nameAndDesc));
localeStrings.push_back(std::get<1>(nameAndDesc));
}
}
if (EnumLocale != nullptr)
{
for (auto& [language, locale] : *EnumLocale)
{
for (auto& [value, nameAndDesc] : locale)
{
localeStrings.push_back(std::get<0>(nameAndDesc));
localeStrings.push_back(std::get<1>(nameAndDesc));
}
}
}
}

View file

@ -0,0 +1,379 @@
#pragma once
#define CONFIG_DEFINE(section, type, name, defaultValue) \
inline static ConfigDef<type> name{section, #name, defaultValue};
#define CONFIG_DEFINE_LOCALISED(section, type, name, defaultValue) \
inline static ConfigDef<type> name{section, #name, &g_##name##_locale, defaultValue};
#define CONFIG_DEFINE_ENUM(section, type, name, defaultValue) \
inline static ConfigDef<type> name{section, #name, defaultValue, &g_##type##_template};
#define CONFIG_DEFINE_ENUM_LOCALISED(section, type, name, defaultValue) \
inline static ConfigDef<type> name{section, #name, &g_##name##_locale, defaultValue, &g_##type##_template, &g_##type##_locale};
#define CONFIG_DEFINE_CALLBACK(section, type, name, defaultValue, readCallback) \
inline static ConfigDef<type> name{section, #name, defaultValue, [](ConfigDef<type>* def) readCallback};
#define CONFIG_DEFINE_ENUM_TEMPLATE(type) \
inline static std::unordered_map<std::string, type> g_##type##_template =
#define CONFIG_LOCALE std::unordered_map<ELanguage, std::tuple<std::string, std::string>>
#define CONFIG_ENUM_LOCALE(type) std::unordered_map<ELanguage, std::unordered_map<type, std::tuple<std::string, std::string>>>
#define WINDOWPOS_CENTRED 0x2FFF0000
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 EUnleashGaugeBehaviour : uint32_t
{
Original,
Revised
};
CONFIG_DEFINE_ENUM_TEMPLATE(EUnleashGaugeBehaviour)
{
{ "Original", EUnleashGaugeBehaviour::Original },
{ "Revised", EUnleashGaugeBehaviour::Revised }
};
enum class ETimeOfDayTransition : uint32_t
{
Xbox,
PlayStation
};
CONFIG_DEFINE_ENUM_TEMPLATE(ETimeOfDayTransition)
{
{ "Xbox", ETimeOfDayTransition::Xbox },
{ "PlayStation", ETimeOfDayTransition::PlayStation }
};
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 EAspectRatio : uint32_t
{
Auto,
Square,
Widescreen
};
CONFIG_DEFINE_ENUM_TEMPLATE(EAspectRatio)
{
{ "Auto", EAspectRatio::Auto },
{ "4:3", EAspectRatio::Square },
{ "16:9", EAspectRatio::Widescreen }
};
enum class ETripleBuffering : uint32_t
{
Auto,
On,
Off
};
CONFIG_DEFINE_ENUM_TEMPLATE(ETripleBuffering)
{
{ "Auto", ETripleBuffering::Auto },
{ "On", ETripleBuffering::On },
{ "Off", ETripleBuffering::Off }
};
enum class EAntiAliasing : uint32_t
{
None = 0,
MSAA2x = 2,
MSAA4x = 4,
MSAA8x = 8
};
CONFIG_DEFINE_ENUM_TEMPLATE(EAntiAliasing)
{
{ "None", EAntiAliasing::None },
{ "2x MSAA", EAntiAliasing::MSAA2x },
{ "4x MSAA", EAntiAliasing::MSAA4x },
{ "8x MSAA", EAntiAliasing::MSAA8x }
};
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
{
Bilinear,
Bicubic
};
CONFIG_DEFINE_ENUM_TEMPLATE(EGITextureFiltering)
{
{ "Bilinear", EGITextureFiltering::Bilinear },
{ "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 }
};
class IConfigDef
{
public:
virtual ~IConfigDef() = default;
virtual void ReadValue(toml::v3::ex::parse_result& toml) = 0;
virtual void MakeDefault() = 0;
virtual std::string_view GetSection() const = 0;
virtual std::string_view GetName() const = 0;
virtual std::string GetNameLocalised() const = 0;
virtual std::string GetDescription() const = 0;
virtual bool IsDefaultValue() const = 0;
virtual const void* GetValue() const = 0;
virtual std::string GetValueLocalised() const = 0;
virtual std::string GetValueDescription() const = 0;
virtual std::string GetDefinition(bool withSection = false) const = 0;
virtual std::string ToString(bool strWithQuotes = true) const = 0;
virtual void GetLocaleStrings(std::vector<std::string_view>& localeStrings) const = 0;
};
template<typename T>
class ConfigDef : public IConfigDef
{
public:
std::string Section{};
std::string Name{};
CONFIG_LOCALE* Locale{};
T DefaultValue{};
T Value{ DefaultValue };
std::unordered_map<std::string, T>* EnumTemplate;
std::map<T, std::string> EnumTemplateReverse{};
CONFIG_ENUM_LOCALE(T)* EnumLocale{};
std::function<void(ConfigDef<T>*)> Callback;
// CONFIG_DEFINE
ConfigDef(std::string section, std::string name, T defaultValue);
// CONFIG_DEFINE_LOCALISED
ConfigDef(std::string section, std::string name, CONFIG_LOCALE* nameLocale, T defaultValue);
// CONFIG_DEFINE_ENUM
ConfigDef(std::string section, std::string name, T defaultValue, std::unordered_map<std::string, T>* enumTemplate);
// CONFIG_DEFINE_ENUM_LOCALISED
ConfigDef(std::string section, std::string name, CONFIG_LOCALE* nameLocale, T defaultValue, std::unordered_map<std::string, T>* enumTemplate, CONFIG_ENUM_LOCALE(T)* enumLocale);
// CONFIG_DEFINE_CALLBACK
ConfigDef(std::string section, std::string name, T defaultValue, std::function<void(ConfigDef<T>*)> callback);
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<T, std::string>::value)
{
Value = section[Name].value_or<std::string>(DefaultValue);
}
else if constexpr (std::is_enum_v<T>)
{
auto it = EnumTemplate->begin();
Value = EnumTemplate->at(section[Name].value_or<std::string>(static_cast<std::string>(it->first)));
}
else
{
Value = section[Name].value_or(DefaultValue);
}
if (Callback)
Callback(this);
}
}
void MakeDefault() override
{
Value = DefaultValue;
}
std::string_view GetSection() const override
{
return Section;
}
std::string_view GetName() const override
{
return Name;
}
std::string GetNameLocalised() const override;
std::string GetDescription() const override;
bool IsDefaultValue() const override
{
return Value == DefaultValue;
}
const void* GetValue() const override
{
return &Value;
}
std::string GetValueLocalised() const override;
std::string GetValueDescription() const override;
std::string GetDefinition(bool withSection = false) const override
{
std::string result;
if (withSection)
result += "[" + Section + "]\n";
result += Name + " = " + ToString();
return result;
}
std::string ToString(bool strWithQuotes = true) const override
{
std::string result = "N/A";
if constexpr (std::is_same_v<T, std::string>)
{
result = std::format("{}", Value);
if (strWithQuotes)
result = std::format("\"{}\"", result);
}
else if constexpr (std::is_enum_v<T>)
{
auto it = EnumTemplateReverse.find(Value);
if (it != EnumTemplateReverse.end())
result = std::format("{}", it->second);
if (strWithQuotes)
result = std::format("\"{}\"", result);
}
else
{
result = std::format("{}", Value);
}
return result;
}
void GetLocaleStrings(std::vector<std::string_view>& localeStrings) const override;
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;
}
};

View file

@ -0,0 +1,25 @@
#pragma once
#define USER_DIRECTORY "SWA"
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 GetSavePath()
{
return GetUserPath() / "save";
}

View file

@ -98,8 +98,9 @@ jump_address_on_false = 0x82468EE0
[[midasm_hook]]
name = "CameraBoostAspectRatioMidAsmHook"
address = 0x8246BDA8
registers = ["r31", "f0"]
address = 0x8246BDA0
registers = ["r31", "f0", "f10", "f12"]
jump_address_on_true = 0x8246BDAC
[[midasm_hook]]
name = "CSDAspectRatioMidAsmHook"
@ -453,3 +454,91 @@ registers = ["r1", "r30"]
name = "StorageDevicePromptMidAsmHook"
address = 0x822C53CC
jump_address = 0x822C53F8
# World Map Pause Menu
[[midasm_hook]]
name = "CHudPauseAddOptionsItemMidAsmHook"
address = 0x824AF140
registers = ["r31"]
# Village Pause Menu
[[midasm_hook]]
name = "CHudPauseAddOptionsItemMidAsmHook"
address = 0x824AF5BC
registers = ["r31"]
# Stage Pause Menu
[[midasm_hook]]
name = "CHudPauseAddOptionsItemMidAsmHook"
address = 0x824AF988
registers = ["r31"]
# Hub Pause Menu
[[midasm_hook]]
name = "CHudPauseAddOptionsItemMidAsmHook"
address = 0x824AFB20
registers = ["r31"]
# Misc Pause Menu
[[midasm_hook]]
name = "CHudPauseAddOptionsItemMidAsmHook"
address = 0x824AFCC8
registers = ["r31"]
# World Map Pause Menu
[[midasm_hook]]
name = "CHudPauseItemCountMidAsmHook"
address = 0x824B02F8
registers = ["r3", "r11"]
return_on_true = true
# Village Pause Menu
[[midasm_hook]]
name = "CHudPauseVillageItemCountMidAsmHook"
address = 0x824B04AC
registers = ["r31", "r10"]
# Stage Pause Menu
[[midasm_hook]]
name = "CHudPauseItemCountMidAsmHook"
address = 0x824B061C
registers = ["r3", "r11"]
return_on_true = true
# Hub Pause Menu
[[midasm_hook]]
name = "CHudPauseItemCountMidAsmHook"
address = 0x824B07C4
registers = ["r3", "r10"]
return_on_true = true
# Misc Pause Menu Index Wrap-around
[[midasm_hook]]
name = "CHudPauseItemCountMidAsmHook"
address = 0x824B08A8
registers = ["r3", "r11"]
return_on_true = true
# Misc Pause Menu Index Comparison
[[midasm_hook]]
name = "CHudPauseMiscItemCountMidAsmHook"
address = 0x824B08B0
registers = ["r11"]
jump_address_on_true = 0x824B08C0
# Misc Pause Menu Inject Options Behaviour
[[midasm_hook]]
name = "CHudPauseMiscInjectOptionsMidAsmHook"
address = 0x824B08C0
registers = ["r3"]
return_on_true = true
[[midasm_hook]]
name = "ToggleSubtitlesMidAsmHook"
address = 0x82B9BB74
registers = ["r27"]
[[midasm_hook]]
name = "AchievementManagerUnlockMidAsmHook"
address = 0x82BCFF28
registers = ["r31"]

@ -1 +1 @@
Subproject commit 7183c6e0e90480fe0f2c270c87cea5cd5df7d5c3
Subproject commit ccb516047d697f9e4009a0b4b534adcef758b64a

1
tools/CMakeLists.txt Normal file
View file

@ -0,0 +1 @@
add_subdirectory(${SWA_TOOLS_ROOT}/file_to_c)

View file

@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include(CMakeParseArguments)
project("file_to_c")
set(CMAKE_CXX_STANDARD 17)
add_executable(file_to_c "file_to_c.cpp")
find_package(zstd CONFIG REQUIRED)
target_link_libraries(file_to_c PRIVATE $<IF:$<TARGET_EXISTS:zstd::libzstd_static>,zstd::libzstd_static,zstd::libzstd>)

View file

@ -0,0 +1,136 @@
/*
MIT License
Copyright (c) 2024 RT64 Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <filesystem>
#include <fstream>
#include <cstdio>
#include <vector>
#include <zstd.h>
std::vector<char> read_file(const char* path) {
std::ifstream input_file{path, std::ios::binary};
std::vector<char> ret{};
if (!input_file.good()) {
return ret;
}
// Get the length of the file
input_file.seekg(0, std::ios::end);
ret.resize(input_file.tellg());
// Read the file contents into the vector
input_file.seekg(0, std::ios::beg);
input_file.read(ret.data(), ret.size());
return ret;
}
void create_parent_if_needed(const char* path) {
std::filesystem::path parent_path = std::filesystem::path{path}.parent_path();
if (!parent_path.empty()) {
std::filesystem::create_directories(parent_path);
}
}
int main(int argc, const char** argv) {
if (argc != 6) {
printf("Usage: %s [input file] [array name] [compression type] [output C file] [output C header]\n", argv[0]);
return EXIT_SUCCESS;
}
const char* input_path = argv[1];
const char* array_name = argv[2];
std::string compression_type = argv[3];
const char* output_c_path = argv[4];
const char* output_h_path = argv[5];
// Read the input file's contents
std::vector<char> contents = read_file(input_path);
if (contents.empty()) {
fprintf(stderr, "Failed to open file %s! (Or it's empty)\n", input_path);
return EXIT_FAILURE;
}
// Compress if requested.
std::vector<char> compressed_contents;
std::transform(compression_type.begin(), compression_type.end(), compression_type.begin(), tolower);
if (compression_type == "zstd") {
size_t bound_size = ZSTD_compressBound(contents.size());
compressed_contents.resize(bound_size);
size_t compressed_size = ZSTD_compress(compressed_contents.data(), bound_size, contents.data(), contents.size(), ZSTD_maxCLevel());
compressed_contents.resize(compressed_size);
}
else if (compression_type != "none") {
fprintf(stderr, "Unknown compression type %s!", compression_type.c_str());
return EXIT_FAILURE;
}
// Create the output directories if they don't exist
create_parent_if_needed(output_c_path);
create_parent_if_needed(output_h_path);
// Write the C file with the array
std::vector<char>& contents_to_write = !compressed_contents.empty() ? compressed_contents : contents;
{
std::ofstream output_c_file{output_c_path};
output_c_file << "extern unsigned char " << array_name << "[" << contents_to_write.size() << "];\n";
output_c_file << "unsigned char " << array_name << "[" << contents_to_write.size() << "] = {";
for (char x : contents_to_write) {
output_c_file << (int)(unsigned char)x << ", ";
}
output_c_file << "};\n";
// Write decompressed size.
if (!compressed_contents.empty()) {
output_c_file << "extern size_t " << array_name << "_uncompressed_size;\n";
output_c_file << "size_t " << array_name << "_uncompressed_size = " << contents.size() << ";\n";
}
}
// Write the header file with the extern array
{
std::ofstream output_h_file{output_h_path};
output_h_file <<
"#ifdef __cplusplus\n"
" extern \"C\" {\n"
"#endif\n"
"extern unsigned char " << array_name << "[" << contents_to_write.size() << "];\n";
// Write decompressed size.
if (!compressed_contents.empty()) {
output_h_file << "extern size_t " << array_name << "_uncompressed_size;\n";
}
output_h_file <<
"#ifdef __cplusplus\n"
" }\n"
"#endif\n";
}
}

View file

@ -22,6 +22,7 @@
"features": [ "sdl2-binding" ]
},
"magic-enum",
"nativefiledialog-extended",
"miniaudio"
]
}