]> git.deb.at Git - rhonda/impostor.git/commitdiff
Add Murder/Protect/Exile API without checks
authorminiduikboot <mini@duikbo.at>
Thu, 27 Feb 2025 21:34:40 +0000 (22:34 +0100)
committerminiduikboot <mini@duikbo.at>
Thu, 22 May 2025 21:50:28 +0000 (23:50 +0200)
Force(.*)Async will perform the same function as \1Async but without
throwing exceptions. The old API is maintained for backwards
compatibility reasons.

Murder checks are now shared with the main function that handles
CheckMurder, as ProtectPlayerAsync and ExileAsync aren't used in the
code I didn't make a similar change there. Checks were also synced
between both methods

src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs

index 3cf9eaf1aaa6ab2949d9ac07ab2585ca8d0a08f1..f6c439c73acfeea587fb36f3d9f61041397ef14a 100644 (file)
@@ -1,3 +1,4 @@
+using System;
 using System.Threading.Tasks;
 using Impostor.Api.Innersloth;
 using Impostor.Api.Innersloth.Customization;
@@ -118,28 +119,52 @@ namespace Impostor.Api.Net.Inner.Objects
         ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
 
         /// <summary>
-        ///     Murder <paramref name="target" /> player successfully.
+        /// Murder <paramref name="target" /> player.
+        /// </summary>
+        /// <param name="target">Target player to murder.</param>
+        /// <param name="result">The result of the murder operation.</param>
+        /// <returns>Task that must be awaited.</returns>
+        ValueTask ForceMurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
+
+        /// <summary>
+        ///     Murder <paramref name="target" /> player after validating if this is a valid kill.
         /// </summary>
         /// <param name="target">Target player to murder.</param>
         /// <exception cref="ImpostorProtocolException">Thrown when player is not the impostor.</exception>
         /// <exception cref="ImpostorProtocolException">Thrown when player is dead.</exception>
         /// <exception cref="ImpostorProtocolException">Thrown when target is dead.</exception>
         /// <returns>Task that must be awaited.</returns>
+        [Obsolete("Please switch to version with the MurderResultFlags argument")]
         ValueTask MurderPlayerAsync(IInnerPlayerControl target);
 
         /// <summary>
         ///     Protect <paramref name="target" /> player.
         /// </summary>
         /// <param name="target">Target player to protect.</param>
-        /// <exception cref="ImpostorProtocolException">Thrown when target is a guardian angel.</exception>
+        /// <exception cref="ImpostorProtocolException">Thrown when target is dead.</exception>
         /// <returns>Task that must be awaited.</returns>
         ValueTask ProtectPlayerAsync(IInnerPlayerControl target);
 
+        /// <summary>
+        ///     Protect <paramref name="target" /> player.
+        /// </summary>
+        /// <param name="target">Target player to protect.</param>
+        /// <returns>Task that must be awaited.</returns>
+        ValueTask ForceProtectPlayerAsync(IInnerPlayerControl target);
+
         /// <summary>
         ///     Exile the current player. This doesn't produce a body to be reported.
         ///     Visible to all players.
         /// </summary>
+        /// <exception cref="ImpostorProtocolException">Thrown if player to be exiled is already dead.</exception>
         /// <returns>Task that must be awaited.</returns>
         ValueTask ExileAsync();
+
+        /// <summary>
+        ///     Exile the current player. This doesn't produce a body to be reported.
+        ///     Visible to all players.
+        /// </summary>
+        /// <returns>Task that must be awaited.</returns>
+        ValueTask ForceExileAsync();
     }
 }
index a4d280c6dfa3ce2038150b92b78977524f6dd23d..31240beb5b11ba6f9781c64485d67ff06284eb16 100644 (file)
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
 using System.Threading.Tasks;
 using Impostor.Api;
 using Impostor.Api.Innersloth;
@@ -100,23 +101,45 @@ namespace Impostor.Server.Net.Inner.Objects
             await Game.FinishRpcAsync(writer, player.OwnerId);
         }
 
