tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
path_agent.h
1#pragma once
2
3#include <tess/diagnostics/diagnostics.h>
4#include <tess/path/path_runtime.h>
5#include <tess/path/precheck.h>
6#include <tess/sim/movement.h>
7
8#include <concepts>
9#include <cstddef>
10#include <cstdint>
11#include <optional>
12#include <span>
13
14namespace tess {
15
16// Lifecycle of a path agent, decoupled from its optional last search result:
17// - Idle: no goal (or arrived); the agent does not consume processing.
18// - NeedsPath: a goal was assigned and no route has been computed yet.
19// - Following: a Found route is being walked tile by tile.
20// - Blocked: the last step or search hit a transient failure; the agent
21// retries a retained occupancy-blocked step or re-searches an invalid route
22// until its shared retry budget runs out, then follows the tick policy.
23// - Unreachable: a structural movement failure or an explicit compatibility
24// exhaustion policy terminalized the goal until a new one is assigned.
26enum class PathAgentPhase : std::uint8_t {
27 Idle,
28 NeedsPath,
29 Following,
30 Blocked,
31 Unreachable,
32};
33
36 Coord3 position{};
37 Coord3 goal{};
38 PathTicket ticket{};
39 std::size_t path_index = 0;
41 std::optional<PathStatus> last_result = std::nullopt;
42 PathAgentPhase phase = PathAgentPhase::Idle;
43 bool has_goal = false;
44 std::uint32_t blocked_retries = 0;
46 std::uint64_t armed_tick = 0;
47};
48
51 std::size_t submitted = 0;
52 std::size_t completed = 0;
53 std::size_t found = 0;
54 std::size_t invalid_start = 0;
55 std::size_t invalid_goal = 0;
56 std::size_t no_path = 0;
57 std::size_t not_computed = 0;
58 std::size_t no_candidate = 0;
59 // Sparse worlds: the search stopped at the resident-set boundary without
60 // ruling out a route through a non-resident chunk
61 // (PathStatus::Indeterminate).
62 std::size_t indeterminate = 0;
63 std::size_t cost_overflow = 0;
64 // Agents whose goal an optional topology precheck proved unreachable before
65 // A* (a subset of no_path). See PathRuntimeStats::precheck_ruled_out.
66 std::size_t precheck_ruled_out = 0;
67 // Total search nodes expanded by the completed results this call applied.
68 // A deterministic work meter: callers can bound planning per tick by
69 // expansion count where a wall-clock budget would break replay.
70 std::size_t expanded_nodes = 0;
71 std::size_t advanced = 0;
72 std::size_t arrived = 0;
73 std::size_t blocked_waits = 0;
74 MovementFailureCounts movement_failures{};
75};
76
79 std::size_t max_steps = 1;
80 DirtyMask movement_dirty_mask{};
81};
82
83// Which agents a processing pass (re)submits (per-agent pathing dirt,
84// optimization-log 2026-07-11/12):
85// - All: every agent with a goal replans -- required after a WORLD change,
86// which can invalidate any existing route.
87// - NeedsOnly: only agents that cannot advance without a plan (NeedsPath or a
88// route-invalidated Blocked state); Following and occupancy-waiting agents
89// keep their retained routes. One agent arming a goal no longer replans the
90// whole batch.
99enum class PathSubmitScope : std::uint8_t {
100 All,
101 NeedsOnly,
102};
103
104// Per-agent route retention, index-paired with the agents span handed to
105// the tick drivers: routes[i] is agents[i]'s current Found route (empty
106// otherwise). Retention is what makes PathSubmitScope::NeedsOnly sound --
107// the runtime rebuilds its result storage every processing pass, so a
108// non-resubmitted agent's route must live here. Vectors keep their
109// capacity across replans, so warm ticks stay allocation-free.
110//
111// CONTRACT: the pairing is by span index. A caller that reorders, removes,
112// or compacts its agents between ticks must call mark_pathing_dirty on the
113// tick state (forcing one full replan) or keep routes[] in sync itself.
116 std::vector<std::vector<Coord3>> routes;
117
118 void ensure_size(std::size_t count) {
119 if (routes.size() < count) {
120 routes.resize(count);
121 }
122 }
123};
124
125namespace detail {
126
127// Occupancy and reservations are deliberately absent from path passability.
128// A new search would return the same route, so these failures should retry the
129// retained next step. Other transient commit failures invalidate the route and
130// need path processing before movement can resume.
131[[nodiscard]] inline bool movement_block_can_retry_route(
132 MovementStatus status) noexcept {
133 return status == MovementStatus::Occupied ||
134 status == MovementStatus::Reserved;
135}
136
137inline void block_path_agent(PathAgentState& agent,
138 MovementStatus status) noexcept {
139 agent.phase = PathAgentPhase::Blocked;
140 if (!movement_block_can_retry_route(status)) {
141 agent.last_result.reset();
142 }
143}
144
145inline void resume_path_agent(PathAgentState& agent) noexcept {
146 agent.phase = PathAgentPhase::Following;
147 agent.blocked_retries = 0;
148}
149
150[[nodiscard]] inline bool can_skip_scoped_path_submission(
151 const PathAgentState& agent) noexcept {
152 return agent.phase == PathAgentPhase::Following ||
153 (agent.phase == PathAgentPhase::Blocked &&
154 agent.last_result == PathStatus::Found);
155}
156
157// Whether a cursor at `path_index` has a step left in a route of `size`.
158//
159// Spelled without addition on purpose. `PathAgentState::path_index` is a
160// public field with no enforced range, so `path_index + 1` wraps to 0 at
161// the maximum -- and a wrapped cursor compares as in-range, which would
162// advance a fully consumed route from its first step.
163[[nodiscard]] constexpr bool has_next_step(std::size_t path_index,
164 std::size_t size) noexcept {
165 return size > 0 && path_index < size - 1;
166}
167
168} // namespace detail
169
174inline void set_path_agent_goal(PathAgentState& agent, Coord3 goal) noexcept {
175 agent.goal = goal;
176 agent.path_index = 0;
177 agent.last_result.reset();
178 agent.phase = PathAgentPhase::NeedsPath;
179 agent.blocked_retries = 0;
180 agent.has_goal = true;
181}
182
187inline void clear_path_agent_goal(PathAgentState& agent) noexcept {
188 agent.goal = {};
189 agent.ticket = {};
190 agent.path_index = 0;
191 agent.last_result.reset();
192 agent.phase = PathAgentPhase::Idle;
193 agent.blocked_retries = 0;
194 agent.has_goal = false;
195}
196
198[[nodiscard]] inline auto path_agent_goal_outstanding(
199 const PathAgentState& agent) noexcept -> bool {
200 return agent.has_goal && agent.phase != PathAgentPhase::Unreachable;
201}
202
206inline void fail_path_agent_flow(
207 const PathAgentState& agent,
208 diagnostics::FlowAccounting* accounting) noexcept {
209 if (accounting != nullptr) {
210 ++accounting->counters.failed;
211 accounting->record_left_outstanding();
212 accounting->counters.residence_ticks_accumulated +=
213 accounting->last_observed_tick - agent.armed_tick;
214 }
215}
216
218inline void arrive_path_agent(
219 PathAgentState& agent, diagnostics::FlowAccounting* accounting) noexcept {
220 if (accounting != nullptr) {
221 ++accounting->counters.completed;
222 accounting->record_left_outstanding();
223 accounting->counters.residence_ticks_accumulated +=
224 accounting->last_observed_tick - agent.armed_tick;
225 }
226 clear_path_agent_goal(agent);
227}
228
230inline auto submit_path_agents(
231 std::span<PathAgentState> agents, PathRequestRuntime& runtime,
232 PathSubmitScope scope = PathSubmitScope::All,
233 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
235 runtime.clear_requests();
236
237 for (auto& agent : agents) {
238 if (scope == PathSubmitScope::NeedsOnly &&
239 detail::can_skip_scoped_path_submission(agent)) {
240 // Keeps its retained route and path_index; the runtime rebuild below
241 // makes its old ticket stale, which nothing reads in the scoped flow.
242 continue;
243 }
244 agent.path_index = 0;
245 if (!agent.has_goal || agent.phase == PathAgentPhase::Unreachable) {
246 continue;
247 }
248 if (agent.position == agent.goal) {
249 arrive_path_agent(agent, accounting);
250 ++stats.arrived;
251 continue;
252 }
253 agent.ticket = runtime.submit(PathRequest{agent.position, agent.goal});
254 ++stats.submitted;
255 }
256
257 return stats;
258}
259
261inline void record_path_agent_status(PathAgentFrameStats& stats,
262 PathStatus status) noexcept {
263 switch (status) {
264 case PathStatus::NotComputed:
265 ++stats.not_computed;
266 return;
267 case PathStatus::Found:
268 ++stats.found;
269 return;
270 case PathStatus::InvalidStart:
271 ++stats.invalid_start;
272 return;
273 case PathStatus::InvalidGoal:
274 ++stats.invalid_goal;
275 return;
276 case PathStatus::NoPath:
277 ++stats.no_path;
278 return;
279 case PathStatus::Indeterminate:
280 ++stats.indeterminate;
281 return;
282 case PathStatus::CostOverflow:
283 ++stats.cost_overflow;
284 return;
285 case PathStatus::NoCandidate:
286 ++stats.no_candidate;
287 return;
288 }
289}
290
293inline auto apply_path_agent_results(std::span<PathAgentState> agents,
294 const PathRequestRuntime& runtime,
295 PathSubmitScope scope,
296 PathAgentRoutes* routes)
299 if (routes != nullptr) {
300 // Callers going through the tick drivers arrive pre-sized; grow here
301 // too so the public process_* overloads cannot index out of bounds.
302 routes->ensure_size(agents.size());
303 }
304
305 for (std::size_t i = 0; i < agents.size(); ++i) {
306 auto& agent = agents[i];
307 if (scope == PathSubmitScope::NeedsOnly &&
308 detail::can_skip_scoped_path_submission(agent)) {
309 // Not resubmitted by the matching scoped submit; its runtime ticket
310 // is stale and its retained route stays as-is.
311 continue;
312 }
313 if (!agent.has_goal || agent.position == agent.goal ||
314 agent.phase == PathAgentPhase::Unreachable) {
315 continue;
316 }
317
318 const auto was_blocked = agent.phase == PathAgentPhase::Blocked;
319 const auto result = runtime.result(agent.ticket);
320 agent.last_result = result.status;
321 agent.path_index = 0;
322 if (result.status == PathStatus::Found) {
323 agent.phase = PathAgentPhase::Following;
324 // A Found search is not progress for an occupancy-blocked agent: the
325 // planner intentionally ignores occupancy and may return the identical
326 // next step. Preserve its consecutive-block budget until movement
327 // actually succeeds.
328 if (!was_blocked) {
329 agent.blocked_retries = 0;
330 }
331 if (routes != nullptr) {
332 routes->routes[i].assign(result.path.begin(), result.path.end());
333 }
334 } else {
335 // Planner failures are retried through the Blocked lifecycle until the
336 // tick driver's retry budget and exhaustion policy take effect.
337 agent.phase = PathAgentPhase::Blocked;
338 if (routes != nullptr) {
339 routes->routes[i].clear();
340 }
341 }
342 ++stats.completed;
343 stats.expanded_nodes += result.expanded_nodes;
344 record_path_agent_status(stats, result.status);
345 }
346
347 return stats;
348}
349
351inline auto apply_path_agent_results(std::span<PathAgentState> agents,
352 const PathRequestRuntime& runtime)
354 return apply_path_agent_results(agents, runtime, PathSubmitScope::All,
355 nullptr);
356}
357
365inline auto advance_path_agents(
366 std::span<PathAgentState> agents, const PathRequestRuntime& runtime,
367 std::size_t max_steps = 1,
368 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
370 if (max_steps == 0) {
371 return stats;
372 }
373
374 for (auto& agent : agents) {
375 if (!agent.has_goal || agent.last_result != PathStatus::Found) {
376 continue;
377 }
378
379 const auto result = runtime.result(agent.ticket);
380 if (result.status != PathStatus::Found || result.path.empty()) {
381 continue;
382 }
383
384 for (std::size_t step = 0; step < max_steps; ++step) {
385 if (!detail::has_next_step(agent.path_index, result.path.size())) {
386 break;
387 }
388 ++agent.path_index;
389 agent.position = result.path[agent.path_index];
390 detail::resume_path_agent(agent);
391 ++stats.advanced;
392 if (agent.position == agent.goal) {
393 arrive_path_agent(agent, accounting);
394 agent.last_result = PathStatus::Found;
395 ++stats.arrived;
396 break;
397 }
398 }
399 }
400
401 return stats;
402}
403
404// Observer form: `on_commit(agent_index, from, to)` is invoked once per
405// successful commit_movement_intent, after the agent's position and the
406// world's occupancy fields are updated and before arrival handling. It is
407// never invoked for a failed validation (nothing was written to the world),
408// so external tile->entity mirrors that update inside the callback stay
409// synchronized with the occupancy field by construction.
410template <typename World, typename ClassOrTag, typename OccupancyTag,
411 typename ReservationTag, typename OnCommit>
412 requires std::invocable<OnCommit&, std::size_t, Coord3, Coord3>
419inline auto advance_path_agents_with_movement(
420 World& world, std::span<PathAgentState> agents,
421 const PathRequestRuntime& runtime, PathAgentAdvanceOptions options,
422 OnCommit&& on_commit, diagnostics::FlowAccounting* accounting = nullptr)
425 if (options.max_steps == 0) {
426 return stats;
427 }
428
429 for (std::size_t agent_index = 0; agent_index < agents.size();
430 ++agent_index) {
431 auto& agent = agents[agent_index];
432 if (!agent.has_goal || agent.last_result != PathStatus::Found) {
433 continue;
434 }
435
436 const auto result = runtime.result(agent.ticket);
437 if (result.status != PathStatus::Found || result.path.empty()) {
438 continue;
439 }
440
441 for (std::size_t step = 0; step < options.max_steps; ++step) {
442 if (!detail::has_next_step(agent.path_index, result.path.size())) {
443 break;
444 }
445
446 const auto from = agent.position;
447 const auto to = result.path[agent.path_index + 1];
448 const auto movement =
449 commit_movement_intent<World, ClassOrTag, OccupancyTag,
450 ReservationTag>(
451 world, MovementIntent{from, to, {}}, options.movement_dirty_mask);
452 if (movement.status != MovementStatus::Moved) {
453 record_movement_failure(stats.movement_failures, movement.status);
454 if (is_transient_movement_failure(movement.status)) {
455 // Wait in place. Occupancy/reservation failures retain Found so the
456 // same step can be retried without a pointless occupancy-blind
457 // search. Other transient failures clear the obsolete search result
458 // and request a fresh route. The following tick starts consuming
459 // the shared bounded
460 // retry budget (see PathAgentTickOptions::max_blocked_retries).
461 detail::block_path_agent(agent, movement.status);
462 ++stats.blocked_waits;
463 } else {
464 // Invalid endpoints or a non-adjacent step indicate a caller
465 // bug; terminal until a new goal re-arms the lifecycle.
466 agent.last_result.reset();
467 agent.phase = PathAgentPhase::Unreachable;
468 fail_path_agent_flow(agent, accounting);
469 }
470 break;
471 }
472
473 ++agent.path_index;
474 agent.position = to;
475 detail::resume_path_agent(agent);
476 on_commit(agent_index, from, to);
477 ++stats.advanced;
478 if (agent.position == agent.goal) {
479 arrive_path_agent(agent, accounting);
480 agent.last_result = PathStatus::Found;
481 ++stats.arrived;
482 break;
483 }
484 }
485 }
486
487 return stats;
488}
489
490template <typename World, typename ClassOrTag, typename OccupancyTag,
491 typename ReservationTag>
498inline auto advance_path_agents_with_movement(
499 World& world, std::span<PathAgentState> agents,
500 const PathRequestRuntime& runtime, PathAgentAdvanceOptions options = {},
501 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
502 return advance_path_agents_with_movement<World, ClassOrTag, OccupancyTag,
503 ReservationTag>(
504 world, agents, runtime, options, [](std::size_t, Coord3, Coord3) {},
505 accounting);
506}
507
508template <typename World, typename ClassOrTag, typename OccupancyTag,
509 typename ReservationTag, typename Provider>
511inline auto advance_path_agents_with_movement(
512 World& world, std::span<PathAgentState> agents,
513 const PathAgentRoutes& routes, PathAgentAdvanceOptions options,
514 const Provider& provider, diagnostics::FlowAccounting* accounting = nullptr)
516 TESS_ASSERT(routes.routes.size() >= agents.size());
518 if (options.max_steps == 0) {
519 return stats;
520 }
521 for (std::size_t agent_index = 0; agent_index < agents.size();
522 ++agent_index) {
523 auto& agent = agents[agent_index];
524 if (!agent.has_goal || agent.last_result != PathStatus::Found) {
525 continue;
526 }
527 const auto& route = routes.routes[agent_index];
528 for (std::size_t step = 0;
529 step < options.max_steps &&
530 detail::has_next_step(agent.path_index, route.size());
531 ++step) {
532 const auto from = agent.position;
533 const auto to = route[agent.path_index + 1];
534 const auto movement =
535 commit_movement_intent<World, ClassOrTag, OccupancyTag,
536 ReservationTag>(
537 world, MovementIntent{from, to, {}}, options.movement_dirty_mask,
538 provider);
539 if (movement.status != MovementStatus::Moved) {
540 record_movement_failure(stats.movement_failures, movement.status);
541 if (is_transient_movement_failure(movement.status)) {
542 detail::block_path_agent(agent, movement.status);
543 ++stats.blocked_waits;
544 } else {
545 agent.last_result.reset();
546 agent.phase = PathAgentPhase::Unreachable;
547 fail_path_agent_flow(agent, accounting);
548 }
549 break;
550 }
551 ++agent.path_index;
552 agent.position = to;
553 detail::resume_path_agent(agent);
554 ++stats.advanced;
555 if (agent.position == agent.goal) {
556 arrive_path_agent(agent, accounting);
557 agent.last_result = PathStatus::Found;
558 ++stats.arrived;
559 break;
560 }
561 }
562 }
563 return stats;
564}
565
566// Route-pool advance: identical stepping semantics to the runtime-reading
567// overloads above, but the route comes from the retained pool, so it
568// survives processing passes that did not resubmit this agent
569// (PathSubmitScope::NeedsOnly).
571inline auto advance_path_agents(
572 std::span<PathAgentState> agents, const PathAgentRoutes& routes,
573 std::size_t max_steps = 1,
574 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
575 // The pool is const here, so it must already cover the span (the tick
576 // drivers ensure_size before processing; direct callers must too).
577 TESS_ASSERT(routes.routes.size() >= agents.size());
579 if (max_steps == 0) {
580 return stats;
581 }
582
583 for (std::size_t i = 0; i < agents.size(); ++i) {
584 auto& agent = agents[i];
585 if (!agent.has_goal || agent.last_result != PathStatus::Found) {
586 continue;
587 }
588
589 const auto& route = routes.routes[i];
590 if (route.empty()) {
591 continue;
592 }
593
594 for (std::size_t step = 0; step < max_steps; ++step) {
595 if (!detail::has_next_step(agent.path_index, route.size())) {
596 break;
597 }
598 ++agent.path_index;
599 agent.position = route[agent.path_index];
600 detail::resume_path_agent(agent);
601 ++stats.advanced;
602 if (agent.position == agent.goal) {
603 arrive_path_agent(agent, accounting);
604 agent.last_result = PathStatus::Found;
605 ++stats.arrived;
606 break;
607 }
608 }
609 }
610
611 return stats;
612}
613
614template <typename World, typename ClassOrTag, typename OccupancyTag,
615 typename ReservationTag, typename OnCommit>
616 requires std::invocable<OnCommit&, std::size_t, Coord3, Coord3>
618inline auto advance_path_agents_with_movement(
619 World& world, std::span<PathAgentState> agents,
620 const PathAgentRoutes& routes, PathAgentAdvanceOptions options,
621 OnCommit&& on_commit, diagnostics::FlowAccounting* accounting = nullptr)
623 // Same pool-coverage precondition as the plain route-pool advance.
624 TESS_ASSERT(routes.routes.size() >= agents.size());
626 if (options.max_steps == 0) {
627 return stats;
628 }
629
630 for (std::size_t agent_index = 0; agent_index < agents.size();
631 ++agent_index) {
632 auto& agent = agents[agent_index];
633 if (!agent.has_goal || agent.last_result != PathStatus::Found) {
634 continue;
635 }
636
637 const auto& route = routes.routes[agent_index];
638 if (route.empty()) {
639 continue;
640 }
641
642 for (std::size_t step = 0; step < options.max_steps; ++step) {
643 if (!detail::has_next_step(agent.path_index, route.size())) {
644 break;
645 }
646
647 const auto from = agent.position;
648 const auto to = route[agent.path_index + 1];
649 const auto movement =
650 commit_movement_intent<World, ClassOrTag, OccupancyTag,
651 ReservationTag>(
652 world, MovementIntent{from, to, {}}, options.movement_dirty_mask);
653 if (movement.status != MovementStatus::Moved) {
654 record_movement_failure(stats.movement_failures, movement.status);
655 if (is_transient_movement_failure(movement.status)) {
656 // Same Blocked/Unreachable split as the runtime-reading overload;
657 // see its comment for the retry-budget semantics.
658 detail::block_path_agent(agent, movement.status);
659 ++stats.blocked_waits;
660 } else {
661 agent.last_result.reset();
662 agent.phase = PathAgentPhase::Unreachable;
663 fail_path_agent_flow(agent, accounting);
664 }
665 break;
666 }
667
668 ++agent.path_index;
669 agent.position = to;
670 detail::resume_path_agent(agent);
671 on_commit(agent_index, from, to);
672 ++stats.advanced;
673 if (agent.position == agent.goal) {
674 arrive_path_agent(agent, accounting);
675 agent.last_result = PathStatus::Found;
676 ++stats.arrived;
677 break;
678 }
679 }
680 }
681
682 return stats;
683}
684
685template <typename World, typename ClassOrTag, typename OccupancyTag,
686 typename ReservationTag>
688inline auto advance_path_agents_with_movement(
689 World& world, std::span<PathAgentState> agents,
690 const PathAgentRoutes& routes, PathAgentAdvanceOptions options = {},
691 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
692 return advance_path_agents_with_movement<World, ClassOrTag, OccupancyTag,
693 ReservationTag>(
694 world, agents, routes, options, [](std::size_t, Coord3, Coord3) {},
695 accounting);
696}
697
699inline void add_path_agent_stats(PathAgentFrameStats& lhs,
700 PathAgentFrameStats rhs) noexcept {
701 lhs.submitted += rhs.submitted;
702 lhs.completed += rhs.completed;
703 lhs.found += rhs.found;
704 lhs.invalid_start += rhs.invalid_start;
705 lhs.invalid_goal += rhs.invalid_goal;
706 lhs.no_path += rhs.no_path;
707 lhs.not_computed += rhs.not_computed;
708 lhs.no_candidate += rhs.no_candidate;
709 lhs.indeterminate += rhs.indeterminate;
710 lhs.cost_overflow += rhs.cost_overflow;
711 lhs.precheck_ruled_out += rhs.precheck_ruled_out;
712 lhs.expanded_nodes += rhs.expanded_nodes;
713 lhs.advanced += rhs.advanced;
714 lhs.arrived += rhs.arrived;
715 lhs.blocked_waits += rhs.blocked_waits;
716 lhs.movement_failures.invalid += rhs.movement_failures.invalid;
717 lhs.movement_failures.impassable += rhs.movement_failures.impassable;
718 lhs.movement_failures.blocked += rhs.movement_failures.blocked;
719 lhs.movement_failures.occupied += rhs.movement_failures.occupied;
720 lhs.movement_failures.reserved += rhs.movement_failures.reserved;
721 lhs.movement_failures.stale_content += rhs.movement_failures.stale_content;
722 lhs.movement_failures.stale_topology += rhs.movement_failures.stale_topology;
723}
724
725template <typename World, typename ClassOrTag>
727[[nodiscard]] auto process_unit_path_agents(
728 const World& world, std::span<PathAgentState> agents,
729 PathRequestRuntime& runtime, PathRuntimeCachePolicy policy = {},
731 PathSubmitScope scope = PathSubmitScope::All,
732 PathAgentRoutes* routes = nullptr,
733 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
734 auto stats = submit_path_agents(agents, runtime, scope, accounting);
735 (void)runtime.template process_unit_cached<World, ClassOrTag>(world, policy,
736 graph);
737 add_path_agent_stats(
738 stats, apply_path_agent_results(agents, runtime, scope, routes));
739 stats.precheck_ruled_out = runtime.stats().precheck_ruled_out;
740 return stats;
741}
742
743template <typename World, typename ClassOrTag, typename Provider>
745[[nodiscard]] auto process_unit_path_agents(
746 const World& world, std::span<PathAgentState> agents,
749 PathSubmitScope scope, PathAgentRoutes* routes, const Provider& provider,
750 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
751 auto stats = submit_path_agents(agents, runtime, scope, accounting);
752 (void)runtime.template process_unit_cached<World, ClassOrTag>(
753 world, policy, graph, provider);
754 add_path_agent_stats(
755 stats, apply_path_agent_results(agents, runtime, scope, routes));
756 stats.precheck_ruled_out = runtime.stats().precheck_ruled_out;
757 return stats;
758}
759
760template <typename World, typename Class, std::uint32_t MaxCost>
762[[nodiscard]] auto process_weighted_path_agents(
763 const World& world, std::span<PathAgentState> agents,
764 PathRequestRuntime& runtime, PathRuntimeCachePolicy policy = {},
766 PathSubmitScope scope = PathSubmitScope::All,
767 PathAgentRoutes* routes = nullptr,
768 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
769 auto stats = submit_path_agents(agents, runtime, scope, accounting);
770 (void)runtime.template process_weighted_batch<World, Class, MaxCost>(
771 world, policy, graph);
772 add_path_agent_stats(
773 stats, apply_path_agent_results(agents, runtime, scope, routes));
774 stats.precheck_ruled_out = runtime.stats().precheck_ruled_out;
775 return stats;
776}
777
778template <typename World, typename Class, std::uint32_t MaxCost,
779 typename Provider>
781[[nodiscard]] auto process_weighted_path_agents(
782 const World& world, std::span<PathAgentState> agents,
785 PathSubmitScope scope, PathAgentRoutes* routes, const Provider& provider,
786 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
787 auto stats = submit_path_agents(agents, runtime, scope, accounting);
788 (void)runtime.template process_weighted_batch<World, Class, MaxCost>(
789 world, policy, graph, provider);
790 add_path_agent_stats(
791 stats, apply_path_agent_results(agents, runtime, scope, routes));
792 stats.precheck_ruled_out = runtime.stats().precheck_ruled_out;
793 return stats;
794}
795
796} // namespace tess
Definition path_runtime.h:197
Region graph storage specialized by dense or sparse residency policy.
Definition topology.h:382
Definition world.h:22
Definition shape.h:46
Definition metadata_types.h:12
Aggregates rejected movement attempts by retry-relevant category.
Definition movement.h:55
Describes an adjacent move and any versions it expects to remain current.
Definition movement.h:41
Configures bounded direct movement and the dirty bits it emits.
Definition path_agent.h:78
Summarizes path submission, results, movement, and failure outcomes.
Definition path_agent.h:50
Owns index-paired route copies retained across scoped processing passes.
Definition path_agent.h:115
Stores one agent's goal, route cursor, and retry lifecycle state.
Definition path_agent.h:35
std::optional< PathStatus > last_result
Most recent search conclusion, absent before search or after invalidation.
Definition path_agent.h:41
std::uint64_t armed_tick
Flow-accounting admission stamp (see the tick-state goal APIs).
Definition path_agent.h:46
Specifies inclusive start and goal coordinates for a path query.
Definition request.h:10
Definition path_runtime.h:70
Definition path_runtime.h:29
Definition diagnostics.h:505