diff --git a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs index 3cf9eaf1..f6c439c7 100644 --- a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs +++ b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs @@ -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); /// - /// Murder player successfully. + /// Murder player. + /// + /// Target player to murder. + /// The result of the murder operation. + /// Task that must be awaited. + ValueTask ForceMurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result); + + /// + /// Murder player after validating if this is a valid kill. /// /// Target player to murder. /// Thrown when player is not the impostor. /// Thrown when player is dead. /// Thrown when target is dead. /// Task that must be awaited. + [Obsolete("Please switch to version with the MurderResultFlags argument")] ValueTask MurderPlayerAsync(IInnerPlayerControl target); /// /// Protect player. /// /// Target player to protect. - /// Thrown when target is a guardian angel. + /// Thrown when target is dead. /// Task that must be awaited. ValueTask ProtectPlayerAsync(IInnerPlayerControl target); + /// + /// Protect player. + /// + /// Target player to protect. + /// Task that must be awaited. + ValueTask ForceProtectPlayerAsync(IInnerPlayerControl target); + + /// + /// Exile the current player. This doesn't produce a body to be reported. + /// Visible to all players. + /// + /// Thrown if player to be exiled is already dead. + /// Task that must be awaited. + ValueTask ExileAsync(); + /// /// Exile the current player. This doesn't produce a body to be reported. /// Visible to all players. /// /// Task that must be awaited. - ValueTask ExileAsync(); + ValueTask ForceExileAsync(); } } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs index a4d280c6..31240beb 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs @@ -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) { - throw new ImpostorProtocolException("Tried to murder a player, but murderer was not alive."); + invalidReason = "Tried to murder a player, but murderer was not alive."; } - - if (target.PlayerInfo.IsDead) + else if (target.PlayerInfo.IsImpostor) { - throw new ImpostorProtocolException("Tried to murder a player, but target was not alive."); + 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 + { + invalidReason = null; + return true; } + return false; + } + + public async ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result) + { + if (!ValidateMurderPlayer(target, result, out var reason)) + { + 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); diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs index a35e7464..4922dfdc 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs @@ -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); } }