Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 18 additions & 28 deletions src/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,11 @@ namespace
return duration;
}
if (!std::isfinite(raw)) {
logger::warn("fHoldDuration is non-finite — using default {:.1f}", HoldFast::kDefaultHoldDuration);
logger::warn("fHoldDuration is non-finite — using default {:.1f}", duration);
return duration;
}
if (raw <= 0.0F) {
logger::warn("fHoldDuration ({:.2f}) must be positive — using default {:.1f}", raw, HoldFast::kDefaultHoldDuration);
return duration;
}
if (raw < HoldFast::kMinHoldDuration) {
logger::warn("fHoldDuration ({:.2f}) is below minimum {:.1f} — using default {:.1f}", raw, HoldFast::kMinHoldDuration, HoldFast::kDefaultHoldDuration);
return duration;
}
logger::warn("fHoldDuration ({:.2f}) exceeds maximum {:.1f} — capping", raw, HoldFast::kMaxHoldDuration);
logger::warn("fHoldDuration ({:.2f}) out of range [{:.1f}, {:.1f}] — using {:.1f}",
raw, HoldFast::kMinHoldDuration, HoldFast::kMaxHoldDuration, duration);
return duration;
}

Expand Down Expand Up @@ -73,25 +66,22 @@ HoldFast::Config::Settings HoldFast::Config::LoadSettings()

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)));
const auto warnIfUnrecognised = [kValidActions](const char* raw, const char* key, LongPressAction action) {
if (action != LongPressAction::kNone) {
return;
}
if (lower != "none") {
logger::warn("sButtonStartAction='{}' is not a recognised action (valid: {}) — disabling button", rawStart, kValidActions);
if (!HoldFast::CaseInsensitiveEqual(HoldFast::TrimWhitespace(raw), HoldFast::kNoneName)) {
logger::warn("{}='{}' is not a recognised action (valid: {}) — disabling button", key, raw, kValidActions);
}
}
};

settings.startAction = hasStart ? ParseAction(rawStart) : LongPressAction::kNone;
settings.backAction = hasBack ? ParseAction(rawBack) : LongPressAction::kNone;
if (hasBack && settings.backAction == LongPressAction::kNone) {
std::string lower{ HoldFast::TrimWhitespace(rawBack) };
for (auto& c : lower) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (lower != "none") {
logger::warn("sButtonBackAction='{}' is not a recognised action (valid: {}) — disabling button", rawBack, kValidActions);
}
if (hasStart) {
warnIfUnrecognised(rawStart, "sButtonStartAction", settings.startAction);
}
if (hasBack) {
warnIfUnrecognised(rawBack, "sButtonBackAction", settings.backAction);
}

settings.startMCMModName = GetMCMTarget(ini, "sButtonStartMCMModName");
Expand All @@ -100,11 +90,11 @@ HoldFast::Config::Settings HoldFast::Config::LoadSettings()
settings.backMCMQuickexit = ini.GetBoolValue("General", "bButtonBackMCMQuickexit", true);

if (settings.startAction == LongPressAction::kMCM &&
HoldFast::CaseInsensitiveEqual(settings.startMCMModName, "None")) {
HoldFast::CaseInsensitiveEqual(settings.startMCMModName, HoldFast::kNoneName)) {
logger::warn("sButtonStartAction=MCM but sButtonStartMCMModName is not set — Start button will open MCM without navigating to a specific mod");
}
if (settings.backAction == LongPressAction::kMCM &&
HoldFast::CaseInsensitiveEqual(settings.backMCMModName, "None")) {
HoldFast::CaseInsensitiveEqual(settings.backMCMModName, HoldFast::kNoneName)) {
logger::warn("sButtonBackAction=MCM but sButtonBackMCMModName is not set — Back button will open MCM without navigating to a specific mod");
}

Expand Down
2 changes: 1 addition & 1 deletion src/InputHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ void InputHandler::InvokeScaleformTab(JournalTab tab)
return;
}

if (_pendingMCMModName.empty() || _pendingMCMModName == "None") {
if (_pendingMCMModName.empty() || _pendingMCMModName == HoldFast::kNoneName) {
return;
}
const auto* taskIface = SKSE::GetTaskInterface();
Expand Down
8 changes: 5 additions & 3 deletions src/LongPressAction.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

#include <cstdint>
#include <string>
#include <string_view>

namespace HoldFast
{
inline constexpr float kMinHoldDuration = 0.1F;
inline constexpr float kDefaultHoldDuration = 0.5F;
inline constexpr float kMaxHoldDuration = 5.0F;
inline constexpr float kMinHoldDuration = 0.1F;
inline constexpr float kDefaultHoldDuration = 0.5F;
inline constexpr float kMaxHoldDuration = 5.0F;
inline constexpr std::string_view kNoneName = "None";
}

enum class LongPressAction
Expand Down
5 changes: 3 additions & 2 deletions src/MCMNavigator.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "PCH.h"

#include "LongPressAction.h"
#include "MCMNavigator.h"
#include "Utils.h"

Expand All @@ -22,7 +23,7 @@ namespace MCMNavigator
{
return std::ranges::lexicographical_compare(
a, b,
[](unsigned char x, unsigned char y) { return std::tolower(x) < std::tolower(y); });
[](unsigned char x, unsigned char y) { return HoldFast::AsciiToLower(x) < HoldFast::AsciiToLower(y); });
}

