]> git.deb.at Git - rhonda/impostor.git/commitdiff
Remove AllowNull from PlayerControl.PlayerInfo
authorminiduikboot <mini@duikbo.at>
Thu, 27 Feb 2025 23:00:21 +0000 (00:00 +0100)
committerminiduikboot <mini@duikbo.at>
Thu, 22 May 2025 21:50:28 +0000 (23:50 +0200)
It is possible to spawn invalid network objects without an attached
PlayerInfo, and Impostor will throw exceptions everywhere if you happen
to spawn one. Officials do allow spawning objects like this, so allow it
in Impostor for compatibility reasons

src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs
src/Impostor.Api/Net/Messages/S2C/Message01JoinGameS2C.cs
src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs
src/Impostor.Server/Net/Inner/InnerNetObject.Anticheat.cs
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
src/Impostor.Server/Net/State/Game.cs

index f6c439c73acfeea587fb36f3d9f61041397ef14a..c33934602a0274d01fedebbb4eac523068134645 100644 (file)
@@ -29,7 +29,7 @@ namespace Impostor.Api.Net.Inner.Objects
         ///     Gets the <see cref="IInnerPlayerInfo" /> of the <see cref="IInnerPlayerControl" />.
         ///     Contains metadata about the player.
         /// </summary>
-        IInnerPlayerInfo PlayerInfo { get; }
+        IInnerPlayerInfo? PlayerInfo { get; }
 
         /// <summary>
         ///     Sets the name of the current <see cref="IInnerPlayerControl" />.
index 02df684b91167a2e4dca5b6469eda1867332f2df..3c89cfb5d7b32c105e1de828d03ef702c239054e 100644 (file)
@@ -18,7 +18,7 @@ namespace Impostor.Api.Net.Messages.S2C
             writer.Write(hostId);
             writer.Write(player.Client.Name);
             player.Client.PlatformSpecificData.Serialize(writer);
-            writer.WritePacked(player.Character?.PlayerInfo.PlayerLevel ?? 1);
+            writer.WritePacked(player.Character?.PlayerInfo?.PlayerLevel ?? 1);
 
             // ProductUserId and FriendCode are not yet known, so set them to an empty string
             writer.Write(string.Empty);
index d5d3718314c407378473f83be5e545feb5646994..9ddeb030715999be1dfda42a910241dd4390bbee 100644 (file)
@@ -22,7 +22,7 @@ namespace Impostor.Api.Net.Messages.S2C
                 writer.WritePacked(ply.Client.Id);
                 writer.Write(ply.Client.Name);
                 ply.Client.PlatformSpecificData.Serialize(writer);
-                writer.WritePacked(ply.Character?.PlayerInfo.PlayerLevel ?? 1);
+                writer.WritePacked(ply.Character?.PlayerInfo?.PlayerLevel ?? 1);
 
                 // ProductUserId and FriendCode are not yet known, so set them to an empty string
                 writer.Write(string.Empty);
index 594ff6f161eab3bd1deb1c6d6b1bb584533e8255..1f38c918eb6fa19fb3dd95c5b074ffccff26ddd7 100644 (file)
@@ -73,9 +73,16 @@ namespace Impostor.Server.Net.Inner
             return true;
         }
 
-        protected async ValueTask<bool> ValidateImpostor(CheatContext context, IClientPlayer sender, InnerPlayerInfo playerInfo, bool value = true)
+        protected async ValueTask<bool> ValidateImpostor(CheatContext context, IClientPlayer sender, InnerPlayerInfo? playerInfo, bool value = true)
         {
-            if (playerInfo.IsImpostor != value)
+            if (playerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.InvalidObject, "Couldn't check if Impostor, playerInfo not set"))
+                {
+                    return false;
+                }
+            }
+            else if (playerInfo.IsImpostor != value)
             {
                 if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, "Failed impostor check"))
                 {
@@ -86,9 +93,16 @@ namespace Impostor.Server.Net.Inner
             return true;
         }
 
-        protected async ValueTask<bool> ValidateCanVent(CheatContext context, IClientPlayer sender, InnerPlayerInfo playerInfo, bool value = true)
+        protected async ValueTask<bool> ValidateCanVent(CheatContext context, IClientPlayer sender, InnerPlayerInfo? playerInfo, bool value = true)
         {
-            if (playerInfo.CanVent != value)
+            if (playerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.InvalidObject, "Couldn't check if can vent, playerInfo not set"))
+                {
+                    return false;
+                }
+            }
+            else if (playerInfo.CanVent != value)
             {
                 if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, "Failed can vent check"))
                 {
@@ -99,9 +113,16 @@ namespace Impostor.Server.Net.Inner
             return true;
         }
 
