]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add role prototype validation tests (#32801)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Mon, 14 Oct 2024 03:05:25 +0000 (16:05 +1300)
committerGitHub <noreply@github.com>
Mon, 14 Oct 2024 03:05:25 +0000 (14:05 +1100)
* Add role prototype validation test

* Rejig GetPrototypesWithComponent

* More tests n stuff

Content.IntegrationTests/Pair/TestPair.Helpers.cs
Content.IntegrationTests/Tests/Minds/RoleTests.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs
Content.IntegrationTests/Tests/StorageTest.cs
Content.Server/EntityEffects/EffectConditions/JobCondition.cs
Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
Content.Server/Ghost/Roles/GhostRoleSystem.cs
Content.Shared/Roles/Jobs/SharedJobSystem.cs
Content.Shared/Roles/SharedRoleSystem.cs

index 4604cd82966689400b15cdb0ccc5c62c37040812..1b4825cc9c7ea32ff265ee2869d4bbe01380c659 100644 (file)
@@ -107,13 +107,41 @@ public sealed partial class TestPair
     /// <summary>
     /// Retrieve all entity prototypes that have some component.
     /// </summary>
-    public List<EntityPrototype> GetPrototypesWithComponent<T>(
+    public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
         HashSet<string>? ignored = null,
         bool ignoreAbstract = true,
         bool ignoreTestPrototypes = true)
         where T : IComponent
     {
         var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(T));
+        var list = new List<(EntityPrototype, T)>();
+        foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
+        {
+            if (ignored != null && ignored.Contains(proto.ID))
+                continue;
+
+            if (ignoreAbstract && proto.Abstract)
+                continue;
+
+            if (ignoreTestPrototypes && IsTestPrototype(proto))
+                continue;
+
+            if (proto.Components.TryGetComponent(id, out var cmp))
+                list.Add((proto, (T)cmp));
+        }
+
+        return list;
+    }
+
+    /// <summary>
+    /// Retrieve all entity prototypes that have some component.
+    /// </summary>
+    public List<EntityPrototype> GetPrototypesWithComponent(Type type,
+        HashSet<string>? ignored = null,
+        bool ignoreAbstract = true,
+        bool ignoreTestPrototypes = true)
+    {
+        var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
         var list = new List<EntityPrototype>();
         foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
         {
@@ -127,7 +155,7 @@ public sealed partial class TestPair
                 continue;
 
             if (proto.Components.ContainsKey(id))
-                list.Add(proto);
+                list.Add((proto));
         }
 
         return list;
diff --git a/Content.IntegrationTests/Tests/Minds/RoleTests.cs b/Content.IntegrationTests/Tests/Minds/RoleTests.cs
new file mode 100644 (file)
index 0000000..fcfe123
--- /dev/null
@@ -0,0 +1,95 @@
+using System.Linq;
+using Content.Server.Roles;
+using Content.Shared.Roles;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Reflection;
+
+namespace Content.IntegrationTests.Tests.Minds;
+
+[TestFixture]
+public sealed class RoleTests
+{
+    /// <summary>
+    /// Check that any prototype with a <see cref="MindRoleComponent"/> is properly configured
+    /// </summary>
+    [Test]
+    public async Task ValidateRolePrototypes()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+
+        var jobComp = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(JobRoleComponent));
+
+        Assert.Multiple(() =>
+        {
+            foreach (var (proto, comp) in pair.GetPrototypesWithComponent<MindRoleComponent>())
+            {
+                Assert.That(comp.AntagPrototype == null || comp.JobPrototype == null, $"Role {proto.ID} has both a job and antag prototype.");
+                Assert.That(!comp.ExclusiveAntag || comp.Antag, $"Role {proto.ID} is marked as an exclusive antag, despite not being an antag.");
+                Assert.That(comp.Antag || comp.AntagPrototype == null, $"Role {proto.ID} has an antag prototype, despite not being an antag.");
+
+                if (comp.JobPrototype != null)
+                    Assert.That(proto.Components.ContainsKey(jobComp), $"Role {proto.ID} is a job, despite not having a job prototype.");
+
+                // It is possible that this is meant to be supported? Though I would assume that it would be for
+                // admin / prototype uploads, and that pre-defined roles should still check this.
+                Assert.That(!comp.Antag || comp.AntagPrototype != null , $"Role {proto.ID} is an antag, despite not having a antag prototype.");
+            }
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    /// <summary>
+    /// Check that any prototype with a <see cref="JobRoleComponent"/> also has a properly configured
+    /// <see cref="MindRoleComponent"/>
+    /// </summary>
+    [Test]
+    public async Task ValidateJobPrototypes()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+
+        var mindCompId = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(MindRoleComponent));
+
+        Assert.Multiple(() =>
+        {
+            foreach (var (proto, comp) in pair.GetPrototypesWithComponent<JobRoleComponent>())
+            {
+                if (proto.Components.TryGetComponent(mindCompId, out var mindComp))
+                    Assert.That(((MindRoleComponent)mindComp).JobPrototype, Is.Not.Null);
+            }
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    /// <summary>
+    /// Check that any prototype with a component that inherits from <see cref="BaseMindRoleComponent"/> also has a
+    /// <see cref="MindRoleComponent"/>
+    /// </summary>
+    [Test]
+    public async Task ValidateRolesHaveMindRoleComp()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+
+        var refMan = pair.Server.ResolveDependency<IReflectionManager>();
+        var mindCompId = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(MindRoleComponent));
+
+        var compTypes = refMan.GetAllChildren(typeof(BaseMindRoleComponent))
+            .Append(typeof(RoleBriefingComponent))
+            .Where(x => !x.IsAbstract);
+
+        Assert.Multiple(() =>
+        {
+            foreach (var comp in compTypes)
+            {
+                foreach (var proto in pair.GetPrototypesWithComponent(comp))
+                {
+                    Assert.That(proto.Components.ContainsKey(mindCompId), $"Role {proto.ID} does not have a {nameof(MindRoleComponent)} despite having a {comp.Name}");
+                }
+            }
+        });
+
+        await pair.CleanReturnAsync();
+    }
+}
index bf75188f02962e79f2461a1438f1b00bc10f005e..da7e1e8e9b028bbdd92e5736245568f56f93a54b 100644 (file)
@@ -40,7 +40,7 @@ public sealed class PrototypeSaveTest
 
         await pair.Client.WaitPost(() =>
         {
-            foreach (var proto in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
+            foreach (var (proto, _) in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
             {
                 var dummy = pair.Client.EntMan.Spawn(proto.ID);
                 pair.Client.EntMan.RunMapInit(dummy, pair.Client.MetaData(dummy));
index 2d28534347d248130ec597db03bc7055723ded2d..983ec7093621b62368907783330f05566699cdae 100644 (file)
@@ -94,14 +94,13 @@ namespace Content.IntegrationTests.Tests
 
             await Assert.MultipleAsync(async () =>
             {
-                foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
+                foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
                 {
                     if (proto.HasComponent<EntityStorageComponent>(compFact))
                         continue;
 
                     StorageComponent? storage = null;
                     ItemComponent? item = null;
-                    StorageFillComponent fill = default!;
                     var size = 0;
                     await server.WaitAssertion(() =>
                     {
@@ -112,7 +111,6 @@ namespace Content.IntegrationTests.Tests
                         }
 
                         proto.TryGetComponent("Item", out item);
-                        fill = (StorageFillComponent) proto.Components[id].Component;
                         size = GetFillSize(fill, false, protoMan, itemSys);
                     });
 
@@ -179,7 +177,7 @@ namespace Content.IntegrationTests.Tests
 
             var itemSys = entMan.System<SharedItemSystem>();
 
-            foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
+            foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
             {
                 if (proto.HasComponent<StorageComponent>(compFact))
                     continue;
@@ -192,7 +190,6 @@ namespace Content.IntegrationTests.Tests
                     if (entStorage == null)
                         return;
 
-                    var fill = (StorageFillComponent) proto.Components[id].Component;
                     var size = GetFillSize(fill, true, protoMan, itemSys);
                     Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
                         $"{proto.ID} storage fill is too large.");
index 9c7bda839e421e483373d4a7ea8ddf4d97385833..9621d6945f6147e6635627e9dfb37664603938ba 100644 (file)
@@ -26,9 +26,17 @@ public sealed partial class JobCondition : EntityEffectCondition
             if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
                 continue;
 
-            if(!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole)
-               || mindRole.JobPrototype is null)
+            if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))
+            {
+                Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}");
                 continue;
+            }
+
+            if (mindRole.JobPrototype == null)
+            {
+                Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}");
+                continue;
+            }
 
             if (Job.Contains(mindRole.JobPrototype.Value))
                 return true;
index 939ab87115372e51f3ee5d98d7513e370e33286e..a313b78eaf13d7de66154713e8c98a3fd8ee32c7 100644 (file)
@@ -155,8 +155,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
 
             if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
             {
-                if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out _, out var role))
-                    role.Value.Comp.ConvertedCount++;
+                if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
+                    role.Value.Comp2.ConvertedCount++;
             }
         }
 
