Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs-website/docs/admin-panel/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ also define which monsters are bosses, by their monster numbers (e.g. 275 for
Kundun), and which levels and master levels are milestones (by default, level
400).

## Discord integration of the client

The feature plugin *Discord integration of the client* tells game clients which
ask for it how the server is connected to Discord: the invite link, the Discord
application of the Rich Presence, the link of the account and which chats are
mirrored. It also lets players link their account from the client, and sends
them the messages written in Discord as such. See
[Discord](../server-features/discord.md#the-game-client).

## Server announcements

The *Server announcements* plugin shows a list of announcements to all players,
Expand Down
27 changes: 27 additions & 0 deletions docs-website/docs/server-features/discord.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,33 @@ Without the intent, players use `/say <message>`. Discord only approves the
intent for verified bots, which are in more than 100 Discord servers, with a
reason.

## The game client

[MuMain](https://github.com/sven-n/MuMain), the open source game client, has
its own Discord features. It asks the server how it's connected to Discord, when
the character enters the game:

* its **Discord** button opens the invite to your Discord server,
* its **Rich Presence** shows the state of the player in the Discord profile,
with your Discord application,
* players link and unlink their account in a dialog, without typing
`/discord link`,
* messages written in Discord are shown as such, with the colours of the guild,
alliance or world chat, and can't be mistaken for messages of a character.

For this, activate the feature plugin *Discord integration of the client* and
fill its configuration:

| Setting | Description |
|---|---|
| `InviteUrl` | The invite link to your Discord server, e.g. `https://discord.gg/abc123`. Create one which doesn't expire. |
| `RichPresenceApplicationId` | The id of the Discord application which the client uses for the Rich Presence, usually the one of your bot (*General Information → Application ID* in the developer portal). |
| `RichPresenceLargeImageKey`, `RichPresenceSmallImageKey` | The keys of the images of the Rich Presence, uploaded under *Rich Presence → Art Assets*. |

Every player can read these values, so they must not contain secrets. Older game
clients don't ask for them; they keep getting the messages from Discord as
normal chat messages with the `@` prefix.

## Settings

All settings are part of the `Discord` section of the configuration, so they can
Expand Down
18 changes: 18 additions & 0 deletions docs/Packets/C1-F5-02-DiscordIntegrationInfoRequest_by-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# C1 F5 02 - DiscordIntegrationInfoRequest (by client)

## Is sent when

A client which supports the Discord integration requests how the server is connected to Discord. It's usually sent after the character entered the game world, and again when the player opens a Discord dialog. By sending it, the client also announces that it understands the ExternalChatMessage.

## Causes the following actions on the server side

The server sends a DiscordIntegrationInfo message, and from then on sends messages which were written in Discord as ExternalChatMessage.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
| 1 | 1 | Byte | 4 | Packet header - length of the packet |
| 2 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 3 | 1 | Byte | 0x02 | Packet header - sub packet type identifier |
27 changes: 27 additions & 0 deletions docs/Packets/C1-F5-03-DiscordIntegrationInfo_by-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# C1 F5 03 - DiscordIntegrationInfo (by server)

## Is sent when

After the client requested the Discord integration info, and after the player unlinked the account.

## Causes the following actions on the client side

The client configures its Discord features with it: the Rich Presence, a button which opens the invite, the account link dialog and the notice that the chat is mirrored.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
| 1 | 1 | Byte | 224 | Packet header - length of the packet |
| 2 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 3 | 1 | Byte | 0x03 | Packet header - sub packet type identifier |
| 4 | 1 | Boolean | | IsAccountLinked; Defines if the account is linked to a Discord user. |
| 5 | 1 | Boolean | | IsGuildChatBridged; Defines if the chat of the guild of the character is mirrored to a Discord channel. |
| 6 | 1 | Boolean | | IsAllianceChatBridged; Defines if the chat of the alliance of the character is mirrored to a Discord channel. |
| 7 | 1 | Boolean | | IsWorldChatBridged; Defines if the world chat leaves the game for Discord: the server publishes it, so a Discord bot with a world chat channel mirrors it. The server can't tell whether the bot has such a channel. |
| 8 | 20 | String | | RichPresenceApplicationId; The id of the Discord application which the client uses for the Rich Presence. Empty, if the server has none. |
| 28 | 32 | String | | RichPresenceLargeImageKey; The key of the large image of the Rich Presence, as uploaded to the Discord application. Empty for no image. |
| 60 | 32 | String | | RichPresenceSmallImageKey; The key of the small image of the Rich Presence, as uploaded to the Discord application. Empty for no image. |
| 92 | 100 | String | | InviteUrl; The invite link to the Discord server of the game server, e.g. 'https://discord.gg/abc123'. Empty, if the server has none. |
| 192 | 32 | String | | LinkedUserName; The name of the Discord user which the account is linked to. Empty, if the account isn't linked. |
18 changes: 18 additions & 0 deletions docs/Packets/C1-F5-04-DiscordLinkCodeRequest_by-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# C1 F5 04 - DiscordLinkCodeRequest (by client)

## Is sent when

The player wants to link the account to a Discord user, e.g. with a button of a Discord dialog.

## Causes the following actions on the server side

The server creates a one-time code and sends it with a DiscordLinkCode message. The player enters the code in Discord to complete the link.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
| 1 | 1 | Byte | 4 | Packet header - length of the packet |
| 2 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 3 | 1 | Byte | 0x04 | Packet header - sub packet type identifier |
31 changes: 31 additions & 0 deletions docs/Packets/C1-F5-05-DiscordLinkCode_by-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# C1 F5 05 - DiscordLinkCode (by server)

## Is sent when

After the client requested a code to link the account to a Discord user.

## Causes the following actions on the client side

The client shows the code, so that the player can enter it in Discord.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
| 1 | 1 | Byte | 16 | Packet header - length of the packet |
| 2 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 3 | 1 | Byte | 0x05 | Packet header - sub packet type identifier |
| 4 | 1 | DiscordLinkCodeResult | | Result |
| 5 | 10 | String | | LinkCode; The one-time code, e.g. 'ABCD-EFGH'. The player enters it in Discord with '/link'. |
| 15 | 1 | Byte | | ValidMinutes; The number of minutes the code is valid. |

### DiscordLinkCodeResult Enum

The result of a request of a code to link the account to a Discord user.

| Value | Name | Description |
|-------|------|-------------|
| 0 | Success | The code was created. |
| 1 | NotAvailable | Linking isn't available, e.g. because the server isn't connected to Discord. |
| 2 | TooSoon | A code was requested a moment ago. Every code replaces the previous one, so the server hands out new ones only every few seconds. |
18 changes: 18 additions & 0 deletions docs/Packets/C1-F5-06-DiscordUnlinkRequest_by-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# C1 F5 06 - DiscordUnlinkRequest (by client)

## Is sent when

The player wants to remove the link of the account to a Discord user.

## Causes the following actions on the server side

The server removes the link and sends an updated DiscordIntegrationInfo message.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
| 1 | 1 | Byte | 4 | Packet header - length of the packet |
| 2 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 3 | 1 | Byte | 0x06 | Packet header - sub packet type identifier |
40 changes: 40 additions & 0 deletions docs/Packets/C2-F5-07-ExternalChatMessage_by-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# C2 F5 07 - ExternalChatMessage (by server)

## Is sent when

A message was written outside of the game, e.g. in a Discord channel which is bound to the chat of a guild, an alliance or the world chat. It's only sent to clients which requested the DiscordIntegrationInfo before; other clients get it as normal chat message, with a prefix in front of the sender.

## Causes the following actions on the client side

The client shows the message in the chat of its scope, marked with its source, so that it can't be mistaken for a message of a character.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC2 | [Packet type](PacketTypes.md) |
| 1 | 2 | Short | | Packet header - length of the packet |
| 3 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 4 | 1 | Byte | 0x07 | Packet header - sub packet type identifier |
| 5 | 1 | ExternalChatSource | | Source |
| 6 | 1 | ExternalChatScope | | Scope |
| 7 | 48 | String | | SenderName; The name of the sender, without a prefix. It's not a character name, so it can't be whispered. |
| 55 | | String | | Message |

### ExternalChatSource Enum

Where an external chat message was written.

| Value | Name | Description |
|-------|------|-------------|
| 0 | Discord | The message was written in Discord. |

### ExternalChatScope Enum

The chat of the game which an external chat message belongs to.

| Value | Name | Description |
|-------|------|-------------|
| 0 | Guild | The chat of the guild. |
| 1 | Alliance | The chat of the alliance. |
| 2 | World | The world chat, which all players of all game servers can read. |
3 changes: 3 additions & 0 deletions docs/Packets/ClientToServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@
* [C1 F3 30 - SaveKeyConfiguration (by client)](C1-F3-30-SaveKeyConfiguration_by-client.md)
* [C1 F3 52 - AddMasterSkillPoint (by client)](C1-F3-52-AddMasterSkillPoint_by-client.md)
* [C1 F5 00 - ChatCommandListRequest (by client)](C1-F5-00-ChatCommandListRequest_by-client.md)
* [C1 F5 02 - DiscordIntegrationInfoRequest (by client)](C1-F5-02-DiscordIntegrationInfoRequest_by-client.md)
* [C1 F5 04 - DiscordLinkCodeRequest (by client)](C1-F5-04-DiscordLinkCodeRequest_by-client.md)
* [C1 F5 06 - DiscordUnlinkRequest (by client)](C1-F5-06-DiscordUnlinkRequest_by-client.md)
* [C1 F6 0A - QuestSelectRequest (by client)](C1-F6-0A-QuestSelectRequest_by-client.md)
* [C1 F6 0B - QuestProceedRequest (by client)](C1-F6-0B-QuestProceedRequest_by-client.md)
* [C1 F6 0D - QuestCompletionRequest (by client)](C1-F6-0D-QuestCompletionRequest_by-client.md)
Expand Down
3 changes: 3 additions & 0 deletions docs/Packets/ServerToClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@
* [C1 F3 52 - MasterSkillLevelUpdate (by server)](C1-F3-52-MasterSkillLevelUpdate_by-server.md)
* [C2 F3 53 - MasterSkillList (by server)](C2-F3-53-MasterSkillList_by-server.md)
* [C2 F5 01 - AvailableChatCommand (by server)](C2-F5-01-AvailableChatCommand_by-server.md)
* [C1 F5 03 - DiscordIntegrationInfo (by server)](C1-F5-03-DiscordIntegrationInfo_by-server.md)
* [C1 F5 05 - DiscordLinkCode (by server)](C1-F5-05-DiscordLinkCode_by-server.md)
* [C2 F5 07 - ExternalChatMessage (by server)](C2-F5-07-ExternalChatMessage_by-server.md)
* [C1 F6 03 - QuestEventResponse (by server)](C1-F6-03-QuestEventResponse_by-server.md)
* [C1 F6 0A - AvailableQuests (by server)](C1-F6-0A-AvailableQuests_by-server.md)
* [C1 F6 0B - QuestStepInfo (by server)](C1-F6-0B-QuestStepInfo_by-server.md)
Expand Down
3 changes: 2 additions & 1 deletion src/Discord/ChatBridge/DiscordChatText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.Discord.ChatBridge;

using System.Text;
using System.Text.RegularExpressions;
using MUnique.OpenMU.GameLogic.Discord;

/// <summary>
/// Converts the texts of chat messages between the game and Discord.
Expand All @@ -16,7 +17,7 @@ public static partial class DiscordChatText
/// The prefix of the names of Discord users in the game. Character names can't contain it,
/// so Discord users can't pretend to be a character in the game.
/// </summary>
public const char DiscordSenderPrefix = '@';
public const char DiscordSenderPrefix = BridgedChatSender.Prefix;

/// <summary>
/// The maximum length of a sender name in the game.
Expand Down
22 changes: 20 additions & 2 deletions src/GameLogic/CharacterInitialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.GameLogic;

using System.Text.RegularExpressions;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Discord;
using MUnique.OpenMU.Persistence;

/// <summary>
Expand All @@ -25,8 +26,25 @@ public static class CharacterInitialization
/// <returns><c>true</c>, if the name is valid; otherwise, <c>false</c>.</returns>
public static bool IsValidCharacterName(this GameConfiguration configuration, string name)
{
return string.IsNullOrWhiteSpace(configuration.CharacterNameRegex)
|| Regex.IsMatch(name, configuration.CharacterNameRegex);
return IsValidCharacterName(name, configuration.CharacterNameRegex);
}

/// <summary>
/// Determines whether the name is valid for a character, according to a regular expression.
/// The prefix of bridged chat senders is never allowed, whatever the regular expression allows,
/// so that a character can't be mistaken for a Discord user.
/// </summary>
/// <param name="name">The name of the character.</param>
/// <param name="nameRegex">The regular expression, see <see cref="GameConfiguration.CharacterNameRegex"/>.</param>
/// <returns><c>true</c>, if the name is valid; otherwise, <c>false</c>.</returns>
public static bool IsValidCharacterName(string name, string? nameRegex)
{
if (!BridgedChatSender.IsAllowedInCharacterName(name))
{
return false;
}

return string.IsNullOrWhiteSpace(nameRegex) || Regex.IsMatch(name, nameRegex);
}

/// <summary>
Expand Down
40 changes: 40 additions & 0 deletions src/GameLogic/Discord/BridgedChatSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// <copyright file="BridgedChatSender.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.Discord;

/// <summary>
/// The sender of a chat message which was written outside of the game, e.g. in Discord, and bridged into it.
/// Its name is the name of a character with a prefix, which character names can't contain
/// (see <see cref="IsAllowedInCharacterName"/>, enforced by <see cref="CharacterInitialization.IsValidCharacterName(string, string?)"/>),
/// so it can't be mistaken for a character.
/// </summary>
public static class BridgedChatSender
{
/// <summary>
/// The prefix of the names of bridged senders.
/// </summary>
public const char Prefix = '@';

/// <summary>
/// Determines whether a name may be the name of a character: it must not contain the prefix.
/// </summary>
/// <param name="name">The name.</param>
/// <returns><c>true</c>, if a character may have the name.</returns>
public static bool IsAllowedInCharacterName(string name) => !name.Contains(Prefix, StringComparison.Ordinal);

/// <summary>
/// Determines whether the sender of a chat message is a bridged sender.
/// </summary>
/// <param name="sender">The sender.</param>
/// <returns><c>true</c>, if the sender is bridged.</returns>
public static bool IsBridged(string sender) => sender.Length > 1 && sender[0] == Prefix;

/// <summary>
/// Gets the name of a bridged sender without its prefix.
/// </summary>
/// <param name="sender">The bridged sender.</param>
/// <returns>The name without its prefix.</returns>
public static string GetName(string sender) => IsBridged(sender) ? sender[1..] : sender;
}
34 changes: 34 additions & 0 deletions src/GameLogic/Discord/DiscordIntegrationConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// <copyright file="DiscordIntegrationConfiguration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.Discord;

/// <summary>
/// The configuration of the Discord integration, which the game client gets with the Discord integration info.
/// It holds no secrets: every player can read these values.
/// </summary>
public class DiscordIntegrationConfiguration
{
/// <summary>
/// Gets or sets the invite link to the Discord server of the game server, e.g. <c>https://discord.gg/abc123</c>.
/// The client opens it with its Discord button. Empty for none.
/// </summary>
public string InviteUrl { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the id of the Discord application which the client uses for the Rich Presence.
/// Usually it's the application of the bot. Empty for none, then the client uses its own configuration.
/// </summary>
public string RichPresenceApplicationId { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the key of the large image of the Rich Presence, as uploaded to the Discord application.
/// </summary>
public string RichPresenceLargeImageKey { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the key of the small image of the Rich Presence, as uploaded to the Discord application.
/// </summary>
public string RichPresenceSmallImageKey { get; set; } = string.Empty;
}
Loading
Loading