Skip to content
Open
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
4 changes: 0 additions & 4 deletions .github/workflows/deploy-beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ on:
types:
- completed

concurrency:
group: deploy-beta
cancel-in-progress: true

jobs:
check-builds:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion MMAPPER_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
26.04.1
26.04.0
25 changes: 0 additions & 25 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,3 @@
## MMapper 26.04.1 (April 15, 2026)

### New Features:
* **Secure Password Saving in MMapper Web:**
* **Password Manager Support:** You can now securely save your MUME account credentials when playing in the browser! We’ve built a custom bridge that lets MMapper talk to your browser’s native password manager (Chrome, Firefox, or Safari).
* **How it works:** When you save your password, a temporary window will appear to satisfy the browser's security checks. Once saved, the browser will treat MMapper just like any other website—allowing for **Automatic Logins** and biometric (TouchID/FaceID) unlocks where available.
* **Overhauled Preferences Dialog:**
* Transitioned the configuration settings to a unified, scrollable view.
* Added a fast, **debounced search** feature that highlights specific results and seamlessly syncs your scroll position with the sidebar navigation.
* **Revamped Group Widget:**
* HP, Mana, and Moves are now displayed as **colored bars** with centered text.
* Added an animated **pulsing effect** to alert you when your health or movement values are critically low.
* **Dockable Timers Panel:** Introduced a brand new, dockable panel for managing in-game timers. Features include live updates, drag-and-drop reordering, and easy context menu actions (stop, reset, clear).
* **Audio Quick Controls:** Added a new audio toolbar featuring a dedicated volume slider for faster access to sound settings.

### Bug Fixes:
* **Background Audio Hang (Windows):** Fixed a critical issue where the MMapper process would often stay running in the background after the window was closed. This was caused by a combination of lingering audio resources on Windows and a graphics "infinite loop" affecting NVIDIA users during teardown.
* **Room Panel Fixes:** Fixed a bug where certain room entries wouldn't display correctly due to strict type errors.

### Changes & Under the Hood:
* **Framework Updates:** Updated macOS and Windows builds to **Qt 6.8.3** and bumped the Linux Flatpak to the **KDE 6.10** runtime.
* **Better Visual Smoothing:** Lighting and time-of-day transitions now use the **Oklab color space**, making sunset and sunrise gradients look much more natural and "smooth" to the human eye.
* **Build Efficiency:** Optimized the "QtKeychain" logic to prevent unnecessary re-downloads during updates, making the build process faster for contributors.
* **Maintenance:** Standard GitHub Actions and CI/CD pipelines have been updated to the latest major versions for better security and reliability.

## MMapper 26.04.0 (April 2, 2026)

