diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index ea19a30005..2360ea0bf4 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -114,8 +114,8 @@ public sealed class NukeOpsTest // The game rule exists, and all the stations/shuttles/maps are properly initialized var rule = entMan.AllComponents().Single().Component; - var mapRule = entMan.AllComponents().Single().Component; - foreach (var grid in mapRule.MapGrids) + var gridsRule = entMan.AllComponents().Single().Component; + foreach (var grid in gridsRule.MapGrids) { Assert.That(entMan.EntityExists(grid)); Assert.That(entMan.HasComponent(grid)); @@ -129,7 +129,7 @@ public sealed class NukeOpsTest Assert.That(entMan.EntityExists(nukieShuttlEnt)); EntityUid? nukieStationEnt = null; - foreach (var grid in mapRule.MapGrids) + foreach (var grid in gridsRule.MapGrids) { if (entMan.HasComponent(grid)) { @@ -144,8 +144,8 @@ public sealed class NukeOpsTest Assert.That(entMan.EntityExists(nukieStation.Station)); Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation)); - Assert.That(server.MapMan.MapExists(mapRule.Map)); - var nukieMap = mapSys.GetMap(mapRule.Map!.Value); + Assert.That(server.MapMan.MapExists(gridsRule.Map)); + var nukieMap = mapSys.GetMap(gridsRule.Map!.Value); var targetStation = entMan.GetComponent(rule.TargetStation!.Value); var targetGrid = targetStation.Grids.First(); diff --git a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs index b7aef0c61d..1f0505c60f 100644 --- a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs @@ -1,8 +1,6 @@ +using Content.Server.GameTicking.Rules; using Content.Server.Maps; using Content.Shared.GridPreloader.Prototypes; -using Content.Shared.Storage; -using Content.Shared.Whitelist; -using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -10,25 +8,27 @@ namespace Content.Server.GameTicking.Rules.Components; /// /// This is used for a game rule that loads a map when activated. +/// Works with . /// -[RegisterComponent] +[RegisterComponent, Access(typeof(LoadMapRuleSystem))] public sealed partial class LoadMapRuleComponent : Component { - [DataField] - public MapId? Map; - + /// + /// A to load on a new map. + /// [DataField] public ProtoId? GameMap; + /// + /// A map path to load on a new map. + /// [DataField] public ResPath? MapPath; + /// + /// A to move to a new map. + /// If there are no instances left nothing is done. + /// [DataField] public ProtoId? PreloadedGrid; - - [DataField] - public List MapGrids = new(); - - [DataField] - public EntityWhitelist? SpawnerWhitelist; } diff --git a/Content.Server/GameTicking/Rules/Components/RuleGridsComponent.cs b/Content.Server/GameTicking/Rules/Components/RuleGridsComponent.cs new file mode 100644 index 0000000000..eec6f88815 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/RuleGridsComponent.cs @@ -0,0 +1,30 @@ +using Content.Server.GameTicking.Rules; +using Content.Shared.Whitelist; +using Robust.Shared.Map; + +/// +/// Stores grids created by another gamerule component. +/// With AntagSelection, spawners on these grids can be used for its antags. +/// +[RegisterComponent, Access(typeof(RuleGridsSystem))] +public sealed partial class RuleGridsComponent : Component +{ + /// + /// The map that was loaded. + /// + [DataField] + public MapId? Map; + + /// + /// The grid entities that have been loaded. + /// + [DataField] + public List MapGrids = new(); + + /// + /// Whitelist for a spawner to be considered for an antag. + /// All spawners must have SpawnPointComponent regardless to be found. + /// + [DataField] + public EntityWhitelist? SpawnerWhitelist; +} diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs index cbd981e99e..9ac7a6edbb 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs @@ -126,4 +126,8 @@ public abstract partial class GameRuleSystem where T: IComponent return found; } + protected void ForceEndSelf(EntityUid uid, GameRuleComponent? component = null) + { + GameTicker.EndGameRule(uid, component); + } } diff --git a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs index c60cf2513e..1c09d6e86e 100644 --- a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs @@ -1,9 +1,6 @@ -using Content.Server.Antag; using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.GridPreloader; -using Content.Server.Spawners.Components; -using Content.Shared.Whitelist; using Robust.Server.GameObjects; using Robust.Server.Maps; using Robust.Shared.Map; @@ -14,97 +11,70 @@ namespace Content.Server.GameTicking.Rules; public sealed class LoadMapRuleSystem : GameRuleSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly MapSystem _map = default!; [Dependency] private readonly MapLoaderSystem _mapLoader = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly GridPreloaderSystem _gridPreloader = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnSelectLocation); - SubscribeLocalEvent(OnGridSplit); - } - - private void OnGridSplit(ref GridSplitEvent args) - { - var rule = QueryActiveRules(); - while (rule.MoveNext(out _, out var mapComp, out _)) - { - if (!mapComp.MapGrids.Contains(args.Grid)) - continue; - - mapComp.MapGrids.AddRange(args.NewGrids); - break; - } - } protected override void Added(EntityUid uid, LoadMapRuleComponent comp, GameRuleComponent rule, GameRuleAddedEvent args) { - if (comp.Map != null) + if (comp.PreloadedGrid != null && !_gridPreloader.PreloadingEnabled) + { + // Preloading will never work if it's disabled, duh + Log.Debug($"Immediately ending {ToPrettyString(uid):rule} as preloading grids is disabled by cvar."); + ForceEndSelf(uid, rule); return; + } // grid preloading needs map to init after moving it - var mapUid = comp.PreloadedGrid != null ? _map.CreateMap(out var mapId, false) : _map.CreateMap(out mapId); - _metaData.SetEntityName(mapUid, $"LoadMapRule destination for rule {ToPrettyString(uid)}"); - comp.Map = mapId; + var mapUid = _map.CreateMap(out var mapId, runMapInit: comp.PreloadedGrid == null); + Log.Info($"Created map {mapId} for {ToPrettyString(uid):rule}"); + + IReadOnlyList grids; if (comp.GameMap != null) { var gameMap = _prototypeManager.Index(comp.GameMap.Value); - comp.MapGrids.AddRange(GameTicker.LoadGameMap(gameMap, comp.Map.Value, new MapLoadOptions())); + grids = GameTicker.LoadGameMap(gameMap, mapId, new MapLoadOptions()); } - else if (comp.MapPath != null) + else if (comp.MapPath is {} path) { - if (!_mapLoader.TryLoad(comp.Map.Value, - comp.MapPath.Value.ToString(), - out var roots, - new MapLoadOptions { LoadMap = true })) + var options = new MapLoadOptions { LoadMap = true }; + if (!_mapLoader.TryLoad(mapId, path.ToString(), out var roots, options)) { - _mapManager.DeleteMap(mapId); + Log.Error($"Failed to load map from {path}!"); + Del(mapUid); + ForceEndSelf(uid, rule); return; } - comp.MapGrids.AddRange(roots); + grids = roots; } - else if (comp.PreloadedGrid != null) + else if (comp.PreloadedGrid is {} preloaded) { // TODO: If there are no preloaded grids left, any rule announcements will still go off! - if (!_gridPreloader.TryGetPreloadedGrid(comp.PreloadedGrid.Value, out var loadedShuttle)) + if (!_gridPreloader.TryGetPreloadedGrid(preloaded, out var loadedShuttle)) { - _mapManager.DeleteMap(mapId); + Log.Error($"Failed to get a preloaded grid with {preloaded}!"); + Del(mapUid); + ForceEndSelf(uid, rule); return; } _transform.SetParent(loadedShuttle.Value, mapUid); - comp.MapGrids.Add(loadedShuttle.Value); - _map.InitializeMap(mapId); + grids = new List() { loadedShuttle.Value }; + _map.InitializeMap(mapUid); } else { Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}"); + Del(mapUid); + ForceEndSelf(uid, rule); + return; } - } - private void OnSelectLocation(Entity ent, ref AntagSelectLocationEvent args) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _, out var xform)) - { - if (xform.MapID != ent.Comp.Map) - continue; - - if (xform.GridUid == null || !ent.Comp.MapGrids.Contains(xform.GridUid.Value)) - continue; - - if (_whitelistSystem.IsWhitelistFail(ent.Comp.SpawnerWhitelist, uid)) - continue; - - args.Coordinates.Add(_transform.GetMapCoordinates(xform)); - } + var ev = new RuleLoadedGridsEvent(mapId, grids); + RaiseLocalEvent(uid, ref ev); } } diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index 1b62778d75..6688bfd980 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -260,10 +260,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem { var map = Transform(ent).MapID; - var rules = EntityQueryEnumerator(); - while (rules.MoveNext(out var uid, out _, out var mapRule)) + var rules = EntityQueryEnumerator(); + while (rules.MoveNext(out var uid, out _, out var grids)) { - if (map != mapRule.Map) + if (map != grids.Map) continue; ent.Comp.AssociatedRule = uid; break; @@ -324,7 +324,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (nukeops.WarDeclaredTime != null) continue; - if (TryComp(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map) + if (TryComp(uid, out var grids) && Transform(ev.DeclaratorEntity).MapID != grids.Map) continue; var newStatus = GetWarCondition(nukeops, ev.Status); @@ -445,7 +445,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem // Check that there are spawns available and that they can access the shuttle. var spawnsAvailable = EntityQuery(true).Any(); - if (spawnsAvailable && CompOrNull(ent)?.Map == shuttleMapId) + if (spawnsAvailable && CompOrNull(ent)?.Map == shuttleMapId) return; // Ghost spawns can still access the shuttle. Continue the round. // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, @@ -478,7 +478,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem /// Is this method the shitty glue holding together the last of my sanity? yes. /// Do i have a better solution? not presently. /// - private EntityUid? GetOutpost(Entity ent) + private EntityUid? GetOutpost(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return null; diff --git a/Content.Server/GameTicking/Rules/RuleGridsSystem.cs b/Content.Server/GameTicking/Rules/RuleGridsSystem.cs new file mode 100644 index 0000000000..9eae9e3c95 --- /dev/null +++ b/Content.Server/GameTicking/Rules/RuleGridsSystem.cs @@ -0,0 +1,78 @@ +using Content.Server.Antag; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Spawners.Components; +using Content.Shared.Whitelist; +using Robust.Server.Physics; +using Robust.Shared.Map; + +namespace Content.Server.GameTicking.Rules; + +/// +/// Handles storing grids from and antags spawning on their spawners. +/// +public sealed class RuleGridsSystem : GameRuleSystem +{ + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGridSplit); + + SubscribeLocalEvent(OnLoadedGrids); + SubscribeLocalEvent(OnSelectLocation); + } + + private void OnGridSplit(ref GridSplitEvent args) + { + var rule = QueryActiveRules(); + while (rule.MoveNext(out _, out var comp, out _)) + { + if (!comp.MapGrids.Contains(args.Grid)) + continue; + + comp.MapGrids.AddRange(args.NewGrids); + break; // only 1 rule can own a grid, not multiple + } + } + + private void OnLoadedGrids(Entity ent, ref RuleLoadedGridsEvent args) + { + var (uid, comp) = ent; + if (comp.Map != null && args.Map != comp.Map) + { + Log.Warning($"{ToPrettyString(uid):rule} loaded grids on multiple maps {comp.Map} and {args.Map}, the second will be ignored."); + return; + } + + comp.Map = args.Map; + comp.MapGrids.AddRange(args.Grids); + } + + private void OnSelectLocation(Entity ent, ref AntagSelectLocationEvent args) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var xform)) + { + if (xform.MapID != ent.Comp.Map) + continue; + + if (xform.GridUid is not {} grid || !ent.Comp.MapGrids.Contains(grid)) + continue; + + if (_whitelist.IsWhitelistFail(ent.Comp.SpawnerWhitelist, uid)) + continue; + + args.Coordinates.Add(_transform.GetMapCoordinates(xform)); + } + } +} + +/// +/// Raised by another gamerule system to store loaded grids, and have other systems work with it. +/// A single rule can only load grids for a single map, attempts to load more are ignored. +/// +[ByRefEvent] +public record struct RuleLoadedGridsEvent(MapId Map, IReadOnlyList Grids); diff --git a/Content.Server/GridPreloader/GridPreloaderSystem.cs b/Content.Server/GridPreloader/GridPreloaderSystem.cs index 569fe54141..e12ce41a31 100644 --- a/Content.Server/GridPreloader/GridPreloaderSystem.cs +++ b/Content.Server/GridPreloader/GridPreloaderSystem.cs @@ -24,12 +24,19 @@ public sealed class GridPreloaderSystem : SharedGridPreloaderSystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + /// + /// Whether the preloading CVar is set or not. + /// + public bool PreloadingEnabled; + public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnRoundRestart); SubscribeLocalEvent(OnPostGameMapLoad); + + Subs.CVar(_cfg, CCVars.PreloadGrids, value => PreloadingEnabled = value, true); } private void OnRoundRestart(RoundRestartCleanupEvent ev) @@ -52,7 +59,7 @@ public sealed class GridPreloaderSystem : SharedGridPreloaderSystem if (GetPreloaderEntity() != null) return; - if (!_cfg.GetCVar(CCVars.PreloadGrids)) + if (!PreloadingEnabled) return; var mapUid = _map.CreateMap(out var mapId, false); diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs index 35dc646bce..06dfdd5276 100644 --- a/Content.Server/StationEvents/Events/StationEventSystem.cs +++ b/Content.Server/StationEvents/Events/StationEventSystem.cs @@ -108,13 +108,4 @@ public abstract class StationEventSystem : GameRuleSystem where T : ICompo } } } - - #region Helper Functions - - protected void ForceEndSelf(EntityUid uid, GameRuleComponent? component = null) - { - GameTicker.EndGameRule(uid, component); - } - - #endregion } diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index f421cf6818..d3b0971be0 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -422,6 +422,7 @@ weight: 5.5 minimumPlayers: 20 duration: 1 + - type: RuleGrids - type: LoadMapRule preloadedGrid: ShuttleStriker - type: NukeopsRule diff --git a/Resources/Prototypes/GameRules/midround.yml b/Resources/Prototypes/GameRules/midround.yml index c190b0333a..5a38c22215 100644 --- a/Resources/Prototypes/GameRules/midround.yml +++ b/Resources/Prototypes/GameRules/midround.yml @@ -3,7 +3,6 @@ - type: entity id: Ninja parent: BaseGameRule - noSpawn: true components: - type: GenericAntagRule agentName: ninja-round-end-agent-name diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 3493749457..88f80fd4f6 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -78,6 +78,7 @@ - operationPrefix - operationSuffix - type: NukeopsRule + - type: RuleGrids - type: AntagSelection - type: AntagLoadProfileRule diff --git a/Resources/Prototypes/GameRules/unknown_shuttles.yml b/Resources/Prototypes/GameRules/unknown_shuttles.yml index 359d7bffc0..1ce365cd81 100644 --- a/Resources/Prototypes/GameRules/unknown_shuttles.yml +++ b/Resources/Prototypes/GameRules/unknown_shuttles.yml @@ -1,7 +1,7 @@ -- type: entity - id: UnknownShuttleCargoLost +- type: entity + abstract: true parent: BaseGameRule - noSpawn: true + id: BaseUnknownShuttleRule components: - type: StationEvent startAnnouncement: station-event-unknown-shuttle-incoming @@ -10,65 +10,44 @@ weight: 5 reoccurrenceDelay: 30 duration: 1 + - type: RuleGrids + - type: LoadMapRule + +- type: entity + parent: BaseUnknownShuttleRule + id: UnknownShuttleCargoLost + components: - type: LoadMapRule preloadedGrid: ShuttleCargoLost - type: entity + parent: BaseUnknownShuttleRule id: UnknownShuttleTravelingCuisine - parent: BaseGameRule - noSpawn: true components: - - type: StationEvent - startAnnouncement: station-event-unknown-shuttle-incoming - startAudio: - path: /Audio/Announcements/attention.ogg - weight: 5 - reoccurrenceDelay: 30 - duration: 1 - type: LoadMapRule preloadedGrid: TravelingCuisine - type: entity + parent: BaseUnknownShuttleRule id: UnknownShuttleDisasterEvacPod - parent: BaseGameRule - noSpawn: true components: - - type: StationEvent - startAnnouncement: station-event-unknown-shuttle-incoming - startAudio: - path: /Audio/Announcements/attention.ogg - weight: 5 - reoccurrenceDelay: 30 - duration: 1 - type: LoadMapRule preloadedGrid: DisasterEvacPod - type: entity + parent: BaseUnknownShuttleRule id: UnknownShuttleHonki - parent: BaseGameRule - noSpawn: true components: - type: StationEvent - startAnnouncement: station-event-unknown-shuttle-incoming - startAudio: - path: /Audio/Announcements/attention.ogg weight: 2 - reoccurrenceDelay: 30 - duration: 1 - type: LoadMapRule preloadedGrid: Honki - type: entity + parent: BaseUnknownShuttleRule id: UnknownShuttleSyndieEvacPod - parent: BaseGameRule - noSpawn: true components: - type: StationEvent - startAnnouncement: station-event-unknown-shuttle-incoming - startAudio: - path: /Audio/Announcements/attention.ogg weight: 2 - reoccurrenceDelay: 30 - duration: 1 - type: LoadMapRule preloadedGrid: SyndieEvacPod