using System.Linq; using Content.Server.Chat; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Server.Station.Components; using Content.Shared.CCVar; using JetBrains.Annotations; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Random; namespace Content.Server.Station.Systems; /// /// System that manages stations. /// A station is, by default, just a name, optional map prototype, and optional grids. /// For jobs, look at StationJobSystem. For spawning, look at StationSpawningSystem. /// [PublicAPI] public sealed class StationSystem : EntitySystem { [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly GameTicker _gameTicker = default!; private ISawmill _sawmill = default!; private readonly HashSet _stations = new(); /// /// All stations that currently exist. /// /// /// I'd have this just invoke an entity query, but I want this to be a hashset for convenience and it allocating on use would be lame. /// public IReadOnlySet Stations => _stations; private bool _randomStationOffset; private bool _randomStationRotation; private float _maxRandomStationOffset; /// public override void Initialize() { _sawmill = _logManager.GetSawmill("station"); SubscribeLocalEvent(OnRoundEnd); SubscribeLocalEvent(OnPreGameMapLoad); SubscribeLocalEvent(OnPostGameMapLoad); SubscribeLocalEvent(OnStationStartup); SubscribeLocalEvent(OnStationDeleted); _configurationManager.OnValueChanged(CCVars.StationOffset, x => _randomStationOffset = x, true); _configurationManager.OnValueChanged(CCVars.MaxStationOffset, x => _maxRandomStationOffset = x, true); _configurationManager.OnValueChanged(CCVars.StationRotation, x => _randomStationRotation = x, true); } #region Event handlers private void OnStationStartup(EntityUid uid, StationDataComponent component, ComponentAdd args) { _stations.Add(uid); } private void OnStationDeleted(EntityUid uid, StationDataComponent component, ComponentShutdown args) { _stations.Remove(uid); } 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>(); void AddGrid(string station, EntityUid grid) { if (dict.ContainsKey(station)) { dict[station].Add(grid); } else { dict[station] = new List {grid}; } } // Iterate over all BecomesStation foreach (var grid in ev.Grids) { // We still setup the grid if (!TryComp(grid, out var becomesStation)) continue; AddGrid(becomesStation.Id, grid); } 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]; AddGrid("Station", grid); } // Iterate over all PartOfStation foreach (var grid in ev.Grids) { if (!TryComp(grid, out var partOfStation)) continue; AddGrid(partOfStation.Id, grid); } foreach (var (id, gridIds) in dict) { StationConfig? stationConfig = null; if (ev.GameMap.Stations.ContainsKey(id)) stationConfig = ev.GameMap.Stations[id]; else _sawmill.Error($"The station {id} in map {ev.GameMap.ID} does not have an associated station config!"); InitializeNewStation(stationConfig, gridIds, ev.StationName); } } private void OnRoundEnd(GameRunLevelChangedEvent eventArgs) { if (eventArgs.New != GameRunLevel.PreRoundLobby) return; foreach (var entity in _stations) { Del(entity); } } #endregion Event handlers /// /// Generates a station name from the given config. /// /// /// public static string GenerateStationName(StationConfig config) { return config.NameGenerator is not null ? config.NameGenerator.FormatName(config.StationNameTemplate) : config.StationNameTemplate; } /// /// Initializes a new station with the given information. /// /// The game map prototype used, if any. /// All grids that should be added to the station. /// Optional override for the station name. /// The initialized station. public EntityUid InitializeNewStation(StationConfig? stationConfig, IEnumerable? gridIds, string? name = null) { //HACK: This needs to go in null-space but that crashes currently. var station = Spawn(null, new MapCoordinates(0, 0, _gameTicker.DefaultMap)); var data = AddComp(station); var metaData = MetaData(station); data.StationConfig = stationConfig; if (stationConfig is not null && name is null) { metaData.EntityName = GenerateStationName(stationConfig); } else if (name is not null) { metaData.EntityName = name; } else { _sawmill.Error($"When setting up station {station}, was unable to find a valid name in the config and no name was provided."); metaData.EntityName = "unnamed station"; } RaiseLocalEvent(new StationInitializedEvent(station)); _sawmill.Info($"Set up station {metaData.EntityName} ({station})."); foreach (var grid in gridIds ?? Array.Empty()) { AddGridToStation(station, grid, null, data); } return station; } /// /// Adds the given grid to a station. /// /// Grid to attach. /// Station to attach the grid to. /// Resolve pattern, grid component of mapGrid. /// Resolve pattern, station data component of station. /// Thrown when mapGrid or station are not a grid or station, respectively. public void AddGridToStation(EntityUid station, EntityUid mapGrid, IMapGridComponent? gridComponent = null, StationDataComponent? stationData = null) { if (!Resolve(mapGrid, ref gridComponent)) throw new ArgumentException("Tried to initialize a station on a non-grid entity!", nameof(mapGrid)); if (!Resolve(station, ref stationData)) throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); var stationMember = AddComp(mapGrid); stationMember.Station = station; stationData.Grids.Add(gridComponent.Owner); RaiseLocalEvent(station, new StationGridAddedEvent(gridComponent.Owner, false)); _sawmill.Info($"Adding grid {mapGrid}:{gridComponent.Owner} to station {Name(station)} ({station})"); } /// /// Removes the given grid from a station. /// /// Station to remove the grid from. /// Grid to remove /// Resolve pattern, grid component of mapGrid. /// Resolve pattern, station data component of station. /// Thrown when mapGrid or station are not a grid or station, respectively. public void RemoveGridFromStation(EntityUid station, EntityUid mapGrid, IMapGridComponent? gridComponent = null, StationDataComponent? stationData = null) { if (!Resolve(mapGrid, ref gridComponent)) throw new ArgumentException("Tried to initialize a station on a non-grid entity!", nameof(mapGrid)); if (!Resolve(station, ref stationData)) throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); RemComp(mapGrid); stationData.Grids.Remove(gridComponent.Owner); RaiseLocalEvent(station, new StationGridRemovedEvent(gridComponent.Owner)); _sawmill.Info($"Removing grid {mapGrid}:{gridComponent.Owner} from station {Name(station)} ({station})"); } /// /// Renames the given station. /// /// Station to rename. /// The new name to apply. /// Whether or not to announce the rename. /// Resolve pattern, station data component of station. /// Resolve pattern, metadata component of station. /// Thrown when the given station is not a station. public void RenameStation(EntityUid station, string name, bool loud = true, StationDataComponent? stationData = null, MetaDataComponent? metaData = null) { if (!Resolve(station, ref stationData, ref metaData)) throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); var oldName = metaData.EntityName; metaData.EntityName = name; if (loud) { _chatSystem.DispatchStationAnnouncement(station, $"The station {oldName} has been renamed to {name}."); } RaiseLocalEvent(station, new StationRenamedEvent(oldName, name)); } /// /// Deletes the given station. /// /// Station to delete. /// Resolve pattern, station data component of station. /// Thrown when the given station is not a station. public void DeleteStation(EntityUid station, StationDataComponent? stationData = null) { if (!Resolve(station, ref stationData)) throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); _stations.Remove(station); Del(station); } /// /// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to) /// /// Entity to find the owner of. /// Resolve pattern, transform of the entity. /// The owning station, if any. /// /// This does not remember what station an entity started on, it simply checks where it is currently located. /// public EntityUid? GetOwningStation(EntityUid entity, TransformComponent? xform = null) { if (!Resolve(entity, ref xform)) throw new ArgumentException("Tried to use an abstract entity!", nameof(entity)); if (TryComp(entity, out _)) { // We are the station, just return ourselves. return entity; } if (TryComp(entity, out _)) { // We are the station, just check ourselves. return CompOrNull(entity)?.Station; } if (xform.GridEntityId == EntityUid.Invalid) { Logger.Debug("A"); return null; } return CompOrNull(xform.GridEntityId)?.Station; } } /// /// Broadcast event fired when a station is first set up. /// This is the ideal point to add components to it. /// [PublicAPI] public sealed class StationInitializedEvent : EntityEventArgs { /// /// Station this event is for. /// public EntityUid Station; public StationInitializedEvent(EntityUid station) { Station = station; } } /// /// Directed event fired on a station when a grid becomes a member of the station. /// [PublicAPI] public sealed class StationGridAddedEvent : EntityEventArgs { /// /// ID of the grid added to the station. /// public EntityUid GridId; /// /// Indicates that the event was fired during station setup, /// so that it can be ignored if StationInitializedEvent was already handled. /// public bool IsSetup; public StationGridAddedEvent(EntityUid gridId, bool isSetup) { GridId = gridId; IsSetup = isSetup; } } /// /// Directed event fired on a station when a grid is no longer a member of the station. /// [PublicAPI] public sealed class StationGridRemovedEvent : EntityEventArgs { /// /// ID of the grid removed from the station. /// public EntityUid GridId; public StationGridRemovedEvent(EntityUid gridId) { GridId = gridId; } } /// /// Directed event fired on a station when it is renamed. /// [PublicAPI] public sealed class StationRenamedEvent : EntityEventArgs { /// /// Prior name of the station. /// public string OldName; /// /// New name of the station. /// public string NewName; public StationRenamedEvent(string oldName, string newName) { OldName = oldName; NewName = newName; } }