using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.Customization;
using Impostor.Api.Net.Inner.Objects.Components;
ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null);
/// <summary>
- /// Murder <paramref name="target" /> player.
+ /// Murder <paramref name="target" /> player or remove their protective shield.
+ /// </summary>
+ /// <param name="target">Target player to murder.</param>
+ /// <param name="result">The result of the murder operation.</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>
+ ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
+
+ /// <summary>
+ /// Murder <paramref name="target" /> player successfully.
/// </summary>
/// <param name="target">Target player to murder.</param>
/// <exception cref="ImpostorProtocolException">Thrown when player is not the impostor.</exception>
/// <returns>Task that must be awaited.</returns>
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>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask ProtectPlayerAsync(IInnerPlayerControl target);
+
/// <summary>
/// Exile the current player. This doesn't produce a body to be reported.
/// Visible to all players.
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)
{
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)
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;
/// <summary> Gets or sets target that was set by the last CheckMurder RPC. </summary>
internal IInnerPlayerControl? IsMurdering { get; set; } = null;
+ internal DateTimeOffset? ProtectedOn { get; set; }
+
+ internal IInnerPlayerControl? ProtectedBy { get; set; }
+
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
{
throw new NotImplementedException();
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
{
+ _logger.LogTrace("Got RPC {0}", call);
switch (call)
{
case RpcCalls.PlayAnimation:
return false;
}
- Rpc48CheckProtect.Deserialize(reader, Game, out _);
-
- break;
+ Rpc48CheckProtect.Deserialize(reader, Game, out var protectTarget);
+ return await HandleCheckProtect(sender, protectTarget);
}
default:
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);
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;
private async ValueTask<bool> 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"))
return true;
}
+ private async ValueTask<bool> 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<bool> HandleSendChat(ClientPlayer sender, string message)
{
var @event = new PlayerChatEvent(Game, sender, this, message);