From 2ff59f153ef965ba4e243a067baebd6f8a0c40f7 Mon Sep 17 00:00:00 2001 From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:48:39 +0100 Subject: [PATCH] Add support for antag-before-job selection (#35789) * Add support for antag-before-job selection * Include logging --- .../Antag/AntagSelectionSystem.API.cs | 72 ++++++++++- Content.Server/Antag/AntagSelectionSystem.cs | 114 +++++++++++++----- .../Components/AntagSelectionComponent.cs | 21 +++- .../Rules/GameRuleSystem.Utility.cs | 5 + .../Systems/StationJobsSystem.Roundstart.cs | 8 ++ Content.Shared/Antag/AntagAcceptability.cs | 10 +- 6 files changed, 188 insertions(+), 42 deletions(-) diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs index a4cb6fd0e5..c89e4df312 100644 --- a/Content.Server/Antag/AntagSelectionSystem.API.cs +++ b/Content.Server/Antag/AntagSelectionSystem.API.cs @@ -3,7 +3,9 @@ using System.Linq; using Content.Server.Antag.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Objectives; +using Content.Shared.Antag; using Content.Shared.Chat; +using Content.Shared.GameTicking.Components; using Content.Shared.Mind; using Content.Shared.Preferences; using JetBrains.Annotations; @@ -25,7 +27,7 @@ public sealed partial class AntagSelectionSystem definition = null; var totalTargetCount = GetTargetAntagCount(ent, players); - var mindCount = ent.Comp.SelectedMinds.Count; + var mindCount = ent.Comp.AssignedMinds.Count; if (mindCount >= totalTargetCount) return false; @@ -115,7 +117,7 @@ public sealed partial class AntagSelectionSystem return new List<(EntityUid, SessionData, string)>(); var output = new List<(EntityUid, SessionData, string)>(); - foreach (var (mind, name) in ent.Comp.SelectedMinds) + foreach (var (mind, name) in ent.Comp.AssignedMinds) { if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null) continue; @@ -137,7 +139,7 @@ public sealed partial class AntagSelectionSystem return new(); var output = new List>(); - foreach (var (mind, _) in ent.Comp.SelectedMinds) + foreach (var (mind, _) in ent.Comp.AssignedMinds) { if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null) continue; @@ -155,7 +157,7 @@ public sealed partial class AntagSelectionSystem if (!Resolve(ent, ref ent.Comp, false)) return new(); - return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList(); + return ent.Comp.AssignedMinds.Select(p => p.Item1).ToList(); } /// @@ -247,7 +249,7 @@ public sealed partial class AntagSelectionSystem if (!Resolve(ent, ref ent.Comp, false)) return false; - return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count; + return GetAliveAntagCount(ent) == ent.Comp.AssignedMinds.Count; } /// @@ -352,8 +354,66 @@ public sealed partial class AntagSelectionSystem var ruleEnt = GameTicker.AddGameRule(id); RemComp(ruleEnt); var antag = Comp(ruleEnt); - antag.SelectionsComplete = true; // don't do normal selection. + antag.AssignmentComplete = true; // don't do normal selection. GameTicker.StartGameRule(ruleEnt); return (ruleEnt, antag); } + + /// + /// Get all sessions that have been preselected for antag. + /// + public HashSet GetPreSelectedAntagSessions(AntagSelectionComponent? except = null) + { + var result = new HashSet(); + var query = QueryAllRules(); + while (query.MoveNext(out var uid, out var comp, out _)) + { + if (HasComp(uid)) + continue; + + if (comp == except) + continue; + + if (!comp.PreSelectionsComplete) + continue; + + foreach (var def in comp.Definitions) + { + result.UnionWith(comp.PreSelectedSessions); + } + } + + return result; + } + + /// + /// Get all sessions that have been preselected for antag and are exclusive, i.e. should not be paired with other antags. + /// + public HashSet GetPreSelectedExclusiveAntagSessions(AntagSelectionComponent? except = null) + { + var result = new HashSet(); + var query = QueryAllRules(); + while (query.MoveNext(out var uid, out var comp, out _)) + { + if (HasComp(uid)) + continue; + + if (comp == except) + continue; + + if (!comp.PreSelectionsComplete) + continue; + + foreach (var def in comp.Definitions) + { + if (def.MultiAntagSetting == AntagAcceptability.None) + { + result.UnionWith(comp.PreSelectedSessions); + break; + } + } + } + + return result; + } } diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index ce7f3fec4c..298fa61a67 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -11,6 +11,7 @@ using Content.Server.Preferences.Managers; using Content.Server.Roles; using Content.Server.Roles.Jobs; using Content.Server.Shuttles.Components; +using Content.Server.Station.Events; using Content.Shared.Antag; using Content.Shared.Clothing; using Content.Shared.GameTicking; @@ -89,19 +90,33 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) && status == PlayerGameStatus.JoinedGame) - .ToList(); + if (!component.PreSelectionsComplete) + { + var players = _playerManager.Sessions + .Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) && + status == PlayerGameStatus.JoinedGame) + .ToList(); - ChooseAntags((uid, component), players, midround: true); + ChooseAntags((uid, component), players, midround: true); + } + + AssignPreSelectedSessions((uid, component)); } /// @@ -201,7 +223,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystemDisable picking players for pre-spawn antags in the middle of a round public void ChooseAntags(Entity ent, IList pool, bool midround = false) { - if (ent.Comp.SelectionsComplete) + if (ent.Comp.PreSelectionsComplete) return; foreach (var def in ent.Comp.Definitions) @@ -209,7 +231,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem @@ -250,17 +272,43 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem + /// Assigns antag roles to sessions selected for it. + /// + public void AssignPreSelectedSessions(Entity ent) + { + // Only assign if there's been a pre-selection, and the selection hasn't already been made + if (!ent.Comp.PreSelectionsComplete || ent.Comp.AssignmentComplete) + return; + + foreach (var def in ent.Comp.Definitions) + { + foreach (var session in ent.Comp.PreSelectedSessions) + { + TryMakeAntag(ent, session, def); + } + } + + ent.Comp.AssignmentComplete = true; + } + /// /// Tries to makes a given player into the specified antagonist. /// @@ -286,7 +334,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem(session.AttachedEntity)) @@ -309,7 +357,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem - /// Has the primary selection of antagonists finished yet? + /// Has the primary assignment of antagonists finished yet? /// [DataField] - public bool SelectionsComplete; + public bool AssignmentComplete; + + /// + /// Has the antagonists been preselected but yet to be fully assigned? + /// + [DataField] + public bool PreSelectionsComplete; /// /// The definitions for the antagonists @@ -26,10 +32,10 @@ public sealed partial class AntagSelectionComponent : Component public List Definitions = new(); /// - /// The minds and original names of the players selected to be antagonists. + /// The minds and original names of the players assigned to be antagonists. /// [DataField] - public List<(EntityUid, string)> SelectedMinds = new(); + public List<(EntityUid, string)> AssignedMinds = new(); /// /// When the antag selection will occur. @@ -37,11 +43,16 @@ public sealed partial class AntagSelectionComponent : Component [DataField] public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn; + /// + /// Cached sessions of players who are chosen yet not given the role yet. + /// + public HashSet PreSelectedSessions = new(); + /// /// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick. /// Is not serialized. /// - public HashSet SelectedSessions = new(); + public HashSet AssignedSessions = new(); /// /// Locale id for the name of the antag. diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs index 5a5eb720cf..33ee91f8a5 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs @@ -19,6 +19,11 @@ public abstract partial class GameRuleSystem where T: IComponent return EntityQueryEnumerator(); } + protected EntityQueryEnumerator QueryDelayedRules() + { + return EntityQueryEnumerator(); + } + /// /// Queries all gamerules, regardless of if they're active or not. /// diff --git a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs index 8a918bd2fd..8045cfed46 100644 --- a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs +++ b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs @@ -1,10 +1,12 @@ using System.Linq; using Content.Server.Administration.Managers; +using Content.Server.Antag; using Content.Server.Players.PlayTimeTracking; using Content.Server.Station.Components; using Content.Server.Station.Events; using Content.Shared.Preferences; using Content.Shared.Roles; +using Robust.Server.Player; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -17,6 +19,8 @@ public sealed partial class StationJobsSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IBanManager _banManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; private Dictionary> _jobsByWeight = default!; private List _orderedWeights = default!; @@ -345,6 +349,7 @@ public sealed partial class StationJobsSystem foreach (var (player, profile) in profiles) { var roleBans = _banManager.GetJobBans(player); + var antagBlocked = _antag.GetPreSelectedAntagSessions(); var profileJobs = profile.JobPriorities.Keys.Select(k => new ProtoId(k)).ToList(); var ev = new StationJobsGetCandidatesEvent(player, profileJobs); RaiseLocalEvent(ref ev); @@ -361,6 +366,9 @@ public sealed partial class StationJobsSystem if (!_prototypeManager.TryIndex(jobId, out var job)) continue; + if (!job.CanBeAntag && (!_playerManager.TryGetSessionById(player, out var session) || antagBlocked.Contains(session))) + continue; + if (weight is not null && job.Weight != weight.Value) continue; diff --git a/Content.Shared/Antag/AntagAcceptability.cs b/Content.Shared/Antag/AntagAcceptability.cs index f56be97503..33323aacf3 100644 --- a/Content.Shared/Antag/AntagAcceptability.cs +++ b/Content.Shared/Antag/AntagAcceptability.cs @@ -17,7 +17,7 @@ public enum AntagAcceptability /// /// Choose anyone /// - All + All, } public enum AntagSelectionTime : byte @@ -28,8 +28,14 @@ public enum AntagSelectionTime : byte /// PrePlayerSpawn, + /// + /// Antag roles are selected to the player session before job assignment and spawning. + /// Unlike PrePlayerSpawn, this does not remove you from the job spawn pool. + /// + IntraPlayerSpawn, + /// /// Antag roles get assigned after players have been assigned jobs and have spawned in. /// - PostPlayerSpawn + PostPlayerSpawn, }