]> git.deb.at Git - rhonda/impostor.git/commitdiff
Make Impostor's AC modular (#559)
authorminiduikboot <mini@duikbo.at>
Thu, 4 Apr 2024 18:09:59 +0000 (20:09 +0200)
committerGitHub <noreply@github.com>
Thu, 4 Apr 2024 18:09:59 +0000 (20:09 +0200)
* Add extra configuration options for the anticheat

* Implement option to exempt hosts from AC checks

* Add AntiCheatConfig to InnerNetObject and friends

* Categorize calls in InnerNetObject.Anticheat

* Categorize other calls to ReportCheatAsync

* Update cosmetics AC handling

When Innersloth changed SetHat etc to SetHatStr as part of the cosmicube
update we accidentally created a few dead methods. Restore and update
the code.

* Push up cheat category to ReportCheatAsync

This makes it possible to log failed checks for diagnosis. Also much
cleaner now that we don't have to push AntiCheatConfig everywhere.
Should've done this immediately q.q

* Add back old version of ReportCheatAsync

some plugins used this method. affected plugins should rebuild after the
next impostor release

* Add documentation for modular anticheat

* Address comments

* Sort AntiCheatConfig keys

* Rework the option to allow cheating hosts

This option should actually be tristate: to not break host-only mods we
should disable the anticheat for hosts running host-only mods.

* Document CheatingHostMode

* Split Limits category in Color and Name Limits

13 files changed:
docs/Server-configuration.md
src/Impostor.Api/CheatCategory.cs [new file with mode: 0644]
src/Impostor.Api/Config/AntiCheatConfig.cs
src/Impostor.Api/Config/CheatingHostMode.cs [new file with mode: 0644]
src/Impostor.Api/Net/IClient.cs
src/Impostor.Server/Net/Client.cs
src/Impostor.Server/Net/ClientBase.cs
src/Impostor.Server/Net/Inner/InnerNetObject.Anticheat.cs
src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs
src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs
src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
src/Impostor.Server/Net/State/Game.Data.cs

index 21bee106b49c258c6d1b2a478211437bf6c45edb..4d4c0d60ae12b1c455ad533e15b413587474834f 100644 (file)
@@ -27,10 +27,20 @@ Impostor has an Http Server that is used by recent versions of Among Us to conne
 
 Impostor has an Anticheat that makes it possible to kick cheaters from games automatically. Note that the anticheat is tuned on the vanilla version of the game, so client-side modifications could trigger the Anticheat if you're playing with them.
 
-| Key               | Default | Value                                                                                                                                                                                                               |
-| ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Enabled**       | `true`  | Whether the anticheat should be enabled.                                                                                                                                                                            |
-| **BanIpFromGame** | `true`  | When anticheat is enabled and a player is caught hacking, they will be kicked from the server. If this value is set to `true`, the player will be banned instead and will not be able to rejoin that specific game. |
+| Key                        | Default   | Value                                                                                                                                                                                                               |
+|----------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Enabled**                | `true`    | Whether the anticheat should be enabled.                                                                                                                                                                            |
+| **BanIpFromGame**          | `true`    | When anticheat is enabled and a player is caught hacking, they will be kicked from the server. If this value is set to `true`, the player will be banned instead and will not be able to rejoin that specific game. |
+| **AllowCheatingHosts**     | `"Never"` | Configure whether hosts are allowed to cheat. "Never" forbids it, "Always" allows it. "IfRequested" allows hosts to cheat if they connect with the DisableServerAuthorityFlag set.                                  |
+| **EnableGameFlowChecks**   | `true`    | Enable checks that check if certain actions are done in the appropriate order or at the appropriate moment in the game. This includes changing cosmetics while in game or murdering too fast.                       |
+| **EnableMustBeHostChecks** | `true`    | Enables checks that check if players are the host before they can do actions that require them to be host of the game. This includes starting the game and spawning objects.                                        |
+| **EnableColorLimitChecks** | `true`    | Enables checks that checks if players request colors that are already in use.                                                                                                                                       |
+| **EnableNameLimitChecks**  | `true`    | Enables checks that checks if player names have a length that is possible to set using the user interface.                                                                                                          |
+| **EnableOwnershipChecks**  | `true`    | Enables checks that check if players are allowed to perform a certain action on themself or another player.                                                                                                         |
+| **EnableRoleChecks**       | `true`    | Enables checks that check if players have the correct role when performing certain role abilities like venting or murdering.                                                                                        |
+| **EnableTargetChecks**     | `true`    | Enables checks that check if certain packets to everyone that should only have been sent to certain players or vice versa. This includes sending votes or network objects.                                          |
+
+| **ForbidProtocolExtensions** | `true`  | If disabled allows players to send network packets that go beyond the network packets sent by the vanilla game. This is necessary for most mods that need all players to install it.                                |
 
 ### Compatibility
 
