Skip to content

feat(discord): Discord integration packets for MuMain - #1174

Open
sven-n wants to merge 4 commits into
claude/sweet-cray-w8crms-m5from
claude/discord-presence-mumain-597-l2q09d-packets
Open

sven-n wants to merge 4 commits into
claude/sweet-cray-w8crms-m5from
claude/discord-presence-mumain-597-l2q09d-packets

Conversation

@sven-n

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

Copy link
Copy Markdown
Member

Packet definitions and server side for the client part of the Discord integration (sven-n/MuMain#696: C2 integration info, C3 link dialog, C4 messages from Discord, C5 guild window). Stacked on #1161 (M5).

Packets (opt-in F5 group, like the chat command list)

Direction Code Name Content
C→S C1 F5 02 DiscordIntegrationInfoRequest Empty. Also announces that the client understands ExternalChatMessage.
S→C C1 F5 03 DiscordIntegrationInfo Account linked; guild, alliance and world chat bridged; Rich Presence application id and large/small image keys; invite URL; linked Discord user name.
C→S C1 F5 04 DiscordLinkCodeRequest Empty.
S→C C1 F5 05 DiscordLinkCode Result (Success / NotAvailable), the one-time code (ABCD-EFGH), minutes valid.
C→S C1 F5 06 DiscordUnlinkRequest Empty. Answered with a new DiscordIntegrationInfo.
S→C C2 F5 07 ExternalChatMessage Source (Discord), scope (Guild / Alliance / World), sender name (48 bytes, not a character), message (variable).

The generated structs, connection extensions, packet docs and packet tests were regenerated from the XML; nothing outside the new packets changed.

Server

  • Feature plugin Discord integration of the client (DiscordIntegrationFeaturePlugIn). Its configuration holds what the client gets: InviteUrl, RichPresenceApplicationId, RichPresenceLargeImageKey, RichPresenceSmallImageKey. These are no secrets, since every player can read them. When the plugin is deactivated, clients get an empty integration info and hide their Discord features.

  • DiscordIntegrationAction collects the info for a player:

    • the configuration
    • the account link, through AccountLinkService (M4)
    • the guild and alliance chat bindings, the same check as GuildChatBridgeNoticePlugIn
    • whether the world chat is published, i.e. the game event publisher is active with Publish chat messages

    It also creates link codes and unlinks. /discord unlink now uses the same unlink, including the AccountUnlinkedEvent.

  • Handlers and view: handlers for F5 02/04/06 in the existing F5 group, plus DiscordIntegrationViewPlugIn. All have MinimumClient(106, 3, Invariant), like the chat command list.

  • Messages from Discord: senders bridged from Discord (the @ prefix of M5, now shared as BridgedChatSender; DiscordChatText.DiscordSenderPrefix refers to it) are sent as ExternalChatMessage to clients that asked for the integration info:

    • guild and alliance chat in ChatViewPlugIn, with the chat prefix @/@@ removed
    • world chat in GameServer.WorldChatMessageAsync

    All other clients get exactly what they got before.

  • Docs: a "The game client" section on the Discord page, and the plugin on the plugins page.

Limitations

  • Sender name: the sender of an ExternalChatMessage is the linked character, as in M5. The Discord display name isn't carried through IEventPublisher yet. The field has room for it (48 bytes).
  • World chat flag: "world chat bridged" means the chat is published. The game server can't see whether the bot's layout has a world chat channel.
  • Binding channel name: no binding channel name is sent, because GuildChatBinding doesn't store it.

Tests

  • Packet tests (generated): 632/632 passed.
  • New:
    • DiscordIntegrationViewPlugInTest: packet contents; link code results; external chat only after the client asked; bridged guild message → ExternalChatMessage for supporting clients and unchanged chat for others; character messages stay normal chat.
    • DiscordIntegrationActionTest: deactivated plugin → no info; configuration and link state; unlink.
  • Full solution dotnet test: all green (3,377 passed, 11 skipped).

Not tested: against a real Discord bot, or a running MuMain client.

The MuMain side follows as a draft PR in sven-n/MuMain. It needs these packets released in MUnique.OpenMU.Network.Packets before it can build in CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj


Generated by Claude Code

Opt-in F5 packets for the client side of the Discord integration
(sven-n/MuMain#696):

- DiscordIntegrationInfoRequest (F5 02) / DiscordIntegrationInfo (F5 03):
  link state, which chats are bridged, the Rich Presence application and
  image keys, and the invite link. The request also announces that the
  client understands ExternalChatMessage.
- DiscordLinkCodeRequest (F5 04) / DiscordLinkCode (F5 05): the one-time
  code to link the account, without typing /discord link.
- DiscordUnlinkRequest (F5 06): removes the link; answered with a new
  DiscordIntegrationInfo.
- ExternalChatMessage (F5 07): a message written in Discord, with source,
  scope (guild, alliance, world) and a sender name which isn't a character.

Generated code, docs and packet tests are regenerated from the XML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
Server side of the new F5 packets:

- New feature plugin "Discord integration of the client" with the values
  the client gets: invite link, Rich Presence application id and image
  keys. Deactivated, the clients get no integration info.
- DiscordIntegrationAction collects the info for a player (configuration,
  account link, guild and alliance chat bindings, published world chat),
  creates link codes and unlinks. /discord unlink uses the same unlink.
- Handlers for DiscordIntegrationInfoRequest, DiscordLinkCodeRequest and
  DiscordUnlinkRequest, and the DiscordIntegrationViewPlugIn which sends
  the answers.
- Messages of senders bridged from Discord (the '@' prefix, now shared as
  BridgedChatSender) go to clients which asked for the integration info as
  ExternalChatMessage: guild and alliance chat in ChatViewPlugIn, world chat
  in GameServer. Other clients keep getting them as before.
- Docs: the game client section of the Discord page and the plugin page.

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

cloudflare-workers-and-pages Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Deploying openmudocs with  Cloudflare Pages  Cloudflare Pages

Latest commit: b4b567d
Status: ✅  Deploy successful!
Preview URL: https://0be7af58.openmudocs.pages.dev
Branch Preview URL: https://claude-discord-presence-muma.openmudocs.pages.dev

View logs

@sven-n sven-n left a comment

Copy link
Copy Markdown
Member 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 stack, including the four client PRs in sven-n/MuMain that consume these packets.

I cross-checked the wire format against the client implementation in sven-n/MuMain#709 (src/source/Network/Discord/DiscordPackets.cpp) and the two agree exactly — ExternalChatMessage at 5/6/7+48/55, DiscordIntegrationInfo at 4/5/6/7/8+20/28+32/60+32/92+100/192+32, DiscordLinkCode at 4/5+10/15. No offset drift.

A few format details I checked and am satisfied with:

  • ExternalChatMessage correctly uses C2HeaderWithSubCode, so a long bridged message isn't constrained to a fixed field.
  • The capability handshake — the client announcing support by sending DiscordIntegrationInfoRequest, with older clients continuing to get prefixed normal chat messages — is a clean way to avoid a client version bump, and ChatViewPlugIn falls back correctly when the view plug-in isn't present.
  • LinkedUserName at 32 bytes exactly matches Discord's maximum username length, so a full-length name leaves no room for a terminator and ends flush with the end of the 224-byte packet. I checked the client side and it's safe — ReadString passes an explicit maxSourceLength to MultiByteToWideChar and terminates the buffer itself, so there's no overread. Worth being aware of if another client implementation ever reads these fields as C strings.

Three comments inline. The two about client-triggered database work are the ones I'd act on; the third is a shared invariant that also affects #1161.


Generated by Claude Code

Comment thread src/GameLogic/PlayerActions/Discord/DiscordIntegrationAction.cs Outdated
Comment thread src/GameLogic/Discord/BridgedChatSender.cs Outdated
sven-n pushed a commit to sven-n/MuMain that referenced this pull request Oct 10, 2026
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
- The guild and alliance chat bindings of a player are read with a
  targeted query (IPlayerContext.GetGuildChatBindingsAsync) instead of
  loading the whole binding table on every integration info request; the
  login notice of M5 uses it too.
- A player gets a new link code at most every 30 seconds
  (DiscordIntegrationAction.LinkCodeCooldown). Every code costs a database
  write and replaces the previous one, so a client asking in a loop
  neither loads the database nor keeps invalidating the code the player
  looks at. DiscordLinkCode gets the result TooSoon for it.
- Character names can't contain the prefix of bridged senders ('@'),
  independently of the configured name expression, so no character can
  pass for a Discord user in the game or in the clients.
- The packet docs say what IsWorldChatBridged means: the world chat is
  published; the server can't tell whether the bot has a channel for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012feEciycvL8PGjsezyRFqj
sven-n pushed a commit to sven-n/MuMain that referenced this pull request Oct 10, 2026
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
…ce-mumain-597-l2q09d-packets

Master moved the validation of character names to
CharacterInitialization.IsValidCharacterName, which rejects the prefix of
bridged senders now. The unlinking uses AccountLinkService.UnlinkAccountAsync,
which publishes the AccountUnlinkedEvent itself since M5.

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

This branch has not been deployed

No deployments
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