From dd294a30d43a1189eea523aaf25a1c85b27b294e Mon Sep 17 00:00:00 2001 From: "Skyth (Asilkan)" <19259897+blueskythlikesclouds@users.noreply.github.com> Date: Tue, 11 Feb 2025 23:50:24 +0300 Subject: [PATCH] Fix achievement sound not playing in certain conditions. (#373) * Fix achievement sound not playing in cutscenes. * Prevent achievement overlay from showing if sounds cannot be played. --- UnleashedRecomp/exports.cpp | 6 +- UnleashedRecomp/kernel/xdbf.h | 4 +- UnleashedRecomp/patches/inspire_patches.cpp | 15 ++-- UnleashedRecomp/patches/inspire_patches.h | 2 + UnleashedRecomp/ui/achievement_overlay.cpp | 76 +++++++++++++++------ 5 files changed, 73 insertions(+), 30 deletions(-) diff --git a/UnleashedRecomp/exports.cpp b/UnleashedRecomp/exports.cpp index 15980ae..e516243 100644 --- a/UnleashedRecomp/exports.cpp +++ b/UnleashedRecomp/exports.cpp @@ -5,6 +5,7 @@ #include #include #include +#include void Game_PlaySound(const char* pName) { @@ -14,8 +15,11 @@ void Game_PlaySound(const char* pName) } else { + // Use EVENT category in cutscenes since SYSTEM gets muted by the game. + uint32_t category = !InspirePatches::s_sceneName.empty() ? 10 : 7; + guest_stack_var soundPlayer; - GuestToHostFunction(sub_82B4DF50, soundPlayer.get(), ((be*)g_memory.Translate(0x83367900))->get(), 7, 0, 0); + GuestToHostFunction(sub_82B4DF50, soundPlayer.get(), ((be*)g_memory.Translate(0x83367900))->get(), category, 0, 0); auto soundPlayerVtable = (be*)g_memory.Translate(*(be*)soundPlayer->get()); uint32_t virtualFunction = *(soundPlayerVtable + 1); diff --git a/UnleashedRecomp/kernel/xdbf.h b/UnleashedRecomp/kernel/xdbf.h index 040bac1..249f8c8 100644 --- a/UnleashedRecomp/kernel/xdbf.h +++ b/UnleashedRecomp/kernel/xdbf.h @@ -10,12 +10,12 @@ namespace xdbf { inline std::string& FixInvalidSequences(std::string& str) { - static std::vector invalidSequences = + static std::array invalidSequences = { "\xE2\x80\x99" }; - static std::vector replaceSequences = + static std::array replaceSequences = { "'" }; diff --git a/UnleashedRecomp/patches/inspire_patches.cpp b/UnleashedRecomp/patches/inspire_patches.cpp index f7cfcf6..a918936 100644 --- a/UnleashedRecomp/patches/inspire_patches.cpp +++ b/UnleashedRecomp/patches/inspire_patches.cpp @@ -5,8 +5,9 @@ #include #include +std::string InspirePatches::s_sceneName; + static SWA::Inspire::CScene* g_pScene; -static std::string g_sceneName; static bool g_isFirstFrameChecked; static uint32_t g_eventDispatchCount; @@ -49,7 +50,7 @@ PPC_FUNC(sub_82B98D30) __imp__sub_82B98D30(ctx, base); g_pScene = nullptr; - g_sceneName.clear(); + InspirePatches::s_sceneName.clear(); SDL_User_EvilSonic(App::s_isWerehog); } @@ -59,7 +60,7 @@ PPC_FUNC(sub_82B9BA98) { auto sceneName = (Hedgehog::Base::CSharedString*)g_memory.Translate(ctx.r5.u32); - g_sceneName = sceneName->c_str(); + InspirePatches::s_sceneName = sceneName->c_str(); __imp__sub_82B9BA98(ctx, base); } @@ -72,7 +73,7 @@ void InspirePatches::DrawDebug() return; } - ImGui::Text("Name: %s", g_sceneName.c_str()); + ImGui::Text("Name: %s", InspirePatches::s_sceneName.c_str()); ImGui::Text("Frame: %f", g_pScene->m_pData->Frame.get()); ImGui::Text("Cut: %d", g_pScene->m_pData->Cut.get()); @@ -97,17 +98,17 @@ void InspirePatches::DrawDebug() void InspirePatches::Update() { - if (!g_pScene || !g_sceneName.size()) + if (!g_pScene || !InspirePatches::s_sceneName.size()) return; - if (!g_isFirstFrameChecked && std::find(g_alwaysEvilSonic.begin(), g_alwaysEvilSonic.end(), g_sceneName) != g_alwaysEvilSonic.end()) + if (!g_isFirstFrameChecked && std::find(g_alwaysEvilSonic.begin(), g_alwaysEvilSonic.end(), InspirePatches::s_sceneName) != g_alwaysEvilSonic.end()) { SDL_User_EvilSonic(true); g_isFirstFrameChecked = true; return; } - auto findResult = g_evilSonicTimings.find(g_sceneName); + auto findResult = g_evilSonicTimings.find(InspirePatches::s_sceneName); if (findResult != g_evilSonicTimings.end()) { diff --git a/UnleashedRecomp/patches/inspire_patches.h b/UnleashedRecomp/patches/inspire_patches.h index 2ff4f75..f553759 100644 --- a/UnleashedRecomp/patches/inspire_patches.h +++ b/UnleashedRecomp/patches/inspire_patches.h @@ -3,6 +3,8 @@ class InspirePatches { public: + static std::string s_sceneName; + static void DrawDebug(); static void Update(); }; diff --git a/UnleashedRecomp/ui/achievement_overlay.cpp b/UnleashedRecomp/ui/achievement_overlay.cpp index 13aa568..18b255a 100644 --- a/UnleashedRecomp/ui/achievement_overlay.cpp +++ b/UnleashedRecomp/ui/achievement_overlay.cpp @@ -10,6 +10,7 @@ #include #include #include +#include constexpr double OVERLAY_CONTAINER_COMMON_MOTION_START = 0; constexpr double OVERLAY_CONTAINER_COMMON_MOTION_END = 11; @@ -80,14 +81,67 @@ void AchievementOverlay::Init() g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf"); } +// Dequeue achievements only when we can actually play sounds. +// Loading thread does not update this object. +static bool g_soundAdministratorUpdated; + +PPC_FUNC_IMPL(__imp__sub_82B43480); +PPC_FUNC(sub_82B43480) +{ + g_soundAdministratorUpdated = true; + __imp__sub_82B43480(ctx, base); +} + +// Dequeue achievements only in the main thread. This is also extra thread safety. +static std::thread::id g_mainThreadId = std::this_thread::get_id(); + +static bool CanDequeueAchievement() +{ + if (g_soundAdministratorUpdated && std::this_thread::get_id() == g_mainThreadId && !AchievementOverlay::s_queue.empty()) + { + // Check if we can actually play any audio right now. If not, we'll wait until we can. + uint32_t audioCenter = *reinterpret_cast*>(g_memory.Translate(0x83362FFC)); + if (audioCenter != NULL) + { + uint32_t member = *reinterpret_cast*>(g_memory.Translate(audioCenter + 0x4)); + uint32_t category = !InspirePatches::s_sceneName.empty() ? 10 : 7; // EVENT category is used during Inspire cutscenes. + + // Check if the volume is non zero. + return *reinterpret_cast(g_memory.Translate(member + 0x7C + category * 0x10 + 0x8)) != 0; + } + } + + return false; +} + void AchievementOverlay::Draw() { + if (!AchievementOverlay::s_isVisible && CanDequeueAchievement()) + { + s_isVisible = true; + g_isClosing = false; + g_appearTime = ImGui::GetTime(); + g_achievement = g_xdbfWrapper.GetAchievement((EXDBFLanguage)Config::Language.Value, s_queue.front()); + s_queue.pop(); + + if (Config::Language == ELanguage::English) + g_achievement.Name = xdbf::FixInvalidSequences(g_achievement.Name); + + Game_PlaySound("obj_navi_appear"); + } + if (!s_isVisible) + { + g_soundAdministratorUpdated = false; return; + } if (ImGui::GetTime() - g_appearTime >= OVERLAY_DURATION) AchievementOverlay::Close(); + // Close function can use this bool so reset it after. + g_soundAdministratorUpdated = false; + auto drawList = ImGui::GetBackgroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; @@ -167,21 +221,7 @@ void AchievementOverlay::Draw() void AchievementOverlay::Open(int id) { - if (s_isVisible && !g_isClosing) - { - s_queue.emplace(id); - return; - } - - s_isVisible = true; - g_isClosing = false; - g_appearTime = ImGui::GetTime(); - g_achievement = g_xdbfWrapper.GetAchievement((EXDBFLanguage)Config::Language.Value, id); - - if (Config::Language == ELanguage::English) - g_achievement.Name = xdbf::FixInvalidSequences(g_achievement.Name); - - Game_PlaySound("obj_navi_appear"); + s_queue.push(id); } void AchievementOverlay::Close() @@ -192,10 +232,6 @@ void AchievementOverlay::Close() g_isClosing = true; } - if (s_queue.size()) - { + if (CanDequeueAchievement()) s_isVisible = false; - AchievementOverlay::Open(s_queue.front()); - s_queue.pop(); - } }