Revert "Cleanup Antag Selection Logic a Lot" (#43041)

Revert "Cleanup Antag Selection Logic a Lot (#42673)"

This reverts commit c2f986ea8b.
This commit is contained in:
Errant 2026-02-26 19:36:48 +01:00 • committed by GitHub
parent 9ae88c418b
commit 8315eaab7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 112 additions and 180 deletions

View file

@ -13,11 +13,6 @@ public static partial class PoolManager
public static readonly ContentPoolManager Instance = new();
public const string TestMap = "Empty";
/// <summary>
/// Designated load bearing station. Sometimes you need a station for a test.
/// </summary>
public const string TestStation = "Saltern";
/// <summary>
/// Runs a server, or a client until a condition is true
/// </summary>

View file

@ -18,8 +18,7 @@ public sealed class StartEndGameRulesTest
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Dirty = true,
DummyTicker = false,
Map = PoolManager.TestStation
DummyTicker = false
});
var server = pair.Server;
await server.WaitIdleAsync();

View file

@ -32,7 +32,7 @@ public sealed class EvacShuttleTest
pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, true);
pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false);
var gameMap = pair.Server.CfgMan.GetCVar(CCVars.GameMap);
pair.Server.CfgMan.SetCVar(CCVars.GameMap, PoolManager.TestStation);
pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Saltern");
await server.WaitPost(() => ticker.RestartRound());
await pair.RunTicksSync(25);

View file

@ -35,6 +35,6 @@ public sealed class AntagMultipleRoleSpawnerSystem : EntitySystem
if (entProtos.Count == 0)
return; // You will just get a normal job
args.Entity = Spawn(ent.Comp.PickAndTake ? _random.PickAndTake(entProtos) : _random.Pick(entProtos), args.Coords);
args.Entity = Spawn(ent.Comp.PickAndTake ? _random.PickAndTake(entProtos) : _random.Pick(entProtos));
}
}

View file

@ -336,14 +336,13 @@ public sealed partial class AntagSelectionSystem
/// This technically is a gamerule-ent-less way to make an entity an antag.
/// You should almost never be using this.
/// </summary>
public void ForceMakeAntag<T>(ICommonSession player, string defaultRule) where T : Component
public void ForceMakeAntag<T>(ICommonSession? player, string defaultRule) where T : Component
{
var rule = ForceGetGameRuleEnt<T>(defaultRule);
if (!TryGetNextAvailableDefinition(rule, out var def))
def = rule.Comp.Definitions.Last();
MakeSessionAntagonist(rule, player, def.Value);
MakeAntag(rule, player, def.Value);
}
/// <summary>

View file