diff --git a/src/Impostor.Api/CheatCategory.cs b/src/Impostor.Api/CheatCategory.cs
new file mode 100644 (file)
index 0000000..bbc8740
--- /dev/null
@@ -0,0 +1,31 @@
+namespace Impostor.Api;
+
+public enum CheatCategory
+{
+    /// <summary>A packet used a part of the network protocol that is unknown to Impostor, like a custom RPC.</summary>
+    ProtocolExtension,
+
+    /// <summary>A packet was sent at an inappropriate moment.</summary>
+    GameFlow,
+
+    /// <summary>A packet was sent by a non-host player that should normally only be sent by the host.</summary>
+    MustBeHost,
+
+    /// <summary>A packet was sent that violated limits on the selection of player colors.</summary>
+    ColorLimits,
+
+    /// <summary>A packet was sent that exceeded the limits of possible nicknames to enter ingame.</summary>
+    NameLimits,
+
+    /// <summary>A packet was sent on behalf of another player.</summary>
+    Ownership,
+
+    /// <summary>An ability was used that the current role cannot access.</summary>
+    Role,
+
+    /// <summary>A packet was sent to a player that should be broadcasted, or vice versa.</summary>
+    Target,
+
+    /// <summary>Legacy category for unsorted anticheat checks.</summary>
+    Other,
+}
index d9eb532410a22fc227b05fac634a5bb4fc3e2f4a..257666417e9cb07af1aef39a908e1c02d2d92bb6 100644 (file)
@@ -7,5 +7,23 @@
         public bool Enabled { get; set; } = true;
 
         public bool BanIpFromGame { get; set; } = true;
+
+        public CheatingHostMode AllowCheatingHosts { get; set; } = CheatingHostMode.Never;
+
+        public bool EnableGameFlowChecks { get; set; } = true;
+
+        public bool EnableMustBeHostChecks { get; set; } = true;
+
+        public bool EnableColorLimitChecks { get; set; } = true;
+
+        public bool EnableNameLimitChecks { get; set; } = true;
+
+        public bool EnableOwnershipChecks { get; set; } = true;
+
+        public bool EnableRoleChecks { get; set; } = true;
+
+        public bool EnableTargetChecks { get; set; } = true;
+
+        public bool ForbidProtocolExtensions { get; set; } = true;
     }
 }
diff --git a/src/Impostor.Api/Config/CheatingHostMode.cs b/src/Impostor.Api/Config/CheatingHostMode.cs
new file mode 100644 (file)
index 0000000..103f6ca
--- /dev/null
@@ -0,0 +1,29 @@
+namespace Impostor.Api.Config
+{
+    /// <summary>
+    /// Details if exceptions are made for hosts that are cheating.
+    /// </summary>
+    public enum CheatingHostMode
+    {
+        /// <summary>
+        /// Hosts follow the same policies as other players.
+        /// </summary>
+        Never,
+
+        /// <summary>
+        /// Hosts are allowed to cheat if they request HostAuthority. If they
+        /// do not request this, the same policies as for other players applies.
+        /// </summary>
+        /// <para>
+        /// HostAuthority can be requested by hosts by adding 25 to their patch
+        /// version when connecting. This flag is used by a lot of (host-only)
+        /// mods and also disable server authority over MurderPlayer packets.
+        /// </para>
+        IfRequested,
+
+        /// <summary>
+        /// Hosts are always allowed to cheat.
+        /// </summary>
+        Always,
+    }
+}
index 74309b2f13ef7bcb53ced3866515ed7ee833dd78..e4f8100144760f7b3cfefb34b74a09d674dc2212 100644 (file)
@@ -1,3 +1,4 @@
+using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using Impostor.Api.Innersloth;
@@ -74,6 +75,9 @@ namespace Impostor.Api.Net
         /// </summary>
         PlatformSpecificData PlatformSpecificData { get; }
 
