]> git.deb.at Git - rhonda/impostor.git/commitdiff
Implement event to detect game option changes
authorminiduikboot <mini@duikbo.at>
Sun, 26 Nov 2023 22:01:54 +0000 (23:01 +0100)
committerminiduikboot <mini@duikbo.at>
Sun, 26 Nov 2023 22:16:11 +0000 (23:16 +0100)
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.

src/Impostor.Api/Events/Game/IGameOptionsChangedEvent.cs [new file with mode: 0644]
src/Impostor.Plugins.Example/Handlers/GameEventListener.cs
src/Impostor.Server/Events/Game/GameOptionsChangedEvent.cs [new file with mode: 0644]
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
src/Impostor.Server/Net/State/Game.Api.cs

diff --git a/src/Impostor.Api/Events/Game/IGameOptionsChangedEvent.cs b/src/Impostor.Api/Events/Game/IGameOptionsChangedEvent.cs
new file mode 100644 (file)
index 0000000..14d7b0c
--- /dev/null
@@ -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; }
+    }
+}
index 404e0a8136b89f6f9ae64054456bd83e265a7671..90dd0eea26aa121149cfce0c125996404daffb77 100644 (file)
@@ -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)
         {
diff --git a/src/Impostor.Server/Events/Game/GameOptionsChangedEvent.cs b/src/Impostor.Server/Events/Game/GameOptionsChangedEvent.cs
new file mode 100644 (file)
index 0000000..075de31
--- /dev/null
@@ -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; }
+    }
+}
index 06b0485229a9b57f43af09b7a536362fe187a431..9b3539def3e6db1d58e7084ff3b1827fb8c6f799 100644 (file)
@@ -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;
                 }
 
index b8a081ec00d6370a73733e8ddf6747eeb0cb4e5a..4ed4279e310e2fb842b8a74934a23dca41287fc0 100644 (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)