@ -26,7 +26,6 @@ using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Roles;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Server.Player;
@ -39,14 +38,6 @@ using Robust.Shared.Utility;
namespace Content.Server.Antag;
/// <summary>
/// Turns players into antags.
/// </summary>
/// <remarks>
/// Do not ever ever ever spawn and initialize an entity prototype in nullspace then move it to the grid.
/// I wasted 4 hours refactoring this system specifically to fix that mistake.
/// Always initialize your entities attached to the entity you're spawning them on, or the correct map at the very least.
/// </remarks>
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
{
[Dependency] private readonly AudioSystem _audio = default!;
@ -96,7 +87,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!Exists(rule) || !TryComp<AntagSelectionComponent>(rule, out var select))
return;
AttachSessionToAntagonist((rule, select), args.Player, def, _transform.GetMapCoordinates(ent));
MakeAntag((rule, select), args.Player, def, ignoreSpawner: true);
args.TookRole = true;
_ghostRole.UnregisterGhostRole((ent, Comp<GhostRoleComponent>(ent)));
}
@ -185,8 +176,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// Attempt to make this player be a late-join antag.
/// </summary>
/// <param name="session">The session to attempt to make antag.</param>
[PublicAPI]
public bool TryMakeLateJoinAntag(ICommonSession session)
public void TryMakeLateJoinAntag(ICommonSession session)
{
// TODO: this really doesn't handle multiple latejoin definitions well
// eventually this should probably store the players per definition with some kind of unique identifier.
@ -218,10 +208,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
continue;
if (TryMakeAntag((uid, antag), session, def.Value))
return true;
break;
}
return false;
}
protected override void Added(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
@ -327,9 +315,15 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
}
if (session == null)
CreateAntagSpawner(ent, def); // Create a spawner since there's no session to attach.
MakeAntag(ent, null, def); // This is for spawner antags
else
PreSelectSessionForAntagonist(ent, session, def);
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session); // Selection done!
Log.Debug($"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
}
}
}
@ -359,181 +353,123 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Tries to makes a given player into the specified antagonist.
/// </summary>
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def, 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 && !ValidAntagPreference(session, def.PrefRoles))
return false;
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session.AttachedEntity, def))
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
return false;
if (onlyPreSelect)
PreSelectSessionForAntagonist(ent, session, def);
if (onlyPreSelect && session != null)
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session);
Log.Debug($"Pre-selected {session!.Name} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
}
else
MakeSessionAntagonist(ent, session, def);
{
MakeAntag(ent, session, def, ignoreSpawner);
}
return true;
}
/// <summary>
/// Create an antag spawner which can be taken over by a player through the ghost role system.
/// Makes a given player into the specified antagonist.
/// </summary>
/// <param name="ent">Antag rule entity</param>
/// <param name="def">Antag selection definition chosen from the entity</param>
[PublicAPI]
private EntityUid? CreateAntagSpawner(Entity<AntagSelectionComponent> ent, AntagSelectionDefinition def)
public void MakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
{
if (def.SpawnerPrototype is not { } proto)
return null;
EntityUid? antagEnt = null;
var isSpawner = false;
var spawner = Spawn(def.SpawnerPrototype);
if (!TryValidSpawnPosition(ent, spawner))
if (session != null)
{
Log.Error($"Found no valid positions to place antag spawner {ToPrettyString(spawner)} prototype: {proto}");
Del(spawner);
return null;
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session);
ent.Comp.AssignedSessions.Add(session);
// we shouldn't be blocking the entity if they're just a ghost or smth.
if (!HasComp<GhostComponent>(session.AttachedEntity))
antagEnt = session.AttachedEntity;
}
else if (!ignoreSpawner && def.SpawnerPrototype != null) // don't add spawners if we have a player, dummy.
{
antagEnt = Spawn(def.SpawnerPrototype);
isSpawner = true;
}
if (!TryComp<GhostRoleAntagSpawnerComponent>(spawner, out var spawnerComp))
if (!antagEnt.HasValue)
{
Log.Error($"Antag spawner {spawner} does not have a {nameof(GhostRoleAntagSpawnerComponent)}.");
_adminLogger.Add(LogType.AntagSelection, $"Antag spawner {spawner} in gamerule {ToPrettyString(ent)} failed due to not having {nameof(GhostRoleAntagSpawnerComponent)}.");
Del(spawner);
return null;
var getEntEv = new AntagSelectEntityEvent(session, ent, def.PrefRoles);
RaiseLocalEvent(ent, ref getEntEv, true);
antagEnt = getEntEv.Entity;
}
spawnerComp.Rule = ent;
spawnerComp.Definition = def;
return spawner;
}
/// <summary>
/// Does antag pre-selection logic, adding a specified player session to the PreSelection list and logging it for admins.
/// </summary>
private void PreSelectSessionForAntagonist(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def)
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session);
Log.Debug($"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
}
/// <summary>
/// Creates a new antagonist entity at the specified coordinates, then attaches the specified player to that antagonist.
/// </summary>
private EntityUid? AttachSessionToAntagonist(Entity<AntagSelectionComponent> ent,
ICommonSession session,
AntagSelectionDefinition def,
MapCoordinates coords)
{
PreSelectSessionForAntagonist(ent, session, def);
ent.Comp.AssignedSessions.Add(session);
return SpawnNewAntagonist(ent, session, def, coords);
}
/// <summary>
/// Makes a specified player into a specified antagonist.
/// If the player is a ghost or has no attached entity, it will attempt to find a valid spawn position and spawn a new entity.
/// Otherwise, it will try to move their current entity to their antag's spawn position (if it exists) and then set them up as antag.
/// </summary>
private EntityUid? MakeSessionAntagonist(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def)
{
PreSelectSessionForAntagonist(ent, session, def);
ent.Comp.AssignedSessions.Add(session);
// If the player has no entity to make an antagonist, make a new entity for them
if (HasComp<GhostComponent>(session.AttachedEntity) || session.AttachedEntity is not { } player)
if (antagEnt is not { } player)
{
return SpawnNewAntagonist(ent, session, def);
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
_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);
}
return;
}
TryValidSpawnPosition(ent, player, session);
InitializeAntag(ent, player, session, def);
return player;
}
// TODO: This is really messy because this part runs twice for midround events.
// Once when the ghostrole spawner is created and once when a player takes it.
// Therefore any component subscribing to this has to make sure both subscriptions return the same value
// or the ghost role raffle location preview will be wrong.
/// <summary>
/// Attempts to create a new antagonist entity and attach a player session to it at a valid spawnpoint.
/// Does nothing if it cannot find a valid spawnpoint.
/// </summary>
private EntityUid? SpawnNewAntagonist(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def)
{
if (GetValidSpawnPosition(ent, session.AttachedEntity, session) is not { } coordinates)
{
Log.Error($"Was unable to find a valid spawn position for, {session.Name}, gamerule: {ToPrettyString(ent)} when trying to make them into an antagonist.");
return null;
}
return SpawnNewAntagonist(ent, session, def, coordinates);
}
/// <summary>
/// Attempts to create a new antagonist entity at the specified coordinates and attach a player session to it.
/// If it cannot spawn an antagonist entity, it does nothing.
/// </summary>
private EntityUid? SpawnNewAntagonist(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def, MapCoordinates coordinates)
{
var getEntEv = new AntagSelectEntityEvent(session, ent, def.PrefRoles, coordinates);
RaiseLocalEvent(ent, ref getEntEv, true);
if (getEntEv.Entity is not { } antag)
{
Log.Error($"Tried to make {session.UserId} into an antagonist but was unable to spawn an entity for them. Gamerule {ToPrettyString(ent)}");
return null;
}
InitializeAntag(ent, antag, session, def);
return antag;
}
/// <summary>
/// Raises an event to the gamerule to check all valid possible spawning points for this rule.
/// Returns a random spawnpoint from a list of valid spawnpoints, or null if there weren't any.
/// </summary>
private MapCoordinates? GetValidSpawnPosition(Entity<AntagSelectionComponent> ent, EntityUid? antag, ICommonSession? session = null)
{
var getPosEv = new AntagSelectLocationEvent(ent, antag, session);
var getPosEv = new AntagSelectLocationEvent(session, ent, player);
RaiseLocalEvent(ent, ref getPosEv, true);
if (getPosEv.Handled)
{
var playerXform = Transform(player);
var pos = RobustRandom.Pick(getPosEv.Coordinates);
_transform.SetMapCoordinates((player, playerXform), pos);
}
if (!getPosEv.Handled)
return null;
// If we want to just do a ghost role spawner, set up data here and then return early.
// This could probably be an event in the future if we want to be more refined about it.
if (isSpawner)
{
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);
ent.Comp.PreSelectedSessions[def].Remove(session);
}
return RobustRandom.Pick(getPosEv.Coordinates);
}
return;
}
/// <summary>
/// Looks for a valid spawn position for this antagonist type, then moves the antagonist entity to that spawn position.
/// </summary>
private bool TryValidSpawnPosition(Entity<AntagSelectionComponent> ent, EntityUid antag, ICommonSession? session = null)
{
if (GetValidSpawnPosition(ent, antag, session) is not { } coordinates)
return false;
spawnerComp.Rule = ent;
spawnerComp.Definition = def;
return;
}
var xform = Transform(antag);
_transform.SetMapCoordinates((antag, xform), coordinates);
return true;
}
/// <summary>
/// Initializes the antagonist status on the specified entity.
/// Adds the needed components, loadouts, items, attaches the player and fires off an event.
/// </summary>
private void InitializeAntag(Entity<AntagSelectionComponent> ent, EntityUid antag, ICommonSession? session, AntagSelectionDefinition def)
{
// The following is where we apply components, equipment, and other changes to our antagonist entity.
EntityManager.AddComponents(antag, def.Components);
EntityManager.AddComponents(player, def.Components);
// Equip the entity's RoleLoadout and LoadoutGroup
List<ProtoId<StartingGearPrototype>> gear = new();
if (def.StartingGear is not null)
gear.Add(def.StartingGear.Value);
_loadout.Equip(antag, gear, def.RoleLoadout);
_loadout.Equip(player, gear, def.RoleLoadout);
if (session != null)
{
@ -541,22 +477,22 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (curMind == null ||
!TryComp<MindComponent>(curMind.Value, out var mindComp) ||
mindComp.OwnedEntity != antag)
mindComp.OwnedEntity != antagEnt)
{
curMind = _mind.CreateMind(session.UserId, Name(antag));
curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value));
_mind.SetUserId(curMind.Value, session.UserId);
}
_mind.TransferTo(curMind.Value, antag, ghostCheckOverride: true);
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
ent.Comp.AssignedMinds.Add((curMind.Value, Name(antag)));
ent.Comp.AssignedMinds.Add((curMind.Value, Name(player)));
SendBriefing(session, def.Briefing);
Log.Debug($"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
}
var afterEv = new AfterAntagEntitySelectedEvent(session, antag, ent, def);
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
RaiseLocalEvent(ent, ref afterEv, true);
}
@ -681,17 +617,14 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// Event raised on a game rule entity in order to determine what the antagonist entity will be.
/// Only raised if the selected player's current entity is invalid.
/// </summary>
/// TODO: This should really be an interface instead, we're always raising this to the same entity anyways and the values are extremely predictable
[ByRefEvent]
public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule, List<ProtoId<AntagPrototype>> AntagRoles, MapCoordinates Coords)
public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule, List<ProtoId<AntagPrototype>> AntagRoles)
{
public readonly ICommonSession? Session = Session;
/// list of antag role prototypes associated with a entity. used by the <see cref="AntagMultipleRoleSpawnerComponent"/>
public readonly List<ProtoId<AntagPrototype>> AntagRoles = AntagRoles;
public readonly MapCoordinates Coords = Coords;
public bool Handled => Entity != null;
public EntityUid? Entity;
@ -701,20 +634,20 @@ public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<Anta
/// Event raised on a game rule entity to determine the location for the antagonist.
/// </summary>
[ByRefEvent]
public record struct AntagSelectLocationEvent(Entity<AntagSelectionComponent> GameRule, EntityUid? Entity, ICommonSession? Session = null)
public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule, EntityUid Entity)
{
public readonly ICommonSession? Session = Session;
public bool Handled => Coordinates.Any();
// the entity of the antagonist
public EntityUid? Entity = Entity;
public EntityUid Entity = Entity;
public List<MapCoordinates> Coordinates = new();
}
/// <summary>
/// Event raised on a game ruleR entity after the setup logic for an antag is complete.
/// Event raised on a game rule entity after the setup logic for an antag is complete.
/// Used for applying additional more complex setup logic.
/// </summary>
[ByRefEvent]

