From: miniduikboot Date: Thu, 27 Feb 2025 21:34:40 +0000 (+0100) Subject: Add Murder/Protect/Exile API without checks X-Git-Tag: v1.10.3~7 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=be0a0b917c0eefaa62717bb95c6ba66d64e98a00;p=rhonda%2Fimpostor.git Add Murder/Protect/Exile API without checks 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 --- diff --git a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs index 3cf9eaf..f6c439c 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 ForceExileAsync(); } } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs index a4d280c..31240be 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) + { + 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); diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs index a35e746..4922dfd 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); } }