diff --git a/src/Impostor.Api/Events/Game/Player/IPlayerCheckMurderEvent.cs b/src/Impostor.Api/Events/Game/Player/IPlayerCheckMurderEvent.cs
new file mode 100644
index 00000000..878d5a54
--- /dev/null
+++ b/src/Impostor.Api/Events/Game/Player/IPlayerCheckMurderEvent.cs
@@ -0,0 +1,27 @@
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Events.Player
+{
+ ///
+ /// Event that allows changing or canceling an upcoming player murder.
+ ///
+ /// Note that this event only triggers if the Host of the Game did not disable server authority.
+ ///
+ /// If you want to get an event after the murder took place, listen to .
+ public interface IPlayerCheckMurderEvent : IPlayerEvent, IEventCancelable
+ {
+ ///
+ /// Gets the player who got murdered.
+ ///
+ IInnerPlayerControl Victim { get; }
+
+ ///
+ /// Gets or sets the result of this event.
+ ///
+ /// Its initial value is what would have happened in the normal game flow.
+ /// - If set to Succeeded, the Victim will die.
+ /// - If set to FailedProtected, the Victim will be protected with a shield animation and the attacker will have a cooldown set.
+ MurderResultFlags Result { get; set; }
+ }
+}
diff --git a/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs b/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs
index c47c00b4..b90ea3ee 100644
--- a/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs
+++ b/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs
@@ -1,12 +1,26 @@
+using Impostor.Api.Innersloth;
using Impostor.Api.Net.Inner.Objects;
namespace Impostor.Api.Events.Player
{
+ ///
+ /// Event that is called when a player is killed by another player.
+ ///
+ /// This event works regardless of server authority is enabled or not.
+ ///
+ /// If you want to cancel this kill, listen to
+ /// If you want to know about players that were voted out, listed to
public interface IPlayerMurderEvent : IPlayerEvent
{
///
/// Gets the player who got murdered.
///
IInnerPlayerControl Victim { get; }
+
+ ///
+ /// Gets the result of the event.
+ ///
+ /// Note that if FailedError or FailedProtected is set, the kill did not take place
+ MurderResultFlags Result { get; }
}
}
diff --git a/src/Impostor.Api/Games/IGame.cs b/src/Impostor.Api/Games/IGame.cs
index 636b016e..8f5d29b6 100644
--- a/src/Impostor.Api/Games/IGame.cs
+++ b/src/Impostor.Api/Games/IGame.cs
@@ -39,6 +39,12 @@ namespace Impostor.Api.Games
int HostId { get; }
+ ///
+ /// Gets a value indicating whether the Host of the game has requested host authority.
+ ///
+ /// Vanilla Among Us does not request this, but certain client-side mods will.
+ bool IsHostAuthoritive { get; }
+
IClientPlayer? GetClientPlayer(int clientId);
T? FindObjectByNetId(uint netId)
diff --git a/src/Impostor.Server/Events/Game/Player/PlayerCheckMurderEvent.cs b/src/Impostor.Server/Events/Game/Player/PlayerCheckMurderEvent.cs
new file mode 100644
index 00000000..fb0a1f0b
--- /dev/null
+++ b/src/Impostor.Server/Events/Game/Player/PlayerCheckMurderEvent.cs
@@ -0,0 +1,32 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerCheckMurderEvent : IPlayerCheckMurderEvent
+ {
+ public PlayerCheckMurderEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl victim, MurderResultFlags result)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ Victim = victim;
+ Result = result;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+
+ public IInnerPlayerControl Victim { get; }
+
+ public MurderResultFlags Result { get; set; }
+
+ public bool IsCancelled { get; set; }
+ }
+}
diff --git a/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs b/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs
index ca64c35d..d157fd4d 100644
--- a/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs
+++ b/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs
@@ -1,5 +1,6 @@
using Impostor.Api.Events.Player;
using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
using Impostor.Api.Net;
using Impostor.Api.Net.Inner.Objects;
@@ -7,12 +8,13 @@ namespace Impostor.Server.Events.Player
{
public class PlayerMurderEvent : IPlayerMurderEvent
{
- public PlayerMurderEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl victim)
+ public PlayerMurderEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl victim, MurderResultFlags result)
{
Game = game;
ClientPlayer = clientPlayer;
PlayerControl = playerControl;
Victim = victim;
+ Result = result;
}
public IGame Game { get; }
@@ -22,5 +24,7 @@ namespace Impostor.Server.Events.Player
public IInnerPlayerControl PlayerControl { get; }
public IInnerPlayerControl Victim { get; }
+
+ public MurderResultFlags Result { get; }
}
}
diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
index eeb4095a..0718400f 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, MurderResultFlags result = MurderResultFlags.Succeeded)
+ public async ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result)
{
if (!PlayerInfo.IsImpostor)
{
@@ -99,7 +99,7 @@ namespace Impostor.Server.Net.Inner.Objects
throw new ImpostorProtocolException("Tried to murder a player, but target was not alive.");
}
- if (result == MurderResultFlags.Succeeded)
+ if ((result & (MurderResultFlags.FailedError | MurderResultFlags.FailedProtected)) == 0)
{
((InnerPlayerControl)target).Die(DeathReason.Kill);
}
@@ -108,7 +108,7 @@ namespace Impostor.Server.Net.Inner.Objects
Rpc12MurderPlayer.Serialize(writer, target, result);
await Game.FinishRpcAsync(writer);
- await _eventManager.CallAsync(new PlayerMurderEvent(Game, Game.GetClientPlayer(OwnerId)!, this, target));
+ await _eventManager.CallAsync(new PlayerMurderEvent(Game, Game.GetClientPlayer(OwnerId)!, this, target, result));
}
public async ValueTask MurderPlayerAsync(IInnerPlayerControl target)
diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
index a964c264..01da1f62 100644
--- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
+++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
@@ -696,7 +696,7 @@ namespace Impostor.Server.Net.Inner.Objects
IsMurdering = target;
// Check if host authority mode is on
- if (_game.IsHostAuthoritive())
+ if (_game.IsHostAuthoritive)
{
// Pass the RPC on unharmed, the client will handle it
return true;
@@ -706,8 +706,15 @@ namespace Impostor.Server.Net.Inner.Objects
{
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);
+
+ var evt = new PlayerCheckMurderEvent(Game, sender, this, target, result);
+ await _eventManager.CallAsync(evt);
+
+ if (!evt.IsCancelled)
+ {
+ tgt.ProtectedOn = null; // Clear GA protection in all cases
+ await MurderPlayerAsync(target, evt.Result);
+ }
}
return false;
@@ -715,7 +722,7 @@ namespace Impostor.Server.Net.Inner.Objects
private async ValueTask HandleMurderPlayer(ClientPlayer sender, IInnerPlayerControl? target, MurderResultFlags result)
{
- if (!_game.IsHostAuthoritive())
+ if (!_game.IsHostAuthoritive)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to murder directly"))
{
@@ -742,8 +749,12 @@ namespace Impostor.Server.Net.Inner.Objects
if (target != null && !target.PlayerInfo.IsDead)
{
- ((InnerPlayerControl)target).Die(DeathReason.Kill);
- await _eventManager.CallAsync(new PlayerMurderEvent(Game, sender, this, target));
+ if ((result & (MurderResultFlags.FailedError | MurderResultFlags.FailedProtected)) == 0)
+ {
+ ((InnerPlayerControl)target).Die(DeathReason.Kill);
+ }
+
+ await _eventManager.CallAsync(new PlayerMurderEvent(Game, sender, this, target, result));
}
IsMurdering = null;
@@ -771,7 +782,7 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
- if (_game.IsHostAuthoritive())
+ if (_game.IsHostAuthoritive)
{
return true;
}
diff --git a/src/Impostor.Server/Net/State/Game.cs b/src/Impostor.Server/Net/State/Game.cs
index 8b7219f7..6bb6fe52 100644
--- a/src/Impostor.Server/Net/State/Game.cs
+++ b/src/Impostor.Server/Net/State/Game.cs
@@ -92,6 +92,21 @@ namespace Impostor.Server.Net.State
public IEnumerable Players => _players.Select(p => p.Value);
+ public bool IsHostAuthoritive
+ {
+ get
+ {
+ if (Host == null)
+ {
+ return false;
+ }
+ else
+ {
+ return Host.Client.GameVersion.HasDisableServerAuthorityFlag;
+ }
+ }
+ }
+
internal GameNet GameNet { get; }
public bool TryGetPlayer(int id, [MaybeNullWhen(false)] out ClientPlayer player)
@@ -145,17 +160,5 @@ namespace Impostor.Server.Net.State
.Select(p => p.Client.Connection)
.Where(c => c != null && c.IsConnected)!;
}
-
- internal bool IsHostAuthoritive()
- {
- if (Host == null)
- {
- return false;
- }
- else
- {
- return Host.Client.GameVersion.HasDisableServerAuthorityFlag;
- }
- }
}
}