+        ValueTask<bool> ReportCheatAsync(CheatContext context, CheatCategory category, string message);
+
+        [Obsolete("Please use the overload that adds a cheat category")]
         ValueTask<bool> ReportCheatAsync(CheatContext context, string message);
 
         ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType);
index 0debdd58fb08cef7616387ba24c7d3640e81b731..8579037306b874df9af6cbdb13b90bbd9bd01cf8 100644 (file)
@@ -35,14 +35,54 @@ namespace Impostor.Server.Net
             _customMessageManager = customMessageManager;
         }
 
-        public override async ValueTask<bool> ReportCheatAsync(CheatContext context, string message)
+        public override async ValueTask<bool> ReportCheatAsync(CheatContext context, CheatCategory category, string message)
         {
             if (!_antiCheatConfig.Enabled)
             {
                 return false;
             }
 
-            _logger.LogWarning("Client {Name} ({Id}) was caught cheating: [{Context}] {Message}", Name, Id, context.Name, message);
+            if (Player != null && Player.IsHost)
+            {
+                var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch {
+                    CheatingHostMode.Always => true,
+                    CheatingHostMode.IfRequested => GameVersion.HasDisableServerAuthorityFlag,
+                    CheatingHostMode.Never => false,
+                    _ => false,
+                };
+
+                if (isHostCheatingAllowed)
+                {
+                    return false;
+                }
+            }
+
+            bool LogUnknownCategory(CheatCategory category)
+            {
+                _logger.LogWarning("Unknown cheat category {Category} was used when reporting", category);
+                return true;
+            }
+
+            var isCategoryEnabled = category switch
+            {
+                CheatCategory.ProtocolExtension => _antiCheatConfig.ForbidProtocolExtensions,
+                CheatCategory.GameFlow => _antiCheatConfig.EnableGameFlowChecks,
+                CheatCategory.MustBeHost => _antiCheatConfig.EnableMustBeHostChecks,
+                CheatCategory.ColorLimits => _antiCheatConfig.EnableColorLimitChecks,
+                CheatCategory.NameLimits => _antiCheatConfig.EnableNameLimitChecks,
+                CheatCategory.Ownership => _antiCheatConfig.EnableOwnershipChecks,
+                CheatCategory.Role => _antiCheatConfig.EnableRoleChecks,
+                CheatCategory.Target => _antiCheatConfig.EnableTargetChecks,
+                CheatCategory.Other => true,
+                _ => LogUnknownCategory(category),
+            };
+
+            if (!isCategoryEnabled)
+            {
+                return false;
+            }
+
+            _logger.LogWarning("Client {Name} ({Id}) was caught cheating: [{Context}-{Category}] {Message}", Name, Id, context.Name, category, message);
 
             if (_antiCheatConfig.BanIpFromGame)
             {
index d967bda5d3979356cabce3cca52f18f98a6ba5d7..b52d51218e28f9ed40ed1ebca1a09cd6c4344b9f 100644 (file)
@@ -41,11 +41,16 @@ namespace Impostor.Server.Net
 
         IClientPlayer? IClient.Player => Player;
 
-        public virtual ValueTask<bool> ReportCheatAsync(CheatContext context, string message)
+        public virtual ValueTask<bool> ReportCheatAsync(CheatContext context, CheatCategory category, string message)
         {
             return new ValueTask<bool>(false);
         }
 
+        public ValueTask<bool> ReportCheatAsync(CheatContext context, string message)
+        {
+            return ReportCheatAsync(context, CheatCategory.Other, message);
+        }
+
         public abstract ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType);
 
         public abstract ValueTask HandleDisconnectAsync(string reason);
index 984d69928b70a437ec9f5ea0cd58edd48e1f7f63..594ff6f161eab3bd1deb1c6d6b1bb584533e8255 100644 (file)
@@ -12,7 +12,7 @@ namespace Impostor.Server.Net.Inner
         {
             if (!sender.IsOwner(this))
             {
-                if (await sender.Client.ReportCheatAsync(context, $"Failed ownership check on {GetType().Name}"))
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.Ownership, $"Failed ownership check on {GetType().Name}"))
                 {
                     return false;
                 }
@@ -25,7 +25,7 @@ namespace Impostor.Server.Net.Inner
         {
             if (!sender.IsHost)
             {
-                if (await sender.Client.ReportCheatAsync(context, "Failed host check"))
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.MustBeHost, "Failed host check"))
                 {
                     return false;
                 }
@@ -38,7 +38,7 @@ namespace Impostor.Server.Net.Inner
         {
             if (target == null)
             {
-                if (await sender.Client.ReportCheatAsync(context, "Failed target check"))
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.Target, "Failed target check"))
                 {
                     return false;
                 }
@@ -51,7 +51,7 @@ namespace Impostor.Server.Net.Inner
         {
             if (target != null)
             {
-                if (await sender.Client.ReportCheatAsync(context, "Failed broadcast check"))
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.Target, "Failed broadcast check"))
                 {
                     return false;
                 }
@@ -64,7 +64,7 @@ namespace Impostor.Server.Net.Inner
         {
             if (target == null || !target.IsHost)
             {
-                if (await sender.Client.ReportCheatAsync(context, "Failed cmd check"))
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.Target, "Failed cmd check"))
                 {
                     return false;
                 }
@@ -77,7 +77,7 @@ namespace Impostor.Server.Net.Inner
         {
             if (playerInfo.IsImpostor != value)
             {
-                if (await sender.Client.ReportCheatAsync(context, "Failed impostor check"))
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, "Failed impostor check"))
                 {
                     return false;
                 }
@@ -90,7 +90,7 @@ namespace Impostor.Server.Net.Inner
         {
             if (playerInfo.CanVent != value)
             {
-                if (await sender.Client.ReportCheatAsync(context, "Failed can vent check"))
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, "Failed can vent check"))
                 {
                     return false;
                 }
@@ -103,7 +103,7 @@ namespace Impostor.Server.Net.Inner
         {
             if (playerInfo.RoleType != role)
             {
-                if (await sender.Client.ReportCheatAsync(context, $"Failed role = {role} check"))
+                if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, $"Failed role = {role} check"))
                 {
                     return false;
                 }
@@ -114,7 +114,7 @@ namespace Impostor.Server.Net.Inner
 
         protected async ValueTask<bool> UnregisteredCall(CheatContext context, IClientPlayer sender)
         {
-            if (await sender.Client.ReportCheatAsync(context, "Client sent unregistered call"))
+            if (await sender.Client.ReportCheatAsync(context, CheatCategory.ProtocolExtension, "Client sent unregistered call"))
             {
                 return false;
             }
index 41b7ef577bb894799945957eb8978b4c7fb4954c..60bf67698a753febc4403dacad24036ac760da6b 100644 (file)
@@ -1,5 +1,6 @@
 using System;
 using System.Threading.Tasks;
+using Impostor.Api;
 using Impostor.Api.Events.Managers;
 using Impostor.Api.Net;
 using Impostor.Api.Net.Custom;
@@ -67,7 +68,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
 
                     if (Game.GameNet.ShipStatus == null)
                     {
-                        if (await sender.Client.ReportCheatAsync(call, "Client interacted with vent on unknown map"))
+                        if (await sender.Client.ReportCheatAsync(call, CheatCategory.ProtocolExtension, "Client interacted with vent on unknown map"))
                         {
                             return false;
                         }
@@ -77,7 +78,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
 
                     if (!Game.GameNet.ShipStatus.Data.Vents.TryGetValue(ventId, out var vent))
                     {
-                        if (await sender.Client.ReportCheatAsync(call, "Client interacted with nonexistent vent"))
+                        if (await sender.Client.ReportCheatAsync(call, CheatCategory.ProtocolExtension, "Client interacted with nonexistent vent"))
                         {
                             return false;
                         }
index 5fcf38bbbc4e2906ff6bdd5022d6a8f38dcb459a..68abcbe2567993ad5e2efd107f8ddb2dc9dec662 100644 (file)
@@ -69,7 +69,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
 
                 if (clientId != sender.Client.Id)
                 {
-                    if (await sender.Client.ReportCheatAsync(RpcCalls.AddVote, $"Client sent {nameof(RpcCalls.AddVote)} as other client"))
+                    if (await sender.Client.ReportCheatAsync(RpcCalls.AddVote, CheatCategory.Ownership, $"Client sent {nameof(RpcCalls.AddVote)} as other client"))
                     {
                         return false;
                     }
index 4ca647aeacccc08fac99a0ea92c63ae1163220cd..6b6e3794dc29beb7a229f7b7c3b7edd77ef7cf8b 100644 (file)
@@ -204,7 +204,7 @@ namespace Impostor.Server.Net.Inner.Objects
 
             if (playerId != sender.Character!.PlayerId)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CastVote, $"Client sent {nameof(RpcCalls.CastVote)} to an unowned {nameof(InnerPlayerControl)}"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CastVote, CheatCategory.Ownership, $"Client sent {nameof(RpcCalls.CastVote)} to an unowned {nameof(InnerPlayerControl)}"))
                 {
                     return false;
                 }
index 8e03663294445a333edd3d5425768abae846482f..a4ee87a1e60d6d3ea8ce5fcee6767f63b9708b24 100644 (file)
@@ -209,7 +209,7 @@ namespace Impostor.Server.Net.Inner.Objects
                     }
 
                     Rpc39SetHatStr.Deserialize(reader, out var hat);
-                    return true;
+                    return await HandleSetHat(sender, hat);
                 }
 
                 case RpcCalls.SetSkinStr:
@@ -220,7 +220,7 @@ namespace Impostor.Server.Net.Inner.Objects
                     }
 
                     Rpc40SetSkinStr.Deserialize(reader, out var skin);
