StationAiVisionSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. using Content.Shared.StationAi;
  2. using Robust.Shared.Map.Components;
  3. using Robust.Shared.Physics;
  4. using Robust.Shared.Threading;
  5. using Robust.Shared.Utility;
  6. namespace Content.Shared.Silicons.StationAi;
  7. public sealed class StationAiVisionSystem : EntitySystem
  8. {
  9. /*
  10. * This class handles 2 things:
  11. * 1. It handles general "what tiles are visible" line of sight checks.
  12. * 2. It does single-tile lookups to tell if they're visible or not with support for a faster range-only path.
  13. */
  14. [Dependency] private readonly IParallelManager _parallel = default!;
  15. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  16. [Dependency] private readonly SharedMapSystem _maps = default!;
  17. [Dependency] private readonly SharedTransformSystem _xforms = default!;
  18. private SeedJob _seedJob;
  19. private ViewJob _job;
  20. private readonly HashSet<Entity<OccluderComponent>> _occluders = new();
  21. private readonly HashSet<Entity<StationAiVisionComponent>> _seeds = new();
  22. private readonly HashSet<Vector2i> _viewportTiles = new();
  23. private EntityQuery<OccluderComponent> _occluderQuery;
  24. // Dummy set
  25. private readonly HashSet<Vector2i> _singleTiles = new();
  26. // Occupied tiles per-run.
  27. // For now it's only 1-grid supported but updating to TileRefs if required shouldn't be too hard.
  28. private readonly HashSet<Vector2i> _opaque = new();
  29. /// <summary>
  30. /// Do we skip line of sight checks and just check vision ranges.
  31. /// </summary>
  32. private bool FastPath;
  33. public override void Initialize()
  34. {
  35. base.Initialize();
  36. _occluderQuery = GetEntityQuery<OccluderComponent>();
  37. _seedJob = new()
  38. {
  39. System = this,
  40. };
  41. _job = new ViewJob()
  42. {
  43. EntManager = EntityManager,
  44. Maps = _maps,
  45. System = this,
  46. VisibleTiles = _singleTiles,
  47. };
  48. }
  49. /// <summary>
  50. /// Returns whether a tile is accessible based on vision.
  51. /// </summary>
  52. public bool IsAccessible(Entity<BroadphaseComponent, MapGridComponent> grid, Vector2i tile, float expansionSize = 8.5f, bool fastPath = false)
  53. {
  54. _viewportTiles.Clear();
  55. _opaque.Clear();
  56. _seeds.Clear();
  57. _viewportTiles.Add(tile);
  58. var localBounds = _lookup.GetLocalBounds(tile, grid.Comp2.TileSize);
  59. var expandedBounds = localBounds.Enlarged(expansionSize);
  60. _seedJob.Grid = (grid.Owner, grid.Comp2);
  61. _seedJob.ExpandedBounds = expandedBounds;
  62. _parallel.ProcessNow(_seedJob);
  63. _job.Data.Clear();
  64. FastPath = fastPath;
  65. foreach (var seed in _seeds)
  66. {
  67. if (!seed.Comp.Enabled)
  68. continue;
  69. _job.Data.Add(seed);
  70. }
  71. if (_seeds.Count == 0)
  72. return false;
  73. // Skip occluders step if we're just doing range checks.
  74. if (!fastPath)
  75. {
  76. var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, expandedBounds, ignoreEmpty: false);
  77. // Get all other relevant tiles.
  78. while (tileEnumerator.MoveNext(out var tileRef))
  79. {
  80. if (IsOccluded(grid, tileRef.GridIndices))
  81. {
  82. _opaque.Add(tileRef.GridIndices);
  83. }
  84. }
  85. }
  86. for (var i = _job.Vis1.Count; i < _job.Data.Count; i++)
  87. {
  88. _job.Vis1.Add(new Dictionary<Vector2i, int>());
  89. _job.Vis2.Add(new Dictionary<Vector2i, int>());
  90. _job.SeedTiles.Add(new HashSet<Vector2i>());
  91. _job.BoundaryTiles.Add(new HashSet<Vector2i>());
  92. }
  93. _singleTiles.Clear();
  94. _job.Grid = (grid.Owner, grid.Comp2);
  95. _job.VisibleTiles = _singleTiles;
  96. _parallel.ProcessNow(_job, _job.Data.Count);
  97. return _job.VisibleTiles.Contains(tile);
  98. }
  99. private bool IsOccluded(Entity<BroadphaseComponent, MapGridComponent> grid, Vector2i tile)
  100. {
  101. var tileBounds = _lookup.GetLocalBounds(tile, grid.Comp2.TileSize).Enlarged(-0.05f);
  102. _occluders.Clear();
  103. _lookup.GetLocalEntitiesIntersecting((grid.Owner, grid.Comp1), tileBounds, _occluders, query: _occluderQuery, flags: LookupFlags.Static | LookupFlags.Approximate);
  104. var anyOccluders = false;
  105. foreach (var occluder in _occluders)
  106. {
  107. if (!occluder.Comp.Enabled)
  108. continue;
  109. anyOccluders = true;
  110. break;
  111. }
  112. return anyOccluders;
  113. }
  114. /// <summary>
  115. /// Gets a byond-equivalent for tiles in the specified worldAABB.
  116. /// </summary>
  117. /// <param name="expansionSize">How much to expand the bounds before to find vision intersecting it. Makes this the largest vision size + 1 tile.</param>
  118. public void GetView(Entity<BroadphaseComponent, MapGridComponent> grid, Box2Rotated worldBounds, HashSet<Vector2i> visibleTiles, float expansionSize = 8.5f)
  119. {
  120. _viewportTiles.Clear();
  121. _opaque.Clear();
  122. _seeds.Clear();
  123. // TODO: Would be nice to be able to run this while running the other stuff.
  124. _seedJob.Grid = (grid.Owner, grid.Comp2);
  125. var invMatrix = _xforms.GetInvWorldMatrix(grid);
  126. var localAabb = invMatrix.TransformBox(worldBounds);
  127. var enlargedLocalAabb = invMatrix.TransformBox(worldBounds.Enlarged(expansionSize));
  128. _seedJob.ExpandedBounds = enlargedLocalAabb;
  129. _parallel.ProcessNow(_seedJob);
  130. _job.Data.Clear();
  131. FastPath = false;
  132. foreach (var seed in _seeds)
  133. {
  134. if (!seed.Comp.Enabled)
  135. continue;
  136. _job.Data.Add(seed);
  137. }
  138. if (_seeds.Count == 0)
  139. return;
  140. // Get viewport tiles
  141. var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAabb, ignoreEmpty: false);
  142. while (tileEnumerator.MoveNext(out var tileRef))
  143. {
  144. if (IsOccluded(grid, tileRef.GridIndices))
  145. {
  146. _opaque.Add(tileRef.GridIndices);
  147. }
  148. _viewportTiles.Add(tileRef.GridIndices);
  149. }
  150. tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, enlargedLocalAabb, ignoreEmpty: false);
  151. while (tileEnumerator.MoveNext(out var tileRef))
  152. {
  153. if (_viewportTiles.Contains(tileRef.GridIndices))
  154. continue;
  155. if (IsOccluded(grid, tileRef.GridIndices))
  156. {
  157. _opaque.Add(tileRef.GridIndices);
  158. }
  159. }
  160. // Wait for seed job here
  161. for (var i = _job.Vis1.Count; i < _job.Data.Count; i++)
  162. {
  163. _job.Vis1.Add(new Dictionary<Vector2i, int>());
  164. _job.Vis2.Add(new Dictionary<Vector2i, int>());
  165. _job.SeedTiles.Add(new HashSet<Vector2i>());
  166. _job.BoundaryTiles.Add(new HashSet<Vector2i>());
  167. }
  168. _job.Grid = (grid.Owner, grid.Comp2);
  169. _job.VisibleTiles = visibleTiles;
  170. _parallel.ProcessNow(_job, _job.Data.Count);
  171. }
  172. private int GetMaxDelta(Vector2i tile, Vector2i center)
  173. {
  174. var delta = tile - center;
  175. return Math.Max(Math.Abs(delta.X), Math.Abs(delta.Y));
  176. }
  177. private int GetSumDelta(Vector2i tile, Vector2i center)
  178. {
  179. var delta = tile - center;
  180. return Math.Abs(delta.X) + Math.Abs(delta.Y);
  181. }
  182. /// <summary>
  183. /// Checks if any of a tile's neighbors are visible.
  184. /// </summary>
  185. private bool CheckNeighborsVis(
  186. Dictionary<Vector2i, int> vis,
  187. Vector2i index,
  188. int d)
  189. {
  190. for (var x = -1; x <= 1; x++)
  191. {
  192. for (var y = -1; y <= 1; y++)
  193. {
  194. if (x == 0 && y == 0)
  195. continue;
  196. var neighbor = index + new Vector2i(x, y);
  197. var neighborD = vis.GetValueOrDefault(neighbor);
  198. if (neighborD == d)
  199. return true;
  200. }
  201. }
  202. return false;
  203. }
  204. /// <summary>
  205. /// Checks whether this tile fits the definition of a "corner"
  206. /// </summary>
  207. private bool IsCorner(
  208. HashSet<Vector2i> tiles,
  209. HashSet<Vector2i> blocked,
  210. Dictionary<Vector2i, int> vis1,
  211. Vector2i index,
  212. Vector2i delta)
  213. {
  214. var diagonalIndex = index + delta;
  215. if (!tiles.TryGetValue(diagonalIndex, out var diagonal))
  216. return false;
  217. var cardinal1 = new Vector2i(index.X, diagonal.Y);
  218. var cardinal2 = new Vector2i(diagonal.X, index.Y);
  219. return vis1.GetValueOrDefault(diagonal) != 0 &&
  220. vis1.GetValueOrDefault(cardinal1) != 0 &&
  221. vis1.GetValueOrDefault(cardinal2) != 0 &&
  222. blocked.Contains(cardinal1) &&
  223. blocked.Contains(cardinal2) &&
  224. !blocked.Contains(diagonal);
  225. }
  226. /// <summary>
  227. /// Gets the relevant vision seeds for later.
  228. /// </summary>
  229. private record struct SeedJob() : IRobustJob
  230. {
  231. public required StationAiVisionSystem System;
  232. public Entity<MapGridComponent> Grid;
  233. public Box2 ExpandedBounds;
  234. public void Execute()
  235. {
  236. System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds, flags: LookupFlags.All | LookupFlags.Approximate);
  237. }
  238. }
  239. private record struct ViewJob() : IParallelRobustJob
  240. {
  241. public int BatchSize => 1;
  242. public required IEntityManager EntManager;
  243. public required SharedMapSystem Maps;
  244. public required StationAiVisionSystem System;
  245. public Entity<MapGridComponent> Grid;
  246. public List<Entity<StationAiVisionComponent>> Data = new();
  247. public required HashSet<Vector2i> VisibleTiles;
  248. public readonly List<Dictionary<Vector2i, int>> Vis1 = new();
  249. public readonly List<Dictionary<Vector2i, int>> Vis2 = new();
  250. public readonly List<HashSet<Vector2i>> SeedTiles = new();
  251. public readonly List<HashSet<Vector2i>> BoundaryTiles = new();
  252. public void Execute(int index)
  253. {
  254. var seed = Data[index];
  255. var seedXform = EntManager.GetComponent<TransformComponent>(seed);
  256. // Fastpath just get tiles in range.
  257. // Either xray-vision or system is doing a quick-and-dirty check.
  258. if (!seed.Comp.Occluded || System.FastPath)
  259. {
  260. var squircles = Maps.GetLocalTilesIntersecting(Grid.Owner,
  261. Grid.Comp,
  262. new Circle(System._xforms.GetWorldPosition(seedXform), seed.Comp.Range), ignoreEmpty: false);
  263. lock (VisibleTiles)
  264. {
  265. foreach (var tile in squircles)
  266. {
  267. VisibleTiles.Add(tile.GridIndices);
  268. }
  269. }
  270. return;
  271. }
  272. // Code based upon https://github.com/OpenDreamProject/OpenDream/blob/c4a3828ccb997bf3722673620460ebb11b95ccdf/OpenDreamShared/Dream/ViewAlgorithm.cs
  273. var range = seed.Comp.Range;
  274. var vis1 = Vis1[index];
  275. var vis2 = Vis2[index];
  276. var seedTiles = SeedTiles[index];
  277. var boundary = BoundaryTiles[index];
  278. // Cleanup last run
  279. vis1.Clear();
  280. vis2.Clear();
  281. seedTiles.Clear();
  282. boundary.Clear();
  283. var maxDepthMax = 0;
  284. var sumDepthMax = 0;
  285. var eyePos = Maps.GetTileRef(Grid.Owner, Grid, seedXform.Coordinates).GridIndices;
  286. for (var x = Math.Floor(eyePos.X - range); x <= eyePos.X + range; x++)
  287. {
  288. for (var y = Math.Floor(eyePos.Y - range); y <= eyePos.Y + range; y++)
  289. {
  290. var tile = new Vector2i((int)x, (int)y);
  291. var delta = tile - eyePos;
  292. var xDelta = Math.Abs(delta.X);
  293. var yDelta = Math.Abs(delta.Y);
  294. var deltaSum = xDelta + yDelta;
  295. maxDepthMax = Math.Max(maxDepthMax, Math.Max(xDelta, yDelta));
  296. sumDepthMax = Math.Max(sumDepthMax, deltaSum);
  297. seedTiles.Add(tile);
  298. }
  299. }
  300. // Step 3, Diagonal shadow loop
  301. for (var d = 0; d < maxDepthMax; d++)
  302. {
  303. foreach (var tile in seedTiles)
  304. {
  305. var maxDelta = System.GetMaxDelta(tile, eyePos);
  306. if (maxDelta == d + 1 && System.CheckNeighborsVis(vis2, tile, d))
  307. {
  308. vis2[tile] = (System._opaque.Contains(tile) ? -1 : d + 1);
  309. }
  310. }
  311. }
  312. // Step 4, Straight shadow loop
  313. for (var d = 0; d < sumDepthMax; d++)
  314. {
  315. foreach (var tile in seedTiles)
  316. {
  317. var sumDelta = System.GetSumDelta(tile, eyePos);
  318. if (sumDelta == d + 1 && System.CheckNeighborsVis(vis1, tile, d))
  319. {
  320. if (System._opaque.Contains(tile))
  321. {
  322. vis1[tile] = -1;
  323. }
  324. else if (vis2.GetValueOrDefault(tile) != 0)
  325. {
  326. vis1[tile] = d + 1;
  327. }
  328. }
  329. }
  330. }
  331. // Add the eye itself
  332. vis1[eyePos] = 1;
  333. // Step 6.
  334. // Step 7.
  335. // Step 8.
  336. foreach (var tile in seedTiles)
  337. {
  338. vis2[tile] = vis1.GetValueOrDefault(tile, 0);
  339. }
  340. // Step 9
  341. foreach (var tile in seedTiles)
  342. {
  343. if (!System._opaque.Contains(tile))
  344. continue;
  345. var tileVis1 = vis1.GetValueOrDefault(tile);
  346. if (tileVis1 != 0)
  347. continue;
  348. if (System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.UpRight) ||
  349. System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.UpLeft) ||
  350. System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.DownLeft) ||
  351. System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.DownRight))
  352. {
  353. boundary.Add(tile);
  354. }
  355. }
  356. // Make all wall/corner tiles visible
  357. foreach (var tile in boundary)
  358. {
  359. vis1[tile] = -1;
  360. }
  361. // vis2 is what we care about for LOS.
  362. foreach (var tile in seedTiles)
  363. {
  364. // If not in viewport don't care.
  365. if (!System._viewportTiles.Contains(tile))
  366. continue;
  367. var tileVis = vis1.GetValueOrDefault(tile, 0);
  368. if (tileVis != 0)
  369. {
  370. // No idea if it's better to do this inside or out.
  371. lock (VisibleTiles)
  372. {
  373. VisibleTiles.Add(tile);
  374. }
  375. }
  376. }
  377. }
  378. }
  379. }