diff --git a/README.md b/README.md
index 8d238e117..546c07ee0 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@ MMapper
[](https://codecov.io/gh/MUME/MMapper)
[](https://github.com/MUME/MMapper/blob/master/COPYING.txt)
[](https://snapcraft.io/mmapper)
-[](https://flathub.org/apps/org.mume.MMapper)
[
Download the latest version of MMapper](https://github.com/MUME/MMapper/releases)
@@ -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
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 585e85b69..f9d4ed37b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -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
@@ -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)
diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp
index 1541e3de1..2cc148e6d 100644
--- a/src/configuration/configuration.cpp
+++ b/src/configuration/configuration.cpp
@@ -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";
@@ -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(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)
@@ -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
diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h
index 58d1d1e9f..f7f6fe0c6 100644
--- a/src/configuration/configuration.h
+++ b/src/configuration/configuration.h
@@ -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 tokenOverrides;
private:
SUBGROUP();
diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp
index cf0ea2fd0..2bc268878 100644
--- a/src/display/Characters.cpp
+++ b/src/display/Characters.cpp
@@ -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"
@@ -24,8 +26,11 @@
#include
+#include
#include
+std::unordered_map 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;
@@ -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;
@@ -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,
@@ -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?
@@ -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);
}
@@ -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()));
@@ -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)
@@ -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 opt_pos = m_data.getCurrentRoomId()) {
const auto &id = opt_pos.value();
if (const auto room = m_data.findRoomHandle(id)) {
@@ -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());
@@ -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;
+ }
}
diff --git a/src/display/Characters.h b/src/display/Characters.h
index da59bb5ab..fde011808 100644
--- a/src/display/Characters.h
+++ b/src/display/Characters.h
@@ -102,6 +102,8 @@ class NODISCARD CharacterBatch final
MatrixStack m_stack;
std::vector m_charTris;
std::vector m_charBeaconQuads;
+ std::vector m_charTokenQuads;
+ std::vector m_charTokenKeys;
std::vector m_charLines;
std::vector m_pathPoints;
std::vector m_pathLineVerts;
@@ -149,7 +151,8 @@ class NODISCARD CharacterBatch final
m = glm::translate(m, v);
}
void drawArrow(bool fill, bool beacon);
- void drawBox(const Coordinate &coord, bool fill, bool beacon, bool isFar);
+ void drawBox(
+ const Coordinate &coord, bool fill, bool beacon, bool isFar, const QString &dispName);
void addScreenSpaceArrow(const glm::vec3 &pos, float degrees, const Color &color, bool fill);
// with blending, without depth; always size 4
@@ -215,7 +218,10 @@ class NODISCARD CharacterBatch final
NODISCARD bool isVisible(const Coordinate &c, float margin) const;
public:
- void drawCharacter(const Coordinate &coordinate, const Color &color, bool fill = true);
+ void drawCharacter(const Coordinate &coordinate,
+ const Color &color,
+ bool fill = true,
+ const QString &dispName = QString());
void drawPreSpammedPath(const Coordinate &coordinate,
const std::vector &path,
diff --git a/src/display/GhostRegistry.h b/src/display/GhostRegistry.h
new file mode 100644
index 000000000..d90acb685
--- /dev/null
+++ b/src/display/GhostRegistry.h
@@ -0,0 +1,13 @@
+#pragma once
+#include "../map/roomid.h" // ServerRoomId
+
+#include
+
+#include
+
+struct GhostInfo
+{
+ QString tokenKey; // icon to draw (display name)
+};
+
+extern std::unordered_map g_ghosts;
diff --git a/src/group/CGroupChar.cpp b/src/group/CGroupChar.cpp
index 832adcc59..c22e15451 100644
--- a/src/group/CGroupChar.cpp
+++ b/src/group/CGroupChar.cpp
@@ -326,3 +326,13 @@ bool CGroupChar::setScore(const QString &textHP, const QString &textMana, const
#undef X_SCORE
return updated;
}
+
+QString CGroupChar::getDisplayName() const
+{
+ if (getLabel().isEmpty()
+ || getName().getStdStringViewUtf8() == getLabel().getStdStringViewUtf8()) {
+ return getName().toQString();
+ } else {
+ return QString("%1 (%2)").arg(getName().toQString(), getLabel().toQString());
+ }
+}
diff --git a/src/group/CGroupChar.h b/src/group/CGroupChar.h
index c93b72ad2..59077c11a 100644
--- a/src/group/CGroupChar.h
+++ b/src/group/CGroupChar.h
@@ -61,6 +61,9 @@ class NODISCARD CGroupChar final : public std::enable_shared_from_this
#include