Skip to content

feat(discord): take the Discord integration from the server (C2–C5) - #709

Draft
sven-n wants to merge 15 commits into
claude/discord-presence-mumain-597-l2q09d-c2-invitefrom
claude/discord-presence-mumain-597-l2q09d-server-info
Draft

sven-n wants to merge 15 commits into
claude/discord-presence-mumain-597-l2q09d-c2-invitefrom
claude/discord-presence-mumain-597-l2q09d-server-info

Conversation

@sven-n

@sven-n sven-n commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Summary

Draft: needs the packets of MUnique/OpenMU#1174 released. OpenMuPacketsVersion (csproj) and OPENMU_PACKETS_VERSION (CMake) point at 0.9.11-discord.2. That is a local build of the packets from that PR, so CI can't restore it yet. Before merging, both have to be bumped to the released version, and the bindings regenerated if the definitions changed in review.

Stacked on #707 (Join button), with #708 (Discord chat) merged in, so the diff contains #708's commits too. Both are stacked on #705.

The client side of the Discord milestones that need the server, using the opt-in F5 packets drafted in MUnique/OpenMU#1174.

C2: integration info from the server

  • Request: after entering the world, the client sends DiscordIntegrationInfoRequest. GameLogic::Discord::ServerIntegration keeps the answer.
  • Precedence: the server's Rich Presence application id, image keys and invite link win over config.ini. config.ini stays the fallback for servers that send none.
  • Older servers: they don't answer, and nothing changes.

C3: account link dialog

  • Entry point: on a server with the integration, the options window's Discord button says Discord... and opens a dialog (CDiscordMsgBox, built like the other custom message boxes). It shows whether the account is linked and as whom, and which chats are mirrored.
  • Buttons: Link / Unlink, Join, Close.
  • Link: requests a code and shows it with its validity. The code is already in the clipboard (SDL_SetClipboardText), Copy copies it again, and the dialog says to enter /link <code> in Discord. If linking isn't available, a system message says so.
  • Unlink: the server answers with a fresh info.

C4: messages from Discord

C5: guild window

  • Notice: under the guild name, "Guild chat mirrored to Discord" (the alliance tab shows it for the alliance chat). It doubles as the privacy notice.
  • Guild masters: when the chat isn't mirrored, a guild master is told to bind it with /guildchat in Discord. OpenMU's M5 binds in Discord itself (/guildchat bind/create), so there is no bind code to show in the game.

Code layout: the messages are read in Network::Discord (DiscordPackets) and dispatched from the F5 group by Network::Discord::HandleMessage, instead of growing WSclient.cpp.

Bindings update (its own commit): regenerated from the new packets. OpenMU master has also typed three Castle Siege requests since 0.9.10:

  • CastleSiegeTaxType
  • CastleSiegeGuildCommandType
  • closeState → isOpen

Their three call sites now pass the enums. The bytes on the wire are the same.

Docs: docs/discord.md covers the account dialog, the guild notice, and the server values taking over from config.ini.

Verification

  • Linux linux-x64 Debug build against the local packets package: full ctest run, 457/457 passed. That includes 7 new tests in tests/network/test_discord_packets.cpp:
    • reading the info, link code and external chat message
    • truncated or unknown-scope messages are refused
    • an empty info offers nothing
    • the server's values win over config.ini and are forgotten on the next request
  • With MU_ENABLE_DISCORD undefined, the touched files compile.
  • clang-format 21.1.8 range check and cppcheck (CI flags) are clean on the changed lines.
  • Not tested: a running client against OpenMU with #1174. Nothing visual (dialog layout, guild notice placement) has been looked at.

Related issues

C2 #698, C3 #699, C4 #700, C5 #701. These close once this and MUnique/OpenMU#1174 are in.

Checklist

  • I have read and followed docs/CODING_RULES.md.
  • I have built the project locally (see docs/build/README.md), against a local build of the packets package.
  • Changes are focused on one concern. Not quite: the four milestones share the integration info, so they come together; the bindings update is its own commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj


