]> git.deb.at Git - rhonda/impostor.git/commitdiff
Implement server side guardian angel
authorminiduikboot <mini@duikbo.at>
Sat, 28 Oct 2023 21:05:28 +0000 (23:05 +0200)
committerminiduikboot <mini@duikbo.at>
Sat, 28 Oct 2023 21:05:28 +0000 (23:05 +0200)
src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs
src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs
src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs

index 471eaf8ffe9350145293d312c4105a258a91f3e2..9f10fd5149933671c9ff41c47a9f0f016657b6a0 100644 (file)
@@ -1,4 +1,5 @@
 using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
 using Impostor.Api.Innersloth.Customization;
 using Impostor.Api.Net.Inner.Objects.Components;
 
@@ -90,7 +91,18 @@ namespace Impostor.Api.Net.Inner.Objects
         ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null);
 
         /// <summary>
-        ///     Murder <paramref name="target" /> player.
+        ///     Murder <paramref name="target" /> player or remove their protective shield.
+        /// </summary>
+        /// <param name="target">Target player to murder.</param>
+        /// <param name="result">The result of the murder operation.</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>
+        ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
+
+        /// <summary>
+        ///     Murder <paramref name="target" /> player successfully.
         /// </summary>
         /// <param name="target">Target player to murder.</param>
         /// <exception cref="ImpostorProtocolException">Thrown when player is not the impostor.</exception>
@@ -99,6 +111,14 @@ namespace Impostor.Api.Net.Inner.Objects
         /// <returns>Task that must be awaited.</returns>
         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>
+        /// <returns>Task that must be awaited.</returns>
+        ValueTask ProtectPlayerAsync(IInnerPlayerControl target);
+
         /// <summary>
         ///     Exile the current player. This doesn't produce a body to be reported.
         ///     Visible to all players.
index 65d671cadf98442ef5a803d626bf86875442adbd..202cea6eca1369a5646e00bca0805e3098def3c8 100644 (file)
@@ -1,6 +1,6 @@
 using Impostor.Api.Games;
-using Impostor.Api.Net.Inner.Objects;
 using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Inner.Objects;
 
 namespace Impostor.Api.Net.Messages.Rpcs
 {
index 91d3fd16bc3c6a25098c51612b524e7b95782ecf..23841dac957337e0a1d53b73deb5fe5245b64cb9 100644 (file)
@@ -8,7 +8,7 @@ namespace Impostor.Api.Net.Messages.Rpcs
     {
         public static void Serialize(IMessageWriter writer, IInnerPlayerControl playerControl, ColorType color)
         {
-            writer.Write(playerControl.NetId);
+            writer.WritePacked(playerControl.NetId);
             writer.Write((byte)color);
         }
 
index 58a5435798fec788603cce1f2e337a3c851e4338..eeb4095a594c1ee9301dfe4ce307b62429a3c543 100644 (file)
@@ -82,7 +82,7 @@ namespace Impostor.Server.Net.Inner.Objects
             await Game.FinishRpcAsync(writer, player.OwnerId);
         }
 
-        public async ValueTask MurderPlayerAsync(IInnerPlayerControl target)
+        public async ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result = MurderResultFlags.Succeeded)
         {
             if (!PlayerInfo.IsImpostor)
             {
@@ -99,15 +99,37 @@ namespace Impostor.Server.Net.Inner.Objects
                 throw new ImpostorProtocolException("Tried to murder a player, but target was not alive.");
             }
 
-            ((InnerPlayerControl)target).Die(DeathReason.Kill);
+            if (result == MurderResultFlags.Succeeded)
+            {
+                ((InnerPlayerControl)target).Die(DeathReason.Kill);
+            }
 
             using var writer = Game.StartRpc(NetId, RpcCalls.MurderPlayer);
-            Rpc12MurderPlayer.Serialize(writer, target, MurderResultFlags.Succeeded);
+            Rpc12MurderPlayer.Serialize(writer, target, result);
             await Game.FinishRpcAsync(writer);
 
             await _eventManager.CallAsync(new PlayerMurderEvent(Game, Game.GetClientPlayer(OwnerId)!, this, target));
         }
 
