Metagame improvements to antag-before-job selection system (#35830)

* Initial commit

* Update the selection to only count for people who have one of the preferences assigned; latejoin on delay no longer applies pre-selection.
This commit is contained in:
SlamBamActionman
2025-03-19 18:28:25 +01:00
committed by GitHub
parent 398d92a064
commit 1d4ff1b0d1
6 changed files with 109 additions and 37 deletions

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Server.Antag.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Events;
using Content.Server.GameTicking.Rules;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
@@ -65,6 +66,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<NoJobsAvailableSpawningEvent>(OnJobNotAssigned);
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
@@ -136,6 +138,28 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
}
}
private void OnJobNotAssigned(NoJobsAvailableSpawningEvent args)
{
// If someone fails to spawn in due to there being no jobs, they should be removed from any preselected antags.
// We only care about delayed rules, since if they're active the player should have already been removed via MakeAntag.
var query = QueryDelayedRules();
while (query.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
continue;
if (!comp.RemoveUponFailedSpawn)
continue;
foreach (var def in comp.Definitions)
{
if (!comp.PreSelectedSessions.TryGetValue(def, out var session))
break;
session.Remove(args.Player);
}
}
}
private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
{
if (!args.LateJoin)
@@ -149,8 +173,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var rules = new List<(EntityUid, AntagSelectionComponent)>();
while (query.MoveNext(out var uid, out var antag, out _))
{
if (HasComp<ActiveGameRuleComponent>(uid) ||
(HasComp<DelayedStartRuleComponent>(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after.
if (HasComp<ActiveGameRuleComponent>(uid))
rules.Add((uid, antag));
}
RobustRandom.Shuffle(rules);
@@ -171,9 +194,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
continue;
var onlyPreSelect = (antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn && !antag.AssignmentComplete); // Don't wanna give them antag status if the rule hasn't assigned its existing ones yet
if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect))
if (TryMakeAntag((uid, antag), args.Player, def.Value))
break;
}
}
@@ -209,16 +230,12 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (component.AssignmentComplete)
return;
if (!component.PreSelectionsComplete)
{
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
status == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players, midround: true);
}
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
status == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players, midround: true);
AssignPreSelectedSessions((uid, component));
}
@@ -230,9 +247,6 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
{
if (ent.Comp.PreSelectionsComplete)
return;
foreach (var def in ent.Comp.Definitions)
{
ChooseAntags(ent, pool, def, midround: midround);
@@ -254,7 +268,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
bool midround = false)
{
var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
var existingAntagCount = ent.Comp.PreSelectedSessions.TryGetValue(def, out var existingAntags) ? existingAntags.Count : 0;
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def) - existingAntagCount;
// if there is both a spawner and players getting picked, let it fall back to a spawner.
var noSpawner = def.SpawnerPrototype == null;
@@ -327,6 +342,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// </summary>
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true, bool onlyPreSelect = false)
{
_adminLogger.Add(LogType.AntagSelection, $"Start trying to make {session} become the antagonist: {ToPrettyString(ent)}");
if (checkPref && !HasPrimaryAntagPreference(session, def))
return false;
@@ -384,7 +401,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (antagEnt is not { } player)
{
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
if (session != null)
_adminLogger.Add(LogType.AntagSelection,$"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
if (session != null && ent.Comp.RemoveUponFailedSpawn)
{
ent.Comp.AssignedSessions.Remove(session);
ent.Comp.PreSelectedSessions[def].Remove(session);
@@ -414,6 +432,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
{
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
_adminLogger.Add(LogType.AntagSelection,$"Antag spawner {player} in gamerule {ToPrettyString(ent)} failed due to not having GhostRoleAntagSpawnerComponent.");
if (session != null)
{
ent.Comp.AssignedSessions.Remove(session);
@@ -475,6 +494,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session.AttachedEntity, def))
continue;
if (ent.Comp.PreSelectedSessions.TryGetValue(def, out var preSelected) && preSelected.Contains(session))
continue;
if (HasPrimaryAntagPreference(session, def))
{
preferredList.Add(session);

View File

@@ -61,6 +61,13 @@ public sealed partial class AntagSelectionComponent : Component
/// </summary>
[DataField]
public LocId? AgentName;
/// <summary>
/// If the player is pre-selected but fails to spawn in (e.g. due to only having antag-immune jobs selected),
/// should they be removed from the pre-selection list?
/// </summary>
[DataField]
public bool RemoveUponFailedSpawn = true;
}
[DataDefinition]

View File

@@ -0,0 +1,8 @@
using Robust.Shared.Player;
namespace Content.Server.GameTicking.Events;
/// <summary>
/// Raised on players who attempt to spawn in but fail to get a job, due to there not being any job slots available.
/// </summary>
public readonly record struct NoJobsAvailableSpawningEvent(ICommonSession Player);

View File

@@ -98,6 +98,9 @@ namespace Content.Server.GameTicking
if (job == null)
{
var playerSession = _playerManager.GetSessionById(netUser);
var evNoJobs = new NoJobsAvailableSpawningEvent(playerSession); // Used by gamerules to wipe their antag slot, if they got one
RaiseLocalEvent(evNoJobs);
_chatManager.DispatchServerMessage(playerSession, Loc.GetString("job-not-available-wait-in-lobby"));
}
else
@@ -209,6 +212,9 @@ namespace Content.Server.GameTicking
JoinAsObserver(player);
}
var evNoJobs = new NoJobsAvailableSpawningEvent(player); // Used by gamerules to wipe their antag slot, if they got one
RaiseLocalEvent(evNoJobs);
_chatManager.DispatchServerMessage(player,
Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
return;

View File

@@ -34,6 +34,15 @@
- id: Thief
prob: 0.5
- type: entity
parent: BaseGameRule
id: DummyNonAntagChance
components:
- type: SubGamemodes
rules:
- id: DummyNonAntag
prob: 0.3
- type: entity
id: DeathMatch31
parent: BaseGameRule
@@ -333,6 +342,22 @@
mindRoles:
- MindRoleInitialInfected
# This rule makes the chosen players unable to get other antag rules, as a way to prevent metagaming job rolls.
# Put this before antags assigned to station jobs, but after non-job antags (NukeOps/Wiz).
- type: entity
id: DummyNonAntag
parent: BaseGameRule
components:
- type: GameRule
minPlayers: 5
- type: AntagSelection
selectionTime: IntraPlayerSpawn # Pre-selection before jobs; assignment doesn't really matter though, we only care about the pre-selection to block other antags.
removeUponFailedSpawn: false
definitions:
- prefRoles: [ InitialInfected, Traitor, Thief, HeadRev ]
max: 2
playerRatio: 30
# event schedulers
- type: entityTable

View File

@@ -149,12 +149,13 @@
description: traitor-description
showInVote: false
rules:
- Traitor
- SubGamemodesRule
- BasicStationEventScheduler
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
- DummyNonAntagChance
- Traitor
- SubGamemodesRule
- BasicStationEventScheduler
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
- type: gamePreset
id: Deathmatch
@@ -177,12 +178,13 @@
description: nukeops-description
showInVote: false
rules:
- Nukeops
- SubGamemodesRule
- BasicStationEventScheduler
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
- Nukeops
- DummyNonAntagChance
- SubGamemodesRule
- BasicStationEventScheduler
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
- type: gamePreset
id: Revolutionary
@@ -194,12 +196,13 @@
description: rev-description
showInVote: false
rules:
- Revolutionary
- SubGamemodesRule
- BasicStationEventScheduler
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
- DummyNonAntagChance
- Revolutionary
- SubGamemodesRule
- BasicStationEventScheduler
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
- type: gamePreset
id: Wizard
@@ -210,6 +213,7 @@
showInVote: false
rules:
- Wizard
- DummyNonAntagChance
- SubGamemodesRuleNoWizard #No Dual Wizards at the start, midround is fine
- BasicStationEventScheduler
- MeteorSwarmScheduler