UnleashedRecomp/UnleashedRecomp/ui/achievement_menu.cpp
Skyth (Asilkan) 67633917bf
Linux support. (#54)
* Initial Linux attempt.

* Add clang toolchain & make tools compile.

* vcpkg as submodule.

* First implementation of IO rewrite. (#31)

* Fix directory iteration resolving symlinks.

* Refactor kernel objects to be lock-free.

* Implement guest critical sections using std::atomic.

* Make D3D12 support optional. (#33)

* Make D3D12 support optional.

* Update ShaderRecomp, fix macros.

* Replace QueryPerformanceCounter. (#35)

* Add Linux home path for GetUserPath(). (#36)

* Cross-platform Sleep. (#37)

* Add mmap implementations for virtual allocation. (#38)

* Cross-platform TLS. (#34)

* Cross-platform TLS.

* Fix front() to back(), use Mutex.

* Fix global variable namings.

---------

Co-authored-by: Skyth <19259897+blueskythlikesclouds@users.noreply.github.com>

* Unicode support. (#39)

* Replace CreateDirectoryA with Unicode version.

* Cross platform thread implementation. (#41)

* Cross-platform thread implementation.

* Put set thread name calls behind a Win32 macro.

* Cross-platform semaphore implementation. (#43)

* xam: use SDL for keyboard input

* Cross-platform atomic operations. (#44)

* Cross-platform spin lock implementation.

* Cross-platform reference counting.

* Cross-platform event implementation. (#47)

* Compiling and running on Linux. (#49)

* Current work trying to get it to compile.

* Update vcpkg.json baseline.

* vcpkg, memory mapped file.

* Bitscan forward.

* Fix localtime_s.

* FPS patches high res clock.

* Rename Window to GameWindow. Fix guest pointers.

* GetCurrentThreadID gone.

* Code cache pointers, RenderWindow type.

* Add Linux stubs.

* Refactor Config.

* Fix paths.

* Add linux-release config.

* FS fixes.

* Fix Windows compilation errors & unicode converter crash.

* Rename physical memory allocation functions to not clash with X11.

* Fix NULL character being added on RtlMultiByteToUnicodeN.

* Use std::exit.

* Add protection to memory on Linux.

* Convert majority of dependencies to submodules. (#48)

* Convert majority of dependencies to submodules.

* Don't compile header-only libraries.

* Fix a few incorrect data types.

* Fix config directory.

* Unicode fixes & sizeof asserts.

* Change the exit function to not call static destructors.

* Fix files picker.

* Add RelWithDebInfo preset for Linux.

* Implement OS Restart on Linux. (#50)

---------

Co-authored-by: Dario <dariosamo@gmail.com>

* Update PowerRecomp.

* Add Env Var detection for VCPKG_ROOT, add DLC detection.

* Use error code version on DLC directory iterator.

* Set D3D12MA::ALLOCATOR_FLAG_DONT_PREFER_SMALL_BUFFERS_COMMITTED flag.

* Linux flatpak. (#51)

* Add flatpak support.

* Add game install directory override for flatpak.

* Flatpak'ing.

* Flatpak it some more.

* We flat it, we pak it.

* Flatpak'd.

* The Marvelous Misadventures of Flatpak.

* Attempt to change logic of NFD and show error.

* Flattenpakken.

* Use game install directory instead of current path.

* Attempt to fix line endings.

* Update io.github.hedge_dev.unleashedrecomp.json

* Fix system time query implementation.

* Add Present Wait to Vulkan to improve frame pacing and reduce latency. (#53)

* Add present wait support to Vulkan.

* Default to triple buffering if presentWait is supported.

* Bracey fellas.

* Update paths.h

* SDL2 audio (again). (#52)

* Implement SDL2 audio (again).

* Call timeBeginPeriod/timeEndPeriod.

* Replace miniaudio with SDL mixer.

* Queue audio samples in a separate thread.

* Enable CMake option override policy & fix compilation error.

* Fix compilation error on Linux.

* Fix but also trim shared strings.

* Wayland support. (#55)

* Make channel index a global variable in embedded player.

* Fix SDL Audio selection for OGG on Flatpak.

* Minor installer wizard fixes.

* Fix compilation error.

* Yield in model consumer and pipeline compiler threads.

* Special case Sleep(0) to yield on Linux.

* Add App Id hint.

* Correct implementation for auto reset events. (#57)

---------

Co-authored-by: Dario <dariosamo@gmail.com>
Co-authored-by: Hyper <34012267+hyperbx@users.noreply.github.com>
2024-12-21 00:44:05 +03:00

832 lines
27 KiB
C++

#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/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);
SetTextSkew((min.y + max.y) / 2.0f, Scale(3.0f));
// TODO: Apply bevel.
DrawTextWithOutline
(
g_fntNewRodinUB,
fontSize,
{ /* X */ min.x + textMarginX, /* Y */ CENTRE_TEXT_VERT(min, max, textSize) - Scale(5) },
IM_COL32(255, 255, 255, 255 * alpha),
text,
4,
IM_COL32(0, 0, 0, 255 * alpha)
);
ResetTextSkew();
}
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);
if (!isUnlocked)
SetShaderModifier(IMGUI_SHADER_MODIFIER_GRAYSCALE);
// Draw achievement icon.
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))
);
if (!isUnlocked)
SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE);
drawList->PushClipRect(min, max, true);
auto colLockedText = IM_COL32(80, 80, 80, 127);
auto colTextShadow = isUnlocked
? IM_COL32(0, 0, 0, 255)
: IM_COL32(20, 20, 20, 127);
auto shadowOffset = isUnlocked ? 2 : 1;
auto shadowRadius = isUnlocked ? 1 : 0.5f;
// Draw achievement name.
DrawTextWithShadow
(
g_fntSeurat,
fontSize,
{ textX, min.y + titleTextY },
isUnlocked ? IM_COL32(252, 243, 5, 255) : colLockedText,
achievement.Name.c_str(),
shadowOffset,
shadowRadius,
colTextShadow
);
ImVec2 marqueeMin = { textMarqueeX, min.y };
ImVec2 marqueeMax = { max.x - Scale(10) /* timestamp margin X */, max.y };
SetMarqueeFade(marqueeMin, marqueeMax, Scale(32));
if (isSelected && textX + textSize.x >= max.x - Scale(10))
{
// Draw achievement description with marquee.
DrawTextWithMarqueeShadow
(
g_fntSeurat,
fontSize,
{ textX, min.y + descTextY },
marqueeMin,
marqueeMax,
isUnlocked ? IM_COL32_WHITE : colLockedText,
desc,
g_rowSelectionTime,
0.9,
Scale(250),
shadowOffset,
shadowRadius,
colTextShadow
);
}
else
{
// Draw achievement description.
DrawTextWithShadow
(
g_fntSeurat,
fontSize,
{ textX, min.y + descTextY },
isUnlocked ? IM_COL32_WHITE : colLockedText,
desc,
shadowOffset,
shadowRadius,
colTextShadow
);
}
ResetMarqueeFade();
drawList->PopClipRect();
if (!isUnlocked)
return;
auto timestamp = AchievementData::GetTimestamp(achievement.ID);
if (!timestamp)
return;
char buffer[32];
#ifdef _WIN32
tm timeStruct;
tm *timePtr = &timeStruct;
localtime_s(timePtr, &timestamp);
#else
tm *timePtr = localtime(&timestamp);
#endif
strftime(buffer, sizeof(buffer), "%Y/%m/%d %H:%M", timePtr);
fontSize = Scale(12);
textSize = g_fntNewRodinDB->CalcTextSizeA(fontSize, FLT_MAX, 0, buffer);
auto containerMarginX = Scale(10);
auto textMarginX = Scale(8);
ImVec2 timestampMin = { max.x - containerMarginX - textSize.x - (textMarginX * 2), min.y + titleTextY };
ImVec2 timestampMax = { max.x - containerMarginX, min.y + Scale(46) };
drawList->PushClipRect(min, max, true);
auto bevelOffset = Scale(6);
// Left
drawList->AddRectFilledMultiColor
(
timestampMin,
{ timestampMin.x + bevelOffset, timestampMax.y },
IM_COL32(255, 255, 255, 255),
IM_COL32(149, 149, 149, 40),
IM_COL32(149, 149, 149, 40),
IM_COL32(255, 255, 255, 255)
);
// Right
drawList->AddRectFilledMultiColor
(
{ timestampMax.x - bevelOffset, timestampMin.y },
{ timestampMax.x, timestampMax.y },
IM_COL32(149, 149, 149, 40),
IM_COL32(255, 255, 255, 255),
IM_COL32(255, 255, 255, 255),
IM_COL32(149, 149, 149, 40)
);
// Centre
drawList->AddRectFilled
(
{ timestampMin.x, timestampMin.y + bevelOffset },
{ timestampMax.x, timestampMax.y - bevelOffset },
IM_COL32(38, 38, 38, 172)
);
// Top
drawList->AddRectFilledMultiColor
(
timestampMin,
{ timestampMax.x, timestampMin.y + bevelOffset },
IM_COL32(16, 16, 16, 192),
IM_COL32(16, 16, 16, 192),
IM_COL32(38, 38, 38, 172),
IM_COL32(38, 38, 38, 172)
);
// Bottom
drawList->AddRectFilledMultiColor
(
{ timestampMin.x, timestampMax.y - bevelOffset },
{ timestampMax.x, timestampMax.y },
IM_COL32(38, 40, 38, 169),
IM_COL32(38, 40, 38, 169),
IM_COL32(16, 16, 16, 192),
IM_COL32(16, 16, 16, 192)
);
// Draw timestamp text.
DrawTextWithOutline
(
g_fntNewRodinDB,
fontSize,
{ /* X */ CENTRE_TEXT_HORZ(timestampMin, timestampMax, textSize), /* Y */ CENTRE_TEXT_VERT(timestampMin, timestampMax, textSize) },
IM_COL32(255, 255, 255, 255),
buffer,
4,
IM_COL32(8, 8, 8, 255)
);
drawList->PopClipRect();
}
static void DrawTrophySparkles(ImVec2 min, ImVec2 max, int recordCount, int trophyFrameIndex)
{
auto drawList = ImGui::GetForegroundDrawList();
constexpr auto recordsHalfTotal = ACH_RECORDS / 2;
// Don't sparkle the bronze trophy.
if (recordCount < recordsHalfTotal)
return;
static int trophyAnimCycles = 0;
static bool isIncrementedCycles = false;
bool isGoldTrophy = recordCount >= ACH_RECORDS;
if (!trophyFrameIndex && !isIncrementedCycles)
{
trophyAnimCycles++;
trophyAnimCycles %= isGoldTrophy ? 4 : 3;
isIncrementedCycles = true;
}
if (trophyFrameIndex >= 1)
isIncrementedCycles = false;
if (trophyAnimCycles >= 2)
{
auto marginX = Scale(9);
auto uv = PIXELS_TO_UV_COORDS(2048, 1024, 1984, 960, 64, 64);
auto& uv0 = std::get<0>(uv);
auto& uv1 = std::get<1>(uv);
auto colour = IM_COL32(240, 240, 200, 200);
static auto scaleStart = ImGui::GetTime();
auto scale = Scale(18) * Hermite(1.0f, 0.0f, (sin((ImGui::GetTime() - scaleStart) * (2.0f * M_PI / (15.0f / 60.0f))) + 1.0f) / 2.0f);
// Don't do extra sparkles for the silver trophy.
if (isGoldTrophy)
{
if (trophyFrameIndex >= 0 && trophyFrameIndex <= 5)
{
auto marginXAdd = Scale(1);
// Centre Left
drawList->AddImage
(
g_upTrophyIcon.get(),
{ min.x - scale / 2 + marginX + marginXAdd, max.y - ((max.y - min.y) / 2) - scale / 2 },
{ min.x + scale / 2 + marginX + marginXAdd, max.y - ((max.y - min.y) / 2) + scale / 2 },
uv0, uv1,
colour
);
}
if (trophyFrameIndex >= 16 && trophyFrameIndex <= 21)
{
auto marginXAdd = Scale(4);
auto marginY = Scale(11);
// Bottom Right
drawList->AddImage
(
g_upTrophyIcon.get(),
{ max.x - scale / 2 - (marginX + marginXAdd), max.y - scale / 2 - marginY },
{ max.x + scale / 2 - (marginX + marginXAdd), max.y + scale / 2 - marginY },
uv0, uv1,
colour
);
}
}
if (trophyFrameIndex >= 24 && trophyFrameIndex <= 29)
{
auto marginY = Scale(1);
// Top Right
drawList->AddImage
(
g_upTrophyIcon.get(),
{ max.x - scale / 2 - marginX, min.y - scale / 2 + marginY },
{ max.x + scale / 2 - marginX, min.y + scale / 2 + marginY },
uv0, uv1,
colour
);
}
}
}
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);
constexpr auto recordsHalfTotal = ACH_RECORDS / 2;
auto records = AchievementData::GetTotalRecords();
ImVec4 colBronze = ImGui::ColorConvertU32ToFloat4(IM_COL32(198, 105, 15, 255 * alpha));
ImVec4 colSilver = ImGui::ColorConvertU32ToFloat4(IM_COL32(220, 220, 220, 255 * alpha));
ImVec4 colGold = ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 195, 56, 255 * alpha));
ImVec4 colResult;
if (records <= 25)
{
float t = (float)records / 25.0f;
// Fade from bronze to silver.
colResult.x = colBronze.x + t * (colSilver.x - colBronze.x);
colResult.y = colBronze.y + t * (colSilver.y - colBronze.y);
colResult.z = colBronze.z + t * (colSilver.z - colBronze.z);
colResult.w = colBronze.w + t * (colSilver.w - colBronze.w);
}
else if (records <= 50)
{
float t = ((float)records - 25.0f) / 25.0f;
// Fade from silver to gold.
colResult.x = colSilver.x + t * (colGold.x - colSilver.x);
colResult.y = colSilver.y + t * (colGold.y - colSilver.y);
colResult.z = colSilver.z + t * (colGold.z - colSilver.z);
colResult.w = colSilver.w + t * (colGold.w - colSilver.w);
}
else
{
colResult = colGold;
}
drawList->AddImage(g_upTrophyIcon.get(), imageMin, imageMax, uv0, uv1, ImGui::ColorConvertFloat4ToU32(colResult));
// Add extra luminance to the trophy for bronze and gold.
if (records < recordsHalfTotal || records >= ACH_RECORDS)
drawList->AddImage(g_upTrophyIcon.get(), imageMin, imageMax, uv0, uv1, IM_COL32(255, 255, 255, 12));
// Draw sparkles on the trophy for silver and gold.
if (records >= recordsHalfTotal || records >= ACH_RECORDS)
DrawTrophySparkles(imageMin, imageMax, records, frameIndex);
auto str = fmt::format("{} / {}", records, ACH_RECORDS);
auto fontSize = Scale(20);
auto textSize = g_fntNewRodinDB->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str());
DrawTextWithOutline
(
g_fntNewRodinDB,
fontSize,
{ /* X */ imageMin.x - textSize.x - Scale(6), /* Y */ CENTRE_TEXT_VERT(imageMin, imageMax, textSize) },
IM_COL32(255, 255, 255, 255 * alpha),
str.c_str(),
4,
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();
g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf");
g_fntNewRodinDB = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-DB.otf");
g_fntNewRodinUB = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-UB.otf");
g_upTrophyIcon = LOAD_ZSTD_TEXTURE(g_trophy);
g_upSelectionCursor = LOAD_ZSTD_TEXTURE(g_select_fill);
g_upWindow = LOAD_ZSTD_TEXTURE(g_general_window);
}
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))
{
if (Config::Language == ELanguage::English)
achievement.Name = xdbf::FixInvalidSequences(achievement.Name);
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");
}