-
Notifications
You must be signed in to change notification settings - Fork 592
Antag Selection Fixes 3 #5934
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Antag Selection Fixes 3 #5934
Changes from 7 commits
6181c37
2049bc1
a7edb79
42ef4b2
0dc98f7
e45638b
6831c4f
117599a
6781c96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
| } | ||
|
|
||
| /// <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
|
||
| } | ||
| } | ||
| 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
|
||
| } | ||
|
|
||
| /// <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; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.