Skip to content
Merged
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
14 changes: 10 additions & 4 deletions docs-website/docs/admin-panel/servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions docs/Packets/C1-0D-SlideNotice_by-server.md
Original file line number Diff line number Diff line change
@@ -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 |
1 change: 1 addition & 0 deletions docs/Packets/ServerToClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions src/GameLogic/PlugIns/ChatCommands/SlideNoticeChatCommandPlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// <copyright file="SlideNoticeChatCommandPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;

using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// A chat command plugin which sends a notice to all players that scrolls across the top of their screen.
/// </summary>
[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";

/// <inheritdoc />
public string Key => CommandKey;

/// <inheritdoc />
public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster;

/// <inheritdoc />
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);
}
}
18 changes: 18 additions & 0 deletions src/GameLogic/Properties/PlugInResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/GameLogic/Properties/PlugInResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,12 @@
<data name="NoticeChatCommandPlugIn_Description" xml:space="preserve">
<value>Handles the chat command '/goldnotice message'. Sends a global golden notice message to all players of the game.</value>
</data>
<data name="SlideNoticeChatCommandPlugIn_Name" xml:space="preserve">
<value>Slide notice chat command</value>
</data>
<data name="SlideNoticeChatCommandPlugIn_Description" xml:space="preserve">
<value>Handles the chat command '/slidenotice message'. Sends a global notice to all players of the game which scrolls across the top of their screen.</value>
</data>
<data name="NpcChatCommandPlugIn_Name" xml:space="preserve">
<value>NPC open merchant chat command</value>
</data>
Expand Down
27 changes: 26 additions & 1 deletion src/GameServer/RemoteView/ShowMessagePlugIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -63,4 +75,17 @@ private static ServerMessage.MessageType ConvertMessageType(OpenMU.Interfaces.Me
_ => throw new NotImplementedException($"Case for {messageType} is not implemented."),
};
}
}

/// <summary>
/// Sends a notice which the client scrolls through the notice band at the top of the screen.
/// </summary>
/// <param name="message">The message.</param>
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);
}
}
5 changes: 5 additions & 0 deletions src/Interfaces/IGameServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public enum MessageType
/// The message is a guild notice (green center).
/// </summary>
GuildNotice = 2,

/// <summary>
/// The message scrolls in bold across the top of the screen (slide notice).
/// </summary>
SlideNotice = 3,
}

/// <summary>
Expand Down
38 changes: 38 additions & 0 deletions src/Network/Packets/ServerToClient/ConnectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4135,6 +4135,44 @@ int WritePacket()
await connection.SendAsync(WritePacket).ConfigureAwait(false);
}

/// <summary>
/// Sends a <see cref="SlideNotice" /> to this connection.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="loopDelay">The delay between the loops, in seconds.</param>
/// <param name="color">The color of the text, as 0xAABBGGRR.</param>
/// <param name="speed">The scroll speed, multiplied by 10. 0 means the default speed.</param>
/// <param name="message">The message.</param>
/// <param name="loopCount">How often the message is shown. The client ignores messages with more than 30 loops.</param>
/// <param name="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.</param>
/// <remarks>
/// 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.
/// </remarks>
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);
}

/// <summary>
/// Sends a <see cref="GuildJoinRequest" /> to this connection.
/// </summary>
Expand Down
133 changes: 133 additions & 0 deletions src/Network/Packets/ServerToClient/ServerToClientPackets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20252,6 +20252,139 @@ public string Message
}


/// <summary>
/// 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.
/// </summary>
public readonly struct SlideNotice
{
private readonly Memory<byte> _data;

/// <summary>
/// Initializes a new instance of the <see cref="SlideNotice"/> struct.
/// </summary>
/// <param name="data">The underlying data.</param>
public SlideNotice(Memory<byte> data)
: this(data, true)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SlideNotice"/> struct.
/// </summary>
/// <param name="data">The underlying data.</param>
/// <param name="initialize">If set to <c>true</c>, the header data is automatically initialized and written to the underlying span.</param>
private SlideNotice(Memory<byte> 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;
}
}

/// <summary>
/// Gets the header type of this data packet.
/// </summary>
public static byte HeaderType => 0xC1;

/// <summary>
/// Gets the operation code of this data packet.
/// </summary>
public static byte Code => 0x0D;

/// <summary>
/// Gets the header of this packet.
/// </summary>
public C1Header Header => new (this._data);

/// <summary>
/// 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.
/// </summary>
public byte Type
{
get => this._data.Span[3];
set => this._data.Span[3] = value;
}

/// <summary>
/// Gets or sets how often the message is shown. The client ignores messages with more than 30 loops.
/// </summary>
public byte LoopCount
{
get => this._data.Span[4];
set => this._data.Span[4] = value;
}

/// <summary>
/// Gets or sets the delay between the loops, in seconds.
/// </summary>
public ushort LoopDelay
{
get => ReadUInt16LittleEndian(this._data.Span[6..]);
set => WriteUInt16LittleEndian(this._data.Span[6..], value);
}

/// <summary>
/// Gets or sets the color of the text, as 0xAABBGGRR.
/// </summary>
public uint Color
{
get => ReadUInt32LittleEndian(this._data.Span[8..]);
set => WriteUInt32LittleEndian(this._data.Span[8..], value);
}

/// <summary>
/// Gets or sets the scroll speed, multiplied by 10. 0 means the default speed.
/// </summary>
public byte Speed
{
get => this._data.Span[12];
set => this._data.Span[12] = value;
}

/// <summary>
/// Gets or sets the message.
/// </summary>
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);
}

/// <summary>
/// Performs an implicit conversion from a Memory of bytes to a <see cref="SlideNotice"/>.
/// </summary>
/// <param name="packet">The packet as span.</param>
/// <returns>The packet as struct.</returns>
public static implicit operator SlideNotice(Memory<byte> packet) => new (packet, false);

/// <summary>
/// Performs an implicit conversion from <see cref="SlideNotice"/> to a Memory of bytes.
/// </summary>
/// <param name="packet">The packet as struct.</param>
/// <returns>The packet as byte span.</returns>
public static implicit operator Memory<byte>(SlideNotice packet) => packet._data;

/// <summary>
/// Calculates the size of the packet for the specified field content.
/// </summary>
/// <param name="content">The content of the variable 'Message' field from which the size will be calculated.</param>
public static int GetRequiredSize(string content) => System.Text.Encoding.UTF8.GetByteCount(content) + 1 + 13;

/// <summary>
/// Calculates the size of the packet for the specified field content.
/// </summary>
/// <param name="contentLength">The content length in bytes of the variable 'Message' field from which the size will be calculated.</param>
public static int GetRequiredSize(int contentLength) => contentLength + 1 + 13;
}


/// <summary>
/// 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.
Expand Down
Loading
Loading