-        public async ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result)
+        private bool ValidateMurderPlayer(IInnerPlayerControl target, MurderResultFlags result, [NotNullWhen(false)] out string? invalidReason)
         {
             if (!PlayerInfo.IsImpostor)
             {
-                throw new ImpostorProtocolException("Tried to murder a player, but murderer was not the impostor.");
+                invalidReason = "Tried to murder a player, but murderer was not an impostor.";
             }
-
-            if (PlayerInfo.IsDead)
+            else if (PlayerInfo.IsDead)
+            {
+                invalidReason = "Tried to murder a player, but murderer was not alive.";
+            }
+            else if (target.PlayerInfo.IsImpostor)
+            {
+                invalidReason = "Tried to murder a player, but target is an impostor";
+            }
+            else if (target.PlayerInfo.IsDead)
+            {
+                invalidReason = "Tried to murder a player, but target was not alive.";
+            }
+            else
             {
-                throw new ImpostorProtocolException("Tried to murder a player, but murderer was not alive.");
+                invalidReason = null;
+                return true;
             }
 
-            if (target.PlayerInfo.IsDead)
+            return false;
+        }
+
+        public async ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result)
+        {
+            if (!ValidateMurderPlayer(target, result, out var reason))
             {
-                throw new ImpostorProtocolException("Tried to murder a player, but target was not alive.");
+                throw new ImpostorProtocolException(reason);
             }
 
+            await ForceMurderPlayerAsync(target, result);
+        }
+
+        public async ValueTask ForceMurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result)
+        {
             if (!result.IsFailed())
             {
                 ((InnerPlayerControl)target).Die(DeathReason.Kill);
@@ -136,11 +159,16 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public async ValueTask ProtectPlayerAsync(IInnerPlayerControl target)
         {
-            if (target.PlayerInfo.RoleType == RoleTypes.GuardianAngel)
+            if (target.PlayerInfo.IsDead)
             {
-                throw new ImpostorProtocolException("Tried to protect another Guardian Angel");
+                throw new ImpostorProtocolException("Tried to protect a player that is dead");
             }
 
+            await ForceProtectPlayerAsync(target);
+        }
+
+        public async ValueTask ForceProtectPlayerAsync(IInnerPlayerControl target)
+        {
             ((InnerPlayerControl)target).Protect(this);
 
             using var writer = Game.StartRpc(NetId, RpcCalls.ProtectPlayer);
@@ -155,6 +183,11 @@ namespace Impostor.Server.Net.Inner.Objects
                 throw new ImpostorProtocolException("Tried to exile a player, but target was not alive.");
             }
 
+            await ForceExileAsync();
+        }
+
+        public async ValueTask ForceExileAsync()
+        {
             // Update player.
             Die(DeathReason.Exile);
 
index a35e7464596674c75fcd1cfed9f1b06a1ad17011..4922dfdc930ff780f357087495e9ab5fb87a79a5 100644 (file)
@@ -946,16 +946,6 @@ namespace Impostor.Server.Net.Inner.Objects
                 }
             }
 
-            // Host-only mods intentionally desync players, so it may appear that one killing role (like a genuine impostor) is killing another
-            // killing role (like an sheriff). So this needs to be allowed if the host requested authority.
-            if (target == null || (target.PlayerInfo.IsImpostor && !_game.IsHostAuthoritive))
-            {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, "Client tried to murder invalid target"))
-                {
-                    return false;
-                }
-            }
-
             PlayerInfo.LastMurder = _dateTimeProvider.UtcNow - TimeSpan.FromMilliseconds(sender.Client.Connection.AveragePing);
             IsMurdering = target;
 
@@ -966,17 +956,34 @@ namespace Impostor.Server.Net.Inner.Objects
                 return true;
             }
 
-            if (target != null)
+            if (target == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, "Client tried to murder a nonexisting target"))
+                {
+                    return false;
+                }
+            }
+            else
             {
                 var result = target.IsProtected ? MurderResultFlags.FailedProtected : MurderResultFlags.Succeeded;
 
+                if (!ValidateMurderPlayer(target, result, out var invalidReason))
+                {
+                    if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, invalidReason))
+                    {
+                        return false;
+                    }
+                }
+
                 var evt = new PlayerCheckMurderEvent(Game, sender, this, target, result);
                 await _eventManager.CallAsync(evt);
 
                 if (!evt.IsCancelled)
                 {
                     target.ProtectedOn = null; // Clear GA protection in all cases
-                    await MurderPlayerAsync(target, evt.Result);
+
+                    // Don't repeat checks as they were already done in ValidateMP
+                    await ForceMurderPlayerAsync(target, evt.Result);
                 }
             }