]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
make objectives use yml defined mind filters (#36030)
authordeltanedas <39013340+deltanedas@users.noreply.github.com>
Fri, 8 Aug 2025 15:58:46 +0000 (16:58 +0100)
committerGitHub <noreply@github.com>
Fri, 8 Aug 2025 15:58:46 +0000 (17:58 +0200)
* add MindHasRole whitelist overload

* add mind filters framework

* add different mind filters and pools

* update traitor stuff to use mind filters

* line

* don't duplicate kill objectives

* g

* gs

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
17 files changed:
Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs [new file with mode: 0644]
Content.Server/Objectives/Components/PickRandomHeadComponent.cs [deleted file]
Content.Server/Objectives/Components/PickRandomPersonComponent.cs
Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs [deleted file]
Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs [deleted file]
Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs
Content.Shared/Mind/Filters/AliveHumansPool.cs [new file with mode: 0644]
Content.Shared/Mind/Filters/AntagonistMindFilter.cs [new file with mode: 0644]
Content.Shared/Mind/Filters/BodyMindFilter.cs [new file with mode: 0644]
Content.Shared/Mind/Filters/HasRoleMindFilter.cs [new file with mode: 0644]
Content.Shared/Mind/Filters/IMindPool.cs [new file with mode: 0644]
Content.Shared/Mind/Filters/JobMindFilter.cs [new file with mode: 0644]
Content.Shared/Mind/Filters/MindFilter.cs [new file with mode: 0644]
Content.Shared/Mind/Filters/ObjectiveMindFilter.cs [new file with mode: 0644]
Content.Shared/Mind/SharedMindSystem.cs
Content.Shared/Roles/SharedRoleSystem.cs
Resources/Prototypes/Objectives/traitor.yml

diff --git a/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs b/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs
new file mode 100644 (file)
index 0000000..dae8079
--- /dev/null
@@ -0,0 +1,43 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Filters;
+using Content.Shared.Whitelist;
+
+namespace Content.Server.Mind.Filters;
+
+/// <summary>
+/// A mind filter that removes minds if you have an objective targeting them matching a blacklist.
+/// </summary>
+/// <remarks>
+/// Used to prevent assigning multiple kill objectives for the same person.
+/// </remarks>
+public sealed partial class TargetObjectiveMindFilter : MindFilter
+{
+    /// <summary>
+    /// A blacklist to check objectives against, for removing a mind.
+    /// If null then any objective targeting it will remove minds.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Blacklist;
+
+    protected override bool ShouldRemove(Entity<MindComponent> mind, EntityUid? excluded, IEntityManager entMan, SharedMindSystem mindSys)
+    {
+        // ignore this filter if there is no user to check
+        if (!entMan.TryGetComponent<MindComponent>(excluded, out var excludedMind))
+            return false;
+
+        var whitelistSys = entMan.System<EntityWhitelistSystem>();
+        foreach (var objective in excludedMind.Objectives)
+        {
+            // if the player has an objective targeting this mind
+            if (entMan.TryGetComponent<TargetObjectiveComponent>(objective, out var kill) && kill.Target == mind.Owner)
+            {
+                // remove the mind if this objective is blacklisted
+                if (whitelistSys.IsBlacklistPassOrNull(Blacklist, objective))
+                    return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/Content.Server/Objectives/Components/PickRandomHeadComponent.cs b/Content.Server/Objectives/Components/PickRandomHeadComponent.cs
deleted file mode 100644 (file)
index 38ed252..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Content.Server.Objectives.Components;
-
-/// <summary>
-/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
-/// If there are no heads it will fallback to any person.
-/// </summary>
-[RegisterComponent]
-public sealed partial class PickRandomHeadComponent : Component;
index bf4135e2a9b34820ef0915502fa6f2d8e2f65e82..2c864a80d4df482ae30b9cfb3e73f45f1ed118a7 100644 (file)
@@ -1,7 +1,26 @@
+using Content.Server.Objectives.Systems;
+using Content.Shared.Mind.Filters;
+
 namespace Content.Server.Objectives.Components;
 
 /// <summary>
-/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
+/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person from a pool and filters.
 /// </summary>
-[RegisterComponent]
-public sealed partial class PickRandomPersonComponent : Component;
+/// <remarks>
+/// Don't copy paste this for a new objective, if you need a new filter just make a new filter and set it in YAML.
+/// </remarks>
+[RegisterComponent, Access(typeof(PickObjectiveTargetSystem))]
+public sealed partial class PickRandomPersonComponent : Component
+{
+    /// <summary>
+    /// A pool to pick potential targets from.
+    /// </summary>
+    [DataField]
+    public IMindPool Pool = new AliveHumansPool();
+
+    /// <summary>
+    /// Filters to apply to <see cref="Pool"/>.
+    /// </summary>
+    [DataField]
+    public List<MindFilter> Filters = new();
+}
diff --git a/Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs b/Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs
deleted file mode 100644 (file)
index 1c45cb4..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Objectives.Components;
-
-/// <summary>
-/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
-/// </summary>
-[RegisterComponent]
-public sealed partial class RandomTraitorAliveComponent : Component;
diff --git a/Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs b/Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs
deleted file mode 100644 (file)
index f2da977..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Objectives.Components;
-
-/// <summary>
-/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
-/// </summary>
-[RegisterComponent]
-public sealed partial class RandomTraitorProgressComponent : Component;
index 2977e10569d3119042b3d00b41829a88b857b0fa..0fa20c27e582e82228a848edf114fdcd13ae6195 100644 (file)
@@ -25,10 +25,6 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
 
         SubscribeLocalEvent<PickSpecificPersonComponent, ObjectiveAssignedEvent>(OnSpecificPersonAssigned);
         SubscribeLocalEvent<PickRandomPersonComponent, ObjectiveAssignedEvent>(OnRandomPersonAssigned);
-        SubscribeLocalEvent<PickRandomHeadComponent, ObjectiveAssignedEvent>(OnRandomHeadAssigned);
-
-        SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnRandomTraitorProgressAssigned);
-        SubscribeLocalEvent<RandomTraitorAliveComponent, ObjectiveAssignedEvent>(OnRandomTraitorAliveAssigned);
     }
 
     private void OnSpecificPersonAssigned(Entity<PickSpecificPersonComponent> ent, ref ObjectiveAssignedEvent args)
@@ -63,41 +59,7 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
     private void OnRandomPersonAssigned(Entity<PickRandomPersonComponent> ent, ref ObjectiveAssignedEvent args)
     {
         // invalid objective prototype
-        if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
-        {
-            args.Cancelled = true;
-            return;
-        }
-
-        // target already assigned
-        if (target.Target != null)
-            return;
-
-        var allHumans = _mind.GetAliveHumans(args.MindId);
-
-        // Can't have multiple objectives to kill the same person
-        foreach (var objective in args.Mind.Objectives)
-        {
-            if (HasComp<KillPersonConditionComponent>(objective) && TryComp<TargetObjectiveComponent>(objective, out var kill))
-            {
-                allHumans.RemoveWhere(x => x.Owner == kill.Target);
-            }
-        }
-
-        // no other humans to kill
-        if (allHumans.Count == 0)
-        {
-            args.Cancelled = true;
-            return;
-        }
-
-        _target.SetTarget(ent.Owner, _random.Pick(allHumans), target);
-    }
-
-    private void OnRandomHeadAssigned(Entity<PickRandomHeadComponent> ent, ref ObjectiveAssignedEvent args)
-    {
-        // invalid prototype
-        if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
+        if (!TryComp<TargetObjectiveComponent>(ent, out var target))
         {
             args.Cancelled = true;
             return;
@@ -107,106 +69,13 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
         if (target.Target != null)
             return;
 
-        // no other humans to kill
-        var allHumans = _mind.GetAliveHumans(args.MindId);
-        if (allHumans.Count == 0)
-        {
-            args.Cancelled = true;
-            return;
-        }
-
-        var allHeads = new HashSet<Entity<MindComponent>>();
-        foreach (var person in allHumans)
-        {
-            if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } owned && HasComp<CommandStaffComponent>(owned))
-                allHeads.Add(person);
-        }
-
-        if (allHeads.Count == 0)
-            allHeads = allHumans; // fallback to non-head target
-
-        _target.SetTarget(ent.Owner, _random.Pick(allHeads), target);
-    }
-
-    private void OnRandomTraitorProgressAssigned(Entity<RandomTraitorProgressComponent> ent, ref ObjectiveAssignedEvent args)
-    {
-        // invalid prototype
-        if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
-        {
-            args.Cancelled = true;
-            return;
-        }
-
-        var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
-
-        // cant help anyone who is tasked with helping:
-        // 1. thats boring
-        // 2. no cyclic progress dependencies!!!
-        foreach (var traitor in traitors)
-        {
-            // TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
-            if (!TryComp<MindComponent>(traitor.Id, out var mind))
-                continue;
-
-            foreach (var objective in mind.Objectives)
-            {
-                if (HasComp<HelpProgressConditionComponent>(objective))
-                    traitors.RemoveWhere(x => x.Mind == mind);
-            }
-        }
-
-        // Can't have multiple objectives to help/save the same person
-        foreach (var objective in args.Mind.Objectives)
-        {
-            if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
-            {
-                if (TryComp<TargetObjectiveComponent>(objective, out var help))
-                {
-                    traitors.RemoveWhere(x => x.Id == help.Target);
-                }
-            }
-        }
-
-        // no more helpable traitors
-        if (traitors.Count == 0)
-        {
-            args.Cancelled = true;
-            return;
-        }
-
-        _target.SetTarget(ent.Owner, _random.Pick(traitors).Id, target);
-    }
-
-    private void OnRandomTraitorAliveAssigned(Entity<RandomTraitorAliveComponent> ent, ref ObjectiveAssignedEvent args)
-    {
-        // invalid prototype
-        if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
-        {
-            args.Cancelled = true;
-            return;
-        }
-
-        var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
-
-        // Can't have multiple objectives to help/save the same person
-        foreach (var objective in args.Mind.Objectives)
-        {
-            if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
-            {
-                if (TryComp<TargetObjectiveComponent>(objective, out var help))
-                {
-                    traitors.RemoveWhere(x => x.Id == help.Target);
-                }
-            }
-        }
-
-        // You are the first/only traitor.
-        if (traitors.Count == 0)
+        // couldn't find a target :(
+        if (_mind.PickFromPool(ent.Comp.Pool, ent.Comp.Filters, args.MindId) is not {} picked)
         {
             args.Cancelled = true;
             return;
         }
 
-        _target.SetTarget(ent.Owner, _random.Pick(traitors).Id, target);
+        _target.SetTarget(ent, picked, target);
     }
 }
diff --git a/Content.Shared/Mind/Filters/AliveHumansPool.cs b/Content.Shared/Mind/Filters/AliveHumansPool.cs
new file mode 100644 (file)
index 0000000..c8e5c55
--- /dev/null
@@ -0,0 +1,12 @@
+namespace Content.Shared.Mind.Filters;
+
+/// <summary>
+/// A mind pool that uses <see cref="SharedMindSystem.AddAliveHumans"/>.
+/// </summary>
+public sealed partial class AliveHumansPool : IMindPool
+{
+    void IMindPool.FindMinds(HashSet<Entity<MindComponent>> minds, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys)
+    {
+        mindSys.AddAliveHumans(minds, exclude);
+    }
+}
diff --git a/Content.Shared/Mind/Filters/AntagonistMindFilter.cs b/Content.Shared/Mind/Filters/AntagonistMindFilter.cs
new file mode 100644 (file)
index 0000000..d805138
--- /dev/null
@@ -0,0 +1,15 @@
+using Content.Shared.Roles;
+
+namespace Content.Shared.Mind.Filters;
+
+/// <summary>
+/// A mind filter that requires minds to have an antagonist role.
+/// </summary>
+public sealed partial class AntagonistMindFilter : MindFilter
+{
+    protected override bool ShouldRemove(Entity<MindComponent> mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys)
+    {
+        var roleSys = entMan.System<SharedRoleSystem>();
+        return !roleSys.MindIsAntagonist(mind);
+    }
+}
diff --git a/Content.Shared/Mind/Filters/BodyMindFilter.cs b/Content.Shared/Mind/Filters/BodyMindFilter.cs
new file mode 100644 (file)
index 0000000..3345396
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Shared.Whitelist;
+
+namespace Content.Shared.Mind.Filters;
+
+/// <summary>
+/// A mind filter that checks the mind's owned entity against a whitelist.
+/// </summary>
+public sealed partial class BodyMindFilter : MindFilter
+{
+    [DataField(required: true)]
+    public EntityWhitelist Whitelist = new();
+
+    protected override bool ShouldRemove(Entity<MindComponent> ent, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys)
+    {
+        if (ent.Comp.OwnedEntity is not {} mob)
+            return true;
+
+        var sys = entMan.System<EntityWhitelistSystem>();
+        return sys.IsWhitelistFail(Whitelist, mob);
+    }
+}
diff --git a/Content.Shared/Mind/Filters/HasRoleMindFilter.cs b/Content.Shared/Mind/Filters/HasRoleMindFilter.cs
new file mode 100644 (file)
index 0000000..22f250d
--- /dev/null
@@ -0,0 +1,22 @@
+using Content.Shared.Roles;
+using Content.Shared.Whitelist;
+
+namespace Content.Shared.Mind.Filters;
+
+/// <summary>
+/// A mind filter that requires minds to have a role matching a whitelist.
+/// </summary>
+public sealed partial class HasRoleMindFilter : MindFilter
+{
+    /// <summary>
+    /// The whitelist a role must match for the mind to pass the filter.
+    /// </summary>
+    [DataField(required: true)]
+    public EntityWhitelist Whitelist;
+
+    protected override bool ShouldRemove(Entity<MindComponent> mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys)
+    {
+        var roleSys = entMan.System<SharedRoleSystem>();
+        return roleSys.MindHasRole(mind, Whitelist);
+    }
+}
diff --git a/Content.Shared/Mind/Filters/IMindPool.cs b/Content.Shared/Mind/Filters/IMindPool.cs
new file mode 100644 (file)
index 0000000..263d15d
--- /dev/null
@@ -0,0 +1,19 @@
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Shared.Mind.Filters;
+
+/// <summary>
+/// A mind pool that can find minds to use for objectives etc.
+/// Further filtered by <see cref="IMindFilter"/>.
+/// </summary>
+[ImplicitDataDefinitionForInheritors]
+public partial interface IMindPool
+{
+    /// <summary>
+    /// Add minds for this pool to a hashset.
+    /// The hashset gets reused and is cleared before this is called.
+    /// </summary>
+    /// <param name="minds">The hashset to add to</param>
+    /// <param name="exclude">A mind entity that must not be returned</param>
+    void FindMinds(HashSet<Entity<MindComponent>> minds, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys);
+}
diff --git a/Content.Shared/Mind/Filters/JobMindFilter.cs b/Content.Shared/Mind/Filters/JobMindFilter.cs
new file mode 100644 (file)
index 0000000..a6565e4
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Shared.Roles;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Mind.Filters;
+
+/// <summary>
+/// A mind filter that requires minds to have a specific job.
+/// This uses mind roles, not ID cards.
+/// </summary>
+public sealed partial class JobMindFilter : MindFilter
+{
+    [DataField(required: true)]
+    public ProtoId<JobPrototype> Job;
+
+    protected override bool ShouldRemove(Entity<MindComponent> mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys)
+    {
+        var jobSys = entMan.System<SharedJobSystem>();
+        return jobSys.MindHasJobWithId(mind, Job);
+    }
+}
diff --git a/Content.Shared/Mind/Filters/MindFilter.cs b/Content.Shared/Mind/Filters/MindFilter.cs
new file mode 100644 (file)
index 0000000..c2daf3e
--- /dev/null
@@ -0,0 +1,32 @@
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Shared.Mind.Filters;
+
+/// <summary>
+/// A mind filter that can be used to filter out minds from a <see cref="IMindPool"/>.
+/// </summary>
+[ImplicitDataDefinitionForInheritors]
+public abstract partial class MindFilter
+{
+    /// <summary>
+    /// The actual filter function, this has to return false for minds that get removed from the pool.
+    /// An excluded mind will be the same one passed to <see cref="IMindPool.FindMinds"/>.
+    /// </summary>
+    /// <param name="mind">The mind to check</param>
+    /// <param name="exclude">The same mind passed to FindMinds</param>
+    protected abstract bool ShouldRemove(Entity<MindComponent> mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys);
+
+    /// <summary>
+    /// The high-level filter function to be used by the mind system.
+    /// </summary>
+    public bool Filter(Entity<MindComponent> mind, EntityUid? exclude, EntityManager entMan, SharedMindSystem mindSys)
+    {
+        return ShouldRemove(mind, exclude, entMan, mindSys) ^ Inverted;
+    }
+
+    /// <summary>
+    /// Whether to invert functionality, only keeping minds that would otherwise be removed.
+    /// </summary>
+    [DataField]
+    public bool Inverted;
+}
diff --git a/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs b/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs
new file mode 100644 (file)
index 0000000..2d3cf6a
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Shared.Whitelist;
+
+namespace Content.Shared.Mind.Filters;
+
+/// <summary>
+/// A mind filter that removes minds with a blacklist objective.
+/// </summary>
+public sealed partial class ObjectiveMindFilter : MindFilter
+{
+    [DataField(required: true)]
+    public EntityWhitelist Blacklist = new();
+
+    protected override bool ShouldRemove(Entity<MindComponent> mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys)
+    {
+        var whitelistSys = entMan.System<EntityWhitelistSystem>();
+        foreach (var obj in mind.Comp.Objectives)
+        {
+            // mind has a blacklisted objective, remove it from the pool
+            if (whitelistSys.IsBlacklistPass(Blacklist, obj))
+                return true;
+        }
+
+        return false;
+    }
+}
index 271639725fb61e60e9acde8afaff56ce3787e12f..98ff77810ce5644e6344f3a322a9dd09c218ea78 100644 (file)
@@ -9,6 +9,7 @@ using Content.Shared.Humanoid;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Movement.Components;
 using Content.Shared.Mind.Components;
+using Content.Shared.Mind.Filters;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Objectives.Systems;
@@ -19,6 +20,7 @@ using Content.Shared.Whitelist;
 using Robust.Shared.Map;
 using Robust.Shared.Network;
 using Robust.Shared.Player;
+using Robust.Shared.Random;
 using Robust.Shared.Utility;
 
 namespace Content.Shared.Mind;
@@ -27,6 +29,7 @@ public abstract partial class SharedMindSystem : EntitySystem
 {
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
     [Dependency] private readonly INetManager _net = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
     [Dependency] private readonly SharedObjectivesSystem _objectives = default!;
     [Dependency] private readonly SharedPlayerSystem _player = default!;
@@ -37,6 +40,8 @@ public abstract partial class SharedMindSystem : EntitySystem
     [ViewVariables]
     protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
 
+    private HashSet<Entity<MindComponent>> _pickingMinds = new();
+
     public override void Initialize()
     {
         base.Initialize();
@@ -618,23 +623,70 @@ public abstract partial class SharedMindSystem : EntitySystem
 
     /// <summary>
     /// Returns a list of every living humanoid player's minds, except for a single one which is exluded.
+    /// A new hashset is allocated for every call, consider using <see cref="AddAliveHumans"/> instead.
     /// </summary>
     public HashSet<Entity<MindComponent>> GetAliveHumans(EntityUid? exclude = null)
     {
         var allHumans = new HashSet<Entity<MindComponent>>();
+        AddAliveHumans(allHumans, exclude);
+        return allHumans;
+    }
+
+    /// <summary>
+    /// Adds to a hashset every living humanoid player's minds, except for a single one which is exluded.
+    /// </summary>
+    public void AddAliveHumans(HashSet<Entity<MindComponent>> allHumans, EntityUid? exclude = null)
+    {
         // HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen
-        var query = EntityQueryEnumerator<MobStateComponent, HumanoidAppearanceComponent>();
-        while (query.MoveNext(out var uid, out var mobState, out _))
+        var query = EntityQueryEnumerator<HumanoidAppearanceComponent, MobStateComponent>();
+        while (query.MoveNext(out var uid, out _, out var mobState))
         {
             // the player needs to have a mind and not be the excluded one +
             // the player has to be alive
             if (!TryGetMind(uid, out var mind, out var mindComp) || mind == exclude || !_mobState.IsAlive(uid, mobState))
                 continue;
 
-            allHumans.Add(new Entity<MindComponent>(mind, mindComp));
+            allHumans.Add((mind, mindComp));
         }
+    }
 
-        return allHumans;
+    /// <summary>
+    /// Picks a random mind from a pool after applying a list of filters.
+    /// Returns null if no valid mind could be found.
+    /// </summary>
+    public Entity<MindComponent>? PickFromPool(IMindPool pool, List<MindFilter> filters, EntityUid? exclude = null)
+    {
+        _pickingMinds.Clear();
+        pool.FindMinds(_pickingMinds, exclude, EntityManager, this);
+        FilterMinds(_pickingMinds, filters, exclude);
+
+        if (_pickingMinds.Count == 0)
+            return null;
+
+        return _random.Pick(_pickingMinds);
+    }
+
+    /// <summary>
+    /// Filters minds from a hashset using a single <see cref="MindFilter"/>.
+    /// </summary>
+    public void FilterMinds(HashSet<Entity<MindComponent>> minds, MindFilter filter, EntityUid? exclude = null)
+    {
+        minds.RemoveWhere(mind => filter.Filter(mind, exclude, EntityManager, this));
+    }
+
+    /// <summary>
+    /// Filters minds from a hashset using a list of <see cref="MindFilter"/>s to apply sequentially.
+    /// </summary>
+    public void FilterMinds(HashSet<Entity<MindComponent>> minds, List<MindFilter> filters, EntityUid? exclude = null)
+    {
+        foreach (var filter in filters)
+        {
+            // no point calling it if there are none left
+            if (minds.Count == 0)
+                break;
+
+            FilterMinds(minds, filter, exclude);
+        }
     }
 
     /// <summary>
index e45c5792bdf512ced8812286b0ae670d04b1d8f6..4f307d8b319f4be799c57e5929fe48bdcb2a0072 100644 (file)
@@ -6,6 +6,7 @@ using Content.Shared.Database;
 using Content.Shared.GameTicking;
 using Content.Shared.Mind;
 using Content.Shared.Roles.Jobs;
+using Content.Shared.Whitelist;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Configuration;
@@ -19,13 +20,14 @@ namespace Content.Shared.Roles;
 
 public abstract class SharedRoleSystem : EntitySystem
 {
-    [Dependency] private   readonly IConfigurationManager _cfg = default!;
-    [Dependency] private   readonly IEntityManager _entityManager = default!;
-    [Dependency] private   readonly IPrototypeManager _prototypes = default!;
-    [Dependency] private   readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly IConfigurationManager _cfg = default!;
     [Dependency] protected readonly ISharedPlayerManager Player = default!;
-    [Dependency] private   readonly SharedAudioSystem _audio = default!;
-    [Dependency] private   readonly SharedMindSystem _minds = default!;
+    [Dependency] private readonly IEntityManager _entityManager = default!;
+    [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+    [Dependency] private readonly SharedMindSystem _minds = default!;
+    [Dependency] private readonly IPrototypeManager _prototypes = default!;
 
     private JobRequirementOverridePrototype? _requirementOverride;
 
@@ -504,6 +506,20 @@ public abstract class SharedRoleSystem : EntitySystem
         return found;
     }
 
+    /// <summary>
+    /// Returns true if a mind has a role that matches a whitelist.
+    /// </summary>
+    public bool MindHasRole(Entity<MindComponent> mind, EntityWhitelist whitelist)
+    {
+        foreach (var roleEnt in mind.Comp.MindRoles)
+        {
+            if (_whitelist.IsWhitelistPass(whitelist, roleEnt))
+                return true;
+        }
+
+        return false;
+    }
+
     /// <summary>
     /// Finds the first mind role of a specific type on a mind entity.
     /// </summary>
index 98c6b9789fb9b4ba59e6cc307b4fcd925766eeb0..411a4579612a5d8df9342b873eca8c4251654a46 100644 (file)
   - type: TargetObjective
     title: objective-condition-maroon-person-title
   - type: PickRandomPerson
+    filters:
+    # Can't have multiple objectives to kill the same person.
+    - !type:TargetObjectiveMindFilter
+      blacklist:
+        components:
+        - KillPersonCondition
   - type: KillPersonCondition
     requireMaroon: true
 
     unique: true
   - type: TargetObjective
     title: objective-condition-kill-maroon-title
-  - type: PickRandomHead
+  - type: PickRandomPerson
+    filters:
+    - !type:BodyMindFilter
+      whitelist:
+        components:
+        - CommandStaff
+    # Can't have multiple objectives to kill the same person.
+    - !type:TargetObjectiveMindFilter
+      blacklist:
+        components:
+        - KillPersonCondition
   - type: KillPersonCondition
     # don't count missing evac as killing as heads are higher profile, so you really need to do the dirty work
     # if ce flies a shittle to centcom you better find a way onto it
     difficulty: 1.75
   - type: TargetObjective
     title: objective-condition-other-traitor-alive-title
-  - type: RandomTraitorAlive
+  - type: PickRandomPerson
+    filters:
+    - !type:HasRoleMindFilter
+      whitelist:
+        components:
+        - TraitorRole
+    # Can't have multiple objectives to help/save the same person
+    - !type:TargetObjectiveMindFilter
+      blacklist:
+        components:
+        - RandomTraitorAlive
+        - RandomTraitorProgress
 
 - type: entity
   parent: [BaseTraitorSocialObjective, BaseHelpProgressObjective]
     difficulty: 2.5
   - type: TargetObjective
     title: objective-condition-other-traitor-progress-title
-  - type: RandomTraitorProgress
+  - type: PickRandomPerson
+    filters:
+    - !type:HasRoleMindFilter
+      whitelist:
+        components:
+        - TraitorRole
+    # Can't help anyone who is tasked with helping:
+    # 1. thats boring
+    # 2. no cyclic progress dependencies!!!
+    - !type:ObjectiveMindFilter
+      blacklist:
+        components:
+        - HelpProgressCondition
+    # Can't have multiple objectives to help/save the same person
+    - !type:TargetObjectiveMindFilter
+      blacklist:
+        components:
+        - RandomTraitorAlive
+        - RandomTraitorProgress
 
 # steal