Remove AllowNull from PlayerControl.PlayerInfo

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
This commit is contained in:
miniduikboot committed 2025-05-22 23:50:28 +02:00
1 parent fff9713697
commit e1bd85313a
8 files changed
+250 -49

No files matched your search

@@ -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" />.
@@ -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);
@@ -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);
@@ -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"))
{
@@ -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));
@@ -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.");
}
@@ -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;
}
+4 -1
View 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)