diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index 62fbeefb65..7fdf812fbe 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -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(OnObjectivesTextGetInfo); + SubscribeLocalEvent(OnJobNotAssigned); SubscribeLocalEvent(OnPlayerSpawning); SubscribeLocalEvent(OnJobsAssigned); SubscribeLocalEvent(OnSpawnComplete); @@ -136,6 +138,28 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out var antag, out _)) { - if (HasComp(uid) || - (HasComp(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after. + if (HasComp(uid)) rules.Add((uid, antag)); } RobustRandom.Shuffle(rules); @@ -171,9 +194,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem 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 : GameRuleSystemDisable picking players for pre-spawn antags in the middle of a round public void ChooseAntags(Entity ent, IList 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 public bool TryMakeAntag(Entity 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(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 [DataField] public LocId? AgentName; + + /// + /// 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? + /// + [DataField] + public bool RemoveUponFailedSpawn = true; } [DataDefinition] diff --git a/Content.Server/GameTicking/Events/NoJobsAvailableSpawningEvent.cs b/Content.Server/GameTicking/Events/NoJobsAvailableSpawningEvent.cs new file mode 100644 index 0000000000..c1e2d99dc4 --- /dev/null +++ b/Content.Server/GameTicking/Events/NoJobsAvailableSpawningEvent.cs @@ -0,0 +1,8 @@ +using Robust.Shared.Player; + +namespace Content.Server.GameTicking.Events; + +/// +/// Raised on players who attempt to spawn in but fail to get a job, due to there not being any job slots available. +/// +public readonly record struct NoJobsAvailableSpawningEvent(ICommonSession Player); diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index a103a19733..561e1cb787 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -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; diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 80b8e9b717..a6c552e2e4 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -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 diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml index 0fbc63c197..3df262096b 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -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