diff --git a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs
index 471eaf8f..9f10fd51 100644
--- a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs
+++ b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.Customization;
using Impostor.Api.Net.Inner.Objects.Components;
@@ -90,7 +91,18 @@ namespace Impostor.Api.Net.Inner.Objects
ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null);
///
- /// Murder player.
+ /// Murder player or remove their protective shield.
+ ///
+ /// Target player to murder.
+ /// The result of the murder operation.
+ /// Thrown when player is not the impostor.
+ /// Thrown when player is dead.
+ /// Thrown when target is dead.
+ /// Task that must be awaited.
+ ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
+
+ ///
+ /// Murder player successfully.
///
/// Target player to murder.
/// Thrown when player is not the impostor.
@@ -99,6 +111,14 @@ namespace Impostor.Api.Net.Inner.Objects
/// Task that must be awaited.
ValueTask MurderPlayerAsync(IInnerPlayerControl target);
+ ///
+ /// Protect player.
+ ///
+ /// Target player to protect.
+ /// Thrown when target is a guardian angel.
+ /// Task that must be awaited.
+ ValueTask ProtectPlayerAsync(IInnerPlayerControl target);
+
///
/// Exile the current player. This doesn't produce a body to be reported.
/// Visible to all players.
diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs
index 65d671ca..202cea6e 100644
--- a/src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs
+++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc12MurderPlayer.cs
@@ -1,6 +1,6 @@
using Impostor.Api.Games;
-using Impostor.Api.Net.Inner.Objects;
using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Inner.Objects;
namespace Impostor.Api.Net.Messages.Rpcs
{
diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs
index 91d3fd16..23841dac 100644
--- a/src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs
+++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc45ProtectPlayer.cs
@@ -8,7 +8,7 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static void Serialize(IMessageWriter writer, IInnerPlayerControl playerControl, ColorType color)
{
- writer.Write(playerControl.NetId);
+ writer.WritePacked(playerControl.NetId);
writer.Write((byte)color);
}
diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
index 58a54357..eeb4095a 100644
--- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
+++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
@@ -82,7 +82,7 @@ namespace Impostor.Server.Net.Inner.Objects
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)
{
@@ -99,15 +99,37 @@ namespace Impostor.Server.Net.Inner.Objects
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)
diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
index bd56462d..a964c264 100644
--- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
+++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
@@ -7,6 +7,8 @@ using Impostor.Api;
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;
@@ -65,6 +67,10 @@ namespace Impostor.Server.Net.Inner.Objects
/// Gets or sets target that was set by the last CheckMurder RPC.
internal IInnerPlayerControl? IsMurdering { get; set; } = null;
+ internal DateTimeOffset? ProtectedOn { get; set; }
+
+ internal IInnerPlayerControl? ProtectedBy { get; set; }
+
public override ValueTask SerializeAsync(IMessageWriter writer, bool initialState)
{
throw new NotImplementedException();
@@ -87,6 +93,7 @@ namespace Impostor.Server.Net.Inner.Objects
public override async ValueTask HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
{
+ _logger.LogTrace("Got RPC {0}", call);
switch (call)
{
case RpcCalls.PlayAnimation:
@@ -404,9 +411,8 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
- Rpc48CheckProtect.Deserialize(reader, Game, out _);
-
- break;
+ Rpc48CheckProtect.Deserialize(reader, Game, out var protectTarget);
+ return await HandleCheckProtect(sender, protectTarget);
}
default:
@@ -422,6 +428,30 @@ namespace Impostor.Server.Net.Inner.Objects
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);
@@ -672,11 +702,12 @@ namespace Impostor.Server.Net.Inner.Objects
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;
@@ -684,6 +715,14 @@ namespace Impostor.Server.Net.Inner.Objects
private async ValueTask 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"))
@@ -712,6 +751,37 @@ namespace Impostor.Server.Net.Inner.Objects
return true;
}
+ private async ValueTask 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 HandleSendChat(ClientPlayer sender, string message)
{
var @event = new PlayerChatEvent(Game, sender, this, message);