2024-02-17 18:42:01 +01:00
/***************************************************************************
* Copyright ( C ) 2008 - 2013 by Heiko Koehn - KoehnHeiko @ googlemail . com *
* Copyright ( C ) 2013 - 2022 by Stephen Lyons - slysven @ virginmedia . com *
* Copyright ( C ) 2014 - 2017 by Ahmed Charles - acharles @ outlook . com *
* Copyright ( C ) 2016 by Eric Wallace - eewallace @ gmail . com *
* Copyright ( C ) 2016 by Chris Leacy - cleacy1972 @ gmail . com *
* Copyright ( C ) 2016 - 2018 by Ian Adkins - ieadkins @ gmail . com *
* Copyright ( C ) 2017 by Chris Reid - WackyWormer @ hotmail . com *
* Copyright ( C ) 2022 - 2023 by Lecker Kebap - Leris @ mudlet . org *
* *
* 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 . *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
// mapper-specific functions of TLuaInterpreter, split out separately
// for convenience and to keep TLuaInterpreter.cpp size reasonable
# include "TLuaInterpreter.h"
# include "EAction.h"
# include "Host.h"
# include "TAlias.h"
# include "TArea.h"
# include "TCommandLine.h"
# include "TConsole.h"
# include "TDebug.h"
# include "TEvent.h"
# include "TFlipButton.h"
# include "TForkedProcess.h"
# include "TLabel.h"
2026-01-08 21:04:24 +01:00
# include "TMap.h"
2024-02-17 18:42:01 +01:00
# include "TMapLabel.h"
# include "TMedia.h"
# include "TRoomDB.h"
# include "TTabBar.h"
# include "TTextEdit.h"
# include "TTimer.h"
# include "dlgComposer.h"
# include "dlgIRC.h"
# include "dlgMapper.h"
# include "dlgModuleManager.h"
# include "dlgTriggerEditor.h"
# include "mapInfoContributorManager.h"
# include "mudlet.h"
# if defined(INCLUDE_3DMAPPER)
Improve: add a new, experimental 3D mapper (#8087)
<!-- Keep the title short & concise so anyone non-technical can
understand it,
the title appears in PTB changelogs -->
#### Brief overview of PR changes/additions
This adds an experimental, new 3D mapper that uses shaders, more modern
openGL, and a far better code reorganization that makes it an easier
foundation to build upon.
The new 3D mapper is here side by side with the original and can be
toggled on for experimentation. There's a lot of work to be done, so I'd
rather merge it early instead of making a mega-PR.
#### Motivation for adding to Mudlet
So we have a new foundation to build upon and improve.
#### Other info (issues closed, discussion etc)
Old and new mapper can be toggled dynamically with:
```lua
-- this can be a keybinding
setConfig("experiment.3dmap.modernmapper", not getConfig("experiment.3dmap.modernmapper"))
```
Smooth movement is one experiment in the new mapper, and it can be
enabled with:
```lua
lua setConfig("experiment.rendering.smooth-camera", true)
```
As you notice an experiments system has been added so we can implement
things at once and experiment to choose the one that works best. This
system can be used in other places in Mudlet as well.
<details><summary>Details</summary>
<p>
## Experiments System
### Overview
Allows enabling/disabling experimental features via
`setConfig`/`getConfig` with validation against a predefined
whitelist.
### Usage
```lua
-- Enable experiment
setConfig("experiment.rendering.more-transparent", true)
-- Check if enabled
local enabled = getConfig("experiment.rendering.more-transparent") --
returns true/false
-- Get active experiment in group
local active = getConfig("experiment.rendering.active") -- returns
"more-transparent"
-- List all valid experiments
local experiments = getConfig("experiment.list") -- returns table of
valid keys
```
### Behavior
- Grouped experiments: Mutually exclusive (enabling one disables others in same group)
- Validation: Only predefined experiments allowed, invalid keys return errors
- Persistence: Experiment states saved/loaded with profiles
### Adding New Experiments
Edit Host::mValidExperiments in src/Host.cpp:
```cpp
const QSet<QString> Host::mValidExperiments = {
qsl("experiment.rendering.originalish"),
qsl("experiment.rendering.more-transparent"),
qsl("experiment.newfeature.option1"), // Add here
};
```
### Current Experiments
- experiment.rendering.originalish
- experiment.rendering.more-transparent
</p>
</details>
---------
Co-authored-by: Vadim Peretokin <vadi2@users.noreply.github.com>
2025-08-29 12:15:48 +02:00
# include "glwidget_integration.h"
2024-02-17 18:42:01 +01:00
# endif
# include <limits>
# include <math.h>
# include <QCollator>
# include <QCoreApplication>
# include <QDesktopServices>
# include <QFileInfo>
# include <QMovie>
# include <QVector>
# ifdef QT_TEXTTOSPEECH_LIB
# include <QTextToSpeech>
# endif // QT_TEXTTOSPEECH_LIB
// No documentation available in wiki - internal function
std : : pair < bool , QString > TLuaInterpreter : : discordApiEnabled ( lua_State * L , bool writeAccess )
{
mudlet * pMudlet = mudlet : : self ( ) ;
if ( ! pMudlet - > mDiscord . libraryLoaded ( ) ) {
return { false , qsl ( " Discord API is not available " ) } ;
}
auto & host = getHostFromLua ( L ) ;
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 ( host . mDiscordMode = = Host : : DiscordDisabled ) {
return { false , qsl ( " Discord is disabled in settings " ) } ;
2024-02-17 18:42:01 +01:00
}
if ( writeAccess & & ! pMudlet - > mDiscord . discordUserIdMatch ( & host ) ) {
return { false , qsl ( " Discord API is read-only as you're logged in with a different account in Discord compared to the one you entered for this profile " ) } ;
}
return { true , QString ( ) } ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#usingMudletsDiscordID
int TLuaInterpreter : : usingMudletsDiscordID ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
auto result = discordApiEnabled ( L ) ;
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
lua_pushboolean ( L , pMudlet - > mDiscord . usingMudletsDiscordID ( & host ) ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getDiscordDetail
int TLuaInterpreter : : getDiscordDetail ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
auto result = discordApiEnabled ( L ) ;
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
fix: Discord getters no longer treat presence text as a format string (#9660)
#### Brief overview of PR changes/additions
- The six Discord getters (`getDiscordDetail`, `getDiscordState`,
`getDiscordLargeIcon`/`Text`, `getDiscordSmallIcon`/`Text`) passed the
stored presence text to `lua_pushfstring()` as its *format* string, so
every `%` in it was read as a printf specifier consuming an argument
that was never passed: `"Level %d Mage"` came back as `"Level 3202416
Mage"`, `"%%"` was silently halved, and a text ending in `%` read past
the end of the buffer and returned the bytes that followed it as part of
the Lua string.
- All six now push the text as data. Output is byte-identical to today's
for any text without a `%`.
- Five regression specs added to `Discord_spec.lua`, covering all six
getters and the `%d`, `%s`, `%%` and trailing-`%` shapes; the
`pending()` entry that recorded this defect is unpended.
#### Motivation for adding to Mudlet
Presence text arrives from the game server over GMCP
(`Host::processDiscordGMCP`), so a game whose status line contains a
stray `%` can garble - and, with a trailing `%` or a `%s`, crash or leak
adjacent heap bytes into a Lua string - for any user whose script reads
the presence back. There is no write primitive (Lua 5.1's
`lua_pushfstring` has no `%n`), so the ceiling is a crash plus memory
disclosure, not code execution.
Stacks on #9631, whose Discord IPC fixture is what makes these specs
possible; it retargets to `development` once that merges.
**Test case:** all five new specs fail against the unfixed binary
(`Level 3202416 Mage`, `100% health` for `100%% health`, and `mana at
50%` returned with trailing heap garbage) and pass with it; Lua suite
1853 successes / 0 failures / 0 errors / 40 pending twice with the
fixture and 1829 / 0 / 0 / 64 without it, ctest 69/70 (only the
known-environmental `TKeySequenceEditTest` Xvfb flake).
Assisted-by: Claude:claude-opus-5
2026-08-05 06:49:43 +02:00
// Pushed as data, never as a format string: presence text can come from the
// game server, and a '%' in it would otherwise be taken as a printf
// specifier. The same holds for the five other Discord text getters below.
lua_pushstring ( L , pMudlet - > mDiscord . getDetailText ( & host ) . toUtf8 ( ) . constData ( ) ) ;
2024-02-17 18:42:01 +01:00
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getDiscordLargeIcon
int TLuaInterpreter : : getDiscordLargeIcon ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
auto result = discordApiEnabled ( L ) ;
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
fix: Discord getters no longer treat presence text as a format string (#9660)
#### Brief overview of PR changes/additions
- The six Discord getters (`getDiscordDetail`, `getDiscordState`,
`getDiscordLargeIcon`/`Text`, `getDiscordSmallIcon`/`Text`) passed the
stored presence text to `lua_pushfstring()` as its *format* string, so
every `%` in it was read as a printf specifier consuming an argument
that was never passed: `"Level %d Mage"` came back as `"Level 3202416
Mage"`, `"%%"` was silently halved, and a text ending in `%` read past
the end of the buffer and returned the bytes that followed it as part of
the Lua string.
- All six now push the text as data. Output is byte-identical to today's
for any text without a `%`.
- Five regression specs added to `Discord_spec.lua`, covering all six
getters and the `%d`, `%s`, `%%` and trailing-`%` shapes; the
`pending()` entry that recorded this defect is unpended.
#### Motivation for adding to Mudlet
Presence text arrives from the game server over GMCP
(`Host::processDiscordGMCP`), so a game whose status line contains a
stray `%` can garble - and, with a trailing `%` or a `%s`, crash or leak
adjacent heap bytes into a Lua string - for any user whose script reads
the presence back. There is no write primitive (Lua 5.1's
`lua_pushfstring` has no `%n`), so the ceiling is a crash plus memory
disclosure, not code execution.
Stacks on #9631, whose Discord IPC fixture is what makes these specs
possible; it retargets to `development` once that merges.
**Test case:** all five new specs fail against the unfixed binary
(`Level 3202416 Mage`, `100% health` for `100%% health`, and `mana at
50%` returned with trailing heap garbage) and pass with it; Lua suite
1853 successes / 0 failures / 0 errors / 40 pending twice with the
fixture and 1829 / 0 / 0 / 64 without it, ctest 69/70 (only the
known-environmental `TKeySequenceEditTest` Xvfb flake).
Assisted-by: Claude:claude-opus-5
2026-08-05 06:49:43 +02:00
lua_pushstring ( L , pMudlet - > mDiscord . getLargeImage ( & host ) . toUtf8 ( ) . constData ( ) ) ;
2024-02-17 18:42:01 +01:00
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getDiscordLargeIconText
int TLuaInterpreter : : getDiscordLargeIconText ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
fix: Discord getters no longer treat presence text as a format string (#9660)
#### Brief overview of PR changes/additions
- The six Discord getters (`getDiscordDetail`, `getDiscordState`,
`getDiscordLargeIcon`/`Text`, `getDiscordSmallIcon`/`Text`) passed the
stored presence text to `lua_pushfstring()` as its *format* string, so
every `%` in it was read as a printf specifier consuming an argument
that was never passed: `"Level %d Mage"` came back as `"Level 3202416
Mage"`, `"%%"` was silently halved, and a text ending in `%` read past
the end of the buffer and returned the bytes that followed it as part of
the Lua string.
- All six now push the text as data. Output is byte-identical to today's
for any text without a `%`.
- Five regression specs added to `Discord_spec.lua`, covering all six
getters and the `%d`, `%s`, `%%` and trailing-`%` shapes; the
`pending()` entry that recorded this defect is unpended.
#### Motivation for adding to Mudlet
Presence text arrives from the game server over GMCP
(`Host::processDiscordGMCP`), so a game whose status line contains a
stray `%` can garble - and, with a trailing `%` or a `%s`, crash or leak
adjacent heap bytes into a Lua string - for any user whose script reads
the presence back. There is no write primitive (Lua 5.1's
`lua_pushfstring` has no `%n`), so the ceiling is a crash plus memory
disclosure, not code execution.
Stacks on #9631, whose Discord IPC fixture is what makes these specs
possible; it retargets to `development` once that merges.
**Test case:** all five new specs fail against the unfixed binary
(`Level 3202416 Mage`, `100% health` for `100%% health`, and `mana at
50%` returned with trailing heap garbage) and pass with it; Lua suite
1853 successes / 0 failures / 0 errors / 40 pending twice with the
fixture and 1829 / 0 / 0 / 64 without it, ctest 69/70 (only the
known-environmental `TKeySequenceEditTest` Xvfb flake).
Assisted-by: Claude:claude-opus-5
2026-08-05 06:49:43 +02:00
lua_pushstring ( L , pMudlet - > mDiscord . getLargeImageText ( & host ) . toUtf8 ( ) . constData ( ) ) ;
2024-02-17 18:42:01 +01:00
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getDiscordParty
int TLuaInterpreter : : getDiscordParty ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
QPair < int , int > const partyValues = pMudlet - > mDiscord . getParty ( & host ) ;
lua_pushnumber ( L , partyValues . first ) ;
lua_pushnumber ( L , partyValues . second ) ;
return 2 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getDiscordSmallIcon
int TLuaInterpreter : : getDiscordSmallIcon ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
fix: Discord getters no longer treat presence text as a format string (#9660)
#### Brief overview of PR changes/additions
- The six Discord getters (`getDiscordDetail`, `getDiscordState`,
`getDiscordLargeIcon`/`Text`, `getDiscordSmallIcon`/`Text`) passed the
stored presence text to `lua_pushfstring()` as its *format* string, so
every `%` in it was read as a printf specifier consuming an argument
that was never passed: `"Level %d Mage"` came back as `"Level 3202416
Mage"`, `"%%"` was silently halved, and a text ending in `%` read past
the end of the buffer and returned the bytes that followed it as part of
the Lua string.
- All six now push the text as data. Output is byte-identical to today's
for any text without a `%`.
- Five regression specs added to `Discord_spec.lua`, covering all six
getters and the `%d`, `%s`, `%%` and trailing-`%` shapes; the
`pending()` entry that recorded this defect is unpended.
#### Motivation for adding to Mudlet
Presence text arrives from the game server over GMCP
(`Host::processDiscordGMCP`), so a game whose status line contains a
stray `%` can garble - and, with a trailing `%` or a `%s`, crash or leak
adjacent heap bytes into a Lua string - for any user whose script reads
the presence back. There is no write primitive (Lua 5.1's
`lua_pushfstring` has no `%n`), so the ceiling is a crash plus memory
disclosure, not code execution.
Stacks on #9631, whose Discord IPC fixture is what makes these specs
possible; it retargets to `development` once that merges.
**Test case:** all five new specs fail against the unfixed binary
(`Level 3202416 Mage`, `100% health` for `100%% health`, and `mana at
50%` returned with trailing heap garbage) and pass with it; Lua suite
1853 successes / 0 failures / 0 errors / 40 pending twice with the
fixture and 1829 / 0 / 0 / 64 without it, ctest 69/70 (only the
known-environmental `TKeySequenceEditTest` Xvfb flake).
Assisted-by: Claude:claude-opus-5
2026-08-05 06:49:43 +02:00
lua_pushstring ( L , pMudlet - > mDiscord . getSmallImage ( & host ) . toUtf8 ( ) . constData ( ) ) ;
2024-02-17 18:42:01 +01:00
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getDiscordSmallIconText
int TLuaInterpreter : : getDiscordSmallIconText ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
fix: Discord getters no longer treat presence text as a format string (#9660)
#### Brief overview of PR changes/additions
- The six Discord getters (`getDiscordDetail`, `getDiscordState`,
`getDiscordLargeIcon`/`Text`, `getDiscordSmallIcon`/`Text`) passed the
stored presence text to `lua_pushfstring()` as its *format* string, so
every `%` in it was read as a printf specifier consuming an argument
that was never passed: `"Level %d Mage"` came back as `"Level 3202416
Mage"`, `"%%"` was silently halved, and a text ending in `%` read past
the end of the buffer and returned the bytes that followed it as part of
the Lua string.
- All six now push the text as data. Output is byte-identical to today's
for any text without a `%`.
- Five regression specs added to `Discord_spec.lua`, covering all six
getters and the `%d`, `%s`, `%%` and trailing-`%` shapes; the
`pending()` entry that recorded this defect is unpended.
#### Motivation for adding to Mudlet
Presence text arrives from the game server over GMCP
(`Host::processDiscordGMCP`), so a game whose status line contains a
stray `%` can garble - and, with a trailing `%` or a `%s`, crash or leak
adjacent heap bytes into a Lua string - for any user whose script reads
the presence back. There is no write primitive (Lua 5.1's
`lua_pushfstring` has no `%n`), so the ceiling is a crash plus memory
disclosure, not code execution.
Stacks on #9631, whose Discord IPC fixture is what makes these specs
possible; it retargets to `development` once that merges.
**Test case:** all five new specs fail against the unfixed binary
(`Level 3202416 Mage`, `100% health` for `100%% health`, and `mana at
50%` returned with trailing heap garbage) and pass with it; Lua suite
1853 successes / 0 failures / 0 errors / 40 pending twice with the
fixture and 1829 / 0 / 0 / 64 without it, ctest 69/70 (only the
known-environmental `TKeySequenceEditTest` Xvfb flake).
Assisted-by: Claude:claude-opus-5
2026-08-05 06:49:43 +02:00
lua_pushstring ( L , pMudlet - > mDiscord . getSmallImageText ( & host ) . toUtf8 ( ) . constData ( ) ) ;
2024-02-17 18:42:01 +01:00
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getDiscordState
int TLuaInterpreter : : getDiscordState ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
auto result = discordApiEnabled ( L ) ;
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
fix: Discord getters no longer treat presence text as a format string (#9660)
#### Brief overview of PR changes/additions
- The six Discord getters (`getDiscordDetail`, `getDiscordState`,
`getDiscordLargeIcon`/`Text`, `getDiscordSmallIcon`/`Text`) passed the
stored presence text to `lua_pushfstring()` as its *format* string, so
every `%` in it was read as a printf specifier consuming an argument
that was never passed: `"Level %d Mage"` came back as `"Level 3202416
Mage"`, `"%%"` was silently halved, and a text ending in `%` read past
the end of the buffer and returned the bytes that followed it as part of
the Lua string.
- All six now push the text as data. Output is byte-identical to today's
for any text without a `%`.
- Five regression specs added to `Discord_spec.lua`, covering all six
getters and the `%d`, `%s`, `%%` and trailing-`%` shapes; the
`pending()` entry that recorded this defect is unpended.
#### Motivation for adding to Mudlet
Presence text arrives from the game server over GMCP
(`Host::processDiscordGMCP`), so a game whose status line contains a
stray `%` can garble - and, with a trailing `%` or a `%s`, crash or leak
adjacent heap bytes into a Lua string - for any user whose script reads
the presence back. There is no write primitive (Lua 5.1's
`lua_pushfstring` has no `%n`), so the ceiling is a crash plus memory
disclosure, not code execution.
Stacks on #9631, whose Discord IPC fixture is what makes these specs
possible; it retargets to `development` once that merges.
**Test case:** all five new specs fail against the unfixed binary
(`Level 3202416 Mage`, `100% health` for `100%% health`, and `mana at
50%` returned with trailing heap garbage) and pass with it; Lua suite
1853 successes / 0 failures / 0 errors / 40 pending twice with the
fixture and 1829 / 0 / 0 / 64 without it, ctest 69/70 (only the
known-environmental `TKeySequenceEditTest` Xvfb flake).
Assisted-by: Claude:claude-opus-5
2026-08-05 06:49:43 +02:00
lua_pushstring ( L , pMudlet - > mDiscord . getStateText ( & host ) . toUtf8 ( ) . constData ( ) ) ;
2024-02-17 18:42:01 +01:00
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getDiscordTimeStamps
int TLuaInterpreter : : getDiscordTimeStamps ( lua_State * L )
{
auto & host = getHostFromLua ( L ) ;
auto result = discordApiEnabled ( L ) ;
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
QPair < int64_t , int64_t > const timeStamps = mudlet : : self ( ) - > mDiscord . getTimeStamps ( & host ) ;
lua_pushnumber ( L , timeStamps . first ) ;
lua_pushnumber ( L , timeStamps . second ) ;
return 2 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#resetDiscordData
int TLuaInterpreter : : resetDiscordData ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
2024-02-17 18:42:01 +01:00
pMudlet - > mDiscord . resetData ( & host ) ;
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordApplicationID
int TLuaInterpreter : : setDiscordApplicationID ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
auto result = discordApiEnabled ( L , true ) ;
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
if ( ! lua_gettop ( L ) ) {
pMudlet - > mDiscord . setApplicationID ( & host , QString ( ) ) ;
lua_pushboolean ( L , true ) ;
return 1 ;
}
const QString inputText = getVerifiedString ( L , __func__ , 1 , " Discord application ID " ) . trimmed ( ) ;
// Treat it as a UTF-8 string because although it is likely to be an
// unsigned long long integer (0 to 18446744073709551615) we want to
// be able to handle any input so we can report bad input strings back.
if ( inputText . isEmpty ( ) ) {
// Empty string input - to reset to default the same as the no
// argument case:
pMudlet - > mDiscord . setApplicationID ( & host , QString ( ) ) ;
// This must always succeed
lua_pushboolean ( L , true ) ;
return 1 ;
}
bool isOk = false ;
quint64 const numericEquivalent = inputText . toULongLong ( & isOk ) ;
if ( numericEquivalent & & isOk ) {
const QString appID = QString : : number ( numericEquivalent ) ;
if ( pMudlet - > mDiscord . setApplicationID ( & host , appID ) ) {
lua_pushboolean ( L , true ) ;
return 1 ;
}
return warnArgumentValue ( L , __func__ , qsl ( " '%1' does not appear to be a valid Discord application ID " ) . arg ( inputText ) ) ;
}
return warnArgumentValue ( L , __func__ , qsl ( " '%1' can not be converted to the expected numeric Discord application ID " ) . arg ( inputText ) ) ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordDetail
int TLuaInterpreter : : setDiscordDetail ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
auto discordText = getVerifiedString ( L , __func__ , 1 , " text " ) ;
if ( discordText . size ( ) = = 1 ) {
return warnArgumentValue ( L , __func__ , " text of length 1 not allowed by Discord " ) ;
}
pMudlet - > mDiscord . setDetailText ( & host , discordText ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetDetail ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordElapsedStartTime
int TLuaInterpreter : : setDiscordElapsedStartTime ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
2026-04-04 06:37:14 +01:00
const auto timeStamp = getVerifiedInt ( L , __func__ , 1 , " epoch time " ) ;
2024-02-17 18:42:01 +01:00
if ( timeStamp < 0 ) {
return warnArgumentValue ( L , __func__ , " the timestamp must be zero to clear the 'elapsed:' time or an epoch time value from the recent past " ) ;
}
2026-04-04 06:37:14 +01:00
pMudlet - > mDiscord . setStartTimeStamp ( & host , static_cast < int64_t > ( timeStamp ) ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetTimeInfo ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordGame
int TLuaInterpreter : : setDiscordGame ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
const QString gamename = getVerifiedString ( L , __func__ , 1 , " game name " ) ;
pMudlet - > mDiscord . setDetailText ( & host , tr ( " Playing %1 " ) . arg ( gamename ) ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetDetail ) ;
2024-02-17 18:42:01 +01:00
pMudlet - > mDiscord . setLargeImage ( & host , gamename . toLower ( ) ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetLargeIcon ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordGameUrl
int TLuaInterpreter : : setDiscordGameUrl ( lua_State * L )
{
// The invite URL changes what the Discord button opens, and the name is
// what it displays on the button. It is not part of rich presence, so it
// does not have the API enabled check that those Discord functions need
// in order to respect privacy.
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
const bool isActiveHost = ( pMudlet - > mpCurrentActiveHost = = & host ) ;
const int args = lua_gettop ( L ) ;
if ( ! args ) { // no args, blank the invite URL and game name
host . setDiscordInviteURL ( QString ( ) ) ;
host . setDiscordGameName ( QString ( ) ) ;
if ( isActiveHost ) {
pMudlet - > updateDiscordNamedIcon ( ) ;
}
lua_pushboolean ( L , true ) ;
return 1 ;
}
Fix: strand no heap objects across the remaining lua_error() raises (#9605)
#### Brief overview of PR changes/additions
- Makes #9602's by-example sweep exhaustive. A static pass over the
**674** functions taking a `lua_State*` - the only frames a
`lua_error()` longjmp can unwind - found **377 sites in 160 functions**
holding a QString, QStringList, QByteArray, std::string or TMediaData
across a raise. **334 are converted, clearing 133 functions.** The **43
sites in 27 functions** left are each verified non-owning: `static`
storage, `qsl()`/QStringLiteral, default-constructed or `""` (Qt's
shared empty buffer), `isEmpty()` being the raise condition itself, a
local declared in the branch that does not raise, and one
`QFileInfo::exists` vs `TLuaInterpreter::exists` name collision. Same
scanner both sides, nothing newly flagged.
- Mechanism is #9602's: the `checkStringArg()` family, extended with
`checkNumberArg`, `checkStringOrIntegerArg`,
`checkCommandOrFunctionArg`, `checkCommandsOrFunctionsTable` and
`checkHintsTable`.
`getVerifiedString`/`Int`/`Bool`/`Float`/`Double`/`StringOrInteger`,
`parseCommandOrFunction` and the two table parsers are reimplemented or
paired on top, so message text and the order errors are reported in
cannot drift.
- Raisers are not only Mudlet's own helpers - lauxlib's `luaL_check*`
and `luaL_opt*` raise too, and `spawn()` was the worst case.
`TForkedProcess`'s constructor raised three times while holding the
program name and the argument list it was still filling, and because a
longjmp out of a constructor skips the rest of it, the `QProcess` that
`startProcess()` had just `new`'d leaked whole. Checking and failure
reporting move to `startProcess()`; the constructor no longer takes a
`lua_State` and cannot raise. `waitForEvent()` swaps `luaL_checkstack`
for the non-raising `lua_checkstack`.
- Two sub-classes a site scan structurally cannot see were caught
separately. Temporaries passed *into* a raising call rather than named
locals: `setLabelCallback()` and `movieFunc()` take a `const char*` now,
as a QByteArray built from their QString name was alive inside every
raising check. And loop-carried accumulation, where a container looks
non-owning at its declaration and only fills up once the loop runs -
`setMergeTables("Char", {})` and `TForkedProcess`'s argument loop both
stranded lists that way. A second detector (container declared outside a
loop, mutated inside it, raise in the same body) reproduces both on the
parent commit and reports nothing here.
#### Motivation for adding to Mudlet
#9602 removed the LSan suppression hiding this class but only fixed what
the suite happened to reach, so the first spec touching any of the rest
would turn CI red. Three things are worth recording for anyone repeating
the audit: `lua_error` only unwinds frames between the raise and the
enclosing `lua_pcall`, so closing the call graph over every function
name gives 1834 "raisers" and thousands of bogus hits - the `lua_State*`
universe is the right scope; `__func__` inside a lambda expands to
`"operator()"`, so wrapping a function body in one silently renames its
error messages; and a constructor that raises strands whatever `new`'d
it.
**Test case:** `Spawn_spec.lua` drives every `spawn()` error path, which
strand 4260 bytes in 20 allocations on the parent commit and nothing
here; `expandAlias`, `findItems` and `setModulePriority` likewise, with
byte-identical error messages either side. Busted 2x 1316 successes / 0
failures / 0 errors / 0 pending, ctest 60/60.
Stacked on #9602 - that one goes in first.
Assisted-by: Claude:claude-opus-5
2026-08-03 11:39:32 +02:00
// argument 1 is applied before argument 2 is checked, as it was before:
// setDiscordInviteURL() persists to the profile, and hoisting the second
// check above it would stop a bad game name from saving the URL
if ( ! checkStringArg ( L , __func__ , 1 , " url " ) ) {
return lua_error ( L ) ;
}
{
const QString inviteUrl = QString { lua_tostring ( L , 1 ) } . trimmed ( ) ;
host . setDiscordInviteURL ( inviteUrl . isEmpty ( ) ? QString ( ) : inviteUrl ) ;
}
2024-02-17 18:42:01 +01:00
if ( args > 1 ) {
Fix: strand no heap objects across the remaining lua_error() raises (#9605)
#### Brief overview of PR changes/additions
- Makes #9602's by-example sweep exhaustive. A static pass over the
**674** functions taking a `lua_State*` - the only frames a
`lua_error()` longjmp can unwind - found **377 sites in 160 functions**
holding a QString, QStringList, QByteArray, std::string or TMediaData
across a raise. **334 are converted, clearing 133 functions.** The **43
sites in 27 functions** left are each verified non-owning: `static`
storage, `qsl()`/QStringLiteral, default-constructed or `""` (Qt's
shared empty buffer), `isEmpty()` being the raise condition itself, a
local declared in the branch that does not raise, and one
`QFileInfo::exists` vs `TLuaInterpreter::exists` name collision. Same
scanner both sides, nothing newly flagged.
- Mechanism is #9602's: the `checkStringArg()` family, extended with
`checkNumberArg`, `checkStringOrIntegerArg`,
`checkCommandOrFunctionArg`, `checkCommandsOrFunctionsTable` and
`checkHintsTable`.
`getVerifiedString`/`Int`/`Bool`/`Float`/`Double`/`StringOrInteger`,
`parseCommandOrFunction` and the two table parsers are reimplemented or
paired on top, so message text and the order errors are reported in
cannot drift.
- Raisers are not only Mudlet's own helpers - lauxlib's `luaL_check*`
and `luaL_opt*` raise too, and `spawn()` was the worst case.
`TForkedProcess`'s constructor raised three times while holding the
program name and the argument list it was still filling, and because a
longjmp out of a constructor skips the rest of it, the `QProcess` that
`startProcess()` had just `new`'d leaked whole. Checking and failure
reporting move to `startProcess()`; the constructor no longer takes a
`lua_State` and cannot raise. `waitForEvent()` swaps `luaL_checkstack`
for the non-raising `lua_checkstack`.
- Two sub-classes a site scan structurally cannot see were caught
separately. Temporaries passed *into* a raising call rather than named
locals: `setLabelCallback()` and `movieFunc()` take a `const char*` now,
as a QByteArray built from their QString name was alive inside every
raising check. And loop-carried accumulation, where a container looks
non-owning at its declaration and only fills up once the loop runs -
`setMergeTables("Char", {})` and `TForkedProcess`'s argument loop both
stranded lists that way. A second detector (container declared outside a
loop, mutated inside it, raise in the same body) reproduces both on the
parent commit and reports nothing here.
#### Motivation for adding to Mudlet
#9602 removed the LSan suppression hiding this class but only fixed what
the suite happened to reach, so the first spec touching any of the rest
would turn CI red. Three things are worth recording for anyone repeating
the audit: `lua_error` only unwinds frames between the raise and the
enclosing `lua_pcall`, so closing the call graph over every function
name gives 1834 "raisers" and thousands of bogus hits - the `lua_State*`
universe is the right scope; `__func__` inside a lambda expands to
`"operator()"`, so wrapping a function body in one silently renames its
error messages; and a constructor that raises strands whatever `new`'d
it.
**Test case:** `Spawn_spec.lua` drives every `spawn()` error path, which
strand 4260 bytes in 20 allocations on the parent commit and nothing
here; `expandAlias`, `findItems` and `setModulePriority` likewise, with
byte-identical error messages either side. Busted 2x 1316 successes / 0
failures / 0 errors / 0 pending, ctest 60/60.
Stacked on #9602 - that one goes in first.
Assisted-by: Claude:claude-opus-5
2026-08-03 11:39:32 +02:00
if ( ! checkStringArg ( L , __func__ , 2 , " game name " ) ) {
return lua_error ( L ) ;
}
host . setDiscordGameName ( QString { lua_tostring ( L , 2 ) } . trimmed ( ) ) ;
2024-02-17 18:42:01 +01:00
} else {
host . setDiscordGameName ( QString ( ) ) ;
}
if ( isActiveHost ) {
pMudlet - > updateDiscordNamedIcon ( ) ;
}
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordLargeIcon
int TLuaInterpreter : : setDiscordLargeIcon ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
auto result = discordApiEnabled ( L , true ) ;
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
pMudlet - > mDiscord . setLargeImage ( & host , getVerifiedString ( L , __func__ , 1 , " key " ) . toLower ( ) ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetLargeIcon ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordLargeIconText
int TLuaInterpreter : : setDiscordLargeIconText ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
auto discordText = getVerifiedString ( L , __func__ , 1 , " text " ) ;
if ( discordText . size ( ) = = 1 ) {
return warnArgumentValue ( L , __func__ , " text of length 1 not allowed by Discord " ) ;
}
pMudlet - > mDiscord . setLargeImageText ( & host , discordText ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetLargeIconText ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordParty
int TLuaInterpreter : : setDiscordParty ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
2026-04-04 06:37:14 +01:00
const auto partySize = getVerifiedInt ( L , __func__ , 1 , " current party size " ) ;
2024-02-17 18:42:01 +01:00
if ( partySize < 0 ) {
return warnArgumentValue ( L , __func__ , " the current party size must be zero or more " ) ;
}
2026-04-04 06:37:14 +01:00
int partyMax = - 1 ;
2024-02-17 18:42:01 +01:00
if ( lua_gettop ( L ) > 1 ) {
partyMax = getVerifiedInt ( L , __func__ , 2 , " party maximum size " , true ) ;
if ( partyMax < 0 ) {
return warnArgumentValue ( L , __func__ , " the optional party maximum size must be zero (to remove the party details) or more ( to set the maximum ) " ) ;
}
2026-04-04 06:37:14 +01:00
pMudlet - > mDiscord . setParty ( & host , partySize , partyMax ) ;
2024-02-17 18:42:01 +01:00
} else {
// Only got the partySize now
2026-04-04 06:37:14 +01:00
pMudlet - > mDiscord . setParty ( & host , partySize ) ;
2024-02-17 18:42:01 +01: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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetPartyInfo ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordRemainingEndTime
int TLuaInterpreter : : setDiscordRemainingEndTime ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
2026-04-04 06:37:14 +01:00
const auto timeStamp = getVerifiedInt ( L , __func__ , 1 , " epoch time " ) ;
2024-02-17 18:42:01 +01:00
if ( timeStamp < 0 ) {
return warnArgumentValue ( L , __func__ , " the timestamp must be zero to clear the 'remaining:' time or an epoch time value in the recent future " ) ;
}
2026-04-04 06:37:14 +01:00
pMudlet - > mDiscord . setEndTimeStamp ( & host , static_cast < int64_t > ( timeStamp ) ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetTimeInfo ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordSmallIcon
int TLuaInterpreter : : setDiscordSmallIcon ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
pMudlet - > mDiscord . setSmallImage ( & host , getVerifiedString ( L , __func__ , 1 , " key " ) . toLower ( ) ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetSmallIcon ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordSmallIconText
int TLuaInterpreter : : setDiscordSmallIconText ( lua_State * L )
{
mudlet * pMudlet = mudlet : : self ( ) ;
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
auto discordText = getVerifiedString ( L , __func__ , 1 , " text " ) ;
if ( discordText . size ( ) = = 1 ) {
return warnArgumentValue ( L , __func__ , " text of length 1 not allowed by Discord " ) ;
}
pMudlet - > mDiscord . setSmallImageText ( & host , discordText ) ;
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
pMudlet - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetSmallIconText ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setDiscordState
int TLuaInterpreter : : setDiscordState ( lua_State * L )
{
auto & host = getHostFromLua ( L ) ;
2026-07-26 10:13:28 +02:00
auto result = discordApiEnabled ( L , true ) ;
2024-02-17 18:42:01 +01:00
if ( ! result . first ) {
return warnArgumentValue ( L , __func__ , result . second ) ;
}
auto discordText = getVerifiedString ( L , __func__ , 1 , " text " ) ;
if ( discordText . size ( ) = = 1 ) {
return warnArgumentValue ( L , __func__ , " text of length 1 not allowed by Discord " ) ;
}
mudlet : : self ( ) - > mDiscord . setStateText ( & host , discordText ) ;
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
mudlet : : self ( ) - > mDiscord . clearServerOrigin ( & host , Host : : DiscordSetState ) ;
2024-02-17 18:42:01 +01:00
lua_pushboolean ( L , true ) ;
return 1 ;
}