-        protected async ValueTask<bool> ValidateRole(CheatContext context, IClientPlayer sender, InnerPlayerInfo playerInfo, RoleTypes role)
+        protected async ValueTask<bool> ValidateRole(CheatContext context, IClientPlayer sender, InnerPlayerInfo? playerInfo, RoleTypes role)
         {
-            if (playerInfo.RoleType != role)
+            if (playerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.InvalidObject, "Couldn't check role, playerInfo not set"))
+                {
+                    return false;
+                }
+            }
+            else if (playerInfo.RoleType != role)
             {
                 if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, $"Failed role = {role} check"))
                 {
index 78916c2b70188951c30aec8867253dd77c811812..92e579357b9519de35248a35e52e82c9c2f1f900 100644 (file)
@@ -287,7 +287,7 @@ namespace Impostor.Server.Net.Inner.Objects
             var max = MaxPair(self, out var tie);
             var exiled = tie ? null : Game.GameNet.GameData!.GetPlayerById(max.Key)?.Controller;
 
-            if (exiled != null)
+            if (exiled != null && exiled.PlayerInfo != null)
             {
                 exiled.PlayerInfo.LastDeathReason = DeathReason.Exile;
                 await _eventManager.CallAsync(new PlayerExileEvent(Game, Game.GetClientPlayer(exiled!.OwnerId)!, exiled));
index 31240beb5b11ba6f9781c64485d67ff06284eb16..99ca2e337ac627dd9aedf1d77049a9118aa6e520 100644 (file)
@@ -17,10 +17,15 @@ namespace Impostor.Server.Net.Inner.Objects
 
         IInnerCustomNetworkTransform IInnerPlayerControl.NetworkTransform => NetworkTransform;
 
-        IInnerPlayerInfo IInnerPlayerControl.PlayerInfo => PlayerInfo;
+        IInnerPlayerInfo? IInnerPlayerControl.PlayerInfo => PlayerInfo;
 
         public async ValueTask SetNameAsync(string name)
         {
+            if (PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Cannot set name, PlayerInfo is null");
+            }
+
             PlayerInfo.CurrentOutfit.PlayerName = name;
 
             using var writer = Game.StartRpc(NetId, RpcCalls.SetName);
@@ -30,6 +35,11 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public async ValueTask SetColorAsync(ColorType color)
         {
+            if (PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Cannot set color, PlayerInfo is null");
+            }
+
             PlayerInfo.CurrentOutfit.Color = color;
 
             using var writer = Game.StartRpc(NetId, RpcCalls.SetColor);
@@ -39,6 +49,11 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public async ValueTask SetHatAsync(string hatId)
         {
+            if (PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Cannot set hat, PlayerInfo is null");
+            }
+
             PlayerInfo.CurrentOutfit.HatId = hatId;
 
             using var writer = Game.StartRpc(NetId, RpcCalls.SetHatStr);
@@ -48,6 +63,11 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public async ValueTask SetPetAsync(string petId)
         {
+            if (PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Cannot set pet, PlayerInfo is null");
+            }
+
             PlayerInfo.CurrentOutfit.PetId = petId;
 
             using var writer = Game.StartRpc(NetId, RpcCalls.SetPetStr);
@@ -57,6 +77,11 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public async ValueTask SetSkinAsync(string skinId)
         {
+            if (PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Cannot set skin, PlayerInfo is null");
+            }
+
             PlayerInfo.CurrentOutfit.SkinId = skinId;
 
             using var writer = Game.StartRpc(NetId, RpcCalls.SetSkinStr);
@@ -66,6 +91,11 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public async ValueTask SetVisorAsync(string visorId)
         {
+            if (PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Cannot set visor, PlayerInfo is null");
+            }
+
             PlayerInfo.CurrentOutfit.VisorId = visorId;
 
             using var writer = Game.StartRpc(NetId, RpcCalls.SetVisorStr);
@@ -75,6 +105,11 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public async ValueTask SetNamePlateAsync(string nameplateId)
         {
+            if (PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Cannot set nameplate, PlayerInfo is null");
+            }
+
             PlayerInfo.CurrentOutfit.NamePlateId = nameplateId;
 
             using var writer = Game.StartRpc(NetId, RpcCalls.SetNamePlateStr);
@@ -103,7 +138,11 @@ namespace Impostor.Server.Net.Inner.Objects
 
         private bool ValidateMurderPlayer(IInnerPlayerControl target, MurderResultFlags result, [NotNullWhen(false)] out string? invalidReason)
         {
-            if (!PlayerInfo.IsImpostor)
+            if (PlayerInfo == null)
+            {
+                invalidReason = "Tried to murder a player, but the murderer didn't have a playerinfo";
+            }
+            else if (!PlayerInfo.IsImpostor)
             {
                 invalidReason = "Tried to murder a player, but murderer was not an impostor.";
             }
@@ -111,6 +150,10 @@ namespace Impostor.Server.Net.Inner.Objects
             {
                 invalidReason = "Tried to murder a player, but murderer was not alive.";
             }
+            else if (target.PlayerInfo == null)
+            {
+                invalidReason = "Tried to murder a player, but the murderer didn't have a playerinfo";
+            }
             else if (target.PlayerInfo.IsImpostor)
             {
                 invalidReason = "Tried to murder a player, but target is an impostor";
@@ -159,7 +202,11 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public async ValueTask ProtectPlayerAsync(IInnerPlayerControl target)
         {
-            if (target.PlayerInfo.IsDead)
+            if (target.PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Tried to exile a player, but target didn't have a playerinfo");
+            }
+            else if (target.PlayerInfo.IsDead)
             {
                 throw new ImpostorProtocolException("Tried to protect a player that is dead");
             }
@@ -172,13 +219,17 @@ namespace Impostor.Server.Net.Inner.Objects
             ((InnerPlayerControl)target).Protect(this);
 
             using var writer = Game.StartRpc(NetId, RpcCalls.ProtectPlayer);
-            Rpc45ProtectPlayer.Serialize(writer, target, PlayerInfo.CurrentOutfit.Color);
+            Rpc45ProtectPlayer.Serialize(writer, target, PlayerInfo?.CurrentOutfit.Color ?? ColorType.Red);
             await Game.FinishRpcAsync(writer);
         }
 
         public async ValueTask ExileAsync()
         {
-            if (PlayerInfo.IsDead)
+            if (PlayerInfo == null)
+            {
+                throw new ImpostorProtocolException("Tried to exile a player, but target didn't have a playerinfo");
+            }
+            else if (PlayerInfo.IsDead)
             {
                 throw new ImpostorProtocolException("Tried to exile a player, but target was not alive.");
             }
index 4922dfdc930ff780f357087495e9ab5fb87a79a5..fb49b74b7b6442e7b1ccbf9c5b7e8fe00abfca68 100644 (file)
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading.Tasks;
 using Impostor.Api;
@@ -57,8 +56,7 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public InnerCustomNetworkTransform NetworkTransform { get; }
 
-        [AllowNull]
-        public InnerPlayerInfo PlayerInfo { get; internal set; }
+        public InnerPlayerInfo? PlayerInfo { get; internal set; }
 
         internal Queue<string> RequestedPlayerName { get; } = new Queue<string>();
 
@@ -385,15 +383,25 @@ namespace Impostor.Server.Net.Inner.Objects
 
                     Rpc44SetRole.Deserialize(reader, out var role, out var _);
 
-                    if (role is RoleTypes.ImpostorGhost or RoleTypes.CrewmateGhost or RoleTypes.GuardianAngel)
+                    if (PlayerInfo == null)
                     {
-                        PlayerInfo.RoleWhenAlive = PlayerInfo.RoleType;
-                        PlayerInfo.IsDead = true;
+                        if (await sender.Client.ReportCheatAsync(RpcCalls.SetRole, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                        {
+                            return false;
+                        }
                     }
+                    else
+                    {
+                        if (role is RoleTypes.ImpostorGhost or RoleTypes.CrewmateGhost or RoleTypes.GuardianAngel)
+                        {
+                            PlayerInfo.RoleWhenAlive = PlayerInfo.RoleType;
+                            PlayerInfo.IsDead = true;
+                        }
 
-                    PlayerInfo.RoleType = role;
+                        PlayerInfo.RoleType = role;
+                    }
 
-                    if (Game.GameState == GameStates.Starting && Game.Players.All(clientPlayer => clientPlayer.Character?.PlayerInfo.RoleType != null))
+                    if (Game.GameState == GameStates.Starting && Game.Players.All(clientPlayer => clientPlayer.Character?.PlayerInfo?.RoleType != null))
                     {
                         await Game.StartedAsync();
                     }
@@ -585,8 +593,19 @@ namespace Impostor.Server.Net.Inner.Objects
 
         internal void Die(DeathReason reason)
         {
-            PlayerInfo.IsDead = true;
-            PlayerInfo.LastDeathReason = reason;
+            if (PlayerInfo == null)
+            {
+                // "Custom Net Objects" aka hacked up PlayerControl objects as popularized by host only mods may trigger this
+                if (!Game.IsHostAuthoritive)
+                {
+                    _logger.LogWarning("Tried to kill player that didn't have a PlayerInfo set, this shouldn't happen");
+                }
+            }
+            else
+            {
+                PlayerInfo.IsDead = true;
+                PlayerInfo.LastDeathReason = reason;
+            }
         }
 
         internal void Protect(InnerPlayerControl guardianAngel)
@@ -598,7 +617,18 @@ namespace Impostor.Server.Net.Inner.Objects
 
         private async ValueTask HandleCompleteTask(ClientPlayer sender, uint taskId)
         {
-            var task = PlayerInfo.Tasks.ElementAtOrDefault((int)taskId);
+            TaskInfo? task = null;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CompleteTask, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return;
+                }
+            }
+            else
+            {
+                task = PlayerInfo.Tasks.ElementAtOrDefault((int)taskId);
+            }
 
             if (task != null)
             {
@@ -662,7 +692,10 @@ namespace Impostor.Server.Net.Inner.Objects
 
             if (sender.IsOwner(this))
             {
-                if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.PlayerName == name))
+                if (Game.Players.Any(x => x.Character != null &&
+                                     x.Character != this &&
+                                     x.Character.PlayerInfo != null &&
+                                     x.Character.PlayerInfo.PlayerName == name))
                 {
                     if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.NameLimits, "Client sent name that is already used"))
                     {
@@ -685,14 +718,20 @@ namespace Impostor.Server.Net.Inner.Objects
                     var expected = RequestedPlayerName.Dequeue();
                     var requested = expected;
 
-                    if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.PlayerName == expected))
+                    if (Game.Players.Any(x => x.Character != null &&
+                                         x.Character != this &&
+                                         x.Character.PlayerInfo != null &&
+                                         x.Character.PlayerInfo.PlayerName == expected))
                     {
                         var i = 1;
                         while (true)
                         {
                             var text = expected + " " + i;
 
-                            if (Game.Players.All(x => x.Character == null || x.Character == this || x.Character.PlayerInfo.PlayerName != text))
+                            if (Game.Players.All(x => x.Character == null ||
+                                                 x.Character == this ||
+                                                 x.Character.PlayerInfo == null ||
+                                                 x.Character.PlayerInfo.PlayerName != text))
                             {
                                 expected = text;
                                 break;
@@ -723,7 +762,17 @@ namespace Impostor.Server.Net.Inner.Objects
                 }
             }
 
-            PlayerInfo.CurrentOutfit.PlayerName = name;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                PlayerInfo.CurrentOutfit.PlayerName = name;
+            }
 
             return true;
         }
@@ -843,7 +892,17 @@ namespace Impostor.Server.Net.Inner.Objects
                 }
             }
 
-            PlayerInfo.CurrentOutfit.Color = color;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                PlayerInfo.CurrentOutfit.Color = color;
+            }
 
             // Record the color so it can be restored on the next game
             if (Game.TryGetPlayer(OwnerId, out var clientPlayer))
@@ -866,8 +925,18 @@ namespace Impostor.Server.Net.Inner.Objects
                 return false;
             }
 
-            PlayerInfo.CurrentOutfit.HatId = hat;
-            PlayerInfo.CurrentOutfit.HatSequenceId = nextRpcSequenceId;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetHatStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                PlayerInfo.CurrentOutfit.HatId = hat;
+                PlayerInfo.CurrentOutfit.HatSequenceId = nextRpcSequenceId;
+            }
 
             return true;
         }
@@ -880,8 +949,18 @@ namespace Impostor.Server.Net.Inner.Objects
                 return false;
             }
 
-            PlayerInfo.CurrentOutfit.SkinId = skin;
-            PlayerInfo.CurrentOutfit.SkinSequenceId = nextRpcSequenceId;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetSkinStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                PlayerInfo.CurrentOutfit.SkinId = skin;
+                PlayerInfo.CurrentOutfit.SkinSequenceId = nextRpcSequenceId;
+            }
 
             return true;
         }
@@ -894,8 +973,18 @@ namespace Impostor.Server.Net.Inner.Objects
                 return false;
             }
 
