| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606 |
- using System.Linq;
- using Content.Server.Chat.Systems;
- using Content.Server.GameTicking;
- using Content.Server.Station.Components;
- using Content.Server.Station.Events;
- using Content.Shared.CCVar;
- using Content.Shared.Station;
- using Content.Shared.Station.Components;
- using JetBrains.Annotations;
- using Robust.Server.GameObjects;
- using Robust.Server.Player;
- using Robust.Shared.Collections;
- using Robust.Shared.Configuration;
- using Robust.Shared.Enums;
- using Robust.Shared.Map;
- using Robust.Shared.Map.Components;
- using Robust.Shared.Player;
- using Robust.Shared.Random;
- using Robust.Shared.Utility;
- namespace Content.Server.Station.Systems;
- /// <summary>
- /// 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.
- /// </summary>
- [PublicAPI]
- public sealed class StationSystem : EntitySystem
- {
- [Dependency] private readonly ILogManager _logManager = default!;
- [Dependency] private readonly IPlayerManager _player = default!;
- [Dependency] private readonly ChatSystem _chatSystem = default!;
- [Dependency] private readonly SharedTransformSystem _transform = default!;
- [Dependency] private readonly MetaDataSystem _metaData = default!;
- [Dependency] private readonly MapSystem _map = default!;
- private ISawmill _sawmill = default!;
- private EntityQuery<MapGridComponent> _gridQuery;
- private EntityQuery<TransformComponent> _xformQuery;
- private ValueList<MapId> _mapIds = new();
- private ValueList<(Box2Rotated Bounds, MapId MapId)> _gridBounds = new();
- /// <inheritdoc/>
- public override void Initialize()
- {
- _sawmill = _logManager.GetSawmill("station");
- _gridQuery = GetEntityQuery<MapGridComponent>();
- _xformQuery = GetEntityQuery<TransformComponent>();
- SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd);
- SubscribeLocalEvent<PostGameMapLoad>(OnPostGameMapLoad);
- SubscribeLocalEvent<StationDataComponent, ComponentStartup>(OnStationAdd);
- SubscribeLocalEvent<StationDataComponent, ComponentShutdown>(OnStationDeleted);
- SubscribeLocalEvent<StationMemberComponent, ComponentShutdown>(OnStationGridDeleted);
- SubscribeLocalEvent<StationMemberComponent, PostGridSplitEvent>(OnStationSplitEvent);
- _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<StationDataComponent>(component.Station, out var stationData))
- return;
- stationData.Grids.Remove(uid);
- }
- 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);
- }
- }
- #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}).");
- }
- private void OnStationDeleted(EntityUid uid, StationDataComponent component, ComponentShutdown args)
- {
- foreach (var grid in component.Grids)
- {
- RemComp<StationMemberComponent>(grid);
- }
- RaiseNetworkEvent(new StationsUpdatedEvent(GetStationNames()), Filter.Broadcast());
- }
- private void OnPostGameMapLoad(PostGameMapLoad ev)
- {
- var dict = new Dictionary<string, List<EntityUid>>();
- // Iterate over all BecomesStation
- foreach (var grid in ev.Grids)
- {
- // We still setup the grid
- if (TryComp<BecomesStationComponent>(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);
- }
- }
- private void OnRoundEnd(GameRunLevelChangedEvent eventArgs)
- {
- if (eventArgs.New != GameRunLevel.PreRoundLobby)
- return;
- var query = EntityQueryEnumerator<StationDataComponent>();
- while (query.MoveNext(out var station, out _))
- {
- QueueDel(station);
- }
- }
- #endregion Event handlers
- /// <summary>
- /// Gets the largest member grid from a station.
- /// </summary>
- public EntityUid? GetLargestGrid(StationDataComponent component)
- {
- EntityUid? largestGrid = null;
- Box2 largestBounds = new Box2();
- foreach (var gridUid in component.Grids)
- {
- if (!TryComp<MapGridComponent>(gridUid, out var grid) ||
- grid.LocalAABB.Size.LengthSquared() < largestBounds.Size.LengthSquared())
- continue;
- largestBounds = grid.LocalAABB;
- largestGrid = gridUid;
- }
- return largestGrid;
- }
- /// <summary>
- /// Returns the total number of tiles contained in the station's grids.
- /// </summary>
- public int GetTileCount(StationDataComponent component)
- {
- var count = 0;
- foreach (var gridUid in component.Grids)
- {
- if (!TryComp<MapGridComponent>(gridUid, out var grid))
- continue;
- count += _map.GetAllTiles(gridUid, grid).Count();
- }
- return count;
- }
- /// <summary>
- /// Tries to retrieve a filter for everything in the station the source is on.
- /// </summary>
- /// <param name="source">The entity to use to find the station.</param>
- /// <param name="range">The range around the station</param>
- /// <returns></returns>
- public Filter GetInOwningStation(EntityUid source, float range = 32f)
- {
- var station = GetOwningStation(source);
- if (TryComp<StationDataComponent>(station, out var data))
- {
- return GetInStation(data);
- }
- return Filter.Empty();
- }
- /// <summary>
- /// Retrieves a filter for everything in a particular station or near its member grids.
- /// </summary>
- public Filter GetInStation(StationDataComponent dataComponent, float range = 32f)
- {
- var filter = Filter.Empty();
- _mapIds.Clear();
- // First collect all valid map IDs where station grids exist
- foreach (var gridUid in dataComponent.Grids)
- {
- if (!_xformQuery.TryGetComponent(gridUid, out var xform))
- continue;
- var mapId = xform.MapID;
- if (!_mapIds.Contains(mapId))
- _mapIds.Add(mapId);
- }
- // Cache the rotated bounds for each grid
- _gridBounds.Clear();
- foreach (var gridUid in dataComponent.Grids)
- {
- if (!_gridQuery.TryComp(gridUid, out var grid) ||
- !_xformQuery.TryGetComponent(gridUid, out var gridXform))
- {
- continue;
- }
- var (worldPos, worldRot) = _transform.GetWorldPositionRotation(gridXform);
- var localBounds = grid.LocalAABB.Enlarged(range);
- // Create a rotated box using the grid's transform
- var rotatedBounds = new Box2Rotated(
- localBounds,
- worldRot,
- worldPos);
- _gridBounds.Add((rotatedBounds, gridXform.MapID));
- }
- foreach (var session in Filter.GetAllPlayers(_player))
- {
- var entity = session.AttachedEntity;
- if (entity == null || !_xformQuery.TryGetComponent(entity, out var xform))
- continue;
- var mapId = xform.MapID;
- if (!_mapIds.Contains(mapId))
- continue;
- // Check if the player is directly on any station grid
- var gridUid = xform.GridUid;
- if (gridUid != null && dataComponent.Grids.Contains(gridUid.Value))
- {
- filter.AddPlayer(session);
- continue;
- }
- // If not directly on a grid, check against cached rotated bounds
- var position = _transform.GetWorldPosition(xform);
- foreach (var (bounds, boundsMapId) in _gridBounds)
- {
- // Skip bounds on different maps
- if (boundsMapId != mapId)
- continue;
- if (!bounds.Contains(position))
- continue;
- filter.AddPlayer(session);
- break;
- }
- }
- return filter;
- }
- /// <summary>
- /// Initializes a new station with the given information.
- /// </summary>
- /// <param name="stationConfig">The game map prototype used, if any.</param>
- /// <param name="gridIds">All grids that should be added to the station.</param>
- /// <param name="name">Optional override for the station name.</param>
- /// <remarks>This is for ease of use, manually spawning the entity works just fine.</remarks>
- /// <returns>The initialized station.</returns>
- public EntityUid InitializeNewStation(StationConfig stationConfig, IEnumerable<EntityUid>? gridIds, string? name = 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<StationDataComponent>(station), "Stations should have StationData in their prototype.");
- var data = Comp<StationDataComponent>(station);
- name ??= MetaData(station).EntityName;
- foreach (var grid in gridIds ?? Array.Empty<EntityUid>())
- {
- AddGridToStation(station, grid, null, data, name);
- }
- var ev = new StationPostInitEvent((station, data));
- RaiseLocalEvent(station, ref ev, true);
- return station;
- }
- /// <summary>
- /// Adds the given grid to a station.
- /// </summary>
- /// <param name="mapGrid">Grid to attach.</param>
- /// <param name="station">Station to attach the grid to.</param>
- /// <param name="gridComponent">Resolve pattern, grid component of mapGrid.</param>
- /// <param name="stationData">Resolve pattern, station data component of station.</param>
- /// <param name="name">The name to assign to the grid if any.</param>
- /// <exception cref="ArgumentException">Thrown when mapGrid or station are not a grid or station, respectively.</exception>
- 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))
- _metaData.SetEntityName(mapGrid, name);
- var stationMember = EnsureComp<StationMemberComponent>(mapGrid);
- stationMember.Station = station;
- stationData.Grids.Add(mapGrid);
- RaiseLocalEvent(station, new StationGridAddedEvent(mapGrid, false), true);
- _sawmill.Info($"Adding grid {mapGrid} to station {Name(station)} ({station})");
- }
- /// <summary>
- /// Removes the given grid from a station.
- /// </summary>
- /// <param name="station">Station to remove the grid from.</param>
- /// <param name="mapGrid">Grid to remove</param>
- /// <param name="gridComponent">Resolve pattern, grid component of mapGrid.</param>
- /// <param name="stationData">Resolve pattern, station data component of station.</param>
- /// <exception cref="ArgumentException">Thrown when mapGrid or station are not a grid or station, respectively.</exception>
- 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<StationMemberComponent>(mapGrid);
- stationData.Grids.Remove(mapGrid);
- RaiseLocalEvent(station, new StationGridRemovedEvent(mapGrid), true);
- _sawmill.Info($"Removing grid {mapGrid} from station {Name(station)} ({station})");
- }
- /// <summary>
- /// Renames the given station.
- /// </summary>
- /// <param name="station">Station to rename.</param>
- /// <param name="name">The new name to apply.</param>
- /// <param name="loud">Whether or not to announce the rename.</param>
- /// <param name="stationData">Resolve pattern, station data component of station.</param>
- /// <param name="metaData">Resolve pattern, metadata component of station.</param>
- /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
- 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.SetEntityName(station, name, metaData);
- if (loud)
- {
- _chatSystem.DispatchStationAnnouncement(station, $"The station {oldName} has been renamed to {name}.");
- }
- RaiseLocalEvent(station, new StationRenamedEvent(oldName, name), true);
- }
- /// <summary>
- /// Deletes the given station.
- /// </summary>
- /// <param name="station">Station to delete.</param>
- /// <param name="stationData">Resolve pattern, station data component of station.</param>
- /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
- 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);
- }
- public EntityUid? GetOwningStation(EntityUid? entity, TransformComponent? xform = null)
- {
- if (entity == null)
- return null;
- return GetOwningStation(entity.Value, xform);
- }
- /// <summary>
- /// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to)
- /// </summary>
- /// <param name="entity">Entity to find the owner of.</param>
- /// <param name="xform">Resolve pattern, transform of the entity.</param>
- /// <returns>The owning station, if any.</returns>
- /// <remarks>
- /// This does not remember what station an entity started on, it simply checks where it is currently located.
- /// </remarks>
- 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<StationDataComponent>(entity, out _))
- {
- // We are the station, just return ourselves.
- return entity;
- }
- if (TryComp<MapGridComponent>(entity, out _))
- {
- // We are the station, just check ourselves.
- return CompOrNull<StationMemberComponent>(entity)?.Station;
- }
- if (xform.GridUid == EntityUid.Invalid)
- {
- Log.Debug("Unable to get owning station - GridUid invalid.");
- return null;
- }
- return CompOrNull<StationMemberComponent>(xform.GridUid)?.Station;
- }
- public List<EntityUid> GetStations()
- {
- var stations = new List<EntityUid>();
- var query = EntityQueryEnumerator<StationDataComponent>();
- while (query.MoveNext(out var uid, out _))
- {
- stations.Add(uid);
- }
- return stations;
- }
- public HashSet<EntityUid> GetStationsSet()
- {
- var stations = new HashSet<EntityUid>();
- var query = EntityQueryEnumerator<StationDataComponent>();
- while (query.MoveNext(out var uid, out _))
- {
- stations.Add(uid);
- }
- return stations;
- }
- public List<(string Name, NetEntity Entity)> GetStationNames()
- {
- var stations = GetStationsSet();
- var stats = new List<(string Name, NetEntity Station)>();
- foreach (var weh in stations)
- {
- stats.Add((MetaData(weh).EntityName, GetNetEntity(weh)));
- }
- return stats;
- }
- /// <summary>
- /// Returns the first station that has a grid in a certain map.
- /// If the map has no stations, null is returned instead.
- /// </summary>
- /// <remarks>
- /// If there are multiple stations on a map it is probably arbitrary which one is returned.
- /// </remarks>
- public EntityUid? GetStationInMap(MapId map)
- {
- var query = EntityQueryEnumerator<StationDataComponent>();
- while (query.MoveNext(out var uid, out var data))
- {
- foreach (var gridUid in data.Grids)
- {
- if (Transform(gridUid).MapID == map)
- {
- return uid;
- }
- }
- }
- return null;
- }
- }
- /// <summary>
- /// Broadcast event fired when a station is first set up.
- /// This is the ideal point to add components to it.
- /// </summary>
- [PublicAPI]
- public sealed class StationInitializedEvent : EntityEventArgs
- {
- /// <summary>
- /// Station this event is for.
- /// </summary>
- public EntityUid Station;
- public StationInitializedEvent(EntityUid station)
- {
- Station = station;
- }
- }
- /// <summary>
- /// Directed event fired on a station when a grid becomes a member of the station.
- /// </summary>
- [PublicAPI]
- public sealed class StationGridAddedEvent : EntityEventArgs
- {
- /// <summary>
- /// ID of the grid added to the station.
- /// </summary>
- public EntityUid GridId;
- /// <summary>
- /// Indicates that the event was fired during station setup,
- /// so that it can be ignored if StationInitializedEvent was already handled.
- /// </summary>
- public bool IsSetup;
- public StationGridAddedEvent(EntityUid gridId, bool isSetup)
- {
- GridId = gridId;
- IsSetup = isSetup;
- }
- }
- /// <summary>
- /// Directed event fired on a station when a grid is no longer a member of the station.
- /// </summary>
- [PublicAPI]
- public sealed class StationGridRemovedEvent : EntityEventArgs
- {
- /// <summary>
- /// ID of the grid removed from the station.
- /// </summary>
- public EntityUid GridId;
- public StationGridRemovedEvent(EntityUid gridId)
- {
- GridId = gridId;
- }
- }
- /// <summary>
- /// Directed event fired on a station when it is renamed.
- /// </summary>
- [PublicAPI]
- public sealed class StationRenamedEvent : EntityEventArgs
- {
- /// <summary>
- /// Prior name of the station.
- /// </summary>
- public string OldName;
- /// <summary>
- /// New name of the station.
- /// </summary>
- public string NewName;
- public StationRenamedEvent(string oldName, string newName)
- {
- OldName = oldName;
- NewName = newName;
- }
- }
|