Skip to content

Commit 1c73aad

Browse files
authored
Merge branch 'starlight-dev' into starlight-dev
2 parents 2afaccd + ffa0558 commit 1c73aad

21 files changed

Lines changed: 32694 additions & 32524 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using Content.Server.Antag.Components;
2+
using Content.Shared.Antag;
3+
using Content.Shared.Humanoid;
4+
using Content.Shared.Preferences;
5+
using JetBrains.Annotations;
6+
using Robust.Shared.Player;
7+
using Robust.Shared.Prototypes;
8+
9+
using static Content.Server.Antag.Components.AntagSelectionTime;
10+
11+
namespace Content.Server.Antag;
12+
13+
public sealed partial class AntagSelectionSystem
14+
{
15+
/// <summary>
16+
/// Checks if a player has already been pre-selected for a different antag within the same game rule.
17+
/// </summary>
18+
private bool HasConflictingPreSelection(
19+
Entity<AntagSelectionComponent> gameRule,
20+
ProtoId<AntagSpecifierPrototype> definition,
21+
ICommonSession player)
22+
{
23+
foreach (var (proto, sessions) in gameRule.Comp.PreSelectedSessions)
24+
{
25+
if (proto != definition && sessions.Contains(player))
26+
return true;
27+
}
28+
29+
return false;
30+
}
31+
32+
/// <summary>
33+
/// Checks if a given player is valid for a given antag definition, checking the player's selected profile if it exists.
34+
/// </summary>
35+
/// <param name="player">The player session to check.</param>
36+
/// <param name="antagEntity">The entity representing the antag.</param>
37+
/// <param name="selectedProfile">The player's selected character profile, if any.</param>
38+
/// <param name="definition">The antag definition to check against.</param>
39+
/// <returns>True if the player is valid for the antag, false otherwise.</returns>
40+
private bool IsSelectedProfileValidForAntag(
41+
ICommonSession player,
42+
EntityUid antagEntity,
43+
HumanoidCharacterProfile? selectedProfile,
44+
AntagSpecifierPrototype definition)
45+
{
46+
if (selectedProfile != null)
47+
return IsProfileValidForAntag(player, selectedProfile, definition);
48+
49+
// Bodies without HumanoidAppearanceComponent have no character profile to
50+
// validate and are allowed through here. Humanoid bodies must have a recoverable
51+
// profile so profile-specific antag requirements, including species restrictions
52+
// and preferences, cannot be bypassed.
53+
if (!TryComp<HumanoidAppearanceComponent>(antagEntity, out var humanoid))
54+
return true;
55+
56+
var profile = _humanoidAppearance.GetBaseProfile((antagEntity, humanoid));
57+
return profile != null && IsProfileValidForAntag(player, profile, definition);
58+
}
59+
60+
/// <summary>
61+
/// Returns whether a player may claim an antagonist ghost role.
62+
/// This intentionally does not require the antag preference to be enabled.
63+
/// </summary>
64+
[PublicAPI]
65+
public bool CanTakeAntagGhostRole(ICommonSession session, ProtoId<AntagSpecifierPrototype> definition)
66+
{
67+
return Proto.Resolve(definition, out var antag) && CanTakeAntagGhostRole(session, antag);
68+
}
69+
70+
/// <summary>
71+
/// Returns whether a player may claim an antagonist ghost role.
72+
/// </summary>
73+
[PublicAPI]
74+
public bool CanTakeAntagGhostRole(ICommonSession session, AntagSpecifierPrototype definition)
75+
{
76+
return !IsAntagBanned(session, definition) && _playTime.IsAllowedNonSpawning(session, definition.PrefRoles);
77+
}
78+
}

Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs

Lines changed: 31 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
using Content.Shared.Hands.Components;
1010
using Content.Shared.Humanoid;
1111
using Content.Shared.Players;
12-
using Content.Shared.Preferences;
12+
using Content.Shared.Silicons.Borgs.Components;
13+
using Content.Shared.Xenoborgs.Components;
1314
using JetBrains.Annotations;
1415
using Robust.Shared.Player;
1516
using Robust.Shared.Prototypes;
@@ -72,53 +73,6 @@ public bool CanBeAntag(ICommonSession player,
7273
return true;
7374
}
7475

