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);
}
}