index bb95b827a7f1161cd6f802cad98326067da1f0ff..cd01c964ef6cba244de2c59f3f994d696fbd270f 100644 (file)
@@ -516,8 +516,8 @@ public sealed class GhostRoleSystem : EntitySystem
 
         _roleSystem.MindAddRole(newMind, "MindRoleGhostMarker");
 
-        if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind, out _, out var markerRole))
-            markerRole.Value.Comp.Name = role.RoleName;
+        if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
+            markerRole.Value.Comp2.Name = role.RoleName;
 
         _mindSystem.SetUserId(newMind, player.UserId);
         _mindSystem.TransferTo(newMind, mob);
index 94447a5af46c1cfd73e3166fdc444562acaaea4b..8a4733c83401fd05c5903ba8bcc0ef80c6a0e2ad 100644 (file)
@@ -103,7 +103,6 @@ public abstract class SharedJobSystem : EntitySystem
     public bool MindHasJobWithId(EntityUid? mindId, string prototypeId)
     {
 
-        MindRoleComponent? comp = null;
         if (mindId is null)
             return false;
 
@@ -112,9 +111,7 @@ public abstract class SharedJobSystem : EntitySystem
         if (role is null)
             return false;
 
-        comp = role.Value.Comp;
-
-        return (comp.JobPrototype == prototypeId);
+        return role.Value.Comp1.JobPrototype == prototypeId;
     }
 
     public bool MindTryGetJob(
@@ -124,7 +121,7 @@ public abstract class SharedJobSystem : EntitySystem
         prototype = null;
         MindTryGetJobId(mindId, out var protoId);
 
-        return (_prototypes.TryIndex<JobPrototype>(protoId, out prototype) || prototype is not null);
+        return _prototypes.TryIndex(protoId, out prototype) || prototype is not null;
     }
 
     public bool MindTryGetJobId(
@@ -137,9 +134,9 @@ public abstract class SharedJobSystem : EntitySystem
             return false;
 
         if (_roles.MindHasRole<JobRoleComponent>(mindId.Value, out var role))
-            job = role.Value.Comp.JobPrototype;
+            job = role.Value.Comp1.JobPrototype;
 
-        return (job is not null);
+        return job is not null;
     }
 
     /// <summary>
