diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index ccc9e8ff..96679e21 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -28,11 +28,6 @@ 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" @@ -80,10 +75,17 @@ set(SWA_PATCHES_CXX_SOURCES ) set(SWA_UI_CXX_SOURCES + "ui/achievement_overlay.cpp" "ui/options_menu.cpp" "ui/window.cpp" ) +set(SWA_USER_CXX_SOURCES + "user/achievement_data.cpp" + "user/config.cpp" + "user/config_detail.cpp" +) + set(SWA_CXX_SOURCES "app.cpp" "exports.cpp" @@ -91,7 +93,6 @@ set(SWA_CXX_SOURCES "misc_impl.cpp" "stdafx.cpp" - ${SWA_CFG_CXX_SOURCES} ${SWA_KERNEL_CXX_SOURCES} ${SWA_CPU_CXX_SOURCES} ${SWA_GPU_CXX_SOURCES} @@ -99,6 +100,7 @@ set(SWA_CXX_SOURCES ${SWA_HID_CXX_SOURCES} ${SWA_PATCHES_CXX_SOURCES} ${SWA_UI_CXX_SOURCES} + ${SWA_USER_CXX_SOURCES} ) if (WIN32) diff --git a/UnleashedRecomp/api/SWA.h b/UnleashedRecomp/api/SWA.h index 305f36d4..07e6b6f3 100644 --- a/UnleashedRecomp/api/SWA.h +++ b/UnleashedRecomp/api/SWA.h @@ -32,6 +32,7 @@ #include "CSD/Manager/csdmSubjectBase.h" #include "CSD/Platform/csdTexList.h" +#include "SWA/Achievement/AchievementManager.h" #include "SWA/Camera/Camera.h" #include "SWA/CSD/CsdDatabaseWrapper.h" #include "SWA/CSD/CsdProject.h" @@ -40,6 +41,7 @@ #include "SWA/HUD/GeneralWindow/GeneralWindow.h" #include "SWA/HUD/Loading/Loading.h" #include "SWA/HUD/Pause/HudPause.h" +#include "SWA/HUD/SaveIcon/SaveIcon.h" #include "SWA/HUD/Sonic/HudSonicStage.h" #include "SWA/Movie/MovieDisplayer.h" #include "SWA/Movie/MovieManager.h" diff --git a/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h b/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h new file mode 100644 index 00000000..60903bce --- /dev/null +++ b/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace SWA::Achievement +{ + class CManager : public Hedgehog::Universe::CUpdateUnit + { + public: + class CMember + { + public: + SWA_INSERT_PADDING(0x08); + be m_AchievementID; + }; + + SWA_INSERT_PADDING(0x98); + xpointer m_pMember; + be m_IsUnlocked; + SWA_INSERT_PADDING(0x10); + }; +} diff --git a/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h b/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h new file mode 100644 index 00000000..a21cbd90 --- /dev/null +++ b/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace SWA +{ + class CAchievementTest + { + public: + SWA_INSERT_PADDING(0x38); + be m_Unk1; + be m_AchievementID; + uint8_t m_Unk2; + }; +} diff --git a/UnleashedRecomp/api/SWA/HUD/SaveIcon/SaveIcon.h b/UnleashedRecomp/api/SWA/HUD/SaveIcon/SaveIcon.h new file mode 100644 index 00000000..61d5ec77 --- /dev/null +++ b/UnleashedRecomp/api/SWA/HUD/SaveIcon/SaveIcon.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace SWA +{ + class CSaveIcon : Hedgehog::Universe::CUpdateUnit + { + public: + SWA_INSERT_PADDING(0xD8); + bool m_IsVisible; + }; +} diff --git a/UnleashedRecomp/api/SWA/System/ApplicationDocument.h b/UnleashedRecomp/api/SWA/System/ApplicationDocument.h index 6dfdc7da..fb81b077 100644 --- a/UnleashedRecomp/api/SWA/System/ApplicationDocument.h +++ b/UnleashedRecomp/api/SWA/System/ApplicationDocument.h @@ -35,7 +35,9 @@ namespace SWA public: SWA_INSERT_PADDING(0x20); boost::shared_ptr m_pGame; - SWA_INSERT_PADDING(0x114); + SWA_INSERT_PADDING(0xD4); + xpointer m_pAchievementManager; + SWA_INSERT_PADDING(0x3C); xpointer m_spGameParameter; }; diff --git a/UnleashedRecomp/app.h b/UnleashedRecomp/app.h index 4e1d379e..a303ba47 100644 --- a/UnleashedRecomp/app.h +++ b/UnleashedRecomp/app.h @@ -1,3 +1,6 @@ #pragma once +#include + extern double g_deltaTime; +extern XDBFWrapper g_xdbf; diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index 103160b6..661a8434 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -7,13 +7,14 @@ #include #include #include +#include #include #include "imgui_snapshot.h" #include "imgui_common.h" #include "video.h" #include -#include +#include #include "shader/copy_vs.hlsl.dxil.h" #include "shader/copy_vs.hlsl.spirv.h" @@ -1011,6 +1012,7 @@ static void CreateImGuiBackend() io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; OptionsMenu::Init(); + AchievementOverlay::Init(); ImGui_ImplSDL2_InitForOther(Window::s_pWindow); g_imFontTexture = std::make_unique(ResourceType::Texture); @@ -1649,6 +1651,7 @@ static void DrawImGui() { ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); + AchievementOverlay::Draw(); OptionsMenu::Draw(); ImGui::Render(); diff --git a/UnleashedRecomp/hid/driver/sdl_hid.cpp b/UnleashedRecomp/hid/driver/sdl_hid.cpp index 56a2df1a..ee57f9be 100644 --- a/UnleashedRecomp/hid/driver/sdl_hid.cpp +++ b/UnleashedRecomp/hid/driver/sdl_hid.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include #include diff --git a/UnleashedRecomp/kernel/imports.cpp b/UnleashedRecomp/kernel/imports.cpp index 277cee8d..9f83e410 100644 --- a/UnleashedRecomp/kernel/imports.cpp +++ b/UnleashedRecomp/kernel/imports.cpp @@ -11,7 +11,7 @@ #include "xam.h" #include "xdm.h" #include -#include +#include #include diff --git a/UnleashedRecomp/kernel/xam.cpp b/UnleashedRecomp/kernel/xam.cpp index bd704bc6..12f61fe2 100644 --- a/UnleashedRecomp/kernel/xam.cpp +++ b/UnleashedRecomp/kernel/xam.cpp @@ -8,6 +8,7 @@ #include #include #include "xxHashMap.h" +#include // Needed for commctrl #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") @@ -245,7 +246,7 @@ SWA_API uint32_t XamContentCreateEx(DWORD dwUserIndex, LPCSTR szRootName, const if (pContentData->dwContentType == XCONTENTTYPE_SAVEDATA) { - root = Config::GetSavePath().string(); + root = GetSavePath().string(); } else if (pContentData->dwContentType == XCONTENTTYPE_DLC) { diff --git a/UnleashedRecomp/locale/config_locale.h b/UnleashedRecomp/locale/config_locale.h index 027976b6..5ac29f11 100644 --- a/UnleashedRecomp/locale/config_locale.h +++ b/UnleashedRecomp/locale/config_locale.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #define CONFIG_DEFINE_LOCALE(name) \ inline static std::unordered_map> g_##name##_locale = @@ -89,6 +89,11 @@ 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]" } } diff --git a/UnleashedRecomp/locale/locale.h b/UnleashedRecomp/locale/locale.h index e7067c4a..d135b093 100644 --- a/UnleashedRecomp/locale/locale.h +++ b/UnleashedRecomp/locale/locale.h @@ -1,6 +1,6 @@ #pragma once -#include +#include inline static std::string g_localeMissing = ""; @@ -65,6 +65,18 @@ inline static std::unordered_map #include #include -#include +#include +#include +#include +#include #define GAME_XEX_PATH "game:\\default.xex" @@ -21,6 +24,7 @@ const size_t XMAIOEnd = XMAIOBegin + 0x0000FFFF; Memory g_memory{ reinterpret_cast(0x100000000), 0x100000000 }; Heap g_userHeap; CodeCache g_codeCache; +XDBFWrapper g_xdbf; // Name inspired from nt's entry point void KiSystemStartup() @@ -40,7 +44,7 @@ void KiSystemStartup() 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? @@ -118,12 +122,17 @@ uint32_t LdrLoadModule(const char* path) } } + auto res = Xex2FindOptionalHeader(xex, XEX_HEADER_RESOURCE_INFO); + + g_xdbf = XDBFWrapper((uint8_t*)g_memory.Translate(res->Offset.get()), res->SizeOfData); + return entry; } int main() { Config::Load(); + AchievementData::Load(); KiSystemStartup(); diff --git a/UnleashedRecomp/patches/audio_patches.cpp b/UnleashedRecomp/patches/audio_patches.cpp index 09234284..9c4ee35c 100644 --- a/UnleashedRecomp/patches/audio_patches.cpp +++ b/UnleashedRecomp/patches/audio_patches.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include diff --git a/UnleashedRecomp/patches/camera_patches.cpp b/UnleashedRecomp/patches/camera_patches.cpp index a8a191de..9b70e525 100644 --- a/UnleashedRecomp/patches/camera_patches.cpp +++ b/UnleashedRecomp/patches/camera_patches.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include constexpr float m_baseAspectRatio = 16.0f / 9.0f; diff --git a/UnleashedRecomp/patches/fps_patches.cpp b/UnleashedRecomp/patches/fps_patches.cpp index 1ad63b10..289fea53 100644 --- a/UnleashedRecomp/patches/fps_patches.cpp +++ b/UnleashedRecomp/patches/fps_patches.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include float m_lastLoadingFrameDelta = 0.0f; diff --git a/UnleashedRecomp/patches/misc_patches.cpp b/UnleashedRecomp/patches/misc_patches.cpp index 6020980e..e31eca3e 100644 --- a/UnleashedRecomp/patches/misc_patches.cpp +++ b/UnleashedRecomp/patches/misc_patches.cpp @@ -1,7 +1,13 @@ #include #include #include -#include +#include +#include + +void AchievementManagerUnlockMidAsmHook(PPCRegister& id) +{ + AchievementData::Unlock(id.u32); +} bool DisableHintsMidAsmHook() { diff --git a/UnleashedRecomp/patches/player_patches.cpp b/UnleashedRecomp/patches/player_patches.cpp index 2a8009aa..8003ae3e 100644 --- a/UnleashedRecomp/patches/player_patches.cpp +++ b/UnleashedRecomp/patches/player_patches.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include uint32_t m_lastCheckpointScore = 0; float m_lastDarkGaiaEnergy = 0.0f; diff --git a/UnleashedRecomp/patches/resident_patches.cpp b/UnleashedRecomp/patches/resident_patches.cpp index ea95d557..94efda94 100644 --- a/UnleashedRecomp/patches/resident_patches.cpp +++ b/UnleashedRecomp/patches/resident_patches.cpp @@ -1,9 +1,12 @@ #include -#include +#include +#include #include const char* m_pStageID; +bool m_isSavedAchievementData = false; + void GetStageIDMidAsmHook(PPCRegister& r5) { m_pStageID = *(xpointer*)g_memory.Translate(r5.u32); @@ -42,13 +45,38 @@ PPC_FUNC(sub_824EB9B0) __imp__sub_824EB9B0(ctx, base); } -// CApplicationDocument::LoadArchiveDatabases +// SWA::CSaveIcon::Update +PPC_FUNC_IMPL(__imp__sub_824E5170); +PPC_FUNC(sub_824E5170) +{ + auto pSaveIcon = (SWA::CSaveIcon*)g_memory.Translate(ctx.r3.u32); + + __imp__sub_824E5170(ctx, base); + + if (pSaveIcon->m_IsVisible) + { + if (!m_isSavedAchievementData) + { + printf("[*] Saving achievements...\n"); + + AchievementData::Save(); + + m_isSavedAchievementData = true; + } + } + else + { + m_isSavedAchievementData = false; + } +} + +// SWA::CApplicationDocument::LoadArchiveDatabases PPC_FUNC_IMPL(__imp__sub_824EFD28); PPC_FUNC(sub_824EFD28) { auto r3 = ctx.r3; - // CSigninXenon::InitializeDLC + // SWA::CSigninXenon::InitializeDLC ctx.r3.u64 = PPC_LOAD_U32(r3.u32 + 4) + 200; ctx.r4.u64 = 0; sub_822C57D8(ctx, base); @@ -57,7 +85,7 @@ PPC_FUNC(sub_824EFD28) __imp__sub_824EFD28(ctx, base); } -// CFileReaderXenon_DLC::InitializeParallel +// SWA::CFileReaderXenon_DLC::InitializeParallel PPC_FUNC(sub_822C3778) { if (!PPC_LOAD_U8(0x83361F10)) // ms_DLCInitialized diff --git a/UnleashedRecomp/patches/video_patches.cpp b/UnleashedRecomp/patches/video_patches.cpp index e4b9aacc..39622eb8 100644 --- a/UnleashedRecomp/patches/video_patches.cpp +++ b/UnleashedRecomp/patches/video_patches.cpp @@ -1,7 +1,7 @@ #include +#include #include #include -#include // TODO: to be removed. constexpr float m_baseAspectRatio = 16.0f / 9.0f; diff --git a/UnleashedRecomp/ui/achievement_overlay.cpp b/UnleashedRecomp/ui/achievement_overlay.cpp new file mode 100644 index 00000000..7040db69 --- /dev/null +++ b/UnleashedRecomp/ui/achievement_overlay.cpp @@ -0,0 +1,183 @@ +#include "achievement_overlay.h" +#include "imgui_utils.h" + +#include +#include +#include +#include +#include +#include +#include + +constexpr double OVERLAY_CONTAINER_MOTION_START = 0.0; +constexpr double OVERLAY_CONTAINER_MOTION_END = 8.0; +constexpr double OVERLAY_CONTAINER_FADE_IN_START = 5.0; +constexpr double OVERLAY_CONTAINER_FADE_OUT_START = 4.0; +constexpr double OVERLAY_ELEMENTS_FADE_START = 10.0; +constexpr double OVERLAY_ELEMENTS_FADE_END = 12.0; +constexpr double OVERLAY_DURATION = 5.0; + +static bool g_isClosing = false; + +static double g_appearTime = 0.0; + +static Achievement g_achievement; +static std::unique_ptr g_upAchievementIcon; + +static ImFont* g_fntSeurat; + +void AchievementOverlay::Init() +{ + auto& io = ImGui::GetIO(); + + g_fntSeurat = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 30.0f); +} + +static double ComputeMotion(double frameOffset, double frames) +{ + double t = std::clamp((ImGui::GetTime() - g_appearTime - frameOffset / 60.0) / frames * 60.0, 0.0, 1.0); + return sqrt(t); +} + +// TODO: move this somewhere where it can be re-used. +void DrawContainer(ImVec2 min, ImVec2 max, float cornerRadius = 25.0f) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + auto containerMotion = ComputeMotion(OVERLAY_CONTAINER_MOTION_START, OVERLAY_CONTAINER_MOTION_END); + auto centreX = (min.x + max.x) / 2.0f; + auto centreY = (min.y + max.y) / 2.0f; + + if (g_isClosing) + { + min.x = CubicEase(min.x, centreX, containerMotion); + max.x = CubicEase(max.x, centreX, containerMotion); + min.y = CubicEase(min.y, centreY, containerMotion); + max.y = CubicEase(max.y, centreY, containerMotion); + } + else + { + min.x = CubicEase(centreX, min.x, containerMotion); + max.x = CubicEase(centreX, max.x, containerMotion); + min.y = CubicEase(centreY, min.y, containerMotion); + max.y = CubicEase(centreY, max.y, containerMotion); + } + + ImVec2 v1 = { min.x, min.y + cornerRadius }; + ImVec2 v2 = { min.x + cornerRadius, min.y }; + ImVec2 v3 = { max.x, min.y }; + ImVec2 v4 = { max.x, min.y + cornerRadius }; + ImVec2 v5 = { max.x, max.y - cornerRadius }; + ImVec2 v6 = { max.x - cornerRadius, max.y }; + ImVec2 v7 = { min.x, max.y }; + ImVec2 v8 = { min.x, max.y - cornerRadius }; + + ImVec2 top[] = { v1, v2, v3, v4 }; + ImVec2 bottom[] = { v5, v6, v7, v8 }; + ImVec2 border[] = { v1, v2, v3, v4, v5, v6, v7, v8 }; + + auto colourMotion = ComputeMotion(g_isClosing ? OVERLAY_CONTAINER_MOTION_START : OVERLAY_CONTAINER_FADE_IN_START, + g_isClosing ? OVERLAY_CONTAINER_FADE_OUT_START : OVERLAY_CONTAINER_MOTION_END); + + auto colShadow = IM_COL32(0, 0, 0, (int)CubicEase(g_isClosing ? 156 : 0, g_isClosing ? 0 : 156, colourMotion)); + auto colGradientTop = IM_COL32(197, 194, 197, (int)CubicEase(g_isClosing ? 200 : 0, g_isClosing ? 0 : 200, colourMotion)); + auto colGradientBottom = IM_COL32(121, 120, 121, (int)CubicEase(g_isClosing ? 236 : 0, g_isClosing ? 0 : 236, colourMotion)); // TODO: match gradient used by the game (115, 113, 115, 236). + + // TODO: add a drop shadow. + + drawList->AddConvexPolyFilled(top, IM_ARRAYSIZE(top), colGradientTop); + drawList->AddRectFilledMultiColor({ min.x, min.y + cornerRadius }, { max.x, max.y - cornerRadius }, colGradientTop, colGradientTop, colGradientBottom, colGradientBottom); + drawList->AddConvexPolyFilled(bottom, IM_ARRAYSIZE(bottom), colGradientBottom); + drawList->AddPolyline(border, IM_ARRAYSIZE(border), IM_COL32(247, 247, 247, (int)CubicEase(g_isClosing ? 255 : 0, g_isClosing ? 0 : 255, colourMotion)), true, Scale(2.5f)); + + for (int i = 0; i < IM_ARRAYSIZE(border); i++) + { + border[i].x -= 0.4f; + border[i].y -= 0.2f; + } + + auto lineAlpha = (int)CubicEase(g_isClosing ? 230 : 0, g_isClosing ? 0 : 230, colourMotion); + auto colLineTop = IM_COL32(165, 170, 165, lineAlpha); + auto colLineBottom = IM_COL32(190, 190, 190, lineAlpha); + auto lineThickness = Scale(1.0f); + + // Top left corner bottom to top left corner top. + drawList->AddLine(border[0], border[1], colLineTop, lineThickness * 0.5f); + + // Top left corner bottom to bottom left. + drawList->AddRectFilledMultiColor({ border[0].x - 0.2f, border[0].y }, { border[6].x + lineThickness - 0.2f, border[6].y }, colLineTop, colLineTop, colLineBottom, colLineBottom); + + // Top left corner top to top right. + drawList->AddLine(border[1], border[2], colLineTop, lineThickness); + + drawList->PushClipRect(min, max); +} + +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(); + + auto fontSize = 30.0f; + auto headerTextSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, strAchievementUnlocked); + auto bodyTextSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, strAchievementName); + auto longestTextSize = std::max(headerTextSize.x, bodyTextSize.x); + + constexpr auto imageX = 25.0f; + constexpr auto imageY = 20.0f; + constexpr auto imageSize = 90.0f; + constexpr auto textX = 140.0f; + + auto width = textX + longestTextSize + 30.0f; + + ImVec2 min = { (res.x / 2.0f) - (width / 2.0f), 50.0f }; + ImVec2 max = { min.x + width, min.y + 125.0f }; + + DrawContainer(min, max); + + auto colourMotion = ComputeMotion(g_isClosing ? OVERLAY_CONTAINER_MOTION_START : OVERLAY_ELEMENTS_FADE_START, + g_isClosing ? OVERLAY_CONTAINER_FADE_OUT_START : OVERLAY_ELEMENTS_FADE_END); + + auto alpha = (int)CubicEase(g_isClosing ? 255 : 0, g_isClosing ? 0 : 255, colourMotion); + + drawList->AddImage(g_upAchievementIcon.get(), { min.x + imageX, min.y + imageY }, { min.x + imageX + imageSize, min.y + imageY + imageSize }, { 0, 0 }, { 1, 1 }, IM_COL32(255, 255, 255, alpha)); + + DrawTextWithShadow(g_fntSeurat, fontSize, { min.x + textX + (longestTextSize - headerTextSize.x) / 2.0f, min.y + 30.0f}, IM_COL32(252, 243, 5, alpha), strAchievementUnlocked, 2.0f, IM_COL32(0, 0, 0, alpha)); + DrawTextWithShadow(g_fntSeurat, fontSize, { min.x + textX + (longestTextSize - bodyTextSize.x) / 2.0f, min.y + 68.0f}, IM_COL32(255, 255, 255, alpha), strAchievementName, 2.0f, IM_COL32(0, 0, 0, alpha)); + + // Pop clip rect from DrawContainer + drawList->PopClipRect(); +} + +void AchievementOverlay::Open(int id) +{ + s_isVisible = true; + g_isClosing = false; + g_appearTime = ImGui::GetTime(); + + g_achievement = g_xdbf.GetAchievement((EXDBFLanguage)Config::Language.Value, id); + g_upAchievementIcon = LoadTexture((uint8_t*)g_achievement.pImageBuffer, g_achievement.ImageBufferSize); + + Game_PlaySound("obj_navi_appear"); +} + +void AchievementOverlay::Close() +{ + if (!g_isClosing) + { + g_appearTime = ImGui::GetTime(); + g_isClosing = true; + } + + if (ImGui::GetTime() - g_appearTime >= OVERLAY_ELEMENTS_FADE_END) + s_isVisible = false; +} diff --git a/UnleashedRecomp/ui/achievement_overlay.h b/UnleashedRecomp/ui/achievement_overlay.h new file mode 100644 index 00000000..ac7e3aec --- /dev/null +++ b/UnleashedRecomp/ui/achievement_overlay.h @@ -0,0 +1,12 @@ +#pragma once + +struct AchievementOverlay +{ +public: + inline static bool s_isVisible = false; + + static void Init(); + static void Draw(); + static void Open(int id); + static void Close(); +}; diff --git a/UnleashedRecomp/ui/imgui_utils.h b/UnleashedRecomp/ui/imgui_utils.h new file mode 100644 index 00000000..eff5bb42 --- /dev/null +++ b/UnleashedRecomp/ui/imgui_utils.h @@ -0,0 +1,95 @@ +#pragma once + +// 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 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.0f, 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(); +} + +static void DrawTextWithOutline(const ImFont* font, float fontSize, const ImVec2& pos, ImU32 color, const char* text, int32_t outlineSize, ImU32 outlineColor) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + // 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.5f, ImU32 shadowColour = IM_COL32(0, 0, 0, 255)) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + drawList->AddText(font, fontSize, { pos.x + offset, pos.y + offset }, shadowColour, text); + drawList->AddText(font, fontSize, pos, colour, text); +} + +static float Lerp(float a, float b, float t) +{ + return a + (b - a) * t; +} + +static float CubicEase(float a, float b, float t) +{ + return a + (b - a) * (t * t * 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); +} diff --git a/UnleashedRecomp/ui/options_menu.cpp b/UnleashedRecomp/ui/options_menu.cpp index e16fb42e..1a5a9f77 100644 --- a/UnleashedRecomp/ui/options_menu.cpp +++ b/UnleashedRecomp/ui/options_menu.cpp @@ -1,4 +1,5 @@ #include "options_menu.h" +#include "imgui_utils.h" #include "window.h" #include "exports.h" @@ -77,73 +78,6 @@ static void SetShaderModifier(uint32_t shaderModifier) callbackData->setShaderModifier.shaderModifier = shaderModifier; } -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 = g_seuratFont->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, 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(); -} - -static void DrawTextWithOutline(const ImFont* font, float fontSize, const ImVec2& pos, ImU32 color, const char* text, int32_t outlineSize, ImU32 outlineColor) -{ - auto drawList = ImGui::GetForegroundDrawList(); - - // 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); -} - -// Not aspect ratio aware. Will stretch. -static float ScaleX(float x) -{ - auto& io = ImGui::GetIO(); - return x * io.DisplaySize.x / 1280.0f; -} - -static float ScaleY(float y) -{ - auto& io = ImGui::GetIO(); - return y * io.DisplaySize.y / 720.0f; -} - -// 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); -} - -static float Lerp(float a, float b, float t) -{ - return a + (b - a) * 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 void DrawScanlineBars() { constexpr uint32_t COLOR0 = IM_COL32(203, 255, 0, 0); @@ -854,6 +788,7 @@ static void DrawConfigOptions() DrawConfigOption(rowCount++, yOffset, &Config::Language, !OptionsMenu::s_isPause, cmnReason); DrawConfigOption(rowCount++, yOffset, &Config::Hints, !isStage, cmnReason); DrawConfigOption(rowCount++, yOffset, &Config::ControlTutorial, !isStage, cmnReason); + DrawConfigOption(rowCount++, yOffset, &Config::AchievementNotifications, true); DrawConfigOption(rowCount++, yOffset, &Config::SaveScoreAtCheckpoints, true); DrawConfigOption(rowCount++, yOffset, &Config::UnleashGaugeBehaviour, true); DrawConfigOption(rowCount++, yOffset, &Config::TimeOfDayTransition, true); @@ -1100,6 +1035,8 @@ void OptionsMenu::Close() *(bool*)g_memory.Translate(0x8328BB26) = true; + Config::Save(); + // TODO: animate Miles Electric out if we're in a stage. } diff --git a/UnleashedRecomp/ui/window.cpp b/UnleashedRecomp/ui/window.cpp index 2d600f66..da0d4ae4 100644 --- a/UnleashedRecomp/ui/window.cpp +++ b/UnleashedRecomp/ui/window.cpp @@ -1,6 +1,6 @@ #include "window.h" #include "sdl_listener.h" -#include +#include #include bool m_isFullscreenKeyReleased = true; diff --git a/UnleashedRecomp/ui/window.h b/UnleashedRecomp/ui/window.h index 3802f8c0..e85a9adb 100644 --- a/UnleashedRecomp/ui/window.h +++ b/UnleashedRecomp/ui/window.h @@ -3,7 +3,7 @@ #include #include #include -#include +#include #define DEFAULT_WIDTH 1280 #define DEFAULT_HEIGHT 720 diff --git a/UnleashedRecomp/user/achievement_data.cpp b/UnleashedRecomp/user/achievement_data.cpp new file mode 100644 index 00000000..25b4670c --- /dev/null +++ b/UnleashedRecomp/user/achievement_data.cpp @@ -0,0 +1,66 @@ +#include "achievement_data.h" +#include +#include +#include + +bool AchievementData::IsUnlocked(uint16_t id) +{ + for (int i = 0; i < sizeof(Data.Records); i++) + { + 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 < sizeof(Data.Records); i++) + { + if (Data.Records[i].ID == 0) + { + Data.Records[i].ID = id; + break; + } + } + + if (Config::AchievementNotifications) + AchievementOverlay::Open(id); +} + +void AchievementData::Load() +{ + auto dataPath = GetDataPath(); + + if (!std::filesystem::exists(dataPath)) + return; + + std::ifstream file(dataPath, std::ios::binary); + + if (!file) + { + printf("[*] Failed to parse achievement data.\n"); + return; + } + + file.read((char*)&Data, sizeof(Data)); + file.close(); +} + +void AchievementData::Save() +{ + std::ofstream file(GetDataPath(), std::ios::binary); + + if (!file) + { + printf("[*] Failed to write achievement data.\n"); + return; + } + + file.write((const char*)&Data, sizeof(Data)); + file.close(); +} diff --git a/UnleashedRecomp/user/achievement_data.h b/UnleashedRecomp/user/achievement_data.h new file mode 100644 index 00000000..cfb8e0d1 --- /dev/null +++ b/UnleashedRecomp/user/achievement_data.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#define ACH_SIGNATURE 'ACH ' +#define ACH_VERSION 0x01000000 + +class AchievementData +{ +public: +#pragma pack(push, 1) + struct Record + { + uint16_t ID; + uint16_t Reserved[7]; + }; +#pragma pack(pop) + + class Data + { + public: + uint32_t Signature{}; + uint32_t Version{}; + uint32_t Reserved[2]; + Record Records[50]; + }; + + inline static Data Data{ ACH_SIGNATURE, ACH_VERSION }; + + static std::filesystem::path GetDataPath() + { + return GetSavePath() / "ACH-DATA"; + } + + static bool IsUnlocked(uint16_t id); + static void Unlock(uint16_t id); + static void Load(); + static void Save(); +}; diff --git a/UnleashedRecomp/cfg/config.cpp b/UnleashedRecomp/user/config.cpp similarity index 91% rename from UnleashedRecomp/cfg/config.cpp rename to UnleashedRecomp/user/config.cpp index b9f4fbcd..100097aa 100644 --- a/UnleashedRecomp/cfg/config.cpp +++ b/UnleashedRecomp/user/config.cpp @@ -24,7 +24,7 @@ void Config::Load() } 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"); } } diff --git a/UnleashedRecomp/cfg/config.h b/UnleashedRecomp/user/config.h similarity index 83% rename from UnleashedRecomp/cfg/config.h rename to UnleashedRecomp/user/config.h index ea7c40a7..5757da31 100644 --- a/UnleashedRecomp/cfg/config.h +++ b/UnleashedRecomp/user/config.h @@ -1,7 +1,8 @@ #pragma once -#include +#include #include +#include #include class Config @@ -12,6 +13,7 @@ public: 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); @@ -65,31 +67,9 @@ public: CONFIG_DEFINE_ENUM_LOCALISED("Video", EMovieScaleMode, MovieScaleMode, EMovieScaleMode::Fit); CONFIG_DEFINE_ENUM_LOCALISED("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"; + return GetUserPath() / "config.toml"; } static void Load(); diff --git a/UnleashedRecomp/cfg/config_detail.cpp b/UnleashedRecomp/user/config_detail.cpp similarity index 100% rename from UnleashedRecomp/cfg/config_detail.cpp rename to UnleashedRecomp/user/config_detail.cpp diff --git a/UnleashedRecomp/cfg/config_detail.h b/UnleashedRecomp/user/config_detail.h similarity index 99% rename from UnleashedRecomp/cfg/config_detail.h rename to UnleashedRecomp/user/config_detail.h index b213120f..25c1ccba 100644 --- a/UnleashedRecomp/cfg/config_detail.h +++ b/UnleashedRecomp/user/config_detail.h @@ -1,9 +1,5 @@ #pragma once -#define USER_DIRECTORY "SWA" - -#define TOML_FILE "config.toml" - #define CONFIG_DEFINE(section, type, name, defaultValue) \ inline static ConfigDef name{section, #name, defaultValue}; diff --git a/UnleashedRecomp/user/paths.h b/UnleashedRecomp/user/paths.h new file mode 100644 index 00000000..c10e045d --- /dev/null +++ b/UnleashedRecomp/user/paths.h @@ -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"; +} diff --git a/UnleashedRecompLib/config/SWA.toml b/UnleashedRecompLib/config/SWA.toml index 4830dae3..ea774e6a 100644 --- a/UnleashedRecompLib/config/SWA.toml +++ b/UnleashedRecompLib/config/SWA.toml @@ -506,3 +506,8 @@ return_on_true = true name = "ToggleSubtitlesMidAsmHook" address = 0x82B9BB74 registers = ["r27"] + +[[midasm_hook]] +name = "AchievementManagerUnlockMidAsmHook" +address = 0x82BCFF28 +registers = ["r31"] diff --git a/thirdparty/PowerRecomp b/thirdparty/PowerRecomp index 7dd4f91a..675b482e 160000 --- a/thirdparty/PowerRecomp +++ b/thirdparty/PowerRecomp @@ -1 +1 @@ -Subproject commit 7dd4f91ac635b001a56cc7a27af48f0436bbad3f +Subproject commit 675b482ec4852b873590fb999d24b426bade2b3a