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
16 changes: 16 additions & 0 deletions docs-website/docs/admin-panel/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ It publishes:

Each kind of event can be switched off in the plugin configuration.

## Server announcements

The *Server announcements* plugin shows a list of announcements to all players,
one after another, so that a game master doesn't have to type recurring ones
like rule reminders or a link to the community. It's disabled by default.

Its configuration has:

* the *Interval* between one announcement and the next (at least 10 seconds),
* the *Announcements*, each with its text and its message type, e.g. the golden
message in the centre of the screen or a blue system message.

The announcements are shown in their order; after the last one, it starts over
with the first one. Each game server has its own rotation. The text can be translated:
players see the translation for their language, if there is one.

## Writing your own plugin

Implementing a plugin is documented next to the code, in the
Expand Down
50 changes: 50 additions & 0 deletions src/GameLogic/PlugIns/ServerAnnouncementsConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// <copyright file="ServerAnnouncementsConfiguration.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;

using MUnique.OpenMU.DataModel.Composition;
using MUnique.OpenMU.Interfaces;

/// <summary>
/// Configuration for the <see cref="ServerAnnouncementsPlugIn"/>.
/// </summary>
public class ServerAnnouncementsConfiguration
{
/// <summary>
/// Gets or sets the interval between one announcement and the next.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.ServerAnnouncementsConfiguration_Interval_Name), Description = nameof(PlugInResources.ServerAnnouncementsConfiguration_Interval_Description))]
public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(15);

/// <summary>
/// Gets or sets the announcements, which are shown one after another in their order.
/// After the last one, it starts over with the first one.
/// </summary>
[MemberOfAggregate]
[ScaffoldColumn(true)]
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.ServerAnnouncementsConfiguration_Messages_Name), Description = nameof(PlugInResources.ServerAnnouncementsConfiguration_Messages_Description))]
public ICollection<Announcement> Messages { get; set; } = new List<Announcement>();