-                    return true;
+                    return await HandleSetSkin(sender, skin);
                 }
 
                 case RpcCalls.SetVisorStr:
@@ -231,7 +231,7 @@ namespace Impostor.Server.Net.Inner.Objects
                     }
 
                     Rpc42SetVisorStr.Deserialize(reader, out var visor);
-                    return true;
+                    return await HandleSetVisor(sender, visor);
                 }
 
                 case RpcCalls.SetNamePlateStr:
@@ -242,7 +242,7 @@ namespace Impostor.Server.Net.Inner.Objects
                     }
 
                     Rpc43SetNamePlateStr.Deserialize(reader, out var namePlate);
-                    return true;
+                    return await HandleSetNamePlate(sender, namePlate);
                 }
 
                 case RpcCalls.SetLevel:
@@ -567,7 +567,7 @@ namespace Impostor.Server.Net.Inner.Objects
         {
             if (name.Length > 10)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, "Client sent name exceeding 10 characters"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, CheatCategory.NameLimits, "Client sent name exceeding 10 characters"))
                 {
                     return false;
                 }
@@ -575,7 +575,7 @@ namespace Impostor.Server.Net.Inner.Objects
 
             if (string.IsNullOrWhiteSpace(name) || !name.All(TextBox.IsCharAllowed))
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, "Client sent name containing illegal characters"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, CheatCategory.NameLimits, "Client sent name containing illegal characters"))
                 {
                     return false;
                 }
