mudlet/src/discord.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

751 lines
26 KiB
C++
Raw Permalink Normal View History

2018-10-05 06:25:57 +02:00
/***************************************************************************
* Copyright (C) 2018 by Vadim Peretokin - vperetokin@gmail.com *
* Copyright (C) 2018-2019, 2022 by Stephen Lyons *
* - slysven@virginmedia.com *
2018-10-05 06:25:57 +02:00
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "discord.h"
#include "mudlet.h"
#include "utils.h"
2018-10-05 06:25:57 +02:00
#include <QtDebug>
#include <QHash>
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
#include <chrono>
using namespace std::chrono_literals;
2018-10-05 06:25:57 +02:00
// Uncomment this to provide some additional qDebug() output:
// #define DEBUG_DISCORD 1
2018-10-05 06:25:57 +02:00
QString Discord::smUserName;
QString Discord::smUserId;
QString Discord::smAvatar;
const QString Discord::mMudletApplicationId = qsl("450571881909583884");
2018-10-05 06:25:57 +02:00
Discord::Discord(QObject* parent)
: QObject(parent)
// For details see https://discord.com/developers/docs/rich-presence/how-to#initialization
2018-10-05 06:25:57 +02:00
// Initialise with a nullptr one with Mudlet's own ID
// NB: for testing the following MUDs have registered:
2018-10-05 06:25:57 +02:00
// "midmud" is "460618737712889858", has "server-icon", "exventure" and "mudlet" icons
// "carinus" is "438335628942376960", has "server-icon" and "mudlet" icons
// "wotmud" is "464945517156106240", has "mudlet", "ajar_(red|green|yellow|blue|white|grey|brown)"
, mHostApplicationIDs{{nullptr, mMudletApplicationId}} // lowercase list of known games
// {game name, {game addresses}}
, mKnownGames{
{"midmud", {"midmud.com"}},
{"wotmud", {"game.wotmud.org"}},
{"luminari", {"luminarimud.com"}},
{"achaea", {"achaea.com", "iron-ach.ironrealms.com"}},
{"aetolia", {"aetolia.com", "iron-aet.ironrealms.com"}},
{"imperian", {"imperian.com", "iron-imp.ironrealms.com"}},
2018-10-05 06:25:57 +02:00
{"lusternia", {"lusternia.com", "iron-lus.ironrealms.com"}},
{"starmourn", {"starmourn.com"}},
{"stickmud", {"stickmud.com"}},
{"clessidra", {"clessidra.it", "mud.clessidra.it"}},
{"mume", {"mume.org"}},
{"asteria", {"asteriamud.com"}},
}
2018-10-05 06:25:57 +02:00
{
#if defined(Q_OS_WIN64)
// Only defined on 64 bit Windows
mpLibrary.reset(new QLibrary(qsl("discord-rpc64")));
#elif defined(Q_OS_WINDOWS)
// Defined on both 32 and 64 bit Windows
mpLibrary.reset(new QLibrary(qsl("discord-rpc32")));
#else
// All other OSes
mpLibrary.reset(new QLibrary(qsl("discord-rpc")));
#endif
2018-10-05 06:25:57 +02:00
using Discord_InitializePrototype = void (*)(const char*, DiscordEventHandlers*, int, const char*);
2018-10-05 06:25:57 +02:00
using Discord_UpdatePresencePrototype = void (*)(const DiscordRichPresence*);
using Discord_RunCallbacksPrototype = void (*)();
using Discord_ShutdownPrototype = void (*)();
Discord_Initialize = reinterpret_cast<Discord_InitializePrototype>(mpLibrary->resolve("Discord_Initialize"));
Discord_UpdatePresence = reinterpret_cast<Discord_UpdatePresencePrototype>(mpLibrary->resolve("Discord_UpdatePresence"));
Discord_RunCallbacks = reinterpret_cast<Discord_RunCallbacksPrototype>(mpLibrary->resolve("Discord_RunCallbacks"));
Discord_Shutdown = reinterpret_cast<Discord_ShutdownPrototype>(mpLibrary->resolve("Discord_Shutdown"));
if (!mpLibrary->isLoaded() || !Discord_Initialize || !Discord_UpdatePresence || !Discord_RunCallbacks || !Discord_Shutdown) {
const auto msg = mpLibrary->errorString();
auto notFound = msg.contains(qsl("not found")) || msg.contains(qsl("No such file or directory"));
qDebug().nospace() << "Could not " << (notFound ? "find" : "load") << " Discord library - searched in:";
for (const auto& libraryPath : qApp->libraryPaths()) {
2018-10-05 06:25:57 +02:00
qDebug() << " " << libraryPath;
}
if (!msg.isEmpty() && !notFound) {
qDebug().noquote().nospace() << " error: \"" << msg << "\".";
}
2018-10-05 06:25:57 +02:00
return;
}
mLoaded = true;
qDebug() << "Discord integration loaded. Using functions from:" << mpLibrary.data()->fileName();
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
mpHandlers = std::make_unique<DiscordEventHandlers>();
memset(mpHandlers.get(), 0, sizeof(DiscordEventHandlers));
2018-10-05 06:25:57 +02:00
mpHandlers->ready = handleDiscordReady;
mpHandlers->errored = handleDiscordError;
mpHandlers->disconnected = handleDiscordDisconnected;
mpHandlers->joinGame = handleDiscordJoinGame;
mpHandlers->spectateGame = handleDiscordSpectateGame;
mpHandlers->joinRequest = handleDiscordJoinRequest;
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
// Don't initialize RPC until a profile is loaded - UpdatePresence will
// call initializeRpc() on demand when there's an active host.
2018-10-05 06:25:57 +02:00
// mudlet instance is not available in this constructor as it's still being initialised, so postpone the connection
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTimer::singleShot(0ms, this, [this]() {
2018-10-05 06:25:57 +02:00
Q_ASSERT(mudlet::self());
connect(mudlet::self(), &mudlet::signal_tabChanged, this, &Discord::UpdatePresence);
// process Discord callbacks every 50ms once we are all set up:
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
startTimer(50ms);
2018-10-05 06:25:57 +02:00
});
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
void Discord::initializeRpc()
{
if (!mLoaded || mRpcActive) {
return;
}
mCurrentApplicationId = mHostApplicationIDs.value(nullptr);
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
Discord_Initialize(mCurrentApplicationId.toUtf8().constData(), mpHandlers.get(), 0, nullptr);
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
mRpcActive = true;
}
void Discord::shutdownRpc()
{
if (!mRpcActive) {
return;
}
Discord_Shutdown();
mRpcActive = false;
mCurrentApplicationId.clear();
}
2018-10-05 06:25:57 +02:00
Discord::~Discord()
{
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (mRpcActive) {
2018-10-05 06:25:57 +02:00
Discord_Shutdown();
// We might expect to have to do an mpLibrary->unload() but we do not
// need to as it happens automagically on the application shutdown...
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
}
2018-10-05 06:25:57 +02:00
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
// Clear out the localDiscordPresence collection:
mPresencePtrs.clear();
2018-10-05 06:25:57 +02:00
}
// For all the setters below the caller is supposed to check that they have the
// permission to do the operation
void Discord::setDetailText(Host* pHost, const QString& text)
{
if (!text.isEmpty()) {
mDetailTexts[pHost] = text;
} else {
mDetailTexts[pHost] = tr("via Mudlet");
}
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setStateText(Host* pHost, const QString& text)
{
mStateTexts[pHost] = text;
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setLargeImage(Host* pHost, const QString& text)
{
mLargeImages[pHost] = text;
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setLargeImageText(Host* pHost, const QString& text)
{
mLargeImageTexts[pHost] = text;
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setSmallImage(Host* pHost, const QString& text)
{
mSmallImages[pHost] = text;
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setSmallImageText(Host* pHost, const QString& text)
{
mSmallImageTexts[pHost] = text;
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setStartTimeStamp(Host* pHost, int64_t epochTimeStamp)
{
mStartTimes[pHost] = epochTimeStamp;
mEndTimes.remove(pHost);
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setEndTimeStamp(Host* pHost, int64_t epochTimeStamp)
{
mEndTimes[pHost] = epochTimeStamp;
mStartTimes.remove(pHost);
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setParty(Host* pHost, int partySize)
{
const int validPartySize = qMax(0, partySize);
2018-10-05 06:25:57 +02:00
if (validPartySize) {
// Is more than zero:
if (mPartyMax.value(pHost) < validPartySize) {
mPartyMax[pHost] = validPartySize;
}
mPartySize[pHost] = validPartySize;
} else if (mPartyMax.contains(pHost)) {
// There is a max size set - so zero this value
mPartySize[pHost] = 0;
} else {
// There isn't a party size set so remove this (zero) value
mPartySize.remove(pHost);
}
if (mLoaded) {
UpdatePresence();
}
}
void Discord::setParty(Host* pHost, int partySize, int partyMax)
{
const int validPartySize = qMax(0, partySize);
const int validPartyMax = qMax(0, partyMax);
2018-10-05 06:25:57 +02:00
if (validPartyMax) {
// We have a party max size that is a positive number - so use the
// largest of it and the size as the maximum:
mPartyMax[pHost] = qMax(validPartySize, validPartyMax);
mPartySize[pHost] = validPartySize;
} else {
// We have explicitly set the party maximum size to 0 (or less) - so
// clear things:
mPartySize.remove(pHost);
mPartyMax.remove(pHost);
}
if (mLoaded) {
UpdatePresence();
}
}
void Discord::timerEvent(QTimerEvent* event)
{
Q_UNUSED(event)
2018-10-05 06:25:57 +02:00
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (mLoaded && mRpcActive) {
2018-10-05 06:25:57 +02:00
Discord_RunCallbacks();
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (mPendingPresenceUpdate) {
mPendingPresenceUpdate = false;
UpdatePresence();
}
2018-10-05 06:25:57 +02:00
}
}
void Discord::handleDiscordReady(const DiscordUser* request)
{
Discord::smUserName = request->username;
Discord::smUserId = request->userId;
Discord::smAvatar = request->avatar;
2018-10-05 06:25:57 +02:00
#if defined(DEBUG_DISCORD)
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
qDebug().noquote().nospace() << "Discord Ready callback received - for UserName: \"" << smUserName << "\", ID: \"" << smUserId << "\".";
#endif
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
// Don't call UpdatePresence directly from here - re-entering the Discord
// library from a callback freezes Mudlet. Instead, signal the timer to
// pick it up on the next tick.
mudlet::self()->mDiscord.mPendingPresenceUpdate = true;
2018-10-05 06:25:57 +02:00
}
void Discord::handleDiscordDisconnected(int errorCode, const char* message)
{
qWarning() << "Discord disconnected - code:" << errorCode << "message:" << message;
}
void Discord::handleDiscordError(int errorCode, const char* message)
{
qWarning() << "Discord error - code:" << errorCode << "message:" << message;
}
void Discord::handleDiscordJoinGame(const char* joinSecret)
{
qDebug() << "Discord JoinGame received with secret:" << joinSecret;
}
void Discord::handleDiscordSpectateGame(const char* spectateSecret)
{
qDebug() << "Discord SpectateGame received with secret:" << spectateSecret;
}
void Discord::handleDiscordJoinRequest(const DiscordUser* request)
{
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
qDebug() << "Discord JoinRequest received from user:" << request->username << "userId:" << request->userId << "avatar:" << request->avatar;
2018-10-05 06:25:57 +02:00
}
void Discord::UpdatePresence()
{
if (!mLoaded) {
return;
}
auto pHost = mudlet::self()->getActiveHost();
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
// Don't send any presence when no profile is active or Discord is
// disabled - showing "Playing Mudlet" would leak information when the
// user hasn't opted in (see issue #6967)
if (!pHost || pHost->mDiscordMode == Host::DiscordDisabled) {
if (mRpcActive) {
shutdownRpc();
}
return;
}
if (!mRpcActive) {
initializeRpc();
// Don't send presence yet - wait for the handleDiscordReady callback
// to signal that the IPC handshake is complete
2018-10-05 06:25:57 +02:00
return;
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (pHost->mDiscordMode == Host::DiscordShowMudletOnly) {
// Ensure we're using the default Mudlet application ID
if (mCurrentApplicationId != mHostApplicationIDs.value(nullptr)) {
shutdownRpc();
initializeRpc();
return;
}
}
if (!pHost->discordUserIdMatch(Discord::smUserName)) {
#if defined(DEBUG_DISCORD)
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
qDebug().nospace().noquote() << "Discord::UpdatePresence() INFO - Discord UserName does not match, not sending this update!";
#endif
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (mRpcActive) {
shutdownRpc();
}
2018-10-05 06:25:57 +02:00
return;
}
// Need to establish which presence to use - will be null if it has not been overridden:
QString applicationID = mHostApplicationIDs.value(pHost);
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
if (mPresencePtrs.empty()) {
2018-10-05 06:25:57 +02:00
// First time only - with no localDiscordPresence in collection,
// must just create the default one:
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
mPresencePtrs.emplace(QString(), std::make_unique<localDiscordPresence>());
2018-10-05 06:25:57 +02:00
}
// If the localDiscordPresence applicationID is NOT present in the existing
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
// map then this will return a nullptr:
2018-10-05 06:25:57 +02:00
localDiscordPresence* pDiscordPresence = nullptr;
if (applicationID.isEmpty()) {
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
auto it = mPresencePtrs.find(QString());
pDiscordPresence = (it != mPresencePtrs.end()) ? it->second.get() : nullptr;
2018-10-05 06:25:57 +02:00
// Reset the empty applicationID to the one that belongs to Mudlet:
applicationID = mHostApplicationIDs.value(nullptr);
Q_ASSERT_X(pDiscordPresence, "Discord", "no Discord presence available for Mudlets default presence");
2018-10-05 06:25:57 +02:00
} else {
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
auto it = mPresencePtrs.find(applicationID);
pDiscordPresence = (it != mPresencePtrs.end()) ? it->second.get() : nullptr;
2018-10-05 06:25:57 +02:00
if (!pDiscordPresence) {
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
auto [newIt, inserted] = mPresencePtrs.emplace(applicationID, std::make_unique<localDiscordPresence>());
pDiscordPresence = newIt->second.get();
2018-10-05 06:25:57 +02:00
}
}
if (mCurrentApplicationId != applicationID) {
#if defined(DEBUG_DISCORD)
qDebug().nospace().noquote() << "Discord::UpdatePresence() INFO - mCurrentApplicationId (\"" << mCurrentApplicationId << "\") does not match the one for this Host instance (\""
<< applicationID << "\"), restarting RPC library with the latter.";
#endif
2018-10-05 06:25:57 +02:00
Discord_Shutdown();
improve: improve memory safety by using smart pointers (#9239) ### Refactor: replace raw pointer ownership with smart pointers across core subsystems #### Brief overview of PR changes/additions Replaces raw pointer ownership patterns with `std::unique_ptr` and `std::map` across several core subsystems: - **Host**: `mStopWatchMap` (`QMap<int, stopWatch*>` → `std::map<int, unique_ptr<stopWatch>>`), `profileShortcuts` (`QMap<QString, QKeySequence*>` → `std::map<QString, unique_ptr<QKeySequence>>`). Removes `qDeleteAll` in destructor and `delete mMMCPServer`. - **TMap**: `mpRoomDB` raw pointer → `unique_ptr` - **VarUnit**: `base` raw pointer → `unique_ptr` - **TTrigger**: condition map storage converted to `unique_ptr`, destructor simplified - **discord**: handler and presence maps converted from raw pointer `QMap` to `unique_ptr` + `std::map` - **Updater**: `mFeed` and `mUpdateDialog` converted to `unique_ptr` #### Motivation for adding to Mudlet These patterns were identified as sources of memory leaks and potential use-after-free bugs. Using smart pointers makes ownership explicit, eliminates manual cleanup code, and ensures correct destruction even on early-exit paths. #### Other info (issues closed, discussion etc) sorry this one is still pretty big, but most of the changes are the same for each thing so reviewing them together probably makes sense. sadly there isn't much to see here other than no slow uptick of heap size :-[ Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:09 +12:00
Discord_Initialize(applicationID.toUtf8().constData(), mpHandlers.get(), 0, nullptr);
2018-10-05 06:25:57 +02:00
mCurrentApplicationId = applicationID;
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
// Wait for the ready callback before sending presence
return;
2018-10-05 06:25:57 +02:00
}
BugFix: attempt to fix some High and Medium Impact Coverity Issues (#3837) Classed as "High Impact": CID Type Detail 1492499 "Uninitialized scalar variable (UNINIT) "5. uninit_use: Using uninitialized value error." 1485860 "No virtual destructor" "A1. dtor_in_derived: Class `XMLimport` has a compiler-generated destructor. It is non-empty because of its field `mpHost`. A pointer to class `XMLimport` is upcast to class `QXmlStreamReader` which doesn't have a virtual destructor." Classed as "Medium Impact": 1492834 "Uninitialized scalar field (UNINIT_CTOR)" "2. uninit_member: Non-static class member `mIsEndTag` is not initialized in this constructor nor in any functions that it calls. 4. uninit_member: Non-static class member `mIsEmptyTag` is not initialized in this constructor nor in any functions that it calls. 6. uninit_member: Non-static class member `mReadingAttrValue` is not initialized in this constructor nor in any functions that it calls. 8. uninit_member: Non-static class member `mOpeningQuote` is not initialized in this constructor nor in any functions that it calls." 1488910 "Uninitialized scalar field (UNINIT_CTOR)" "2. uninit_member: Non-static class member `mPlayerRoomStyle` is not initialized in this constructor nor in any functions that it calls." "4. uninit_member: Non-static class member `mPlayerRoomOuterDiameterPercentage` is not initialized in this constructor nor in any functions that it calls." "6. uninit_member: Non-static class member `mPlayerRoomInnerDiameterPercentage` is not initialized in this constructor nor in any functions that it calls." 1478854 "Uninitialized pointer field (UNINIT_CTOR)" "4. uninit_member: Non-static class member `mpOutOfBandDataIncomingCodec` is not initialized in this constructor nor in any functions that it calls." 1468478 "Unchecked return value (CHECKED_RETURN)" "10. check_return: Calling `luaL_loadstring` without checking return value (as is done elsewhere 17 out of 21 times)." 1468477 "Unchecked return value (CHECKED_RETURN)" "14. check_return: Calling `luaL_loadstring` without checking return value (as is done elsewhere 17 out of 21 times)." 1468474 "Unchecked return value (CHECKED_RETURN)" "16. check_return: Calling `luaL_loadstring` without checking return value (as is done elsewhere 17 out of 21 times)." - x 2 1468468 "Logically dead code (DEADCODE)" "dead_error_line: Execution cannot reach this statement: `return 1;`" 1415097 "Dereference null return value (NULL_RETURNS)" "8. dereference: Dereferencing timer, which is known to be `nullptr`" 1415092 "Identical code for different branches (IDENTICAL_BRANCHES)" "identical_branches: The same code is executed regardless of whether `areaExit` is true, because the 'then' and 'else' branches are identical. Should one of the branches be modified, or the entire 'if' statement replaced?" 1415023 "Dereference null return value (NULL_RETURNS)" "26. dereference: Dereferencing a pointer that might be `nullptr` `pR->name` when calling `QString`. 1414989 "Explicit null dereferenced (FORWARD_NULL)" "81. var_deref_op: Dereferencing null pointer `this->originalExits.value(dirCode, TExit * const(NULL))`." x 11 1414977 "Logically dead code (DEADCODE)" "dead_error_line: Execution cannot reach this statement: `return false;`." Also removed unused: * (int) cTelnet::curX & curY, * (double) cTelnet::networkLatencyMin & networkLatencyMax * (QMutex) TimerUnit::mTimerUnitLock Signed-off-by: Stephen Lyons <slysven@virginmedia.com>
2020-06-01 17:35:35 +01:00
if (!pDiscordPresence) {
qCritical().noquote() << "Discord::UpdatePresence() CRITICAL - pDiscordPresence is unexpectedly a nullptr, unable to proceed with this procedure, please report this to Mudlet Makers!";
return;
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
// Helper to decide if a field should be shown. Server-origin fields are
// subject to mode and privacy flags; Lua-origin fields always pass.
const bool isShowGameDetails = (pHost->mDiscordMode == Host::DiscordShowGameDetails);
const auto shouldShow = [&](Host::DiscordOptionFlag flag) -> bool {
if (!isServerOrigin(pHost, flag)) {
return true;
}
// Server-origin: only show in ShowGameDetails mode when the privacy flag allows it
return isShowGameDetails && (pHost->mDiscordAccessFlags & flag);
};
if (shouldShow(Host::DiscordSetDetail)) {
2018-10-05 06:25:57 +02:00
pDiscordPresence->setDetailText(mDetailTexts.value(pHost));
} else {
pDiscordPresence->setDetailText(QString());
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (shouldShow(Host::DiscordSetState)) {
2018-10-05 06:25:57 +02:00
pDiscordPresence->setStateText(mStateTexts.value(pHost));
} else {
pDiscordPresence->setStateText(QString());
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (shouldShow(Host::DiscordSetLargeIcon)) {
2018-10-05 06:25:57 +02:00
auto image = mLargeImages.value(pHost);
if (image.isEmpty() && applicationID == mMudletApplicationId) {
image = qsl("mudlet");
2018-10-05 06:25:57 +02:00
}
pDiscordPresence->setLargeImageKey(image);
} else {
pDiscordPresence->setLargeImageKey(QString());
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (shouldShow(Host::DiscordSetLargeIconText)) {
2018-10-05 06:25:57 +02:00
pDiscordPresence->setLargeImageText(mLargeImageTexts.value(pHost));
} else {
pDiscordPresence->setLargeImageText(QString());
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (shouldShow(Host::DiscordSetSmallIcon)) {
2018-10-05 06:25:57 +02:00
pDiscordPresence->setSmallImageKey(mSmallImages.value(pHost));
} else {
pDiscordPresence->setSmallImageKey(QString());
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (shouldShow(Host::DiscordSetSmallIconText)) {
2018-10-05 06:25:57 +02:00
pDiscordPresence->setSmallImageText(mSmallImageTexts.value(pHost));
} else {
pDiscordPresence->setSmallImageText(QString());
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (shouldShow(Host::DiscordSetPartyInfo) && mPartyMax.value(pHost)) {
2018-10-05 06:25:57 +02:00
pDiscordPresence->setPartySize(mPartySize.value(pHost));
pDiscordPresence->setPartyMax(mPartyMax.value(pHost));
} else {
pDiscordPresence->setPartySize(0);
pDiscordPresence->setPartyMax(0);
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
if (shouldShow(Host::DiscordSetTimeInfo)) {
2018-10-05 06:25:57 +02:00
if (mEndTimes.value(pHost)) {
pDiscordPresence->setEndTimeStamp(mEndTimes.value(pHost));
pDiscordPresence->setStartTimeStamp(0);
} else {
pDiscordPresence->setEndTimeStamp(0);
pDiscordPresence->setStartTimeStamp(mStartTimes.value(pHost, 0));
}
} else {
pDiscordPresence->setEndTimeStamp(0);
pDiscordPresence->setStartTimeStamp(0);
}
#if defined(DEBUG_DISCORD)
qDebug().nospace().noquote() << "Discord::UpdatePresence() INFO - sending update:\n" << *pDiscordPresence;
#endif
DiscordRichPresence const convertedPresence(pDiscordPresence->convert());
2018-10-05 06:25:57 +02:00
Discord_UpdatePresence(&convertedPresence);
}
QString Discord::deduceGameName(const QString& address)
{
// Handle using localhost as an off-line testing case
if (address == QLatin1String("localhost") || address == QLatin1String("127.0.0.1") || address == QLatin1String("::1")) {
return qsl("localhost");
2018-10-05 06:25:57 +02:00
}
// Handle the cases where the server url contains the "well-known" Server
// name - that being the key of the QHash mKnownGames:
if (mKnownGames.contains(address)) {
return address;
}
// Do a bit of URL processing on the (potentially) host url:
QString otherName;
switch (address.count(QChar('.'))) {
default:
// Too complex - abandon
qDebug().noquote().noquote() << "Discord::deduceGameName(\"" << address << "\") WARN - Unable to deduce MUD name from given address.";
break;
case 2: {
// three terms - assume last is a TLD so remove it but the first may be significant
QStringList fragments = address.split(QChar('.'));
fragments.removeLast();
otherName = fragments.join(QLatin1String("."));
if (otherName.startsWith(QLatin1String("game."))) {
2021-08-22 08:01:05 +02:00
// WoTMUD type case - so take remaining term in the middle of original
2018-10-05 06:25:57 +02:00
otherName = otherName.split(QChar('.')).last();
break;
}
if (otherName.startsWith(QLatin1String("www."))) {
// Error(?) in entering details so that a web-server name was given:
2018-10-05 06:25:57 +02:00
otherName = otherName.split(QChar('.')).last();
break;
}
}
otherName.clear();
break;
case 1:
// two terms - assume last is a TLD so remove it
otherName = address.split(QChar('.')).first();
break;
case 0:
// single term no need to split it
otherName = address;
break;
}
if (address.endsWith(qsl(".com"))) {
2018-10-05 06:25:57 +02:00
otherName = address.left(address.length() - 4);
} else if (address.endsWith(qsl(".de"))) {
2018-10-05 06:25:57 +02:00
// Handle avalon.de case
otherName = address.left(address.length() - 4);
}
// Handle the remaining cases where the known URL is something else - like
// say a fixed IP address stored as a member of the value for the QHash
// mKnownGames:
QHashIterator<QString, QVector<QString>> itServer(mKnownGames);
while (itServer.hasNext()) {
itServer.next();
QVectorIterator<QString> itUrl(itServer.value());
while (itUrl.hasNext()) {
if (itUrl.next().contains(address)) {
return itServer.key();
}
}
}
// This may be an empty string but it is the best guess otherwise:
return otherName;
}
// Returns true in First if this is a MUD we know about (and have an Icon for in
// on the Mudlet Discord server!) and the deduced name in Second - if the
2018-10-05 06:25:57 +02:00
// first is true.
QPair<bool, QString> Discord::gameIntegrationSupported(const QString& address)
{
const QString deducedName = deduceGameName(address);
2018-10-05 06:25:57 +02:00
// Handle using localhost as an off-line testing case
if (deducedName == QLatin1String("localhost")) {
return qMakePair(true, deducedName);
}
return qMakePair((!deducedName.isEmpty() && mKnownGames.contains(deducedName)), deducedName);
2018-10-05 06:25:57 +02:00
}
bool Discord::libraryLoaded()
{
return mLoaded;
}
// AFAICT A Discord Application Id is an unsigned long long int (a.k.a. a
// quint64, or qulonglong)
bool Discord::setApplicationID(Host* pHost, const QString& text)
{
const QString oldID = mHostApplicationIDs.value(pHost);
2018-10-05 06:25:57 +02:00
if (oldID == text) {
// No change so do nothing
return true;
}
// Note what the current app ID is for the given Host - will be an empty
// string if not overridden from the default Mudlet one:
if (text.isEmpty()) {
// An empty or null string is the signal to switch back to default
// "Mudlet" presence - and always succeeds
mHostApplicationIDs.remove(pHost);
pHost->setDiscordApplicationID(QString());
UpdatePresence();
return true;
}
bool ok = false;
if (text.toLongLong(&ok) && ok) {
// Got something that makes a non-zero number - so assume it is ok
mHostApplicationIDs[pHost] = text;
pHost->setDiscordApplicationID(text);
UpdatePresence();
return true;
}
return false;
2018-10-05 06:25:57 +02:00
}
void Discord::resetData(Host* pHost)
{
mStartTimes.remove(pHost);
mEndTimes.remove(pHost);
Fix: small memory leaks when closing/reopening profiles (#9110) <!-- Keep the title short & concise so anyone non-technical can understand it, the title appears in PTB changelogs --> #### Brief overview of PR changes/additions 1. Host::mStopWatchMap not cleaned in destructor ``` createStopWatch() [TLuaInterpreterMudletObjects.cpp:256] → Host::createStopWatch() [Host.cpp:1410] → new stopWatch() [Host.cpp:1433] → mStopWatchMap.insert() [Host.cpp:1439] mudlet::closeHost() [mudlet.cpp:1970] → HostManager::deleteHost() [HostManager.cpp:31] → mHostPool.remove() [HostManager.cpp:42] → ~Host() [Host.cpp:418] → qDeleteAll(profileShortcuts) ← PRESENT [Host.cpp:428] → qDeleteAll(mStopWatchMap) ← MISSING ``` 2. Discord stale Host* map entries (dangling pointers) ``` setDiscord*() functions → insert into QMap<Host*, ...> [discord.h:251-260] (10 maps) Profile close → mudlet::closeHost() [mudlet.cpp:1970] → mHostManager.deleteHost() [mudlet.cpp:2020] → Host destroyed ← Discord::resetData() NEVER called (not connected to signal_hostDestroyed) ← 10 maps retain entries keyed by dangling Host* ``` 3. MMCPServer + MMCPClient leak ``` chatStartServer() → Host::initMMCPServer() [Host.cpp:3050] → mMMCPServer = new MMCPServer(this) [Host.cpp:3056] ← 'this' passed as arg only, NOT as QObject parent [MMCPServer.cpp:38] ← mMMCPServer is QPointer (non-owning) [Host.h:789] Profile close → ~Host() [Host.cpp:418] ← NEVER deletes mMMCPServer ← MMCPServer leaked with all connected MMCPClients ``` #### Motivation for adding to Mudlet Addressing memory leaks #### Other info (issues closed, discussion etc)
2026-03-26 06:55:19 +01:00
mDetailTexts.remove(pHost);
mStateTexts.remove(pHost);
mLargeImages.remove(pHost);
mLargeImageTexts.remove(pHost);
mSmallImages.remove(pHost);
mSmallImageTexts.remove(pHost);
mPartySize.remove(pHost);
mPartyMax.remove(pHost);
mHostApplicationIDs.remove(pHost);
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
mServerOriginFlags.remove(pHost);
UpdatePresence();
}
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
void Discord::setServerOrigin(Host* pHost, const Host::DiscordOptionFlag flag)
{
mServerOriginFlags[pHost] |= flag;
}
void Discord::clearServerOrigin(Host* pHost, const Host::DiscordOptionFlag flag)
{
mServerOriginFlags[pHost] &= ~flag;
}
bool Discord::isServerOrigin(Host* pHost, const Host::DiscordOptionFlag flag) const
{
return mServerOriginFlags.value(pHost, Host::DiscordNoOption) & flag;
}
2018-10-05 06:25:57 +02:00
// Returns Host set app ID or the default Mudlet one if none set for the
// specific Host:
QString Discord::getApplicationId(Host* pHost) const
{
return mHostApplicationIDs.value(pHost, mHostApplicationIDs.value(nullptr));
}
DiscordRichPresence localDiscordPresence::convert() const
{
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
// Discord RPC distinguishes between nullptr (field not set) and ""
// (field set to empty). Pass nullptr for empty strings so Discord
// hides the field rather than showing it as blank.
const auto nullIfEmpty = [](const char* str) -> const char* {
return (str && str[0] != '\0') ? str : nullptr;
};
return DiscordRichPresence{nullIfEmpty(mState),
nullIfEmpty(mDetails),
2018-10-05 06:25:57 +02:00
mStartTimestamp,
mEndTimestamp,
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
nullIfEmpty(mLargeImageKey),
nullIfEmpty(mLargeImageText),
nullIfEmpty(mSmallImageKey),
nullIfEmpty(mSmallImageText),
nullIfEmpty(mPartyId),
2018-10-05 06:25:57 +02:00
mPartySize,
mPartyMax,
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
nullIfEmpty(mMatchSecret),
nullIfEmpty(mJoinSecret),
nullIfEmpty(mSpectateSecret),
2018-10-05 06:25:57 +02:00
mInstance};
}
void localDiscordPresence::setDetailText(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mDetails, sizeof(mDetails), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
void localDiscordPresence::setStateText(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mState, sizeof(mState), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
void localDiscordPresence::setLargeImageText(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mLargeImageText, sizeof(mLargeImageText), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
void localDiscordPresence::setLargeImageKey(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mLargeImageKey, sizeof(mLargeImageKey), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
void localDiscordPresence::setSmallImageText(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mSmallImageText, sizeof(mSmallImageText), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
void localDiscordPresence::setSmallImageKey(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mSmallImageKey, sizeof(mSmallImageKey), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
void localDiscordPresence::setJoinSecret(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mJoinSecret, sizeof(mJoinSecret), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
void localDiscordPresence::setMatchSecret(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mMatchSecret, sizeof(mMatchSecret), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
void localDiscordPresence::setSpectateSecret(const QString& text)
{
const QByteArray utf8Data = text.toUtf8();
fix: profile close during a map operation, Discord presence truncation, and interrupting ttsSpeak() (#9686) #### Brief overview of PR changes/additions - Closing a profile no longer frees the map out from under a running import, export or download. `TMap` counts the operations that pump `qApp->processEvents()`, and `mudlet::closeHost()` - which one of those pumps is what delivers it - stops the operation and destroys the `Host` once it has unwound, instead of half way through it. - Discord presence fields keep their last character and are only ever cut between characters: each buffer is now the documented limit plus room for its terminator, and a new `utils::copyUtf8String()` walks the cut back to a character boundary. - An interrupting `ttsSpeak()` announces the utterance it starts, and the `Ready` an engine reports for the utterance it cut off no longer drains `ttsQueue()` over the top of the one the script asked for. #### Motivation for adding to Mudlet Each is a filed defect, and each was reproduced before it was fixed. The map one is a use-after-free: ASan reports `heap-use-after-free` inside `TMap::readJsonMapFile()`, freed by `~TMap` <- `~Host` <- `HostManager::deleteHost` <- `mudlet::closeHost` delivered by the import's own `processEvents()`. The Discord one is worse than one field looking wrong: a single over-long non-ASCII field makes the whole `SET_ACTIVITY` payload undecodable, so the entire presence update is discarded - the fake Discord client recorded exactly that. The TTS one silently drops speech: `ttsQueue()` plus an interrupting `ttsSpeak()` speaks the queued line and never speaks the requested one. #### Other info (issues closed, discussion etc) Closes #9520, closes #9634, closes #9659. `MapCloseDuringImportTest` stages the close through `mudlet::slot_closeProfileByName()` and lets the map operation's own pump deliver it; the functional tests build with ASan, so the pre-fix run is a sanitizer report rather than an inference. `TtsInterruptingSpeakTest` hands `ttsStateChanged()` the `Ready` a real engine sends, which Qt's mock engine never does - the mock-visible half is pinned in `Media_spec.lua`, where the two specs that recorded the old behaviour are updated. `Discord_spec.lua` gains four end-to-end specs against `CI/discord-ipc-fixture.py` asserting that the captured frame still decodes as JSON and that a field is cut on a character boundary, and `DiscordTest.cpp` covers the same at unit level. Every new or changed test was confirmed to fail without its fix. Two things deliberately left alone, both older than this PR: `Host::requestClose()` still runs nested inside the map operation's pump (it saves the profile there), and an XML import or a map download has no cancel to poll, so a close waits for it rather than stopping it. **Test case:** Export a large map with `exportJsonMap()` and close the profile's tab while it runs; then `setDiscordDetail(string.rep("ä", 65))` and confirm the presence still updates; then `ttsQueue("queued line") ttsSpeak("first")` followed immediately by `ttsSpeak("second")` and confirm "second" is what gets spoken. Assisted-by: Claude:claude-opus-5
2026-08-07 06:10:42 +02:00
utils::copyUtf8String(mSpectateSecret, sizeof(mSpectateSecret), utf8Data.constData(), utf8Data.size());
2018-10-05 06:25:57 +02:00
}
bool Discord::usingMudletsDiscordID(Host* pHost) const
{
return (!mHostApplicationIDs.contains(pHost));
}
bool Discord::discordUserIdMatch(Host* pHost) const
{
improve: Give players full control over Discord Rich Presence (#9116) ## Summary Players had no clear way to control what Discord shows about their Mudlet activity. The old checkbox in the connection pane only gated server GMCP data but didn't prevent Discord from showing "Playing Mudlet", and the privacy controls were confusing. This PR replaces all of that with three straightforward modes via radio buttons in Profile Preferences > Chat: - **Show full game details (if supported)** - full game integration with server-provided presence (default) - **Show Mudlet only** - only shows "playing Mudlet", game server is not told about Discord - **Disabled** - Discord shows nothing about Mudlet Players pick the mode that matches their comfort level, and the existing privacy checkboxes (hide detail, hide state, etc.) remain available in Game details mode for finer control. ### What changed - **Three-mode radio buttons** in Profile Preferences > Chat with a two-column layout (modes on the left, privacy controls on the right), replacing the old connection-pane checkbox - **Server-origin tracking** so privacy checkboxes only gate data sent by the game server - Lua API calls always pass through (only Disabled mode blocks Lua entirely) - **Mid-session mode switching** via dynamic GMCP negotiation (Core.Supports.Add/Remove + External.Discord.Hello/Get) - **Deferred RPC init** - Discord RPC now starts when a profile loads, not on app launch - **Username restriction improvements** - takes effect immediately, case-insensitive (Discord usernames are lowercase-only since 2023), shuts down RPC when mismatched - **Shows logged-in Discord user** in preferences next to the restriction field, with a tooltip explaining the desktop app requirement when not connected - **Presence fix** - empty string fields now send nullptr so Discord hides them instead of showing blanks - **Memory leak fix** - presence allocations are now freed in the destructor regardless of RPC state ### Cleanup - Removed obsolete discriminator field (`mRequiredDiscordUserDiscriminator`) - Discord removed discriminators in 2023 - Removed dead code (`getDiscordUserDetails()`, never called) - Restored `Discord_ClearPresence` function pointer for potential future use - Use proper `Host::DiscordOptionFlags` types instead of raw `int` (thanks @SlySven) ### Known quirks - The "Hide timer" checkbox correctly omits timestamps from presence data, but Discord's client starts its own activity timer for any presence without a timestamp - this is Discord client behavior outside our control. - The "Hide large icon" setting clears the image key, but some Discord clients fall back to the application's default icon instead of hiding it entirely. ### Test plan - [ ] Open Profile Preferences > Chat tab - [ ] Switch between the three radio button modes and verify Discord presence updates accordingly - [ ] In Game details mode, toggle privacy checkboxes and verify fields are hidden/shown - [ ] Use Lua API (e.g. `setDiscordDetail("test")`) in Mudlet only mode - should work. Try in Disabled mode - should fail with error - [ ] Set a username restriction and verify presence clears immediately if mismatched - [ ] Run unit tests: `cd build && ./test/DiscordTest` - [ ] Run functional tests: `cd build && ctest -R TDiscordModeTest -V` Closes #6967. Supersedes #7438. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: SlySven <slysven@virginmedia.com>
2026-04-08 13:33:09 +02:00
return pHost->discordUserIdMatch(Discord::smUserName);
2018-10-05 06:25:57 +02:00
}