diff --git a/Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs b/Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs new file mode 100644 index 000000000000..5dee8c0cef61 --- /dev/null +++ b/Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs @@ -0,0 +1,78 @@ +using Content.Server.Antag.Components; +using Content.Shared.Antag; +using Content.Shared.Humanoid; +using Content.Shared.Preferences; +using JetBrains.Annotations; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +using static Content.Server.Antag.Components.AntagSelectionTime; + +namespace Content.Server.Antag; + +public sealed partial class AntagSelectionSystem +{ + /// + /// Checks if a player has already been pre-selected for a different antag within the same game rule. + /// + private bool HasConflictingPreSelection( + Entity gameRule, + ProtoId definition, + ICommonSession player) + { + foreach (var (proto, sessions) in gameRule.Comp.PreSelectedSessions) + { + if (proto != definition && sessions.Contains(player)) + return true; + } + + return false; + } + + /// + /// Checks if a given player is valid for a given antag definition, checking the player's selected profile if it exists. + /// + /// The player session to check. + /// The entity representing the antag. + /// The player's selected character profile, if any. + /// The antag definition to check against. + /// True if the player is valid for the antag, false otherwise. + private bool IsSelectedProfileValidForAntag( + ICommonSession player, + EntityUid antagEntity, + HumanoidCharacterProfile? selectedProfile, + AntagSpecifierPrototype definition) + { + if (selectedProfile != null) + return IsProfileValidForAntag(player, selectedProfile, definition); + + // Bodies without HumanoidAppearanceComponent have no character profile to + // validate and are allowed through here. Humanoid bodies must have a recoverable + // profile so profile-specific antag requirements, including species restrictions + // and preferences, cannot be bypassed. + if (!TryComp(antagEntity, out var humanoid)) + return true; + + var profile = _humanoidAppearance.GetBaseProfile((antagEntity, humanoid)); + return profile != null && IsProfileValidForAntag(player, profile, definition); + } + + /// + /// Returns whether a player may claim an antagonist ghost role. + /// This intentionally does not require the antag preference to be enabled. + /// + [PublicAPI] + public bool CanTakeAntagGhostRole(ICommonSession session, ProtoId definition) + { + return Proto.Resolve(definition, out var antag) && CanTakeAntagGhostRole(session, antag); + } + + /// + /// Returns whether a player may claim an antagonist ghost role. + /// + [PublicAPI] + public bool CanTakeAntagGhostRole(ICommonSession session, AntagSpecifierPrototype definition) + { + return !IsAntagBanned(session, definition) && _playTime.IsAllowedNonSpawning(session, definition.PrefRoles); + } +} diff --git a/Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs b/Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs index 51414c73549f..353310f510b1 100644 --- a/Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs +++ b/Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs @@ -9,7 +9,8 @@ using Content.Shared.Hands.Components; using Content.Shared.Humanoid; using Content.Shared.Players; -using Content.Shared.Preferences; +using Content.Shared.Silicons.Borgs.Components; +using Content.Shared.Xenoborgs.Components; using JetBrains.Annotations; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -72,53 +73,6 @@ public bool CanBeAntag(ICommonSession player, return true; } - #region Starlight - /// - /// Checks if a player has already been pre-selected for a different antag within the same game rule. - /// - private bool HasConflictingPreSelection( - Entity gameRule, - ProtoId definition, - ICommonSession player) - { - foreach (var (proto, sessions) in gameRule.Comp.PreSelectedSessions) - { - if (proto != definition && sessions.Contains(player)) - return true; - } - - return false; - } - - /// - /// Checks if a given player is valid for a given antag definition, checking the player's selected profile if it exists. - /// - /// The player session to check. - /// The entity representing the antag. - /// The player's selected character profile, if any. - /// The antag definition to check against. - /// True if the player is valid for the antag, false otherwise. - private bool IsSelectedProfileValidForAntag( - ICommonSession player, - EntityUid antagEntity, - HumanoidCharacterProfile? selectedProfile, - AntagSpecifierPrototype definition) - { - if (selectedProfile != null) - return IsProfileValidForAntag(player, selectedProfile, definition); - - // Bodies without HumanoidAppearanceComponent have no character profile to - // validate and are allowed through here. Humanoid bodies must have a recoverable - // profile so profile-specific antag requirements, including species restrictions - // and preferences, cannot be bypassed. - if (!TryComp(antagEntity, out var humanoid)) - return true; - - var profile = _humanoidAppearance.GetBaseProfile((antagEntity, humanoid)); - return profile != null && IsProfileValidForAntag(player, profile, definition); - } - #endregion - /// public bool IsSessionValid(ICommonSession player, Entity gameRule, @@ -239,6 +193,9 @@ public bool IsEntityValid(EntityUid? uid, AntagSpecifierPrototype def) // Starli if (HasComp(uid)) return false; + if (HasComp(uid) && !HasComp(uid)) // Starlight, we should really make this look better in the future if we add more borg antags + return false; // Starlight + if (!HasComp(uid) && (!def.AllowNonHumans || !HasComp(uid))) // Starlight, Cheese, you have sent me down the path of the hell trying to get this working reliably return false; @@ -268,27 +225,6 @@ public bool IsAntagBanned(ICommonSession session, AntagSpecifierPrototype defini return false; } - #region Starlight - /// - /// Returns whether a player may claim an antagonist ghost role. - /// This intentionally does not require the antag preference to be enabled. - /// - [PublicAPI] - public bool CanTakeAntagGhostRole(ICommonSession session, ProtoId definition) - { - return Proto.Resolve(definition, out var antag) && CanTakeAntagGhostRole(session, antag); - } - - /// - /// Returns whether a player may claim an antagonist ghost role. - /// - [PublicAPI] - public bool CanTakeAntagGhostRole(ICommonSession session, AntagSpecifierPrototype definition) - { - return !IsAntagBanned(session, definition) && _playTime.IsAllowedNonSpawning(session, definition.PrefRoles); - } - #endregion - /// [PublicAPI] public bool TryMakeAntag(Entity gameRule, diff --git a/Content.Server/Antag/AntagSelectionSystem.API.Starlight.cs b/Content.Server/Antag/AntagSelectionSystem.API.Starlight.cs new file mode 100644 index 000000000000..3ec3586bd387 --- /dev/null +++ b/Content.Server/Antag/AntagSelectionSystem.API.Starlight.cs @@ -0,0 +1,153 @@ +using Content.Server.Antag.Components; +using Content.Shared.Antag; +using Content.Shared.GameTicking.Components; +using Content.Shared.Roles; +using JetBrains.Annotations; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Content.Shared.Preferences; + +namespace Content.Server.Antag; + +public sealed partial class AntagSelectionSystem +{ + readonly int _effectivePlayerCutoff = 30; // The number of online players at which unready players start counting as effectively ready + readonly double _unreadyPlayerMultiplier = 0.25; // The fraction of unready players that count as effectively ready when above the cutoff + private int GetEffectivePlayerCountPlayerRatio(int activePlayers) + { + var onlinePlayers = _playerManager.Sessions.Length; + + if (onlinePlayers < _effectivePlayerCutoff) + return activePlayers; + + var inactivePlayers = Math.Max(0, onlinePlayers - activePlayers); + + // Count 25% of lobby/spectating/unready players. + return activePlayers + (int)(inactivePlayers * _unreadyPlayerMultiplier); + } + + /// + /// Gets the number of still-available ghost roles reserving slots for an antagonist type. + /// + [PublicAPI] + public int GetPendingAntagGhostRoleCount( + Entity gameRule, + ProtoId proto) + { + var count = 0; + + foreach (var ghostRole in _ghostRole.GhostRoles) + { + if (ghostRole.Comp.Taken || + !TryComp(ghostRole.Owner, out var spawner) || + spawner.Rule != gameRule.Owner || + spawner.Definition != proto) + { + continue; + } + + count++; + } + + return count; + } + + /// + /// Merges the job whitelist and blacklist of a given antag definition with the existing job whitelist and blacklist for a player. + /// + /// The existing job whitelist and blacklist for a player. + /// The antag definition containing its own job whitelist and blacklist. + /// The merged job whitelist and blacklist. + private static (HashSet>? Whitelist, HashSet>? Blacklist) + MergeAntagJobs( + (HashSet>? Whitelist, HashSet>? Blacklist) jobs, + AntagSpecifierPrototype definition) + { + if (definition.JobWhitelist != null) + { + if (jobs.Whitelist == null) + jobs.Whitelist = new HashSet>(definition.JobWhitelist); + else + jobs.Whitelist.IntersectWith(definition.JobWhitelist); + } + + if (definition.JobBlacklist != null) + { + if (jobs.Blacklist == null) + jobs.Blacklist = new HashSet>(definition.JobBlacklist); + else + jobs.Blacklist.UnionWith(definition.JobBlacklist); + } + + return jobs; + } + + /// + /// Returns whether this specific character profile can be used for an antag definition. + /// Account-level antag eligibility is not sufficient here because another enabled character may + /// be the profile that actually satisfies the preference or a profile-specific requirement. + /// TLDR: blame multi-slot + /// + [PublicAPI] + public bool IsProfileValidForAntag( + ICommonSession session, + HumanoidCharacterProfile profile, + ProtoId definition) + { + return Proto.Resolve(definition, out var antag) && IsProfileValidForAntag(session, profile, antag); + } + + /// + [PublicAPI] + public bool IsProfileValidForAntag( + ICommonSession session, + HumanoidCharacterProfile profile, + AntagSpecifierPrototype definition) + { + foreach (var role in definition.PrefRoles) + { + if (!profile.AntagPreferences.Contains(role)) + continue; + + // Session bans and playtime are checked before pre-selection. Passing null here + // intentionally checks only profile-specific requirements such as species, age, + // and traits for the exact character that will spawn. + if (JobRequirements.TryRequirementsMet( + _role.GetRoleRequirements(role), + session, + null, + out _, + EntityManager, + _prototypeManager, + profile)) + { + return true; + } + } + + return false; + } + + /// + /// Returns the antag specifier prototypes this session has been preselected for. + /// + [PublicAPI] + public HashSet> GetPreSelectedAntagSpecifiers(ICommonSession session) + { + var result = new HashSet>(); + var query = QueryAllRules(); + while (query.MoveNext(out var uid, out var comp, out _)) + { + if (HasComp(uid)) + continue; + + foreach (var antag in comp.Antags) + { + if (comp.PreSelectedSessions.TryGetValue(antag, out var set) && set.Contains(session)) + result.Add(antag); + } + } + + return result; + } +} diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs index 7b0ba758fdcf..c80c250b8628 100644 --- a/Content.Server/Antag/AntagSelectionSystem.API.cs +++ b/Content.Server/Antag/AntagSelectionSystem.API.cs @@ -18,9 +18,6 @@ namespace Content.Server.Antag; public sealed partial class AntagSelectionSystem { - readonly int _effectivePlayerCutoff = 30; // Starlight, the number of online players at which unready players start counting as effectively ready - readonly double _unreadyPlayerMultiplier = 0.25; // Starlight, the fraction of unready players that count as effectively ready when above the cutoff - /// [PublicAPI] public int GetActivePlayerCount() @@ -52,21 +49,6 @@ public int GetActivePlayerCount(IList pool) return count; } - #region Starlight - private int GetEffectivePlayerCountPlayerRatio(int activePlayers) - { - var onlinePlayers = _playerManager.Sessions.Length; - - if (onlinePlayers < _effectivePlayerCutoff) - return activePlayers; - - var inactivePlayers = Math.Max(0, onlinePlayers - activePlayers); - - // Count 25% of lobby/spectating/unready players. - return activePlayers + (int)(inactivePlayers * _unreadyPlayerMultiplier); - } - #endregion - [PublicAPI] public IEnumerable GetActivePlayers() { @@ -190,34 +172,6 @@ public int GetAssignedAntagCount(Entity gameRule, Proto return !gameRule.Comp.AssignedMinds.TryGetValue(proto, out var assigned) ? 0 : assigned.Count; } - #region Starlight - /// - /// Gets the number of still-available ghost roles reserving slots for an antagonist type. - /// - [PublicAPI] - public int GetPendingAntagGhostRoleCount( - Entity gameRule, - ProtoId proto) - { - var count = 0; - - foreach (var ghostRole in _ghostRole.GhostRoles) - { - if (ghostRole.Comp.Taken || - !TryComp(ghostRole.Owner, out var spawner) || - spawner.Rule != gameRule.Owner || - spawner.Definition != proto) - { - continue; - } - - count++; - } - - return count; - } - #endregion - /// /// Checks if all antags of this specific type from this specific game rule have been assigned. /// @@ -561,38 +515,6 @@ public void SendBriefing( return (whitelist, blacklist); } - #region Starlight - /// - /// Merges the job whitelist and blacklist of a given antag definition with the existing job whitelist and blacklist for a player. - /// - /// The existing job whitelist and blacklist for a player. - /// The antag definition containing its own job whitelist and blacklist. - /// The merged job whitelist and blacklist. - private static (HashSet>? Whitelist, HashSet>? Blacklist) - MergeAntagJobs( - (HashSet>? Whitelist, HashSet>? Blacklist) jobs, - AntagSpecifierPrototype definition) - { - if (definition.JobWhitelist != null) - { - if (jobs.Whitelist == null) - jobs.Whitelist = new HashSet>(definition.JobWhitelist); - else - jobs.Whitelist.IntersectWith(definition.JobWhitelist); - } - - if (definition.JobBlacklist != null) - { - if (jobs.Blacklist == null) - jobs.Blacklist = new HashSet>(definition.JobBlacklist); - else - jobs.Blacklist.UnionWith(definition.JobBlacklist); - } - - return jobs; - } - #endregion - /// /// Get all sessions that have been preselected for antag. /// @@ -669,54 +591,6 @@ public IEnumerable> GetValidAntagPreferences(ICommonSess } } - #region Starlight - /// - /// Returns whether this specific character profile can be used for an antag definition. - /// Account-level antag eligibility is not sufficient here because another enabled character may - /// be the profile that actually satisfies the preference or a profile-specific requirement. - /// TLDR: blame multi-slot - /// - [PublicAPI] - public bool IsProfileValidForAntag( - ICommonSession session, - HumanoidCharacterProfile profile, - ProtoId definition) - { - return Proto.Resolve(definition, out var antag) && IsProfileValidForAntag(session, profile, antag); - } - - /// - [PublicAPI] - public bool IsProfileValidForAntag( - ICommonSession session, - HumanoidCharacterProfile profile, - AntagSpecifierPrototype definition) - { - foreach (var role in definition.PrefRoles) - { - if (!profile.AntagPreferences.Contains(role)) - continue; - - // Session bans and playtime are checked before pre-selection. Passing null here - // intentionally checks only profile-specific requirements such as species, age, - // and traits for the exact character that will spawn. - if (JobRequirements.TryRequirementsMet( - _role.GetRoleRequirements(role), - session, - null, - out _, - EntityManager, - _prototypeManager, - profile)) - { - return true; - } - } - - return false; - } - #endregion - /// /// Checks if a player has been assigned antag for a specific game rule. /// Does not check if that game rule is active or ended so check that beforehand if it matters. @@ -820,29 +694,4 @@ public bool IsAssignedExclusiveAntag(ICommonSession player, params HashSet - /// Returns the antag specifier prototypes this session has been preselected for. - /// - [PublicAPI] - public HashSet> GetPreSelectedAntagSpecifiers(ICommonSession session) - { - var result = new HashSet>(); - var query = QueryAllRules(); - while (query.MoveNext(out var uid, out var comp, out _)) - { - if (HasComp(uid)) - continue; - - foreach (var antag in comp.Antags) - { - if (comp.PreSelectedSessions.TryGetValue(antag, out var set) && set.Contains(session)) - result.Add(antag); - } - } - - return result; - } - #endregion } diff --git a/Content.Server/Antag/AntagSelectionSystem.Starlight.cs b/Content.Server/Antag/AntagSelectionSystem.Starlight.cs index 6bfc62ad2067..2d67d480a3dc 100644 --- a/Content.Server/Antag/AntagSelectionSystem.Starlight.cs +++ b/Content.Server/Antag/AntagSelectionSystem.Starlight.cs @@ -346,9 +346,9 @@ private void RecordLateJoinAntagAssignment( /// /// Enforces each rule's cached primary-selection target, allowing latejoins to raise it - /// only when LateJoinAdditional is enabled. Missing live-player slots are retried through normal - /// antagonist selection, and any remaining slots are reserved as ghost roles only for antagonist - /// definitions with a configured SpawnerPrototype. + /// only when LateJoinAdditional is enabled. Missing slots without a ghost-role spawner are retried + /// through normal antagonist selection, while definitions with a configured SpawnerPrototype + /// reserve their missing slots as ghost roles. /// Returns true when a timed repair should be retried because an eligible live assignment /// or a configured ghost-role spawner failed. /// aka: "antags didn't roll correctly, screw it, try again" @@ -388,7 +388,10 @@ player.AttachedEntity is { } entity && IsSelectedProfileValidForAntag(player, entity, null, definition)); targets[definition.ID] = (definition, target, assigned, eligible); - if (gameRule.Comp.SelectionTime != Never && definition.PickPlayer && assigned < target) + if (gameRule.Comp.SelectionTime != Never && + definition.PickPlayer && + definition.SpawnerPrototype is null && + assigned < target) shortfalls.Add((definition, target - assigned)); } @@ -443,6 +446,7 @@ player.AttachedEntity is { } entity && // ghost roles (if the antag allows them) are the final result. Retry only on a // failed assignment or failed spawner. var liveRetryPossible = definition.PickPlayer && + definition.SpawnerPrototype is null && gameRule.Comp.SelectionTime != Never && assigned < target && assignedBefore + eligibleBefore >= target; diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index 5bfb4f408b60..6649028ead65 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -156,19 +156,13 @@ protected override void Started(EntityUid uid, AntagSelectionComponent component return; } - // A rule added after spawning missed its configured round-start selection event, so every - // player-selecting timing must fall back to selecting live players when the rule activates. var players = GetActivePlayers().ToArray(); #region Starlight - //if (component.SelectionTime == RuleStarted) // Only pre-select antags if we pre-select on rule start - // AssignAntags((uid, component), players); - //else // Otherwise, we only spawn the ghost roles! - // If the selection time is set to never, we don't want to assign antags, we just want to spawn ghost roles for the game rule. - if (component.SelectionTime == Never) - SpawnGhostRoles((uid, component), players.Length); - else + if (component.SelectionTime == RuleStarted) AssignAntags((uid, component), players); + else + SpawnGhostRoles((uid, component), players.Length); // AssignAntags already exhausts the live pool and creates fallback ghost roles. This final // pass also verifies their actual counts and catches failed spawner creation immediately.