Skip to content
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Checks if a player has already been pre-selected for a different antag within the same game rule.
/// </summary>
private bool HasConflictingPreSelection(
Entity<AntagSelectionComponent> gameRule,
ProtoId<AntagSpecifierPrototype> definition,
ICommonSession player)
{
foreach (var (proto, sessions) in gameRule.Comp.PreSelectedSessions)
{
if (proto != definition && sessions.Contains(player))
return true;
}

return false;
}

/// <summary>
/// Checks if a given player is valid for a given antag definition, checking the player's selected profile if it exists.
/// </summary>
/// <param name="player">The player session to check.</param>
/// <param name="antagEntity">The entity representing the antag.</param>
/// <param name="selectedProfile">The player's selected character profile, if any.</param>
/// <param name="definition">The antag definition to check against.</param>
/// <returns>True if the player is valid for the antag, false otherwise.</returns>
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<HumanoidAppearanceComponent>(antagEntity, out var humanoid))
return true;

var profile = _humanoidAppearance.GetBaseProfile((antagEntity, humanoid));
return profile != null && IsProfileValidForAntag(player, profile, definition);
}

/// <summary>
/// Returns whether a player may claim an antagonist ghost role.
/// This intentionally does not require the antag preference to be enabled.
/// </summary>
[PublicAPI]
public bool CanTakeAntagGhostRole(ICommonSession session, ProtoId<AntagSpecifierPrototype> definition)
{
return Proto.Resolve(definition, out var antag) && CanTakeAntagGhostRole(session, antag);

Check warning on line 67 in Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs

View workflow job for this annotation

GitHub Actions / build

[C# diagnostics] reported by reviewdog 🐶 Use expression body for method Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/space-station-14/space-station-14/Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs"},"region":{"endColumn":98,"endLine":67,"startColumn":9,"startLine":67}}}],"message":{"text":"Use expression body for method"},"properties":{},"relatedLocations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/space-station-14/space-station-14/Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs"},"region":{"endColumn":6,"endLine":68,"startColumn":5,"startLine":64}}}],"ruleId":"IDE0022","ruleIndex":663}
}

/// <summary>
/// Returns whether a player may claim an antagonist ghost role.
/// </summary>
[PublicAPI]
public bool CanTakeAntagGhostRole(ICommonSession session, AntagSpecifierPrototype definition)
{
return !IsAntagBanned(session, definition) && _playTime.IsAllowedNonSpawning(session, definition.PrefRoles);

Check warning on line 76 in Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs

View workflow job for this annotation

GitHub Actions / build

[C# diagnostics] reported by reviewdog 🐶 Use expression body for method Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/space-station-14/space-station-14/Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs"},"region":{"endColumn":117,"endLine":76,"startColumn":9,"startLine":76}}}],"message":{"text":"Use expression body for method"},"properties":{},"relatedLocations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/space-station-14/space-station-14/Content.Server/Antag/AntagSelectionSystem.API.Assignment.Starlight.cs"},"region":{"endColumn":6,"endLine":77,"startColumn":5,"startLine":73}}}],"ruleId":"IDE0022","ruleIndex":663}
}
}
74 changes: 5 additions & 69 deletions Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,53 +73,6 @@ public bool CanBeAntag(ICommonSession player,
return true;
}

#region Starlight
/// <summary>
/// Checks if a player has already been pre-selected for a different antag within the same game rule.
/// </summary>
private bool HasConflictingPreSelection(
Entity<AntagSelectionComponent> gameRule,
ProtoId<AntagSpecifierPrototype> definition,
ICommonSession player)
{
foreach (var (proto, sessions) in gameRule.Comp.PreSelectedSessions)
{
if (proto != definition && sessions.Contains(player))
return true;
}

return false;
}

/// <summary>
/// Checks if a given player is valid for a given antag definition, checking the player's selected profile if it exists.
/// </summary>
/// <param name="player">The player session to check.</param>
/// <param name="antagEntity">The entity representing the antag.</param>
/// <param name="selectedProfile">The player's selected character profile, if any.</param>
/// <param name="definition">The antag definition to check against.</param>
/// <returns>True if the player is valid for the antag, false otherwise.</returns>
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<HumanoidAppearanceComponent>(antagEntity, out var humanoid))
return true;

var profile = _humanoidAppearance.GetBaseProfile((antagEntity, humanoid));
return profile != null && IsProfileValidForAntag(player, profile, definition);
}
#endregion

