PathfindingSystem.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. using System.Buffers;
  2. using System.Linq;
  3. using System.Numerics;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using Content.Server.Administration.Managers;
  7. using Content.Server.Destructible;
  8. using Content.Server.NPC.Systems;
  9. using Content.Shared.Access.Components;
  10. using Content.Shared.Administration;
  11. using Content.Shared.Climbing.Components;
  12. using Content.Shared.Doors.Components;
  13. using Content.Shared.NPC;
  14. using Robust.Server.Player;
  15. using Robust.Shared.Enums;
  16. using Robust.Shared.Map;
  17. using Robust.Shared.Map.Components;
  18. using Robust.Shared.Physics;
  19. using Robust.Shared.Physics.Systems;
  20. using Robust.Shared.Player;
  21. using Robust.Shared.Random;
  22. using Robust.Shared.Threading;
  23. using Robust.Shared.Timing;
  24. using Robust.Shared.Utility;
  25. namespace Content.Server.NPC.Pathfinding
  26. {
  27. /// <summary>
  28. /// This system handles pathfinding graph updates as well as dispatches to the pathfinder
  29. /// (90% of what it's doing is graph updates so not much point splitting the 2 roles)
  30. /// </summary>
  31. public sealed partial class PathfindingSystem : SharedPathfindingSystem
  32. {
  33. /*
  34. * I have spent many hours looking at what pathfinding to use
  35. * Ideally we would be able to use something grid based with hierarchy, but the problem is
  36. * we also have triangular / diagonal walls and thindows which makes that not exactly feasible
  37. * Recast is also overkill for our usecase, plus another lib, hence you get this.
  38. *
  39. * See PathfindingSystem.Grid for a description of the grid implementation.
  40. */
  41. [Dependency] private readonly IAdminManager _adminManager = default!;
  42. [Dependency] private readonly IGameTiming _timing = default!;
  43. [Dependency] private readonly IParallelManager _parallel = default!;
  44. [Dependency] private readonly IPlayerManager _playerManager = default!;
  45. [Dependency] private readonly IRobustRandom _random = default!;
  46. [Dependency] private readonly DestructibleSystem _destructible = default!;
  47. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  48. [Dependency] private readonly FixtureSystem _fixtures = default!;
  49. [Dependency] private readonly NPCSystem _npc = default!;
  50. [Dependency] private readonly SharedMapSystem _maps = default!;
  51. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  52. [Dependency] private readonly SharedTransformSystem _transform = default!;
  53. private readonly Dictionary<ICommonSession, PathfindingDebugMode> _subscribedSessions = new();
  54. [ViewVariables]
  55. private readonly List<PathRequest> _pathRequests = new(PathTickLimit);
  56. private static readonly TimeSpan PathTime = TimeSpan.FromMilliseconds(3);
  57. /// <summary>
  58. /// How many paths we can process in a single tick.
  59. /// </summary>
  60. private const int PathTickLimit = 256;
  61. private int _portalIndex;
  62. private readonly Dictionary<int, PathPortal> _portals = new();
  63. private EntityQuery<AccessReaderComponent> _accessQuery;
  64. private EntityQuery<DestructibleComponent> _destructibleQuery;
  65. private EntityQuery<DoorComponent> _doorQuery;
  66. private EntityQuery<ClimbableComponent> _climbableQuery;
  67. private EntityQuery<FixturesComponent> _fixturesQuery;
  68. private EntityQuery<MapGridComponent> _gridQuery;
  69. private EntityQuery<TransformComponent> _xformQuery;
  70. public override void Initialize()
  71. {
  72. base.Initialize();
  73. _accessQuery = GetEntityQuery<AccessReaderComponent>();
  74. _destructibleQuery = GetEntityQuery<DestructibleComponent>();
  75. _doorQuery = GetEntityQuery<DoorComponent>();
  76. _climbableQuery = GetEntityQuery<ClimbableComponent>();
  77. _fixturesQuery = GetEntityQuery<FixturesComponent>();
  78. _gridQuery = GetEntityQuery<MapGridComponent>();
  79. _xformQuery = GetEntityQuery<TransformComponent>();
  80. _playerManager.PlayerStatusChanged += OnPlayerChange;
  81. InitializeGrid();
  82. SubscribeNetworkEvent<RequestPathfindingDebugMessage>(OnBreadcrumbs);
  83. }
  84. public override void Shutdown()
  85. {
  86. base.Shutdown();
  87. _subscribedSessions.Clear();
  88. _playerManager.PlayerStatusChanged -= OnPlayerChange;
  89. _transform.OnGlobalMoveEvent -= OnMoveEvent;
  90. }
  91. public override void Update(float frameTime)
  92. {
  93. base.Update(frameTime);
  94. var options = new ParallelOptions()
  95. {
  96. MaxDegreeOfParallelism = _parallel.ParallelProcessCount,
  97. };
  98. UpdateGrid(options);
  99. _stopwatch.Restart();
  100. var amount = Math.Min(PathTickLimit, _pathRequests.Count);
  101. var results = ArrayPool<PathResult>.Shared.Rent(amount);
  102. Parallel.For(0, amount, options, i =>
  103. {
  104. // If we're over the limit (either time-sliced or hard cap).
  105. if (_stopwatch.Elapsed >= PathTime)
  106. {
  107. results[i] = PathResult.Continuing;
  108. return;
  109. }
  110. var request = _pathRequests[i];
  111. try
  112. {
  113. switch (request)
  114. {
  115. case AStarPathRequest astar:
  116. results[i] = UpdateAStarPath(astar);
  117. break;
  118. case BFSPathRequest bfs:
  119. results[i] = UpdateBFSPath(_random, bfs);
  120. break;
  121. default:
  122. throw new NotImplementedException();
  123. }
  124. }
  125. catch (Exception)
  126. {
  127. results[i] = PathResult.NoPath;
  128. throw;
  129. }
  130. });
  131. var offset = 0;
  132. // then, single-threaded cleanup.
  133. for (var i = 0; i < amount; i++)
  134. {
  135. var resultIndex = i + offset;
  136. var path = _pathRequests[resultIndex];
  137. var result = results[i];
  138. if (path.Task.Exception != null)
  139. {
  140. throw path.Task.Exception;
  141. }
  142. switch (result)
  143. {
  144. case PathResult.Continuing:
  145. break;
  146. case PathResult.PartialPath:
  147. case PathResult.Path:
  148. case PathResult.NoPath:
  149. SendDebug(path);
  150. // Don't use RemoveSwap because we still want to try and process them in order.
  151. _pathRequests.RemoveAt(resultIndex);
  152. offset--;
  153. path.Tcs.SetResult(result);
  154. SendRoute(path);
  155. break;
  156. default:
  157. throw new NotImplementedException();
  158. }
  159. }
  160. ArrayPool<PathResult>.Shared.Return(results);
  161. }
  162. /// <summary>
  163. /// Creates neighbouring edges at both locations, each leading to the other.
  164. /// </summary>
  165. public bool TryCreatePortal(EntityCoordinates coordsA, EntityCoordinates coordsB, out int handle)
  166. {
  167. var mapUidA = coordsA.GetMapUid(EntityManager);
  168. var mapUidB = coordsB.GetMapUid(EntityManager);
  169. handle = -1;
  170. if (mapUidA != mapUidB || mapUidA == null)
  171. {
  172. return false;
  173. }
  174. var gridUidA = coordsA.GetGridUid(EntityManager);
  175. var gridUidB = coordsB.GetGridUid(EntityManager);
  176. if (!TryComp<GridPathfindingComponent>(gridUidA, out var gridA) ||
  177. !TryComp<GridPathfindingComponent>(gridUidB, out var gridB))
  178. {
  179. return false;
  180. }
  181. handle = _portalIndex++;
  182. var portal = new PathPortal(handle, coordsA, coordsB);
  183. _portals[handle] = portal;
  184. var originA = GetOrigin(coordsA, gridUidA.Value);
  185. var originB = GetOrigin(coordsB, gridUidB.Value);
  186. gridA.PortalLookup.Add(portal, originA);
  187. gridB.PortalLookup.Add(portal, originB);
  188. var chunkA = GetChunk(originA, gridUidA.Value);
  189. var chunkB = GetChunk(originB, gridUidB.Value);
  190. chunkA.Portals.Add(portal);
  191. chunkB.Portals.Add(portal);
  192. // TODO: You already have the chunks
  193. DirtyChunk(gridUidA.Value, coordsA);
  194. DirtyChunk(gridUidB.Value, coordsB);
  195. return true;
  196. }
  197. public bool RemovePortal(int handle)
  198. {
  199. if (!_portals.TryGetValue(handle, out var portal))
  200. {
  201. return false;
  202. }
  203. _portals.Remove(handle);
  204. var gridUidA = portal.CoordinatesA.GetGridUid(EntityManager);
  205. var gridUidB = portal.CoordinatesB.GetGridUid(EntityManager);
  206. if (!TryComp<GridPathfindingComponent>(gridUidA, out var gridA) ||
  207. !TryComp<GridPathfindingComponent>(gridUidB, out var gridB))
  208. {
  209. return false;
  210. }
  211. gridA.PortalLookup.Remove(portal);
  212. gridB.PortalLookup.Remove(portal);
  213. var chunkA = GetChunk(GetOrigin(portal.CoordinatesA, gridUidA.Value), gridUidA.Value, gridA);
  214. var chunkB = GetChunk(GetOrigin(portal.CoordinatesB, gridUidB.Value), gridUidB.Value, gridB);
  215. chunkA.Portals.Remove(portal);
  216. chunkB.Portals.Remove(portal);
  217. DirtyChunk(gridUidA.Value, portal.CoordinatesA);
  218. DirtyChunk(gridUidB.Value, portal.CoordinatesB);
  219. return true;
  220. }
  221. public async Task<PathResultEvent> GetRandomPath(
  222. EntityUid entity,
  223. float maxRange,
  224. CancellationToken cancelToken,
  225. int limit = 40,
  226. PathFlags flags = PathFlags.None)
  227. {
  228. if (!TryComp(entity, out TransformComponent? start))
  229. return new PathResultEvent(PathResult.NoPath, new List<PathPoly>());
  230. var layer = 0;
  231. var mask = 0;
  232. if (TryComp<FixturesComponent>(entity, out var fixtures))
  233. {
  234. (layer, mask) = _physics.GetHardCollision(entity, fixtures);
  235. }
  236. var request = new BFSPathRequest(maxRange, limit, start.Coordinates, flags, layer, mask, cancelToken);
  237. var path = await GetPath(request);
  238. if (path.Result != PathResult.Path)
  239. return new PathResultEvent(PathResult.NoPath, new List<PathPoly>());
  240. return new PathResultEvent(PathResult.Path, path.Path);
  241. }
  242. /// <summary>
  243. /// Gets the estimated distance from the entity to the target node.
  244. /// </summary>
  245. public async Task<float?> GetPathDistance(
  246. EntityUid entity,
  247. EntityCoordinates end,
  248. float range,
  249. CancellationToken cancelToken,
  250. PathFlags flags = PathFlags.None)
  251. {
  252. if (!TryComp(entity, out TransformComponent? start))
  253. return null;
  254. var request = GetRequest(entity, start.Coordinates, end, range, cancelToken, flags);
  255. var path = await GetPath(request);
  256. if (path.Result != PathResult.Path)
  257. return null;
  258. if (path.Path.Count == 0)
  259. return 0f;
  260. var distance = 0f;
  261. var lastNode = path.Path[0];
  262. for (var i = 1; i < path.Path.Count; i++)
  263. {
  264. var node = path.Path[i];
  265. distance += GetTileCost(request, lastNode, node);
  266. }
  267. return distance;
  268. }
  269. public async Task<PathResultEvent> GetPath(
  270. EntityUid entity,
  271. EntityUid target,
  272. float range,
  273. CancellationToken cancelToken,
  274. PathFlags flags = PathFlags.None)
  275. {
  276. if (!TryComp(entity, out TransformComponent? xform) ||
  277. !TryComp(target, out TransformComponent? targetXform))
  278. return new PathResultEvent(PathResult.NoPath, new List<PathPoly>());
  279. var request = GetRequest(entity, xform.Coordinates, targetXform.Coordinates, range, cancelToken, flags);
  280. return await GetPath(request);
  281. }
  282. public async Task<PathResultEvent> GetPath(
  283. EntityUid entity,
  284. EntityCoordinates start,
  285. EntityCoordinates end,
  286. float range,
  287. CancellationToken cancelToken,
  288. PathFlags flags = PathFlags.None)
  289. {
  290. var request = GetRequest(entity, start, end, range, cancelToken, flags);
  291. return await GetPath(request);
  292. }
  293. /// <summary>
  294. /// Gets a path in a thread-safe way.
  295. /// </summary>
  296. public async Task<PathResultEvent> GetPathSafe(
  297. EntityUid entity,
  298. EntityCoordinates start,
  299. EntityCoordinates end,
  300. float range,
  301. CancellationToken cancelToken,
  302. PathFlags flags = PathFlags.None)
  303. {
  304. var request = GetRequest(entity, start, end, range, cancelToken, flags);
  305. return await GetPath(request, true);
  306. }
  307. /// <summary>
  308. /// Asynchronously gets a path.
  309. /// </summary>
  310. public async Task<PathResultEvent> GetPath(
  311. EntityCoordinates start,
  312. EntityCoordinates end,
  313. float range,
  314. int layer,
  315. int mask,
  316. CancellationToken cancelToken,
  317. PathFlags flags = PathFlags.None)
  318. {
  319. // Don't allow the caller to pass in the request in case they try to do something with its data.
  320. var request = new AStarPathRequest(start, end, flags, range, layer, mask, cancelToken);
  321. return await GetPath(request);
  322. }
  323. /// <summary>
  324. /// Raises the pathfinding result event on the entity when finished.
  325. /// </summary>
  326. public async void GetPathEvent(
  327. EntityUid uid,
  328. EntityCoordinates start,
  329. EntityCoordinates end,
  330. float range,
  331. CancellationToken cancelToken,
  332. PathFlags flags = PathFlags.None)
  333. {
  334. var path = await GetPath(uid, start, end, range, cancelToken);
  335. RaiseLocalEvent(uid, path);
  336. }
  337. /// <summary>
  338. /// Gets the relevant poly for the specified coordinates if it exists.
  339. /// </summary>
  340. public PathPoly? GetPoly(EntityCoordinates coordinates)
  341. {
  342. var gridUid = coordinates.GetGridUid(EntityManager);
  343. if (!TryComp<GridPathfindingComponent>(gridUid, out var comp) ||
  344. !TryComp(gridUid, out TransformComponent? xform))
  345. {
  346. return null;
  347. }
  348. var localPos = Vector2.Transform(coordinates.ToMapPos(EntityManager, _transform), _transform.GetInvWorldMatrix(xform));
  349. var origin = GetOrigin(localPos);
  350. if (!TryGetChunk(origin, comp, out var chunk))
  351. return null;
  352. var chunkPos = new Vector2(MathHelper.Mod(localPos.X, ChunkSize), MathHelper.Mod(localPos.Y, ChunkSize));
  353. var polys = chunk.Polygons[(int) chunkPos.X * ChunkSize + (int) chunkPos.Y];
  354. foreach (var poly in polys)
  355. {
  356. if (!poly.Box.Contains(localPos))
  357. continue;
  358. return poly;
  359. }
  360. return null;
  361. }
  362. private PathRequest GetRequest(EntityUid entity, EntityCoordinates start, EntityCoordinates end, float range, CancellationToken cancelToken, PathFlags flags)
  363. {
  364. var layer = 0;
  365. var mask = 0;
  366. if (TryComp<FixturesComponent>(entity, out var fixtures))
  367. {
  368. (layer, mask) = _physics.GetHardCollision(entity, fixtures);
  369. }
  370. return new AStarPathRequest(start, end, flags, range, layer, mask, cancelToken);
  371. }
  372. public PathFlags GetFlags(EntityUid uid)
  373. {
  374. if (!_npc.TryGetNpc(uid, out var npc))
  375. {
  376. return PathFlags.None;
  377. }
  378. return GetFlags(npc.Blackboard);
  379. }
  380. public PathFlags GetFlags(NPCBlackboard blackboard)
  381. {
  382. var flags = PathFlags.None;
  383. if (blackboard.TryGetValue<bool>(NPCBlackboard.NavPry, out var pry, EntityManager) && pry)
  384. {
  385. flags |= PathFlags.Prying;
  386. }
  387. if (blackboard.TryGetValue<bool>(NPCBlackboard.NavSmash, out var smash, EntityManager) && smash)
  388. {
  389. flags |= PathFlags.Smashing;
  390. }
  391. if (blackboard.TryGetValue<bool>(NPCBlackboard.NavClimb, out var climb, EntityManager) && climb)
  392. {
  393. flags |= PathFlags.Climbing;
  394. }
  395. if (blackboard.TryGetValue<bool>(NPCBlackboard.NavInteract, out var interact, EntityManager) && interact)
  396. {
  397. flags |= PathFlags.Interact;
  398. }
  399. return flags;
  400. }
  401. private async Task<PathResultEvent> GetPath(
  402. PathRequest request, bool safe = false)
  403. {
  404. // We could maybe try an initial quick run to avoid forcing time-slicing over ticks.
  405. // For now it seems okay and it shouldn't block on 1 NPC anyway.
  406. if (safe)
  407. {
  408. lock (_pathRequests)
  409. {
  410. _pathRequests.Add(request);
  411. }
  412. }
  413. else
  414. {
  415. _pathRequests.Add(request);
  416. }
  417. await request.Task;
  418. if (request.Task.Exception != null)
  419. {
  420. throw request.Task.Exception;
  421. }
  422. if (!request.Task.IsCompletedSuccessfully)
  423. {
  424. return new PathResultEvent(PathResult.NoPath, new List<PathPoly>());
  425. }
  426. // Same context as do_after and not synchronously blocking soooo
  427. #pragma warning disable RA0004
  428. var ev = new PathResultEvent(request.Task.Result, request.Polys);
  429. #pragma warning restore RA0004
  430. return ev;
  431. }
  432. #region Debug handlers
  433. private DebugPathPoly GetDebugPoly(PathPoly poly)
  434. {
  435. // Create fake neighbors for it
  436. var neighbors = new List<NetCoordinates>(poly.Neighbors.Count);
  437. foreach (var neighbor in poly.Neighbors)
  438. {
  439. neighbors.Add(GetNetCoordinates(neighbor.Coordinates));
  440. }
  441. return new DebugPathPoly()
  442. {
  443. GraphUid = GetNetEntity(poly.GraphUid),
  444. ChunkOrigin = poly.ChunkOrigin,
  445. TileIndex = poly.TileIndex,
  446. Box = poly.Box,
  447. Data = poly.Data,
  448. Neighbors = neighbors,
  449. };
  450. }
  451. private void SendDebug(PathRequest request)
  452. {
  453. if (_subscribedSessions.Count == 0)
  454. return;
  455. foreach (var session in _subscribedSessions)
  456. {
  457. if ((session.Value & PathfindingDebugMode.Routes) == 0x0)
  458. continue;
  459. RaiseNetworkEvent(new PathRouteMessage(request.Polys.Select(GetDebugPoly).ToList(), new Dictionary<DebugPathPoly, float>()), session.Key.Channel);
  460. }
  461. }
  462. private void OnBreadcrumbs(RequestPathfindingDebugMessage msg, EntitySessionEventArgs args)
  463. {
  464. var pSession = args.SenderSession;
  465. if (!_adminManager.HasAdminFlag(pSession, AdminFlags.Debug))
  466. {
  467. return;
  468. }
  469. var sessions = _subscribedSessions.GetOrNew(args.SenderSession);
  470. if (msg.Mode == PathfindingDebugMode.None)
  471. {
  472. _subscribedSessions.Remove(args.SenderSession);
  473. return;
  474. }
  475. sessions = msg.Mode;
  476. _subscribedSessions[args.SenderSession] = sessions;
  477. if (IsCrumb(sessions))
  478. {
  479. SendBreadcrumbs(pSession);
  480. }
  481. if (IsPoly(sessions))
  482. {
  483. SendPolys(pSession);
  484. }
  485. }
  486. private bool IsCrumb(PathfindingDebugMode mode)
  487. {
  488. return (mode & (PathfindingDebugMode.Breadcrumbs | PathfindingDebugMode.Crumb)) != 0x0;
  489. }
  490. private bool IsPoly(PathfindingDebugMode mode)
  491. {
  492. return (mode & (PathfindingDebugMode.Chunks | PathfindingDebugMode.Polys | PathfindingDebugMode.Poly | PathfindingDebugMode.PolyNeighbors)) != 0x0;
  493. }
  494. private bool IsRoute(PathfindingDebugMode mode)
  495. {
  496. return (mode & (PathfindingDebugMode.Routes | PathfindingDebugMode.RouteCosts)) != 0x0;
  497. }
  498. private void SendBreadcrumbs(ICommonSession pSession)
  499. {
  500. var msg = new PathBreadcrumbsMessage();
  501. var query = AllEntityQuery<GridPathfindingComponent>();
  502. while (query.MoveNext(out var uid, out var comp))
  503. {
  504. var netGrid = GetNetEntity(uid);
  505. msg.Breadcrumbs.Add(netGrid, new Dictionary<Vector2i, List<PathfindingBreadcrumb>>(comp.Chunks.Count));
  506. foreach (var chunk in comp.Chunks)
  507. {
  508. var data = GetCrumbs(chunk.Value);
  509. msg.Breadcrumbs[netGrid].Add(chunk.Key, data);
  510. }
  511. }
  512. RaiseNetworkEvent(msg, pSession.Channel);
  513. }
  514. private void SendRoute(PathRequest request)
  515. {
  516. if (_subscribedSessions.Count == 0)
  517. return;
  518. var polys = new List<DebugPathPoly>();
  519. var costs = new Dictionary<DebugPathPoly, float>();
  520. foreach (var poly in request.Polys)
  521. {
  522. polys.Add(GetDebugPoly(poly));
  523. }
  524. foreach (var (poly, value) in request.CostSoFar)
  525. {
  526. costs.Add(GetDebugPoly(poly), value);
  527. }
  528. var msg = new PathRouteMessage(polys, costs);
  529. foreach (var session in _subscribedSessions)
  530. {
  531. if (!IsRoute(session.Value))
  532. continue;
  533. RaiseNetworkEvent(msg, session.Key.Channel);
  534. }
  535. }
  536. private void SendPolys(ICommonSession pSession)
  537. {
  538. var msg = new PathPolysMessage();
  539. var query = AllEntityQuery<GridPathfindingComponent>();
  540. while (query.MoveNext(out var uid, out var comp))
  541. {
  542. var netGrid = GetNetEntity(uid);
  543. msg.Polys.Add(netGrid, new Dictionary<Vector2i, Dictionary<Vector2i, List<DebugPathPoly>>>(comp.Chunks.Count));
  544. foreach (var chunk in comp.Chunks)
  545. {
  546. var data = GetPolys(chunk.Value);
  547. msg.Polys[netGrid].Add(chunk.Key, data);
  548. }
  549. }
  550. RaiseNetworkEvent(msg, pSession.Channel);
  551. }
  552. private void SendBreadcrumbs(GridPathfindingChunk chunk, EntityUid gridUid)
  553. {
  554. if (_subscribedSessions.Count == 0)
  555. return;
  556. var msg = new PathBreadcrumbsRefreshMessage()
  557. {
  558. Origin = chunk.Origin,
  559. GridUid = GetNetEntity(gridUid),
  560. Data = GetCrumbs(chunk),
  561. };
  562. foreach (var session in _subscribedSessions)
  563. {
  564. if (!IsCrumb(session.Value))
  565. continue;
  566. RaiseNetworkEvent(msg, session.Key.Channel);
  567. }
  568. }
  569. private void SendPolys(GridPathfindingChunk chunk, EntityUid gridUid,
  570. List<PathPoly>[] tilePolys)
  571. {
  572. if (_subscribedSessions.Count == 0)
  573. return;
  574. var data = new Dictionary<Vector2i, List<DebugPathPoly>>(tilePolys.Length);
  575. var extent = Math.Sqrt(tilePolys.Length);
  576. for (var x = 0; x < extent; x++)
  577. {
  578. for (var y = 0; y < extent; y++)
  579. {
  580. var index = GetIndex(x, y);
  581. data[new Vector2i(x, y)] = tilePolys[index].Select(GetDebugPoly).ToList();
  582. }
  583. }
  584. var msg = new PathPolysRefreshMessage()
  585. {
  586. Origin = chunk.Origin,
  587. GridUid = GetNetEntity(gridUid),
  588. Polys = data,
  589. };
  590. foreach (var session in _subscribedSessions)
  591. {
  592. if (!IsPoly(session.Value))
  593. continue;
  594. RaiseNetworkEvent(msg, session.Key.Channel);
  595. }
  596. }
  597. private List<PathfindingBreadcrumb> GetCrumbs(GridPathfindingChunk chunk)
  598. {
  599. var crumbs = new List<PathfindingBreadcrumb>(chunk.Points.Length);
  600. const int extent = ChunkSize * SubStep;
  601. for (var x = 0; x < extent; x++)
  602. {
  603. for (var y = 0; y < extent; y++)
  604. {
  605. crumbs.Add(chunk.Points[x, y]);
  606. }
  607. }
  608. return crumbs;
  609. }
  610. private Dictionary<Vector2i, List<DebugPathPoly>> GetPolys(GridPathfindingChunk chunk)
  611. {
  612. var polys = new Dictionary<Vector2i, List<DebugPathPoly>>(chunk.Polygons.Length);
  613. for (var x = 0; x < ChunkSize; x++)
  614. {
  615. for (var y = 0; y < ChunkSize; y++)
  616. {
  617. var index = GetIndex(x, y);
  618. polys[new Vector2i(x, y)] = chunk.Polygons[index].Select(GetDebugPoly).ToList();
  619. }
  620. }
  621. return polys;
  622. }
  623. private void OnPlayerChange(object? sender, SessionStatusEventArgs e)
  624. {
  625. if (e.NewStatus == SessionStatus.Connected || !_subscribedSessions.ContainsKey(e.Session))
  626. return;
  627. _subscribedSessions.Remove(e.Session);
  628. }
  629. #endregion
  630. }
  631. }