-            PlayerInfo.CurrentOutfit.VisorId = visor;
-            PlayerInfo.CurrentOutfit.VisorSequenceId = nextRpcSequenceId;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetVisorStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                PlayerInfo.CurrentOutfit.VisorId = visor;
+                PlayerInfo.CurrentOutfit.VisorSequenceId = nextRpcSequenceId;
+            }
 
             return true;
         }
@@ -908,8 +997,18 @@ namespace Impostor.Server.Net.Inner.Objects
                 return false;
             }
 
-            PlayerInfo.CurrentOutfit.NamePlateId = namePlate;
-            PlayerInfo.CurrentOutfit.NamePlateSequenceId = nextRpcSequenceId;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetNamePlateStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                PlayerInfo.CurrentOutfit.NamePlateId = namePlate;
+                PlayerInfo.CurrentOutfit.NamePlateSequenceId = nextRpcSequenceId;
+            }
 
             return true;
         }
@@ -922,14 +1021,31 @@ namespace Impostor.Server.Net.Inner.Objects
                 return false;
             }
 
-            PlayerInfo.PlayerLevel = level;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetLevel, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                PlayerInfo.PlayerLevel = level;
+            }
 
             return true;
         }
 
         private async ValueTask<bool> HandleCheckMurder(ClientPlayer sender, InnerPlayerControl? target)
         {
-            if (!PlayerInfo.CanMurder(Game, _dateTimeProvider))
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else if (!PlayerInfo.CanMurder(Game, _dateTimeProvider))
             {
                 if (IsMurdering == target)
                 {
@@ -944,10 +1060,10 @@ namespace Impostor.Server.Net.Inner.Objects
                         return false;
                     }
                 }
-            }
 
