diff --git a/Content.IntegrationTests/Tests/Body/LungTest.cs b/Content.IntegrationTests/Tests/Body/LungTest.cs index fbe96b905d..e4802d6fad 100644 --- a/Content.IntegrationTests/Tests/Body/LungTest.cs +++ b/Content.IntegrationTests/Tests/Body/LungTest.cs @@ -64,7 +64,7 @@ namespace Content.IntegrationTests.Tests.Body MetabolizerSystem metaSys = default; MapId mapId; - IMapGrid grid = null; + GridId? grid = null; SharedBodyComponent body = default; EntityUid human = default; GridAtmosphereComponent relevantAtmos = default; @@ -75,7 +75,7 @@ namespace Content.IntegrationTests.Tests.Body await server.WaitPost(() => { mapId = mapManager.CreateMap(); - grid = mapLoader.LoadBlueprint(mapId, testMapName); + grid = mapLoader.LoadBlueprint(mapId, testMapName).gridId; }); Assert.NotNull(grid, $"Test blueprint {testMapName} not found."); @@ -94,11 +94,12 @@ namespace Content.IntegrationTests.Tests.Body await server.WaitAssertion(() => { var coords = new Vector2(0.5f, -1f); - var coordinates = new EntityCoordinates(grid.GridEntityId, coords); + var geid = mapManager.GetGridEuid(grid.Value); + var coordinates = new EntityCoordinates(geid, coords); human = entityManager.SpawnEntity("HumanBodyDummy", coordinates); respSys = EntitySystem.Get(); metaSys = EntitySystem.Get(); - relevantAtmos = entityManager.GetComponent(grid.GridEntityId); + relevantAtmos = entityManager.GetComponent(geid); startingMoles = GetMapMoles(); Assert.True(entityManager.TryGetComponent(human, out body)); @@ -140,7 +141,7 @@ namespace Content.IntegrationTests.Tests.Body var entityManager = server.ResolveDependency(); MapId mapId; - IMapGrid grid = null; + GridId? grid = null; RespiratorComponent respirator = null; EntityUid human = default; @@ -149,7 +150,7 @@ namespace Content.IntegrationTests.Tests.Body await server.WaitPost(() => { mapId = mapManager.CreateMap(); - grid = mapLoader.LoadBlueprint(mapId, testMapName); + grid = mapLoader.LoadBlueprint(mapId, testMapName).gridId; }); Assert.NotNull(grid, $"Test blueprint {testMapName} not found."); @@ -157,7 +158,8 @@ namespace Content.IntegrationTests.Tests.Body await server.WaitAssertion(() => { var center = new Vector2(0.5f, -1.5f); - var coordinates = new EntityCoordinates(grid.GridEntityId, center); + var geid = mapManager.GetGridEuid(grid.Value); + var coordinates = new EntityCoordinates(geid, center); human = entityManager.SpawnEntity("HumanBodyDummy", coordinates); Assert.True(entityManager.HasComponent(human)); diff --git a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs index cfba1ee3e5..3d8692bf9c 100644 --- a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs @@ -59,15 +59,15 @@ public sealed class FluidSpill : ContentIntegrationTest var spillSystem = server.ResolveDependency().GetEntitySystem(); var gameTiming = server.ResolveDependency(); MapId mapId; - IMapGrid? grid = null; + GridId? gridid = null; await server.WaitPost(() => { mapId = mapManager.CreateMap(); - grid = mapLoader.LoadBlueprint(mapId, SpillMapsYml)!; + gridid = mapLoader.LoadBlueprint(mapId, SpillMapsYml).gridId; }); - if (grid == null) + if (gridid == null) { Assert.Fail($"Test blueprint {SpillMapsYml} not found."); return; @@ -75,6 +75,7 @@ public sealed class FluidSpill : ContentIntegrationTest await server.WaitAssertion(() => { + var grid = mapManager.GetGrid(gridid.Value); var solution = new Solution("Water", FixedPoint2.New(100)); var tileRef = grid.GetTileRef(_origin); var puddle = spillSystem.SpillAt(tileRef, solution, "PuddleSmear"); @@ -87,6 +88,7 @@ public sealed class FluidSpill : ContentIntegrationTest server.Assert(() => { + var grid = mapManager.GetGrid(gridid.Value); var puddle = GetPuddle(entityManager, grid, _origin); Assert.That(puddle, Is.Not.Null); @@ -118,15 +120,15 @@ public sealed class FluidSpill : ContentIntegrationTest var spillSystem = server.ResolveDependency().GetEntitySystem(); var gameTiming = server.ResolveDependency(); MapId mapId; - IMapGrid? grid = null; + GridId? gridId = null; await server.WaitPost(() => { mapId = mapManager.CreateMap(); - grid = mapLoader.LoadBlueprint(mapId, SpillMapsYml)!; + gridId = mapLoader.LoadBlueprint(mapId, SpillMapsYml).gridId; }); - if (grid == null) + if (gridId == null) { Assert.Fail($"Test blueprint {SpillMapsYml} not found."); return; @@ -135,14 +137,14 @@ public sealed class FluidSpill : ContentIntegrationTest await server.WaitAssertion(() => { var solution = new Solution("Water", FixedPoint2.New(20.01)); - + var grid = mapManager.GetGrid(gridId.Value); var tileRef = grid.GetTileRef(_origin); var puddle = spillSystem.SpillAt(tileRef, solution, "PuddleSmear"); Assert.That(puddle, Is.Not.Null); }); - if (grid == null) + if (gridId == null) { Assert.Fail($"Test blueprint {SpillMapsYml} not found."); return; @@ -153,6 +155,7 @@ public sealed class FluidSpill : ContentIntegrationTest server.Assert(() => { + var grid = mapManager.GetGrid(gridId.Value); var puddle = GetPuddle(entityManager, grid, _origin); Assert.That(puddle, Is.Not.Null); Assert.That(puddle!.CurrentVolume, Is.EqualTo(FixedPoint2.New(20))); diff --git a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs index 0054addfd5..9a05be27a3 100644 --- a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs @@ -28,8 +28,8 @@ namespace Content.IntegrationTests.Tests // TODO: Un-hardcode the grid Id for this test. mapLoader.SaveBlueprint(new GridId(1), "save load save 1.yml"); var mapId = mapManager.CreateMap(); - var grid = mapLoader.LoadBlueprint(mapId, "save load save 1.yml"); - mapLoader.SaveBlueprint(grid!.Index, "save load save 2.yml"); + var grid = mapLoader.LoadBlueprint(mapId, "save load save 1.yml").gridId; + mapLoader.SaveBlueprint(grid!.Value, "save load save 2.yml"); }); await server.WaitIdleAsync(); @@ -83,7 +83,7 @@ namespace Content.IntegrationTests.Tests var mapLoader = server.ResolveDependency(); var mapManager = server.ResolveDependency(); - IMapGrid grid = default; + GridId? grid = default; // Load saltern.yml as uninitialized map, and save it to ensure it's up to date. server.Post(() => @@ -91,8 +91,8 @@ namespace Content.IntegrationTests.Tests var mapId = mapManager.CreateMap(); mapManager.AddUninitializedMap(mapId); mapManager.SetMapPaused(mapId, true); - grid = mapLoader.LoadBlueprint(mapId, "Maps/saltern.yml"); - mapLoader.SaveBlueprint(grid.Index, "load save ticks save 1.yml"); + grid = mapLoader.LoadBlueprint(mapId, "Maps/saltern.yml").gridId; + mapLoader.SaveBlueprint(grid!.Value, "load save ticks save 1.yml"); }); // Run 5 ticks. @@ -100,7 +100,7 @@ namespace Content.IntegrationTests.Tests server.Post(() => { - mapLoader.SaveBlueprint(grid.Index, "/load save ticks save 2.yml"); + mapLoader.SaveBlueprint(grid!.Value, "/load save ticks save 2.yml"); }); await server.WaitIdleAsync(); diff --git a/Content.Server/Administration/Commands/LoadGameMapCommand.cs b/Content.Server/Administration/Commands/LoadGameMapCommand.cs index 873705d83a..d107a75cdd 100644 --- a/Content.Server/Administration/Commands/LoadGameMapCommand.cs +++ b/Content.Server/Administration/Commands/LoadGameMapCommand.cs @@ -1,14 +1,9 @@ +using Content.Server.GameTicking; using Content.Server.Maps; -using Content.Server.Roles; -using Content.Server.Station; using Content.Shared.Administration; using Robust.Server.Maps; using Robust.Shared.Console; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; namespace Content.Server.Administration.Commands @@ -25,9 +20,8 @@ namespace Content.Server.Administration.Commands public void Execute(IConsoleShell shell, string argStr, string[] args) { var prototypeManager = IoCManager.Resolve(); - var mapLoader = IoCManager.Resolve(); var entityManager = IoCManager.Resolve(); - var stationSystem = entityManager.EntitySysManager.GetEntitySystem(); + var gameTicker = entityManager.EntitySysManager.GetEntitySystem(); if (args.Length is not (2 or 4 or 5)) { @@ -37,25 +31,17 @@ namespace Content.Server.Administration.Commands if (prototypeManager.TryIndex(args[0], out var gameMap)) { - if (int.TryParse(args[1], out var mapId)) + if (!int.TryParse(args[1], out var mapId)) return; + + var loadOptions = new MapLoadOptions(); + var stationName = args.Length == 5 ? args[4] : null; + + if (args.Length >= 4 && int.TryParse(args[2], out var x) && int.TryParse(args[3], out var y)) { - var gameMapEnt = mapLoader.LoadBlueprint(new MapId(mapId), gameMap.MapPath.ToString()); - if (gameMapEnt is null) - { - shell.WriteError($"Failed to create the given game map, is the path {gameMap.MapPath} correct?"); - return; - } - - if (args.Length >= 4 && int.TryParse(args[2], out var x) && int.TryParse(args[3], out var y)) - { - var transform = entityManager.GetComponent(gameMapEnt.GridEntityId); - transform.WorldPosition = new Vector2(x, y); - } - - var stationName = args.Length == 5 ? args[4] : null; - - stationSystem.InitialSetupStationGrid(gameMapEnt.GridEntityId, gameMap, stationName); + loadOptions.Offset = new Vector2(x, y); } + var (ents, grids) = gameTicker.LoadGameMap(gameMap, new MapId(mapId), loadOptions, stationName); + shell.WriteLine($"Loaded {ents.Count} entities and {grids.Count} grids."); } else { diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 3d14b5c61d..1f5f1166df 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -11,7 +11,9 @@ using Content.Shared.Coordinates; using Content.Shared.GameTicking; using Content.Shared.Preferences; using Content.Shared.Station; +using JetBrains.Annotations; using Prometheus; +using Robust.Server.Maps; using Robust.Server.Player; using Robust.Shared.Map; using Robust.Shared.Network; @@ -64,6 +66,12 @@ namespace Content.Server.GameTicking [ViewVariables] public int RoundId { get; private set; } + /// + /// Loads all the maps for the given round. + /// + /// + /// Must be called before the runlevel is set to InRound. + /// private void LoadMaps() { AddGamePresetRules(); @@ -86,85 +94,36 @@ namespace Content.Server.GameTicking _mapManager.AddUninitializedMap(toLoad); } - _mapLoader.LoadMap(toLoad, map.MapPath.ToString()); - - var grids = _mapManager.GetAllMapGrids(toLoad).ToList(); - var dict = new Dictionary(); - - StationId SetupInitialStation(IMapGrid grid, GameMapPrototype map) - { - var stationId = _stationSystem.InitialSetupStationGrid(grid.GridEntityId, map); - SetupGridStation(grid); - - // ass! - _spawnPoint = grid.ToCoordinates(); - return stationId; - } - - // Iterate over all BecomesStation - for (var i = 0; i < grids.Count; i++) - { - var grid = grids[i]; - - // We still setup the grid - if (!TryComp(grid.GridEntityId, out var becomesStation)) - continue; - - var stationId = SetupInitialStation(grid, map); - - dict.Add(becomesStation.Id, stationId); - } - - if (!dict.Any()) - { - // Oh jeez, no stations got loaded. - // We'll just take the first grid and setup that, then. - - var grid = grids[0]; - var stationId = SetupInitialStation(grid, map); - - dict.Add("Station", stationId); - } - - // Iterate over all PartOfStation - for (var i = 0; i < grids.Count; i++) - { - var grid = grids[i]; - if (!TryComp(grid.GridEntityId, out var partOfStation)) - continue; - SetupGridStation(grid); - - if (dict.TryGetValue(partOfStation.Id, out var stationId)) - { - _stationSystem.AddGridToStation(grid.GridEntityId, stationId); - } - else - { - _sawmill.Error($"Grid {grid.Index} ({grid.GridEntityId}) specified that it was part of station {partOfStation.Id} which does not exist"); - } - } + LoadGameMap(map, toLoad, null); } var timeSpan = _gameTiming.RealTime - startTime; _sawmill.Info($"Loaded maps in {timeSpan.TotalMilliseconds:N2}ms."); } - private void SetupGridStation(IMapGrid grid) + + /// + /// Loads a new map, allowing systems interested in it to handle loading events. + /// In the base game, this is required to be used if you want to load a station. + /// + /// Game map prototype to load in. + /// Map to load into. + /// Map loading options, includes offset. + /// Name to assign to the loaded station. + /// All loaded entities and grids. + public (IReadOnlyList, IReadOnlyList) LoadGameMap(GameMapPrototype map, MapId targetMapId, MapLoadOptions? loadOptions, string? stationName = null) { - var stationXform = EntityManager.GetComponent(grid.GridEntityId); + var loadOpts = loadOptions ?? new MapLoadOptions(); - if (StationOffset) - { - // Apply a random offset to the station grid entity. - var x = _robustRandom.NextFloat(-MaxStationOffset, MaxStationOffset); - var y = _robustRandom.NextFloat(-MaxStationOffset, MaxStationOffset); - stationXform.LocalPosition = new Vector2(x, y); - } + var ev = new PreGameMapLoad(targetMapId, map, loadOpts); + RaiseLocalEvent(ev); - if (StationRotation) - { - stationXform.LocalRotation = _robustRandom.NextFloat(MathF.Tau); - } + var (entities, gridIds) = _mapLoader.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options); + + RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, entities, gridIds, stationName)); + + _spawnPoint = _mapManager.GetGrid(gridIds[0]).ToCoordinates(); + return (entities, gridIds); } public void StartRound(bool force = false) @@ -532,6 +491,7 @@ namespace Content.Server.GameTicking /// Contains a list of game map prototypes to load; modify it if you want to load different maps, /// for example as part of a game rule. /// + [PublicAPI] public sealed class LoadingMapsEvent : EntityEventArgs { public List Maps; @@ -542,6 +502,54 @@ namespace Content.Server.GameTicking } } + /// + /// Event raised before the game loads a given map. + /// This event is mutable, and load options should be tweaked if necessary. + /// + /// + /// You likely want to subscribe to this after StationSystem. + /// + [PublicAPI] + public sealed class PreGameMapLoad : EntityEventArgs + { + public readonly MapId Map; + public GameMapPrototype GameMap; + public MapLoadOptions Options; + + public PreGameMapLoad(MapId map, GameMapPrototype gameMap, MapLoadOptions options) + { + Map = map; + GameMap = gameMap; + Options = options; + } + } + + + /// + /// Event raised after the game loads a given map. + /// + /// + /// You likely want to subscribe to this after StationSystem. + /// + [PublicAPI] + public sealed class PostGameMapLoad : EntityEventArgs + { + public readonly GameMapPrototype GameMap; + public readonly MapId Map; + public readonly IReadOnlyList Entities; + public readonly IReadOnlyList Grids; + public readonly string? StationName; + + public PostGameMapLoad(GameMapPrototype gameMap, MapId map, IReadOnlyList entities, IReadOnlyList grids, string? stationName) + { + GameMap = gameMap; + Map = map; + Entities = entities; + Grids = grids; + StationName = stationName; + } + } + /// /// Event raised to refresh the late join status. /// If you want to disallow late joins, listen to this and call Disallow. diff --git a/Content.Server/Salvage/SalvageSystem.cs b/Content.Server/Salvage/SalvageSystem.cs index 928379c095..383ebe5d8f 100644 --- a/Content.Server/Salvage/SalvageSystem.cs +++ b/Content.Server/Salvage/SalvageSystem.cs @@ -276,17 +276,22 @@ namespace Content.Server.Salvage Report("salvage-system-announcement-spawn-no-debris-available"); return false; } - var bp = _mapLoader.LoadBlueprint(spl.MapId, map.MapPath.ToString()); - if (bp == null) + + var opts = new MapLoadOptions + { + Offset = spl.Position + }; + + var (_, gridId) = _mapLoader.LoadBlueprint(spl.MapId, map.MapPath.ToString(), opts); + if (gridId == null) { Report("salvage-system-announcement-spawn-debris-disintegrated"); return false; } - var salvageEntityId = bp.GridEntityId; + var salvageEntityId = _mapManager.GetGridEuid(gridId.Value); component.AttachedEntity = salvageEntityId; var pulledTransform = EntityManager.GetComponent(salvageEntityId); - pulledTransform.Coordinates = EntityCoordinates.FromMap(_mapManager, spl); pulledTransform.WorldRotation = spAngle; Report("salvage-system-announcement-arrived", ("timeLeft", HoldTime.TotalSeconds)); diff --git a/Content.Server/Station/StationSystem.cs b/Content.Server/Station/StationSystem.cs index d6956d2fd5..6c8ee31d34 100644 --- a/Content.Server/Station/StationSystem.cs +++ b/Content.Server/Station/StationSystem.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; using System.Linq; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Server.Maps; +using Content.Shared.CCVar; using Content.Shared.Roles; using Content.Shared.Station; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; +using Robust.Shared.Configuration; +using Robust.Shared.Map; +using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.Station; @@ -18,21 +17,101 @@ namespace Content.Server.Station; /// public sealed class StationSystem : EntitySystem { - [Dependency] private GameTicker _gameTicker = default!; - [Dependency] private IChatManager _chatManager = default!; - [Dependency] private IGameMapManager _gameMapManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly IGameMapManager _gameMapManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + + private ISawmill _sawmill = default!; + private uint _idCounter = 1; private Dictionary _stationInfo = new(); + /// - /// List of stations for the current round. + /// List of stations currently loaded. /// public IReadOnlyDictionary StationInfo => _stationInfo; + private bool _randomStationOffset = false; + private bool _randomStationRotation = false; + private float _maxRandomStationOffset = 0.0f; + public override void Initialize() { - base.Initialize(); + _sawmill = _logManager.GetSawmill("station"); + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnPreGameMapLoad); + SubscribeLocalEvent(OnPostGameMapLoad); + + _configurationManager.OnValueChanged(CCVars.StationOffset, x => _randomStationOffset = x, true); + _configurationManager.OnValueChanged(CCVars.MaxStationOffset, x => _maxRandomStationOffset = x, true); + _configurationManager.OnValueChanged(CCVars.StationRotation, x => _randomStationRotation = x, true); + } + + private void OnPreGameMapLoad(PreGameMapLoad ev) + { + // this is only for maps loaded during round setup! + if (_gameTicker.RunLevel == GameRunLevel.InRound) + return; + + if (_randomStationOffset) + ev.Options.Offset += _random.NextVector2(_maxRandomStationOffset); + + if (_randomStationRotation) + ev.Options.Rotation = _random.NextAngle(); + } + + private void OnPostGameMapLoad(PostGameMapLoad ev) + { + var dict = new Dictionary(); + + // Iterate over all BecomesStation + for (var i = 0; i < ev.Grids.Count; i++) + { + var grid = ev.Grids[i]; + + // We still setup the grid + if (!TryComp(_mapManager.GetGridEuid(grid), out var becomesStation)) + continue; + + var stationId = InitialSetupStationGrid(grid, ev.GameMap, ev.StationName); + + dict.Add(becomesStation.Id, stationId); + } + + if (!dict.Any()) + { + // Oh jeez, no stations got loaded. + // We'll just take the first grid and setup that, then. + + var grid = ev.Grids[0]; + var stationId = InitialSetupStationGrid(grid, ev.GameMap, ev.StationName); + + dict.Add("Station", stationId); + } + + // Iterate over all PartOfStation + for (var i = 0; i < ev.Grids.Count; i++) + { + var grid = ev.Grids[i]; + var geid = _mapManager.GetGridEuid(grid); + if (!TryComp(geid, out var partOfStation)) + continue; + + if (dict.TryGetValue(partOfStation.Id, out var stationId)) + { + AddGridToStation(geid, stationId); + } + else + { + _sawmill.Error($"Grid {grid} ({geid}) specified that it was part of station {partOfStation.Id} which does not exist"); + } + } } /// @@ -119,8 +198,7 @@ public sealed class StationSystem : EntitySystem _gameTicker.UpdateJobsAvailable(); // new station means new jobs, tell any lobby-goers. - Logger.InfoS("stations", - $"Setting up new {mapPrototype.ID} called {_stationInfo[id].Name} on grid {mapGrid}:{gridComponent.GridIndex}"); + _sawmill.Info($"Setting up new {mapPrototype.ID} called {_stationInfo[id].Name} on grid {mapGrid}:{gridComponent.GridIndex}"); return id; } @@ -139,7 +217,7 @@ public sealed class StationSystem : EntitySystem var stationComponent = EntityManager.AddComponent(mapGrid); stationComponent.Station = station; - Logger.InfoS("stations", $"Adding grid {mapGrid}:{gridComponent.GridIndex} to station {station} named {_stationInfo[station].Name}"); + _sawmill.Info( $"Adding grid {mapGrid}:{gridComponent.GridIndex} to station {station} named {_stationInfo[station].Name}"); } /// diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 2c09731c78..b3baf8c79f 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -173,7 +173,7 @@ namespace Content.Shared.CCVar /// Whether a random rotation will be applied to the station on roundstart. /// public static readonly CVarDef StationRotation = - CVarDef.Create("game.station_rotation", false); + CVarDef.Create("game.station_rotation", true); /// /// When enabled, guests will be assigned permanent UIDs and will have their preferences stored. @@ -428,7 +428,7 @@ namespace Content.Shared.CCVar /// /// Large nukes tend to generate a lot of shrapnel that flies through space. This can functionally cripple /// the server TPS for a while after an explosion (or even during, if the explosion is processed - /// incrementally. + /// incrementally. /// public static readonly CVarDef ExplosionThrowLimit = CVarDef.Create("explosion.throw_limit", 400, CVar.SERVERONLY);