1
0

NavMapSystem.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. using Content.Server.Administration.Logs;
  2. using Content.Server.Atmos.Components;
  3. using Content.Server.Atmos.EntitySystems;
  4. using Content.Server.Station.Systems;
  5. using Content.Server.Warps;
  6. using Content.Shared.Database;
  7. using Content.Shared.Examine;
  8. using Content.Shared.Localizations;
  9. using Content.Shared.Maps;
  10. using Content.Shared.Pinpointer;
  11. using JetBrains.Annotations;
  12. using Robust.Shared.Map;
  13. using Robust.Shared.Map.Components;
  14. using Robust.Shared.Timing;
  15. using System.Diagnostics.CodeAnalysis;
  16. namespace Content.Server.Pinpointer;
  17. /// <summary>
  18. /// Handles data to be used for in-grid map displays.
  19. /// </summary>
  20. public sealed partial class NavMapSystem : SharedNavMapSystem
  21. {
  22. [Dependency] private readonly IAdminLogManager _adminLog = default!;
  23. [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
  24. [Dependency] private readonly SharedMapSystem _mapSystem = default!;
  25. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  26. [Dependency] private readonly IMapManager _mapManager = default!;
  27. [Dependency] private readonly IGameTiming _gameTiming = default!;
  28. [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
  29. public const float CloseDistance = 15f;
  30. public const float FarDistance = 30f;
  31. private EntityQuery<AirtightComponent> _airtightQuery;
  32. private EntityQuery<MapGridComponent> _gridQuery;
  33. private EntityQuery<NavMapComponent> _navQuery;
  34. public override void Initialize()
  35. {
  36. base.Initialize();
  37. var categories = Enum.GetNames(typeof(NavMapChunkType)).Length - 1; // -1 due to "Invalid" entry.
  38. if (Categories != categories)
  39. throw new Exception($"{nameof(Categories)} must be equal to the number of chunk types");
  40. _airtightQuery = GetEntityQuery<AirtightComponent>();
  41. _gridQuery = GetEntityQuery<MapGridComponent>();
  42. _navQuery = GetEntityQuery<NavMapComponent>();
  43. // Initialization events
  44. SubscribeLocalEvent<StationGridAddedEvent>(OnStationInit);
  45. // Grid change events
  46. SubscribeLocalEvent<GridSplitEvent>(OnNavMapSplit);
  47. SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);
  48. SubscribeLocalEvent<AirtightChanged>(OnAirtightChange);
  49. // Beacon events
  50. SubscribeLocalEvent<NavMapBeaconComponent, MapInitEvent>(OnNavMapBeaconMapInit);
  51. SubscribeLocalEvent<NavMapBeaconComponent, AnchorStateChangedEvent>(OnNavMapBeaconAnchor);
  52. SubscribeLocalEvent<ConfigurableNavMapBeaconComponent, NavMapBeaconConfigureBuiMessage>(OnConfigureMessage);
  53. SubscribeLocalEvent<ConfigurableNavMapBeaconComponent, MapInitEvent>(OnConfigurableMapInit);
  54. SubscribeLocalEvent<ConfigurableNavMapBeaconComponent, ExaminedEvent>(OnConfigurableExamined);
  55. }
  56. private void OnStationInit(StationGridAddedEvent ev)
  57. {
  58. var comp = EnsureComp<NavMapComponent>(ev.GridId);
  59. RefreshGrid(ev.GridId, comp, Comp<MapGridComponent>(ev.GridId));
  60. }
  61. #region: Grid change event handling
  62. private void OnNavMapSplit(ref GridSplitEvent args)
  63. {
  64. if (!_navQuery.TryComp(args.Grid, out var comp))
  65. return;
  66. foreach (var grid in args.NewGrids)
  67. {
  68. var newComp = EnsureComp<NavMapComponent>(grid);
  69. RefreshGrid(grid, newComp, _gridQuery.GetComponent(grid));
  70. }
  71. RefreshGrid(args.Grid, comp, _gridQuery.GetComponent(args.Grid));
  72. }
  73. private NavMapChunk EnsureChunk(NavMapComponent component, Vector2i origin)
  74. {
  75. if (!component.Chunks.TryGetValue(origin, out var chunk))
  76. {
  77. chunk = new(origin);
  78. component.Chunks[origin] = chunk;
  79. }
  80. return chunk;
  81. }
  82. private void OnTileChanged(ref TileChangedEvent ev)
  83. {
  84. if (!ev.EmptyChanged || !_navQuery.TryComp(ev.NewTile.GridUid, out var navMap))
  85. return;
  86. var tile = ev.NewTile.GridIndices;
  87. var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
  88. var chunk = EnsureChunk(navMap, chunkOrigin);
  89. // This could be easily replaced in the future to accommodate diagonal tiles
  90. var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
  91. ref var tileData = ref chunk.TileData[GetTileIndex(relative)];
  92. if (ev.NewTile.IsSpace(_tileDefManager))
  93. {
  94. tileData = 0;
  95. if (PruneEmpty((ev.NewTile.GridUid, navMap), chunk))
  96. return;
  97. }
  98. else
  99. {
  100. tileData = FloorMask;
  101. }
  102. DirtyChunk((ev.NewTile.GridUid, navMap), chunk);
  103. }
  104. private void DirtyChunk(Entity<NavMapComponent> entity, NavMapChunk chunk)
  105. {
  106. if (chunk.LastUpdate == _gameTiming.CurTick)
  107. return;
  108. chunk.LastUpdate = _gameTiming.CurTick;
  109. Dirty(entity);
  110. }
  111. private void OnAirtightChange(ref AirtightChanged args)
  112. {
  113. if (args.AirBlockedChanged)
  114. return;
  115. var gridUid = args.Position.Grid;
  116. if (!_navQuery.TryComp(gridUid, out var navMap) ||
  117. !_gridQuery.TryComp(gridUid, out var mapGrid))
  118. {
  119. return;
  120. }
  121. var chunkOrigin = SharedMapSystem.GetChunkIndices(args.Position.Tile, ChunkSize);
  122. var (newValue, chunk) = RefreshTileEntityContents(gridUid, navMap, mapGrid, chunkOrigin, args.Position.Tile, setFloor: false);
  123. if (newValue == 0 && PruneEmpty((gridUid, navMap), chunk))
  124. return;
  125. DirtyChunk((gridUid, navMap), chunk);
  126. }
  127. #endregion
  128. #region: Beacon event handling
  129. private void OnNavMapBeaconMapInit(EntityUid uid, NavMapBeaconComponent component, MapInitEvent args)
  130. {
  131. if (component.DefaultText == null || component.Text != null)
  132. return;
  133. component.Text = Loc.GetString(component.DefaultText);
  134. Dirty(uid, component);
  135. UpdateNavMapBeaconData(uid, component);
  136. }
  137. private void OnNavMapBeaconAnchor(EntityUid uid, NavMapBeaconComponent component, ref AnchorStateChangedEvent args)
  138. {
  139. UpdateBeaconEnabledVisuals((uid, component));
  140. UpdateNavMapBeaconData(uid, component);
  141. }
  142. private void OnConfigureMessage(Entity<ConfigurableNavMapBeaconComponent> ent, ref NavMapBeaconConfigureBuiMessage args)
  143. {
  144. if (!TryComp<NavMapBeaconComponent>(ent, out var beacon))
  145. return;
  146. if (beacon.Text == args.Text &&
  147. beacon.Color == args.Color &&
  148. beacon.Enabled == args.Enabled)
  149. return;
  150. _adminLog.Add(LogType.Action, LogImpact.Medium,
  151. $"{ToPrettyString(args.Actor):player} configured NavMapBeacon \'{ToPrettyString(ent):entity}\' with text \'{args.Text}\', color {args.Color.ToHexNoAlpha()}, and {(args.Enabled ? "enabled" : "disabled")} it.");
  152. if (TryComp<WarpPointComponent>(ent, out var warpPoint))
  153. {
  154. warpPoint.Location = args.Text;
  155. }
  156. beacon.Text = args.Text;
  157. beacon.Color = args.Color;
  158. beacon.Enabled = args.Enabled;
  159. UpdateBeaconEnabledVisuals((ent, beacon));
  160. UpdateNavMapBeaconData(ent, beacon);
  161. }
  162. private void OnConfigurableMapInit(Entity<ConfigurableNavMapBeaconComponent> ent, ref MapInitEvent args)
  163. {
  164. if (!TryComp<NavMapBeaconComponent>(ent, out var navMap))
  165. return;
  166. // We set this on mapinit just in case the text was edited via VV or something.
  167. if (TryComp<WarpPointComponent>(ent, out var warpPoint))
  168. warpPoint.Location = navMap.Text;
  169. UpdateBeaconEnabledVisuals((ent, navMap));
  170. }
  171. private void OnConfigurableExamined(Entity<ConfigurableNavMapBeaconComponent> ent, ref ExaminedEvent args)
  172. {
  173. if (!args.IsInDetailsRange || !TryComp<NavMapBeaconComponent>(ent, out var navMap))
  174. return;
  175. args.PushMarkup(Loc.GetString("nav-beacon-examine-text",
  176. ("enabled", navMap.Enabled),
  177. ("color", navMap.Color.ToHexNoAlpha()),
  178. ("label", navMap.Text ?? string.Empty)));
  179. }
  180. #endregion
  181. #region: Grid functions
  182. private void RefreshGrid(EntityUid uid, NavMapComponent component, MapGridComponent mapGrid)
  183. {
  184. // Clear stale data
  185. component.Chunks.Clear();
  186. component.Beacons.Clear();
  187. // Refresh beacons
  188. var query = EntityQueryEnumerator<NavMapBeaconComponent, TransformComponent>();
  189. while (query.MoveNext(out var qUid, out var qNavComp, out var qTransComp))
  190. {
  191. if (qTransComp.ParentUid != uid)
  192. continue;
  193. UpdateNavMapBeaconData(qUid, qNavComp);
  194. }
  195. // Loop over all tiles
  196. var tileRefs = _mapSystem.GetAllTiles(uid, mapGrid);
  197. foreach (var tileRef in tileRefs)
  198. {
  199. var tile = tileRef.GridIndices;
  200. var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
  201. var chunk = EnsureChunk(component, chunkOrigin);
  202. chunk.LastUpdate = _gameTiming.CurTick;
  203. RefreshTileEntityContents(uid, component, mapGrid, chunkOrigin, tile, setFloor: true);
  204. }
  205. Dirty(uid, component);
  206. }
  207. private (int NewVal, NavMapChunk Chunk) RefreshTileEntityContents(EntityUid uid,
  208. NavMapComponent component,
  209. MapGridComponent mapGrid,
  210. Vector2i chunkOrigin,
  211. Vector2i tile,
  212. bool setFloor)
  213. {
  214. var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
  215. var chunk = EnsureChunk(component, chunkOrigin);
  216. ref var tileData = ref chunk.TileData[GetTileIndex(relative)];
  217. // Clear all data except for floor bits
  218. if (setFloor)
  219. tileData = FloorMask;
  220. else
  221. tileData &= FloorMask;
  222. var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(uid, mapGrid, tile);
  223. while (enumerator.MoveNext(out var ent))
  224. {
  225. if (!_airtightQuery.TryComp(ent, out var airtight))
  226. continue;
  227. var category = GetEntityType(ent.Value);
  228. if (category == NavMapChunkType.Invalid)
  229. continue;
  230. var directions = (int)airtight.AirBlockedDirection;
  231. tileData |= directions << (int) category;
  232. }
  233. // Remove walls that intersect with doors (unless they can both physically fit on the same tile)
  234. // TODO NAVMAP why can this even happen?
  235. // Is this for blast-doors or something?
  236. // Shift airlock bits over to the wall bits
  237. var shiftedAirlockBits = (tileData & AirlockMask) >> ((int) NavMapChunkType.Airlock - (int) NavMapChunkType.Wall);
  238. // And then mask door bits
  239. tileData &= ~shiftedAirlockBits;
  240. return (tileData, chunk);
  241. }
  242. private bool PruneEmpty(Entity<NavMapComponent> entity, NavMapChunk chunk)
  243. {
  244. foreach (var val in chunk.TileData)
  245. {
  246. // TODO NAVMAP SIMD
  247. if (val != 0)
  248. return false;
  249. }
  250. entity.Comp.Chunks.Remove(chunk.Origin);
  251. Dirty(entity);
  252. return true;
  253. }
  254. #endregion
  255. #region: Beacon functions
  256. private void UpdateNavMapBeaconData(EntityUid uid, NavMapBeaconComponent component, TransformComponent? xform = null)
  257. {
  258. if (!Resolve(uid, ref xform))
  259. return;
  260. if (xform.GridUid == null)
  261. return;
  262. if (!_navQuery.TryComp(xform.GridUid, out var navMap))
  263. return;
  264. var meta = MetaData(uid);
  265. var changed = navMap.Beacons.Remove(meta.NetEntity);
  266. if (TryCreateNavMapBeaconData(uid, component, xform, meta, out var beaconData))
  267. {
  268. navMap.Beacons.Add(meta.NetEntity, beaconData.Value);
  269. changed = true;
  270. }
  271. if (changed)
  272. Dirty(xform.GridUid.Value, navMap);
  273. }
  274. private void UpdateBeaconEnabledVisuals(Entity<NavMapBeaconComponent> ent)
  275. {
  276. _appearance.SetData(ent, NavMapBeaconVisuals.Enabled, ent.Comp.Enabled && Transform(ent).Anchored);
  277. }
  278. /// <summary>
  279. /// Sets the beacon's Enabled field and refreshes the grid.
  280. /// </summary>
  281. public void SetBeaconEnabled(EntityUid uid, bool enabled, NavMapBeaconComponent? comp = null)
  282. {
  283. if (!Resolve(uid, ref comp) || comp.Enabled == enabled)
  284. return;
  285. comp.Enabled = enabled;
  286. UpdateBeaconEnabledVisuals((uid, comp));
  287. }
  288. /// <summary>
  289. /// Toggles the beacon's Enabled field and refreshes the grid.
  290. /// </summary>
  291. public void ToggleBeacon(EntityUid uid, NavMapBeaconComponent? comp = null)
  292. {
  293. if (!Resolve(uid, ref comp))
  294. return;
  295. SetBeaconEnabled(uid, !comp.Enabled, comp);
  296. }
  297. /// <summary>
  298. /// For a given position, tries to find the nearest configurable beacon that is marked as visible.
  299. /// This is used for things like announcements where you want to find the closest "landmark" to something.
  300. /// </summary>
  301. [PublicAPI]
  302. public bool TryGetNearestBeacon(Entity<TransformComponent?> ent,
  303. [NotNullWhen(true)] out Entity<NavMapBeaconComponent>? beacon,
  304. [NotNullWhen(true)] out MapCoordinates? beaconCoords)
  305. {
  306. beacon = null;
  307. beaconCoords = null;
  308. if (!Resolve(ent, ref ent.Comp))
  309. return false;
  310. return TryGetNearestBeacon(_transformSystem.GetMapCoordinates(ent, ent.Comp), out beacon, out beaconCoords);
  311. }
  312. /// <summary>
  313. /// For a given position, tries to find the nearest configurable beacon that is marked as visible.
  314. /// This is used for things like announcements where you want to find the closest "landmark" to something.
  315. /// </summary>
  316. public bool TryGetNearestBeacon(MapCoordinates coordinates,
  317. [NotNullWhen(true)] out Entity<NavMapBeaconComponent>? beacon,
  318. [NotNullWhen(true)] out MapCoordinates? beaconCoords)
  319. {
  320. beacon = null;
  321. beaconCoords = null;
  322. var minDistance = float.PositiveInfinity;
  323. var query = EntityQueryEnumerator<ConfigurableNavMapBeaconComponent, NavMapBeaconComponent, TransformComponent>();
  324. while (query.MoveNext(out var uid, out _, out var navBeacon, out var xform))
  325. {
  326. if (!navBeacon.Enabled)
  327. continue;
  328. if (navBeacon.Text == null)
  329. continue;
  330. if (coordinates.MapId != xform.MapID)
  331. continue;
  332. var coords = _transformSystem.GetWorldPosition(xform);
  333. var distanceSquared = (coordinates.Position - coords).LengthSquared();
  334. if (!float.IsInfinity(minDistance) && distanceSquared >= minDistance)
  335. continue;
  336. minDistance = distanceSquared;
  337. beacon = (uid, navBeacon);
  338. beaconCoords = new MapCoordinates(coords, xform.MapID);
  339. }
  340. return beacon != null;
  341. }
  342. /// <summary>
  343. /// Returns a string describing the rough distance and direction
  344. /// to the position of <paramref name="ent"/> from the nearest beacon.
  345. /// </summary>
  346. [PublicAPI]
  347. public string GetNearestBeaconString(Entity<TransformComponent?> ent)
  348. {
  349. if (!Resolve(ent, ref ent.Comp))
  350. return Loc.GetString("nav-beacon-pos-no-beacons");
  351. return GetNearestBeaconString(_transformSystem.GetMapCoordinates(ent, ent.Comp));
  352. }
  353. /// <summary>
  354. /// Returns a string describing the rough distance and direction
  355. /// to <paramref name="coordinates"/> from the nearest beacon.
  356. /// </summary>
  357. public string GetNearestBeaconString(MapCoordinates coordinates)
  358. {
  359. if (!TryGetNearestBeacon(coordinates, out var beacon, out var pos))
  360. return Loc.GetString("nav-beacon-pos-no-beacons");
  361. var gridOffset = Angle.Zero;
  362. if (_mapManager.TryFindGridAt(pos.Value, out var grid, out _))
  363. gridOffset = Transform(grid).LocalRotation;
  364. // get the angle between the two positions, adjusted for the grid rotation so that
  365. // we properly preserve north in relation to the grid.
  366. var offset = coordinates.Position - pos.Value.Position;
  367. var dir = offset.ToWorldAngle();
  368. var adjustedDir = (dir - gridOffset).GetDir();
  369. var length = offset.Length();
  370. if (length < CloseDistance)
  371. {
  372. return Loc.GetString("nav-beacon-pos-format",
  373. ("color", beacon.Value.Comp.Color),
  374. ("marker", beacon.Value.Comp.Text!));
  375. }
  376. var modifier = length > FarDistance
  377. ? Loc.GetString("nav-beacon-pos-format-direction-mod-far")
  378. : string.Empty;
  379. // we can null suppress the text being null because TryGetNearestVisibleStationBeacon always gives us a beacon with not-null text.
  380. return Loc.GetString("nav-beacon-pos-format-direction",
  381. ("modifier", modifier),
  382. ("direction", ContentLocalizationManager.FormatDirection(adjustedDir).ToLowerInvariant()),
  383. ("color", beacon.Value.Comp.Color),
  384. ("marker", beacon.Value.Comp.Text!));
  385. }
  386. #endregion
  387. }