using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Systems; using Content.Server.GameTicking; using Content.Server.Station.Events; using Content.Shared.GameTicking; using Content.Shared.Station; using Content.Shared.Station.Components; using JetBrains.Annotations; using Robust.Server.GameStates; using Robust.Server.Player; using Robust.Shared.Enums; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Player; using Robust.Shared.Utility; 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 partial class ServerStationSystem : Shared.Station.Systems.StationSystem { [Dependency] private IPlayerManager _player = default!; [Dependency] private AtmosphereSystem _atmos = default!; [Dependency] private ChatSystem _chatSystem = default!; [Dependency] private PvsOverrideSystem _pvsOverride = default!; private ISawmill _sawmill = default!; /// public override void Initialize() { base.Initialize(); _sawmill = LogManager.GetSawmill("station"); SubscribeLocalEvent(OnRoundEnd); SubscribeLocalEvent(OnPostGameMapLoad); SubscribeLocalEvent(OnStationAdd); SubscribeLocalEvent(OnStationDeleted); SubscribeLocalEvent(OnStationGridDeleted); SubscribeLocalEvent(OnStationSplitEvent); SubscribeLocalEvent(OnStationGridAdded); SubscribeLocalEvent(OnStationGridRemoved); _player.PlayerStatusChanged += OnPlayerStatusChanged; } private void OnStationSplitEvent(EntityUid uid, StationMemberComponent component, ref PostGridSplitEvent args) { AddGridToStation(component.Station, args.Grid); // Add the new grid as a member. } private void OnStationGridDeleted(EntityUid uid, StationMemberComponent component, ComponentShutdown args) { if (!TryComp(component.Station, out var stationData)) return; stationData.Grids.Remove(uid); Dirty(component.Station, stationData); } public override void Shutdown() { base.Shutdown(); _player.PlayerStatusChanged -= OnPlayerStatusChanged; } private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { if (e.NewStatus == SessionStatus.Connected) { RaiseNetworkEvent(new StationsUpdatedEvent(GetStationNames()), e.Session); } } private void UpdateTrackersOnGrid(EntityUid gridId, EntityUid? station) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var tracker, out var xform)) { if (xform.GridUid == gridId) { SetStation((uid, tracker), station); } } } #region Event handlers private void OnStationAdd(EntityUid uid, StationDataComponent component, ComponentStartup args) { RaiseNetworkEvent(new StationsUpdatedEvent(GetStationNames()), Filter.Broadcast()); var metaData = MetaData(uid); RaiseLocalEvent(new StationInitializedEvent(uid)); _sawmill.Info($"Set up station {metaData.EntityName} ({uid})."); _pvsOverride.AddGlobalOverride(uid); } private void OnStationDeleted(EntityUid uid, StationDataComponent component, ComponentShutdown args) { foreach (var grid in component.Grids) { RemComp(grid); // If the station gets deleted, we raise the event for every grid that was a part of it RaiseLocalEvent(new StationGridRemovedEvent(grid, uid)); } RaiseNetworkEvent(new StationsUpdatedEvent(GetStationNames()), Filter.Broadcast()); } private void OnPostGameMapLoad(PostGameMapLoad ev) { var dict = new Dictionary>(); // Iterate over all BecomesStation foreach (var grid in ev.Grids) { // We still setup the grid if (TryComp(grid, out var becomesStation)) dict.GetOrNew(becomesStation.Id).Add(grid); } if (!dict.Any()) { // Oh jeez, no stations got loaded. // We'll yell about it, but the thing this used to do with creating a dummy is kinda pointless now. _sawmill.Error($"There were no station grids for {ev.GameMap.ID}!"); } foreach (var (id, gridIds) in dict) { StationConfig stationConfig; 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!"); continue; } InitializeNewStation(stationConfig, gridIds, ev.StationName, ev.GameMap); } } private void OnRoundEnd(GameRunLevelChangedEvent eventArgs) { if (eventArgs.New != GameRunLevel.PreRoundLobby) return; var query = EntityQueryEnumerator(); while (query.MoveNext(out var station, out _)) { QueueDel(station); } } private void OnStationGridAdded(StationGridAddedEvent ev) { // When a grid is added to a station, update all trackers on that grid UpdateTrackersOnGrid(ev.GridId, ev.Station); } private void OnStationGridRemoved(StationGridRemovedEvent ev) { // When a grid is removed from a station, update all trackers on that grid to null UpdateTrackersOnGrid(ev.GridId, null); } #endregion Event handlers /// /// Initializes a new station with the given information. /// /// The station configuration to use. /// All grids that should be added to the station. /// Optional override for the station name. /// The game map that created this station, if any. /// This is for ease of use, manually spawning the entity works just fine. /// The initialized station. public EntityUid InitializeNewStation( StationConfig stationConfig, IEnumerable? gridIds, string? name = null, Content.Shared.Maps.GameMapPrototype? gameMap = null) { // Use overrides for setup. var station = EntityManager.SpawnEntity(stationConfig.StationPrototype, MapCoordinates.Nullspace, stationConfig.StationComponentOverrides); if (name is not null) RenameStation(station, name, false); DebugTools.Assert(HasComp(station), "Stations should have StationData in their prototype."); var data = Comp(station); data.JobWeights = gameMap?.JobWeights; Dirty(station, data); name ??= MetaData(station).EntityName; foreach (var grid in gridIds ?? Array.Empty()) { AddGridToStation(station, grid, null, data, name); } var ev = new StationPostInitEvent((station, data)); RaiseLocalEvent(station, ref ev, true); 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. /// The name to assign to the grid if any. /// Thrown when mapGrid or station are not a grid or station, respectively. public void AddGridToStation(EntityUid station, EntityUid mapGrid, MapGridComponent? gridComponent = null, StationDataComponent? stationData = null, string? name = 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)); if (!string.IsNullOrEmpty(name)) Meta.SetEntityName(mapGrid, name); var stationMember = EnsureComp(mapGrid); stationMember.Station = station; stationData.Grids.Add(mapGrid); Dirty(station, stationData); Dirty(mapGrid, stationMember); RaiseLocalEvent(station, new StationGridAddedEvent(mapGrid, station, false), true); _sawmill.Info($"Adding grid {mapGrid} 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, MapGridComponent? 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(mapGrid); Dirty(station, stationData); RaiseLocalEvent(station, new StationGridRemovedEvent(mapGrid, station), true); _sawmill.Info($"Removing grid {mapGrid} 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; Meta.SetEntityName(station, name, metaData); if (loud) { _chatSystem.DispatchStationAnnouncement(station, $"The station {oldName} has been renamed to {name}."); } RaiseLocalEvent(station, new StationRenamedEvent(oldName, name), true); } /// /// 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)); QueueDel(station); } /// /// Get entities with on a random event-eligible station's largest grid. /// /// Whether to only get anchored entities. /// Good check for air vents, bad for containers like crates. /// All matching entities. public HashSet> GetEntitiesWithComponentOnStation(bool onlyAnchored) where TComponent : IComponent { return GetEntitiesWithComponentOnStation(onlyAnchored, out _); } /// The station picked to search for the entities. /// public HashSet> GetEntitiesWithComponentOnStation(bool onlyAnchored, out Entity? station) where TComponent : IComponent { HashSet> entities = []; if (!TryGetRandomStation(out var eligibleStation)) { station = null; return entities; } station = (eligibleStation.Value.Owner, eligibleStation.Value.Comp1); var grid = GetLargestGrid(station.Value.Owner); if (grid is null) return entities; var locations = EntityQueryEnumerator(); while (locations.MoveNext(out var uid, out var component, out var transform)) { if (onlyAnchored && !transform.Anchored) continue; if (transform.GridUid != grid) continue; entities.Add((uid, component)); } return entities; } /// public override bool TryFindRandomTileOnStation(Entity station, out Vector2i tile, [NotNullWhen(true)] out Entity? targetGrid, out EntityCoordinates targetCoords, int numAttempts = 10) { tile = default; targetGrid = null; targetCoords = EntityCoordinates.Invalid; var targetGridMaybe = GetLargestGrid(station.Owner); if (targetGridMaybe is null) return false; if (!TryComp(targetGridMaybe.Value, out var comp)) return false; targetGrid = (targetGridMaybe.Value, comp); var grid = targetGrid.Value; var gridTiles = Map.GetAllTiles(grid, grid.Comp).ToList(); var totalTiles = gridTiles.Count; if (totalTiles == 0) return false; for (var i = 0; i < numAttempts && totalTiles > 0; i++) { // Find random tile within list. var nextTileIndex = Random.Next(totalTiles); var tileRef = gridTiles[nextTileIndex]; gridTiles.RemoveSwap(nextTileIndex); totalTiles--; // Invalid tile, try again. if (_atmos.IsTileSpace(grid.Owner, Transform(grid).MapUid, tileRef.GridIndices) || _atmos.IsTileAirBlockedCached(grid.Owner, tileRef.GridIndices)) { continue; } targetCoords = Map.GridTileToLocal(grid, grid.Comp, tileRef.GridIndices); tile = tileRef.GridIndices; return true; } return false; } } /// /// 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(EntityUid station) : EntityEventArgs { /// /// Station this event is for. /// public EntityUid Station = station; } /// /// Directed event fired on a station when a grid becomes a member of the station. /// [PublicAPI] public sealed class StationGridAddedEvent(EntityUid gridId, EntityUid station, bool isSetup) : EntityEventArgs { /// /// ID of the grid added to the station. /// public EntityUid GridId = gridId; /// /// EntityUid of the station this grid was added to. /// public EntityUid Station = station; /// /// Indicates that the event was fired during station setup, /// so that it can be ignored if StationInitializedEvent was already handled. /// public bool IsSetup = isSetup; } /// /// Directed event fired on a station when a grid is no longer a member of the station. /// [PublicAPI] public sealed class StationGridRemovedEvent(EntityUid gridId, EntityUid station) : EntityEventArgs { /// /// ID of the grid removed from the station. /// public EntityUid GridId = gridId; /// /// EntityUid of the station this grid was added to. /// public EntityUid Station = station; } /// /// Directed event fired on a station when it is renamed. /// [PublicAPI] public sealed class StationRenamedEvent(string oldName, string newName) : EntityEventArgs { /// /// Prior name of the station. /// public string OldName = oldName; /// /// New name of the station. /// public string NewName = newName; }