PathfindingSystem.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. using System.Linq;
  2. using System.Numerics;
  3. using System.Text;
  4. using Content.Shared.NPC;
  5. using Robust.Client.GameObjects;
  6. using Robust.Client.Graphics;
  7. using Robust.Client.Input;
  8. using Robust.Client.ResourceManagement;
  9. using Robust.Shared.Enums;
  10. using Robust.Shared.Map;
  11. using Robust.Shared.Map.Components;
  12. using Robust.Shared.Timing;
  13. using Robust.Shared.Utility;
  14. namespace Content.Client.NPC
  15. {
  16. public sealed class PathfindingSystem : SharedPathfindingSystem
  17. {
  18. [Dependency] private readonly IEyeManager _eyeManager = default!;
  19. [Dependency] private readonly IGameTiming _timing = default!;
  20. [Dependency] private readonly IInputManager _inputManager = default!;
  21. [Dependency] private readonly IMapManager _mapManager = default!;
  22. [Dependency] private readonly IResourceCache _cache = default!;
  23. [Dependency] private readonly NPCSteeringSystem _steering = default!;
  24. [Dependency] private readonly MapSystem _mapSystem = default!;
  25. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  26. public PathfindingDebugMode Modes
  27. {
  28. get => _modes;
  29. set
  30. {
  31. var overlayManager = IoCManager.Resolve<IOverlayManager>();
  32. if (value == PathfindingDebugMode.None)
  33. {
  34. Breadcrumbs.Clear();
  35. Polys.Clear();
  36. overlayManager.RemoveOverlay<PathfindingOverlay>();
  37. }
  38. else if (!overlayManager.HasOverlay<PathfindingOverlay>())
  39. {
  40. overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
  41. }
  42. if ((value & PathfindingDebugMode.Steering) != 0x0)
  43. {
  44. _steering.DebugEnabled = true;
  45. }
  46. else
  47. {
  48. _steering.DebugEnabled = false;
  49. }
  50. _modes = value;
  51. RaiseNetworkEvent(new RequestPathfindingDebugMessage()
  52. {
  53. Mode = _modes,
  54. });
  55. }
  56. }
  57. private PathfindingDebugMode _modes = PathfindingDebugMode.None;
  58. // It's debug data IDC if it doesn't support snapshots I just want something fast.
  59. public Dictionary<NetEntity, Dictionary<Vector2i, List<PathfindingBreadcrumb>>> Breadcrumbs = new();
  60. public Dictionary<NetEntity, Dictionary<Vector2i, Dictionary<Vector2i, List<DebugPathPoly>>>> Polys = new();
  61. public readonly List<(TimeSpan Time, PathRouteMessage Message)> Routes = new();
  62. public override void Initialize()
  63. {
  64. base.Initialize();
  65. SubscribeNetworkEvent<PathBreadcrumbsMessage>(OnBreadcrumbs);
  66. SubscribeNetworkEvent<PathBreadcrumbsRefreshMessage>(OnBreadcrumbsRefresh);
  67. SubscribeNetworkEvent<PathPolysMessage>(OnPolys);
  68. SubscribeNetworkEvent<PathPolysRefreshMessage>(OnPolysRefresh);
  69. SubscribeNetworkEvent<PathRouteMessage>(OnRoute);
  70. }
  71. public override void Update(float frameTime)
  72. {
  73. base.Update(frameTime);
  74. if (!_timing.IsFirstTimePredicted)
  75. return;
  76. for (var i = 0; i < Routes.Count; i++)
  77. {
  78. var route = Routes[i];
  79. if (_timing.RealTime < route.Time)
  80. break;
  81. Routes.RemoveAt(i);
  82. }
  83. }
  84. private void OnRoute(PathRouteMessage ev)
  85. {
  86. Routes.Add((_timing.RealTime + TimeSpan.FromSeconds(0.5), ev));
  87. }
  88. private void OnPolys(PathPolysMessage ev)
  89. {
  90. Polys = ev.Polys;
  91. }
  92. private void OnPolysRefresh(PathPolysRefreshMessage ev)
  93. {
  94. var chunks = Polys.GetOrNew(ev.GridUid);
  95. chunks[ev.Origin] = ev.Polys;
  96. }
  97. public override void Shutdown()
  98. {
  99. base.Shutdown();
  100. // Don't send any messages to server, just shut down quietly.
  101. _modes = PathfindingDebugMode.None;
  102. }
  103. private void OnBreadcrumbs(PathBreadcrumbsMessage ev)
  104. {
  105. Breadcrumbs = ev.Breadcrumbs;
  106. }
  107. private void OnBreadcrumbsRefresh(PathBreadcrumbsRefreshMessage ev)
  108. {
  109. if (!Breadcrumbs.TryGetValue(ev.GridUid, out var chunks))
  110. return;
  111. chunks[ev.Origin] = ev.Data;
  112. }
  113. }
  114. public sealed class PathfindingOverlay : Overlay
  115. {
  116. private readonly IEntityManager _entManager;
  117. private readonly IEyeManager _eyeManager;
  118. private readonly IInputManager _inputManager;
  119. private readonly IMapManager _mapManager;
  120. private readonly PathfindingSystem _system;
  121. private readonly MapSystem _mapSystem;
  122. private readonly SharedTransformSystem _transformSystem;
  123. public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
  124. private readonly Font _font;
  125. private List<Entity<MapGridComponent>> _grids = new();
  126. public PathfindingOverlay(
  127. IEntityManager entManager,
  128. IEyeManager eyeManager,
  129. IInputManager inputManager,
  130. IMapManager mapManager,
  131. IResourceCache cache,
  132. PathfindingSystem system,
  133. MapSystem mapSystem,
  134. SharedTransformSystem transformSystem)
  135. {
  136. _entManager = entManager;
  137. _eyeManager = eyeManager;
  138. _inputManager = inputManager;
  139. _mapManager = mapManager;
  140. _system = system;
  141. _mapSystem = mapSystem;
  142. _transformSystem = transformSystem;
  143. _font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
  144. }
  145. protected override void Draw(in OverlayDrawArgs args)
  146. {
  147. switch (args.DrawingHandle)
  148. {
  149. case DrawingHandleScreen screenHandle:
  150. DrawScreen(args, screenHandle);
  151. break;
  152. case DrawingHandleWorld worldHandle:
  153. DrawWorld(args, worldHandle);
  154. break;
  155. }
  156. }
  157. private void DrawScreen(OverlayDrawArgs args, DrawingHandleScreen screenHandle)
  158. {
  159. var mousePos = _inputManager.MouseScreenPosition;
  160. var mouseWorldPos = _eyeManager.PixelToMap(mousePos);
  161. var aabb = new Box2(mouseWorldPos.Position - SharedPathfindingSystem.ChunkSizeVec, mouseWorldPos.Position + SharedPathfindingSystem.ChunkSizeVec);
  162. var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
  163. if ((_system.Modes & PathfindingDebugMode.Crumb) != 0x0 &&
  164. mouseWorldPos.MapId == args.MapId)
  165. {
  166. var found = false;
  167. _grids.Clear();
  168. _mapManager.FindGridsIntersecting(mouseWorldPos.MapId, aabb, ref _grids);
  169. foreach (var grid in _grids)
  170. {
  171. var netGrid = _entManager.GetNetEntity(grid);
  172. if (found || !_system.Breadcrumbs.TryGetValue(netGrid, out var crumbs) || !xformQuery.TryGetComponent(grid, out var gridXform))
  173. continue;
  174. var (_, _, worldMatrix, invWorldMatrix) = _transformSystem.GetWorldPositionRotationMatrixWithInv(gridXform);
  175. var localAABB = invWorldMatrix.TransformBox(aabb.Enlarged(float.Epsilon - SharedPathfindingSystem.ChunkSize));
  176. foreach (var chunk in crumbs)
  177. {
  178. if (found)
  179. continue;
  180. var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
  181. var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
  182. if (!chunkAABB.Intersects(localAABB))
  183. continue;
  184. PathfindingBreadcrumb? nearest = null;
  185. var nearestDistance = float.MaxValue;
  186. foreach (var crumb in chunk.Value)
  187. {
  188. var crumbMapPos = Vector2.Transform(_system.GetCoordinate(chunk.Key, crumb.Coordinates), worldMatrix);
  189. var distance = (crumbMapPos - mouseWorldPos.Position).Length();
  190. if (distance < nearestDistance)
  191. {
  192. nearestDistance = distance;
  193. nearest = crumb;
  194. }
  195. }
  196. if (nearest != null)
  197. {
  198. var text = new StringBuilder();
  199. // Sandbox moment
  200. var coords = $"Point coordinates: {nearest.Value.Coordinates.ToString()}";
  201. var gridCoords =
  202. $"Grid coordinates: {_system.GetCoordinate(chunk.Key, nearest.Value.Coordinates).ToString()}";
  203. var layer = $"Layer: {nearest.Value.Data.CollisionLayer.ToString()}";
  204. var mask = $"Mask: {nearest.Value.Data.CollisionMask.ToString()}";
  205. text.AppendLine(coords);
  206. text.AppendLine(gridCoords);
  207. text.AppendLine(layer);
  208. text.AppendLine(mask);
  209. text.AppendLine($"Flags:");
  210. foreach (var flag in Enum.GetValues<PathfindingBreadcrumbFlag>())
  211. {
  212. if ((flag & nearest.Value.Data.Flags) == 0x0)
  213. continue;
  214. var flagStr = $"- {flag.ToString()}";
  215. text.AppendLine(flagStr);
  216. }
  217. screenHandle.DrawString(_font, mousePos.Position, text.ToString());
  218. found = true;
  219. break;
  220. }
  221. }
  222. }
  223. }
  224. if ((_system.Modes & PathfindingDebugMode.Poly) != 0x0 &&
  225. mouseWorldPos.MapId == args.MapId)
  226. {
  227. if (!_mapManager.TryFindGridAt(mouseWorldPos, out var gridUid, out var grid) || !xformQuery.TryGetComponent(gridUid, out var gridXform))
  228. return;
  229. if (!_system.Polys.TryGetValue(_entManager.GetNetEntity(gridUid), out var data))
  230. return;
  231. var tileRef = _mapSystem.GetTileRef(gridUid, grid, mouseWorldPos);
  232. var localPos = tileRef.GridIndices;
  233. var chunkOrigin = localPos / SharedPathfindingSystem.ChunkSize;
  234. if (!data.TryGetValue(chunkOrigin, out var chunk) ||
  235. !chunk.TryGetValue(new Vector2i(localPos.X % SharedPathfindingSystem.ChunkSize,
  236. localPos.Y % SharedPathfindingSystem.ChunkSize), out var tile))
  237. {
  238. return;
  239. }
  240. var invGridMatrix = _transformSystem.GetInvWorldMatrix(gridXform);
  241. DebugPathPoly? nearest = null;
  242. foreach (var poly in tile)
  243. {
  244. if (poly.Box.Contains(Vector2.Transform(mouseWorldPos.Position, invGridMatrix)))
  245. {
  246. nearest = poly;
  247. break;
  248. }
  249. }
  250. if (nearest != null)
  251. {
  252. var text = new StringBuilder();
  253. /*
  254. // Sandbox moment
  255. var coords = $"Point coordinates: {nearest.Value.Coordinates.ToString()}";
  256. var gridCoords =
  257. $"Grid coordinates: {_system.GetCoordinate(chunk.Key, nearest.Value.Coordinates).ToString()}";
  258. var layer = $"Layer: {nearest.Value.Data.CollisionLayer.ToString()}";
  259. var mask = $"Mask: {nearest.Value.Data.CollisionMask.ToString()}";
  260. text.AppendLine(coords);
  261. text.AppendLine(gridCoords);
  262. text.AppendLine(layer);
  263. text.AppendLine(mask);
  264. text.AppendLine($"Flags:");
  265. foreach (var flag in Enum.GetValues<PathfindingBreadcrumbFlag>())
  266. {
  267. if ((flag & nearest.Value.Data.Flags) == 0x0)
  268. continue;
  269. var flagStr = $"- {flag.ToString()}";
  270. text.AppendLine(flagStr);
  271. }
  272. foreach (var neighbor in )
  273. screenHandle.DrawString(_font, mousePos.Position, text.ToString());
  274. found = true;
  275. break;
  276. */
  277. }
  278. }
  279. }
  280. private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle)
  281. {
  282. var mousePos = _inputManager.MouseScreenPosition;
  283. var mouseWorldPos = _eyeManager.PixelToMap(mousePos);
  284. var aabb = new Box2(mouseWorldPos.Position - Vector2.One / 4f, mouseWorldPos.Position + Vector2.One / 4f);
  285. var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
  286. if ((_system.Modes & PathfindingDebugMode.Breadcrumbs) != 0x0 &&
  287. mouseWorldPos.MapId == args.MapId)
  288. {
  289. _grids.Clear();
  290. _mapManager.FindGridsIntersecting(mouseWorldPos.MapId, aabb, ref _grids);
  291. foreach (var grid in _grids)
  292. {
  293. var netGrid = _entManager.GetNetEntity(grid);
  294. if (!_system.Breadcrumbs.TryGetValue(netGrid, out var crumbs) ||
  295. !xformQuery.TryGetComponent(grid, out var gridXform))
  296. {
  297. continue;
  298. }
  299. var (_, _, worldMatrix, invWorldMatrix) = _transformSystem.GetWorldPositionRotationMatrixWithInv(gridXform);
  300. worldHandle.SetTransform(worldMatrix);
  301. var localAABB = invWorldMatrix.TransformBox(aabb);
  302. foreach (var chunk in crumbs)
  303. {
  304. var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
  305. var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
  306. if (!chunkAABB.Intersects(localAABB))
  307. continue;
  308. foreach (var crumb in chunk.Value)
  309. {
  310. if (crumb.Equals(PathfindingBreadcrumb.Invalid))
  311. {
  312. continue;
  313. }
  314. const float edge = 1f / SharedPathfindingSystem.SubStep / 4f;
  315. var edgeVec = new Vector2(edge, edge);
  316. var masked = crumb.Data.CollisionMask != 0 || crumb.Data.CollisionLayer != 0;
  317. Color color;
  318. if ((crumb.Data.Flags & PathfindingBreadcrumbFlag.Space) != 0x0)
  319. {
  320. color = Color.Green;
  321. }
  322. else if (masked)
  323. {
  324. color = Color.Blue;
  325. }
  326. else
  327. {
  328. color = Color.Orange;
  329. }
  330. var coordinate = _system.GetCoordinate(chunk.Key, crumb.Coordinates);
  331. worldHandle.DrawRect(new Box2(coordinate - edgeVec, coordinate + edgeVec), color.WithAlpha(0.25f));
  332. }
  333. }
  334. }
  335. }
  336. if ((_system.Modes & PathfindingDebugMode.Polys) != 0x0 &&
  337. mouseWorldPos.MapId == args.MapId)
  338. {
  339. _grids.Clear();
  340. _mapManager.FindGridsIntersecting(args.MapId, aabb, ref _grids);
  341. foreach (var grid in _grids)
  342. {
  343. var netGrid = _entManager.GetNetEntity(grid);
  344. if (!_system.Polys.TryGetValue(netGrid, out var data) ||
  345. !xformQuery.TryGetComponent(grid, out var gridXform))
  346. continue;
  347. var (_, _, worldMatrix, invWorldMatrix) = _transformSystem.GetWorldPositionRotationMatrixWithInv(gridXform);
  348. worldHandle.SetTransform(worldMatrix);
  349. var localAABB = invWorldMatrix.TransformBox(aabb);
  350. foreach (var chunk in data)
  351. {
  352. var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
  353. var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
  354. if (!chunkAABB.Intersects(localAABB))
  355. continue;
  356. foreach (var tile in chunk.Value)
  357. {
  358. foreach (var poly in tile.Value)
  359. {
  360. worldHandle.DrawRect(poly.Box, Color.Green.WithAlpha(0.25f));
  361. worldHandle.DrawRect(poly.Box, Color.Red, false);
  362. }
  363. }
  364. }
  365. }
  366. }
  367. if ((_system.Modes & PathfindingDebugMode.PolyNeighbors) != 0x0 &&
  368. mouseWorldPos.MapId == args.MapId)
  369. {
  370. _grids.Clear();
  371. _mapManager.FindGridsIntersecting(args.MapId, aabb, ref _grids);
  372. foreach (var grid in _grids)
  373. {
  374. var netGrid = _entManager.GetNetEntity(grid);
  375. if (!_system.Polys.TryGetValue(netGrid, out var data) ||
  376. !xformQuery.TryGetComponent(grid, out var gridXform))
  377. continue;
  378. var (_, _, worldMatrix, invMatrix) = _transformSystem.GetWorldPositionRotationMatrixWithInv(gridXform);
  379. worldHandle.SetTransform(worldMatrix);
  380. var localAABB = invMatrix.TransformBox(aabb);
  381. foreach (var chunk in data)
  382. {
  383. var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
  384. var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
  385. if (!chunkAABB.Intersects(localAABB))
  386. continue;
  387. foreach (var tile in chunk.Value)
  388. {
  389. foreach (var poly in tile.Value)
  390. {
  391. foreach (var neighborPoly in poly.Neighbors)
  392. {
  393. Color color;
  394. Vector2 neighborPos;
  395. if (neighborPoly.NetEntity != poly.GraphUid)
  396. {
  397. color = Color.Green;
  398. var neighborMap = _transformSystem.ToMapCoordinates(_entManager.GetCoordinates(neighborPoly));
  399. if (neighborMap.MapId != args.MapId)
  400. continue;
  401. neighborPos = Vector2.Transform(neighborMap.Position, invMatrix);
  402. }
  403. else
  404. {
  405. color = Color.Blue;
  406. neighborPos = neighborPoly.Position;
  407. }
  408. worldHandle.DrawLine(poly.Box.Center, neighborPos, color);
  409. }
  410. }
  411. }
  412. }
  413. }
  414. }
  415. if ((_system.Modes & PathfindingDebugMode.Chunks) != 0x0)
  416. {
  417. _grids.Clear();
  418. _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
  419. foreach (var grid in _grids)
  420. {
  421. var netGrid = _entManager.GetNetEntity(grid);
  422. if (!_system.Breadcrumbs.TryGetValue(netGrid, out var crumbs) ||
  423. !xformQuery.TryGetComponent(grid, out var gridXform))
  424. continue;
  425. var (_, _, worldMatrix, invWorldMatrix) = _transformSystem.GetWorldPositionRotationMatrixWithInv(gridXform);
  426. worldHandle.SetTransform(worldMatrix);
  427. var localAABB = invWorldMatrix.TransformBox(args.WorldBounds);
  428. foreach (var chunk in crumbs)
  429. {
  430. var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
  431. var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
  432. if (!chunkAABB.Intersects(localAABB))
  433. continue;
  434. worldHandle.DrawRect(chunkAABB, Color.Red, false);
  435. }
  436. }
  437. }
  438. if ((_system.Modes & PathfindingDebugMode.Routes) != 0x0)
  439. {
  440. foreach (var route in _system.Routes)
  441. {
  442. foreach (var node in route.Message.Path)
  443. {
  444. if (!_entManager.TryGetComponent<TransformComponent>(_entManager.GetEntity(node.GraphUid), out var graphXform))
  445. continue;
  446. worldHandle.SetTransform(_transformSystem.GetWorldMatrix(graphXform));
  447. worldHandle.DrawRect(node.Box, Color.Orange.WithAlpha(0.10f));
  448. }
  449. }
  450. }
  451. if ((_system.Modes & PathfindingDebugMode.RouteCosts) != 0x0)
  452. {
  453. var matrix = EntityUid.Invalid;
  454. foreach (var route in _system.Routes)
  455. {
  456. var highestGScore = route.Message.Costs.Values.Max();
  457. foreach (var (node, cost) in route.Message.Costs)
  458. {
  459. var graph = _entManager.GetEntity(node.GraphUid);
  460. if (matrix != graph)
  461. {
  462. if (!_entManager.TryGetComponent<TransformComponent>(graph, out var graphXform))
  463. continue;
  464. matrix = graph;
  465. worldHandle.SetTransform(_transformSystem.GetWorldMatrix(graphXform));
  466. }
  467. worldHandle.DrawRect(node.Box, new Color(0f, cost / highestGScore, 1f - (cost / highestGScore), 0.10f));
  468. }
  469. }
  470. }
  471. worldHandle.SetTransform(Matrix3x2.Identity);
  472. }
  473. }
  474. }