Skip to content

feat(discord): chat bridge for guild, alliance and world chat (M5) - #1161

Open
sven-n wants to merge 5 commits into
claude/sweet-cray-w8crms-m4from
claude/sweet-cray-w8crms-m5
Open

sven-n wants to merge 5 commits into
claude/sweet-cray-w8crms-m4from
claude/sweet-cray-w8crms-m5

Conversation

@sven-n

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

Copy link
Copy Markdown
Member

Milestone M5 of the Discord integration (#1103). Stacked on #1160 (M4).

What

  • World chat (M5.5):
    • In game: the new chat command /world <message> reaches all players of all game servers. The chat ban and IChatMessageReceivedPlugIn apply.
    • Plumbing: new IEventPublisher.WorldChatMessageAsync and IGameServer.WorldChatMessageAsync, implemented in-memory, as a Dapr topic and in GameServerClient.
    • Display: players see a localized blue message [World] Name: text.
    • Discord: the chat is bridged to the layout channel with isWorldChat (#world-chat).
  • Publishing chat (M5.4, game → Discord):
    • GameEventPublisherPlugIn publishes a new ChatMessageEvent for guild, alliance and world chat.
    • Gated by the new configuration flag Publish guild, alliance and world chat, off by default for privacy.
    • Discord → game uses IEventPublisher.GuildMessageAsync / AllianceMessageAsync / WorldChatMessageAsync.
    • The Dapr EventPublisher moved from GameServer.Host to Dapr.Common, so the Discord host can use it. In the all-in-one deployment, GameServerContainer.EventPublisher is registered as IEventPublisher and resolved lazily, because the publisher depends on the bot.
  • Binding model (M5.1): new aggregate GuildChatBinding with persistent guild id, scope (guild/alliance), provider, Discord server and channel id, hosted flag, bound by and bound at. It has a migration and compiled models, and an FK to Guild with cascade, so disbanding a guild removes its bindings (M5.8). Operator settings Bot__ChatBridge__*: BindingMode (Both/HostedOnly/GuildOwnedOnly), AllowedDiscordServerIds, ReadMessages, MaximumMessageLength, MaximumMessagesPerMinute.
  • Hosted mode (M5.2): /guildchat create [scope] creates #guild-<name> / #alliance-<name> in the new Guilds category of the layout. Only the bot and linked guild members can see it. Visibility uses per-user channel overwrites, synchronized after linking and every 10 status ticks.
  • Guild-owned mode (M5.3): the guild master invites the bot and runs /guildchat bind [scope] in the channel. That requires Manage Channel on that channel; the allowlist applies. Players get a localized in-game notice at login when their guild or alliance chat is mirrored (new GuildChatBridgeNoticePlugIn).
  • Who may post (M5.6):
    • Linking and membership: only linked users (M4) whose selected character still belongs to the account and is a member of the guild or alliance.
    • Limits: Account.ChatBanUntil applies, plus a flood limit (10 messages per minute by default).
    • Sanitizing: mentions are removed, <:name:id> becomes :name:, markdown and line breaks are stripped, and the length is capped. The sender is @ plus the character name, at most 10 characters, so Discord users can't impersonate characters.
  • Sending without privileged intent (M5.7):
    • Without the intent: /say <message> works in any bridged channel.
    • With the intent: with ReadMessages the bot uses the Message Content intent and reads normal messages, and reacts with ❌ when one is rejected.
    • Batching: game → Discord messages are batched per channel up to 2000 characters.
  • Lifecycle (M5.8): bindings are removed when the channel is deleted, the bot leaves the Discord server, /guildchat unbind is used (by the guild master or by users who manage the channel), or the guild is disbanded. When the guild master changes, the binding stays, but only the current master can change it.
  • Persistence helpers: IGuildServerContext.GetGuildMembershipAsync(characterName) and IPlayerContext.GetAccountChatBanUntilAsync(accountId), for EF and in-memory.

Deviations from the issue, for discussion

  • No in-game code for guild-owned binding. The M4 account link already proves control of the guild master character, and Manage Channel proves control of the Discord channel. So /discord bindguild + /bind <code> became /guildchat bind.
  • /say instead of /g and /w. It works in any bridged channel.
  • No Discord roles for hosted guild channels and no GM role sync (M5.9). Without the privileged Server Members intent the bot can't see who has a role, so it uses per-user channel overwrites, which it can read and reconcile. Role sync in guild-owned Discords is not implemented (optional in the issue).
  • No auto-creation of hosted channels at guild creation. The guild master creates the channel with /guildchat create.
  • No dedicated chat packet for MuMain yet. World chat is shown as a localized blue message; the dedicated packet is MuMain C4.

Tests

  • DiscordChatBridgeTest, against the in-memory DB with guilds, an alliance and linked users. It covers:
    • binding by guild or alliance master only, channel in use, mode and allowlist;
    • mirroring game → Discord;
    • posting: sent, not bridged, not linked, not a member, empty, flood, chat ban, world chat;
    • unbinding, and the linked members of guilds and alliances.
  • DiscordChatTextTest (sanitizing), DiscordChatQueueTest (batching).
  • GameEventPublisherPlugInTest: chat publishing on and off, the /world command, and the chat ban for it.
  • GuildMembershipQueryTest: EF query translation.
  • Layout and routing tests.
  • Full solution test run is green.
  • Smoke start of the all-in-one with the bot enabled (invalid token): no DI cycle, and the game servers start.

Not tested: against a real Discord bot or server, and the migration against a real PostgreSQL database.

🤖 Generated with Claude Code

https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ


Generated by Claude Code

claude added 4 commits October 9, 2026 08:17
/world sends a message to all players of all game servers. The game event
publisher plugin publishes the guild, alliance and world chat as
ChatMessageEvent, if it's enabled in its configuration (off by default).
Players are told when they enter the game, if the chat of their guild or
alliance is mirrored to Discord. The Dapr event publisher moves to the
common project, so that the Discord host can send chat messages, too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ
Guild masters bind the chat of their guild or alliance to a channel: on the
Discord server of the game server with /guildchat create, or on their own
Discord server with /guildchat bind. Linked users write into the game with
/say, or with normal messages when the Message Content intent is enabled.

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

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

Copy link
Copy Markdown

Deploying openmudocs with  Cloudflare Pages  Cloudflare Pages

Latest commit: b6dd4db
Status: ✅  Deploy successful!
Preview URL: https://62c753b5.openmudocs.pages.dev
Branch Preview URL: https://claude-sweet-cray-w8crms-m5.openmudocs.pages.dev

View logs

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