-namespace Impostor.Api.Events.Meeting
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Events.Meeting
{
public interface IMeetingEndedEvent : IMeetingEvent
{
+ /// <summary>
+ /// Gets the exiled player.
+ /// </summary>
+ IInnerPlayerControl? Exiled { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether meeting result is a tie.
+ /// </summary>
+ bool IsTie { get; }
}
}
--- /dev/null
+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
+ {
+ /// <summary>
+ /// Gets the player he voted for.
+ /// </summary>
+ IInnerPlayerControl? VotedFor { get; }
+
+ /// <summary>
+ /// Gets the Vote type.
+ /// </summary>
+ VoteType VoteType { get; }
+ }
+}
-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
+ {
+ /// <summary>
+ /// Gets the player info of this vote state.
+ /// </summary>
+ IInnerPlayerInfo TargetPlayer { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the player is a dead.
+ /// </summary>
+ bool IsDead { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the player started this meeting.
+ /// </summary>
+ bool DidReport { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the player voted.
+ /// </summary>
+ bool DidVote { get; }
+
+ /// <summary>
+ /// Gets the vote type.
+ /// </summary>
+ /// <remarks>
+ /// Null when <see cref="DidVote" /> is false.
+ /// </remarks>
+ [MemberNotNullWhen(true, nameof(DidVote))]
+ public VoteType? VoteType { get; }
+
+ /// <summary>
+ /// Gets the player voted for.
+ /// </summary>
+ /// <remarks>
+ /// Null when <see cref="VoteType" /> isn't <see cref="F:Impostor.Api.Events.Player.VoteType.Player"/>.
+ /// </remarks>
+ public IInnerPlayerControl? VotedFor { get; }
+ }
+
+ /// <summary>
+ /// Gets states of players taking part in this meeting.
+ /// </summary>
+ IReadOnlyCollection<IPlayerVoteArea> PlayerStates { get; }
+
+ /// <summary>
+ /// Gets the player that started the meeting.
+ /// </summary>
+ IInnerPlayerInfo? Reporter { get; }
}
}
[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);
+ }
+ }
}
}
}
_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)
{
{
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; }
}
}
--- /dev/null
+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; }
+ }
+}
return null;
}
+ internal void RemovePlayer(InnerPlayerControl control)
+ {
+ _allPlayers.TryRemove(control.PlayerInfo.PlayerId, out _);
+ }
+
private void SetTasks(byte playerId, ReadOnlyMemory<byte> taskTypeIds)
{
var player = GetPlayerById(playerId);
-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.IPlayerVoteArea> IInnerMeetingHud.PlayerStates => Array.AsReadOnly(_playerStates);
+
+ IInnerPlayerInfo? IInnerMeetingHud.Reporter => Reporter;
}
}
-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; }
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;
}
}
}
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;
private readonly ILogger<InnerMeetingHud> _logger;
private readonly IEventManager _eventManager;
+ private readonly CancellationTokenSource _timerToken;
+
[AllowNull]
private PlayerVoteArea[] _playerStates;
_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<bool> SerializeAsync(IMessageWriter writer, bool initialState)
{
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;
}
}
}
{
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();
+ }
}
}
}
}
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:
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<byte> 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<bool> HandleCastVote(ClientPlayer sender, ClientPlayer? target, byte playerId, sbyte suspectPlayerId)
+ private async ValueTask<bool> HandleCastVoteAsync(ClientPlayer sender, ClientPlayer? target, byte playerId, sbyte suspectPlayerId)
{
if (sender.IsHost)
{
}
}
+ 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>(T[] self, Func<T, int> 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));
+ }
}
}
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;
case InnerPlayerControl control:
{
+ if (GameState != GameStates.Started)
+ {
+ GameNet.GameData?.RemovePlayer(control);
+ }
+
// Remove InnerPlayerControl <-> IClientPlayer.
if (TryGetPlayer(control.OwnerId, out var player))
{
}
_allObjectsFast.Remove(obj.NetId);
-
- obj.NetId = uint.MaxValue;
}
}
}
_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)
{