@@ -583,7 +583,7 @@ namespace Impostor.Server.Net.Inner.Objects
 
             if (sender.Client.Name != name)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, "Client sent name not matching his name from handshake"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, CheatCategory.GameFlow, "Client sent name not matching his name from handshake"))
                 {
                     return false;
                 }
@@ -598,7 +598,7 @@ namespace Impostor.Server.Net.Inner.Objects
         {
             if (Game.GameState == GameStates.Started)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, "Client tried to set a name midgame"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.GameFlow, "Client tried to set a name midgame"))
                 {
                     return false;
                 }
@@ -608,7 +608,7 @@ namespace Impostor.Server.Net.Inner.Objects
             {
                 if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.PlayerName == name))
                 {
-                    if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, "Client sent name that is already used"))
+                    if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.NameLimits, "Client sent name that is already used"))
                     {
                         return false;
                     }
@@ -616,7 +616,7 @@ namespace Impostor.Server.Net.Inner.Objects
 
                 if (sender.Client.Name != name)
                 {
-                    if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, "Client sent name not matching his name from handshake"))
+                    if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.GameFlow, "Client sent name not matching his name from handshake"))
                     {
                         return false;
                     }
@@ -654,7 +654,7 @@ namespace Impostor.Server.Net.Inner.Objects
                 }
                 else
                 {
-                    if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, $"Client sent {nameof(RpcCalls.SetName)} for a player that didn't request it"))
+                    if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.GameFlow, $"Client sent {nameof(RpcCalls.SetName)} for a player that didn't request it"))
                     {
                         return false;
                     }