index 925f61e7c75dd2d8f2c367273ba5915dd0d77074..00271693abe29a17e2863002e7ef6e10f509946c 100644 (file)
@@ -10,6 +10,7 @@ using Robust.Shared.Audio.Systems;
 using Robust.Shared.Configuration;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Roles;
 
@@ -92,19 +93,18 @@ public abstract class SharedRoleSystem : EntitySystem
         bool silent = false,
         string? jobPrototype = null)
     {
+        if (!Resolve(mindId, ref mind))
+            return;
+
         // Can't have someone get paid for two jobs now, can we
-        if (MindHasRole<JobRoleComponent>(mindId, out var jobRole)
-            && jobRole.Value.Comp.JobPrototype != jobPrototype)
+        if (MindHasRole<JobRoleComponent>((mindId, mind), out var jobRole)
+            && jobRole.Value.Comp1.JobPrototype != jobPrototype)
         {
-            Resolve(mindId, ref mind);
-            if (mind is not null)
-            {
-                _adminLogger.Add(LogType.Mind,
-                    LogImpact.Low,
-                    $"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp.JobPrototype}' to '{jobPrototype}'");
-            }
+            _adminLogger.Add(LogType.Mind,
+                LogImpact.Low,
+                $"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp1.JobPrototype}' to '{jobPrototype}'");
 
-            jobRole.Value.Comp.JobPrototype = jobPrototype;
+            jobRole.Value.Comp1.JobPrototype = jobPrototype;
         }
         else
             MindAddRoleDo(mindId, "MindRoleJob", mind, silent, jobPrototype);
@@ -146,11 +146,12 @@ public abstract class SharedRoleSystem : EntitySystem
         {
             mindRoleComp.JobPrototype = jobPrototype;
             EnsureComp<JobRoleComponent>(mindRoleId);
+            DebugTools.AssertNull(mindRoleComp.AntagPrototype);
+            DebugTools.Assert(!mindRoleComp.Antag);
+            DebugTools.Assert(!mindRoleComp.ExclusiveAntag);
         }
 
-        if (mindRoleComp.Antag || mindRoleComp.ExclusiveAntag)
-            antagonist = true;
-
+        antagonist |= mindRoleComp.Antag;
         mind.MindRoles.Add(mindRoleId);
 
         var mindEv = new MindRoleAddedEvent(silent);
@@ -182,51 +183,55 @@ public abstract class SharedRoleSystem : EntitySystem
     /// <summary>
     ///     Removes all instances of a specific role from this mind.
     /// </summary>
-    /// <param name="mindId">The mind to remove the role from.</param>
+    /// <param name="mind">The mind to remove the role from.</param>
     /// <typeparam name="T">The type of the role to remove.</typeparam>
-    /// <exception cref="ArgumentException">Thrown if the mind does not exist or does not have this role.</exception>
-    /// <returns>Returns False if there was something wrong with the mind or the removal. True if successful</returns>>
-    public bool MindRemoveRole<T>(EntityUid mindId) where T : IComponent
+    /// <returns>Returns false if the role did not exist. True if successful</returns>>
+    public bool MindRemoveRole<T>(Entity<MindComponent?> mind) where T : IComponent
     {
-        if (!TryComp<MindComponent>(mindId, out var mind) )
-            throw new ArgumentException($"{mindId} does not exist or does not have mind component");
+        if (typeof(T) == typeof(MindRoleComponent))
+            throw new InvalidOperationException();
+
+        if (!Resolve(mind.Owner, ref mind.Comp))
+            return false;
 
         var found = false;
         var antagonist = false;
         var delete = new List<EntityUid>();
-        foreach (var role in mind.MindRoles)
+        foreach (var role in mind.Comp.MindRoles)
         {
             if (!HasComp<T>(role))
                 continue;
 
-            var roleComp = Comp<MindRoleComponent>(role);
-            antagonist = roleComp.Antag;
-            _entityManager.DeleteEntity(role);
+            if (!TryComp(role, out MindRoleComponent? roleComp))
+            {
+                Log.Error($"Encountered mind role entity {ToPrettyString(role)} without a {nameof(MindRoleComponent)}");
+                continue;
+            }
 
+            antagonist |= roleComp.Antag | roleComp.ExclusiveAntag;
+            _entityManager.DeleteEntity(role);
             delete.Add(role);
             found = true;
-
         }
 
+        if (!found)
+            return false;
+
         foreach (var role in delete)
         {
-            mind.MindRoles.Remove(role);
+            mind.Comp.MindRoles.Remove(role);
         }
 
-        if (!found)
+        if (mind.Comp.OwnedEntity != null)
         {
-            throw new ArgumentException($"{mindId} does not have this role: {typeof(T)}");
+            var message = new RoleRemovedEvent(mind.Owner, mind.Comp, antagonist);
+            RaiseLocalEvent(mind.Comp.OwnedEntity.Value, message, true);
         }
 
-        var message = new RoleRemovedEvent(mindId, mind, antagonist);
-
-        if (mind.OwnedEntity != null)
-        {
-            RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
-        }
         _adminLogger.Add(LogType.Mind,
             LogImpact.Low,
-            $"'Role {typeof(T).Name}' removed from mind of {ToPrettyString(mind.OwnedEntity)}");
+            $"All roles of type '{typeof(T).Name}' removed from mind of {ToPrettyString(mind.Comp.OwnedEntity)}");
+
         return true;
     }
 
