From: miniduikboot Date: Sat, 28 Oct 2023 21:05:28 +0000 (+0200) Subject: Implement server side guardian angel X-Git-Tag: v1.9.0~14^2~11 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=946d7e216493d2ef1cf6906a9777bbf0f19aa647;p=rhonda%2Fimpostor.git Implement server side guardian angel --- diff --git a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs index 471eaf8..9f10fd5 100644 --- a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs +++ b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs @@ -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); /// - /// Murder player. + /// Murder player or remove their protective shield. + /// + /// Target player to murder. + /// The result of the murder operation. + /// Thrown when player is not the impostor. + /// Thrown when player is dead. + /// Thrown when target is dead. + /// Task that must be awaited. + ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result); + + /// + /// Murder player successfully. /// /// Target player to murder. /// Thrown when player is not the impostor. @@ -99,6 +111,14 @@ namespace Impostor.Api.Net.Inner.Objects /// Task that must be awaited. ValueTask MurderPlayerAsync(IInnerPlayerControl target); + /// + /// Protect player. + /// + /// Target player to protect. + /// Thrown when target is a guardian angel. + /// Task that must be awaited. + ValueTask ProtectPlayerAsync(IInnerPlayerControl target); + /// /// Exile the current player. This doesn't produce a body to be reported. /// Visible to all players. diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs index 65d671c..202cea6 100644 --- a/src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs @@ -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 { diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs index 91d3fd1..23841da 100644 --- a/src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs @@ -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); } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs index 58a5435..eeb4095 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs @@ -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) diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs index bd56462..a964c26 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs @@ -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 /// Gets or sets target that was set by the last CheckMurder RPC. internal IInnerPlayerControl? IsMurdering { get; set; } = null; + internal DateTimeOffset? ProtectedOn { get; set; } + + internal IInnerPlayerControl? ProtectedBy { get; set; } + public override ValueTask SerializeAsync(IMessageWriter writer, bool initialState) { throw new NotImplementedException(); @@ -87,6 +93,7 @@ namespace Impostor.Server.Net.Inner.Objects public override async ValueTask 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 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 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 HandleSendChat(ClientPlayer sender, string message) { var @event = new PlayerChatEvent(Game, sender, this, message);