### New Features:
Expand Down
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ set(mmapper_SRCS
client/Hotkey.h
client/HotkeyManager.cpp
client/HotkeyManager.h
client/UserActionManager.cpp
client/UserActionManager.h
client/PasswordDialog.cpp
client/PasswordDialog.h
client/PreviewWidget.cpp
Expand Down Expand Up @@ -464,6 +466,7 @@ set(mmapper_SRCS
parser/AbstractParser-Config.cpp
parser/AbstractParser-Group.cpp
parser/AbstractParser-Hotkey.cpp
parser/AbstractParser-UserActions.cpp
parser/AbstractParser-Mark.cpp
parser/AbstractParser-Room.cpp
parser/AbstractParser-Timer.cpp
Expand Down
228 changes: 228 additions & 0 deletions src/client/UserActionManager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 The MMapper Authors

#include "UserActionManager.h"

#include "../configuration/configuration.h"
#include "../global/TextUtils.h"
#include "../global/logging.h"

#include <algorithm>
#include <regex>

std::string UserAction::serialize() const
{
std::string typeStr;
switch (type) {
case UserActionType::Regex:
typeStr = "regex";
break;
case UserActionType::StartsWith:
typeStr = "starts";
break;
case UserActionType::EndsWith:
typeStr = "ends";
break;
}
return typeStr + ":" + command;
}

std::unique_ptr<UserAction> UserAction::deserialize(const std::string &pattern,
const std::string &serialized)
{
size_t colon = serialized.find(':');
if (colon == std::string::npos) {
return nullptr;
}

std::string typeStr = serialized.substr(0, colon);
std::string command = serialized.substr(colon + 1);

UserActionType type;
if (typeStr == "regex") {
type = UserActionType::Regex;
} else if (typeStr == "starts") {
type = UserActionType::StartsWith;
} else if (typeStr == "ends") {
type = UserActionType::EndsWith;
} else {
return nullptr;
}

return std::make_unique<UserAction>(UserAction{type, pattern, command});
}

CompiledUserAction::CompiledUserAction(UserAction a)
: action(std::move(a))
{
using namespace char_consts;
switch (action.type) {
case UserActionType::Regex:
try {
regex.emplace(action.pattern, std::regex::optimize);
if (action.pattern.length() > 2 && action.pattern[0] == C_CARET
&& action.pattern[1] != C_BACKSLASH && action.pattern[1] != C_OPEN_PARENS) {
hint = action.pattern[1];
}
} catch (const std::regex_error &) {
// ignore
}
break;
case UserActionType::StartsWith:
if (!action.pattern.empty()) {
hint = action.pattern[0];
}
break;
case UserActionType::EndsWith:
break;
}
}

UserActionManager::UserActionManager()
{
setConfig().actions.registerChangeCallback(m_configLifetime, [this]() { syncFromConfig(); });
setConfig().actions.registerResetCallback(m_configLifetime, [this]() {
m_actions.clear();
m_actionMap.clear();
setConfig().actions.setData(QVariantMap());
});
syncFromConfig();
}

void UserActionManager::syncFromConfig()
{
m_actions.clear();
m_actionMap.clear();
const QVariantMap &data = getConfig().actions.data();
for (auto it = data.begin(); it != data.end(); ++it) {
std::string pattern = mmqt::toStdStringUtf8(it.key());
std::string serialized = mmqt::toStdStringUtf8(it.value().toString());
if (auto action = UserAction::deserialize(pattern, serialized)) {
auto compiled = std::make_shared<CompiledUserAction>(std::move(*action));
m_actions[pattern] = compiled;
m_actionMap.emplace(compiled->hint, compiled);
} else {
MMLOG_WARNING() << "invalid action" << mmqt::toStdStringUtf8(it.key())
<< mmqt::toStdStringUtf8(it.value().toString());
}
}
}

bool UserActionManager::setAction(UserActionType type, std::string pattern, std::string command)
{
if (pattern.empty()) {
return false;
}

QVariantMap data = getConfig().actions.data();
UserAction action{type, pattern, command};
data[mmqt::toQStringUtf8(pattern)] = mmqt::toQStringUtf8(action.serialize());
setConfig().actions.setData(std::move(data));
return true;
}

bool UserActionManager::removeAction(const std::string &pattern)
{
QVariantMap data = getConfig().actions.data();
QString key = mmqt::toQStringUtf8(pattern);
if (!data.contains(key)) {
return false;
}
data.remove(key);
setConfig().actions.setData(std::move(data));
return true;
}

std::vector<const UserAction *> UserActionManager::getAllActions() const
{
std::vector<const UserAction *> result;
for (const auto &pair : m_actions) {
result.push_back(&pair.second->action);
}
return result;
}

std::string UserActionManager::substitute(const std::string &command,
const std::vector<std::string> &captures)
{
std::string result;
result.reserve(command.size());

for (size_t i = 0; i < command.size(); ++i) {
if (command[i] == '%' && i + 1 < command.size() && std::isdigit(command[i + 1])) {
size_t idx = static_cast<size_t>(command[i + 1] - '0');
if (idx < captures.size()) {
result += captures[idx];
}
++i;
} else {
result += command[i];
}
}
return result;
}

void UserActionManager::evaluate(
StringView line, const std::function<void(const std::string &)> &executeCallback) const
{
if (line.empty()) {
return;
}

auto runMatch = [&](const std::shared_ptr<CompiledUserAction> &compiled) {
const UserAction &action = compiled->action;
bool matched = false;
std::vector<std::string> captures;

switch (action.type) {
case UserActionType::Regex: {
if (compiled->regex) {
std::cmatch match;
const std::string_view sv = line.getStdStringView();
if (std::regex_search(sv.data(), sv.data() + sv.size(), match, *compiled->regex)) {
matched = true;
captures.reserve(match.size());
for (size_t i = 0; i < match.size(); ++i) {
const auto &sub = match[i];
if (sub.matched) {
captures.emplace_back(sub.first, static_cast<size_t>(sub.length()));
} else {
captures.emplace_back();
}
}
}
}
break;
}
case UserActionType::StartsWith:
if (line.startsWith(std::string_view(action.pattern))) {
matched = true;
captures.emplace_back(line.toStdString());
}
break;
case UserActionType::EndsWith:
if (line.endsWith(std::string_view(action.pattern))) {
matched = true;
captures.emplace_back(line.toStdString());
}
break;
}

if (matched) {
executeCallback(substitute(action.command, captures));
}
};

const char firstChar = line.firstChar();
auto range = m_actionMap.equal_range(firstChar);
for (auto it = range.first; it != range.second; ++it) {
runMatch(it->second);
}

if (firstChar != 0) {
range = m_actionMap.equal_range(0);
for (auto it = range.first; it != range.second; ++it) {
runMatch(it->second);
}
}
}
64 changes: 64 additions & 0 deletions src/client/UserActionManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#pragma once
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 The MMapper Authors

#include "../global/ChangeMonitor.h"
#include "../global/RuleOf5.h"
#include "../global/StringView.h"
#include "../global/macros.h"

#include <memory>
#include <optional>
#include <regex>
#include <string>
#include <unordered_map>
#include <vector>

#include <QVariantMap>

enum class UserActionType { Regex, StartsWith, EndsWith };

struct UserAction
{
UserActionType type;
std::string pattern;
std::string command;

NODISCARD std::string serialize() const;
NODISCARD static std::unique_ptr<UserAction> deserialize(const std::string &pattern,
const std::string &serialized);
};

struct CompiledUserAction
{
UserAction action;
std::optional<std::regex> regex;
char hint = 0;

explicit CompiledUserAction(UserAction a);
};

class UserActionManager
{
private:
std::unordered_map<std::string, std::shared_ptr<CompiledUserAction>> m_actions;
std::unordered_multimap<char, std::shared_ptr<CompiledUserAction>> m_actionMap;
ChangeMonitor::Lifetime m_configLifetime;

public:
UserActionManager();
~UserActionManager() = default;
DELETE_CTORS_AND_ASSIGN_OPS(UserActionManager);

void syncFromConfig();
NODISCARD bool setAction(UserActionType type, std::string pattern, std::string command);
NODISCARD bool removeAction(const std::string &pattern);
NODISCARD std::vector<const UserAction *> getAllActions() const;

void evaluate(StringView line,
const std::function<void(const std::string &)> &executeCallback) const;

private:
static std::string substitute(const std::string &command,
const std::vector<std::string> &captures);
};
4 changes: 4 additions & 0 deletions src/configuration/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ ConstString GRP_FINDROOMS_DIALOG = "FindRooms Dialog";
ConstString GRP_GENERAL = "General";
ConstString GRP_GROUP_MANAGER = "Group Manager";
ConstString GRP_HOTKEYS = "Hotkeys";
ConstString GRP_ACTIONS = "Actions";
ConstString GRP_INFOMARKS_DIALOG = "InfoMarks Dialog";
ConstString GRP_INTEGRATED_MUD_CLIENT = "Integrated Mud Client";
ConstString GRP_MUME_CLIENT_PROTOCOL = "Mume client protocol";
Expand All @@ -208,6 +209,7 @@ ConstString GRP_ROOMEDIT_DIALOG = "RoomEdit Dialog";

Configuration::Configuration()
: hotkeys(GRP_HOTKEYS)
, actions(GRP_ACTIONS)
{
read(); // read the settings or set them to the default values
}
Expand Down Expand Up @@ -497,6 +499,7 @@ NODISCARD static uint16_t sanitizeUint16(const int input, const uint16_t default
GROUP_CALLBACK(callback, GRP_ROOM_PANEL, roomPanel); \
GROUP_CALLBACK(callback, GRP_FINDROOMS_DIALOG, findRoomsDialog); \
GROUP_CALLBACK(callback, GRP_HOTKEYS, hotkeys); \
GROUP_CALLBACK(callback, GRP_ACTIONS, actions); \
} while (false)

void Configuration::read()
Expand Down Expand Up @@ -528,6 +531,7 @@ void Configuration::readFrom(QSettings &conf)
autoLog.autoLog = (CURRENT_PLATFORM != PlatformEnum::Wasm);

hotkeys.resetToDefault();
actions.resetToDefault();
}

assert(canvas.backgroundColor == colorSettings.BACKGROUND);
Expand Down
1 change: 1 addition & 0 deletions src/configuration/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ class NODISCARD Configuration final
} findRoomsDialog;

GroupConfig hotkeys;
GroupConfig actions;

public:
DELETE_CTORS_AND_ASSIGN_OPS(Configuration);
Expand Down
Loading
Loading