-            PlayerInfo.LastMurder = _dateTimeProvider.UtcNow - TimeSpan.FromMilliseconds(sender.Client.Connection.AveragePing);
-            IsMurdering = target;
+                PlayerInfo.LastMurder = _dateTimeProvider.UtcNow - TimeSpan.FromMilliseconds(sender.Client.Connection.AveragePing);
+                IsMurdering = target;
+            }
 
             // Check if host authority mode is on
             if (_game.IsHostAuthoritive)
@@ -1017,7 +1133,7 @@ namespace Impostor.Server.Net.Inner.Objects
                 }
             }
 
-            if (target != null && !target.PlayerInfo.IsDead)
+            if (target != null && target.PlayerInfo != null && !target.PlayerInfo.IsDead)
             {
                 // In host authoritive mode every client has to figure out if the kill was prevented by guardian protection on it's own
                 if ((result & MurderResultFlags.Succeeded) != 0 && target.IsProtected)
@@ -1086,8 +1202,18 @@ namespace Impostor.Server.Net.Inner.Objects
                 return false;
             }
 
-            PlayerInfo.CurrentOutfit.PetId = pet;
-            PlayerInfo.CurrentOutfit.PetSequenceId = nextRpcSequenceId;
+            if (PlayerInfo == null)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetPetStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                PlayerInfo.CurrentOutfit.PetId = pet;
+                PlayerInfo.CurrentOutfit.PetSequenceId = nextRpcSequenceId;
+            }
 
             return true;
         }
index 5b5921d3a851b588f8d1ad7527161200ebd9fb9c..07fd480b9ec805eb81591d815d0f994430b12195 100644 (file)
@@ -139,7 +139,10 @@ namespace Impostor.Server.Net.State
         /// <returns>True if there is player other than exceptBy that uses that color.</returns>
         internal bool IsColorUsed(ColorType color, IInnerPlayerControl? exceptBy = null)
         {
-            return Players.Any(p => p.Character != null && p.Character != exceptBy && p.Character.PlayerInfo.CurrentOutfit.Color == color);
+            return Players.Any(p => p.Character != null &&
+                               p.Character != exceptBy &&
+                               p.Character.PlayerInfo != null &&
+                               p.Character.PlayerInfo.CurrentOutfit.Color == color);
         }
 
         private ValueTask BroadcastJoinMessage(IMessageWriter message, bool clear, ClientPlayer player)