Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c2b296b
feat(action): add MCM action
codepuncher Jun 14, 2026
f042f36
chore(navigator): add missing standard library includes
codepuncher Jun 15, 2026
82218f5
fix(navigator): add null guards for GetTaskInterface and missing ccty…
codepuncher Jun 15, 2026
fcd3322
fix(navigator): throttle EnsureCachePopulated to schedule at most onc…
codepuncher Jun 15, 2026
c0aad62
fix(navigator): replace detached threads in DelayCallForUI with frame…
codepuncher Jun 15, 2026
9a00cf9
fix(config): normalize case-insensitive none to canonical None in Get…
codepuncher Jun 15, 2026
d814d3e
fix(navigator): use case-insensitive comparison for mod name matching
codepuncher Jun 15, 2026
5bcade6
chore(navigator): update EnsureCachePopulated comment to reflect one-…
codepuncher Jun 15, 2026
882c01e
fix(navigator): guard TryCacheFromOpenMCM against unavailable task in…
codepuncher Jun 15, 2026
3d08e6e
fix(navigator): release g_lock if task interface unavailable before f…
codepuncher Jun 15, 2026
b63326c
fix(ui): defer GetCachedModNames until combo is open to avoid per-fra…
codepuncher Jun 15, 2026
6ce65f2
fix(navigator): use listPath instead of varName in SelectEntryByName …
codepuncher Jun 15, 2026
277feb4
fix(handler): gate MCM quickexit on overlay closed not just mod page …
codepuncher Jun 15, 2026
a22695e
fix(navigator): short-circuit CacheModListFromPapyrus when cache is a…
codepuncher Jun 15, 2026
230a56f
chore(navigator): clarify EnsureCachePopulated comment to reflect tas…
codepuncher Jun 15, 2026
865d4cf
chore(navigator): correct g_modCache comment to reflect both game and…
codepuncher Jun 15, 2026
6002e01
chore(navigator): clarify g_skyUICacheDone comment to cover all termi…
codepuncher Jun 15, 2026
1dc2ed6
fix(config): cast tolower result to char to avoid implicit narrowing …
codepuncher Jun 15, 2026
df1f74a
fix(handler): defer _pendingMCMModName move until task interface is c…
codepuncher Jun 15, 2026
fa6739f
fix(navigator): remove ClearPending from CacheModListFromPapyrus to a…
codepuncher Jun 15, 2026
0e2af56
fix(navigator): release g_lock if task interface becomes unavailable …
codepuncher Jun 15, 2026
96d5546
fix(navigator): downgrade repeatable CacheModListFromPapyrus log mess…
codepuncher Jun 15, 2026
2c7444b
fix(ui): use case-insensitive comparison for combo selected-state in …
codepuncher Jun 15, 2026
753a2b2
fix(navigator): check DelayCallForUI return in NavigateToTargetImpl t…
codepuncher Jun 16, 2026
6ae9679
fix(plugin): use EnsureCachePopulated instead of direct AddTask in me…
codepuncher Jun 16, 2026
20a5afc
chore(test): add ParseAction and ActionName test cases for kMCM
codepuncher Jun 16, 2026
67d8456
fix(navigator): add g_papyrusPending debounce to prevent flooding gam…
codepuncher Jun 16, 2026
8498be9
fix(navigator): always clear g_papyrusPending on exit from CacheModLi…
codepuncher Jun 16, 2026
3c653cf
fix(navigator): set g_papyrusPending in EnsureCachePopulated before s…
codepuncher Jun 16, 2026
ea7d788
fix(navigator): use AddTask game-thread delay in DelayCallForUI to gu…
codepuncher Jun 16, 2026
7c4ec95
fix(handler): revert quickexit to close journal on mod page exit not …
codepuncher Jun 16, 2026
e2db47c
fix(navigator): replace retry counter with wall-clock deadline in Ope…
codepuncher Jun 16, 2026
300fa3b
fix(navigator): guard AddTask in EnsureCachePopulated with g_papyrusP…
codepuncher Jun 16, 2026
a62b7dd
chore(ui): clarify quickexit label to say MCM mod page not MCM
codepuncher Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ else()
src/InputHandler.h
src/InputHandler.cpp
src/LongPressAction.h
src/MCMNavigator.h
src/MCMNavigator.cpp
src/MenuUI.h
src/MenuUI.cpp
src/Plugin.cpp
Expand Down
12 changes: 10 additions & 2 deletions HoldFast.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
; Duration in seconds a button must be held to trigger its long-press action (default: 0.5, max: 5.0)
fHoldDuration=0.5
; Long-press action for the Start (Menu) button. Short press performs the button's normal function.
; Valid values: Map, System, Quests, Stats, Inventory, Magic, Favorites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, None (case-insensitive)
; Valid values: Map, System, Quests, Stats, Inventory, Magic, Favorites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, MCM, None (case-insensitive)
sButtonStartAction=Map
; Mod to open when sButtonStartAction=MCM. Leave as None to open the MCM mod list.
sButtonStartMCMModName=None
; Close the journal automatically when leaving a MCM mod page via Start. Only applies when sButtonStartAction=MCM.
bButtonStartMCMQuickexit=true
Comment thread
codepuncher marked this conversation as resolved.
; Long-press action for the Back (View) button. Short press performs the button's normal function.
; Valid values: Map, System, Quests, Stats, Inventory, Magic, Favorites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, None (case-insensitive)
; Valid values: Map, System, Quests, Stats, Inventory, Magic, Favorites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, MCM, None (case-insensitive)
sButtonBackAction=System
; Mod to open when sButtonBackAction=MCM. Leave as None to open the MCM mod list.
sButtonBackMCMModName=None
; Close the journal automatically when leaving a MCM mod page via Back. Only applies when sButtonBackAction=MCM.
bButtonBackMCMQuickexit=true
45 changes: 27 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,19 @@ Edit `Data\SKSE\Plugins\HoldFast.ini`:
; Duration in seconds a button must be held to trigger its long-press action (default: 0.5, max: 5.0)
fHoldDuration=0.5
; Long-press action for the Start (Menu) button. Short press performs the button's normal function.
; Valid values: Map, System, Quests, Stats, Inventory, Magic, Favorites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, None (case-insensitive)
; Valid values: Map, System, Quests, Stats, Inventory, Magic, Favorites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, MCM, None (case-insensitive)
sButtonStartAction=Map
; Mod to open when sButtonStartAction=MCM. Leave as None to open the MCM mod list.
sButtonStartMCMModName=None
; Close the journal automatically when leaving a MCM mod page via Start. Only applies when sButtonStartAction=MCM.
bButtonStartMCMQuickexit=true
Comment thread
codepuncher marked this conversation as resolved.
; Long-press action for the Back (View) button. Short press performs the button's normal function.
; Valid values: Map, System, Quests, Stats, Inventory, Magic, Favorites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, None (case-insensitive)
; Valid values: Map, System, Quests, Stats, Inventory, Magic, Favorites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, MCM, None (case-insensitive)
sButtonBackAction=System
; Mod to open when sButtonBackAction=MCM. Leave as None to open the MCM mod list.
sButtonBackMCMModName=None
; Close the journal automatically when leaving a MCM mod page via Back. Only applies when sButtonBackAction=MCM.
bButtonBackMCMQuickexit=true
```

**In-game settings (optional):**
Expand All @@ -56,22 +64,23 @@ Use **Save to config** to persist and apply changes, **Reload from config** to d

**Valid actions:**

| Value | What it does |
| ---------------- | ---------------------------------------------- |
| `Map` | Opens the map |
| `System` | Opens the Journal on the System tab |
| `Quests` | Opens the Journal on the Quests tab |
| `Stats` | Opens the Journal on the Stats tab |
| `Inventory` | Opens the inventory |
| `Magic` | Opens the magic menu |
| `Favorites` | Opens the favourites menu |
| `TweenMenu` | Opens the tween menu (Items/Magic/Map/Skills) |
| `Wait` | Opens the sleep/wait menu |
| `NewSave` | Performs a new save |
| `QuickSave` | Performs a quicksave |
| `Bestiary` | Opens The Dragonborn's Bestiary (requires mod) |
| `CharacterSheet` | Opens Character Menu SE (requires mod) |
| `None` | Button not intercepted |
| Value | What it does |
| ---------------- | ------------------------------------------------------------- |
| `Map` | Opens the map |
| `System` | Opens the Journal on the System tab |
| `Quests` | Opens the Journal on the Quests tab |
| `Stats` | Opens the Journal on the Stats tab |
| `Inventory` | Opens the inventory |
| `Magic` | Opens the magic menu |
| `Favorites` | Opens the favourites menu |
| `TweenMenu` | Opens the tween menu (Items/Magic/Map/Skills) |
| `Wait` | Opens the sleep/wait menu |
| `NewSave` | Performs a new save |
| `QuickSave` | Performs a quicksave |
| `Bestiary` | Opens The Dragonborn's Bestiary (requires mod) |
| `CharacterSheet` | Opens Character Menu SE (requires mod) |
| `MCM` | Opens the MCM. Optionally navigates to a specific mod's page. |
| `None` | Button not intercepted |

Logs are written to:

Expand Down
47 changes: 43 additions & 4 deletions src/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ namespace
logger::warn("fHoldDuration ({:.2f}) exceeds maximum {:.1f} — capping", raw, HoldFast::kMaxHoldDuration);
return duration;
}

[[nodiscard]] std::string GetMCMTarget(const CSimpleIniA& ini, const char* modKey)
{
const char* raw = ini.GetValue("General", modKey, nullptr);
if (!raw) {
return "None";
}
const auto trimmed = HoldFast::TrimWhitespace(raw);
if (trimmed.empty()) {
return "None";
}
std::string lower{ trimmed };
std::ranges::transform(lower, lower.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return lower == "none" ? "None" : std::string{ trimmed };
Comment thread
codepuncher marked this conversation as resolved.
}
}

HoldFast::Config::Settings HoldFast::Config::LoadSettings()
Expand All @@ -56,14 +71,16 @@ HoldFast::Config::Settings HoldFast::Config::LoadSettings()
return settings;
}

constexpr auto kValidActions = "Map, System, Quests, Stats, Inventory, Magic, Favorites/Favourites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, MCM, None";

settings.startAction = hasStart ? ParseAction(rawStart) : LongPressAction::kNone;
if (hasStart && settings.startAction == LongPressAction::kNone) {
std::string lower{ HoldFast::TrimWhitespace(rawStart) };
for (auto& c : lower) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (lower != "none") {
logger::warn("sButtonStartAction='{}' is not a recognised action (valid: Map, System, Quests, Stats, Inventory, Magic, Favorites/Favourites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, None) — disabling button", rawStart);
logger::warn("sButtonStartAction='{}' is not a recognised action (valid: {}) — disabling button", rawStart, kValidActions);
}
}
settings.backAction = hasBack ? ParseAction(rawBack) : LongPressAction::kNone;
Expand All @@ -73,9 +90,15 @@ HoldFast::Config::Settings HoldFast::Config::LoadSettings()
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (lower != "none") {
logger::warn("sButtonBackAction='{}' is not a recognised action (valid: Map, System, Quests, Stats, Inventory, Magic, Favorites/Favourites, TweenMenu, Wait, NewSave, QuickSave, Bestiary, CharacterSheet, None) — disabling button", rawBack);
logger::warn("sButtonBackAction='{}' is not a recognised action (valid: {}) — disabling button", rawBack, kValidActions);
}
}

settings.startMCMModName = GetMCMTarget(ini, "sButtonStartMCMModName");
settings.backMCMModName = GetMCMTarget(ini, "sButtonBackMCMModName");
settings.startMCMQuickexit = ini.GetBoolValue("General", "bButtonStartMCMQuickexit", true);
settings.backMCMQuickexit = ini.GetBoolValue("General", "bButtonBackMCMQuickexit", true);

return settings;
}

Expand All @@ -92,6 +115,10 @@ bool HoldFast::Config::SaveSettings(const Settings& settings)
ini.SetValue("General", "fHoldDuration", holdDurationStr.c_str());
ini.SetValue("General", "sButtonStartAction", startActionName.c_str());
ini.SetValue("General", "sButtonBackAction", backActionName.c_str());
ini.SetValue("General", "sButtonStartMCMModName", settings.startMCMModName.c_str());
ini.SetValue("General", "sButtonBackMCMModName", settings.backMCMModName.c_str());
ini.SetBoolValue("General", "bButtonStartMCMQuickexit", settings.startMCMQuickexit);
ini.SetBoolValue("General", "bButtonBackMCMQuickexit", settings.backMCMQuickexit);

const auto rc = ini.SaveFile(kIniPath);
if (rc < SI_OK) {
Expand All @@ -107,10 +134,22 @@ std::vector<ButtonConfig> HoldFast::Config::BuildButtons(const Settings& setting

std::vector<ButtonConfig> buttons;
if (settings.startAction != LongPressAction::kNone) {
buttons.push_back({ .keyCode = static_cast<std::uint32_t>(Key::kStart), .name = "Start", .action = settings.startAction });
buttons.push_back({
.keyCode = static_cast<std::uint32_t>(Key::kStart),
.name = "Start",
.action = settings.startAction,
.mcmModName = settings.startMCMModName,
.mcmQuickexit = settings.startMCMQuickexit,
});
}
if (settings.backAction != LongPressAction::kNone) {
buttons.push_back({ .keyCode = static_cast<std::uint32_t>(Key::kBack), .name = "Back", .action = settings.backAction });
buttons.push_back({
.keyCode = static_cast<std::uint32_t>(Key::kBack),
.name = "Back",
.action = settings.backAction,
.mcmModName = settings.backMCMModName,
.mcmQuickexit = settings.backMCMQuickexit,
});
}
return buttons;
}
Expand Down
7 changes: 6 additions & 1 deletion src/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ namespace HoldFast::Config
float holdDuration{ HoldFast::kDefaultHoldDuration };
LongPressAction startAction{ LongPressAction::kMap };
LongPressAction backAction{ LongPressAction::kSystem };
std::string startMCMModName{ "None" };
std::string backMCMModName{ "None" };
bool startMCMQuickexit{ true };
bool backMCMQuickexit{ true };
};

struct ActionOption
Expand All @@ -23,7 +27,7 @@ namespace HoldFast::Config
LongPressAction action;
};

inline constexpr std::array<ActionOption, 14> kActionOptions{ {
inline constexpr std::array<ActionOption, 15> kActionOptions{ {
{ "Map", LongPressAction::kMap },
{ "System", LongPressAction::kSystem },
{ "Quests", LongPressAction::kQuests },
Expand All @@ -37,6 +41,7 @@ namespace HoldFast::Config
{ "QuickSave", LongPressAction::kQuickSave },
{ "Bestiary", LongPressAction::kBestiary },
{ "CharacterSheet", LongPressAction::kCharacterSheet },
{ "MCM", LongPressAction::kMCM },
{ "None", LongPressAction::kNone },
} };

Expand Down
1 change: 1 addition & 0 deletions src/ConfigParsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ LongPressAction HoldFast::Config::ParseAction(std::string_view raw)
{ "quicksave", LongPressAction::kQuickSave },
{ "bestiary", LongPressAction::kBestiary },
{ "charactersheet", LongPressAction::kCharacterSheet },
{ "mcm", LongPressAction::kMCM },
{ "none", LongPressAction::kNone },
};

Expand Down
Loading
Loading