@@ -238,16 +243,14 @@ public abstract class SharedRoleSystem : EntitySystem
     /// <returns>True if the role existed and was removed</returns>
     public bool MindTryRemoveRole<T>(EntityUid mindId) where T : IComponent
     {
-        if (!MindHasRole<T>(mindId))
-        {
-            Log.Warning($"Failed to remove role {typeof(T)} from {mindId} : mind does not have role ");
-            return false;
-        }
-
         if (typeof(T) == typeof(MindRoleComponent))
             return false;
 
-        return MindRemoveRole<T>(mindId);
+        if (MindRemoveRole<T>(mindId))
+            return true;
+
+        Log.Warning($"Failed to remove role {typeof(T)} from {ToPrettyString(mindId)} : mind does not have role ");
+        return false;
     }
 
     /// <summary>
@@ -259,30 +262,29 @@ public abstract class SharedRoleSystem : EntitySystem
     /// <param name="role">The Mind Role entity component</param>
     /// <param name="roleT">The Mind Role's entity component for T</param>
     /// <returns>True if the role is found</returns>
-    public bool MindHasRole<T>(EntityUid mindId,
-        [NotNullWhen(true)] out Entity<MindRoleComponent>? role,
-        [NotNullWhen(true)] out Entity<T>? roleT) where T : IComponent
+    public bool MindHasRole<T>(Entity<MindComponent?> mind,
+        [NotNullWhen(true)] out Entity<MindRoleComponent, T>? role) where T : IComponent
     {
         role = null;
-        roleT = null;
-
-        if (!TryComp<MindComponent>(mindId, out var mind))
+        if (!Resolve(mind.Owner, ref mind.Comp))
             return false;
 
-        var found = false;
-
-        foreach (var roleEnt in mind.MindRoles)
+        foreach (var roleEnt in mind.Comp.MindRoles)
         {
-            if (!HasComp<T>(roleEnt))
+            if (!TryComp(roleEnt, out T? tcomp))
                 continue;
 
-            role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
-            roleT = (roleEnt,Comp<T>(roleEnt));
-            found = true;
-            break;
+            if (!TryComp(roleEnt, out MindRoleComponent? roleComp))
+            {
+                Log.Error($"Encountered mind role entity {ToPrettyString(roleEnt)} without a {nameof(MindRoleComponent)}");
+                continue;
+            }
+
+            role = (roleEnt, roleComp, tcomp);
+            return true;
         }
 
-        return found;
+        return false;
     }
 
     /// <summary>
