diff --git a/docs-website/docs/admin-panel/servers.md b/docs-website/docs/admin-panel/servers.md index 31ddd46565..a4a05ce79a 100644 --- a/docs-website/docs/admin-panel/servers.md +++ b/docs-website/docs/admin-panel/servers.md @@ -128,12 +128,18 @@ forward it in your router or docker compose file. See [Ports](../reference/ports ## Global messages -The card at the top of the page sends a message to the players — the golden -message in the centre of the screen. +The card at the top of the page sends a message to the players. 1. Type the text. -2. Choose the target: *All game servers*, or one specific started game server. -3. Click **Send**. +2. Choose the message type: + * *Golden (center)*: the golden message in the centre of the screen, like + `/goldnotice`. + * *Blue (system)*: a blue system message in the chat. + * *Slide (top band)*: a notice which scrolls once through the band at the + top of the screen, like `/slidenotice`. Clients older than season 1 don't + have this band and get the golden message instead. +3. Choose the target: *All game servers*, or one specific started game server. +4. Click **Send**. Only started game servers can be targeted. If the target stopped in the meantime, the panel keeps your text so you can pick another target and retry. diff --git a/docs/Packets/C1-0D-SlideNotice_by-server.md b/docs/Packets/C1-0D-SlideNotice_by-server.md new file mode 100644 index 0000000000..c928408cf7 --- /dev/null +++ b/docs/Packets/C1-0D-SlideNotice_by-server.md @@ -0,0 +1,23 @@ +# C1 0D - SlideNotice (by server) + +## Is sent when + +A game master or the server sends a notice which should scroll across the top of the screen. + +## Causes the following actions on the client side + +The client scrolls the message through the band at the top of the screen, as often as the loop count says. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | | Packet header - length of the packet | +| 2 | 1 | Byte | 0x0D | Packet header - packet type identifier | +| 3 | 1 | Byte | 14 | Type; The band and queueing of the message. 10 to 12 use the help band, 13 to 15 the notice band. Within a band, the first value drops the message when it could not be shown within a minute, the second one queues it, and the third one puts it in front of the queue, ignoring the loop delay. | +| 4 | 1 | Byte | 1 | LoopCount; How often the message is shown. The client ignores messages with more than 30 loops. | +| 6 | 2 | ShortLittleEndian | | LoopDelay; The delay between the loops, in seconds. | +| 8 | 4 | IntegerLittleEndian | | Color; The color of the text, as 0xAABBGGRR. | +| 12 | 1 | Byte | | Speed; The scroll speed, multiplied by 10. 0 means the default speed. | +| 13 | | String | | Message | \ No newline at end of file diff --git a/docs/Packets/ServerToClient.md b/docs/Packets/ServerToClient.md index 2805673cb1..72747d44be 100644 --- a/docs/Packets/ServerToClient.md +++ b/docs/Packets/ServerToClient.md @@ -5,6 +5,7 @@ * [C1 07 - MagicEffectStatus (by server)](C1-07-MagicEffectStatus_by-server.md) * [C1 0B - MapEventState (by server)](C1-0B-MapEventState_by-server.md) * [C1 0D - ServerMessage (by server)](C1-0D-ServerMessage_by-server.md) + * [C1 0D - SlideNotice (by server)](C1-0D-SlideNotice_by-server.md) * [C1 0F - WeatherStatusUpdate (by server)](C1-0F-WeatherStatusUpdate_by-server.md) * [C1 10 - ObjectWalked075 (by server)](C1-10-ObjectWalked075_by-server.md) * [C1 11 - ObjectHit (by server)](C1-11-ObjectHit_by-server.md) diff --git a/src/GameLogic/PlugIns/ChatCommands/SlideNoticeChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/SlideNoticeChatCommandPlugIn.cs new file mode 100644 index 0000000000..d6b7ede1f4 --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/SlideNoticeChatCommandPlugIn.cs @@ -0,0 +1,41 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; + +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using MUnique.OpenMU.PlugIns; + +/// +/// A chat command plugin which sends a notice to all players that scrolls across the top of their screen. +/// +[Guid("6F0B8F37-3C1D-4E5A-9B7E-2D4C8A1F5E93")] +[PlugIn] +[Display(Name = nameof(PlugInResources.SlideNoticeChatCommandPlugIn_Name), Description = nameof(PlugInResources.SlideNoticeChatCommandPlugIn_Description), ResourceType = typeof(PlugInResources))] +[ChatCommandHelp(CommandKey, CharacterStatus.GameMaster)] +public class SlideNoticeChatCommandPlugIn : IChatCommandPlugIn +{ + private const string CommandKey = "/slidenotice"; + + /// + public string Key => CommandKey; + + /// + public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster; + + /// + public async ValueTask HandleCommandAsync(Player player, string command) + { + var regex = new Regex(Regex.Escape(CommandKey)); + var message = regex.Replace(command, string.Empty, 1)?.Trim(); + + if (string.IsNullOrWhiteSpace(message)) + { + return; + } + + await player.GameContext.SendGlobalMessageAsync(message, Interfaces.MessageType.SlideNotice).ConfigureAwait(false); + } +} diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index 8065227541..8a9eaf60fc 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -3221,6 +3221,24 @@ public static string SkipMiniGameWaitChatCommandPlugIn_Name { } } + /// + /// Looks up a localized string similar to Handles the chat command '/slidenotice message'. Sends a global notice to all players of the game which scrolls across the top of their screen.. + /// + public static string SlideNoticeChatCommandPlugIn_Description { + get { + return ResourceManager.GetString("SlideNoticeChatCommandPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Slide notice chat command. + /// + public static string SlideNoticeChatCommandPlugIn_Name { + get { + return ResourceManager.GetString("SlideNoticeChatCommandPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Plugin which handles the small complex potion consumption.. /// diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index 3db454069a..46277d16f7 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -651,6 +651,12 @@ Handles the chat command '/goldnotice message'. Sends a global golden notice message to all players of the game. + + Slide notice chat command + + + Handles the chat command '/slidenotice message'. Sends a global notice to all players of the game which scrolls across the top of their screen. + NPC open merchant chat command diff --git a/src/GameServer/RemoteView/ShowMessagePlugIn.cs b/src/GameServer/RemoteView/ShowMessagePlugIn.cs index dc9775d5c4..0d89225652 100644 --- a/src/GameServer/RemoteView/ShowMessagePlugIn.cs +++ b/src/GameServer/RemoteView/ShowMessagePlugIn.cs @@ -48,6 +48,18 @@ public async ValueTask ShowMessageAsync(string message, OpenMU.Interfaces.Messag return; } + if (messageType == Interfaces.MessageType.SlideNotice) + { + if (this._player.ClientVersion.Season > 0) + { + await this.SendSlideNoticeAsync(message).ConfigureAwait(false); + return; + } + + // Clients before season 1 don't know the slide notice. + messageType = Interfaces.MessageType.GoldenCenter; + } + const string messagePrefix = "000000000"; var content = this._player.ClientVersion.Season > 0 ? messagePrefix + message : message; await this._player.Connection.SendServerMessageAsync(ConvertMessageType(messageType), content).ConfigureAwait(false); @@ -63,4 +75,17 @@ private static ServerMessage.MessageType ConvertMessageType(OpenMU.Interfaces.Me _ => throw new NotImplementedException($"Case for {messageType} is not implemented."), }; } -} \ No newline at end of file + + /// + /// Sends a notice which the client scrolls through the notice band at the top of the screen. + /// + /// The message. + private ValueTask SendSlideNoticeAsync(string message) + { + // White text at the default speed of the client. The type and the loop count keep + // the defaults of the packet: queued in the notice band, shown once. + const uint white = 0xFFFFFFFF; + const byte defaultSpeed = 0; + return this._player.Connection.SendSlideNoticeAsync(loopDelay: 0, white, defaultSpeed, message); + } +} diff --git a/src/Interfaces/IGameServer.cs b/src/Interfaces/IGameServer.cs index 83a075cd10..f442296cd2 100644 --- a/src/Interfaces/IGameServer.cs +++ b/src/Interfaces/IGameServer.cs @@ -54,6 +54,11 @@ public enum MessageType /// The message is a guild notice (green center). /// GuildNotice = 2, + + /// + /// The message scrolls in bold across the top of the screen (slide notice). + /// + SlideNotice = 3, } /// diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs index 5728b45871..7b3ad73a82 100644 --- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs +++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs @@ -4135,6 +4135,44 @@ int WritePacket() await connection.SendAsync(WritePacket).ConfigureAwait(false); } + /// + /// Sends a to this connection. + /// + /// The connection. + /// The delay between the loops, in seconds. + /// The color of the text, as 0xAABBGGRR. + /// The scroll speed, multiplied by 10. 0 means the default speed. + /// The message. + /// How often the message is shown. The client ignores messages with more than 30 loops. + /// The band and queueing of the message. 10 to 12 use the help band, 13 to 15 the notice band. Within a band, the first value drops the message when it could not be shown within a minute, the second one queues it, and the third one puts it in front of the queue, ignoring the loop delay. + /// + /// Is sent by the server when: A game master or the server sends a notice which should scroll across the top of the screen. + /// Causes reaction on client side: The client scrolls the message through the band at the top of the screen, as often as the loop count says. + /// + public static async ValueTask SendSlideNoticeAsync(this IConnection? connection, ushort @loopDelay, uint @color, byte @speed, string @message, byte @loopCount = 1, byte @type = 14) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = SlideNoticeRef.GetRequiredSize(message); + var packet = new SlideNoticeRef(connection.Output.GetSpan(length)[..length]); + packet.Type = @type; + packet.LoopCount = @loopCount; + packet.LoopDelay = @loopDelay; + packet.Color = @color; + packet.Speed = @speed; + packet.Message = @message; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + /// /// Sends a to this connection. /// diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs index c3e544c052..861d6223b1 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs @@ -20252,6 +20252,139 @@ public string Message } +/// +/// Is sent by the server when: A game master or the server sends a notice which should scroll across the top of the screen. +/// Causes reaction on client side: The client scrolls the message through the band at the top of the screen, as often as the loop count says. +/// +public readonly struct SlideNotice +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public SlideNotice(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private SlideNotice(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)data.Length; + this.Type = 14; + this.LoopCount = 1; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0x0D; + + /// + /// Gets the header of this packet. + /// + public C1Header Header => new (this._data); + + /// + /// Gets or sets the band and queueing of the message. 10 to 12 use the help band, 13 to 15 the notice band. Within a band, the first value drops the message when it could not be shown within a minute, the second one queues it, and the third one puts it in front of the queue, ignoring the loop delay. + /// + public byte Type + { + get => this._data.Span[3]; + set => this._data.Span[3] = value; + } + + /// + /// Gets or sets how often the message is shown. The client ignores messages with more than 30 loops. + /// + public byte LoopCount + { + get => this._data.Span[4]; + set => this._data.Span[4] = value; + } + + /// + /// Gets or sets the delay between the loops, in seconds. + /// + public ushort LoopDelay + { + get => ReadUInt16LittleEndian(this._data.Span[6..]); + set => WriteUInt16LittleEndian(this._data.Span[6..], value); + } + + /// + /// Gets or sets the color of the text, as 0xAABBGGRR. + /// + public uint Color + { + get => ReadUInt32LittleEndian(this._data.Span[8..]); + set => WriteUInt32LittleEndian(this._data.Span[8..], value); + } + + /// + /// Gets or sets the scroll speed, multiplied by 10. 0 means the default speed. + /// + public byte Speed + { + get => this._data.Span[12]; + set => this._data.Span[12] = value; + } + + /// + /// Gets or sets the message. + /// + public string Message + { + get => this._data.Span.ExtractString(13, this._data.Length - 13, System.Text.Encoding.UTF8); + set => this._data.Slice(13).Span.WriteString(value, System.Text.Encoding.UTF8); + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator SlideNotice(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(SlideNotice packet) => packet._data; + + /// + /// Calculates the size of the packet for the specified field content. + /// + /// The content of the variable 'Message' field from which the size will be calculated. + public static int GetRequiredSize(string content) => System.Text.Encoding.UTF8.GetByteCount(content) + 1 + 13; + + /// + /// Calculates the size of the packet for the specified field content. + /// + /// The content length in bytes of the variable 'Message' field from which the size will be calculated. + public static int GetRequiredSize(int contentLength) => contentLength + 1 + 13; +} + + /// /// Is sent by the server when: A player requested to join a guild. This message is sent then to the guild master. /// Causes reaction on client side: The guild master gets a message box with the request popping up. diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml index 590cb7fe3c..2bbd26b766 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml @@ -7433,6 +7433,53 @@ + + C1Header + 0D + SlideNotice + ServerToClient + A game master or the server sends a notice which should scroll across the top of the screen. + The client scrolls the message through the band at the top of the screen, as often as the loop count says. + + + 3 + Byte + Type + The band and queueing of the message. 10 to 12 use the help band, 13 to 15 the notice band. Within a band, the first value drops the message when it could not be shown within a minute, the second one queues it, and the third one puts it in front of the queue, ignoring the loop delay. + 14 + + + 4 + Byte + LoopCount + How often the message is shown. The client ignores messages with more than 30 loops. + 1 + + + 6 + ShortLittleEndian + LoopDelay + The delay between the loops, in seconds. + + + 8 + IntegerLittleEndian + Color + The color of the text, as 0xAABBGGRR. + + + 12 + Byte + Speed + The scroll speed, multiplied by 10. 0 means the default speed. + + + 13 + String + Message + + + C1Header 50 diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs index e5051fc3e8..a00ade05b3 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs @@ -19374,6 +19374,139 @@ public string Message } +/// +/// Is sent by the server when: A game master or the server sends a notice which should scroll across the top of the screen. +/// Causes reaction on client side: The client scrolls the message through the band at the top of the screen, as often as the loop count says. +/// +public readonly ref struct SlideNoticeRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public SlideNoticeRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private SlideNoticeRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)data.Length; + this.Type = 14; + this.LoopCount = 1; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0x0D; + + /// + /// Gets the header of this packet. + /// + public C1HeaderRef Header => new (this._data); + + /// + /// Gets or sets the band and queueing of the message. 10 to 12 use the help band, 13 to 15 the notice band. Within a band, the first value drops the message when it could not be shown within a minute, the second one queues it, and the third one puts it in front of the queue, ignoring the loop delay. + /// + public byte Type + { + get => this._data[3]; + set => this._data[3] = value; + } + + /// + /// Gets or sets how often the message is shown. The client ignores messages with more than 30 loops. + /// + public byte LoopCount + { + get => this._data[4]; + set => this._data[4] = value; + } + + /// + /// Gets or sets the delay between the loops, in seconds. + /// + public ushort LoopDelay + { + get => ReadUInt16LittleEndian(this._data[6..]); + set => WriteUInt16LittleEndian(this._data[6..], value); + } + + /// + /// Gets or sets the color of the text, as 0xAABBGGRR. + /// + public uint Color + { + get => ReadUInt32LittleEndian(this._data[8..]); + set => WriteUInt32LittleEndian(this._data[8..], value); + } + + /// + /// Gets or sets the scroll speed, multiplied by 10. 0 means the default speed. + /// + public byte Speed + { + get => this._data[12]; + set => this._data[12] = value; + } + + /// + /// Gets or sets the message. + /// + public string Message + { + get => this._data.ExtractString(13, this._data.Length - 13, System.Text.Encoding.UTF8); + set => this._data.Slice(13).WriteString(value, System.Text.Encoding.UTF8); + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator SlideNoticeRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(SlideNoticeRef packet) => packet._data; + + /// + /// Calculates the size of the packet for the specified field content. + /// + /// The content of the variable 'Message' field from which the size will be calculated. + public static int GetRequiredSize(string content) => System.Text.Encoding.UTF8.GetByteCount(content) + 1 + 13; + + /// + /// Calculates the size of the packet for the specified field content. + /// + /// The content length in bytes of the variable 'Message' field from which the size will be calculated. + public static int GetRequiredSize(int contentLength) => contentLength + 1 + 13; +} + + /// /// Is sent by the server when: A player requested to join a guild. This message is sent then to the guild master. /// Causes reaction on client side: The guild master gets a message box with the request popping up. diff --git a/src/Web/AdminPanel/Pages/Servers.razor b/src/Web/AdminPanel/Pages/Servers.razor index 595ca2dd66..4e65234a86 100644 --- a/src/Web/AdminPanel/Pages/Servers.razor +++ b/src/Web/AdminPanel/Pages/Servers.razor @@ -22,11 +22,19 @@ else
-
+
-
+
+ + +
+