Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a6153f6
Add Character Token system and column to Group Manager with image cac…
Sunnyl75 Jun 18, 2025
91c483c
GroupManager: add showTokens preference to toggle icon column
Sunnyl75 Jun 24, 2025
1095952
GroupManager: add tokenIconSize preference (16???64 px live-resize)
Sunnyl75 Jun 24, 2025
200fb40
GroupManager: add per-row icon picker and default-icon reset
Sunnyl75 Jun 24, 2025
adc22ee
feat(display): character & NPC map tokens
Sunnyl75 Jul 10, 2025
84ac8f8
WIP: Preserve widget edits before switching branch
nschimme Jun 15, 2025
b16c197
groupwidget: refresh token column after preference change
Sunnyl75 Jul 10, 2025
538170b
Update map immediately after token change
Sunnyl75 Jul 11, 2025
29130fb
grouppage: tidy header before PR
Sunnyl75 Jul 11, 2025
0faf3c4
style: whitespace and clang-format clean-up
Sunnyl75 Jul 11, 2025
39abe5f
style: remove two redundant blank lines (CodeFactor)
Sunnyl75 Jul 11, 2025
fb321ad
refactor: simplify getToken() and extract helper funcs
Sunnyl75 Jul 11, 2025
f2385ef
refactor: simplify dataForCharacter() and remove duplicate ToolTipRole
Sunnyl75 Jul 11, 2025
dedd0b8
refactor: shorten setCharacters() and move context-menu to helper slots
Sunnyl75 Jul 11, 2025
7b5ccce
fix: rename remaining insertNewChars call to insertNewCharactersInto
Sunnyl75 Jul 11, 2025
f2f790e
Create build-tokens
Sunnyl75 Jul 21, 2025
14de596
Create build-map-tokens-clean
Sunnyl75 Jul 21, 2025
19f20f1
Update build-map-tokens-clean
Sunnyl75 Jul 21, 2025
d5a6489
chore: add GhostRegistry skeleton and global instance
Sunnyl75 Jul 19, 2025
5df5605
feat: add provisional isMount() helper (currently == isNpc())
Sunnyl75 Jul 19, 2025
c036a01
feat: add provisional isMount() helper and capture GhostInfo on mount…
Sunnyl75 Jul 19, 2025
12b6ab9
feat: clear ghost entry when mount is visible again
Sunnyl75 Jul 19, 2025
65ffab7
feat: add last-seen mount ghost token
Sunnyl75 Jul 19, 2025
8322add
fix: token quad now uses caller color, enabling 70% ghost opacity
Sunnyl75 Jul 19, 2025
c6bf0fc
fix: token quad now uses caller color, enabling 50% ghost opacity
Sunnyl75 Jul 19, 2025
3fc7e9e
fix: purge stale ghost using roomSid while keeping string-keyed registry
Sunnyl75 Jul 19, 2025
29e4ee5
fix: use room-id map key in ghost draw loop (struct no longer stores …
Sunnyl75 Jul 19, 2025
c846571
config: define KEY_GROUP_SHOW_NPC_GHOSTS constant
Sunnyl75 Jul 19, 2025
9f115ed
config: add showNpcGhosts flag (default true)
Sunnyl75 Jul 19, 2025
a345c59
feat: honour 'Show last-seen NPC icon' preference in ghost logic
Sunnyl75 Jul 20, 2025
1bbcb9e
feat: preferences checkbox & build tweaks for NPC ghost feature
Sunnyl75 Jul 21, 2025
6d74966
ci: trigger map-tokens-clean build
Sunnyl75 Jul 21, 2025
ae14737
tokens: draw map tokens at 85% of room size, centered (no asset changes)
Sunnyl75 Aug 5, 2025
b19d1ef
group: keep row height; render token icons at ~85% of row (no centering)
Sunnyl75 Aug 5, 2025
1e65256
Group Manager: center character token icons and size column to scaled…
Sunnyl75 Aug 11, 2025
0208c4e
Group Manager: token column — paint background then centered icon; si…
Sunnyl75 Aug 11, 2025
2c12e09
CI: add Qt6 tripack workflow (Linux/macOS/Windows) and package as Mma…
Sunnyl75 Aug 11, 2025
4cffdf9
CI: align workflows with upstream; remove custom workflows
Sunnyl75 Aug 14, 2025
91ca251
Revert "Group Manager: token column — paint background then centered …
Sunnyl75 Sep 17, 2025
bda71ce
Revert "Group Manager: center character token icons and size column t…
Sunnyl75 Sep 17, 2025
96772c6
Revert "group: keep row height; render token icons at ~85% of row (no…
Sunnyl75 Sep 17, 2025
1962896
UI: default unchecked checkboxes; default token size 32px
Sunnyl75 Oct 22, 2025
1baa2f0
chore: trigger CI
Sunnyl75 Oct 22, 2025
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: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ MMapper
[![Code Coverage](https://codecov.io/gh/MUME/MMapper/branch/master/graph/badge.svg)](https://codecov.io/gh/MUME/MMapper)
[![GitHub](https://img.shields.io/github/license/MUME/MMapper.svg)](https://github.com/MUME/MMapper/blob/master/COPYING.txt)
[![snapcraft](https://snapcraft.io/mmapper/badge.svg)](https://snapcraft.io/mmapper)
[![Flathub Downloads](https://img.shields.io/flathub/downloads/org.mume.MMapper)](https://flathub.org/apps/org.mume.MMapper)

[![MMapper Screenshot](/../master/appdata/screenshot1.png?raw=true "MMapper")<br>Download the latest version of MMapper](https://github.com/MUME/MMapper/releases)

Expand All @@ -28,4 +27,5 @@ MMapper is a graphical mapping tool for the MUD (Multi-User Dungeon) game MUME (
To get started with MMapper, follow the [setup instructions](https://github.com/MUME/MMapper/wiki) in our wiki. It includes detailed steps for configuring MMapper with your MUME client and installing it on Linux, Windows, and macOS. Once installed, you'll be ready to start your mapping with ease.

## Contributing
We welcome contributions to MMapper! If you're interested in improving the tool, simply submit a pull request on GitHub. Your contributions will help improve the experience for all MUME players. You can also read the [BUILD.md](BUILD.md) to get started with building MMapper from source.
We welcome contributions to MMapper! If you're interested in improving the tool, simply submit a pull request on GitHub. Your contributions will help improve the experience for all MUME players. You can also check out the [build instructions](https://github.com/MUME/MMapper/wiki/Build) on the wiki to get started with building MMapper from source.
# trigger CI
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ set(mmapper_SRCS
group/mmapper2character.h
group/mmapper2group.cpp
group/mmapper2group.h
group/tokenmanager.cpp
group/tokenmanager.h
logger/autologger.cpp
logger/autologger.h
mainwindow/DescriptionWidget.cpp
Expand Down Expand Up @@ -648,6 +650,7 @@ add_executable(mmapper WIN32 MACOSX_BUNDLE
${mmapper_RCS}
${mmapper_QRC}
${mmapper_DATA}
display/GhostRegistry.h
)

set(mm_libs mm_map mm_global)
Expand Down
27 changes: 27 additions & 0 deletions src/configuration/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ ConstString KEY_GROUP_NPC_COLOR = "npc color";
ConstString KEY_GROUP_NPC_COLOR_OVERRIDE = "npc color override";
ConstString KEY_GROUP_NPC_SORT_BOTTOM = "npc sort bottom";
ConstString KEY_GROUP_NPC_HIDE = "npc hide";
ConstString KEY_GROUP_SHOW_MAP_TOKENS = "show map tokens";
ConstString KEY_GROUP_SHOW_NPC_GHOSTS = "show npc ghosts";
ConstString KEY_GROUP_SHOW_TOKENS = "show tokens";
ConstString KEY_GROUP_TOKEN_ICON_SIZE = "token icon size";
ConstString KEY_GROUP_TOKEN_OVERRIDES = "token overrides";
ConstString KEY_AUTO_LOG = "Auto log";
ConstString KEY_AUTO_LOG_ASK_DELETE = "Auto log ask before deleting";
ConstString KEY_AUTO_LOG_CLEANUP_STRATEGY = "Auto log cleanup strategy";
Expand Down Expand Up @@ -689,6 +694,18 @@ void Configuration::GroupManagerSettings::read(const QSettings &conf)
npcColorOverride = conf.value(KEY_GROUP_NPC_COLOR_OVERRIDE, false).toBool();
npcHide = conf.value(KEY_GROUP_NPC_HIDE, false).toBool();
npcSortBottom = conf.value(KEY_GROUP_NPC_SORT_BOTTOM, false).toBool();
showTokens = conf.value(KEY_GROUP_SHOW_TOKENS, true).toBool();
showMapTokens = conf.value(KEY_GROUP_SHOW_MAP_TOKENS, true).toBool();
tokenIconSize = conf.value(KEY_GROUP_TOKEN_ICON_SIZE, 32).toInt();
showNpcGhosts = conf.value(KEY_GROUP_SHOW_NPC_GHOSTS, true).toBool();

tokenOverrides.clear();
QSettings &rw = const_cast<QSettings &>(conf);
rw.beginGroup(KEY_GROUP_TOKEN_OVERRIDES);
const QStringList keys = rw.childKeys();
for (const QString &k : keys)
tokenOverrides.insert(k, rw.value(k).toString());
rw.endGroup();
}

void Configuration::MumeClockSettings::read(const QSettings &conf)
Expand Down Expand Up @@ -855,6 +872,16 @@ void Configuration::GroupManagerSettings::write(QSettings &conf) const
conf.setValue(KEY_GROUP_NPC_COLOR_OVERRIDE, npcColorOverride);
conf.setValue(KEY_GROUP_NPC_HIDE, npcHide);
conf.setValue(KEY_GROUP_NPC_SORT_BOTTOM, npcSortBottom);
conf.setValue(KEY_GROUP_SHOW_TOKENS, showTokens);
conf.setValue(KEY_GROUP_SHOW_MAP_TOKENS, showMapTokens);
conf.setValue(KEY_GROUP_TOKEN_ICON_SIZE, tokenIconSize);
conf.setValue(KEY_GROUP_SHOW_NPC_GHOSTS, showNpcGhosts);

conf.beginGroup(KEY_GROUP_TOKEN_OVERRIDES);
conf.remove(""); // wipe old map entries
for (auto it = tokenOverrides.cbegin(); it != tokenOverrides.cend(); ++it)
conf.setValue(it.key(), it.value());
conf.endGroup();
}

void Configuration::MumeClockSettings::write(QSettings &conf) const
Expand Down
5 changes: 5 additions & 0 deletions src/configuration/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ class NODISCARD Configuration final
bool npcColorOverride = false;
bool npcSortBottom = false;
bool npcHide = false;
bool showTokens = true;
bool showMapTokens = true;
int tokenIconSize = 32;
bool showNpcGhosts = true;
QMap<QString, QString> tokenOverrides;

private:
SUBGROUP();
Expand Down
180 changes: 149 additions & 31 deletions src/display/Characters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
#include "../configuration/configuration.h"
#include "../group/CGroupChar.h"
#include "../group/mmapper2group.h"
#include "../group/tokenmanager.h"
#include "../map/room.h"
#include "../map/roomid.h"
#include "../mapdata/mapdata.h"
#include "../mapdata/roomselection.h"
#include "../opengl/OpenGL.h"
#include "../opengl/OpenGLTypes.h"
#include "GhostRegistry.h"
#include "MapCanvasData.h"
#include "Textures.h"
#include "mapcanvas.h"
Expand All @@ -24,8 +26,11 @@

#include <glm/glm.hpp>

#include <QColor>
#include <QtCore>

std::unordered_map<ServerRoomId, GhostInfo> g_ghosts;

static constexpr float CHAR_ARROW_LINE_WIDTH = 2.f;
static constexpr float PATH_LINE_WIDTH = 4.f;
static constexpr float PATH_POINT_SIZE = 8.f;
Expand All @@ -48,7 +53,10 @@ bool CharacterBatch::isVisible(const Coordinate &c, float margin) const
return m_mapScreen.isRoomVisible(c, margin);
}

void CharacterBatch::drawCharacter(const Coordinate &c, const Color &color, bool fill)
void CharacterBatch::drawCharacter(const Coordinate &c,
const Color &color,
bool fill,
const QString &dispName)
{
const Configuration::CanvasSettings &settings = getConfig().canvas;

Expand Down Expand Up @@ -99,7 +107,7 @@ void CharacterBatch::drawCharacter(const Coordinate &c, const Color &color, bool
}

const bool beacon = visible && !differentLayer && wantBeacons;
gl.drawBox(c, fill, beacon, isFar);
gl.drawBox(c, fill, beacon, isFar, dispName);
}

void CharacterBatch::drawPreSpammedPath(const Coordinate &c1,
Expand Down Expand Up @@ -211,10 +219,8 @@ void CharacterBatch::CharFakeGL::drawQuadCommon(const glm::vec2 &in_a,
}
}

void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord,
bool fill,
bool beacon,
const bool isFar)
void CharacterBatch::CharFakeGL::drawBox(
const Coordinate &coord, bool fill, bool beacon, const bool isFar, const QString &dispName)
{
const bool dontFillRotatedQuads = true;
const bool shrinkRotatedQuads = false; // REVISIT: make this a user option?
Expand Down Expand Up @@ -274,6 +280,40 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord,
addTransformed(c);
addTransformed(d);

// ── NEW: also queue a token quad (drawn under coloured overlay) ──
if (!dispName.isEmpty() && getConfig().groupManager.showMapTokens) {
const Color &tokenColor = color; // inherits alpha from caller
const auto &mtx = m_stack.top().modelView;

auto pushVert = [this, &tokenColor, &mtx](const glm::vec2 &roomPos,
const glm::vec2 &uv) {
const auto tmp = mtx * glm::vec4(roomPos, 0.f, 1.f);
m_charTokenQuads.emplace_back(tokenColor, uv, glm::vec3{tmp / tmp.w});
};

// Scale the room quad around its center to 85%
static constexpr float kTokenScale = 0.85f;
const glm::vec2 center = 0.5f * (a + c); // midpoint of opposite corners
const auto scaleAround = [&](const glm::vec2 &p) {
return center + (p - center) * kTokenScale;
};

const glm::vec2 sa = scaleAround(a);
const glm::vec2 sb = scaleAround(b);
const glm::vec2 sc = scaleAround(c);
const glm::vec2 sd = scaleAround(d);

// Keep full UVs so the whole texture shows on the smaller quad
pushVert(sa, {0.f, 0.f}); // lower-left
pushVert(sb, {1.f, 0.f}); // lower-right
pushVert(sc, {1.f, 1.f}); // upper-right
pushVert(sd, {0.f, 1.f}); // upper-left

QString key = TokenManager::overrideFor(dispName);
key = key.isEmpty() ? canonicalTokenKey(dispName) : canonicalTokenKey(key);
m_charTokenKeys.emplace_back(key);
}

if (beacon) {
drawQuadCommon(a, b, c, d, QuadOptsEnum::BEACON);
}
Expand Down Expand Up @@ -312,6 +352,37 @@ void CharacterBatch::CharFakeGL::reallyDrawCharacters(OpenGL &gl, const MapCanva
gl.renderColoredQuads(m_charBeaconQuads, blended_noDepth.withCulling(CullingEnum::FRONT));
}

// ── draw map-tokens underneath the coloured overlay ──
for (size_t q = 0; q < m_charTokenKeys.size(); ++q) {
const size_t base = q * 4;
if (base + 3 >= m_charTokenQuads.size())
break;

const QString &key = m_charTokenKeys[q];
MMTextureId id = tokenManager().textureIdFor(key);

if (id == INVALID_MM_TEXTURE_ID) {
QPixmap px = tokenManager().getToken(key);
id = tokenManager().uploadNow(key, px);
}

if (id == INVALID_MM_TEXTURE_ID)
continue;

SharedMMTexture tex = tokenManager().textureById(id);
if (tex)
gl.setTextureLookup(id, tex);

if (id == INVALID_MM_TEXTURE_ID)
continue;

gl.renderColoredTexturedQuads({m_charTokenQuads[base + 0],
m_charTokenQuads[base + 1],
m_charTokenQuads[base + 2],
m_charTokenQuads[base + 3]},
blended_noDepth.withTexture0(id));
}

if (!m_charRoomQuads.empty()) {
gl.renderColoredTexturedQuads(m_charRoomQuads,
blended_noDepth.withTexture0(textures.char_room_sel->getId()));
Expand All @@ -335,6 +406,9 @@ void CharacterBatch::CharFakeGL::reallyDrawCharacters(OpenGL &gl, const MapCanva
gl.renderFont3d(textures.char_arrows, m_screenSpaceArrows);
m_screenSpaceArrows.clear();
}

m_charTokenQuads.clear();
m_charTokenKeys.clear();
}

void CharacterBatch::CharFakeGL::reallyDrawPaths(OpenGL &gl)
Expand Down Expand Up @@ -383,9 +457,16 @@ void MapCanvas::paintCharacters()
}

CharacterBatch characterBatch{m_mapScreen, m_currentLayer, getTotalScaleFactor()};
const CGroupChar *playerChar = nullptr;
for (const auto &pCharacter : m_groupManager.selectAll()) {
if (pCharacter->isYou()) { // ← ‘isYou()’ marks the local player
playerChar = pCharacter.get();
break;
}
}

// IIFE to abuse return to avoid duplicate else branches
[this, &characterBatch]() {
[this, &characterBatch, playerChar]() {
if (const std::optional<RoomId> opt_pos = m_data.getCurrentRoomId()) {
const auto &id = opt_pos.value();
if (const auto room = m_data.findRoomHandle(id)) {
Expand All @@ -397,7 +478,11 @@ void MapCanvas::paintCharacters()

// paint char current position
const Color color{getConfig().groupManager.color};
characterBatch.drawCharacter(pos, color);
characterBatch
.drawCharacter(pos,
color,
/*fill =*/true,
/*name =*/playerChar ? playerChar->getDisplayName() : QString());

// paint prespam
const auto prespam = m_data.getPath(id, m_prespammedPath.getQueue());
Expand All @@ -417,40 +502,73 @@ void MapCanvas::paintCharacters()

void MapCanvas::drawGroupCharacters(CharacterBatch &batch)
{
if (m_data.isEmpty()) {
if (m_data.isEmpty())
return;
}

RoomIdSet drawnRoomIds;
const Map &map = m_data.getCurrentMap();
for (const auto &pCharacter : m_groupManager.selectAll()) {
// Omit player so that they know group members are below them
if (pCharacter->isYou())
continue;

const CGroupChar &character = deref(pCharacter);
/* Find the player's room once */
const CGroupChar *playerChar = nullptr;
ServerRoomId playerRoomSid = INVALID_SERVER_ROOMID;
RoomId playerRoomId = INVALID_ROOMID;
for (const auto &p : m_groupManager.selectAll()) {
if (p->isYou()) {
playerChar = p.get();
playerRoomSid = p->getServerId();
if (const auto r = map.findRoomHandle(p->getServerId()))
playerRoomId = r.getId();
break;
}
}

const auto &r = [&character, &map]() -> RoomHandle {
const ServerRoomId srvId = character.getServerId();
if (srvId != INVALID_SERVER_ROOMID) {
if (const auto &room = map.findRoomHandle(srvId)) {
return room;
}
}
return RoomHandle{};
}();
RoomIdSet drawnRoomIds;

// Do not draw the character if they're in an "Unknown" room
if (!r) {
for (const auto &pCharacter : m_groupManager.selectAll()) {
const CGroupChar &character = *pCharacter;
if (character.isYou())
continue;
}
const auto r = map.findRoomHandle(character.getServerId());
if (!r)
continue; // skip Unknown rooms

const RoomId id = r.getId();
const auto &pos = r.getPosition();
const auto color = Color{character.getColor()};
const Coordinate &pos = r.getPosition();
const Color col = Color{character.getColor()};
const bool fill = !drawnRoomIds.contains(id);

batch.drawCharacter(pos, color, fill);
const bool showToken = (id != playerRoomId);

const QString tokenKey = showToken ? character.getDisplayName()
: QString(); // empty → no token

batch.drawCharacter(pos, col, fill, tokenKey);
drawnRoomIds.insert(id);
}
/* ---------- draw (and purge) ghost tokens ------------------------------ */
if (!getConfig().groupManager.showNpcGhosts) {
g_ghosts.clear(); // purge any stale registry entries
} else
for (auto it = g_ghosts.begin(); it != g_ghosts.end(); /* ++ in body */) {
ServerRoomId ghostSid = it->first; // map key is the room id
const QString tokenKey = it->second.tokenKey;

if (ghostSid == playerRoomSid) { // player in same room → purge
it = g_ghosts.erase(it);
continue;
}

/* use ghostSid here ▾ instead of ghostInfo.roomSid */
if (const auto h = map.findRoomHandle(ghostSid)) {
const Coordinate &pos = h.getPosition();

QColor tint(Qt::white);
tint.setAlphaF(0.50f);
Color col(tint);

const bool fill = !drawnRoomIds.contains(h.getId());
batch.drawCharacter(pos, col, fill, tokenKey /*, 0.9f */);
drawnRoomIds.insert(h.getId());
}
++it;
}
}
Loading