/// <inhereitdoc cref="IsSessionValid(ICommonSession,Entity{AntagSelectionComponent},ProtoId{AntagSpecifierPrototype})"/>
public bool IsSessionValid(ICommonSession player,
Entity<AntagSelectionComponent> gameRule,
Expand Down Expand Up @@ -239,6 +193,9 @@ public bool IsEntityValid(EntityUid? uid, AntagSpecifierPrototype def) // Starli
if (HasComp<GhostComponent>(uid))
return false;

if (HasComp<BorgChassisComponent>(uid) && !HasComp<XenoborgComponent>(uid)) // Starlight, we should really make this look better in the future if we add more borg antags
return false; // Starlight

if (!HasComp<HumanoidAppearanceComponent>(uid) && (!def.AllowNonHumans || !HasComp<HandsComponent>(uid))) // Starlight, Cheese, you have sent me down the path of the hell trying to get this working reliably
return false;

Expand Down Expand Up @@ -268,27 +225,6 @@ public bool IsAntagBanned(ICommonSession session, AntagSpecifierPrototype defini
return false;
}

#region Starlight
/// <summary>
/// Returns whether a player may claim an antagonist ghost role.
/// This intentionally does not require the antag preference to be enabled.
/// </summary>
[PublicAPI]
public bool CanTakeAntagGhostRole(ICommonSession session, ProtoId<AntagSpecifierPrototype> definition)
{
return Proto.Resolve(definition, out var antag) && CanTakeAntagGhostRole(session, antag);
}

/// <summary>
/// Returns whether a player may claim an antagonist ghost role.
/// </summary>
[PublicAPI]
public bool CanTakeAntagGhostRole(ICommonSession session, AntagSpecifierPrototype definition)
{
return !IsAntagBanned(session, definition) && _playTime.IsAllowedNonSpawning(session, definition.PrefRoles);
}
#endregion

/// <inheritdoc cref="TryMakeAntag(Entity{AntagSelectionComponent},AntagSpecifierPrototype,ICommonSession,bool)"/>
[PublicAPI]
public bool TryMakeAntag(Entity<AntagSelectionComponent> gameRule,
Expand Down
153 changes: 153 additions & 0 deletions Content.Server/Antag/AntagSelectionSystem.API.Starlight.cs
Original file line number Diff line number Diff line change
@@ -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);
}

/// <summary>
/// Gets the number of still-available ghost roles reserving slots for an antagonist type.
/// </summary>
[PublicAPI]
public int GetPendingAntagGhostRoleCount(
Entity<AntagSelectionComponent> gameRule,
ProtoId<AntagSpecifierPrototype> proto)
{
var count = 0;

foreach (var ghostRole in _ghostRole.GhostRoles)
{
if (ghostRole.Comp.Taken ||
!TryComp<GhostRoleAntagSpawnerComponent>(ghostRole.Owner, out var spawner) ||
spawner.Rule != gameRule.Owner ||
spawner.Definition != proto)
{
continue;
}

count++;
}

return count;
}

/// <summary>
/// Merges the job whitelist and blacklist of a given antag definition with the existing job whitelist and blacklist for a player.
/// </summary>
/// <param name="jobs">The existing job whitelist and blacklist for a player.</param>
/// <param name="definition">The antag definition containing its own job whitelist and blacklist.</param>
/// <returns>The merged job whitelist and blacklist.</returns>
private static (HashSet<ProtoId<JobPrototype>>? Whitelist, HashSet<ProtoId<JobPrototype>>? Blacklist)
MergeAntagJobs(
(HashSet<ProtoId<JobPrototype>>? Whitelist, HashSet<ProtoId<JobPrototype>>? Blacklist) jobs,
AntagSpecifierPrototype definition)
{
if (definition.JobWhitelist != null)
{
if (jobs.Whitelist == null)
jobs.Whitelist = new HashSet<ProtoId<JobPrototype>>(definition.JobWhitelist);
else
jobs.Whitelist.IntersectWith(definition.JobWhitelist);
}

if (definition.JobBlacklist != null)
{
if (jobs.Blacklist == null)
jobs.Blacklist = new HashSet<ProtoId<JobPrototype>>(definition.JobBlacklist);
else
jobs.Blacklist.UnionWith(definition.JobBlacklist);
}

return jobs;
}

/// <summary>
/// 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
/// </summary>
[PublicAPI]
public bool IsProfileValidForAntag(
ICommonSession session,
HumanoidCharacterProfile profile,
ProtoId<AntagSpecifierPrototype> definition)
{
return Proto.Resolve(definition, out var antag) && IsProfileValidForAntag(session, profile, antag);

Check warning on line 97 in Content.Server/Antag/AntagSelectionSystem.API.Starlight.cs

View workflow job for this annotation

GitHub Actions / build

[C# diagnostics] reported by reviewdog 🐶 Use expression body for method Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/space-station-14/space-station-14/Content.Server/Antag/AntagSelectionSystem.API.Starlight.cs"},"region":{"endColumn":108,"endLine":97,"startColumn":9,"startLine":97}}}],"message":{"text":"Use expression body for method"},"properties":{},"relatedLocations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/space-station-14/space-station-14/Content.Server/Antag/AntagSelectionSystem.API.Starlight.cs"},"region":{"endColumn":6,"endLine":98,"startColumn":5,"startLine":91}}}],"ruleId":"IDE0022","ruleIndex":663}
}

/// <inheritdoc cref="IsProfileValidForAntag(ICommonSession,HumanoidCharacterProfile,ProtoId{AntagSpecifierPrototype})"/>
[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;
}

/// <summary>
/// Returns the antag specifier prototypes this session has been preselected for.
/// </summary>
[PublicAPI]
public HashSet<ProtoId<AntagSpecifierPrototype>> GetPreSelectedAntagSpecifiers(ICommonSession session)
{
var result = new HashSet<ProtoId<AntagSpecifierPrototype>>();
var query = QueryAllRules();
while (query.MoveNext(out var uid, out var comp, out _))
{
if (HasComp<EndedGameRuleComponent>(uid))
continue;

foreach (var antag in comp.Antags)
{
if (comp.PreSelectedSessions.TryGetValue(antag, out var set) && set.Contains(session))
result.Add(antag);
}
}

return result;
}
}
Loading
Loading