+        public async ValueTask MurderPlayerAsync(IInnerPlayerControl target)
+        {
+            await MurderPlayerAsync(target, MurderResultFlags.Succeeded);
+        }
+
+        public async ValueTask ProtectPlayerAsync(IInnerPlayerControl target)
+        {
+            if (target.PlayerInfo.RoleType == RoleTypes.GuardianAngel)
+            {
+                throw new ImpostorProtocolException("Tried to protect another Guardian Angel");
+            }
+
+            ((InnerPlayerControl)target).Protect(this);
+
+            using var writer = Game.StartRpc(NetId, RpcCalls.ProtectPlayer);
+            Rpc45ProtectPlayer.Serialize(writer, target, PlayerInfo.CurrentOutfit.Color);
+            await Game.FinishRpcAsync(writer);
+        }
+
         public async ValueTask ExileAsync()
         {
             if (PlayerInfo.IsDead)
index bd56462d4841e0c8b05a4fa9d99e2770848a189c..a964c264d56a8788903dcfd14140667f1a80d3f7 100644 (file)
@@ -7,6 +7,8 @@ using Impostor.Api;
 using Impostor.Api.Events.Managers;
 using Impostor.Api.Innersloth;
 using Impostor.Api.Innersloth.Customization;
+using Impostor.Api.Innersloth.GameOptions;
+using Impostor.Api.Innersloth.GameOptions.RoleOptions;
 using Impostor.Api.Net;
 using Impostor.Api.Net.Custom;
 using Impostor.Api.Net.Inner;
@@ -65,6 +67,10 @@ namespace Impostor.Server.Net.Inner.Objects
         /// <summary> Gets or sets target that was set by the last CheckMurder RPC. </summary>
         internal IInnerPlayerControl? IsMurdering { get; set; } = null;
 
+        internal DateTimeOffset? ProtectedOn { get; set; }
+
+        internal IInnerPlayerControl? ProtectedBy { get; set; }
+
         public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
         {
             throw new NotImplementedException();
@@ -87,6 +93,7 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
         {
+            _logger.LogTrace("Got RPC {0}", call);
             switch (call)
             {
                 case RpcCalls.PlayAnimation:
@@ -404,9 +411,8 @@ namespace Impostor.Server.Net.Inner.Objects
                         return false;
                     }
 
-                    Rpc48CheckProtect.Deserialize(reader, Game, out _);
-
-                    break;
+                    Rpc48CheckProtect.Deserialize(reader, Game, out var protectTarget);
+                    return await HandleCheckProtect(sender, protectTarget);
                 }
 
                 default:
@@ -422,6 +428,30 @@ namespace Impostor.Server.Net.Inner.Objects
             PlayerInfo.LastDeathReason = reason;
         }
 
+        internal void Protect(InnerPlayerControl guardianAngel)
+        {
+            // NOTE: Vanilla dispells all GA shields when a kill is blocked, so it suffices to keep track of the last protection action
+            ProtectedOn = _dateTimeProvider.UtcNow;
+            ProtectedBy = guardianAngel;
+        }
+
+        internal bool IsProtected()
+        {
+            // HnS doesn't have guardian angels
+            if (Game.Options.GameMode == GameModes.Normal && ProtectedOn != null)
+            {
+                var opts = (NormalGameOptions)Game.Options;
+                var guardianAngelOpts = (GuardianAngelRoleOptions)opts.RoleOptions.Roles[RoleTypes.GuardianAngel].RoleOptions;
+                var duration = guardianAngelOpts.ProtectionDurationSeconds;
+                var protectionExpiresAt = ProtectedOn.Value.AddSeconds(duration);
+                return protectionExpiresAt >= _dateTimeProvider.UtcNow;
+            }
+            else
+            {
+                return false;
+            }
+        }
+
         private async ValueTask HandleCompleteTask(ClientPlayer sender, uint taskId)
         {
             var task = PlayerInfo.Tasks.ElementAtOrDefault((int)taskId);
@@ -672,11 +702,12 @@ namespace Impostor.Server.Net.Inner.Objects
                 return true;
             }
 
-            // TODO check if protected by GA
-
             if (target != null)
             {
-                await MurderPlayerAsync(target);
+                var tgt = (InnerPlayerControl)target;
+                var result = tgt.IsProtected() ? MurderResultFlags.FailedProtected : MurderResultFlags.Succeeded;
+                tgt.ProtectedOn = null; // Clear GA protection in all cases
+                await MurderPlayerAsync(target, result);
             }
 
             return false;
@@ -684,6 +715,14 @@ namespace Impostor.Server.Net.Inner.Objects
 
         private async ValueTask<bool> HandleMurderPlayer(ClientPlayer sender, IInnerPlayerControl? target, MurderResultFlags result)
         {
+            if (!_game.IsHostAuthoritive())
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to murder directly"))
+                {
+                    return false;
+                }
+            }
+
             if (target == null || target.PlayerInfo.IsImpostor)
             {
                 if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to murder invalid target"))
@@ -712,6 +751,37 @@ namespace Impostor.Server.Net.Inner.Objects
             return true;
         }
 
+        private async ValueTask<bool> HandleCheckProtect(ClientPlayer sender, IInnerPlayerControl? target)
+        {
+            if (target == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckProtect, "Client tried to protect invalid target"))
+                {
+                    return false;
+                }
+
+                return true;
+            }
+
+            if (PlayerInfo.RoleType == RoleTypes.GuardianAngel)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckProtect, "Sender tried to protect target it couldn't protect"))
+                {
+                    return false;
+                }
+            }
+
+            if (_game.IsHostAuthoritive())
+            {
+                return true;
+            }
+            else
+            {
+                await ProtectPlayerAsync(target);
+                return false;
+            }
+        }
+
         private async ValueTask<bool> HandleSendChat(ClientPlayer sender, string message)
         {
             var @event = new PlayerChatEvent(Game, sender, this, message);