/// <summary>
/// One announcement of the rotation.
/// </summary>
public class Announcement
{
/// <summary>
/// Gets or sets the text of the announcement.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.ServerAnnouncementsConfiguration_Text_Name))]
public LocalizedString Text { get; set; }

/// <summary>
/// Gets or sets how the announcement is shown.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.ServerAnnouncementsConfiguration_Type_Name), Description = nameof(PlugInResources.ServerAnnouncementsConfiguration_Type_Description))]
public MessageType Type { get; set; } = MessageType.GoldenCenter;

/// <inheritdoc />
public override string ToString() => this.Text.ToString() ?? string.Empty;
}
}
134 changes: 134 additions & 0 deletions src/GameLogic/PlugIns/ServerAnnouncementsPlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// <copyright file="ServerAnnouncementsPlugIn.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;

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// Shows a configured list of announcements to all players of a game server, one after another
/// in a fixed interval, so that they don't have to be sent by a game master.
/// </summary>
/// <remarks>
/// Only one announcement is shown per interval, so they never overlap each other.
/// The rotation doesn't coordinate with other announcements, like the ones of the events.
/// </remarks>
[PlugIn]
[Display(Name = nameof(PlugInResources.ServerAnnouncementsPlugIn_Name), Description = nameof(PlugInResources.ServerAnnouncementsPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("2D6C9E4A-7B3E-4F8C-9E1D-8A4B6F0C2E7D")]
public class ServerAnnouncementsPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration<ServerAnnouncementsConfiguration>, ISupportDefaultCustomConfiguration, IDisabledByDefault
{
/// <summary>
/// The rotation state per game server. The plugin instance is shared between the
/// game servers of a process, but each of them has its own rotation.
/// </summary>
private readonly ConditionalWeakTable<IGameContext, RotationState> _states = new();

/// <summary>
/// The number of calls of <see cref="ForceStart"/>. A rotation which didn't see the
/// current number yet shows its next announcement without waiting for the interval.
/// </summary>
private int _forceStartCount;

/// <inheritdoc />
public ServerAnnouncementsConfiguration? Configuration { get; set; }

/// <inheritdoc />
public async ValueTask ExecuteTaskAsync(GameContext gameContext)
{
if (this.Configuration is not { Messages.Count: > 0 } configuration)
{
return;
}

var state = this._states.GetValue(gameContext, _ => new RotationState());
if (state.TryGetNextAnnouncement(configuration, Volatile.Read(ref this._forceStartCount)) is not { } announcement)
{
return;
}

await gameContext.ForEachPlayerAsync(player =>
{
var text = announcement.Text.GetTranslation(player.Culture);
if (string.IsNullOrWhiteSpace(text))
{
return Task.CompletedTask;
}

return player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(text, announcement.Type)).AsTask();
}).ConfigureAwait(false);
}

/// <inheritdoc />
/// <remarks>
/// Shows the next announcement of each game server without waiting for the interval.
/// </remarks>
public void ForceStart()
{
Interlocked.Increment(ref this._forceStartCount);
}

/// <inheritdoc />
public object CreateDefaultConfig()
{
return new ServerAnnouncementsConfiguration
{
Messages =
{
new() { Text = "Our team will never ask you for your password." },
},
};
}

/// <summary>
/// The state of the rotation of a game server.
/// </summary>
private sealed class RotationState
{
/// <summary>
/// The minimum interval between two announcements. It protects the players from
/// getting an announcement every second when the configured interval is zero.
/// </summary>
private static readonly TimeSpan MinimumInterval = TimeSpan.FromSeconds(10);

private readonly Lock _lock = new();

private int _nextIndex;

private DateTime _nextRunUtc;

private int _handledForceStartCount;

/// <summary>
/// Gets the announcement which is due, and advances the rotation.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="forceStartCount">The current number of forced starts.</param>
/// <returns>The due announcement, or <c>null</c>, if the interval isn't over yet.</returns>
public ServerAnnouncementsConfiguration.Announcement? TryGetNextAnnouncement(ServerAnnouncementsConfiguration configuration, int forceStartCount)
{
lock (this._lock)
{
var now = DateTime.UtcNow;
if (now < this._nextRunUtc && forceStartCount == this._handledForceStartCount)
{
return null;
}

this._handledForceStartCount = forceStartCount;

// The configuration may have changed since the last announcement, so the index is wrapped here.
var messages = configuration.Messages;
var announcement = messages.ElementAt(this._nextIndex % messages.Count);
this._nextIndex = (this._nextIndex + 1) % messages.Count;
this._nextRunUtc = now + (configuration.Interval > MinimumInterval ? configuration.Interval : MinimumInterval);
return announcement;
}
}
}
}
81 changes: 81 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.

27 changes: 27 additions & 0 deletions src/GameLogic/Properties/PlugInResources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1833,4 +1833,31 @@
<data name="ShopPriceChatCommandPlugIn_Description" xml:space="preserve">
<value>Legt den Preis eines Gegenstands des eigenen persönlichen Ladens in einer der aktivierten Währungen fest, z. B. '/shopprice 1 10 bless'. Ohne Argumente werden die Preise aufgelistet.</value>
</data>
<data name="ServerAnnouncementsPlugIn_Name" xml:space="preserve">
<value>Server-Ankündigungen</value>
</data>
<data name="ServerAnnouncementsPlugIn_Description" xml:space="preserve">
<value>Zeigt allen Spielern eines Spielservers eine konfigurierte Liste von Ankündigungen, eine nach der anderen in einem festen Intervall.</value>
</data>
<data name="ServerAnnouncementsConfiguration_Interval_Name" xml:space="preserve">
<value>Intervall</value>
</data>
<data name="ServerAnnouncementsConfiguration_Interval_Description" xml:space="preserve">
<value>Die Zeit zwischen einer Ankündigung und der nächsten. Das Minimum sind 10 Sekunden.</value>
</data>
<data name="ServerAnnouncementsConfiguration_Messages_Name" xml:space="preserve">
<value>Ankündigungen</value>
</data>
<data name="ServerAnnouncementsConfiguration_Messages_Description" xml:space="preserve">
<value>Die Ankündigungen in der Reihenfolge, in der sie gezeigt werden. Nach der letzten beginnt es wieder mit der ersten.</value>
</data>
<data name="ServerAnnouncementsConfiguration_Text_Name" xml:space="preserve">
<value>Text</value>
</data>
<data name="ServerAnnouncementsConfiguration_Type_Name" xml:space="preserve">
<value>Nachrichtentyp</value>
</data>
<data name="ServerAnnouncementsConfiguration_Type_Description" xml:space="preserve">
<value>Wie die Ankündigung gezeigt wird, z. B. als goldene Nachricht in der Bildschirmmitte oder als blaue Systemnachricht.</value>
</data>
</root>
27 changes: 27 additions & 0 deletions src/GameLogic/Properties/PlugInResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1634,4 +1634,31 @@
<data name="ShopPriceChatCommandPlugIn_Description" xml:space="preserve">
<value>Sets the price of an item of the own personal store in one of the enabled currencies, e.g. '/shopprice 1 10 bless'. Without arguments, it lists the prices.</value>
</data>
<data name="ServerAnnouncementsPlugIn_Name" xml:space="preserve">
<value>Server announcements</value>
</data>
<data name="ServerAnnouncementsPlugIn_Description" xml:space="preserve">
<value>Shows a configured list of announcements to all players of a game server, one after another in a fixed interval.</value>
</data>
<data name="ServerAnnouncementsConfiguration_Interval_Name" xml:space="preserve">
<value>Interval</value>
</data>
<data name="ServerAnnouncementsConfiguration_Interval_Description" xml:space="preserve">
<value>The time between one announcement and the next. The minimum is 10 seconds.</value>
</data>
<data name="ServerAnnouncementsConfiguration_Messages_Name" xml:space="preserve">
<value>Announcements</value>
</data>
<data name="ServerAnnouncementsConfiguration_Messages_Description" xml:space="preserve">
<value>The announcements, in the order in which they are shown. After the last one, it starts over with the first one.</value>
</data>
<data name="ServerAnnouncementsConfiguration_Text_Name" xml:space="preserve">
<value>Text</value>
</data>
<data name="ServerAnnouncementsConfiguration_Type_Name" xml:space="preserve">
<value>Message Type</value>
</data>
<data name="ServerAnnouncementsConfiguration_Type_Description" xml:space="preserve">
<value>How the announcement is shown, e.g. as golden message in the center of the screen or as blue system message.</value>
</data>
</root>
Loading
Loading