diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs index 7fd6c4e542..6e0df92ad4 100644 --- a/Content.IntegrationTests/PoolManager.cs +++ b/Content.IntegrationTests/PoolManager.cs @@ -13,11 +13,6 @@ public static partial class PoolManager public static readonly ContentPoolManager Instance = new(); public const string TestMap = "Empty"; - /// - /// Designated load bearing station. Sometimes you need a station for a test. - /// - public const string TestStation = "Saltern"; - /// /// Runs a server, or a client until a condition is true /// diff --git a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs index d4d30406ae..bda931397b 100644 --- a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs @@ -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(); diff --git a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs index f7047578ee..02552669f7 100644 --- a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs +++ b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs @@ -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); diff --git a/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs b/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs index 2b454105bf..d59fbc82b4 100644 --- a/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs +++ b/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs @@ -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)); } } diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs index 44d6022e76..5debd10b6e 100644 --- a/Content.Server/Antag/AntagSelectionSystem.API.cs +++ b/Content.Server/Antag/AntagSelectionSystem.API.cs @@ -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. /// - public void ForceMakeAntag(ICommonSession player, string defaultRule) where T : Component + public void ForceMakeAntag(ICommonSession? player, string defaultRule) where T : Component { var rule = ForceGetGameRuleEnt(defaultRule); if (!TryGetNextAvailableDefinition(rule, out var def)) def = rule.Comp.Definitions.Last(); - - MakeSessionAntagonist(rule, player, def.Value); + MakeAntag(rule, player, def.Value); } /// diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index 604a061903..5c94942c9a 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -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; -/// -/// Turns players into antags. -/// -/// -/// 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. -/// public sealed partial class AntagSelectionSystem : GameRuleSystem { [Dependency] private readonly AudioSystem _audio = default!; @@ -96,7 +87,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem(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(ent))); } @@ -185,8 +176,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem /// The session to attempt to make antag. - [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()); + 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 /// Tries to makes a given player into the specified antagonist. /// - public bool TryMakeAntag(Entity ent, ICommonSession session, AntagSelectionDefinition def, bool checkPref = true, bool onlyPreSelect = false) + 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 && !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()); + 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; } /// - /// 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. /// - /// Antag rule entity - /// Antag selection definition chosen from the entity - [PublicAPI] - private EntityUid? CreateAntagSpawner(Entity ent, AntagSelectionDefinition def) + public void MakeAntag(Entity 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()); + 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(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(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; - } - - /// - /// Does antag pre-selection logic, adding a specified player session to the PreSelection list and logging it for admins. - /// - private void PreSelectSessionForAntagonist(Entity ent, ICommonSession session, AntagSelectionDefinition def) - { - if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set)) - ent.Comp.PreSelectedSessions.Add(def, set = new HashSet()); - 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)}"); - } - - /// - /// Creates a new antagonist entity at the specified coordinates, then attaches the specified player to that antagonist. - /// - private EntityUid? AttachSessionToAntagonist(Entity ent, - ICommonSession session, - AntagSelectionDefinition def, - MapCoordinates coords) - { - PreSelectSessionForAntagonist(ent, session, def); - ent.Comp.AssignedSessions.Add(session); - return SpawnNewAntagonist(ent, session, def, coords); - } - - /// - /// 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. - /// - private EntityUid? MakeSessionAntagonist(Entity 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(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. - /// - /// 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. - /// - private EntityUid? SpawnNewAntagonist(Entity 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); - } - - /// - /// 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. - /// - private EntityUid? SpawnNewAntagonist(Entity 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; - } - - /// - /// 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. - /// - private MapCoordinates? GetValidSpawnPosition(Entity 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(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; + } - /// - /// Looks for a valid spawn position for this antagonist type, then moves the antagonist entity to that spawn position. - /// - private bool TryValidSpawnPosition(Entity 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; - } - - /// - /// Initializes the antagonist status on the specified entity. - /// Adds the needed components, loadouts, items, attaches the player and fires off an event. - /// - private void InitializeAntag(Entity 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> 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(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 -/// 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 GameRule, List> AntagRoles, MapCoordinates Coords) +public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity GameRule, List> AntagRoles) { public readonly ICommonSession? Session = Session; /// list of antag role prototypes associated with a entity. used by the public readonly List> 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 [ByRefEvent] -public record struct AntagSelectLocationEvent(Entity GameRule, EntityUid? Entity, ICommonSession? Session = null) +public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity 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 Coordinates = new(); } /// -/// 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. /// [ByRefEvent] diff --git a/Content.Server/Antag/AntagSpawnerSystem.cs b/Content.Server/Antag/AntagSpawnerSystem.cs index dd0c99cc0e..f8a036749a 100644 --- a/Content.Server/Antag/AntagSpawnerSystem.cs +++ b/Content.Server/Antag/AntagSpawnerSystem.cs @@ -16,6 +16,6 @@ public sealed class AntagSpawnerSystem : EntitySystem private void OnSelectEntity(Entity ent, ref AntagSelectEntityEvent args) { - args.Entity = Spawn(ent.Comp.Prototype, args.Coords); + args.Entity = Spawn(ent.Comp.Prototype); } } diff --git a/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs b/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs index fff3ee8de6..22916f0c18 100644 --- a/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs @@ -36,7 +36,7 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem(HumanoidCharacterProfile.DefaultSpecies); } if (ent.Comp.SpeciesOverride != null @@ -45,7 +45,7 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem