From 07787e82add47727b06477d2de21c176e70441ea Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Thu, 30 Jul 2026 02:35:00 +0200 Subject: [PATCH] An experimental fix for unresponsive dialogs --- source/Enderal DLL/src/EventListener.cpp | 2 + .../src/Patches/DialogueMenuPatch.h | 81 +++++++++++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/source/Enderal DLL/src/EventListener.cpp b/source/Enderal DLL/src/EventListener.cpp index 3f32d5a3d..cdb9f5756 100644 --- a/source/Enderal DLL/src/EventListener.cpp +++ b/source/Enderal DLL/src/EventListener.cpp @@ -48,6 +48,7 @@ auto EventListener::ProcessEvent( if (a_event->menuName == RE::DialogueMenu::MENU_NAME) { // Make sure Tab is not blocked. If it should be, a Papyrus script will block it a few frames later. DialogueMenuPatch::BlockTab(false); + DialogueMenuPatch::ResetProgressFailsafe(); } else if (a_event->menuName == "CustomMenu") { auto uiMovie = RE::UI::GetSingleton()->GetMovieView("CustomMenu"); if (uiMovie && std::strstr(uiMovie->GetMovieDef()->GetFileURL(), "_heromenu.swf") != NULL) { @@ -60,6 +61,7 @@ auto EventListener::ProcessEvent( } else { if (a_event->menuName == RE::DialogueMenu::MENU_NAME) { DialogueMenuPatch::BlockTab(false); + DialogueMenuPatch::ResetProgressFailsafe(); } } diff --git a/source/Enderal DLL/src/Patches/DialogueMenuPatch.h b/source/Enderal DLL/src/Patches/DialogueMenuPatch.h index 1be2686ab..ffb9a6fe0 100644 --- a/source/Enderal DLL/src/Patches/DialogueMenuPatch.h +++ b/source/Enderal DLL/src/Patches/DialogueMenuPatch.h @@ -1,5 +1,7 @@ #pragma once +#include + inline bool bTabBlocked = false; class DialogueMenuPatch final : public RE::DialogueMenu @@ -16,15 +18,80 @@ public: bTabBlocked = a_block; } + static void ResetProgressFailsafe() + { + progressBlockedSince = {}; + } + private: + // dialoguemenu.swf gates every forward interaction - clicking a topic, Activate, and skipping + // a line - on DialogueMenu_mc.bAllowProgress. That flag is cleared after each of them, and is + // set back to true only by a setInterval callback firing ALLOW_PROGRESS_DELAY (750 ms) after + // the engine delivers the NotifyVoiceReady message. If that message never arrives, or the + // interval never fires, the topic list stays on screen but ignores every click and keypress + // for the rest of the session. Tab and Esc keep working, because onCancelPress() -> + // StartHideMenu() is not gated - which is exactly how this reaches us in bug reports: + // "clicking a dialogue option does nothing, but I can still leave with Tab". + static constexpr auto ALLOW_PROGRESS_VAR = "DialogueMenu_mc.bAllowProgress"; + static constexpr auto MENU_STATE_VAR = "DialogueMenu_mc.eMenuState"; + static constexpr auto TOPIC_LIST_SHOWN = 1; // DialogueMenu.TOPIC_LIST_SHOWN + static constexpr auto PROGRESS_TIMEOUT = std::chrono::milliseconds(2000); + + // Re-enables input when the topic list has sat unresponsive for longer than the SWF's own + // debounce can account for. + void UnstickProgress() + { + auto* mtm = RE::MenuTopicManager::GetSingleton(); + + // currentTopicInfo is only set while the speaker is saying a line. Never touch the + // debounce mid-response, only once the game is idle and waiting for a selection. + if (!uiMovie || !mtm || mtm->currentTopicInfo) { + ResetProgressFailsafe(); + return; + } + + // Only the topic list waits for input. Greeting, transition and post-click states are + // expected to be unresponsive. + RE::GFxValue menuState; + if (!uiMovie->GetVariable(&menuState, MENU_STATE_VAR) || !menuState.IsNumber() || static_cast(menuState.GetNumber()) != TOPIC_LIST_SHOWN) { + ResetProgressFailsafe(); + return; + } + + RE::GFxValue allowProgress; + if (!uiMovie->GetVariable(&allowProgress, ALLOW_PROGRESS_VAR) || !allowProgress.IsBool()) { + return; + } + + if (allowProgress.GetBool()) { + ResetProgressFailsafe(); + return; + } + + const auto now = std::chrono::steady_clock::now(); + + if (progressBlockedSince == std::chrono::steady_clock::time_point{}) { + progressBlockedSince = now; + } else if (now - progressBlockedSince >= PROGRESS_TIMEOUT) { + ResetProgressFailsafe(); + allowProgress.SetBoolean(true); + uiMovie->SetVariable(ALLOW_PROGRESS_VAR, allowProgress); + logger::warn("Dialogue menu stopped accepting input, re-enabled bAllowProgress"); + } + } + RE::UI_MESSAGE_RESULTS ProcessMessageEx(RE::UIMessage& a_message) { - // Unblock Tab if Goodbye response has finished playing (End script has already run at this point). - // A failsafe when Goodbye does not close the dialogue menu. - if (bTabBlocked && a_message.type == RE::UI_MESSAGE_TYPE::kUpdate) { - auto* mtm = RE::MenuTopicManager::GetSingleton(); - if (mtm && mtm->isSayingGoodbye && !mtm->currentTopicInfo && RE::UI::GetSingleton()->IsMenuOpen(MENU_NAME)) { - BlockTab(false); + if (a_message.type == RE::UI_MESSAGE_TYPE::kUpdate) { + UnstickProgress(); + + // Unblock Tab if Goodbye response has finished playing (End script has already run at this point). + // A failsafe when Goodbye does not close the dialogue menu. + if (bTabBlocked) { + auto* mtm = RE::MenuTopicManager::GetSingleton(); + if (mtm && mtm->isSayingGoodbye && !mtm->currentTopicInfo && RE::UI::GetSingleton()->IsMenuOpen(MENU_NAME)) { + BlockTab(false); + } } } @@ -59,6 +126,8 @@ private: func(a_bool); } + inline static std::chrono::steady_clock::time_point progressBlockedSince{}; + using ProcessMessageFn = decltype(&ProcessMessage); inline static REL::Relocation _ProcessMessageFn; };