StationSystem.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. using System.Linq;
  2. using Content.Server.Chat.Systems;
  3. using Content.Server.GameTicking;
  4. using Content.Server.Station.Components;
  5. using Content.Server.Station.Events;
  6. using Content.Shared.CCVar;
  7. using Content.Shared.Station;
  8. using Content.Shared.Station.Components;
  9. using JetBrains.Annotations;
  10. using Robust.Server.GameObjects;
  11. using Robust.Server.Player;
  12. using Robust.Shared.Collections;
  13. using Robust.Shared.Configuration;
  14. using Robust.Shared.Enums;
  15. using Robust.Shared.Map;
  16. using Robust.Shared.Map.Components;
  17. using Robust.Shared.Player;
  18. using Robust.Shared.Random;
  19. using Robust.Shared.Utility;
  20. namespace Content.Server.Station.Systems;
  21. /// <summary>
  22. /// System that manages stations.
  23. /// A station is, by default, just a name, optional map prototype, and optional grids.
  24. /// For jobs, look at StationJobSystem. For spawning, look at StationSpawningSystem.
  25. /// </summary>
  26. [PublicAPI]
  27. public sealed class StationSystem : EntitySystem
  28. {
  29. [Dependency] private readonly ILogManager _logManager = default!;
  30. [Dependency] private readonly IPlayerManager _player = default!;
  31. [Dependency] private readonly ChatSystem _chatSystem = default!;
  32. [Dependency] private readonly SharedTransformSystem _transform = default!;
  33. [Dependency] private readonly MetaDataSystem _metaData = default!;
  34. [Dependency] private readonly MapSystem _map = default!;
  35. private ISawmill _sawmill = default!;
  36. private EntityQuery<MapGridComponent> _gridQuery;
  37. private EntityQuery<TransformComponent> _xformQuery;
  38. private ValueList<MapId> _mapIds = new();
  39. private ValueList<(Box2Rotated Bounds, MapId MapId)> _gridBounds = new();
  40. /// <inheritdoc/>
  41. public override void Initialize()
  42. {
  43. _sawmill = _logManager.GetSawmill("station");
  44. _gridQuery = GetEntityQuery<MapGridComponent>();
  45. _xformQuery = GetEntityQuery<TransformComponent>();
  46. SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd);
  47. SubscribeLocalEvent<PostGameMapLoad>(OnPostGameMapLoad);
  48. SubscribeLocalEvent<StationDataComponent, ComponentStartup>(OnStationAdd);
  49. SubscribeLocalEvent<StationDataComponent, ComponentShutdown>(OnStationDeleted);
  50. SubscribeLocalEvent<StationMemberComponent, ComponentShutdown>(OnStationGridDeleted);
  51. SubscribeLocalEvent<StationMemberComponent, PostGridSplitEvent>(OnStationSplitEvent);
  52. _player.PlayerStatusChanged += OnPlayerStatusChanged;
  53. }
  54. private void OnStationSplitEvent(EntityUid uid, StationMemberComponent component, ref PostGridSplitEvent args)
  55. {
  56. AddGridToStation(component.Station, args.Grid); // Add the new grid as a member.
  57. }
  58. private void OnStationGridDeleted(EntityUid uid, StationMemberComponent component, ComponentShutdown args)
  59. {
  60. if (!TryComp<StationDataComponent>(component.Station, out var stationData))
  61. return;
  62. stationData.Grids.Remove(uid);
  63. }
  64. public override void Shutdown()
  65. {
  66. base.Shutdown();
  67. _player.PlayerStatusChanged -= OnPlayerStatusChanged;
  68. }
  69. private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
  70. {
  71. if (e.NewStatus == SessionStatus.Connected)
  72. {
  73. RaiseNetworkEvent(new StationsUpdatedEvent(GetStationNames()), e.Session);
  74. }
  75. }
  76. #region Event handlers
  77. private void OnStationAdd(EntityUid uid, StationDataComponent component, ComponentStartup args)
  78. {
  79. RaiseNetworkEvent(new StationsUpdatedEvent(GetStationNames()), Filter.Broadcast());
  80. var metaData = MetaData(uid);
  81. RaiseLocalEvent(new StationInitializedEvent(uid));
  82. _sawmill.Info($"Set up station {metaData.EntityName} ({uid}).");
  83. }
  84. private void OnStationDeleted(EntityUid uid, StationDataComponent component, ComponentShutdown args)
  85. {
  86. foreach (var grid in component.Grids)
  87. {
  88. RemComp<StationMemberComponent>(grid);
  89. }
  90. RaiseNetworkEvent(new StationsUpdatedEvent(GetStationNames()), Filter.Broadcast());
  91. }
  92. private void OnPostGameMapLoad(PostGameMapLoad ev)
  93. {
  94. var dict = new Dictionary<string, List<EntityUid>>();
  95. // Iterate over all BecomesStation
  96. foreach (var grid in ev.Grids)
  97. {
  98. // We still setup the grid
  99. if (TryComp<BecomesStationComponent>(grid, out var becomesStation))
  100. dict.GetOrNew(becomesStation.Id).Add(grid);
  101. }
  102. if (!dict.Any())
  103. {
  104. // Oh jeez, no stations got loaded.
  105. // We'll yell about it, but the thing this used to do with creating a dummy is kinda pointless now.
  106. _sawmill.Error($"There were no station grids for {ev.GameMap.ID}!");
  107. }
  108. foreach (var (id, gridIds) in dict)
  109. {
  110. StationConfig stationConfig;
  111. if (ev.GameMap.Stations.ContainsKey(id))
  112. stationConfig = ev.GameMap.Stations[id];
  113. else
  114. {
  115. _sawmill.Error($"The station {id} in map {ev.GameMap.ID} does not have an associated station config!");
  116. continue;
  117. }
  118. InitializeNewStation(stationConfig, gridIds, ev.StationName);
  119. }
  120. }
  121. private void OnRoundEnd(GameRunLevelChangedEvent eventArgs)
  122. {
  123. if (eventArgs.New != GameRunLevel.PreRoundLobby)
  124. return;
  125. var query = EntityQueryEnumerator<StationDataComponent>();
  126. while (query.MoveNext(out var station, out _))
  127. {
  128. QueueDel(station);
  129. }
  130. }
  131. #endregion Event handlers
  132. /// <summary>
  133. /// Gets the largest member grid from a station.
  134. /// </summary>
  135. public EntityUid? GetLargestGrid(StationDataComponent component)
  136. {
  137. EntityUid? largestGrid = null;
  138. Box2 largestBounds = new Box2();
  139. foreach (var gridUid in component.Grids)
  140. {
  141. if (!TryComp<MapGridComponent>(gridUid, out var grid) ||
  142. grid.LocalAABB.Size.LengthSquared() < largestBounds.Size.LengthSquared())
  143. continue;
  144. largestBounds = grid.LocalAABB;
  145. largestGrid = gridUid;
  146. }
  147. return largestGrid;
  148. }
  149. /// <summary>
  150. /// Returns the total number of tiles contained in the station's grids.
  151. /// </summary>
  152. public int GetTileCount(StationDataComponent component)
  153. {
  154. var count = 0;
  155. foreach (var gridUid in component.Grids)
  156. {
  157. if (!TryComp<MapGridComponent>(gridUid, out var grid))
  158. continue;
  159. count += _map.GetAllTiles(gridUid, grid).Count();
  160. }
  161. return count;
  162. }
  163. /// <summary>
  164. /// Tries to retrieve a filter for everything in the station the source is on.
  165. /// </summary>
  166. /// <param name="source">The entity to use to find the station.</param>
  167. /// <param name="range">The range around the station</param>
  168. /// <returns></returns>
  169. public Filter GetInOwningStation(EntityUid source, float range = 32f)
  170. {
  171. var station = GetOwningStation(source);
  172. if (TryComp<StationDataComponent>(station, out var data))
  173. {
  174. return GetInStation(data);
  175. }
  176. return Filter.Empty();
  177. }
  178. /// <summary>
  179. /// Retrieves a filter for everything in a particular station or near its member grids.
  180. /// </summary>
  181. public Filter GetInStation(StationDataComponent dataComponent, float range = 32f)
  182. {
  183. var filter = Filter.Empty();
  184. _mapIds.Clear();
  185. // First collect all valid map IDs where station grids exist
  186. foreach (var gridUid in dataComponent.Grids)
  187. {
  188. if (!_xformQuery.TryGetComponent(gridUid, out var xform))
  189. continue;
  190. var mapId = xform.MapID;
  191. if (!_mapIds.Contains(mapId))
  192. _mapIds.Add(mapId);
  193. }
  194. // Cache the rotated bounds for each grid
  195. _gridBounds.Clear();
  196. foreach (var gridUid in dataComponent.Grids)
  197. {
  198. if (!_gridQuery.TryComp(gridUid, out var grid) ||
  199. !_xformQuery.TryGetComponent(gridUid, out var gridXform))
  200. {
  201. continue;
  202. }
  203. var (worldPos, worldRot) = _transform.GetWorldPositionRotation(gridXform);
  204. var localBounds = grid.LocalAABB.Enlarged(range);
  205. // Create a rotated box using the grid's transform
  206. var rotatedBounds = new Box2Rotated(
  207. localBounds,
  208. worldRot,
  209. worldPos);
  210. _gridBounds.Add((rotatedBounds, gridXform.MapID));
  211. }
  212. foreach (var session in Filter.GetAllPlayers(_player))
  213. {
  214. var entity = session.AttachedEntity;
  215. if (entity == null || !_xformQuery.TryGetComponent(entity, out var xform))
  216. continue;
  217. var mapId = xform.MapID;
  218. if (!_mapIds.Contains(mapId))
  219. continue;
  220. // Check if the player is directly on any station grid
  221. var gridUid = xform.GridUid;
  222. if (gridUid != null && dataComponent.Grids.Contains(gridUid.Value))
  223. {
  224. filter.AddPlayer(session);
  225. continue;
  226. }
  227. // If not directly on a grid, check against cached rotated bounds
  228. var position = _transform.GetWorldPosition(xform);
  229. foreach (var (bounds, boundsMapId) in _gridBounds)
  230. {
  231. // Skip bounds on different maps
  232. if (boundsMapId != mapId)
  233. continue;
  234. if (!bounds.Contains(position))
  235. continue;
  236. filter.AddPlayer(session);
  237. break;
  238. }
  239. }
  240. return filter;
  241. }
  242. /// <summary>
  243. /// Initializes a new station with the given information.
  244. /// </summary>
  245. /// <param name="stationConfig">The game map prototype used, if any.</param>
  246. /// <param name="gridIds">All grids that should be added to the station.</param>
  247. /// <param name="name">Optional override for the station name.</param>
  248. /// <remarks>This is for ease of use, manually spawning the entity works just fine.</remarks>
  249. /// <returns>The initialized station.</returns>
  250. public EntityUid InitializeNewStation(StationConfig stationConfig, IEnumerable<EntityUid>? gridIds, string? name = null)
  251. {
  252. // Use overrides for setup.
  253. var station = EntityManager.SpawnEntity(stationConfig.StationPrototype, MapCoordinates.Nullspace, stationConfig.StationComponentOverrides);
  254. if (name is not null)
  255. RenameStation(station, name, false);
  256. DebugTools.Assert(HasComp<StationDataComponent>(station), "Stations should have StationData in their prototype.");
  257. var data = Comp<StationDataComponent>(station);
  258. name ??= MetaData(station).EntityName;
  259. foreach (var grid in gridIds ?? Array.Empty<EntityUid>())
  260. {
  261. AddGridToStation(station, grid, null, data, name);
  262. }
  263. var ev = new StationPostInitEvent((station, data));
  264. RaiseLocalEvent(station, ref ev, true);
  265. return station;
  266. }
  267. /// <summary>
  268. /// Adds the given grid to a station.
  269. /// </summary>
  270. /// <param name="mapGrid">Grid to attach.</param>
  271. /// <param name="station">Station to attach the grid to.</param>
  272. /// <param name="gridComponent">Resolve pattern, grid component of mapGrid.</param>
  273. /// <param name="stationData">Resolve pattern, station data component of station.</param>
  274. /// <param name="name">The name to assign to the grid if any.</param>
  275. /// <exception cref="ArgumentException">Thrown when mapGrid or station are not a grid or station, respectively.</exception>
  276. public void AddGridToStation(EntityUid station, EntityUid mapGrid, MapGridComponent? gridComponent = null, StationDataComponent? stationData = null, string? name = null)
  277. {
  278. if (!Resolve(mapGrid, ref gridComponent))
  279. throw new ArgumentException("Tried to initialize a station on a non-grid entity!", nameof(mapGrid));
  280. if (!Resolve(station, ref stationData))
  281. throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
  282. if (!string.IsNullOrEmpty(name))
  283. _metaData.SetEntityName(mapGrid, name);
  284. var stationMember = EnsureComp<StationMemberComponent>(mapGrid);
  285. stationMember.Station = station;
  286. stationData.Grids.Add(mapGrid);
  287. RaiseLocalEvent(station, new StationGridAddedEvent(mapGrid, false), true);
  288. _sawmill.Info($"Adding grid {mapGrid} to station {Name(station)} ({station})");
  289. }
  290. /// <summary>
  291. /// Removes the given grid from a station.
  292. /// </summary>
  293. /// <param name="station">Station to remove the grid from.</param>
  294. /// <param name="mapGrid">Grid to remove</param>
  295. /// <param name="gridComponent">Resolve pattern, grid component of mapGrid.</param>
  296. /// <param name="stationData">Resolve pattern, station data component of station.</param>
  297. /// <exception cref="ArgumentException">Thrown when mapGrid or station are not a grid or station, respectively.</exception>
  298. public void RemoveGridFromStation(EntityUid station, EntityUid mapGrid, MapGridComponent? gridComponent = null, StationDataComponent? stationData = null)
  299. {
  300. if (!Resolve(mapGrid, ref gridComponent))
  301. throw new ArgumentException("Tried to initialize a station on a non-grid entity!", nameof(mapGrid));
  302. if (!Resolve(station, ref stationData))
  303. throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
  304. RemComp<StationMemberComponent>(mapGrid);
  305. stationData.Grids.Remove(mapGrid);
  306. RaiseLocalEvent(station, new StationGridRemovedEvent(mapGrid), true);
  307. _sawmill.Info($"Removing grid {mapGrid} from station {Name(station)} ({station})");
  308. }
  309. /// <summary>
  310. /// Renames the given station.
  311. /// </summary>
  312. /// <param name="station">Station to rename.</param>
  313. /// <param name="name">The new name to apply.</param>
  314. /// <param name="loud">Whether or not to announce the rename.</param>
  315. /// <param name="stationData">Resolve pattern, station data component of station.</param>
  316. /// <param name="metaData">Resolve pattern, metadata component of station.</param>
  317. /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
  318. public void RenameStation(EntityUid station, string name, bool loud = true, StationDataComponent? stationData = null, MetaDataComponent? metaData = null)
  319. {
  320. if (!Resolve(station, ref stationData, ref metaData))
  321. throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
  322. var oldName = metaData.EntityName;
  323. _metaData.SetEntityName(station, name, metaData);
  324. if (loud)
  325. {
  326. _chatSystem.DispatchStationAnnouncement(station, $"The station {oldName} has been renamed to {name}.");
  327. }
  328. RaiseLocalEvent(station, new StationRenamedEvent(oldName, name), true);
  329. }
  330. /// <summary>
  331. /// Deletes the given station.
  332. /// </summary>
  333. /// <param name="station">Station to delete.</param>
  334. /// <param name="stationData">Resolve pattern, station data component of station.</param>
  335. /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
  336. public void DeleteStation(EntityUid station, StationDataComponent? stationData = null)
  337. {
  338. if (!Resolve(station, ref stationData))
  339. throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
  340. QueueDel(station);
  341. }
  342. public EntityUid? GetOwningStation(EntityUid? entity, TransformComponent? xform = null)
  343. {
  344. if (entity == null)
  345. return null;
  346. return GetOwningStation(entity.Value, xform);
  347. }
  348. /// <summary>
  349. /// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to)
  350. /// </summary>
  351. /// <param name="entity">Entity to find the owner of.</param>
  352. /// <param name="xform">Resolve pattern, transform of the entity.</param>
  353. /// <returns>The owning station, if any.</returns>
  354. /// <remarks>
  355. /// This does not remember what station an entity started on, it simply checks where it is currently located.
  356. /// </remarks>
  357. public EntityUid? GetOwningStation(EntityUid entity, TransformComponent? xform = null)
  358. {
  359. if (!Resolve(entity, ref xform))
  360. throw new ArgumentException("Tried to use an abstract entity!", nameof(entity));
  361. if (TryComp<StationDataComponent>(entity, out _))
  362. {
  363. // We are the station, just return ourselves.
  364. return entity;
  365. }
  366. if (TryComp<MapGridComponent>(entity, out _))
  367. {
  368. // We are the station, just check ourselves.
  369. return CompOrNull<StationMemberComponent>(entity)?.Station;
  370. }
  371. if (xform.GridUid == EntityUid.Invalid)
  372. {
  373. Log.Debug("Unable to get owning station - GridUid invalid.");
  374. return null;
  375. }
  376. return CompOrNull<StationMemberComponent>(xform.GridUid)?.Station;
  377. }
  378. public List<EntityUid> GetStations()
  379. {
  380. var stations = new List<EntityUid>();
  381. var query = EntityQueryEnumerator<StationDataComponent>();
  382. while (query.MoveNext(out var uid, out _))
  383. {
  384. stations.Add(uid);
  385. }
  386. return stations;
  387. }
  388. public HashSet<EntityUid> GetStationsSet()
  389. {
  390. var stations = new HashSet<EntityUid>();
  391. var query = EntityQueryEnumerator<StationDataComponent>();
  392. while (query.MoveNext(out var uid, out _))
  393. {
  394. stations.Add(uid);
  395. }
  396. return stations;
  397. }
  398. public List<(string Name, NetEntity Entity)> GetStationNames()
  399. {
  400. var stations = GetStationsSet();
  401. var stats = new List<(string Name, NetEntity Station)>();
  402. foreach (var weh in stations)
  403. {
  404. stats.Add((MetaData(weh).EntityName, GetNetEntity(weh)));
  405. }
  406. return stats;
  407. }
  408. /// <summary>
  409. /// Returns the first station that has a grid in a certain map.
  410. /// If the map has no stations, null is returned instead.
  411. /// </summary>
  412. /// <remarks>
  413. /// If there are multiple stations on a map it is probably arbitrary which one is returned.
  414. /// </remarks>
  415. public EntityUid? GetStationInMap(MapId map)
  416. {
  417. var query = EntityQueryEnumerator<StationDataComponent>();
  418. while (query.MoveNext(out var uid, out var data))
  419. {
  420. foreach (var gridUid in data.Grids)
  421. {
  422. if (Transform(gridUid).MapID == map)
  423. {
  424. return uid;
  425. }
  426. }
  427. }
  428. return null;
  429. }
  430. }
  431. /// <summary>
  432. /// Broadcast event fired when a station is first set up.
  433. /// This is the ideal point to add components to it.
  434. /// </summary>
  435. [PublicAPI]
  436. public sealed class StationInitializedEvent : EntityEventArgs
  437. {
  438. /// <summary>
  439. /// Station this event is for.
  440. /// </summary>
  441. public EntityUid Station;
  442. public StationInitializedEvent(EntityUid station)
  443. {
  444. Station = station;
  445. }
  446. }
  447. /// <summary>
  448. /// Directed event fired on a station when a grid becomes a member of the station.
  449. /// </summary>
  450. [PublicAPI]
  451. public sealed class StationGridAddedEvent : EntityEventArgs
  452. {
  453. /// <summary>
  454. /// ID of the grid added to the station.
  455. /// </summary>
  456. public EntityUid GridId;
  457. /// <summary>
  458. /// Indicates that the event was fired during station setup,
  459. /// so that it can be ignored if StationInitializedEvent was already handled.
  460. /// </summary>
  461. public bool IsSetup;
  462. public StationGridAddedEvent(EntityUid gridId, bool isSetup)
  463. {
  464. GridId = gridId;
  465. IsSetup = isSetup;
  466. }
  467. }
  468. /// <summary>
  469. /// Directed event fired on a station when a grid is no longer a member of the station.
  470. /// </summary>
  471. [PublicAPI]
  472. public sealed class StationGridRemovedEvent : EntityEventArgs
  473. {
  474. /// <summary>
  475. /// ID of the grid removed from the station.
  476. /// </summary>
  477. public EntityUid GridId;
  478. public StationGridRemovedEvent(EntityUid gridId)
  479. {
  480. GridId = gridId;
  481. }
  482. }
  483. /// <summary>
  484. /// Directed event fired on a station when it is renamed.
  485. /// </summary>
  486. [PublicAPI]
  487. public sealed class StationRenamedEvent : EntityEventArgs
  488. {
  489. /// <summary>
  490. /// Prior name of the station.
  491. /// </summary>
  492. public string OldName;
  493. /// <summary>
  494. /// New name of the station.
  495. /// </summary>
  496. public string NewName;
  497. public StationRenamedEvent(string oldName, string newName)
  498. {
  499. OldName = oldName;
  500. NewName = newName;
  501. }
  502. }