From 9193097422268c4bae29e9af5b0b207545bddf4b Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:07:32 +0100 Subject: [PATCH 1/9] fix: dispatch short press via MenuOpenHandler for any control binding --- README.md | 4 +-- docs/nexus-page.md | 2 +- src/InputHandler.cpp | 59 +++++++++++++++++++++++++------------------- src/InputHandler.h | 4 +-- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 7719b6e..02250cc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # QuickMap -Hold the gamepad **Start button** for a configurable duration to open the **world map**. A short press opens the Journal Menu (or Tween Menu) as normal. +Hold the gamepad **Start button** for a configurable duration to open the **world map**. A short press opens whatever Start is bound to in your controls. -> **Gamepad only.** Vanilla behaviour is fully preserved. +> **Gamepad only.** Default behaviour is fully preserved. Inspired by Red Dead Redemption 2's hold-Start-to-open-map mechanic. diff --git a/docs/nexus-page.md b/docs/nexus-page.md index f152b29..ba6772a 100644 --- a/docs/nexus-page.md +++ b/docs/nexus-page.md @@ -27,7 +27,7 @@ Hold the gamepad Start button to open the world map. Short press opens the Journ [size=4][b]Overview[/b][/size] -Hold the gamepad [b]Start button[/b] for a configurable duration to open the [b]world map[/b] directly. A short press opens the Journal Menu (or Tween Menu) as normal — vanilla behaviour is fully preserved. +Hold the gamepad [b]Start button[/b] for a configurable duration to open the [b]world map[/b] directly. A short press opens whatever Start is bound to in your controls. [color=#ffcc00][b]Gamepad only.[/b][/color] diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 89bee90..ef09153 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -15,27 +15,18 @@ void InputHandler::UpdateShortPressMenu() auto* controlMap = RE::ControlMap::GetSingleton(); if (!controlMap) { logger::error("ControlMap unavailable — short press will have no effect"); - shortPressMenuName.clear(); + shortPressUserEvent = ""; return; } constexpr auto kStart = static_cast(RE::BSWin32GamepadDevice::Key::kStart); - const auto userEvent = controlMap->GetUserEventName(kStart, RE::INPUT_DEVICE::kGamepad); + shortPressUserEvent = controlMap->GetUserEventName(kStart, RE::INPUT_DEVICE::kGamepad); - if (userEvent == "Tween Menu") { - shortPressMenuName = RE::TweenMenu::MENU_NAME; - } else if (userEvent == "Journal") { - shortPressMenuName = RE::JournalMenu::MENU_NAME; + if (shortPressUserEvent.empty()) { + logger::warn("Start has no binding in kGameplay context — short press disabled"); } else { - if (userEvent.empty()) { - logger::warn("Start has no binding in kGameplay context — short press disabled"); - } else { - logger::warn("Unknown user event '{}' for Start — short press disabled", userEvent); - } - shortPressMenuName.clear(); + logger::info("Start short press user event: '{}'", shortPressUserEvent); } - - logger::info("Start short press bound to '{}'", shortPressMenuName.empty() ? "(none)" : shortPressMenuName); } RE::BSEventNotifyControl InputHandler::ProcessEvent( @@ -97,12 +88,12 @@ bool InputHandler::ProcessStartButton(const RE::ButtonEvent* btn) if (btn->IsHeld() && _pressTime) { if (!_mapTriggered && btn->HeldDuration() >= holdDuration) { + _mapTriggered = true; auto* uiQueue = RE::UIMessageQueue::GetSingleton(); if (!uiQueue) { logger::error("UIMessageQueue unavailable — hold threshold reached but map not opened"); } else { uiQueue->AddMessage(RE::MapMenu::MENU_NAME, RE::UI_MESSAGE_TYPE::kShow, nullptr); - _mapTriggered = true; } } return true; @@ -132,22 +123,38 @@ void InputHandler::DispatchShortPress(float held) const return; } - auto* uiQueue = RE::UIMessageQueue::GetSingleton(); - if (!uiQueue) { - logger::error("UIMessageQueue unavailable — Start press consumed but no menu opened"); + if (shortPressUserEvent.empty()) { + logger::warn("Start short press has no binding — press consumed but no menu opened"); return; } - if (shortPressMenuName.empty()) { - logger::warn("Start short press has no known binding — press consumed but no menu opened"); + auto* menuControls = RE::MenuControls::GetSingleton(); + if (!menuControls || !menuControls->menuOpenHandler) { + logger::error("MenuControls or menuOpenHandler unavailable — short press consumed but no menu opened"); return; } - if (shortPressMenuName == RE::JournalMenu::MENU_NAME) { - using func_t = void (*)(bool); - static REL::Relocation openJournal{ RELOCATION_ID(52428, 53327) }; - openJournal(true); - } else { - uiQueue->AddMessage(shortPressMenuName, RE::UI_MESSAGE_TYPE::kShow, nullptr); + constexpr auto kStart = static_cast(RE::BSWin32GamepadDevice::Key::kStart); + auto* syntheticEvent = RE::ButtonEvent::Create( + RE::INPUT_DEVICE::kGamepad, + shortPressUserEvent, + kStart, + 1.0F, // value=1.0 → IsPressed()=true, IsDown()=true + 0.0F // heldDownSecs=0.0 → IsDown()=true + ); + if (!syntheticEvent) { + logger::error("Failed to allocate synthetic ButtonEvent — short press consumed but no menu opened"); + return; } + + if (!menuControls->menuOpenHandler->CanProcess(syntheticEvent)) { + logger::warn("MenuOpenHandler rejected short press — press consumed but no menu opened"); + syntheticEvent->~ButtonEvent(); + RE::free(syntheticEvent); + return; + } + + menuControls->menuOpenHandler->ProcessButton(syntheticEvent); + syntheticEvent->~ButtonEvent(); + RE::free(syntheticEvent); } diff --git a/src/InputHandler.h b/src/InputHandler.h index d6d196a..03a56aa 100644 --- a/src/InputHandler.h +++ b/src/InputHandler.h @@ -17,8 +17,8 @@ class InputHandler : static constexpr float kDefaultHoldDuration{ 0.5F }; - float holdDuration{ kDefaultHoldDuration }; - std::string shortPressMenuName; + float holdDuration{ kDefaultHoldDuration }; + RE::BSFixedString shortPressUserEvent; // Queries ControlMap for the menu bound to Start and caches it in shortPressMenuName. // Call at kInputLoaded, kPostLoadGame, and kNewGame. From 29d712172b033f042ef2876014bca78cd202c130 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:15:29 +0100 Subject: [PATCH 2/9] ci: pin clang-format to version 22 to match local toolchain --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d213a2c..a8cb028 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: with: source: src extensions: cpp,h - clangFormatVersion: 19 + clangFormatVersion: 22 clang-tidy: needs: clang-format From 3685674a39ed172b4e078fad9047f4f496f18384 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:20:02 +0100 Subject: [PATCH 3/9] ci: switch to jidicula/clang-format-action, pin to clang-format 22 --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8cb028..53bc096 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,11 +23,10 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: DoozyX/clang-format-lint-action@v0.20 + - uses: jidicula/clang-format-action@v4.18.0 with: - source: src - extensions: cpp,h - clangFormatVersion: 22 + clang-format-version: '22' + check-path: src clang-tidy: needs: clang-format From 8d31383ea4f1cfc9e8fe784acb4489cbf00d17c4 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:23:18 +0100 Subject: [PATCH 4/9] fix: fix stale comment in InputHandler.h --- src/InputHandler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputHandler.h b/src/InputHandler.h index 03a56aa..de64685 100644 --- a/src/InputHandler.h +++ b/src/InputHandler.h @@ -20,7 +20,7 @@ class InputHandler : float holdDuration{ kDefaultHoldDuration }; RE::BSFixedString shortPressUserEvent; - // Queries ControlMap for the menu bound to Start and caches it in shortPressMenuName. + // Queries ControlMap for the user event bound to Start and caches it in shortPressUserEvent. // Call at kInputLoaded, kPostLoadGame, and kNewGame. void UpdateShortPressMenu(); From c871be0d3e6799bc329f418c09f23de098c2be58 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:27:25 +0100 Subject: [PATCH 5/9] refactor: use unique_ptr with custom deleter for synthetic ButtonEvent --- src/InputHandler.cpp | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index ef09153..441cfa1 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -135,26 +135,29 @@ void InputHandler::DispatchShortPress(float held) const } constexpr auto kStart = static_cast(RE::BSWin32GamepadDevice::Key::kStart); - auto* syntheticEvent = RE::ButtonEvent::Create( - RE::INPUT_DEVICE::kGamepad, - shortPressUserEvent, - kStart, - 1.0F, // value=1.0 → IsPressed()=true, IsDown()=true - 0.0F // heldDownSecs=0.0 → IsDown()=true - ); + auto deleter = [](RE::ButtonEvent* e) { + e->~ButtonEvent(); + RE::free(e); + }; + std::unique_ptr syntheticEvent{ + RE::ButtonEvent::Create( + RE::INPUT_DEVICE::kGamepad, + shortPressUserEvent, + kStart, + 1.0F, // value=1.0 → IsPressed()=true, IsDown()=true + 0.0F // heldDownSecs=0.0 → IsDown()=true + ), + deleter + }; if (!syntheticEvent) { logger::error("Failed to allocate synthetic ButtonEvent — short press consumed but no menu opened"); return; } - if (!menuControls->menuOpenHandler->CanProcess(syntheticEvent)) { + if (!menuControls->menuOpenHandler->CanProcess(syntheticEvent.get())) { logger::warn("MenuOpenHandler rejected short press — press consumed but no menu opened"); - syntheticEvent->~ButtonEvent(); - RE::free(syntheticEvent); return; } - menuControls->menuOpenHandler->ProcessButton(syntheticEvent); - syntheticEvent->~ButtonEvent(); - RE::free(syntheticEvent); + menuControls->menuOpenHandler->ProcessButton(syntheticEvent.get()); } From 6fe64bf1526d0b49ba29e73bbb73856c20a3956a Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:29:15 +0100 Subject: [PATCH 6/9] docs: clarify short press only handles menu bindings --- README.md | 2 +- docs/nexus-page.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 02250cc..f92efb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # QuickMap -Hold the gamepad **Start button** for a configurable duration to open the **world map**. A short press opens whatever Start is bound to in your controls. +Hold the gamepad **Start button** for a configurable duration to open the **world map**. A short press opens whatever menu Start is bound to in your controls. > **Gamepad only.** Default behaviour is fully preserved. diff --git a/docs/nexus-page.md b/docs/nexus-page.md index ba6772a..2fd2f9f 100644 --- a/docs/nexus-page.md +++ b/docs/nexus-page.md @@ -2,7 +2,7 @@ ## Short Description -Hold the gamepad Start button to open the world map. Short press opens the Journal (or whatever Start is bound to) as normal. +Hold the gamepad Start button to open the world map. Short press opens whatever menu Start is bound to in your controls. --- @@ -27,7 +27,7 @@ Hold the gamepad Start button to open the world map. Short press opens the Journ [size=4][b]Overview[/b][/size] -Hold the gamepad [b]Start button[/b] for a configurable duration to open the [b]world map[/b] directly. A short press opens whatever Start is bound to in your controls. +Hold the gamepad [b]Start button[/b] for a configurable duration to open the [b]world map[/b] directly. A short press opens whatever menu Start is bound to in your controls. [color=#ffcc00][b]Gamepad only.[/b][/color] From f6f75bbade29aa81343ddd30bc6765decfbee940 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:37:49 +0100 Subject: [PATCH 7/9] refactor: rename UpdateShortPressMenu to UpdateShortPressUserEvent --- src/InputHandler.cpp | 4 ++-- src/InputHandler.h | 2 +- src/Plugin.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 441cfa1..2c05c65 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -10,7 +10,7 @@ InputHandler* InputHandler::GetSingleton() return &instance; } -void InputHandler::UpdateShortPressMenu() +void InputHandler::UpdateShortPressUserEvent() { auto* controlMap = RE::ControlMap::GetSingleton(); if (!controlMap) { @@ -34,7 +34,7 @@ RE::BSEventNotifyControl InputHandler::ProcessEvent( RE::BSTEventSource* /*a_eventSource*/) { if (a_event && !a_event->opening && a_event->menuName == RE::JournalMenu::MENU_NAME) { - UpdateShortPressMenu(); + UpdateShortPressUserEvent(); } return RE::BSEventNotifyControl::kContinue; } diff --git a/src/InputHandler.h b/src/InputHandler.h index de64685..c27d7eb 100644 --- a/src/InputHandler.h +++ b/src/InputHandler.h @@ -22,7 +22,7 @@ class InputHandler : // Queries ControlMap for the user event bound to Start and caches it in shortPressUserEvent. // Call at kInputLoaded, kPostLoadGame, and kNewGame. - void UpdateShortPressMenu(); + void UpdateShortPressUserEvent(); private: InputHandler() = default; diff --git a/src/Plugin.cpp b/src/Plugin.cpp index 0f40346..732c355 100644 --- a/src/Plugin.cpp +++ b/src/Plugin.cpp @@ -57,7 +57,7 @@ void OnInputLoaded() ui->AddEventSink(handler); } - handler->UpdateShortPressMenu(); + handler->UpdateShortPressUserEvent(); } SKSEPluginLoad(const SKSE::LoadInterface* a_skse) @@ -85,7 +85,7 @@ SKSEPluginLoad(const SKSE::LoadInterface* a_skse) break; case SKSE::MessagingInterface::kPostLoadGame: case SKSE::MessagingInterface::kNewGame: - InputHandler::GetSingleton()->UpdateShortPressMenu(); + InputHandler::GetSingleton()->UpdateShortPressUserEvent(); break; default: break; From 21e3b4939395c8eee3e3bbd954a36232112a19b4 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:39:54 +0100 Subject: [PATCH 8/9] ci: update format.sh to use clang-format-22 --- scripts/format.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/format.sh b/scripts/format.sh index 5bbc6e3..7f7daa5 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -if command -v clang-format-19 &>/dev/null; then - FMT=clang-format-19 +if command -v clang-format-22 &>/dev/null; then + FMT=clang-format-22 else - echo "warning: clang-format-19 not found, using $(clang-format --version); CI uses v19" >&2 + echo "warning: clang-format-22 not found, using $(clang-format --version); CI uses v22" >&2 FMT=clang-format fi From dbd5740924ddb538372373db617981b179c43178 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Sun, 17 May 2026 14:45:30 +0100 Subject: [PATCH 9/9] refactor: clang-tidy warnings --- src/InputHandler.h | 15 ++++++++++----- src/Plugin.cpp | 7 ++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/InputHandler.h b/src/InputHandler.h index c27d7eb..c6210e5 100644 --- a/src/InputHandler.h +++ b/src/InputHandler.h @@ -7,6 +7,11 @@ class InputHandler : public: static InputHandler* GetSingleton(); + InputHandler(const InputHandler&) = delete; + InputHandler(InputHandler&&) = delete; + InputHandler& operator=(const InputHandler&) = delete; + InputHandler& operator=(InputHandler&&) = delete; + RE::BSEventNotifyControl ProcessEvent( RE::InputEvent* const* a_events, RE::BSTEventSource* a_source) override; @@ -17,22 +22,22 @@ class InputHandler : static constexpr float kDefaultHoldDuration{ 0.5F }; - float holdDuration{ kDefaultHoldDuration }; - RE::BSFixedString shortPressUserEvent; + void SetHoldDuration(float a_duration) noexcept { holdDuration = a_duration; } // Queries ControlMap for the user event bound to Start and caches it in shortPressUserEvent. // Call at kInputLoaded, kPostLoadGame, and kNewGame. void UpdateShortPressUserEvent(); + ~InputHandler() override = default; + private: InputHandler() = default; - ~InputHandler() = default; - InputHandler(const InputHandler&) = delete; - InputHandler& operator=(const InputHandler&) = delete; bool ProcessStartButton(const RE::ButtonEvent* btn); void DispatchShortPress(float held) const; + float holdDuration{ kDefaultHoldDuration }; + RE::BSFixedString shortPressUserEvent; std::optional _pressTime; bool _mapTriggered{ false }; }; diff --git a/src/Plugin.cpp b/src/Plugin.cpp index 732c355..d678ba3 100644 --- a/src/Plugin.cpp +++ b/src/Plugin.cpp @@ -37,9 +37,10 @@ float ReadHoldDuration() void OnInputLoaded() { - auto* handler = InputHandler::GetSingleton(); - handler->holdDuration = ReadHoldDuration(); - logger::info("Hold duration: {:.2f}s", handler->holdDuration); + auto* handler = InputHandler::GetSingleton(); + const float holdDuration = ReadHoldDuration(); + handler->SetHoldDuration(holdDuration); + logger::info("Hold duration: {:.2f}s", holdDuration); auto* inputDeviceMgr = RE::BSInputDeviceManager::GetSingleton(); if (!inputDeviceMgr) {