Implement event to detect game option changes

This can be triggered by both RPC 02 and a(nother) plugin calling
SyncSettings to force certain settings on the host. Users of this API
could cause this event to recurse by calling SyncSettingsAsync inside an
event handler, so we prevent such reasonable foreseeable misuse.
This commit is contained in:
miniduikboot committed 2023-11-26 23:16:11 +01:00
1 parent 66763a4066
commit 5edb409388
5 files changed
+102

No files matched your search

@@ -0,0 +1,51 @@
using Impostor.Api.Games;
using Impostor.Api.Net.Messages.Rpcs;
namespace Impostor.Api.Events
{
/// <summary>
/// This event is triggered when game options are going to be changed.
/// </summary>
/// <remarks>
/// Be careful when calling SyncSettingsAsync while handling this event,
/// as it may call this event again in some cases.
/// </remarks>
public interface IGameOptionsChangedEvent : IGameEvent
{
/// <summary>
/// Lists the possible reasons that the game options may have changed.
/// </summary>
public enum ChangeReason
{
/// <summary>
/// The options were changed by the host using
/// <see cref="Rpc02SyncSettings"/>.
/// </summary>
/// <remarks>
/// This event does not change the message sent to other players,
/// so changes made to Game.Options are not synced until
/// SyncSettingsAsync is called afterwards.
/// </remarks>
Rpc,
/// <summary>
/// The options were changed by a plugin using the
/// SyncSettingsAsync method in <see cref="IGame"/>.
/// </summary>
/// <remarks>
/// This event is called after serializing the settings, which
/// means that changes made by event handlers will not be synced
/// over.
///
/// We strongly recommend against calling SyncSettingsAsync while
/// handling this event.
/// </remarks>
Api,
}
/// <summary>
/// Gets the reason that the game options have changed.
/// </summary>
public ChangeReason ChangedBy { get; }
}
}
@@ -79,6 +79,16 @@ namespace Impostor.Plugins.Example.Handlers
);
}
[EventListener]
public async void OnGameOptionsChanged(IGameOptionsChangedEvent e)
{
_logger.LogInformation(
"Game {code} > new options because of {source}",
e.Game.Code,
e.ChangedBy
);
}
[EventListener]
public void OnPlayerJoined(IGamePlayerJoinedEvent e)
{
@@ -0,0 +1,19 @@
using Impostor.Api.Events;
using Impostor.Api.Games;
using static Impostor.Api.Events.IGameOptionsChangedEvent;
namespace Impostor.Server.Events
{
public class GameOptionsChangedEvent : IGameOptionsChangedEvent
{
public GameOptionsChangedEvent(IGame game, ChangeReason changedBy)
{
Game = game;
ChangedBy = changedBy;
}
public ChangeReason ChangedBy { get; }
public IGame Game { get; }
}
}
@@ -15,6 +15,7 @@ using Impostor.Api.Net.Inner;
using Impostor.Api.Net.Inner.Objects;
using Impostor.Api.Net.Messages.Rpcs;
using Impostor.Api.Utils;
using Impostor.Server.Events;
using Impostor.Server.Events.Player;
using Impostor.Server.Net.Inner.Objects.Components;
using Impostor.Server.Net.State;
@@ -142,6 +143,9 @@ namespace Impostor.Server.Net.Inner.Objects
}
Rpc02SyncSettings.DeserializeInto(reader, Game.Options);
await _eventManager.CallAsync(new GameOptionsChangedEvent(
Game,
Api.Events.IGameOptionsChangedEvent.ChangeReason.Rpc));
break;
}
+18
View File
@@ -6,12 +6,16 @@ using Impostor.Api.Net;
using Impostor.Api.Net.Inner;
using Impostor.Api.Net.Messages;
using Impostor.Hazel;
using Impostor.Server.Events;
using Impostor.Server.Net.Inner;
using Microsoft.Extensions.Logging;
namespace Impostor.Server.Net.State
{
internal partial class Game : IGame
{
private bool alreadyCallingOptionsChangedEvent = false;
IClientPlayer? IGame.Host => Host;
IGameNet IGame.GameNet => GameNet;
@@ -59,6 +63,20 @@ namespace Impostor.Server.Net.State
writer.EndMessage();
await SendToAllAsync(writer);
// Prevent bad plugins from causing a server crash by recursing into this function
if (alreadyCallingOptionsChangedEvent)
{
_logger.LogError("Plugin called SyncSettingsAsync while processing a GameOptionsChangedEvent, aborting to prevent recursion");
}
else
{
alreadyCallingOptionsChangedEvent = true;
await _eventManager.CallAsync(new GameOptionsChangedEvent(
this,
Api.Events.IGameOptionsChangedEvent.ChangeReason.Api));
alreadyCallingOptionsChangedEvent = false;
}
}
public async ValueTask SetPrivacyAsync(bool isPublic)