Skip to content

feat(discord): post game events to Discord webhooks - #1155

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

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

Conversation

@sven-n

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

Copy link
Copy Markdown
Member

Implements milestone M1 of the Discord integration epic (#1097): announcements of game events in Discord channels, through webhooks. Builds on the game event stream of M0 (#1151).

Closes #1099

Changes

Boss kills and level milestones (80964056)

  • New events BossKilledEvent and CharacterLevelMilestoneEvent, published by the GameEventPublisherPlugIn.
  • Its configuration gets the filters: which monsters are bosses (monster numbers — the monster definitions don't say which monsters are bosses), and which levels / master levels are milestones (default: level 400). Resources in English and German.

New project MUnique.OpenMU.Discord (74a52e44)

It only depends on Interfaces, so it can be hosted by the all-in-one server now and by the separate bot service of M2 later.

  • DiscordWebhookNotifier (IGameEventListener): posts the events as embeds, one webhook per category — Events (mini games, invasions), CastleSiege, WorldNews (boss kills, rare drops, milestones), Notices (GM notices).
  • DiscordMessageFormatter: localized messages (Properties/Resources.resx, English and German), names of maps/monsters/items translated, markdown in player texts escaped, times as Discord time markup (<t:…:R>) so every user sees them in their own time zone and language. Uninteresting events (e.g. mini game started, idle siege states, mini games without winner) aren't posted.
  • DiscordWebhookSender: a bounded queue per webhook, sent in the background:
    • queued embeds are combined into one message (up to 10), so bursts don't hit the rate limit,
    • the rate limit headers and Retry-After / retry_after are respected, server errors are retried with backoff, rejected messages (e.g. deleted webhook) are not retried,
    • when the queue is full, the oldest messages are dropped (logged),
    • allowed_mentions is always empty, so player texts can't ping @everyone or roles.
  • Duplicates: mini games which open all levels at once (Blood Castle) are announced once; EventAnnouncingServerIds restricts which game servers announce mini games and invasions.

All-in-one server and docs (ab9a0dc5)

  • The all-in-one Startup registers the notifier when at least one webhook is configured in the Discord configuration section, e.g. via Discord__Webhooks__Events. The URLs are secrets, so they're part of the process configuration, not the game configuration in the database.
  • New docs page Server features → Discord (setup, settings, limits); the plugins page lists the new events and filters.

Notes for review

  • Deviation from [Discord] M1: Announcements via webhooks (outbound only) #1099: the issue planned a feature plug-in in GameLogic which implements the plug-in points directly. With the event stream from M0 in place, the notifier is an event listener in its own project instead: no HTTP code in GameLogic, and M2.4 ("move announcements to the event stream") is already covered. The game-related filters (bosses, milestones, drop qualities) stay in the plug-in configuration in the admin panel; the Discord-specific settings are in the process configuration.
  • Distributed deployment isn't covered yet: the events are published to the Dapr topic, but nothing subscribes; that comes with the bot host in M2.
  • The solution file got the new project entry only (added by hand; dotnet sln add would have rewritten the whole file).

Testing

  • dotnet test tests/MUnique.OpenMU.Tests -p:ci=true: all 1647 tests pass (22 new): formatter (translation, escaping, item names, castle siege owner, ignored events), sender (payload and disabled mentions, combining of queued embeds, retry after rate limit, no retry of rejected messages, dropping the oldest messages) — against a fake HTTP handler, no real Discord — notifier (one announcement per mini game opening, server filter, categories without webhook), and the publisher plug-in (boss kills, milestones).
  • Binding of the settings from environment variables (enum-keyed webhook dictionary, server id list) checked with a scratch program.
  • Started the all-in-one server in demo mode with webhooks configured: it starts normally. Not tested end-to-end against a real Discord server.
  • Unrelated: in demo mode, the castle siege logs An item with the same key has already been added. Key: …CastleSiegeGuildRegistration at startup. This happens on master too (a race between the game servers), so I left it alone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ


Generated by Claude Code

claude added 3 commits October 8, 2026 20:04
The game event publisher now also publishes the kills of boss monsters and
characters which reach a notable level or master level. Which monsters are
bosses and which levels are milestones is configured in the plug-in, because
the monster definitions don't tell which monsters are bosses.

Refs #1099

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ
Adds the project MUnique.OpenMU.Discord with a DiscordWebhookNotifier,
which listens to the game events and posts them as embeds to Discord
webhooks, one webhook per category (events, castle siege, world news,
notices).

* The messages are localized in the configured language, names of
  configuration objects are translated, and markdown in texts of players
  is escaped.
* Times are posted as Discord time markup, so each user sees them in
  their own time zone and language.
* Mini games which open several levels at once are announced just once,
  and the servers which announce events can be restricted, to avoid
  duplicates.
* Each webhook has a bounded queue which is sent in the background. Queued
  embeds are combined into one message, the rate limit headers and
  Retry-After are respected, failed requests are retried, and the oldest
  messages are dropped when the queue is full.
* Mentions are disabled, so texts of players can't ping anybody.

The project only depends on the interfaces, so it can be hosted by the
all-in-one server as well as by a separate bot service later.

Refs #1099

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ
The all-in-one server registers the Discord webhook notifier as listener
for the game events, when at least one webhook is configured in the
"Discord" configuration section, e.g. with the environment variable
Discord__Webhooks__Events.

The new documentation page describes how to set it up.

Refs #1099

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 8, 2026 •

Copy link
Copy Markdown

Deploying openmudocs with  Cloudflare Pages  Cloudflare Pages

Latest commit: f2b2600
Status: ✅  Deploy successful!
Preview URL: https://047d1349.openmudocs.pages.dev
Branch Preview URL: https://claude-sweet-cray-w8crms.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.

Reviewed as part of a pass over the whole Discord stack (M1–M6 plus the client packet PR, and the four MuMain client PRs).

M1 is in good shape. The things I specifically went looking for are handled: allowed_mentions is always empty so player text can't ping roles, markdown in player text is escaped, the queue is bounded with DropOldest and logs what it drops, rate-limit headers and both forms of Retry-After are respected, rejections aren't retried, and the mini-game duplicate suppression has actual retention cleanup rather than growing forever. The tests cover the interesting paths against a fake handler.

Two comments inline, both minor and neither blocking. Nothing I found in M1 is a correctness bug on today's event set.

One note for the stack rather than this PR: DiscordWebhookSettings is renamed to DiscordSettings in M2 and the notifier is renamed alongside it, so some of what this PR adds is churned by the next one. Not a problem if they land in order, just worth knowing if M1 is ever merged on its own.


Generated by Claude Code

Comment thread src/Discord/DiscordWebhookSender.cs
Comment thread src/Startup/DiscordServiceCollectionExtensions.cs Outdated
…8crms

# Conflicts:
#	docs-website/sidebars.js
#	src/GameLogic/Properties/PlugInResources.de.resx
#	src/GameLogic/Properties/PlugInResources.resx
…equests

Titles, descriptions and footers are cut to the lengths which Discord
accepts, and the embeds of one message don't exceed the total length.
Content which Discord rejects with 400 is logged with the response, not as
an invalid webhook. The HTTP client times out after 10 seconds instead of
100, and a timed-out request is retried.

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.

[Discord] M1: Announcements via webhooks (outbound only)

2 participants