@@ -317,7 +319,13 @@ public abstract class SharedRoleSystem : EntitySystem
             if (!HasComp(roleEnt, type))
                 continue;
 
-            role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
+            if (!TryComp(roleEnt, out MindRoleComponent? roleComp))
+            {
+                Log.Error($"Encountered mind role entity {ToPrettyString(roleEnt)} without a {nameof(MindRoleComponent)}");
+                continue;
+            }
+
+            role = (roleEnt, roleComp);
             found = true;
             break;
         }
@@ -325,20 +333,6 @@ public abstract class SharedRoleSystem : EntitySystem
         return found;
     }
 
-    /// <summary>
-    /// Finds the first mind role of a specific type on a mind entity.
-    /// Outputs an entity component for the mind role's MindRoleComponent
-    /// </summary>
-    /// <param name="mindId">The mind entity</param>
-    /// <param name="role">The Mind Role entity component</param>
-    /// <typeparam name="T">The type of the role to find.</typeparam>
-    /// <returns>True if the role is found</returns>
-    public bool MindHasRole<T>(EntityUid mindId,
-        [NotNullWhen(true)] out Entity<MindRoleComponent>? role) where T : IComponent
-    {
-        return MindHasRole<T>(mindId, out role, out _);
-    }
-
     /// <summary>
     /// Finds the first mind role of a specific type on a mind entity.
     /// </summary>
@@ -347,7 +341,7 @@ public abstract class SharedRoleSystem : EntitySystem
     /// <returns>True if the role is found</returns>
     public bool MindHasRole<T>(EntityUid mindId) where T : IComponent
     {
-        return MindHasRole<T>(mindId, out _, out _);
+        return MindHasRole<T>(mindId, out _);
     }
 
     //TODO: Delete this later
@@ -374,28 +368,31 @@ public abstract class SharedRoleSystem : EntitySystem
     /// <summary>
     /// Reads all Roles of a mind Entity and returns their data as RoleInfo
     /// </summary>
-    /// <param name="mindId">The mind entity</param>
+    /// <param name="mind">The mind entity</param>
     /// <returns>RoleInfo list</returns>
