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.