75-
#region Starlight
76-
/// <summary>
77-
/// Checks if a player has already been pre-selected for a different antag within the same game rule.
78-
/// </summary>
79-
private bool HasConflictingPreSelection(
80-
Entity<AntagSelectionComponent> gameRule,
81-
ProtoId<AntagSpecifierPrototype> definition,
82-
ICommonSession player)
83-
{
84-
foreach (var (proto, sessions) in gameRule.Comp.PreSelectedSessions)
85-
{
86-
if (proto != definition && sessions.Contains(player))
87-
return true;
88-
}
89-
90-
return false;
91-
}
92-
93-
/// <summary>
94-
/// Checks if a given player is valid for a given antag definition, checking the player's selected profile if it exists.
95-
/// </summary>
96-
/// <param name="player">The player session to check.</param>
97-
/// <param name="antagEntity">The entity representing the antag.</param>
98-
/// <param name="selectedProfile">The player's selected character profile, if any.</param>
99-
/// <param name="definition">The antag definition to check against.</param>
100-
/// <returns>True if the player is valid for the antag, false otherwise.</returns>
101-
private bool IsSelectedProfileValidForAntag(
102-
ICommonSession player,
103-
EntityUid antagEntity,
104-
HumanoidCharacterProfile? selectedProfile,
105-
AntagSpecifierPrototype definition)
106-
{
107-
if (selectedProfile != null)
108-
return IsProfileValidForAntag(player, selectedProfile, definition);
109-
110-
// Bodies without HumanoidAppearanceComponent have no character profile to
111-
// validate and are allowed through here. Humanoid bodies must have a recoverable
112-
// profile so profile-specific antag requirements, including species restrictions
113-
// and preferences, cannot be bypassed.
114-
if (!TryComp<HumanoidAppearanceComponent>(antagEntity, out var humanoid))
115-
return true;
116-
117-
var profile = _humanoidAppearance.GetBaseProfile((antagEntity, humanoid));
118-
return profile != null && IsProfileValidForAntag(player, profile, definition);
119-
}
120-
#endregion
121-
12276
/// <inhereitdoc cref="IsSessionValid(ICommonSession,Entity{AntagSelectionComponent},ProtoId{AntagSpecifierPrototype})"/>
12377
public bool IsSessionValid(ICommonSession player,
12478
Entity<AntagSelectionComponent> gameRule,
@@ -239,6 +193,9 @@ public bool IsEntityValid(EntityUid? uid, AntagSpecifierPrototype def) // Starli
239193
if (HasComp<GhostComponent>(uid))
240194
return false;
241195

196+
if (HasComp<BorgChassisComponent>(uid) && !HasComp<XenoborgComponent>(uid)) // Starlight, we should really make this look better in the future if we add more borg antags
197+
return false; // Starlight
198+
242199
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
243200
return false;
244201

@@ -268,27 +225,6 @@ public bool IsAntagBanned(ICommonSession session, AntagSpecifierPrototype defini
268225
return false;
269226
}
270227

271-
#region Starlight
272-
/// <summary>
273-
/// Returns whether a player may claim an antagonist ghost role.
274-
/// This intentionally does not require the antag preference to be enabled.
275-
/// </summary>
276-
[PublicAPI]
277-
public bool CanTakeAntagGhostRole(ICommonSession session, ProtoId<AntagSpecifierPrototype> definition)
278-
{
279-
return Proto.Resolve(definition, out var antag) && CanTakeAntagGhostRole(session, antag);
280-
}
281-
282-
/// <summary>
283-
/// Returns whether a player may claim an antagonist ghost role.
284-
/// </summary>
285-
[PublicAPI]
286-
public bool CanTakeAntagGhostRole(ICommonSession session, AntagSpecifierPrototype definition)
287-
{
288-
return !IsAntagBanned(session, definition) && _playTime.IsAllowedNonSpawning(session, definition.PrefRoles);
289-
}
290-
#endregion
291-
292228
/// <inheritdoc cref="TryMakeAntag(Entity{AntagSelectionComponent},AntagSpecifierPrototype,ICommonSession,bool)"/>
293229
[PublicAPI]
294230
public bool TryMakeAntag(Entity<AntagSelectionComponent> gameRule,
@@ -338,6 +274,18 @@ public bool TryAssignNextAvailableAntag(Entity<AntagSelectionComponent> gameRule
338274
ICommonSession session,
339275
int players)
340276
{
277+
#region Starlight
278+
return TryAssignNextAvailableAntag(gameRule, session, players, out _);
279+
}
280+
281+
/// <summary>
282+
/// Tries to find an open antag slot for a given player and returns the definition that was assigned.
283+
/// </summary>
284+
private bool TryAssignNextAvailableAntag(Entity<AntagSelectionComponent> gameRule, ICommonSession session, int players, [NotNullWhen(true)] out AntagSpecifierPrototype? assignedAntag)
285+
{
286+
assignedAntag = null;
287+
#endregion
288+
341289
foreach (var selector in gameRule.Comp.Antags)
342290
{
343291
if (!Proto.Resolve(selector.Proto, out var antag))
@@ -348,8 +296,13 @@ public bool TryAssignNextAvailableAntag(Entity<AntagSelectionComponent> gameRule
348296
continue;
349297

350298
// Try and assign this antag, if we fail, then try the next definition!
351-
if (TryMakeAntag(gameRule, antag, session))
352-
return true;
299+
#region Starlight
300+
if (!TryMakeAntag(gameRule, antag, session))
301+
continue;
302+
303+
assignedAntag = antag;
304+
return true;
305+
#endregion
353306
}
354307

355308
return false;
@@ -385,8 +338,13 @@ public bool TryMakeLateJoinAntag(ICommonSession session)
385338

386339
foreach (var (uid, antag) in rules)
387340
{
388-
if (TryAssignNextAvailableAntag((uid, antag), session, players))
389-
return true;
341+
#region Starlight
342+
if (!TryAssignNextAvailableAntag((uid, antag), session, players, out var assignedAntag))
343+
continue;
344+
345+
RecordLateJoinAntagAssignment((uid, antag), assignedAntag); // logging
346+
return true;
347+
#endregion
390348
}
391349

392350
return false;
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using Content.Server.Antag.Components;
2+
using Content.Shared.Antag;
3+
using Content.Shared.GameTicking.Components;
4+
using Content.Shared.Roles;
5+
using JetBrains.Annotations;
6+
using Robust.Shared.Player;
7+
using Robust.Shared.Prototypes;
8+
using Content.Shared.Preferences;
9+
10+
namespace Content.Server.Antag;
11+
12+
public sealed partial class AntagSelectionSystem
13+
{
14+
readonly int _effectivePlayerCutoff = 30; // The number of online players at which unready players start counting as effectively ready
15+
readonly double _unreadyPlayerMultiplier = 0.25; // The fraction of unready players that count as effectively ready when above the cutoff
16+
private int GetEffectivePlayerCountPlayerRatio(int activePlayers)
17+
{
18+
var onlinePlayers = _playerManager.Sessions.Length;
19+
20+
if (onlinePlayers < _effectivePlayerCutoff)
21+
return activePlayers;
22+
23+
var inactivePlayers = Math.Max(0, onlinePlayers - activePlayers);
24+
25+
// Count 25% of lobby/spectating/unready players.
26+
return activePlayers + (int)(inactivePlayers * _unreadyPlayerMultiplier);
27+
}
28+
29+
/// <summary>
30+
/// Gets the number of still-available ghost roles reserving slots for an antagonist type.
31+
/// </summary>
32+
[PublicAPI]
33+
public int GetPendingAntagGhostRoleCount(
34+
Entity<AntagSelectionComponent> gameRule,
35+
ProtoId<AntagSpecifierPrototype> proto)
36+
{
37+
var count = 0;
38+
39+
foreach (var ghostRole in _ghostRole.GhostRoles)
40+
{
41+
if (ghostRole.Comp.Taken ||
42+
!TryComp<GhostRoleAntagSpawnerComponent>(ghostRole.Owner, out var spawner) ||
43+
spawner.Rule != gameRule.Owner ||
44+
spawner.Definition != proto)
45+
{
46+
continue;
47+
}
48+
49+
count++;
50+
}
51+
52+
return count;
53+
}
54+
55+
/// <summary>
56+
/// Merges the job whitelist and blacklist of a given antag definition with the existing job whitelist and blacklist for a player.
57+
/// </summary>
58+
/// <param name="jobs">The existing job whitelist and blacklist for a player.</param>
59+
/// <param name="definition">The antag definition containing its own job whitelist and blacklist.</param>
60+
/// <returns>The merged job whitelist and blacklist.</returns>
61+
private static (HashSet<ProtoId<JobPrototype>>? Whitelist, HashSet<ProtoId<JobPrototype>>? Blacklist)
62+
MergeAntagJobs(
63+
(HashSet<ProtoId<JobPrototype>>? Whitelist, HashSet<ProtoId<JobPrototype>>? Blacklist) jobs,
64+
AntagSpecifierPrototype definition)
65+
{
66+
if (definition.JobWhitelist != null)
67+
{
68+
if (jobs.Whitelist == null)
69+
jobs.Whitelist = new HashSet<ProtoId<JobPrototype>>(definition.JobWhitelist);
70+
else
71+
jobs.Whitelist.IntersectWith(definition.JobWhitelist);
72+
}
73+
74+
if (definition.JobBlacklist != null)
75+
{
76+
if (jobs.Blacklist == null)
77+
jobs.Blacklist = new HashSet<ProtoId<JobPrototype>>(definition.JobBlacklist);
78+
else
79+
jobs.Blacklist.UnionWith(definition.JobBlacklist);
80+
}
81+
82+
return jobs;
83+
}
84+
85+
/// <summary>
86+
/// Returns whether this specific character profile can be used for an antag definition.
87+
/// Account-level antag eligibility is not sufficient here because another enabled character may
88+
/// be the profile that actually satisfies the preference or a profile-specific requirement.
89+
/// TLDR: blame multi-slot
90+
/// </summary>
91+
[PublicAPI]
92+
public bool IsProfileValidForAntag(
93+
ICommonSession session,
94+
HumanoidCharacterProfile profile,
95+
ProtoId<AntagSpecifierPrototype> definition)
96+
{
97+
return Proto.Resolve(definition, out var antag) && IsProfileValidForAntag(session, profile, antag);
98+
}
99+
100+
/// <inheritdoc cref="IsProfileValidForAntag(ICommonSession,HumanoidCharacterProfile,ProtoId{AntagSpecifierPrototype})"/>
101+
[PublicAPI]
102+
public bool IsProfileValidForAntag(
103+
ICommonSession session,
104+
HumanoidCharacterProfile profile,
105+
AntagSpecifierPrototype definition)
106+
{
107+
foreach (var role in definition.PrefRoles)
108+
{
109+
if (!profile.AntagPreferences.Contains(role))
110+
continue;
111+
112+
// Session bans and playtime are checked before pre-selection. Passing null here
113+
// intentionally checks only profile-specific requirements such as species, age,
114+
// and traits for the exact character that will spawn.
115+
if (JobRequirements.TryRequirementsMet(
116+
_role.GetRoleRequirements(role),
117+
session,
118+
null,
119+
out _,
120+
EntityManager,
121+
_prototypeManager,
122+
profile))
123+
{
124+
return true;
125+
}
126+
}
127+
128+
return false;
129+
}
130+
131+
/// <summary>
132+
/// Returns the antag specifier prototypes this session has been preselected for.
133+
/// </summary>
134+
[PublicAPI]
135+
public HashSet<ProtoId<AntagSpecifierPrototype>> GetPreSelectedAntagSpecifiers(ICommonSession session)
136+
{
137+
var result = new HashSet<ProtoId<AntagSpecifierPrototype>>();
138+
var query = QueryAllRules();
139+
while (query.MoveNext(out var uid, out var comp, out _))
140+
{
141+
if (HasComp<EndedGameRuleComponent>(uid))
142+
continue;
143+
144+
foreach (var antag in comp.Antags)
145+
{
146+
if (comp.PreSelectedSessions.TryGetValue(antag, out var set) && set.Contains(session))
147+
result.Add(antag);
148+
}
149+
}
150+
151+
return result;
152+
}
153+
}

0 commit comments

Comments
 (0)