-    public List<RoleInfo> MindGetAllRoleInfo(EntityUid mindId)
+    public List<RoleInfo> MindGetAllRoleInfo(Entity<MindComponent?> mind)
     {
         var roleInfo = new List<RoleInfo>();
 
-        if (!TryComp<MindComponent>(mindId, out var mind))
+        if (!Resolve(mind.Owner, ref mind.Comp))
             return roleInfo;
 
-        foreach (var role in mind.MindRoles)
+        foreach (var role in mind.Comp.MindRoles)
         {
             var valid = false;
             var name = "game-ticker-unknown-role";
             var prototype = "";
-           string? playTimeTracker = null;
+            string? playTimeTracker = null;
 
-            var comp = Comp<MindRoleComponent>(role);
-            if (comp.AntagPrototype is not null)
+            if (!TryComp(role, out MindRoleComponent? comp))
             {
-                prototype = comp.AntagPrototype;
+                Log.Error($"Encountered mind role entity {ToPrettyString(role)} without a {nameof(MindRoleComponent)}");
+                continue;
             }
 
+            if (comp.AntagPrototype is not null)
+                prototype = comp.AntagPrototype;
+
             if (comp.JobPrototype is not null && comp.AntagPrototype is null)
             {
                 prototype = comp.JobPrototype;
@@ -429,7 +426,7 @@ public abstract class SharedRoleSystem : EntitySystem
             }
 
             if (valid)
-                roleInfo.Add(new RoleInfo(name, comp.Antag || comp.ExclusiveAntag , playTimeTracker, prototype));
+                roleInfo.Add(new RoleInfo(name, comp.Antag, playTimeTracker, prototype));
         }
         return roleInfo;
     }
@@ -442,12 +439,9 @@ public abstract class SharedRoleSystem : EntitySystem
     public bool MindIsAntagonist(EntityUid? mindId)
     {
         if (mindId is null)
-        {
-            Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
             return false;
-        }
 
-        return CheckAntagonistStatus(mindId.Value).Item1;
+        return CheckAntagonistStatus(mindId.Value).Antag;
     }
 
     /// <summary>
@@ -458,37 +452,28 @@ public abstract class SharedRoleSystem : EntitySystem
     public bool MindIsExclusiveAntagonist(EntityUid? mindId)
     {
         if (mindId is null)
-        {
-            Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
             return false;
-        }
 
-        return CheckAntagonistStatus(mindId.Value).Item2;
+        return CheckAntagonistStatus(mindId.Value).ExclusiveAntag;
     }
 
-   private (bool, bool) CheckAntagonistStatus(EntityUid mindId)
+   public (bool Antag, bool ExclusiveAntag) CheckAntagonistStatus(Entity<MindComponent?> mind)
    {
-       if (!TryComp<MindComponent>(mindId, out var mind))
-       {
-           Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind component not found");
+       if (!Resolve(mind.Owner, ref mind.Comp))
            return (false, false);
-       }
 
         var antagonist = false;
         var exclusiveAntag = false;
-        foreach (var role in mind.MindRoles)
+        foreach (var role in mind.Comp.MindRoles)
         {
             if (!TryComp<MindRoleComponent>(role, out var roleComp))
             {
-                //If this ever shows up outside of an integration test, then we need to look into this further.
-                Log.Warning($"Mind Role Entity {role} does not have MindRoleComponent!");
+                Log.Error($"Mind Role Entity {ToPrettyString(role)} does not have a MindRoleComponent, despite being listed as a role belonging to {ToPrettyString(mind)}|");
                 continue;
             }
 
-            if (roleComp.Antag || exclusiveAntag)
-                antagonist = true;
-            if (roleComp.ExclusiveAntag)
-                exclusiveAntag = true;
+            antagonist |= roleComp.Antag;
+            exclusiveAntag |= roleComp.ExclusiveAntag;
         }
 
         return (antagonist, exclusiveAntag);
@@ -504,6 +489,9 @@ public abstract class SharedRoleSystem : EntitySystem
             _audio.PlayGlobal(sound, mind.Session);
     }
 
+    // TODO ROLES Change to readonly.
+    // Passing around a reference to a prototype's hashset makes me uncomfortable because it might be accidentally
+    // mutated.
     public HashSet<JobRequirement>? GetJobRequirement(JobPrototype job)
     {
         if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req))
@@ -512,6 +500,7 @@ public abstract class SharedRoleSystem : EntitySystem
         return job.Requirements;
     }
 
+    // TODO ROLES Change to readonly.
     public HashSet<JobRequirement>? GetJobRequirement(ProtoId<JobPrototype> job)
     {
         if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job, out var req))
@@ -520,6 +509,7 @@ public abstract class SharedRoleSystem : EntitySystem
         return _prototypes.Index(job).Requirements;
     }
 
+    // TODO ROLES Change to readonly.
     public HashSet<JobRequirement>? GetAntagRequirement(ProtoId<AntagPrototype> antag)
     {
         if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag, out var req))
@@ -528,6 +518,7 @@ public abstract class SharedRoleSystem : EntitySystem
         return _prototypes.Index(antag).Requirements;
     }
 
+    // TODO ROLES Change to readonly.
     public HashSet<JobRequirement>? GetAntagRequirement(AntagPrototype antag)
     {
         if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req))