std::string_view StripModNamePrefix(std::string_view name)
Expand Down Expand Up @@ -294,7 +295,7 @@ namespace MCMNavigator

void NavigateToTargetImpl(const std::string& modName)
{
if (modName.empty() || modName == "None") {
if (modName.empty() || modName == HoldFast::kNoneName) {
return;
}

Expand Down
6 changes: 3 additions & 3 deletions src/MenuUI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,17 @@ namespace
{
MCMNavigator::EnsureCachePopulated();

const char* preview = modName.empty() || modName == "None" ? "None" : modName.c_str();
const char* preview = modName.empty() || modName == HoldFast::kNoneName ? "None" : modName.c_str();

if (!ImGuiMCP::BeginCombo(modLabel, preview)) {
return;
}

const auto cachedMods = MCMNavigator::GetCachedModNames();

const bool noneSelected = modName.empty() || modName == "None";
const bool noneSelected = modName.empty() || modName == HoldFast::kNoneName;
if (ImGuiMCP::Selectable("None", noneSelected)) {
modName = "None";
modName = HoldFast::kNoneName;
changed = true;
Comment thread
codepuncher marked this conversation as resolved.
}

Expand Down
8 changes: 6 additions & 2 deletions src/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

#include <algorithm>
#include <cassert>
#include <cctype>
#include <cmath>
#include <string_view>

Expand All @@ -18,11 +17,16 @@ namespace HoldFast
return s.substr(first, s.find_last_not_of(" \t\r\n") - first + 1);
}

[[nodiscard]] inline constexpr unsigned char AsciiToLower(unsigned char c) noexcept
{
return (c >= 'A' && c <= 'Z') ? static_cast<unsigned char>(c + ('a' - 'A')) : c;
}
Comment thread
codepuncher marked this conversation as resolved.

[[nodiscard]] inline bool CaseInsensitiveEqual(std::string_view a, std::string_view b)
{
return std::ranges::equal(
a, b,
[](unsigned char x, unsigned char y) { return std::tolower(x) == std::tolower(y); });
[](unsigned char x, unsigned char y) { return AsciiToLower(x) == AsciiToLower(y); });
}
Comment thread
codepuncher marked this conversation as resolved.

[[nodiscard]] inline float ClampHoldDuration(float value, float defaultVal, float minVal, float maxVal)
Expand Down
28 changes: 28 additions & 0 deletions test/PluginTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ using HoldFast::TrimWhitespace;
using HoldFast::Config::ActionName;
using HoldFast::Config::ParseAction;

TEST_CASE("AsciiToLower lowercases A-Z only", "[utils]")
{
using HoldFast::AsciiToLower;

CHECK(AsciiToLower('A') == 'a');
CHECK(AsciiToLower('Z') == 'z');
CHECK(AsciiToLower('M') == 'm');
CHECK(AsciiToLower('a') == 'a');
CHECK(AsciiToLower('z') == 'z');
CHECK(AsciiToLower('0') == '0');
CHECK(AsciiToLower('!') == '!');
CHECK(AsciiToLower(static_cast<unsigned char>(0xFF)) == static_cast<unsigned char>(0xFF));
}

TEST_CASE("CaseInsensitiveEqual matches ASCII case-insensitively", "[utils]")
{
using HoldFast::CaseInsensitiveEqual;

CHECK(CaseInsensitiveEqual("None", "none"));
CHECK(CaseInsensitiveEqual("NONE", "None"));
CHECK(CaseInsensitiveEqual("MCM", "mcm"));
CHECK(CaseInsensitiveEqual("Map", "MAP"));
CHECK(CaseInsensitiveEqual("", ""));
CHECK_FALSE(CaseInsensitiveEqual("Map", "Mcm"));
CHECK_FALSE(CaseInsensitiveEqual("None", ""));
CHECK_FALSE(CaseInsensitiveEqual("abc", "abcd"));
}

TEST_CASE("TrimWhitespace removes leading and trailing whitespace", "[utils]")
{
CHECK(TrimWhitespace(" hello ") == "hello");
Expand Down
Loading