]> git.deb.at Git - rhonda/impostor.git/commitdiff
Added player voted event (#181)
authorMr-VIT <vit@v1t.su>
Fri, 11 Jun 2021 12:33:38 +0000 (15:33 +0300)
committerGitHub <noreply@github.com>
Fri, 11 Jun 2021 12:33:38 +0000 (12:33 +0000)
Co-authored-by: AeonLucid <aeonlucid@outlook.com>
Co-authored-by: js6pak <kubastaron@hotmail.com>
13 files changed:
src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs
src/Impostor.Api/Events/Game/Player/IPlayerVotedEvent.cs [new file with mode: 0644]
src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs
src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs
src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs
src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs
src/Impostor.Server/Events/Game/Player/PlayerVotedEvent.cs [new file with mode: 0644]
src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs
src/Impostor.Server/Net/State/Game.Data.cs
src/Impostor.Server/Net/State/Game.State.cs

index a2175806aeab2c1a0604a60cf657d6e327dc6f9d..7787be69ff248939ed6ea88ab9c7a556b5c2d51a 100644 (file)
@@ -1,6 +1,17 @@
-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; }
     }
 }
diff --git a/src/Impostor.Api/Events/Game/Player/IPlayerVotedEvent.cs b/src/Impostor.Api/Events/Game/Player/IPlayerVotedEvent.cs
new file mode 100644 (file)
index 0000000..2533266
--- /dev/null
@@ -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
+    {
+        /// <summary>
+        ///     Gets the player he voted for.
+        /// </summary>
+        IInnerPlayerControl? VotedFor { get; }
+
+        /// <summary>
+        ///     Gets the Vote type.
+        /// </summary>
+        VoteType VoteType { get; }
+    }
+}
index 6d8f10672886aa8937a6eb59aa5d53ac8b08398c..b6c781fc3cd4d0249deeeaf3918b17e79f63635d 100644 (file)
@@ -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
+        {
+            /// <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; }
     }
 }
index 84948a71cb491d0347be3b09e65c0174aa72cda8..8fb9d50f28be9356359b31663564c5333df51aa3 100644 (file)
@@ -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);
+                }
+            }
         }
     }
 }
index 4477c0478af6734c2f92766d6b36d7b6ec1fb567..986b220fbf732d2e8e35697a5009000542473fda 100644 (file)
@@ -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)
         {
index cf55f7d37ac5d8fe1d1dc2b7dafeee3acb6ca3db..b64699f61785a1cb3fa21efdfdfa8c57bd8b9458 100644 (file)
@@ -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 (file)
index 0000000..68a9096
--- /dev/null
@@ -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; }
+    }
+}
index c466746a5f1762997af16c70282855dd0a62ae66..a7c2e3044c976fcaa675381c5a5221d84f8b24c8 100644 (file)
@@ -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<byte> taskTypeIds)
         {
             var player = GetPlayerById(playerId);
index 0219fbab20ba09ed5dc7756cc03ed52253af2aaf..8b1528d41b07ae605521f80ef7412b912ae0fdcd 100644 (file)
@@ -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.IPlayerVoteArea> IInnerMeetingHud.PlayerStates => Array.AsReadOnly(_playerStates);
+
+        IInnerPlayerInfo? IInnerMeetingHud.Reporter => Reporter;
     }
 }
index 48f551b9a8d90205d01053c81487aaea1843fee7..26ac98111acd26a8827e6e5878adb769c5cba2a9 100644 (file)
@@ -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;
             }
         }
     }
index 104ceae8ce06c75ed1d1651ab66fc6a063e81492..6dcd105a9abf8f1b6b81b3e37a3bc5028b41e41a 100644 (file)
@@ -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<InnerMeetingHud> _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<bool> 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<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)
             {
@@ -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>(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));
+        }
     }
 }
index ee1111c1389a10672b6f8cfd13f693b74af23f15..19d2dab1993bc2da896fa6f1f30537c2e2459610 100644 (file)
@@ -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;
         }
     }
 }
index a2105f1465a0c1bf7b6ace99a266b3172b34909f..ae688d8bfb2f206bc7c584cf1d724f41dfc2df18 100644 (file)
@@ -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)
                 {