diff --git a/cmake/sourcelist.cmake b/cmake/sourcelist.cmake index bd24f98..a9e0d7a 100644 --- a/cmake/sourcelist.cmake +++ b/cmake/sourcelist.cmake @@ -18,6 +18,7 @@ set(sources ${sources} src/System/Hooks.cpp src/System/Hotkeys.cpp src/System/Input.cpp + src/System/Journal.cpp src/System/Papyrus.cpp src/System/Settings.cpp src/System/Translation.cpp diff --git a/src/FUCK-Host.cpp b/src/FUCK-Host.cpp index 0f63299..b83a0a4 100644 --- a/src/FUCK-Host.cpp +++ b/src/FUCK-Host.cpp @@ -467,7 +467,7 @@ namespace FUCK::Host return static_cast(FUCK::InputDevice::kGamepad); return static_cast(FUCK::InputDevice::kMouseKeyboard); } - static const char* GetKeyName_Impl(std::uint32_t key) { return Input::Manager::GetSingleton()->GetKeyName(key); } + static const char* GetKeyName_Impl(std::uint32_t key) { return Input::Keymap::GetKeyName(key); } static bool IsGamepadKey_Impl(std::uint32_t k) { return k >= Input::Keymap::kGPBase; } static bool IsBinding_Impl() { return Input::Manager::GetSingleton()->IsBinding(); } static void AbortBinding_Impl() { Input::Manager::GetSingleton()->AbortBinding(); } diff --git a/src/FUCK-Man.cpp b/src/FUCK-Man.cpp index 4ab207f..cb7fdb6 100644 --- a/src/FUCK-Man.cpp +++ b/src/FUCK-Man.cpp @@ -753,6 +753,19 @@ void FUCKMan::Toggle() _isOpen ? Close() : Open(); } +EventResult FUCKMan::ProcessEvent(RE::InputEvent* const* a_events, RE::BSTEventSource*) +{ + MANAGER(Input)->ProcessInputEvents(a_events); + + // Do not interfere if the console is open + if (auto ui = RE::UI::GetSingleton(); ui && ui->IsMenuOpen(RE::Console::MENU_NAME)) { + return EventResult::kContinue; + } + + ProcessAsyncInput(a_events); + return EventResult::kContinue; +} + EventResult FUCKMan::ProcessEvent(const RE::MenuOpenCloseEvent* a_event, RE::BSTEventSource*) { if (!a_event) diff --git a/src/FUCK-Man.h b/src/FUCK-Man.h index 6a5fc90..c7bb80d 100644 --- a/src/FUCK-Man.h +++ b/src/FUCK-Man.h @@ -5,7 +5,8 @@ class FUCKMan : public REX::Singleton, - public RE::BSTEventSink + public RE::BSTEventSink, + public RE::BSTEventSink { public: enum class PauseType : int @@ -134,6 +135,7 @@ class FUCKMan : protected: EventResult ProcessEvent(const RE::MenuOpenCloseEvent* a_event, RE::BSTEventSource*) override; + EventResult ProcessEvent(RE::InputEvent* const* a_events, RE::BSTEventSource*) override; private: void UpdateGameState(); diff --git a/src/ImGui/Widgets.cpp b/src/ImGui/Widgets.cpp index 7a4788c..8ba6c82 100644 --- a/src/ImGui/Widgets.cpp +++ b/src/ImGui/Widgets.cpp @@ -1159,7 +1159,7 @@ namespace ImGui if (kIcon) { AddIcon(kIcon); } else { - AddText(Input::Manager::GetSingleton()->GetKeyName(key)); + AddText(Input::Keymap::GetKeyName(key)); } // 2. Mod 2 ( Grows Left ) diff --git a/src/System/Hooks.cpp b/src/System/Hooks.cpp index 044ac12..9978621 100644 --- a/src/System/Hooks.cpp +++ b/src/System/Hooks.cpp @@ -1,434 +1,72 @@ #include "FUCK-Man.h" -#include "ImGui/Audio.h" - #include "Hooks.h" #include "Input.h" +#include "Journal.h" namespace Hooks { - static bool s_fuckButtonInjected = false; - - constexpr const char* kSystemPagePath = "_root.QuestJournalFader.Menu_mc.SystemFader.Page_mc"; - - static void TryInjectFUCKButton(RE::GFxMovieView * a_movieView) + template + struct InputHandler_CanProcess { - if (!a_movieView || s_fuckButtonInjected) - return; - - auto* manager = FUCKMan::GetSingleton(); - - RE::GFxValue page, cat, listObj, entryList; - if (!a_movieView->GetVariable(&page, kSystemPagePath) || !page.IsObject() || - !page.GetMember("CategoryList_mc", &cat) || !cat.IsObject() || - !cat.GetMember("List_mc", &listObj) || !listObj.IsObject() || - !listObj.GetMember("entryList", &entryList) || !entryList.IsArray()) { - return; - } - - const std::uint32_t arraySize = entryList.GetArraySize(); - if (arraySize == 0) - return; - - const char* menuName = ("$FUCK_Title"_T); - - // Determine menu version - bool isSafeMenu = page.HasMember("UpdateIndices"); - manager->SetJournalMenuType(isSafeMenu ? FUCKMan::JournalMenuType::kSafe : FUCKMan::JournalMenuType::kSkyUIv5); - - bool replaced = false; - std::uint32_t quitIdx = arraySize; - - // 1. Check for duplicates, locate $QUIT, and optionally replace $HELP - for (std::uint32_t i = 0; i < arraySize; ++i) { - RE::GFxValue element, textVal; - if (entryList.GetElement(i, &element) && element.IsObject() && element.GetMember("text", &textVal) && textVal.IsString()) { - std::string_view textStr(textVal.GetString()); - - if (textStr == menuName) { - s_fuckButtonInjected = true; - return; - } - - if (textStr == "$QUIT") { - quitIdx = i; - } - - // Only replace $HELP if the user enabled it AND we are on SkyUI v5 or Vanilla - if (!isSafeMenu && manager->GetReplaceHelpMenu() && textStr == "$HELP") { - RE::GFxValue newEntry; - a_movieView->CreateObject(&newEntry); - newEntry.SetMember("text", menuName); - entryList.SetElement(i, newEntry); - replaced = true; - } - } - } - - // 2. If we didn't replace $HELP, inject our new entry - if (!replaced) { - RE::GFxValue newEntry; - a_movieView->CreateObject(&newEntry); - newEntry.SetMember("text", menuName); - - if (isSafeMenu && quitIdx < arraySize) { - // Push the last element back to safely grow the array size by 1 in GFx - RE::GFxValue lastTemp; - entryList.GetElement(arraySize - 1, &lastTemp); - entryList.PushBack(lastTemp); - - // Shift elements down by 1 to make room at quitIdx - for (std::uint32_t i = arraySize - 1; i > quitIdx; --i) { - RE::GFxValue temp; - entryList.GetElement(i - 1, &temp); - entryList.SetElement(i, temp); - } - // Insert our new entry above $QUIT - entryList.SetElement(quitIdx, newEntry); - - // Re-align internal indices for SkyUI v6+ - page.Invoke("UpdateIndices", nullptr, nullptr, 0); - } else { - // Fallback: append to the very bottom for SkyUI v5 so we don't shift hardcoded indices - entryList.PushBack(newEntry); - } - } - - // 3. Invalidate lets us actually select the new entry - listObj.Invoke("InvalidateData", nullptr, nullptr, 0); - - s_fuckButtonInjected = true; - } - - static void TryRemoveFUCKButton(RE::GFxMovieView * a_movieView) - { - if (!a_movieView || !s_fuckButtonInjected) - return; - - auto* manager = FUCKMan::GetSingleton(); - RE::GFxValue page, cat, listObj, entryList; - if (!a_movieView->GetVariable(&page, kSystemPagePath) || !page.IsObject() || - !page.GetMember("CategoryList_mc", &cat) || !cat.IsObject() || - !cat.GetMember("List_mc", &listObj) || !listObj.IsObject() || - !listObj.GetMember("entryList", &entryList) || !entryList.IsArray()) { - return; - } - - const std::uint32_t arraySize = entryList.GetArraySize(); - if (arraySize == 0) - return; - - const char* menuName = ("$FUCK_Title"_T); - bool isSafeMenu = page.HasMember("UpdateIndices"); - bool removed = false; - - for (std::uint32_t i = 0; i < arraySize; ++i) { - RE::GFxValue element, textVal; - if (entryList.GetElement(i, &element) && element.IsObject() && element.GetMember("text", &textVal) && textVal.IsString()) { - if (std::string_view(textVal.GetString()) == menuName) { - if (!isSafeMenu && manager->GetReplaceHelpMenu()) { - element.SetMember("text", "$HELP"); - entryList.SetElement(i, element); - } else { - // Shift up to overwrite the injected entry - for (std::uint32_t j = i; j < arraySize - 1; ++j) { - RE::GFxValue temp; - entryList.GetElement(j + 1, &temp); - entryList.SetElement(j, temp); - } - - entryList.SetArraySize(arraySize - 1); - - if (isSafeMenu) { - page.Invoke("UpdateIndices", nullptr, nullptr, 0); - } - } - removed = true; - break; - } - } - } - - if (removed) { - listObj.Invoke("InvalidateData", nullptr, nullptr, 0); - } - s_fuckButtonInjected = false; - } - - // Returns true if the injected entry is selected and an accept input is detected. - [[nodiscard]] static bool CheckForJournalAccept(RE::InputEvent* const* a_events) - { - if (!FUCKMan::GetSingleton()->GetInjectSystemMenu() || !s_fuckButtonInjected) { - return false; - } - - // Fast path: check input before doing GFx queries - auto* input = MANAGER(Input); - - constexpr std::uint32_t kKeyEnter = Input::Keymap::AsKey(KEY::kEnter); - constexpr std::uint32_t kMouseLeft = Input::Keymap::kMBBase; - constexpr std::uint32_t kGamepadA = Input::Keymap::kGPBase + SKSE::InputMap::kGamepadButtonOffset_A; - constexpr std::uint32_t kGamepadDpadRight = Input::Keymap::kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_RIGHT; - - if (!input->IsInputPressed(a_events, kKeyEnter) && - !input->IsInputPressed(a_events, kMouseLeft) && - !input->IsInputPressed(a_events, kGamepadA) && - !input->IsInputPressed(a_events, kGamepadDpadRight)) { - return false; - } - - // Slow path: validate active selection - auto* journal = RE::UI::GetSingleton()->GetMenu().get(); - if (!journal || !journal->uiMovie) { - return false; - } - - RE::GFxValue page, cat, listObj, selIdx, entryList, selectedEntry, textVal; - - // Fetch the base object - if (!journal->uiMovie->GetVariable(&page, kSystemPagePath) || !page.IsObject()) { - return false; - } - - // Only process if the System menu is actually focused and in the MAIN_STATE (0). - // This prevents wire-crossing when the user clicks inside sub-menus like the Quit confirmation. - RE::GFxValue currentStateVal; - if (page.GetMember("iCurrentState", ¤tStateVal) && currentStateVal.IsNumber()) { - if (currentStateVal.GetNumber() != 0.0) { + static bool thunk(T* a_this, RE::InputEvent* a_event) + { + if (FUCKMan::GetSingleton()->IsInputBlocked()) { return false; } + return func(a_this, a_event); } + static inline REL::Relocation func; - // Null check harder than my ex did - if (!page.GetMember("CategoryList_mc", &cat) || !cat.IsObject() || - !cat.GetMember("List_mc", &listObj) || !listObj.IsObject() || - !listObj.GetMember("selectedIndex", &selIdx) || !selIdx.IsNumber() || - !listObj.GetMember("entryList", &entryList) || !entryList.IsArray()) { - return false; - } - - double rawIdx = selIdx.GetNumber(); - if (rawIdx < 0.0) { - return false; - } - - std::uint32_t index = static_cast(rawIdx); - if (index >= entryList.GetArraySize()) { - return false; - } - - if (!entryList.GetElement(index, &selectedEntry) || !selectedEntry.IsObject() || - !selectedEntry.GetMember("text", &textVal) || !textVal.IsString()) { - return false; - } - - const char* menuName = ("$FUCK_Title"_T); - if (std::string_view(textVal.GetString()) != menuName) { - return false; - } - - // It's go time - ImGui::PlayAudio(ImGui::Audio::kOk); - - // Close the Journal menu natively so the game state clears - if (auto queue = RE::UIMessageQueue::GetSingleton()) { - queue->AddMessage(RE::JournalMenu::MENU_NAME, RE::UI_MESSAGE_TYPE::kHide, nullptr); - } - - // Defer opening our menu to the next frame to prevent input collisions - SKSE::GetTaskInterface()->AddTask([]() { - FUCKMan::GetSingleton()->Open(); - }); - - return true; - } - - // ================================================== - // EVENT HOOKS - // ================================================== - - struct ProcessInputQueue - { - // Tracks the blocking state of the previous frame - static inline bool s_wasBlocking = false; - - static void thunk(RE::BSTEventSource* a_dispatcher, RE::InputEvent* const* a_events) + static void Install(std::uint64_t a_offset = 0x1) { - // Update keystate tracking - MANAGER(Input)->ProcessInputEvents(a_events); - - // Do not interfere if console is open - if (auto ui = RE::UI::GetSingleton(); ui && ui->IsMenuOpen(RE::Console::MENU_NAME)) { - func(a_dispatcher, a_events); - return; - } - - auto* manager = FUCKMan::GetSingleton(); - - // Process menu input (may change the open/blocked state mid-frame) - const bool consumed = manager->ProcessAsyncInput(a_events); - - // State after processing - const bool isBlocking = manager->IsInputBlocked() || manager->IsOpen() || consumed; - - // Detect the exact frame we transition into a blocked state - const bool justBlocked = isBlocking && !s_wasBlocking; - s_wasBlocking = isBlocking; - - // Check if the user just clicked our injected System Menu button - if (CheckForJournalAccept(a_events)) { - // Swallow the input - constexpr RE::InputEvent* const empty_events[] = { nullptr }; - func(a_dispatcher, empty_events); - return; - } - - if (isBlocking) { - auto* userEvents = RE::UserEvents::GetSingleton(); - const bool allowGameMenus = !manager->IsOpen() && manager->HasWindowWithFlag(FUCK::WindowFlags::kCloseOnGameMenu) && !ImGui::GetIO().WantTextInput; - - RE::InputEvent* newHead = nullptr; - RE::InputEvent* newTail = nullptr; - - if (a_events && *a_events) { - for (auto iter = *a_events; iter; iter = iter->next) { - bool keep = false; - - if (auto btn = iter->AsButtonEvent()) { - // Always allow Screenshot and Console through the block - if (userEvents && (btn->userEvent == userEvents->screenshot || btn->userEvent == userEvents->console)) { - keep = true; - } - // Game menu passthrough — only relevant when a kCloseOnGameMenu window is open - else if (allowGameMenus && userEvents) { - if (btn->userEvent == userEvents->tweenMenu || - btn->userEvent == userEvents->journal || - btn->userEvent == userEvents->map || - btn->userEvent == userEvents->quickMap || - btn->userEvent == userEvents->inventory || - btn->userEvent == userEvents->quickInventory || - btn->userEvent == userEvents->quickMagic || - btn->userEvent == userEvents->stats || - btn->userEvent == userEvents->quickStats || - btn->userEvent == userEvents->favorites) { - keep = true; - } - } - - // Zero-out on transition to blocked state to prevent "stuck" inputs - if (!keep && justBlocked) { - btn->value = 0.0f; - btn->heldDownSecs = 0.0f; - keep = true; - } - } else if (auto idEvent = iter->AsIDEvent()) { - if (justBlocked) { - if (auto thumb = idEvent->AsThumbstickEvent()) { - thumb->xValue = 0.0f; - thumb->yValue = 0.0f; - } - keep = true; - } - } - - // Rebuild the linked list with only kept (or zeroed) events - if (keep) { - if (!newHead) { - newHead = iter; - } else { - newTail->next = iter; - } - newTail = iter; - } - } - } - - // Terminate the chain - if (newTail) { - newTail->next = nullptr; - } - - // Temporarily unpause the game if a paused menu opens, to ensure our filtered events are processed and don't get "stuck" - std::uint32_t savedPauses = 0; - bool savedFreeze = false; - auto ui = RE::UI::GetSingleton(); - auto main = RE::Main::GetSingleton(); - - if (justBlocked) { - if (ui && ui->numPausesGame > 0) { - savedPauses = ui->numPausesGame; - ui->numPausesGame = 0; - } - if (main && main->freezeTime) { - savedFreeze = main->freezeTime; - main->freezeTime = false; - } - } - - // Dispatch the filtered/zeroed events - if (newHead) { - RE::InputEvent* const filtered[] = { newHead }; - func(a_dispatcher, filtered); - } else { - constexpr RE::InputEvent* const dummy[] = { nullptr }; - func(a_dispatcher, dummy); - } - - // Restore the pauses immediately after dispatch - if (justBlocked) { - if (main && savedFreeze) { - main->freezeTime = savedFreeze; - } - if (ui && savedPauses > 0) { - ui->numPausesGame += savedPauses; - } - } - - return; - } - - // Not blocking, normal pass-through - func(a_dispatcher, a_events); + REL::Relocation vtbl{ T::VTABLE[0] }; + func = vtbl.write_vfunc(a_offset, &thunk); } - static inline REL::Relocation func; }; - struct JournalMenu_ProcessMessage + struct MenuOpenHandler_CanProcess { - static RE::UI_MESSAGE_RESULTS thunk(RE::JournalMenu* a_this, RE::UIMessage& a_message) + static bool thunk(RE::MenuOpenHandler* a_this, RE::InputEvent* a_event) { - // Reset tracking when the journal menu closes - if (a_message.type == RE::UI_MESSAGE_TYPE::kHide) { - s_fuckButtonInjected = false; - return func(a_this, a_message); - } - - auto result = func(a_this, a_message); - - if (a_message.type == RE::UI_MESSAGE_TYPE::kUpdate && a_this->uiMovie) { - auto* manager = FUCKMan::GetSingleton(); - bool shouldInject = manager->GetInjectSystemMenu() && MANAGER(Input)->IsInputGamepad(); - - if (shouldInject && !s_fuckButtonInjected) { - TryInjectFUCKButton(a_this->uiMovie.get()); - } else if (!shouldInject && s_fuckButtonInjected) { - TryRemoveFUCKButton(a_this->uiMovie.get()); + auto* manager = FUCKMan::GetSingleton(); + if (manager->IsInputBlocked()) { + const bool allowGameMenus = !manager->IsOpen() && manager->HasWindowWithFlag(FUCK::WindowFlags::kCloseOnGameMenu) && !ImGui::GetIO().WantTextInput; + if (!allowGameMenus) { + return false; } } - - return result; + return func(a_this, a_event); } static inline REL::Relocation func; }; void Install() { - REL::Relocation inputUnk(RELOCATION_ID(67315, 68617), 0x7B); - stl::write_thunk_call(inputUnk.address()); - - REL::Relocation journalVtbl(RE::VTABLE_JournalMenu[0]); - JournalMenu_ProcessMessage::func = journalVtbl.write_vfunc(0x4, &JournalMenu_ProcessMessage::thunk); - - logger::info("Installed Input and Journal Menu Hooks"); + // Gameplay input handlers - silenced while FUCK is capturing input + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + InputHandler_CanProcess::Install(); + + InputHandler_CanProcess::Install(0xB); + InputHandler_CanProcess::Install(0x12); + + // Menu-open handler - blocks opening game menus while capturing input + REL::Relocation menuOpenVtbl{ RE::MenuOpenHandler::VTABLE[0] }; + MenuOpenHandler_CanProcess::func = menuOpenVtbl.write_vfunc(0x1, &MenuOpenHandler_CanProcess::thunk); + + Journal::Install(); + + logger::info("Installed Hooks"); } } diff --git a/src/System/Input.cpp b/src/System/Input.cpp index 3db9a74..5e718ad 100644 --- a/src/System/Input.cpp +++ b/src/System/Input.cpp @@ -29,11 +29,6 @@ namespace Input kGPBase + SKSE::InputMap::kGamepadButtonOffset_RT }; - void Manager::Register() - { - logger::info("Input Manager Initialized"); - } - void Manager::ClearState() { std::unique_lock lock(_dataLock); @@ -63,16 +58,7 @@ namespace Input void Manager::ToggleCursor(bool a_enable) { - SKSE::GetTaskInterface()->AddUITask([a_enable]() { - RE::UIMessageQueue::GetSingleton()->AddMessage(RE::CursorMenu::MENU_NAME, - a_enable ? RE::UI_MESSAGE_TYPE::kShow : RE::UI_MESSAGE_TYPE::kHide, - nullptr); - }); - } - - void Manager::ResetCursorState() - { - _cursorInit = std::nullopt; + RE::UIMessageQueue::GetSingleton()->AddMessage(RE::CursorMenu::MENU_NAME, a_enable ? RE::UI_MESSAGE_TYPE::kShow : RE::UI_MESSAGE_TYPE::kHide, nullptr); } bool Manager::IsInputPressed(const RE::InputEvent* const* a_event, std::uint32_t a_unifiedKey) @@ -414,108 +400,6 @@ namespace Input return std::ranges::contains(KB_MODS, a_unifiedKey) || std::ranges::contains(GP_MODS, a_unifiedKey); } - // ================================================== - // Key Names - // ================================================== - - const char* Manager::GetKeyName(std::uint32_t a_key) const - { - // Letters — QWERTY row ranges (DirectInput scancode order, not alphabetical) - if (a_key >= AsKey(KEY::kQ) && a_key <= AsKey(KEY::kP)) { - static const char* row1[] = { "Q","W","E","R","T","Y","U","I","O","P" }; - return row1[a_key - AsKey(KEY::kQ)]; - } - if (a_key >= AsKey(KEY::kA) && a_key <= AsKey(KEY::kL)) { - static const char* row2[] = { "A","S","D","F","G","H","J","K","L" }; - return row2[a_key - AsKey(KEY::kA)]; - } - if (a_key >= AsKey(KEY::kZ) && a_key <= AsKey(KEY::kM)) { - static const char* row3[] = { "Z","X","C","V","B","N","M" }; - return row3[a_key - AsKey(KEY::kZ)]; - } - - // Numbers — scancode order is 1-9 then 0 (0x02-0x0B) - if (a_key >= AsKey(KEY::kNum1) && a_key <= AsKey(KEY::kNum0)) { - static const char* nums[] = { "1","2","3","4","5","6","7","8","9","0" }; - return nums[a_key - AsKey(KEY::kNum1)]; - } - - // F-keys — F11/F12 (0x57-0x58) are not contiguous with F1-F10 (0x3B-0x44) - if (a_key >= AsKey(KEY::kF1) && a_key <= AsKey(KEY::kF10)) { - static const char* fkeys[] = { "F1","F2","F3","F4","F5","F6","F7","F8","F9","F10" }; - return fkeys[a_key - AsKey(KEY::kF1)]; - } - if (a_key >= AsKey(KEY::kF11) && a_key <= AsKey(KEY::kF12)) { - static const char* fkeys[] = { "F11","F12" }; - return fkeys[a_key - AsKey(KEY::kF11)]; - } - - // Mouse - if (a_key >= kMBBase && a_key < kMBBase + 8) { - static const char* mouse[] = { - "Mouse1","Mouse2","Mouse3","Mouse4","Mouse5","Mouse6","Mouse7","Mouse8" - }; - return mouse[a_key - kMBBase]; - } - - // Gamepad - if (a_key >= kGPBase) { - static const Map gp = { - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_A, "A" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_B, "B" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_X, "X" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_Y, "Y" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_LEFT_SHOULDER, "LB" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_RIGHT_SHOULDER, "RB" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_LT, "LT" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_RT, "RT" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_LEFT_THUMB, "LS" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_RIGHT_THUMB, "RS" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_START, "Start" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_BACK, "Back" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_UP, "DUp" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_DOWN, "DDown" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_LEFT, "DLeft" }, - { kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_RIGHT, "DRight" }, - }; - auto it = gp.find(a_key); - return it != gp.end() ? it->second : "GP?"; - } - - // Everything else — irregular keyboard keys + all KP digits (non-contiguous) - static const Map kb = { - { AsKey(KEY::kEscape), "Esc" }, { AsKey(KEY::kEnter), "Enter" }, - { AsKey(KEY::kSpacebar), "Space" }, { AsKey(KEY::kTab), "Tab" }, - { AsKey(KEY::kBackspace), "Backspace" }, { AsKey(KEY::kCapsLock), "Caps" }, - { AsKey(KEY::kLeftShift), "LShift" }, { AsKey(KEY::kRightShift), "RShift" }, - { AsKey(KEY::kLeftControl), "LCtrl" }, { AsKey(KEY::kRightControl), "RCtrl" }, - { AsKey(KEY::kLeftAlt), "LAlt" }, { AsKey(KEY::kRightAlt), "RAlt" }, - { AsKey(KEY::kLeft), "Left" }, { AsKey(KEY::kRight), "Right" }, - { AsKey(KEY::kUp), "Up" }, { AsKey(KEY::kDown), "Down" }, - { AsKey(KEY::kHome), "Home" }, { AsKey(KEY::kEnd), "End" }, - { AsKey(KEY::kPageUp), "PgUp" }, { AsKey(KEY::kPageDown), "PgDn" }, - { AsKey(KEY::kInsert), "Insert" }, { AsKey(KEY::kDelete), "Delete" }, - { AsKey(KEY::kPrintScreen), "PrtSc" }, { AsKey(KEY::kScrollLock), "ScrLk" }, - { AsKey(KEY::kPause), "Pause" }, { AsKey(KEY::kNumLock), "NumLk" }, - { AsKey(KEY::kMinus), "-" }, { AsKey(KEY::kEquals), "=" }, - { AsKey(KEY::kBracketLeft), "[" }, { AsKey(KEY::kBracketRight), "]" }, - { AsKey(KEY::kBackslash), "\\" }, { AsKey(KEY::kSemicolon), ";" }, - { AsKey(KEY::kApostrophe), "'" }, { AsKey(KEY::kComma), "," }, - { AsKey(KEY::kPeriod), "." }, { AsKey(KEY::kSlash), "/" }, - { AsKey(KEY::kTilde), "`" }, - { AsKey(KEY::kKP_0), "KP0" }, { AsKey(KEY::kKP_1), "KP1" }, - { AsKey(KEY::kKP_2), "KP2" }, { AsKey(KEY::kKP_3), "KP3" }, - { AsKey(KEY::kKP_4), "KP4" }, { AsKey(KEY::kKP_5), "KP5" }, - { AsKey(KEY::kKP_6), "KP6" }, { AsKey(KEY::kKP_7), "KP7" }, - { AsKey(KEY::kKP_8), "KP8" }, { AsKey(KEY::kKP_9), "KP9" }, - { AsKey(KEY::kKP_Decimal), "KP." }, { AsKey(KEY::kKP_Divide), "KP/" }, - { AsKey(KEY::kKP_Multiply), "KP*" }, { AsKey(KEY::kKP_Subtract), "KP-" }, - { AsKey(KEY::kKP_Plus), "KP+" }, { AsKey(KEY::kKP_Enter), "KPEnter" }, - }; - auto it = kb.find(a_key); - return it != kb.end() ? it->second : "?"; - } - // ================================================== // Processing // ================================================== @@ -536,12 +420,9 @@ namespace Input meaningful = true; } } else if (event->GetEventType() == RE::INPUT_EVENT_TYPE::kMouseMove) { - // Ignore mouse events entirely if we just issued a synthetic OS cursor snap - if (!_expectingSyntheticMouseMove) { - auto mouse = event->AsMouseMoveEvent(); - if (std::abs(mouse->mouseInputX) > 2.0f || std::abs(mouse->mouseInputY) > 2.0f) { - meaningful = true; - } + auto mouse = event->AsMouseMoveEvent(); + if (std::abs(mouse->mouseInputX) > 2.0f || std::abs(mouse->mouseInputY) > 2.0f) { + meaningful = true; } } @@ -632,213 +513,151 @@ namespace Input return; } - // Cursor Visibility - const auto fuck = FUCKMan::GetSingleton(); - const bool blockInput = fuck->IsInputBlocked(); - const bool forceCursor = fuck->IsCursorForced(); - const bool menuOpen = fuck->IsOpen(); - const bool shouldRender = fuck->ShouldRender(); + UpdateCursorVisibility(); - bool shouldShowCursor = forceCursor || menuOpen; - if (!shouldShowCursor && blockInput) { - if (CanNavigateWithMouse() || IsInputGamepad()) { - shouldShowCursor = true; - } + if (a_events && *a_events) { + ForwardEventsToImGui(a_events); } + } + + void Manager::UpdateCursorVisibility() + { + const auto fuck = FUCKMan::GetSingleton(); + const bool blockInput = fuck->IsInputBlocked(); + const bool forceCursor = fuck->IsCursorForced(); + const bool menuOpen = fuck->IsOpen(); - bool cursorCurrentlyOpen = false; - if (auto ui = RE::UI::GetSingleton()) { - cursorCurrentlyOpen = ui->IsMenuOpen(RE::CursorMenu::MENU_NAME); + bool shouldShowCursor = forceCursor || menuOpen; + if (!shouldShowCursor && blockInput && (CanNavigateWithMouse() || IsInputGamepad())) { + shouldShowCursor = true; } - static double lastCursorToggleTime = 0.0; - double currentTime = ImGui::GetTime(); + auto ui = RE::UI::GetSingleton(); + // We only ever hide a cursor we ourselves showed, so we never steal one another menu owns. + // Re-issuing kShow whenever the cursor is unexpectedly closed makes recovery self-correcting. + const bool cursorCurrentlyOpen = ui && ui->IsMenuOpen(RE::CursorMenu::MENU_NAME); if (shouldShowCursor) { - if (!_cursorInit.has_value() || (*_cursorInit == false && !cursorCurrentlyOpen)) { - if (!cursorCurrentlyOpen) { - ToggleCursor(true); - _cursorInit = true; - lastCursorToggleTime = currentTime; - } else { - _cursorInit = false; - } - } else if (*_cursorInit == true && !cursorCurrentlyOpen) { - // Recover the cursor if it was unexpectedly closed by the game engine - if (currentTime - lastCursorToggleTime > 0.1) { - ToggleCursor(true); - lastCursorToggleTime = currentTime; - } + if (!cursorCurrentlyOpen) { + ToggleCursor(true); + _cursorShownByUs = true; } } else { - if (_cursorInit.has_value()) { - if (*_cursorInit == true) { - ToggleCursor(false); - } - _cursorInit = std::nullopt; + if (_cursorShownByUs) { + ToggleCursor(false); } + _cursorShownByUs = false; } + } - // Hide visual cursor when playing with a gamepad - if (auto ui = RE::UI::GetSingleton()) { - if (auto cursorMenu = ui->GetMenu()) { - if (cursorMenu->uiMovie) { - RE::GFxValue root; - if (cursorMenu->uiMovie->GetVariable(&root, "_root")) { - bool hideVisual = IsInputGamepad() && !menuOpen; - - // Exception: The Map Menu uses the cursor - if (hideVisual && ui->IsMenuOpen(RE::MapMenu::MENU_NAME)) { - hideVisual = false; - } - - root.SetMember("_alpha", RE::GFxValue(hideVisual ? 0.0 : 100.0)); - } - } - } - } + void Manager::ForwardEventsToImGui(RE::InputEvent* const* a_events) + { + const auto fuck = FUCKMan::GetSingleton(); + const bool blockInput = fuck->IsInputBlocked(); + const bool forceCursor = fuck->IsCursorForced(); + const bool shouldRender = fuck->ShouldRender(); - // Event Forwarding - auto& io = ImGui::GetIO(); - const bool cursorMenuOpen = RE::UI::GetSingleton()->IsMenuOpen(RE::CursorMenu::MENU_NAME); + auto& io = ImGui::GetIO(); - const bool isBinding = IsBinding(); - const bool passKeyboardAndGamepad = shouldRender && blockInput && !isBinding; + const bool passKeyboardAndGamepad = shouldRender && blockInput && !IsBinding(); const bool passMouse = shouldRender && (blockInput || forceCursor); - if (a_events && *a_events) { - for (auto event = *a_events; event; event = event->next) { - if (const auto charEvent = event->AsCharEvent()) { - if (passKeyboardAndGamepad) { - io.AddInputCharacter(charEvent->keyCode); + for (auto event = *a_events; event; event = event->next) { + if (const auto charEvent = event->AsCharEvent()) { + if (passKeyboardAndGamepad) { + io.AddInputCharacter(charEvent->keyCode); + } + } else if (const auto buttonEvent = event->AsButtonEvent()) { + const auto key = buttonEvent->GetIDCode(); + const float value = buttonEvent->Value(); + const bool isDown = value > 0.0f; + + std::uint32_t unifiedKey = Keymap::GetUnifiedKey(event->GetDevice(), key); + + switch (event->GetDevice()) { + case RE::INPUT_DEVICE::kKeyboard: + // Always forward KeyUp events (!isDown) to prevent stuck keys in ImGui + if (passKeyboardAndGamepad || !isDown) + io.AddKeyEvent(Keymap::ToImGuiKey(static_cast(key)), isDown); + break; + case RE::INPUT_DEVICE::kMouse: + if (passMouse || !isDown) { + switch (auto mouseKey = static_cast(key)) { + case MOUSE::kWheelUp: + if (passMouse) + io.AddMouseWheelEvent(0, value); + break; + case MOUSE::kWheelDown: + if (passMouse) + io.AddMouseWheelEvent(0, value * -1); + break; + default: + io.AddMouseButtonEvent(key, isDown); + break; + } } - } else if (const auto buttonEvent = event->AsButtonEvent()) { - const auto key = buttonEvent->GetIDCode(); - const float value = buttonEvent->Value(); - const bool isDown = value > 0.0f; - - std::uint32_t unifiedKey = Keymap::GetUnifiedKey(event->GetDevice(), key); - - switch (event->GetDevice()) { - case RE::INPUT_DEVICE::kKeyboard: - // Always forward KeyUp events (!isDown) to prevent stuck keys in ImGui - if (passKeyboardAndGamepad || !isDown) - io.AddKeyEvent(Keymap::ToImGuiKey(static_cast(key)), isDown); - break; - case RE::INPUT_DEVICE::kMouse: - if (passMouse || !isDown) { - switch (auto mouseKey = static_cast(key)) { - case MOUSE::kWheelUp: - if (passMouse) - io.AddMouseWheelEvent(0, value); - break; - case MOUSE::kWheelDown: - if (passMouse) - io.AddMouseWheelEvent(0, value * -1); - break; - default: - io.AddMouseButtonEvent(key, isDown); - break; - } + break; + case RE::INPUT_DEVICE::kGamepad: + if (passKeyboardAndGamepad || !isDown) { + bool isDpad = (unifiedKey == kGP_Up || unifiedKey == kGP_Down || unifiedKey == kGP_Left || unifiedKey == kGP_Right); + if (isDpad && isDown) { + _cursorMovedByJoystick = false; // Revert to standard D-Pad Nav } - break; - case RE::INPUT_DEVICE::kGamepad: - if (passKeyboardAndGamepad || !isDown) { - bool isDpad = (unifiedKey == kGP_Up || unifiedKey == kGP_Down || unifiedKey == kGP_Left || unifiedKey == kGP_Right); - if (isDpad && isDown) { - _cursorMovedByJoystick = false; // Revert to standard D-Pad Nav - } - bool isMouseClickOverride = (unifiedKey == kGP_L3) || (unifiedKey == kGP_R3) || (unifiedKey == kGP_A && _cursorMovedByJoystick); - - if (isMouseClickOverride) { - // Inject mouse click AND suppress native gamepad button - io.AddMouseButtonEvent(0, isDown); + bool isMouseClickOverride = (unifiedKey == kGP_L3) || (unifiedKey == kGP_R3) || (unifiedKey == kGP_A && _cursorMovedByJoystick); + + if (isMouseClickOverride) { + // Inject mouse click AND suppress native gamepad button + io.AddMouseButtonEvent(0, isDown); + } else { + // Pass native gamepad button to ImGui + if (RE::ControlMap::GetSingleton()->GetGamePadType() == RE::PC_GAMEPAD_TYPE::kOrbis) { + auto mapped = Keymap::ToImGuiKey(static_cast(key)); + if (mapped.second) { + if (passKeyboardAndGamepad) + io.AddKeyAnalogEvent(mapped.first, isDown, value); + } else { + io.AddKeyEvent(mapped.first, isDown); + } } else { - // Pass native gamepad button to ImGui - if (RE::ControlMap::GetSingleton()->GetGamePadType() == RE::PC_GAMEPAD_TYPE::kOrbis) { - auto mapped = Keymap::ToImGuiKey(static_cast(key)); - if (mapped.second) { - if (passKeyboardAndGamepad) - io.AddKeyAnalogEvent(mapped.first, isDown, value); - } else { - io.AddKeyEvent(mapped.first, isDown); - } + auto mapped = Keymap::ToImGuiKey(static_cast(key)); + if (mapped.second) { + if (passKeyboardAndGamepad) + io.AddKeyAnalogEvent(mapped.first, isDown, value); } else { - auto mapped = Keymap::ToImGuiKey(static_cast(key)); - if (mapped.second) { - if (passKeyboardAndGamepad) - io.AddKeyAnalogEvent(mapped.first, isDown, value); - } else { - io.AddKeyEvent(mapped.first, isDown); - } + io.AddKeyEvent(mapped.first, isDown); } } } - break; - default: - break; } - } else if (passMouse) { - if (auto mouseEvent = event->AsMouseMoveEvent()) { - // Drop the "Echo" from SetCursorPos - if (_expectingSyntheticMouseMove) { - _expectingSyntheticMouseMove = false; - continue; - } - if (std::abs(mouseEvent->mouseInputX) > 2.0f || std::abs(mouseEvent->mouseInputY) > 2.0f) { - _cursorMovedByJoystick = false; - } - if (cursorMenuOpen) { - if (auto cursorMenu = RE::UI::GetSingleton()->GetMenu()) { - cursorMenu->ProcessMouseMove(mouseEvent); - } - } - } else if (const auto thumbstickEvent = event->AsThumbstickEvent()) { - if (std::abs(thumbstickEvent->xValue) > 0.05f || std::abs(thumbstickEvent->yValue) > 0.05f) { - _cursorMovedByJoystick = true; - } - if (cursorMenuOpen && (fuck->IsOpen() || forceCursor)) { - if (auto cursorMenu = RE::UI::GetSingleton()->GetMenu()) { - cursorMenu->ProcessThumbstick(thumbstickEvent); - } - } + break; + default: + break; + } + } else if (passMouse) { + // The game's CursorMenu now drives cursor movement natively; we only + // observe which device is steering it so ImGui nav stays in sync. + if (auto mouseEvent = event->AsMouseMoveEvent()) { + if (std::abs(mouseEvent->mouseInputX) > 2.0f || std::abs(mouseEvent->mouseInputY) > 2.0f) { + _cursorMovedByJoystick = false; + } + } else if (const auto thumbstickEvent = event->AsThumbstickEvent()) { + if (std::abs(thumbstickEvent->xValue) > 0.05f || std::abs(thumbstickEvent->yValue) > 0.05f) { + _cursorMovedByJoystick = true; } } } + } - // Sync modifiers to ImGui - if (passKeyboardAndGamepad) { - io.AddKeyEvent(ImGuiMod_Ctrl, IsModifierPressed(FUCK::Modifier::kCtrl)); - io.AddKeyEvent(ImGuiMod_Shift, IsModifierPressed(FUCK::Modifier::kShift)); - io.AddKeyEvent(ImGuiMod_Alt, IsModifierPressed(FUCK::Modifier::kAlt)); - - bool superDown = IsInputDown(AsKey(KEY::kLeftWin)) || - IsInputDown(AsKey(KEY::kRightWin)); - io.AddKeyEvent(ImGuiMod_Super, superDown); - } - - // Sync OS hardware cursor to the virtual joystick cursor - if (_cursorMovedByJoystick && cursorMenuOpen && passMouse) { - if (auto mc = RE::MenuCursor::GetSingleton()) { - ImVec2 clientPos = ImGui::TranslateScaleformToScreen(mc->cursorPosX, mc->cursorPosY); - - static float s_lastClientX = -1.0f; - static float s_lastClientY = -1.0f; - - if (clientPos.x != s_lastClientX || clientPos.y != s_lastClientY) { - s_lastClientX = clientPos.x; - s_lastClientY = clientPos.y; + // Sync modifiers to ImGui + if (passKeyboardAndGamepad) { + io.AddKeyEvent(ImGuiMod_Ctrl, IsModifierPressed(FUCK::Modifier::kCtrl)); + io.AddKeyEvent(ImGuiMod_Shift, IsModifierPressed(FUCK::Modifier::kShift)); + io.AddKeyEvent(ImGuiMod_Alt, IsModifierPressed(FUCK::Modifier::kAlt)); - if (HWND hwnd = ::GetActiveWindow()) { - POINT pt = { static_cast(clientPos.x), static_cast(clientPos.y) }; - ::ClientToScreen(hwnd, &pt); - ::SetCursorPos(pt.x, pt.y); - _expectingSyntheticMouseMove = true; - } - } - } - } + bool superDown = IsInputDown(AsKey(KEY::kLeftWin)) || + IsInputDown(AsKey(KEY::kRightWin)); + io.AddKeyEvent(ImGuiMod_Super, superDown); } } } diff --git a/src/System/Input.h b/src/System/Input.h index c51c535..adb758e 100644 --- a/src/System/Input.h +++ b/src/System/Input.h @@ -15,13 +15,6 @@ namespace Input kGamepadOrbis }; - struct Context - { - std::string name; - int priority{ 0 }; - bool blocksLower{ true }; - }; - struct RebindContext { bool active{ false }; @@ -49,7 +42,6 @@ namespace Input class Manager : public REX::Singleton { public: - static void Register(); void ClearState(); @@ -59,7 +51,6 @@ namespace Input bool CanNavigateWithMouse() const; static void ToggleCursor(bool a_enable); - void ResetCursorState(); void ProcessInputEvents(RE::InputEvent* const* a_events); @@ -82,11 +73,11 @@ namespace Input static bool IsUnifiedModifier(std::uint32_t a_unifiedKey); bool IsCursorMovedByJoystick() const { return _cursorMovedByJoystick; } - const char* GetKeyName(std::uint32_t a_key) const; - private: void UpdateInputDevice(RE::INPUT_DEVICE a_device); void CacheInputState(const RE::InputEvent* const* a_events); + void UpdateCursorVisibility(); + void ForwardEventsToImGui(RE::InputEvent* const* a_events); bool CheckModifiersStrict(const std::uint32_t* mods, size_t count, std::int32_t req1, std::int32_t req2, std::uint32_t primaryKey) const; @@ -95,13 +86,10 @@ namespace Input RebindContext _rebindCtx; - std::optional _cursorInit{ std::nullopt }; - bool _cursorMovedByJoystick{ false }; - bool _expectingSyntheticMouseMove{ false }; + bool _cursorShownByUs{ false }; + bool _cursorMovedByJoystick{ false }; mutable std::shared_mutex _dataLock; Map _keyStateCache; - - std::vector _contexts; }; } diff --git a/src/System/InputMap.h b/src/System/InputMap.h index 1935555..d572ef4 100644 --- a/src/System/InputMap.h +++ b/src/System/InputMap.h @@ -338,4 +338,102 @@ namespace Input::Keymap return SKSE::InputMap::GamepadMaskToKeycode(a_key) + kGPBase; return a_key; } + + inline const char* GetKeyName(std::uint32_t a_key) + { + // Letters — QWERTY row ranges (DirectInput scancode order, not alphabetical) + if (a_key >= AsKey(KEY::kQ) && a_key <= AsKey(KEY::kP)) { + static const char* row1[] = { "Q","W","E","R","T","Y","U","I","O","P" }; + return row1[a_key - AsKey(KEY::kQ)]; + } + if (a_key >= AsKey(KEY::kA) && a_key <= AsKey(KEY::kL)) { + static const char* row2[] = { "A","S","D","F","G","H","J","K","L" }; + return row2[a_key - AsKey(KEY::kA)]; + } + if (a_key >= AsKey(KEY::kZ) && a_key <= AsKey(KEY::kM)) { + static const char* row3[] = { "Z","X","C","V","B","N","M" }; + return row3[a_key - AsKey(KEY::kZ)]; + } + + // Numbers — scancode order is 1-9 then 0 (0x02-0x0B) + if (a_key >= AsKey(KEY::kNum1) && a_key <= AsKey(KEY::kNum0)) { + static const char* nums[] = { "1","2","3","4","5","6","7","8","9","0" }; + return nums[a_key - AsKey(KEY::kNum1)]; + } + + // F-keys — F11/F12 (0x57-0x58) are not contiguous with F1-F10 (0x3B-0x44) + if (a_key >= AsKey(KEY::kF1) && a_key <= AsKey(KEY::kF10)) { + static const char* fkeys[] = { "F1","F2","F3","F4","F5","F6","F7","F8","F9","F10" }; + return fkeys[a_key - AsKey(KEY::kF1)]; + } + if (a_key >= AsKey(KEY::kF11) && a_key <= AsKey(KEY::kF12)) { + static const char* fkeys[] = { "F11","F12" }; + return fkeys[a_key - AsKey(KEY::kF11)]; + } + + // Mouse + if (a_key >= kMBBase && a_key < kMBBase + 8) { + static const char* mouse[] = { + "Mouse1","Mouse2","Mouse3","Mouse4","Mouse5","Mouse6","Mouse7","Mouse8" + }; + return mouse[a_key - kMBBase]; + } + + // Gamepad + if (a_key >= kGPBase) { + static const Map gp = { + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_A, "A" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_B, "B" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_X, "X" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_Y, "Y" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_LEFT_SHOULDER, "LB" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_RIGHT_SHOULDER, "RB" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_LT, "LT" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_RT, "RT" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_LEFT_THUMB, "LS" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_RIGHT_THUMB, "RS" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_START, "Start" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_BACK, "Back" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_UP, "DUp" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_DOWN, "DDown" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_LEFT, "DLeft" }, + { kGPBase + SKSE::InputMap::kGamepadButtonOffset_DPAD_RIGHT, "DRight" }, + }; + auto it = gp.find(a_key); + return it != gp.end() ? it->second : "GP?"; + } + + // Everything else — irregular keyboard keys + all KP digits (non-contiguous) + static const Map kb = { + { AsKey(KEY::kEscape), "Esc" }, { AsKey(KEY::kEnter), "Enter" }, + { AsKey(KEY::kSpacebar), "Space" }, { AsKey(KEY::kTab), "Tab" }, + { AsKey(KEY::kBackspace), "Backspace" }, { AsKey(KEY::kCapsLock), "Caps" }, + { AsKey(KEY::kLeftShift), "LShift" }, { AsKey(KEY::kRightShift), "RShift" }, + { AsKey(KEY::kLeftControl), "LCtrl" }, { AsKey(KEY::kRightControl), "RCtrl" }, + { AsKey(KEY::kLeftAlt), "LAlt" }, { AsKey(KEY::kRightAlt), "RAlt" }, + { AsKey(KEY::kLeft), "Left" }, { AsKey(KEY::kRight), "Right" }, + { AsKey(KEY::kUp), "Up" }, { AsKey(KEY::kDown), "Down" }, + { AsKey(KEY::kHome), "Home" }, { AsKey(KEY::kEnd), "End" }, + { AsKey(KEY::kPageUp), "PgUp" }, { AsKey(KEY::kPageDown), "PgDn" }, + { AsKey(KEY::kInsert), "Insert" }, { AsKey(KEY::kDelete), "Delete" }, + { AsKey(KEY::kPrintScreen), "PrtSc" }, { AsKey(KEY::kScrollLock), "ScrLk" }, + { AsKey(KEY::kPause), "Pause" }, { AsKey(KEY::kNumLock), "NumLk" }, + { AsKey(KEY::kMinus), "-" }, { AsKey(KEY::kEquals), "=" }, + { AsKey(KEY::kBracketLeft), "[" }, { AsKey(KEY::kBracketRight), "]" }, + { AsKey(KEY::kBackslash), "\\" }, { AsKey(KEY::kSemicolon), ";" }, + { AsKey(KEY::kApostrophe), "'" }, { AsKey(KEY::kComma), "," }, + { AsKey(KEY::kPeriod), "." }, { AsKey(KEY::kSlash), "/" }, + { AsKey(KEY::kTilde), "`" }, + { AsKey(KEY::kKP_0), "KP0" }, { AsKey(KEY::kKP_1), "KP1" }, + { AsKey(KEY::kKP_2), "KP2" }, { AsKey(KEY::kKP_3), "KP3" }, + { AsKey(KEY::kKP_4), "KP4" }, { AsKey(KEY::kKP_5), "KP5" }, + { AsKey(KEY::kKP_6), "KP6" }, { AsKey(KEY::kKP_7), "KP7" }, + { AsKey(KEY::kKP_8), "KP8" }, { AsKey(KEY::kKP_9), "KP9" }, + { AsKey(KEY::kKP_Decimal), "KP." }, { AsKey(KEY::kKP_Divide), "KP/" }, + { AsKey(KEY::kKP_Multiply), "KP*" }, { AsKey(KEY::kKP_Subtract), "KP-" }, + { AsKey(KEY::kKP_Plus), "KP+" }, { AsKey(KEY::kKP_Enter), "KPEnter" }, + }; + auto it = kb.find(a_key); + return it != kb.end() ? it->second : "?"; + } } diff --git a/src/System/Journal.cpp b/src/System/Journal.cpp new file mode 100644 index 0000000..1a641a0 --- /dev/null +++ b/src/System/Journal.cpp @@ -0,0 +1,282 @@ +#include "FUCK-Man.h" + +#include "ImGui/Audio.h" + +#include "Journal.h" +#include "Input.h" + +namespace Journal +{ + static bool s_fuckButtonInjected = false; + + constexpr const char* kSystemPagePath = "_root.QuestJournalFader.Menu_mc.SystemFader.Page_mc"; + + static void TryInjectFUCKButton(RE::GFxMovieView * a_movieView) + { + if (!a_movieView || s_fuckButtonInjected) + return; + + auto* manager = FUCKMan::GetSingleton(); + + RE::GFxValue page, cat, listObj, entryList; + if (!a_movieView->GetVariable(&page, kSystemPagePath) || !page.IsObject() || + !page.GetMember("CategoryList_mc", &cat) || !cat.IsObject() || + !cat.GetMember("List_mc", &listObj) || !listObj.IsObject() || + !listObj.GetMember("entryList", &entryList) || !entryList.IsArray()) { + return; + } + + const std::uint32_t arraySize = entryList.GetArraySize(); + if (arraySize == 0) + return; + + const char* menuName = ("$FUCK_Title"_T); + + // Determine menu version + bool isSafeMenu = page.HasMember("UpdateIndices"); + manager->SetJournalMenuType(isSafeMenu ? FUCKMan::JournalMenuType::kSafe : FUCKMan::JournalMenuType::kSkyUIv5); + + bool replaced = false; + std::uint32_t quitIdx = arraySize; + + // 1. Check for duplicates, locate $QUIT, and optionally replace $HELP + for (std::uint32_t i = 0; i < arraySize; ++i) { + RE::GFxValue element, textVal; + if (entryList.GetElement(i, &element) && element.IsObject() && element.GetMember("text", &textVal) && textVal.IsString()) { + std::string_view textStr(textVal.GetString()); + + if (textStr == menuName) { + s_fuckButtonInjected = true; + return; + } + + if (textStr == "$QUIT") { + quitIdx = i; + } + + // Only replace $HELP if the user enabled it AND we are on SkyUI v5 or Vanilla + if (!isSafeMenu && manager->GetReplaceHelpMenu() && textStr == "$HELP") { + RE::GFxValue newEntry; + a_movieView->CreateObject(&newEntry); + newEntry.SetMember("text", menuName); + entryList.SetElement(i, newEntry); + replaced = true; + } + } + } + + // 2. If we didn't replace $HELP, inject our new entry + if (!replaced) { + RE::GFxValue newEntry; + a_movieView->CreateObject(&newEntry); + newEntry.SetMember("text", menuName); + + if (isSafeMenu && quitIdx < arraySize) { + // Push the last element back to safely grow the array size by 1 in GFx + RE::GFxValue lastTemp; + entryList.GetElement(arraySize - 1, &lastTemp); + entryList.PushBack(lastTemp); + + // Shift elements down by 1 to make room at quitIdx + for (std::uint32_t i = arraySize - 1; i > quitIdx; --i) { + RE::GFxValue temp; + entryList.GetElement(i - 1, &temp); + entryList.SetElement(i, temp); + } + // Insert our new entry above $QUIT + entryList.SetElement(quitIdx, newEntry); + + // Re-align internal indices for SkyUI v6+ + page.Invoke("UpdateIndices", nullptr, nullptr, 0); + } else { + // Fallback: append to the very bottom for SkyUI v5 so we don't shift hardcoded indices + entryList.PushBack(newEntry); + } + } + + // 3. Invalidate lets us actually select the new entry + listObj.Invoke("InvalidateData", nullptr, nullptr, 0); + + s_fuckButtonInjected = true; + } + + static void TryRemoveFUCKButton(RE::GFxMovieView * a_movieView) + { + if (!a_movieView || !s_fuckButtonInjected) + return; + + auto* manager = FUCKMan::GetSingleton(); + RE::GFxValue page, cat, listObj, entryList; + if (!a_movieView->GetVariable(&page, kSystemPagePath) || !page.IsObject() || + !page.GetMember("CategoryList_mc", &cat) || !cat.IsObject() || + !cat.GetMember("List_mc", &listObj) || !listObj.IsObject() || + !listObj.GetMember("entryList", &entryList) || !entryList.IsArray()) { + return; + } + + const std::uint32_t arraySize = entryList.GetArraySize(); + if (arraySize == 0) + return; + + const char* menuName = ("$FUCK_Title"_T); + bool isSafeMenu = page.HasMember("UpdateIndices"); + bool removed = false; + + for (std::uint32_t i = 0; i < arraySize; ++i) { + RE::GFxValue element, textVal; + if (entryList.GetElement(i, &element) && element.IsObject() && element.GetMember("text", &textVal) && textVal.IsString()) { + if (std::string_view(textVal.GetString()) == menuName) { + if (!isSafeMenu && manager->GetReplaceHelpMenu()) { + element.SetMember("text", "$HELP"); + entryList.SetElement(i, element); + } else { + // Shift up to overwrite the injected entry + for (std::uint32_t j = i; j < arraySize - 1; ++j) { + RE::GFxValue temp; + entryList.GetElement(j + 1, &temp); + entryList.SetElement(j, temp); + } + + entryList.SetArraySize(arraySize - 1); + + if (isSafeMenu) { + page.Invoke("UpdateIndices", nullptr, nullptr, 0); + } + } + removed = true; + break; + } + } + } + + if (removed) { + listObj.Invoke("InvalidateData", nullptr, nullptr, 0); + } + s_fuckButtonInjected = false; + } + + // Returns true if our injected entry is the currently highlighted System Menu item. + static bool IsFUCKEntrySelected(RE::GFxMovieView * a_movieView) + { + if (!a_movieView) { + return false; + } + + RE::GFxValue page, cat, listObj, selIdx, entryList, selectedEntry, textVal; + + // Fetch the base object + if (!a_movieView->GetVariable(&page, kSystemPagePath) || !page.IsObject()) { + return false; + } + + // Only process if the System menu is actually focused and in the MAIN_STATE (0). + // This prevents wire-crossing when the user clicks inside sub-menus like the Quit confirmation. + RE::GFxValue currentStateVal; + if (page.GetMember("iCurrentState", ¤tStateVal) && currentStateVal.IsNumber()) { + if (currentStateVal.GetNumber() != 0.0) { + return false; + } + } + + // Null check harder than my ex did + if (!page.GetMember("CategoryList_mc", &cat) || !cat.IsObject() || + !cat.GetMember("List_mc", &listObj) || !listObj.IsObject() || + !listObj.GetMember("selectedIndex", &selIdx) || !selIdx.IsNumber() || + !listObj.GetMember("entryList", &entryList) || !entryList.IsArray()) { + return false; + } + + double rawIdx = selIdx.GetNumber(); + if (rawIdx < 0.0) { + return false; + } + + std::uint32_t index = static_cast(rawIdx); + if (index >= entryList.GetArraySize()) { + return false; + } + + if (!entryList.GetElement(index, &selectedEntry) || !selectedEntry.IsObject() || + !selectedEntry.GetMember("text", &textVal) || !textVal.IsString()) { + return false; + } + + const char* menuName = ("$FUCK_Title"_T); + return std::string_view(textVal.GetString()) == menuName; + } + + // Closes the Journal and opens FUCK on the next frame. + static void ActivateFUCK() + { + // It's go time + ImGui::PlayAudio(ImGui::Audio::kOk); + + // Close the Journal menu natively so the game state clears + if (auto queue = RE::UIMessageQueue::GetSingleton()) { + queue->AddMessage(RE::JournalMenu::MENU_NAME, RE::UI_MESSAGE_TYPE::kHide, nullptr); + } + + // Defer opening our menu to the next frame to prevent input collisions + SKSE::GetTaskInterface()->AddTask([]() { + FUCKMan::GetSingleton()->Open(); + }); + } + + struct JournalMenu_ProcessMessage + { + static RE::UI_MESSAGE_RESULTS thunk(RE::JournalMenu* a_this, RE::UIMessage& a_message) + { + // Reset tracking when the journal menu closes + if (a_message.type == RE::UI_MESSAGE_TYPE::kHide) { + s_fuckButtonInjected = false; + return func(a_this, a_message); + } + + // Activate FUCK when our injected entry is highlighted and accepted. + // "Accept" (Enter/A/click) arrives as a user event, while the D-pad "Right" is swallowed by DirectionHandler and forwarded as a Scaleform key event. + if (s_fuckButtonInjected && a_message.data && a_this->uiMovie) { + bool activate = false; + + if (a_message.type == RE::UI_MESSAGE_TYPE::kUserEvent) { + auto* data = static_cast(a_message.data); + auto* userEvents = RE::UserEvents::GetSingleton(); + activate = userEvents && data->fixedStr == userEvents->accept; + } else if (a_message.type == RE::UI_MESSAGE_TYPE::kScaleformEvent) { + auto* event = static_cast(a_message.data)->scaleformEvent; + if (event && event->type == RE::GFxEvent::EventType::kKeyDown) { + activate = static_cast(event)->keyCode == RE::GFxKey::kRight; + } + } + + if (activate && IsFUCKEntrySelected(a_this->uiMovie.get())) { + ActivateFUCK(); + return RE::UI_MESSAGE_RESULTS::kHandled; + } + } + + auto result = func(a_this, a_message); + + if (a_message.type == RE::UI_MESSAGE_TYPE::kUpdate && a_this->uiMovie) { + auto* manager = FUCKMan::GetSingleton(); + bool shouldInject = manager->GetInjectSystemMenu() && MANAGER(Input)->IsInputGamepad(); + + if (shouldInject && !s_fuckButtonInjected) { + TryInjectFUCKButton(a_this->uiMovie.get()); + } else if (!shouldInject && s_fuckButtonInjected) { + TryRemoveFUCKButton(a_this->uiMovie.get()); + } + } + + return result; + } + static inline REL::Relocation func; + }; + + void Install() + { + REL::Relocation journalVtbl(RE::VTABLE_JournalMenu[0]); + JournalMenu_ProcessMessage::func = journalVtbl.write_vfunc(0x4, &JournalMenu_ProcessMessage::thunk); + + logger::info("Installed Journal Menu Hooks"); + } +} diff --git a/src/System/Journal.h b/src/System/Journal.h new file mode 100644 index 0000000..e84f7d4 --- /dev/null +++ b/src/System/Journal.h @@ -0,0 +1,6 @@ +#pragma once + +namespace Journal +{ + void Install(); +} diff --git a/src/main.cpp b/src/main.cpp index 53f4d03..6a3c361 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,6 +34,10 @@ void OnInit(SKSE::MessagingInterface::Message* a_msg) ui->AddEventSink(FUCKMan::GetSingleton()); } + if (auto idm = RE::BSInputDeviceManager::GetSingleton()) { + idm->AddEventSink(FUCKMan::GetSingleton()); + } + Translation::Manager::GetSingleton()->BuildTranslationMap(); Console::Install(); Compat::ImmersiveHUD::Initialize();