@@ -668,9 +668,17 @@ namespace Impostor.Server.Net.Inner.Objects
 
         private async ValueTask<bool> HandleCheckColor(ClientPlayer sender, ColorType color)
         {
+            if (Game.GameState == GameStates.Started)
+            {
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckColor, CheatCategory.GameFlow, "Client tried to ask for a color midgame"))
+                {
+                    return false;
+                }
+            }
+
             if ((byte)color > ColorsCount)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckColor, "Client sent invalid color"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckColor, CheatCategory.ProtocolExtension, "Client sent invalid color"))
                 {
                     return false;
                 }
@@ -685,7 +693,7 @@ namespace Impostor.Server.Net.Inner.Objects
         {
             if (Game.GameState == GameStates.Started)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, "Client tried to set a color midgame"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.GameFlow, "Client tried to set a color midgame"))
                 {
                     return false;
                 }
@@ -695,7 +703,7 @@ namespace Impostor.Server.Net.Inner.Objects
             {
                 if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.CurrentOutfit.Color == color))
                 {
-                    if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, "Client sent a color that is already used"))
+                    if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ColorLimits, "Client sent a color that is already used"))
                     {
                         return false;
                     }
@@ -731,7 +739,8 @@ namespace Impostor.Server.Net.Inner.Objects
 
         private async ValueTask<bool> HandleSetHat(ClientPlayer sender, string hat)
         {
-            if (Game.GameState == GameStates.Started && await sender.Client.ReportCheatAsync(RpcCalls.SetHat, "Client tried to change hat while not in lobby"))
+            if (Game.GameState == GameStates.Started &&
+                await sender.Client.ReportCheatAsync(RpcCalls.SetHat, CheatCategory.GameFlow, "Client tried to change hat while not in lobby"))
             {
                 return false;
             }
@@ -743,7 +752,8 @@ namespace Impostor.Server.Net.Inner.Objects
 
         private async ValueTask<bool> HandleSetSkin(ClientPlayer sender, string skin)
         {
-            if (Game.GameState == GameStates.Started && await sender.Client.ReportCheatAsync(RpcCalls.SetSkin, "Client tried to change skin while not in lobby"))
+            if (Game.GameState == GameStates.Started &&
+                await sender.Client.ReportCheatAsync(RpcCalls.SetSkin, CheatCategory.GameFlow, "Client tried to change skin while not in lobby"))
             {
                 return false;
             }
@@ -753,6 +763,32 @@ namespace Impostor.Server.Net.Inner.Objects
             return true;
         }
 
+        private async ValueTask<bool> HandleSetVisor(ClientPlayer sender, string visor)
+        {
+            if (Game.GameState == GameStates.Started &&
+                await sender.Client.ReportCheatAsync(RpcCalls.SetVisor, CheatCategory.GameFlow, "Client tried to change visor while not in lobby"))
+            {
+                return false;
+            }
+
+            PlayerInfo.CurrentOutfit.VisorId = visor;
+
+            return true;
+        }
+
+        private async ValueTask<bool> HandleSetNamePlate(ClientPlayer sender, string skin)
+        {
+            if (Game.GameState == GameStates.Started &&
+                await sender.Client.ReportCheatAsync(RpcCalls.SetNamePlate, CheatCategory.GameFlow, "Client tried to change skin while not in lobby"))
+            {
+                return false;
+            }
+
+            PlayerInfo.CurrentOutfit.NamePlateId = skin;
+
+            return true;
+        }
+
         private async ValueTask<bool> HandleCheckMurder(ClientPlayer sender, InnerPlayerControl? target)
         {
             if (!PlayerInfo.CanMurder(Game, _dateTimeProvider))
@@ -762,7 +798,7 @@ namespace Impostor.Server.Net.Inner.Objects
                     // This request was made too quickly by spamming the kill button, cancel it if we're in server authoritive mode
                     return _game.IsHostAuthoritive;
                 }
-                else if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, "Client tried to murder too fast"))
+                else if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, "Client tried to murder too fast"))
                 {
                     return false;
                 }
@@ -770,7 +806,7 @@ namespace Impostor.Server.Net.Inner.Objects
 
             if (target == null || target.PlayerInfo.IsImpostor)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, "Client tried to murder invalid target"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, "Client tried to murder invalid target"))
                 {
                     return false;
                 }
