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;
///
/// 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 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 _gridQuery;
private EntityQuery _xformQuery;
private ValueList _mapIds = new();
private ValueList<(Box2Rotated Bounds, MapId MapId)> _gridBounds = new();
///
public override void Initialize()
{
_sawmill = _logManager.GetSawmill("station");
_gridQuery = GetEntityQuery();
_xformQuery = GetEntityQuery();
SubscribeLocalEvent(OnRoundEnd);
SubscribeLocalEvent(OnPostGameMapLoad);
SubscribeLocalEvent(OnStationAdd);
SubscribeLocalEvent(OnStationDeleted);
SubscribeLocalEvent(OnStationGridDeleted);
SubscribeLocalEvent(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(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(grid);
}
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);
}
}
private void OnRoundEnd(GameRunLevelChangedEvent eventArgs)
{
if (eventArgs.New != GameRunLevel.PreRoundLobby)
return;
var query = EntityQueryEnumerator();
while (query.MoveNext(out var station, out _))
{
QueueDel(station);
}
}
#endregion Event handlers
///
/// Gets the largest member grid from a station.
///
public EntityUid? GetLargestGrid(StationDataComponent component)
{
EntityUid? largestGrid = null;
Box2 largestBounds = new Box2();
foreach (var gridUid in component.Grids)
{
if (!TryComp(gridUid, out var grid) ||
grid.LocalAABB.Size.LengthSquared() < largestBounds.Size.LengthSquared())
continue;
largestBounds = grid.LocalAABB;
largestGrid = gridUid;
}
return largestGrid;
}
///
/// Returns the total number of tiles contained in the station's grids.
///
public int GetTileCount(StationDataComponent component)
{
var count = 0;
foreach (var gridUid in component.Grids)
{
if (!TryComp(gridUid, out var grid))
continue;
count += _map.GetAllTiles(gridUid, grid).Count();
}
return count;
}
///
/// Tries to retrieve a filter for everything in the station the source is on.
///
/// The entity to use to find the station.
/// The range around the station
///
public Filter GetInOwningStation(EntityUid source, float range = 32f)
{
var station = GetOwningStation(source);
if (TryComp(station, out var data))
{
return GetInStation(data);
}
return Filter.Empty();
}
///
/// Retrieves a filter for everything in a particular station or near its member grids.
///
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;
}
///
/// 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.
/// 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)
{
// 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);
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))
_metaData.SetEntityName(mapGrid, name);
var stationMember = EnsureComp(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})");
}
///
/// 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);
RaiseLocalEvent(station, new StationGridRemovedEvent(mapGrid), 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;
_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);
}
///
/// 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);
}
public EntityUid? GetOwningStation(EntityUid? entity, TransformComponent? xform = null)
{
if (entity == null)
return null;
return GetOwningStation(entity.Value, xform);
}
///
/// 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.GridUid == EntityUid.Invalid)
{
Log.Debug("Unable to get owning station - GridUid invalid.");
return null;
}
return CompOrNull(xform.GridUid)?.Station;
}
public List GetStations()
{
var stations = new List();
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out _))
{
stations.Add(uid);
}
return stations;
}
public HashSet GetStationsSet()
{
var stations = new HashSet();
var query = EntityQueryEnumerator();
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;
}
///
/// Returns the first station that has a grid in a certain map.
/// If the map has no stations, null is returned instead.
///
///
/// If there are multiple stations on a map it is probably arbitrary which one is returned.
///
public EntityUid? GetStationInMap(MapId map)
{
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var data))
{
foreach (var gridUid in data.Grids)
{
if (Transform(gridUid).MapID == map)
{
return uid;
}
}
}
return null;
}
}
///
/// 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;
}
}