From ced530b83af5b249ddcb347fbcdb66a4c1deb4d3 Mon Sep 17 00:00:00 2001 From: Mr-VIT Date: Fri, 11 Jun 2021 15:33:38 +0300 Subject: [PATCH] Added player voted event (#181) Co-authored-by: AeonLucid Co-authored-by: js6pak --- .../Events/Game/Meeting/IMeetingEndedEvent.cs | 13 +- .../Events/Game/Player/IPlayerVotedEvent.cs | 24 +++ .../Net/Inner/Objects/IInnerMeetingHud.cs | 55 +++++- .../Handlers/MeetingEventListener.cs | 14 +- .../Handlers/PlayerEventListener.cs | 6 + .../Events/Game/Meeting/MeetingEndedEvent.cs | 8 +- .../Events/Game/Player/PlayerVotedEvent.cs | 29 ++++ .../Net/Inner/Objects/InnerGameData.cs | 5 + .../Net/Inner/Objects/InnerMeetingHud.Api.cs | 7 +- .../Objects/InnerMeetingHud.PlayerVoteArea.cs | 92 ++++++++-- .../Net/Inner/Objects/InnerMeetingHud.cs | 159 +++++++++++++++--- src/Impostor.Server/Net/State/Game.Data.cs | 8 +- src/Impostor.Server/Net/State/Game.State.cs | 2 +- 13 files changed, 373 insertions(+), 49 deletions(-) create mode 100644 src/Impostor.Api/Events/Game/Player/IPlayerVotedEvent.cs create mode 100644 src/Impostor.Server/Events/Game/Player/PlayerVotedEvent.cs diff --git a/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs b/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs index a217580..7787be6 100644 --- a/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs +++ b/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs @@ -1,6 +1,17 @@ -namespace Impostor.Api.Events.Meeting +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Events.Meeting { public interface IMeetingEndedEvent : IMeetingEvent { + /// + /// Gets the exiled player. + /// + IInnerPlayerControl? Exiled { get; } + + /// + /// Gets a value indicating whether meeting result is a tie. + /// + bool IsTie { get; } } } diff --git a/src/Impostor.Api/Events/Game/Player/IPlayerVotedEvent.cs b/src/Impostor.Api/Events/Game/Player/IPlayerVotedEvent.cs new file mode 100644 index 0000000..2533266 --- /dev/null +++ b/src/Impostor.Api/Events/Game/Player/IPlayerVotedEvent.cs @@ -0,0 +1,24 @@ +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Events.Player +{ + public enum VoteType : sbyte + { + ForceSkip = -2, + Skip = -1, + Player = 0, + } + + public interface IPlayerVotedEvent : IPlayerEvent + { + /// + /// Gets the player he voted for. + /// + IInnerPlayerControl? VotedFor { get; } + + /// + /// Gets the Vote type. + /// + VoteType VoteType { get; } + } +} diff --git a/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs b/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs index 6d8f106..b6c781f 100644 --- a/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs +++ b/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs @@ -1,6 +1,59 @@ -namespace Impostor.Api.Net.Inner.Objects +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Impostor.Api.Events.Player; + +namespace Impostor.Api.Net.Inner.Objects { public interface IInnerMeetingHud : IInnerNetObject { + public interface IPlayerVoteArea + { + /// + /// Gets the player info of this vote state. + /// + IInnerPlayerInfo TargetPlayer { get; } + + /// + /// Gets a value indicating whether the player is a dead. + /// + bool IsDead { get; } + + /// + /// Gets a value indicating whether the player started this meeting. + /// + bool DidReport { get; } + + /// + /// Gets a value indicating whether the player voted. + /// + bool DidVote { get; } + + /// + /// Gets the vote type. + /// + /// + /// Null when is false. + /// + [MemberNotNullWhen(true, nameof(DidVote))] + public VoteType? VoteType { get; } + + /// + /// Gets the player voted for. + /// + /// + /// Null when isn't . + /// + public IInnerPlayerControl? VotedFor { get; } + } + + /// + /// Gets states of players taking part in this meeting. + /// + IReadOnlyCollection PlayerStates { get; } + + /// + /// Gets the player that started the meeting. + /// + IInnerPlayerInfo? Reporter { get; } } } diff --git a/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs b/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs index 84948a7..8fb9d50 100644 --- a/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs +++ b/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs @@ -22,7 +22,19 @@ namespace Impostor.Plugins.Example.Handlers [EventListener] public void OnMeetingEnded(IMeetingEndedEvent e) { - _logger.LogInformation("Meeting > ended"); + _logger.LogInformation("Meeting > ended, exiled: {exiled}, tie: {tie}", e.Exiled?.PlayerInfo.PlayerName, e.IsTie); + + foreach (var playerState in e.MeetingHud.PlayerStates) + { + if (playerState.IsDead) + { + _logger.LogInformation("- {player} is dead", playerState.TargetPlayer.PlayerName); + } + else + { + _logger.LogInformation("- {player} voted for {voteType} {votedFor}", playerState.TargetPlayer.PlayerName, playerState.VoteType, playerState.VotedFor?.PlayerInfo.PlayerName); + } + } } } } diff --git a/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs b/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs index 4477c04..986b220 100644 --- a/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs +++ b/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs @@ -120,6 +120,12 @@ namespace Impostor.Plugins.Example.Handlers _logger.LogInformation("Player {player} vented to {vent}", e.PlayerControl.PlayerInfo.PlayerName, e.NewVent.Name); } + [EventListener] + public void OnPlayerVoted(IPlayerVotedEvent e) + { + _logger.LogDebug("Player {player} voted for {type} {votedFor}", e.PlayerControl.PlayerInfo.PlayerName, e.VoteType, e.VotedFor?.PlayerInfo.PlayerName); + } + [EventListener] public void OnPlayerCompletedTaskEvent(IPlayerCompletedTaskEvent e) { diff --git a/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs b/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs index cf55f7d..b64699f 100644 --- a/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs +++ b/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs @@ -6,14 +6,20 @@ namespace Impostor.Server.Events.Meeting { public class MeetingEndedEvent : IMeetingEndedEvent { - public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud) + public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud, IInnerPlayerControl? exiled, bool isTie) { Game = game; MeetingHud = meetingHud; + Exiled = exiled; + IsTie = isTie; } public IGame Game { get; } public IInnerMeetingHud MeetingHud { get; } + + public IInnerPlayerControl? Exiled { get; } + + public bool IsTie { get; } } } diff --git a/src/Impostor.Server/Events/Game/Player/PlayerVotedEvent.cs b/src/Impostor.Server/Events/Game/Player/PlayerVotedEvent.cs new file mode 100644 index 0000000..68a9096 --- /dev/null +++ b/src/Impostor.Server/Events/Game/Player/PlayerVotedEvent.cs @@ -0,0 +1,29 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerVotedEvent : IPlayerVotedEvent + { + public PlayerVotedEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, VoteType voteType, IInnerPlayerControl? votedFor) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + VoteType = voteType; + VotedFor = votedFor; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public IInnerPlayerControl? VotedFor { get; } + + public VoteType VoteType { get; } + } +} diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs b/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs index c466746..a7c2e30 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs @@ -137,6 +137,11 @@ namespace Impostor.Server.Net.Inner.Objects return null; } + internal void RemovePlayer(InnerPlayerControl control) + { + _allPlayers.TryRemove(control.PlayerInfo.PlayerId, out _); + } + private void SetTasks(byte playerId, ReadOnlyMemory taskTypeIds) { var player = GetPlayerById(playerId); diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs index 0219fba..8b1528d 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs @@ -1,8 +1,13 @@ -using Impostor.Api.Net.Inner.Objects; +using System; +using System.Collections.Generic; +using Impostor.Api.Net.Inner.Objects; namespace Impostor.Server.Net.Inner.Objects { internal partial class InnerMeetingHud : IInnerMeetingHud { + IReadOnlyCollection IInnerMeetingHud.PlayerStates => Array.AsReadOnly(_playerStates); + + IInnerPlayerInfo? IInnerMeetingHud.Reporter => Reporter; } } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs index 48f551b..26ac981 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs @@ -1,25 +1,32 @@ -using Impostor.Api.Net.Messages; +using Impostor.Api.Events.Player; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using TVoteType = Impostor.Api.Events.Player.VoteType; namespace Impostor.Server.Net.Inner.Objects { internal partial class InnerMeetingHud { - public class PlayerVoteArea + public class PlayerVoteArea : IInnerMeetingHud.IPlayerVoteArea { private const byte VoteMask = 15; private const byte ReportedBit = 32; private const byte VotedBit = 64; private const byte DeadBit = 128; - public PlayerVoteArea(InnerMeetingHud parent, byte targetPlayerId) + private sbyte _votedForId; + + public PlayerVoteArea(InnerMeetingHud parent, InnerPlayerInfo targetPlayer, bool isDead) { Parent = parent; - TargetPlayerId = targetPlayerId; + TargetPlayer = targetPlayer; + + IsDead = isDead; } public InnerMeetingHud Parent { get; } - public byte TargetPlayerId { get; } + public InnerPlayerInfo TargetPlayer { get; } public bool IsDead { get; private set; } @@ -27,22 +34,77 @@ namespace Impostor.Server.Net.Inner.Objects public bool DidReport { get; private set; } - public sbyte VotedFor { get; private set; } - - public void Deserialize(IMessageReader reader) + public sbyte VotedForId { - var num = reader.ReadByte(); + get => _votedForId; + + private set + { + _votedForId = value; + + if (DidVote) + { + switch ((VoteType)value) + { + case TVoteType.ForceSkip: + case TVoteType.Skip: + VoteType = (VoteType)value; + break; - VotedFor = (sbyte)((num & VoteMask) - 1); - IsDead = (num & DeadBit) > 0; - DidVote = (num & VotedBit) > 0; - DidReport = (num & ReportedBit) > 0; + default: + VoteType = TVoteType.Player; + VotedFor = Parent.Game.GameNet.GameData!.GetPlayerById((byte)value)?.Controller; + break; + } + } + else + { + VoteType = null; + VotedFor = null; + } + } } - internal void SetDead(bool didReport, bool isDead) + public VoteType? VoteType { get; private set; } + + public IInnerPlayerControl? VotedFor { get; private set; } + + IInnerPlayerInfo IInnerMeetingHud.IPlayerVoteArea.TargetPlayer => TargetPlayer; + + internal void Deserialize(IMessageReader reader, bool updateVote) { - DidReport = didReport; + var state = reader.ReadByte(); + DeserializeState(state, out var votedForId, out var isDead, out var didVote, out var didReport); + IsDead = isDead; + DidReport = didReport; + + if (updateVote) + { + DidVote = didVote; + VotedForId = votedForId; + } + } + + internal void SetVotedFor(sbyte votedFor) + { + DidVote = true; + VotedForId = votedFor; + } + + private static void DeserializeState(byte state, out sbyte votedForId, out bool isDead, out bool didVote, out bool didReport) + { + votedForId = (sbyte)((state & VoteMask) - 1); + + // Among Us's meeting system is really scuffed, we need this to ensure that ForceSkip state is correct (this will break any 14+ players game) + if (votedForId == ((sbyte)TVoteType.ForceSkip + 1 & 14)) + { + votedForId = (sbyte)TVoteType.ForceSkip; + } + + isDead = (state & DeadBit) > 0; + didVote = (state & VotedBit) > 0; + didReport = (state & ReportedBit) > 0; } } } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs index 104ceae..6dcd105 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs @@ -1,9 +1,11 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Impostor.Api; using Impostor.Api.Events.Managers; +using Impostor.Api.Events.Player; using Impostor.Api.Innersloth; using Impostor.Api.Net; using Impostor.Api.Net.Custom; @@ -22,6 +24,8 @@ namespace Impostor.Server.Net.Inner.Objects private readonly ILogger _logger; private readonly IEventManager _eventManager; + private readonly CancellationTokenSource _timerToken; + [AllowNull] private PlayerVoteArea[] _playerStates; @@ -32,9 +36,24 @@ namespace Impostor.Server.Net.Inner.Objects _playerStates = null; Components.Add(this); + + _timerToken = new CancellationTokenSource(); + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(Game.Options.DiscussionTime + Game.Options.VotingTime), _timerToken.Token); + } + catch (TaskCanceledException) + { + return; + } + + await HandleVotingCompleteAsync(); + }); } - public byte ReporterId { get; private set; } + public InnerPlayerInfo? Reporter { get; private set; } public override ValueTask SerializeAsync(IMessageWriter writer, bool initialState) { @@ -50,15 +69,15 @@ namespace Impostor.Server.Net.Inner.Objects if (initialState) { - PopulateButtons(0); + PopulateButtons(); foreach (var playerState in _playerStates) { - playerState.Deserialize(reader); + playerState.Deserialize(reader, false); if (playerState.DidReport) { - ReporterId = playerState.TargetPlayerId; + Reporter = playerState.TargetPlayer; } } } @@ -70,7 +89,18 @@ namespace Impostor.Server.Net.Inner.Objects { if ((num & 1 << i) != 0) { - _playerStates[i].Deserialize(reader); + var playerVoteArea = _playerStates[i]; + + var clientPlayer = Game.Players.SingleOrDefault(x => x.Character?.PlayerId == playerVoteArea.TargetPlayer.PlayerId); + var isHost = (clientPlayer?.IsHost ?? false) && playerVoteArea.VoteType != VoteType.ForceSkip; + + playerVoteArea.Deserialize(reader, isHost); + + if (isHost) + { + await HandleVoteAsync(playerVoteArea); + await CheckForEndVotingAsync(); + } } } } @@ -99,14 +129,17 @@ namespace Impostor.Server.Net.Inner.Objects } Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie); - await HandleVotingComplete(sender, states, playerId, tie); + + // This would be a nice place to implement an anti cheat. + // But for whatever reason host sends VotingComplete before sending his vote. + // Also every client executes his own VotingComplete after other client players CastVote rpc, like wtf. break; } case RpcCalls.CastVote: { Rpc24CastVote.Deserialize(reader, out var playerId, out var suspectPlayerId); - return await HandleCastVote(sender, target, playerId, suspectPlayerId); + return await HandleCastVoteAsync(sender, target, playerId, suspectPlayerId); } case RpcCalls.ClearVote: @@ -127,34 +160,24 @@ namespace Impostor.Server.Net.Inner.Objects return true; } - private void PopulateButtons(byte reporter) + private void PopulateButtons() { - _playerStates = Game.GameNet.GameData!.Players - .Select(x => - { - var area = new PlayerVoteArea(this, x.Key); - area.SetDead(x.Value.PlayerId == reporter, x.Value.Disconnected || x.Value.IsDead); - return area; - }) + _playerStates = Game.GameNet.GameData!.Players.Values + .OrderBy(x => x.Controller?.NetId) // The host player hold MeetingHud players list sorted by NetId + .Select(x => new PlayerVoteArea(this, x, x.Disconnected || x.IsDead)) .ToArray(); } - private async ValueTask HandleVotingComplete(ClientPlayer sender, ReadOnlyMemory states, byte playerId, bool tie) + private async ValueTask HandleVoteAsync(PlayerVoteArea playerState) { - if (playerId != byte.MaxValue) + if (playerState.DidVote && !playerState.IsDead) { - var player = Game.GameNet.GameData!.GetPlayerById(playerId); - if (player?.Controller != null) - { - player.Controller.Die(DeathReason.Exile); - await _eventManager.CallAsync(new PlayerExileEvent(Game, sender, player.Controller)); - } + var player = playerState.TargetPlayer.Controller!; + await _eventManager.CallAsync(new PlayerVotedEvent(Game, Game.GetClientPlayer(player!.OwnerId)!, player, playerState.VoteType!.Value, playerState.VotedFor)); } - - await _eventManager.CallAsync(new MeetingEndedEvent(Game, this)); } - private async ValueTask HandleCastVote(ClientPlayer sender, ClientPlayer? target, byte playerId, sbyte suspectPlayerId) + private async ValueTask HandleCastVoteAsync(ClientPlayer sender, ClientPlayer? target, byte playerId, sbyte suspectPlayerId) { if (sender.IsHost) { @@ -179,7 +202,91 @@ namespace Impostor.Server.Net.Inner.Objects } } + if (!sender.IsHost) + { + var playerVoteArea = _playerStates.Single(x => x.TargetPlayer.PlayerId == playerId); + playerVoteArea.SetVotedFor(suspectPlayerId); + await HandleVoteAsync(playerVoteArea); + await CheckForEndVotingAsync(); + } + return true; } + + private async ValueTask CheckForEndVotingAsync() + { + if (_playerStates.All(ps => ps.IsDead || ps.DidVote)) + { + await HandleVotingCompleteAsync(); + } + } + + private byte[] CalculateVotes() + { + byte[] array = new byte[_playerStates.Max(x => x.TargetPlayer.PlayerId) + 2]; + foreach (var playerVoteArea in _playerStates) + { + if (playerVoteArea.DidVote) + { + var index = playerVoteArea.VotedForId + 1; + if (index >= 0 && index < array.Length) + { + array[index] += 1; + } + } + } + + return array; + } + + private int IndexOfMax(T[] self, Func comparer, out bool tie) + { + tie = false; + var num = int.MinValue; + var result = -1; + for (var i = 0; i < self.Length; i++) + { + var num2 = comparer.Invoke(self[i]); + if (num2 > num) + { + result = i; + num = num2; + tie = false; + } + else if (num2 == num) + { + tie = true; + result = -1; + } + } + + return result; + } + + private async ValueTask HandleVotingCompleteAsync() + { + _timerToken.Cancel(); + + foreach (var playerVoteArea in _playerStates) + { + if (!playerVoteArea.DidVote) + { + playerVoteArea.SetVotedFor((sbyte)VoteType.ForceSkip); + await HandleVoteAsync(playerVoteArea); + } + } + + byte[] self = this.CalculateVotes(); + var maxIdx = IndexOfMax(self, p => p, out var tie) - 1; + var exiled = Game.GameNet.GameData!.GetPlayerById((byte)maxIdx)?.Controller; + + if (exiled != null) + { + exiled.Die(DeathReason.Exile); + await _eventManager.CallAsync(new PlayerExileEvent(Game, Game.GetClientPlayer(exiled!.OwnerId)!, exiled)); + } + + await _eventManager.CallAsync(new MeetingEndedEvent(Game, this, exiled, tie)); + } } } diff --git a/src/Impostor.Server/Net/State/Game.Data.cs b/src/Impostor.Server/Net/State/Game.Data.cs index ee1111c..19d2dab 100644 --- a/src/Impostor.Server/Net/State/Game.Data.cs +++ b/src/Impostor.Server/Net/State/Game.Data.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Impostor.Api; +using Impostor.Api.Innersloth; using Impostor.Api.Net.Inner; using Impostor.Api.Net.Messages; using Impostor.Api.Unity; @@ -381,6 +382,11 @@ namespace Impostor.Server.Net.State case InnerPlayerControl control: { + if (GameState != GameStates.Started) + { + GameNet.GameData?.RemovePlayer(control); + } + // Remove InnerPlayerControl <-> IClientPlayer. if (TryGetPlayer(control.OwnerId, out var player)) { @@ -414,8 +420,6 @@ namespace Impostor.Server.Net.State } _allObjectsFast.Remove(obj.NetId); - - obj.NetId = uint.MaxValue; } } } diff --git a/src/Impostor.Server/Net/State/Game.State.cs b/src/Impostor.Server/Net/State/Game.State.cs index a2105f1..ae688d8 100644 --- a/src/Impostor.Server/Net/State/Game.State.cs +++ b/src/Impostor.Server/Net/State/Game.State.cs @@ -39,7 +39,7 @@ namespace Impostor.Server.Net.State _logger.LogInformation("{0} - Player {1} ({2}) has left.", Code, player.Client.Name, playerId); - if (GameState == GameStates.Starting || GameState == GameStates.Started) + if (GameState == GameStates.Starting || GameState == GameStates.Started || GameState == GameStates.NotStarted) { if (player.Character?.PlayerInfo != null) { -- 2.39.5