@@ -807,7 +843,7 @@ namespace Impostor.Server.Net.Inner.Objects
         {
             if (!_game.IsHostAuthoritive)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to murder directly"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, CheatCategory.GameFlow, "Client tried to murder directly"))
                 {
                     return false;
                 }
@@ -815,7 +851,7 @@ namespace Impostor.Server.Net.Inner.Objects
 
             if (target == null || target.PlayerInfo.IsImpostor)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to murder invalid target"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, CheatCategory.GameFlow, "Client tried to murder invalid target"))
                 {
                     return false;
                 }
@@ -824,7 +860,7 @@ namespace Impostor.Server.Net.Inner.Objects
             // If the host is also the impostor that committed the murder, CheckMurder is actually sent *after* the MurderPlayer RPC
             if (sender.Character != this && target != IsMurdering)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Host tried to murder incorrect target"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, CheatCategory.GameFlow, "Host tried to murder incorrect target"))
                 {
                     return false;
                 }
@@ -859,7 +895,7 @@ namespace Impostor.Server.Net.Inner.Objects
         {
             if (target == null)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckProtect, "Client tried to protect invalid target"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckProtect, CheatCategory.Target, "Client tried to protect invalid target"))
                 {
                     return false;
                 }
@@ -867,12 +903,9 @@ namespace Impostor.Server.Net.Inner.Objects
                 return true;
             }
 
-            if (PlayerInfo.RoleType != RoleTypes.GuardianAngel)
+            if (await ValidateRole(RpcCalls.ProtectPlayer, sender, PlayerInfo, RoleTypes.GuardianAngel))
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.CheckProtect, "Client tried to protect but it wasn't a guardian angel"))
-                {
                     return false;
-                }
             }
 
             ((InnerPlayerControl)target).Protect(this);
@@ -896,7 +929,8 @@ namespace Impostor.Server.Net.Inner.Objects
 
         private async ValueTask<bool> HandleSetPet(ClientPlayer sender, string pet)
         {
-            if (Game.GameState == GameStates.Started && await sender.Client.ReportCheatAsync(RpcCalls.SetPet, "Client tried to change pet while not in lobby"))
+            if (Game.GameState == GameStates.Started &&
+                await sender.Client.ReportCheatAsync(RpcCalls.SetPet, CheatCategory.GameFlow, "Client tried to change pet while not in lobby"))
             {
                 return false;
             }
@@ -910,7 +944,7 @@ namespace Impostor.Server.Net.Inner.Objects
         {
             if (!sender.IsHost && startCounter != -1)
             {
-                if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to set start counter as a non-host"))
+                if (await sender.Client.ReportCheatAsync(RpcCalls.SetStartCounter, CheatCategory.MustBeHost, "Client tried to set start counter as a non-host"))
                 {
                     return false;
                 }
index b458eb39f0882d470ce1948242724c579abbacbd..4cfd7aed5e5fa5995bc0dae25f36a4e6584b3c85 100644 (file)
@@ -121,10 +121,10 @@ namespace Impostor.Server.Net.State
 
                     case GameDataTag.SpawnFlag:
                     {
-                        // Only the host is allowed to despawn objects.
+                        // Only the host is allowed to spawn objects.
                         if (!sender.IsHost)
                         {
-                            if (await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), "Tried to send SpawnFlag as non-host."))
+                            if (await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), CheatCategory.MustBeHost, "Tried to send SpawnFlag as non-host."))
                             {
                                 return false;
                             }
@@ -259,7 +259,7 @@ namespace Impostor.Server.Net.State
 
                         if (clientId != sender.Client.Id)
                         {
-                            if (await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.ConsoleDeclareClientPlatformFlag)), "Client sent info with wrong client id"))
+                            if (await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.ConsoleDeclareClientPlatformFlag)), CheatCategory.Ownership, "Client sent info with wrong client id"))
                             {
                                 return false;
                             }
@@ -331,7 +331,7 @@ namespace Impostor.Server.Net.State
                     }
                     else
                     {
-                        await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), "Failed to find player that spawned the InnerPlayerControl");
+                        await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), CheatCategory.GameFlow, "Failed to find player that spawned the InnerPlayerControl");
                     }
 
                     // Hook up InnerPlayerControl <-> InnerPlayerControl.PlayerInfo.