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:
@@ -2,6 +2,7 @@ using System.Linq;
|
|||||||
using Content.Server.Antag.Components;
|
using Content.Server.Antag.Components;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.GameTicking.Events;
|
||||||
using Content.Server.GameTicking.Rules;
|
using Content.Server.GameTicking.Rules;
|
||||||
using Content.Server.Ghost.Roles;
|
using Content.Server.Ghost.Roles;
|
||||||
using Content.Server.Ghost.Roles.Components;
|
using Content.Server.Ghost.Roles.Components;
|
||||||
@@ -65,6 +66,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
|
|
||||||
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<NoJobsAvailableSpawningEvent>(OnJobNotAssigned);
|
||||||
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
|
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
|
||||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
|
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
|
||||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
|
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)
|
private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||||
{
|
{
|
||||||
if (!args.LateJoin)
|
if (!args.LateJoin)
|
||||||
@@ -149,8 +173,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
var rules = new List<(EntityUid, AntagSelectionComponent)>();
|
var rules = new List<(EntityUid, AntagSelectionComponent)>();
|
||||||
while (query.MoveNext(out var uid, out var antag, out _))
|
while (query.MoveNext(out var uid, out var antag, out _))
|
||||||
{
|
{
|
||||||
if (HasComp<ActiveGameRuleComponent>(uid) ||
|
if (HasComp<ActiveGameRuleComponent>(uid))
|
||||||
(HasComp<DelayedStartRuleComponent>(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after.
|
|
||||||
rules.Add((uid, antag));
|
rules.Add((uid, antag));
|
||||||
}
|
}
|
||||||
RobustRandom.Shuffle(rules);
|
RobustRandom.Shuffle(rules);
|
||||||
@@ -171,9 +194,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
|
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
|
||||||
continue;
|
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))
|
||||||
|
|
||||||
if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect))
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,16 +230,12 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (component.AssignmentComplete)
|
if (component.AssignmentComplete)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!component.PreSelectionsComplete)
|
|
||||||
{
|
|
||||||
var players = _playerManager.Sessions
|
var players = _playerManager.Sessions
|
||||||
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
|
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
|
||||||
status == PlayerGameStatus.JoinedGame)
|
status == PlayerGameStatus.JoinedGame)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
ChooseAntags((uid, component), players, midround: true);
|
ChooseAntags((uid, component), players, midround: true);
|
||||||
}
|
|
||||||
|
|
||||||
AssignPreSelectedSessions((uid, component));
|
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>
|
/// <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)
|
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
|
||||||
{
|
{
|
||||||
if (ent.Comp.PreSelectionsComplete)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var def in ent.Comp.Definitions)
|
foreach (var def in ent.Comp.Definitions)
|
||||||
{
|
{
|
||||||
ChooseAntags(ent, pool, def, midround: midround);
|
ChooseAntags(ent, pool, def, midround: midround);
|
||||||
@@ -254,7 +268,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
bool midround = false)
|
bool midround = false)
|
||||||
{
|
{
|
||||||
var playerPool = GetPlayerPool(ent, pool, def);
|
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.
|
// if there is both a spawner and players getting picked, let it fall back to a spawner.
|
||||||
var noSpawner = def.SpawnerPrototype == null;
|
var noSpawner = def.SpawnerPrototype == null;
|
||||||
@@ -327,6 +342,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true, bool onlyPreSelect = false)
|
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))
|
if (checkPref && !HasPrimaryAntagPreference(session, def))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -384,7 +401,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (antagEnt is not { } player)
|
if (antagEnt is not { } player)
|
||||||
{
|
{
|
||||||
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for 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.AssignedSessions.Remove(session);
|
||||||
ent.Comp.PreSelectedSessions[def].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))
|
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
|
||||||
{
|
{
|
||||||
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
|
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)
|
if (session != null)
|
||||||
{
|
{
|
||||||
ent.Comp.AssignedSessions.Remove(session);
|
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))
|
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session.AttachedEntity, def))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (ent.Comp.PreSelectedSessions.TryGetValue(def, out var preSelected) && preSelected.Contains(session))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (HasPrimaryAntagPreference(session, def))
|
if (HasPrimaryAntagPreference(session, def))
|
||||||
{
|
{
|
||||||
preferredList.Add(session);
|
preferredList.Add(session);
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ public sealed partial class AntagSelectionComponent : Component
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public LocId? AgentName;
|
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]
|
[DataDefinition]
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -98,6 +98,9 @@ namespace Content.Server.GameTicking
|
|||||||
if (job == null)
|
if (job == null)
|
||||||
{
|
{
|
||||||
var playerSession = _playerManager.GetSessionById(netUser);
|
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"));
|
_chatManager.DispatchServerMessage(playerSession, Loc.GetString("job-not-available-wait-in-lobby"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -209,6 +212,9 @@ namespace Content.Server.GameTicking
|
|||||||
JoinAsObserver(player);
|
JoinAsObserver(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var evNoJobs = new NoJobsAvailableSpawningEvent(player); // Used by gamerules to wipe their antag slot, if they got one
|
||||||
|
RaiseLocalEvent(evNoJobs);
|
||||||
|
|
||||||
_chatManager.DispatchServerMessage(player,
|
_chatManager.DispatchServerMessage(player,
|
||||||
Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
|
Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -34,6 +34,15 @@
|
|||||||
- id: Thief
|
- id: Thief
|
||||||
prob: 0.5
|
prob: 0.5
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
parent: BaseGameRule
|
||||||
|
id: DummyNonAntagChance
|
||||||
|
components:
|
||||||
|
- type: SubGamemodes
|
||||||
|
rules:
|
||||||
|
- id: DummyNonAntag
|
||||||
|
prob: 0.3
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: DeathMatch31
|
id: DeathMatch31
|
||||||
parent: BaseGameRule
|
parent: BaseGameRule
|
||||||
@@ -333,6 +342,22 @@
|
|||||||
mindRoles:
|
mindRoles:
|
||||||
- MindRoleInitialInfected
|
- 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
|
# event schedulers
|
||||||
|
|
||||||
- type: entityTable
|
- type: entityTable
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
description: traitor-description
|
description: traitor-description
|
||||||
showInVote: false
|
showInVote: false
|
||||||
rules:
|
rules:
|
||||||
|
- DummyNonAntagChance
|
||||||
- Traitor
|
- Traitor
|
||||||
- SubGamemodesRule
|
- SubGamemodesRule
|
||||||
- BasicStationEventScheduler
|
- BasicStationEventScheduler
|
||||||
@@ -178,6 +179,7 @@
|
|||||||
showInVote: false
|
showInVote: false
|
||||||
rules:
|
rules:
|
||||||
- Nukeops
|
- Nukeops
|
||||||
|
- DummyNonAntagChance
|
||||||
- SubGamemodesRule
|
- SubGamemodesRule
|
||||||
- BasicStationEventScheduler
|
- BasicStationEventScheduler
|
||||||
- MeteorSwarmScheduler
|
- MeteorSwarmScheduler
|
||||||
@@ -194,6 +196,7 @@
|
|||||||
description: rev-description
|
description: rev-description
|
||||||
showInVote: false
|
showInVote: false
|
||||||
rules:
|
rules:
|
||||||
|
- DummyNonAntagChance
|
||||||
- Revolutionary
|
- Revolutionary
|
||||||
- SubGamemodesRule
|
- SubGamemodesRule
|
||||||
- BasicStationEventScheduler
|
- BasicStationEventScheduler
|
||||||
@@ -210,6 +213,7 @@
|
|||||||
showInVote: false
|
showInVote: false
|
||||||
rules:
|
rules:
|
||||||
- Wizard
|
- Wizard
|
||||||
|
- DummyNonAntagChance
|
||||||
- SubGamemodesRuleNoWizard #No Dual Wizards at the start, midround is fine
|
- SubGamemodesRuleNoWizard #No Dual Wizards at the start, midround is fine
|
||||||
- BasicStationEventScheduler
|
- BasicStationEventScheduler
|
||||||
- MeteorSwarmScheduler
|
- MeteorSwarmScheduler
|
||||||
|
|||||||
Reference in New Issue
Block a user