Generated by Claude Code

Groundwork for #700 that needs no server support yet: the display path the
ExternalChatMessage packet handler will call (UI::Chat::External::Show).

- The sender is shown as "[Discord] <name>", with the full Discord name
  (up to 32 characters) instead of the 10-byte character name.
- The message takes its scope's chat type and colours (guild, alliance,
  world) and is also listed in a new Discord view of the chat log.
- Discord users can't be whispered: no whisper allow-list entry, no speech
  balloon, and right-clicking their line doesn't start a whisper.
- F2 cycles all -> whispers -> Discord -> all; the Discord view is only
  offered once a message from Discord arrived, so nothing changes on
  servers without a bridge.

The repeated create-and-append blocks of CNewUIChatLogWindow::ProcessAddText
are extracted into AppendMessage, and the copy into a second view (an
error's origin, now also the Discord view) into GetSecondaryMsgs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
OpenMU's chat bridge (MUnique/OpenMU#1161) sends messages written in Discord
through the ordinary guild and alliance chat, as the linked character with
an '@' prefix that no character name can contain. Such lines are now shown
through UI::Chat::External as messages from Discord: "[Discord] <name>" in
the guild or alliance colours, in the Discord view, and not whisperable.

This makes the C4 display path work against OpenMU today, without waiting
for a dedicated packet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
… claude/discord-presence-mumain-597-l2q09d-server-info

# Conflicts:
#	docs/discord.md
Regenerates the bindings from a build of MUnique.OpenMU.Network.Packets
which contains the Discord integration packets (MUnique/OpenMU#1174):
DiscordIntegrationInfoRequest, DiscordLinkCodeRequest and
DiscordUnlinkRequest.

The definitions on OpenMU's master also typed three Castle Siege requests
since 0.9.10 (tax type, guild command, gate state); their callers pass the
new enums, the bytes on the wire stay the same.

OpenMuPacketsVersion points at a local pre-release build until the
packets are released; it has to be bumped to the released version before
merging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
Uses the Discord integration messages of OpenMU (MUnique/OpenMU#1174):

- C2: after entering the world the client asks for the integration info
  (GameLogic::Discord::ServerIntegration). The server's Rich Presence
  application, image keys and invite link win over config.ini, which stays
  the fallback for servers that don't send them.
- C3: on such a server the options window's Discord button opens a Discord
  dialog: whether the account is linked and which chats are mirrored, with
  Link / Unlink and Join. Link shows the one-time code, copies it to the
  clipboard and says to enter /link <code> in Discord.
- C4: ExternalChatMessage is shown through UI::Chat::External with the
  Discord name and its scope, now including the world chat.

The messages are read in Network::Discord (DiscordPackets) and dispatched
from the F5 group by Network::Discord::HandleMessage.

Also brings the formatting of the Castle Siege call sites of the bindings
update in line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
…cord

Under the guild name: "Guild chat mirrored to Discord" (alliance tab: the
alliance chat), from the server's Discord integration info - the notice
that what is written there leaves the game. A guild master whose chat
isn't mirrored is told to bind it with /guildchat in Discord; the binding
itself happens in Discord (MUnique/OpenMU#1161).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj

@sven-n sven-n left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of a review pass over the whole Discord integration — the four client PRs here and the seven server PRs in MUnique/OpenMU. Reviewing as a draft, so this is early feedback rather than a gate.

I spent most of the time on DiscordPackets.cpp, since untrusted packet parsing is where this PR can actually hurt, and it holds up:

  • Every parser length-checks before indexing (packet.size() < InfoLength, < LinkCodeLength, <= ExternalMessageOffset), and unknown enum values are rejected rather than cast blindly.
  • ReadString is safe for the awkward case I went looking for — a fixed field filled to its last byte with no NUL. It passes an explicit maxSourceLength to MultiByteToWideChar, so there's no scan past the field; the buffer is length + 1 and zero-initialised, and buffer[length] is set explicitly rather than relying on ConvertFromUtf8, which only terminates when written < maxSourceLength. LinkedUserName is exactly the case that matters: 32 bytes, flush with the end of the 224-byte packet, and a 32-character Discord username is legal. It's handled.
  • I diffed the offsets against the server definitions in MUnique/OpenMU#1174 field by field — ExternalChatMessage 5/6/7+48/55, DiscordIntegrationInfo 4/5/6/7/8+20/28+32/60+32/92+100/192+32, DiscordLinkCode 4/5+10/15. No drift.
  • The server-supplied invite URL is correctly routed through #707's allowlist: NewUIOptionWindow.cpp:813 checks IsInviteUrl before showing the button and Open re-validates. That's the right place to be strict.

Two comments inline on ServerIntegration.cpp, both minor.

One process note: this PR's base is claude/discord-presence-mumain-597-l2q09d-c2-invite (#707) rather than #708's branch, so its diff re-includes all of #708's C4 work — ExternalChat.*, the NewUIChatLogWindow changes and test_external_chat.cpp are byte-identical in both. #708 is an ancestor here, so nothing will conflict; it just means this PR currently shows ~400 lines that belong to #708's review. Retargeting the base onto ...-c4-chat, or landing #708 first, would narrow this diff to the C2–C5 work and make it much easier to read when it comes out of draft.

I have not reviewed DiscordMsgBox.cpp or the ClientLibrary/PacketFunctions wiring in depth — happy to do that when you take it out of draft.


Generated by Claude Code

Comment thread src/source/GameLogic/Discord/ServerIntegration.cpp Outdated
Comment thread src/source/GameLogic/Discord/ServerIntegration.cpp
Says next to IsBridgedSender that the prefix is a convention of the
server (enforced in character names by MUnique/OpenMU#1174), not something
the client can verify, and that ExternalChatMessage is the authoritative
signal where the server sends it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
…to claude/discord-presence-mumain-597-l2q09d-server-info

# Conflicts:
#	src/source/UI/NewUI/Options/NewUIOptionWindow.cpp
… claude/discord-presence-mumain-597-l2q09d-server-info
- The Rich Presence application id is only taken from the server when it
  is a Discord snowflake (17 to 20 digits); anything else falls back to
  config.ini, like the invite link is checked before it's offered.
- Asking the server again keeps its previous answer until the new one
  arrives, so the account dialog doesn't show "not linked" for a round
  trip. The answer is forgotten on entering the world instead, so a
  previous server's info doesn't stay when the new server doesn't answer.
- Opening the account dialog asks again, so the next opening reflects a
  link made in Discord meanwhile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
OpenMU hands out a new link code at most every 30 seconds and answers in
between with the new DiscordLinkCode result TooSoon (MUnique/OpenMU#1174).
The client tells the player to wait a moment instead of claiming that
linking isn't available; an unknown result still reads as not available.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
…discord-presence-mumain-597-l2q09d-c4-chat

main moved the chat log to UI/HUD/ChatLogWindow (mu::ui::window, drawn
with RmlUi), the message types to UI/Chat/MessageType.h and the chat
posting to UI::Chat::PostChat. The Discord view and the external
messages move along: TYPE_DISCORD_MESSAGE joins MessageType.h, and the
RmlUi right-click no longer offers a whisper to a Discord sender.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ
… claude/discord-presence-mumain-597-l2q09d-server-info

Brings in main through C4. main moved the remaining native windows to
RmlUi, so the server integration's UI moves along:

- The Discord dialog is shown on the generic menu dialog instead of its
  own native message box (UI/Dialogs/DiscordMsgBox).
- The guild window's Discord notice is a line of its RmlUi model.
- The siege commander and tax call sites already use the packet enums
  on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ
…to claude/discord-presence-mumain-597-l2q09d-server-info

The Discord button of the RmlUi options window opens the account dialog
when the server has the Discord integration, and the invite otherwise,
as the native window's button did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants