From 8c3b50738d560aa84f16c576c87ffa8c3aa2007b Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Thu, 28 Nov 2024 00:34:57 +0000 Subject: [PATCH] Implemented achievements menu (WIP) --- UnleashedRecomp/CMakeLists.txt | 3 + UnleashedRecomp/gpu/video.cpp | 15 +- UnleashedRecomp/locale/locale.h | 6 + .../patches/ui/CHudPause_patches.cpp | 61 ++- UnleashedRecomp/ui/achievement_menu.cpp | 370 ++++++++++++++++++ UnleashedRecomp/ui/achievement_menu.h | 14 + UnleashedRecomp/ui/achievement_overlay.cpp | 44 ++- UnleashedRecomp/ui/achievement_overlay.h | 8 +- UnleashedRecomp/ui/imgui_utils.h | 91 ++++- UnleashedRecomp/ui/imgui_view.cpp | 7 + UnleashedRecomp/ui/imgui_view.h | 23 ++ UnleashedRecomp/ui/options_menu.cpp | 48 +-- UnleashedRecomp/ui/options_menu.h | 7 +- UnleashedRecomp/ui/sdl_listener.cpp | 7 + UnleashedRecomp/ui/sdl_listener.h | 6 +- UnleashedRecomp/ui/window.cpp | 2 +- UnleashedRecomp/ui/window.h | 4 - UnleashedRecomp/user/achievement_data.cpp | 4 +- 18 files changed, 616 insertions(+), 104 deletions(-) create mode 100644 UnleashedRecomp/ui/achievement_menu.cpp create mode 100644 UnleashedRecomp/ui/achievement_menu.h create mode 100644 UnleashedRecomp/ui/imgui_view.cpp create mode 100644 UnleashedRecomp/ui/imgui_view.h create mode 100644 UnleashedRecomp/ui/sdl_listener.cpp diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 96679e21..be4a5d34 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -75,8 +75,11 @@ set(SWA_PATCHES_CXX_SOURCES ) set(SWA_UI_CXX_SOURCES + "ui/achievement_menu.cpp" "ui/achievement_overlay.cpp" + "ui/imgui_view.cpp" "ui/options_menu.cpp" + "ui/sdl_listener.cpp" "ui/window.cpp" ) diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index 661a8434..d76c7c9a 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -7,8 +7,7 @@ #include #include #include -#include -#include +#include #include "imgui_snapshot.h" #include "imgui_common.h" @@ -1011,8 +1010,10 @@ static void CreateImGuiBackend() ImGuiIO& io = ImGui::GetIO(); io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; - OptionsMenu::Init(); - AchievementOverlay::Init(); + + for (auto& view : GetImGuiViews()) + view->Init(); + ImGui_ImplSDL2_InitForOther(Window::s_pWindow); g_imFontTexture = std::make_unique(ResourceType::Texture); @@ -1651,8 +1652,10 @@ static void DrawImGui() { ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); - AchievementOverlay::Draw(); - OptionsMenu::Draw(); + + for (auto& view : GetImGuiViews()) + view->Draw(); + ImGui::Render(); auto drawData = ImGui::GetDrawData(); diff --git a/UnleashedRecomp/locale/locale.h b/UnleashedRecomp/locale/locale.h index d135b093..12757bb4 100644 --- a/UnleashedRecomp/locale/locale.h +++ b/UnleashedRecomp/locale/locale.h @@ -72,6 +72,12 @@ inline static std::unordered_map #include #include +#include #include +#include + +float m_ungracefulExitWaitTime = 0.0f; +constexpr float m_ungracefulExitWaitThreshold = 3.0f; void CHudPauseAddOptionsItemMidAsmHook(PPCRegister& pThis) { @@ -11,7 +16,7 @@ void CHudPauseAddOptionsItemMidAsmHook(PPCRegister& pThis) GuestToHostFunction(0x824AE690, pThis.u32, menu.get(), name.get()); } -bool InjectOptionsBehaviour(uint32_t pThis, uint32_t count) +bool InjectMenuBehaviour(uint32_t pThis, uint32_t count) { auto pHudPause = (SWA::CHudPause*)g_memory.Translate(pThis); auto cursorIndex = *(be*)g_memory.Translate(4 * (*(be*)g_memory.Translate(pThis + 0x19C) + 0x68) + pThis); @@ -35,6 +40,19 @@ bool InjectOptionsBehaviour(uint32_t pThis, uint32_t count) 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) @@ -62,14 +80,14 @@ bool CHudPauseItemCountMidAsmHook(PPCRegister& pThis, PPCRegister& count) { count.u32 += 1; - return InjectOptionsBehaviour(pThis.u32, count.u32); + return InjectMenuBehaviour(pThis.u32, count.u32); } void CHudPauseVillageItemCountMidAsmHook(PPCRegister& pThis, PPCRegister& count) { count.u32 += 1; - InjectOptionsBehaviour(pThis.u32, count.u32); + InjectMenuBehaviour(pThis.u32, count.u32); } bool CHudPauseMiscItemCountMidAsmHook(PPCRegister& count) @@ -82,28 +100,45 @@ bool CHudPauseMiscItemCountMidAsmHook(PPCRegister& count) bool CHudPauseMiscInjectOptionsMidAsmHook(PPCRegister& pThis) { - return InjectOptionsBehaviour(pThis.u32, 3); + return InjectMenuBehaviour(pThis.u32, 3); } // SWA::CHudPause::Update PPC_FUNC_IMPL(__imp__sub_824B0930); PPC_FUNC(sub_824B0930) { - if (!OptionsMenu::s_isVisible || !OptionsMenu::s_isPause) - { - __imp__sub_824B0930(ctx, base); - return; - } + auto pHudPause = (SWA::CHudPause*)g_memory.Translate(ctx.r3.u32); + auto pInputState = SWA::CInputState::GetInstance(); - if (auto pInputState = SWA::CInputState::GetInstance()) + m_ungracefulExitWaitTime += g_deltaTime; + + // TODO: disable Start button closing menu. + if (AchievementMenu::s_isVisible) + { + // HACK: wait for transition to finish before restoring control. + if (m_ungracefulExitWaitThreshold >= m_ungracefulExitWaitTime) + __imp__sub_824B0930(ctx, base); + + if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B)) + { + AchievementMenu::Close(); + + GuestToHostFunction(0x824AFD28, pHudPause, 0, 1, 0, 0); + } + } + else if (OptionsMenu::s_isVisible && OptionsMenu::s_isPause) { - // TODO: disable Start button closing menu. if (OptionsMenu::CanClose() && pInputState->GetPadState().IsTapped(SWA::eKeyState_B)) { OptionsMenu::Close(); - // Re-open pause menu. - GuestToHostFunction(0x824AFD28, ctx.r3.u32, 0, 0, 0, 1); + GuestToHostFunction(0x824AFD28, pHudPause, 0, 0, 0, 1); } } + else + { + m_ungracefulExitWaitTime = 0.0f; + + __imp__sub_824B0930(ctx, base); + } } diff --git a/UnleashedRecomp/ui/achievement_menu.cpp b/UnleashedRecomp/ui/achievement_menu.cpp new file mode 100644 index 00000000..e37962b7 --- /dev/null +++ b/UnleashedRecomp/ui/achievement_menu.cpp @@ -0,0 +1,370 @@ +#include "achievement_menu.h" +#include "imgui_utils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +AchievementMenu m_achievementMenu; + +static std::vector g_achievements; +static std::unordered_map g_achievementTextures; + +static ImFont* g_fntSeurat; +static ImFont* g_fntNewRodin; + +static int g_firstVisibleRowIndex; +static int g_prevSelectedRowIndex; +static int g_selectedRowIndex; +static double g_rowSelectionTime; + +static bool g_upWasHeld; +static bool g_downWasHeld; + +static void ResetSelection() +{ + g_firstVisibleRowIndex = 0; + g_selectedRowIndex = 0; + g_prevSelectedRowIndex = 0; + g_rowSelectionTime = ImGui::GetTime(); + g_upWasHeld = false; + g_downWasHeld = false; +} + +void AchievementMenu::Init() +{ + auto& io = ImGui::GetIO(); + + constexpr float FONT_SCALE = 2.0f; + + g_fntSeurat = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 24.0f * FONT_SCALE); + g_fntNewRodin = io.Fonts->AddFontFromFileTTF("FOT-NewRodinPro-UB.otf", 20.0f * FONT_SCALE); + + g_achievements = g_xdbf.GetAchievements((EXDBFLanguage)Config::Language.Value); + + for (auto& achievement : g_achievements) + { + auto texture = LoadTexture((uint8_t*)achievement.pImageBuffer, achievement.ImageBufferSize); + g_achievementTextures[achievement.ID] = texture.release(); + } +} + +static void DrawContainer(ImVec2 min, ImVec2 max, ImU32 gradientTop, ImU32 gradientBottom, float cornerRadius = 25.0f) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + 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 vertices[] = { v1, v2, v3, v4, v5, v6, v7, v8 }; + + // TODO: add a drop shadow. + + SetGradient(min, max, gradientTop, gradientBottom); + drawList->AddConvexPolyFilled(vertices, IM_ARRAYSIZE(vertices), IM_COL32(255, 255, 255, 255)); + ResetGradient(); + + drawList->AddPolyline(vertices, IM_ARRAYSIZE(vertices), IM_COL32(247, 247, 247, 255), true, Scale(2.5f)); + + for (int i = 0; i < IM_ARRAYSIZE(vertices); i++) + { + vertices[i].x -= 0.4f; + vertices[i].y -= 0.2f; + } + + auto colLineTop = IM_COL32(165, 170, 165, 230); + auto colLineBottom = IM_COL32(190, 190, 190, 230); + auto lineThickness = Scale(1.0f); + + // Top left corner bottom to top left corner top. + drawList->AddLine(vertices[0], vertices[1], colLineTop, lineThickness * 0.5f); + + // Top left corner bottom to bottom left. + drawList->AddRectFilledMultiColor({ vertices[0].x - 0.2f, vertices[0].y }, { vertices[6].x + lineThickness - 0.2f, vertices[6].y }, colLineTop, colLineTop, colLineBottom, colLineBottom); + + // Top left corner top to top right. + drawList->AddLine(vertices[1], vertices[2], colLineTop, lineThickness); + + drawList->PushClipRect({ min.x, min.y + 20.0f }, { max.x, max.y - 5.0f }); +} + +static void DrawSelectionContainer(ImVec2 min, ImVec2 max) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + auto cornerRadius = Scale(10.0f); + 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 vertices[] = { v1, v2, v3, v4, v5, v6, v7, v8 }; + + SetGradient(min, max, IM_COL32(255, 246, 0, 129), IM_COL32(255, 194, 0, 118)); + drawList->AddConvexPolyFilled(vertices, IM_ARRAYSIZE(vertices), IM_COL32(255, 255, 255, 255)); + ResetGradient(); +} + +static void DrawHeaderContainer(const char* text) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + ImVec2 min = { Scale(256.0f), Scale(138.0f) }; + ImVec2 max = { Scale(556.0f), Scale(185.0f) }; + + DrawContainer(min, max, IM_COL32(140, 142, 140, 201), IM_COL32(66, 65, 66, 234), Scale(23.0f)); + drawList->PopClipRect(); + + auto textSize = g_fntNewRodin->CalcTextSizeA(Scale(26.0f), FLT_MAX, 0.0f, text); + + // TODO: skew this text and apply bevel. + DrawTextWithOutline(g_fntNewRodin, Scale(26.0f), { min.x + Scale(20.0f), min.y + textSize.y / 2.0f - 3.0f }, IM_COL32(255, 255, 255, 255), text, Scale(3), IM_COL32(0, 0, 0, 255)); +} + +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(708.0f); + auto itemHeight = Scale(94.0f); + auto itemMarginX = Scale(13.0f); + auto imageMarginX = Scale(25.0f); + auto imageMarginY = Scale(18.0f); + auto imageSize = Scale(60.0f); + + ImVec2 min = { itemMarginX + clipRectMin.x, clipRectMin.y + itemHeight * rowIndex + yOffset }; + ImVec2 max = { itemMarginX + min.x + itemWidth, min.y + itemHeight }; + + auto icon = g_achievementTextures[achievement.ID]; + auto isSelected = rowIndex == g_selectedRowIndex; + + if (isSelected) + DrawSelectionContainer(min, max); + + auto alpha = isUnlocked ? 255 : 127; + auto desc = isUnlocked ? achievement.UnlockedDesc.c_str() : achievement.LockedDesc.c_str(); + auto fontSize = Scale(24.0f); + auto textSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, desc); + auto textX = min.x + imageMarginX + imageSize + itemMarginX * 2.0f; + auto textMarqueeX = min.x + imageMarginX + imageSize; + auto titleTextY = Scale(20.0f); + auto descTextY = Scale(52.0f); + auto cmnShadowOffset = Scale(2.0f); + auto cmnShadowScale = Scale(0.4f); + + // Draw achievement icon. + // TODO: make image greyscale if locked. + drawList->AddImage(icon, { min.x + imageMarginX, min.y + imageMarginY }, { min.x + imageMarginX + imageSize, min.y + imageMarginY + imageSize }, { 0, 0 }, { 1, 1 }, IM_COL32(255, 255, 255, alpha)); + + drawList->PushClipRect(min, max, true); + + // Draw achievement name. + DrawTextWithShadow(g_fntSeurat, fontSize, { textX, min.y + titleTextY }, IM_COL32(252, 243, 5, alpha), achievement.Name.c_str(), cmnShadowOffset, cmnShadowScale, IM_COL32(0, 0, 0, alpha)); + + if (isSelected && textX + textSize.x >= max.x) + { + // Draw achievement description with marquee. + DrawTextWithMarqueeShadow(g_fntSeurat, fontSize, { textX, min.y + descTextY }, { textMarqueeX, min.y }, max, IM_COL32(255, 255, 255, alpha), desc, g_rowSelectionTime, 0.9, 250.0, cmnShadowOffset, cmnShadowScale); + } + else + { + // Draw achievement description. + DrawTextWithShadow(g_fntSeurat, fontSize, { textX, min.y + descTextY }, IM_COL32(255, 255, 255, alpha), desc, cmnShadowOffset, cmnShadowScale, IM_COL32(0, 0, 0, alpha)); + } + + drawList->PopClipRect(); +} + +static void DrawContentContainer() +{ + auto drawList = ImGui::GetForegroundDrawList(); + + ImVec2 min = { Scale(256.0f), Scale(192.0f) }; + ImVec2 max = { Scale(1026.0f), Scale(601.0f) }; + + DrawContainer(min, max, IM_COL32(197, 194, 197, 200), IM_COL32(115, 113, 115, 236), Scale(25.0f)); + + auto clipRectMin = drawList->GetClipRectMin(); + auto clipRectMax = drawList->GetClipRectMax(); + + auto itemHeight = Scale(94.0f); + auto yOffset = -g_firstVisibleRowIndex * itemHeight + Scale(2.0f); + auto rowCount = 0; + + // Draw separators. + for (int i = 1; i <= 3; i++) + { + auto lineMarginLeft = Scale(31.0f); + auto lineMarginRight = Scale(46.0f); + auto lineMarginY = Scale(2.0f); + + 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.0f) }, { lineMax.x, lineMax.y + Scale(1.0f) }, IM_COL32(143, 148, 143, 255)); + } + + for (auto achievement : g_achievements) + { + if (AchievementData::IsUnlocked(achievement.ID)) + DrawAchievement(rowCount++, yOffset, achievement, true); + } + + for (auto achievement : g_achievements) + { + 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 scrollUp = !g_upWasHeld && upIsHeld; + bool scrollDown = !g_downWasHeld && downIsHeld; + + 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; + } + + if (scrollUp || scrollDown) + { + g_rowSelectionTime = ImGui::GetTime(); + g_prevSelectedRowIndex = prevSelectedRowIndex; + Game_PlaySound("sys_actstg_pausecursor"); + } + + g_upWasHeld = upIsHeld; + g_downWasHeld = downIsHeld; + + int visibleRowCount = int(floor((clipRectMax.y - clipRectMin.y) / itemHeight)); + + bool disableMoveAnimation = false; + + if (g_firstVisibleRowIndex > g_selectedRowIndex) + { + g_firstVisibleRowIndex = g_selectedRowIndex; + disableMoveAnimation = true; + } + + if (g_firstVisibleRowIndex + visibleRowCount - 1 < g_selectedRowIndex) + { + g_firstVisibleRowIndex = std::max(0, g_selectedRowIndex - visibleRowCount + 1); + disableMoveAnimation = true; + } + + if (disableMoveAnimation) + g_prevSelectedRowIndex = g_selectedRowIndex; + + // Pop clip rect from DrawContentContainer + drawList->PopClipRect(); + + // Draw scroll bar + if (rowCount > visibleRowCount) + { + float cornerRadius = Scale(25.0f); + float totalHeight = (clipRectMax.y - clipRectMin.y - cornerRadius) - Scale(3.0f); + float heightRatio = float(visibleRowCount) / float(rowCount); + float offsetRatio = float(g_firstVisibleRowIndex) / float(rowCount); + float offsetX = clipRectMax.x - Scale(31.0f); + float offsetY = offsetRatio * totalHeight + clipRectMin.y + Scale(4.0f); + float lineThickness = Scale(1.0f); + float innerMarginX = Scale(2.0f); + float outerMarginX = Scale(16.0f); + + // Outline + drawList->AddRect + ( + { offsetX - lineThickness, clipRectMin.y - lineThickness }, + { clipRectMax.x - outerMarginX + lineThickness, max.y - cornerRadius + lineThickness }, + IM_COL32(255, 255, 255, 155), + Scale(0.5f) + ); + + // Background + drawList->AddRectFilledMultiColor + ( + { offsetX, clipRectMin.y }, + { clipRectMax.x - outerMarginX, max.y - cornerRadius }, + IM_COL32(82, 85, 82, 186), + IM_COL32(82, 85, 82, 186), + IM_COL32(74, 73, 74, 185), + IM_COL32(74, 73, 74, 185) + ); + + // Scroll Bar Outline + drawList->AddRectFilledMultiColor + ( + { offsetX + innerMarginX, offsetY - lineThickness }, + { clipRectMax.x - outerMarginX - innerMarginX, 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 + ( + { offsetX + innerMarginX + lineThickness, offsetY }, + { clipRectMax.x - outerMarginX - innerMarginX - lineThickness, offsetY + totalHeight * heightRatio }, + IM_COL32(255, 255, 255, 255) + ); + } +} + +void AchievementMenu::Draw() +{ + if (!s_isVisible) + return; + + DrawHeaderContainer(Localise("Achievements_Name_Uppercase").c_str()); + DrawContentContainer(); +} + +void AchievementMenu::Open() +{ + s_isVisible = true; + + ResetSelection(); + Game_PlaySound("sys_actstg_pausewinopen"); +} + +void AchievementMenu::Close() +{ + s_isVisible = false; + + Game_PlaySound("sys_actstg_pausewinclose"); + Game_PlaySound("sys_actstg_pausecansel"); +} diff --git a/UnleashedRecomp/ui/achievement_menu.h b/UnleashedRecomp/ui/achievement_menu.h new file mode 100644 index 00000000..3bbfcd05 --- /dev/null +++ b/UnleashedRecomp/ui/achievement_menu.h @@ -0,0 +1,14 @@ +#pragma once + +#include "imgui_view.h" + +class AchievementMenu : ImGuiView +{ +public: + inline static bool s_isVisible = false; + + void Init() override; + void Draw() override; + static void Open(); + static void Close(); +}; diff --git a/UnleashedRecomp/ui/achievement_overlay.cpp b/UnleashedRecomp/ui/achievement_overlay.cpp index 7040db69..f48a3332 100644 --- a/UnleashedRecomp/ui/achievement_overlay.cpp +++ b/UnleashedRecomp/ui/achievement_overlay.cpp @@ -9,6 +9,8 @@ #include #include +AchievementOverlay m_achievementOverlay; + 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; @@ -30,7 +32,9 @@ void AchievementOverlay::Init() { auto& io = ImGui::GetIO(); - g_fntSeurat = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 30.0f); + constexpr float FONT_SCALE = 2.0f; + + g_fntSeurat = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 26.0f * FONT_SCALE); } static double ComputeMotion(double frameOffset, double frames) @@ -39,8 +43,7 @@ static double ComputeMotion(double frameOffset, double frames) return sqrt(t); } -// TODO: move this somewhere where it can be re-used. -void DrawContainer(ImVec2 min, ImVec2 max, float cornerRadius = 25.0f) +static void DrawContainer(ImVec2 min, ImVec2 max, float cornerRadius = 25.0f) { auto drawList = ImGui::GetForegroundDrawList(); @@ -71,29 +74,27 @@ void DrawContainer(ImVec2 min, ImVec2 max, float cornerRadius = 25.0f) 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 }; + ImVec2 vertices[] = { 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). + auto colGradientBottom = IM_COL32(115, 113, 115, (int)CubicEase(g_isClosing ? 236 : 0, g_isClosing ? 0 : 236, colourMotion)); // 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)); + SetGradient(min, max, colGradientTop, colGradientBottom); + drawList->AddConvexPolyFilled(vertices, IM_ARRAYSIZE(vertices), IM_COL32(255, 255, 255, 255)); + ResetGradient(); - for (int i = 0; i < IM_ARRAYSIZE(border); i++) + drawList->AddPolyline(vertices, IM_ARRAYSIZE(vertices), 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(vertices); i++) { - border[i].x -= 0.4f; - border[i].y -= 0.2f; + vertices[i].x -= 0.4f; + vertices[i].y -= 0.2f; } auto lineAlpha = (int)CubicEase(g_isClosing ? 230 : 0, g_isClosing ? 0 : 230, colourMotion); @@ -102,13 +103,13 @@ void DrawContainer(ImVec2 min, ImVec2 max, float cornerRadius = 25.0f) auto lineThickness = Scale(1.0f); // Top left corner bottom to top left corner top. - drawList->AddLine(border[0], border[1], colLineTop, lineThickness * 0.5f); + drawList->AddLine(vertices[0], vertices[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); + drawList->AddRectFilledMultiColor({ vertices[0].x - 0.2f, vertices[0].y }, { vertices[6].x + lineThickness - 0.2f, vertices[6].y }, colLineTop, colLineTop, colLineBottom, colLineBottom); // Top left corner top to top right. - drawList->AddLine(border[1], border[2], colLineTop, lineThickness); + drawList->AddLine(vertices[1], vertices[2], colLineTop, lineThickness); drawList->PushClipRect(min, max); } @@ -151,8 +152,11 @@ void AchievementOverlay::Draw() 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)); + auto cmnShadowOffset = Scale(2.0f); + auto cmnShadowScale = Scale(0.4f); + + DrawTextWithShadow(g_fntSeurat, fontSize, { min.x + textX + (longestTextSize - headerTextSize.x) / 2.0f, min.y + 30.0f}, IM_COL32(252, 243, 5, alpha), strAchievementUnlocked, cmnShadowOffset, cmnShadowScale, 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, cmnShadowOffset, cmnShadowScale, IM_COL32(0, 0, 0, alpha)); // Pop clip rect from DrawContainer drawList->PopClipRect(); diff --git a/UnleashedRecomp/ui/achievement_overlay.h b/UnleashedRecomp/ui/achievement_overlay.h index ac7e3aec..02d80011 100644 --- a/UnleashedRecomp/ui/achievement_overlay.h +++ b/UnleashedRecomp/ui/achievement_overlay.h @@ -1,12 +1,14 @@ #pragma once -struct AchievementOverlay +#include "imgui_view.h" + +class AchievementOverlay : ImGuiView { public: inline static bool s_isVisible = false; - static void Init(); - static void Draw(); + void Init() override; + void Draw() override; static void Open(int id); static void Close(); }; diff --git a/UnleashedRecomp/ui/imgui_utils.h b/UnleashedRecomp/ui/imgui_utils.h index eff5bb42..974166db 100644 --- a/UnleashedRecomp/ui/imgui_utils.h +++ b/UnleashedRecomp/ui/imgui_utils.h @@ -1,5 +1,46 @@ #pragma once +#include + +static std::vector> g_callbackData; +static uint32_t g_callbackDataIndex = 0; + +static ImGuiCallbackData* AddCallback(ImGuiCallback callback) +{ + if (g_callbackDataIndex >= g_callbackData.size()) + g_callbackData.emplace_back(std::make_unique()); + + auto& callbackData = g_callbackData[g_callbackDataIndex]; + ++g_callbackDataIndex; + + ImGui::GetForegroundDrawList()->AddCallback(reinterpret_cast(callback), callbackData.get()); + + return callbackData.get(); +} + +static void SetGradient(const ImVec2& min, const ImVec2& max, ImU32 top, ImU32 bottom) +{ + auto callbackData = AddCallback(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 = AddCallback(ImGuiCallback::SetGradient); + memset(&callbackData->setGradient, 0, sizeof(callbackData->setGradient)); +} + +static void SetShaderModifier(uint32_t shaderModifier) +{ + auto callbackData = AddCallback(ImGuiCallback::SetShaderModifier); + callbackData->setShaderModifier.shaderModifier = shaderModifier; +} + // Aspect ratio aware. static float Scale(float size) { @@ -43,28 +84,64 @@ static void DrawTextWithMarquee(const ImFont* font, float fontSize, const ImVec2 drawList->PopClipRect(); } -static void DrawTextWithOutline(const ImFont* font, float fontSize, const ImVec2& pos, ImU32 color, const char* text, int32_t outlineSize, ImU32 outlineColor) +template +static void DrawTextWithOutline(const ImFont* font, float fontSize, const ImVec2& pos, ImU32 color, const char* text, T outlineSize, ImU32 outlineColor) { auto drawList = ImGui::GetForegroundDrawList(); - // TODO: This is very inefficient! - for (int32_t i = -outlineSize + 1; i < outlineSize; i++) + if constexpr (std::is_same_v || std::is_same_v) { - for (int32_t j = -outlineSize + 1; j < outlineSize; j++) - drawList->AddText(font, fontSize, { pos.x + i, pos.y + j }, outlineColor, text); + // 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) + { + // 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)) +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(); - drawList->AddText(font, fontSize, { pos.x + offset, pos.y + offset }, shadowColour, text); + DrawTextWithOutline(font, fontSize, { pos.x + offset, pos.y + offset }, shadowColour, text, radius, shadowColour); drawList->AddText(font, fontSize, pos, colour, text); } +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.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) + 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; diff --git a/UnleashedRecomp/ui/imgui_view.cpp b/UnleashedRecomp/ui/imgui_view.cpp new file mode 100644 index 00000000..02c45831 --- /dev/null +++ b/UnleashedRecomp/ui/imgui_view.cpp @@ -0,0 +1,7 @@ +#include "imgui_view.h" + +std::vector& GetImGuiViews() +{ + static std::vector g_imGuiViews; + return g_imGuiViews; +} diff --git a/UnleashedRecomp/ui/imgui_view.h b/UnleashedRecomp/ui/imgui_view.h new file mode 100644 index 00000000..d6441291 --- /dev/null +++ b/UnleashedRecomp/ui/imgui_view.h @@ -0,0 +1,23 @@ +#pragma once + +class IImGuiView +{ +public: + virtual ~IImGuiView() = default; + virtual void Init() = 0; + virtual void Draw() = 0; +}; + +std::vector& GetImGuiViews(); + +class ImGuiView : public IImGuiView +{ +public: + ImGuiView() + { + GetImGuiViews().emplace_back(this); + } + + void Init() override {} + void Draw() override {} +}; diff --git a/UnleashedRecomp/ui/options_menu.cpp b/UnleashedRecomp/ui/options_menu.cpp index 1a5a9f77..cde57d3e 100644 --- a/UnleashedRecomp/ui/options_menu.cpp +++ b/UnleashedRecomp/ui/options_menu.cpp @@ -12,6 +12,8 @@ #include +OptionsMenu m_optionsMenu; + constexpr float COMMON_PADDING_POS_Y = 118.0f; constexpr float COMMON_PADDING_POS_X = 30.0f; constexpr float INFO_CONTAINER_POS_X = 870.0f; @@ -39,45 +41,6 @@ void OptionsMenu::Init() g_newRodinFont = io.Fonts->AddFontFromFileTTF("FOT-NewRodinPro-DB.otf", 20.0f * FONT_SCALE); } -static std::vector> g_callbackData; -static uint32_t g_callbackDataIndex = 0; - -static ImGuiCallbackData* AddCallback(ImGuiCallback callback) -{ - if (g_callbackDataIndex >= g_callbackData.size()) - g_callbackData.emplace_back(std::make_unique()); - - auto& callbackData = g_callbackData[g_callbackDataIndex]; - ++g_callbackDataIndex; - - ImGui::GetForegroundDrawList()->AddCallback(reinterpret_cast(callback), callbackData.get()); - - return callbackData.get(); -} - -static void SetGradient(const ImVec2& min, const ImVec2& max, ImU32 top, ImU32 bottom) -{ - auto callbackData = AddCallback(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 = AddCallback(ImGuiCallback::SetGradient); - memset(&callbackData->setGradient, 0, sizeof(callbackData->setGradient)); -} - -static void SetShaderModifier(uint32_t shaderModifier) -{ - auto callbackData = AddCallback(ImGuiCallback::SetShaderModifier); - callbackData->setShaderModifier.shaderModifier = shaderModifier; -} - static void DrawScanlineBars() { constexpr uint32_t COLOR0 = IM_COL32(203, 255, 0, 0); @@ -135,7 +98,8 @@ static void DrawScanlineBars() SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); // Options text - DrawTextWithOutline(g_dfsogeistdFont, Scale(48.0f), { Scale(122.0f), Scale(56.0f) }, IM_COL32(255, 195, 0, 255), "OPTIONS", Scale(4), IM_COL32_BLACK); + // TODO: localise this. + DrawTextWithOutline(g_dfsogeistdFont, Scale(48.0f), { Scale(122.0f), Scale(56.0f) }, IM_COL32(255, 195, 0, 255), "OPTIONS", Scale(4), IM_COL32_BLACK); // Top bar line drawList->AddLine( @@ -417,7 +381,7 @@ static bool DrawCategories() IM_COL32(128, 255, 0, alpha), IM_COL32(255, 192, 0, alpha)); - DrawTextWithOutline( + DrawTextWithOutline( g_dfsogeistdFont, size, min, @@ -752,7 +716,7 @@ static void DrawConfigOption(int32_t rowIndex, float yOffset, ConfigDef* conf IM_COL32(128, 170, 0, 255) ); - DrawTextWithOutline( + DrawTextWithOutline( g_newRodinFont, size, min, diff --git a/UnleashedRecomp/ui/options_menu.h b/UnleashedRecomp/ui/options_menu.h index 6ea404b5..90cdbd42 100644 --- a/UnleashedRecomp/ui/options_menu.h +++ b/UnleashedRecomp/ui/options_menu.h @@ -1,8 +1,9 @@ #pragma once +#include "imgui_view.h" #include -struct OptionsMenu +struct OptionsMenu : ImGuiView { public: inline static bool s_isVisible = false; @@ -10,8 +11,8 @@ public: inline static SWA::EMenuType s_pauseMenuType; - static void Init(); - static void Draw(); + void Init() override; + void Draw() override; static void Open(bool isPause = false, SWA::EMenuType pauseMenuType = SWA::eMenuType_WorldMap); static void Close(); diff --git a/UnleashedRecomp/ui/sdl_listener.cpp b/UnleashedRecomp/ui/sdl_listener.cpp new file mode 100644 index 00000000..cb503b01 --- /dev/null +++ b/UnleashedRecomp/ui/sdl_listener.cpp @@ -0,0 +1,7 @@ +#include "sdl_listener.h" + +std::vector& GetEventListeners() +{ + static std::vector g_eventListeners; + return g_eventListeners; +} diff --git a/UnleashedRecomp/ui/sdl_listener.h b/UnleashedRecomp/ui/sdl_listener.h index 785b774b..8ca2cad3 100644 --- a/UnleashedRecomp/ui/sdl_listener.h +++ b/UnleashedRecomp/ui/sdl_listener.h @@ -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& GetEventListeners(); + class SDLEventListener : public ISDLEventListener { public: SDLEventListener() { - Window::s_eventListeners.emplace_back(this); + GetEventListeners().emplace_back(this); } void OnSDLEvent(SDL_Event* event) override {} diff --git a/UnleashedRecomp/ui/window.cpp b/UnleashedRecomp/ui/window.cpp index da0d4ae4..8fe785f4 100644 --- a/UnleashedRecomp/ui/window.cpp +++ b/UnleashedRecomp/ui/window.cpp @@ -131,7 +131,7 @@ int Window_OnSDLEvent(void*, SDL_Event* event) } } - for (auto listener : Window::s_eventListeners) + for (auto listener : GetEventListeners()) listener->OnSDLEvent(event); return 0; diff --git a/UnleashedRecomp/ui/window.h b/UnleashedRecomp/ui/window.h index e85a9adb..bceb219d 100644 --- a/UnleashedRecomp/ui/window.h +++ b/UnleashedRecomp/ui/window.h @@ -8,8 +8,6 @@ #define DEFAULT_WIDTH 1280 #define DEFAULT_HEIGHT 720 -class SDLEventListener; - class Window { public: @@ -24,8 +22,6 @@ public: inline static bool s_isFocused; inline static bool s_isIconNight; - inline static std::vector s_eventListeners; - static SDL_Surface* GetIconSurface(void* pIconBmp, size_t iconSize) { auto rw = SDL_RWFromMem(pIconBmp, iconSize); diff --git a/UnleashedRecomp/user/achievement_data.cpp b/UnleashedRecomp/user/achievement_data.cpp index 25b4670c..640dcade 100644 --- a/UnleashedRecomp/user/achievement_data.cpp +++ b/UnleashedRecomp/user/achievement_data.cpp @@ -5,7 +5,7 @@ bool AchievementData::IsUnlocked(uint16_t id) { - for (int i = 0; i < sizeof(Data.Records); i++) + for (int i = 0; i < sizeof(Data.Records) / sizeof(Record); i++) { if (Data.Records[i].ID == id) return true; @@ -19,7 +19,7 @@ void AchievementData::Unlock(uint16_t id) if (IsUnlocked(id)) return; - for (int i = 0; i < sizeof(Data.Records); i++) + for (int i = 0; i < sizeof(Data.Records) / sizeof(Record); i++) { if (Data.Records[i].ID == 0) {