+using System;
using System.Threading.Tasks;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.Customization;
ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
/// <summary>
- /// Murder <paramref name="target" /> player successfully.
+ /// Murder <paramref name="target" /> player.
+ /// </summary>
+ /// <param name="target">Target player to murder.</param>
+ /// <param name="result">The result of the murder operation.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask ForceMurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
+
+ /// <summary>
+ /// Murder <paramref name="target" /> player after validating if this is a valid kill.
/// </summary>
/// <param name="target">Target player to murder.</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>
+ [Obsolete("Please switch to version with the MurderResultFlags argument")]
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>
+ /// <exception cref="ImpostorProtocolException">Thrown when target is dead.</exception>
/// <returns>Task that must be awaited.</returns>
ValueTask ProtectPlayerAsync(IInnerPlayerControl target);
+ /// <summary>
+ /// Protect <paramref name="target" /> player.
+ /// </summary>
+ /// <param name="target">Target player to protect.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask ForceProtectPlayerAsync(IInnerPlayerControl target);
+
/// <summary>
/// Exile the current player. This doesn't produce a body to be reported.
/// Visible to all players.
/// </summary>
+ /// <exception cref="ImpostorProtocolException">Thrown if player to be exiled is already dead.</exception>
/// <returns>Task that must be awaited.</returns>
ValueTask ExileAsync();
+
+ /// <summary>
+ /// Exile the current player. This doesn't produce a body to be reported.
+ /// Visible to all players.
+ /// </summary>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask ForceExileAsync();
}
}
+using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Impostor.Api;
using Impostor.Api.Innersloth;
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);
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);
throw new ImpostorProtocolException("Tried to exile a player, but target was not alive.");
}
+ await ForceExileAsync();
+ }
+
+ public async ValueTask ForceExileAsync()
+ {
// Update player.
Die(DeathReason.Exile);
}
}
- // 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;
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);
}
}