View file

@ -16,6 +16,6 @@ public sealed class AntagSpawnerSystem : EntitySystem
private void OnSelectEntity(Entity<AntagSpawnerComponent> ent, ref AntagSelectEntityEvent args)
{
args.Entity = Spawn(ent.Comp.Prototype, args.Coords);
args.Entity = Spawn(ent.Comp.Prototype);
}
}

View file

@ -36,7 +36,7 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfile
if (profile?.Species is not { } speciesId || !_proto.Resolve(speciesId, out var species))
{
species = _proto.Index(HumanoidCharacterProfile.DefaultSpecies);
species = _proto.Index<SpeciesPrototype>(HumanoidCharacterProfile.DefaultSpecies);
}
if (ent.Comp.SpeciesOverride != null
@ -45,7 +45,7 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfile
species = _proto.Index(ent.Comp.SpeciesOverride.Value);
}
args.Entity = Spawn(species.Prototype, args.Coords);
args.Entity = Spawn(species.Prototype);
if (profile?.WithSpecies(species.ID) is { } humanoidProfile)
{
_visualBody.ApplyProfileTo(args.Entity.Value, humanoidProfile);

View file

@ -5,9 +5,13 @@
components:
- type: GridSpawnPointWhitelist
whitelist:
components:
- Xenoborg
tags:
- XenoborgGhostrole
blacklist:
components:
- MothershipCore
tags:
- MothershipCoreGhostrole
- type: SpawnPoint
@ -24,6 +28,8 @@
components:
- type: GridSpawnPointWhitelist
whitelist:
components:
- MothershipCore
tags:
- MothershipCoreGhostrole
- type: SpawnPoint