tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
path_agent_tick.h
1#pragma once
2
3#include <tess/sim/path_agent.h>
4#include <tess/sim/time.h>
5
6#include <cstddef>
7#include <cstdint>
8#include <limits>
9#include <optional>
10#include <span>
11#include <vector>
12
13namespace tess {
14
17struct PathAgentTickState {
18 SimClock clock{};
19 // WORLD-scoped pathing dirt: set it (via mark_pathing_dirty) after any
20 // world change that can invalidate existing routes; the next tick then
21 // replans EVERY agent. Agent-scoped needs (a newly armed goal, a Blocked
22 // retry) do not set it -- those agents alone replan while Following
23 // agents keep walking their retained routes (audit/optimization-log
24 // per-agent pathing-dirty item). Starts true so the first tick plans
25 // everyone.
26 bool pathing_dirty = true;
27 // Per-agent retained routes; see PathAgentRoutes for the index-pairing
28 // contract (reorder/remove agents => mark_pathing_dirty).
29 PathAgentRoutes routes{};
37
38 PathAgentTickState() = default;
39 PathAgentTickState(const PathAgentTickState& other)
40 : clock{other.clock},
41 pathing_dirty{other.pathing_dirty},
42 routes{other.routes} {}
43 auto operator=(const PathAgentTickState& other) -> PathAgentTickState& {
44 if (this != &other) {
45 clock = other.clock;
46 pathing_dirty = other.pathing_dirty;
47 routes = other.routes;
48 flow_accounting = nullptr;
49 }
50 return *this;
51 }
52 PathAgentTickState(PathAgentTickState&& other) noexcept
53 : clock{other.clock},
54 pathing_dirty{other.pathing_dirty},
55 routes{std::move(other.routes)},
56 flow_accounting{other.flow_accounting} {
57 other.flow_accounting = nullptr;
58 }
59 auto operator=(PathAgentTickState&& other) noexcept -> PathAgentTickState& {
60 if (this != &other) {
61 clock = other.clock;
62 pathing_dirty = other.pathing_dirty;
63 routes = std::move(other.routes);
64 flow_accounting = other.flow_accounting;
65 other.flow_accounting = nullptr;
66 }
67 return *this;
68 }
69 ~PathAgentTickState() = default;
70};
71
73enum class BlockedAgentExhaustionPolicy : std::uint8_t {
77 RemainBlocked,
80 MarkUnreachable,
81};
82
85 std::size_t max_steps = 1;
86 DirtyMask movement_dirty_mask{};
87 PathRuntimeCachePolicy cache_policy{};
96 std::uint32_t max_blocked_retries = 8;
97 BlockedAgentExhaustionPolicy blocked_exhaustion_policy =
98 BlockedAgentExhaustionPolicy::RemainBlocked;
99};
100
104 std::uint32_t initial_delay_ticks = 16;
106 std::uint32_t max_delay_ticks = 256;
108 std::size_t max_probes_per_tick = 8;
110 std::uint64_t jitter_seed = 0;
111};
112
115 std::size_t blocked = 0;
116 std::size_t due = 0;
117 std::size_t selected = 0;
118 std::size_t deferred = 0;
119};
120
136 public:
137 void reserve(std::size_t agent_count) {
138 entries_.reserve(agent_count);
139 due_indices_.reserve(agent_count);
140 }
141
142 void clear() noexcept {
143 entries_.clear();
144 due_indices_.clear();
145 scan_cursor_ = 0;
146 }
147
148 [[nodiscard]] auto collect_due(std::span<const PathAgentState> agents,
149 std::uint64_t tick,
150 BlockedAgentRecoveryOptions options = {})
152 if (entries_.size() < agents.size()) {
153 entries_.resize(agents.size());
154 }
155 for (std::size_t i = agents.size(); i < entries_.size(); ++i) {
156 entries_[i].active = false;
157 }
158
159 due_indices_.clear();
161 for (std::size_t i = 0; i < agents.size(); ++i) {
162 const auto& agent = agents[i];
163 auto& entry = entries_[i];
164 if (!agent.has_goal || agent.phase != PathAgentPhase::Blocked) {
165 entry.active = false;
166 entry.attempt = 0;
167 entry.next_tick = 0;
168 continue;
169 }
170
171 ++stats.blocked;
172 if (!entry.active || entry.observed_position != agent.position) {
173 entry.active = true;
174 entry.attempt = 0;
175 ++entry.episode;
176 entry.observed_position = agent.position;
177 entry.next_tick =
178 add_saturating(tick, jittered_delay(i, entry, options));
179 }
180 }
181
182 if (!agents.empty()) {
183 scan_cursor_ %= agents.size();
184 for (std::size_t offset = 0; offset < agents.size(); ++offset) {
185 const auto i = (scan_cursor_ + offset) % agents.size();
186 const auto& entry = entries_[i];
187 if (!entry.active || tick < entry.next_tick) {
188 continue;
189 }
190 ++stats.due;
191 if (due_indices_.size() < options.max_probes_per_tick) {
192 due_indices_.push_back(i);
193 }
194 }
195 if (!due_indices_.empty()) {
196 scan_cursor_ = (due_indices_.back() + 1U) % agents.size();
197 }
198 }
199 stats.selected = due_indices_.size();
200 stats.deferred = stats.due - stats.selected;
201 return stats;
202 }
203
204 [[nodiscard]] auto due_agent_indices() const noexcept
205 -> std::span<const std::size_t> {
206 return due_indices_;
207 }
208
209 void record_attempt(std::size_t agent_index, std::uint64_t tick,
210 BlockedAgentRecoveryOptions options = {}) noexcept {
211 if (agent_index >= entries_.size()) {
212 return;
213 }
214 auto& entry = entries_[agent_index];
215 if (!entry.active) {
216 return;
217 }
218 if (entry.attempt != std::numeric_limits<std::uint32_t>::max()) {
219 ++entry.attempt;
220 }
221 entry.next_tick =
222 add_saturating(tick, jittered_delay(agent_index, entry, options));
223 }
224
225 private:
226 struct Entry {
227 std::uint64_t next_tick = 0;
228 std::uint64_t episode = 0;
229 std::uint32_t attempt = 0;
230 Coord3 observed_position{};
231 bool active = false;
232 };
233
234 [[nodiscard]] static constexpr auto mix(std::uint64_t value) noexcept
235 -> std::uint64_t {
236 value += 0x9e3779b97f4a7c15ULL;
237 value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL;
238 value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL;
239 return value ^ (value >> 31U);
240 }
241
242 [[nodiscard]] static constexpr auto delay_cap(
243 std::uint32_t attempt, BlockedAgentRecoveryOptions options) noexcept
244 -> std::uint32_t {
245 auto cap = options.initial_delay_ticks < options.max_delay_ticks
246 ? options.initial_delay_ticks
247 : options.max_delay_ticks;
248 if (cap == 0) {
249 return 0;
250 }
251 for (std::uint32_t i = 0; i < attempt && cap < options.max_delay_ticks;
252 ++i) {
253 if (cap > options.max_delay_ticks / 2U) {
254 cap = options.max_delay_ticks;
255 } else {
256 cap *= 2U;
257 }
258 }
259 return cap;
260 }
261
262 [[nodiscard]] static constexpr auto jittered_delay(
263 std::size_t agent_index, const Entry& entry,
264 BlockedAgentRecoveryOptions options) noexcept -> std::uint32_t {
265 const auto cap = delay_cap(entry.attempt, options);
266 if (cap == 0) {
267 return 0;
268 }
269 // Equal jitter: preserve half of the exponential delay and spread the
270 // remainder deterministically. Unlike full jitter this cannot repeatedly
271 // select a zero-delay retry.
272 const auto floor = cap / 2U + cap % 2U;
273 const auto width = cap - floor + 1U;
274 auto key = options.jitter_seed;
275 key ^= mix(static_cast<std::uint64_t>(agent_index));
276 key ^= mix(entry.episode);
277 key ^= mix(entry.attempt);
278 return floor + static_cast<std::uint32_t>(mix(key) % width);
279 }
280
281 [[nodiscard]] static constexpr auto add_saturating(
282 std::uint64_t tick, std::uint32_t delay) noexcept -> std::uint64_t {
283 const auto max = std::numeric_limits<std::uint64_t>::max();
284 return tick > max - delay ? max : tick + delay;
285 }
286
287 std::vector<Entry> entries_;
288 std::vector<std::size_t> due_indices_;
289 std::size_t scan_cursor_ = 0;
290};
291
295 std::size_t max_requests = 8;
297 MissingChunkPolicy missing_chunk_policy =
298 MissingChunkPolicy::ReportIndeterminate;
306 std::uint64_t equal_cost_tie_seed = 0;
307};
308
319 public:
320 void reserve(std::size_t agent_count) {
321 const auto doubled =
322 agent_count > std::numeric_limits<std::size_t>::max() / 2U
323 ? std::numeric_limits<std::size_t>::max()
324 : agent_count * 2U;
325 indices_.reserve(doubled);
326 queued_.reserve(agent_count);
327 }
328
329 void clear() noexcept {
330 for (auto i = head_; i < indices_.size(); ++i) {
331 queued_[indices_[i]] = 0;
332 }
333 indices_.clear();
334 head_ = 0;
335 }
336
337 [[nodiscard]] auto request(std::size_t index, const PathAgentState& agent)
338 -> bool {
339 if (!agent.has_goal || agent.phase == PathAgentPhase::Unreachable) {
340 return false;
341 }
342 if (queued_.size() <= index) {
343 queued_.resize(index + 1U, 0);
344 }
345 if (queued_[index] != 0) {
346 return false;
347 }
348 if (head_ != 0 && head_ >= indices_.size() / 2U) {
349 indices_.erase(indices_.begin(),
350 indices_.begin() + static_cast<std::ptrdiff_t>(head_));
351 head_ = 0;
352 }
353 indices_.push_back(index);
354 queued_[index] = 1;
355 return true;
356 }
357
358 void request_all(std::span<const PathAgentState> agents) {
359 if (queued_.size() < agents.size()) {
360 queued_.resize(agents.size(), 0);
361 }
362 for (std::size_t i = 0; i < agents.size(); ++i) {
363 (void)request(i, agents[i]);
364 }
365 }
366
367 [[nodiscard]] auto empty() const noexcept -> bool {
368 return head_ == indices_.size();
369 }
370
371 [[nodiscard]] auto pending() const noexcept -> std::size_t {
372 return indices_.size() - head_;
373 }
374
383 [[nodiscard]] auto contains(std::size_t index) const noexcept -> bool {
384 return index < queued_.size() && queued_[index] != 0;
385 }
386
387 [[nodiscard]] auto front() const noexcept -> std::optional<std::size_t> {
388 if (empty()) {
389 return std::nullopt;
390 }
391 return indices_[head_];
392 }
393
394 void pop_front() noexcept {
395 if (empty()) {
396 return;
397 }
398 const auto index = indices_[head_];
399 TESS_ASSERT(index < queued_.size());
400 if (index < queued_.size()) {
401 queued_[index] = 0;
402 }
403 ++head_;
404 if (head_ == indices_.size()) {
405 indices_.clear();
406 head_ = 0;
407 }
408 }
409
410 private:
411 std::vector<std::size_t> indices_;
412 std::vector<std::uint8_t> queued_;
413 std::size_t head_ = 0;
414};
415
431template <typename Search>
432[[nodiscard]] auto process_path_agent_replans(
433 std::span<PathAgentState> agents, PathAgentRoutes& routes,
434 PathAgentReplanQueue& queue, std::size_t max_requests, Search&& search,
435 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
437 routes.ensure_size(agents.size());
438 while (!queue.empty() && stats.submitted < max_requests) {
439 const auto pending_index = queue.front();
440 if (!pending_index.has_value()) {
441 break;
442 }
443 const auto index = pending_index.value();
444 if (index >= agents.size()) {
445 queue.pop_front();
446 continue;
447 }
448 auto& agent = agents[index];
449 if (!agent.has_goal || agent.phase == PathAgentPhase::Unreachable) {
450 queue.pop_front();
451 continue;
452 }
453 if (agent.position == agent.goal) {
454 arrive_path_agent(agent, accounting);
455 routes.routes[index].clear();
456 ++stats.arrived;
457 queue.pop_front();
458 continue;
459 }
460
461 const auto was_blocked = agent.phase == PathAgentPhase::Blocked;
462 const auto result = search(index, PathRequest{agent.position, agent.goal});
463 ++stats.submitted;
464 ++stats.completed;
465 stats.expanded_nodes += result.expanded_nodes;
466 record_path_agent_status(stats, result.status);
467 if (result.status == PathStatus::Found) {
468 // Assign first: if allocation throws, the pending queue item and agent
469 // lifecycle remain unchanged and the caller can retry.
470 routes.routes[index].assign(result.path.begin(), result.path.end());
471 agent.path_index = 0;
472 agent.last_result = PathStatus::Found;
473 agent.phase = PathAgentPhase::Following;
474 if (!was_blocked) {
475 agent.blocked_retries = 0;
476 }
477 } else {
478 routes.routes[index].clear();
479 agent.path_index = 0;
480 agent.last_result = result.status;
481 agent.phase = PathAgentPhase::Blocked;
482 }
483 queue.pop_front();
484 }
485 return stats;
486}
487
489template <typename World, typename ClassOrTag>
490[[nodiscard]] auto process_unit_path_agent_replans(
491 const World& world, std::span<PathAgentState> agents,
492 PathAgentRoutes& routes, PathAgentReplanQueue& queue, PathScratch& scratch,
493 PathAgentReplanOptions options = {},
494 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
495 return process_path_agent_replans(
496 agents, routes, queue, options.max_requests,
497 [&](std::size_t, PathRequest request) {
498 return astar_path<World, ClassOrTag>(world, request, scratch,
499 options.missing_chunk_policy);
500 },
501 accounting);
502}
503
505template <typename World, typename Class>
506[[nodiscard]] auto process_weighted_path_agent_replans(
507 const World& world, std::span<PathAgentState> agents,
508 PathAgentRoutes& routes, PathAgentReplanQueue& queue, PathScratch& scratch,
509 PathAgentReplanOptions options = {},
510 diagnostics::FlowAccounting* accounting = nullptr) -> PathAgentFrameStats {
511 return process_path_agent_replans(
512 agents, routes, queue, options.max_requests,
513 [&](std::size_t index, PathRequest request) {
514 if (options.equal_cost_tie_seed != 0) {
515 auto seed = options.equal_cost_tie_seed +
516 static_cast<std::uint64_t>(index) + 1U;
517 if (seed == 0) {
518 seed = options.equal_cost_tie_seed;
519 }
520 return weighted_astar_path<World, Class>(
521 world, request, scratch, PathTieBreak{seed},
522 options.missing_chunk_policy);
523 }
524 return weighted_astar_path<World, Class>(world, request, scratch,
525 options.missing_chunk_policy);
526 },
527 accounting);
528}
529
532 std::uint64_t tick = 0;
533 bool processed_paths = false;
534 PathAgentFrameStats pathing{};
535 PathAgentFrameStats movement{};
536 // Actual route-invalidating retries that requested path processing.
537 std::size_t repaths_requested = 0;
538 // Historical name retained for source compatibility: counts agents whose
539 // exhausted budget the selected policy terminalized.
540 std::size_t repath_exhausted = 0;
541};
542
544inline void mark_pathing_dirty(PathAgentTickState& state) noexcept {
545 state.pathing_dirty = true;
546}
547
548// Arms a goal WITHOUT touching the world-scoped pathing-dirty marker: the agent
549// enters NeedsPath, which the next tick picks up as an agent-scoped
550// (NeedsOnly) processing pass. Before the per-agent split this marked the
551// shared flag and one new goal replanned the whole batch every tick
552// (optimization-log 2026-07-11, S11.4 soak observation).
559inline void set_path_agent_goal(PathAgentTickState& state,
560 PathAgentState& agent, Coord3 goal) noexcept {
561 if (state.flow_accounting != nullptr) {
562 auto& accounting = *state.flow_accounting;
563 if (path_agent_goal_outstanding(agent)) {
564 ++accounting.counters.superseded;
565 accounting.record_left_outstanding();
566 accounting.counters.residence_ticks_accumulated +=
567 accounting.last_observed_tick - agent.armed_tick;
568 }
569 ++accounting.counters.offered;
570 accounting.record_admitted();
571 agent.armed_tick = accounting.last_observed_tick;
572 }
573 set_path_agent_goal(agent, goal);
574}
575
577inline void clear_path_agent_goal(PathAgentTickState& state,
578 PathAgentState& agent) noexcept {
579 if (state.flow_accounting != nullptr && path_agent_goal_outstanding(agent)) {
580 auto& accounting = *state.flow_accounting;
581 ++accounting.counters.cancelled;
582 accounting.record_left_outstanding();
583 accounting.counters.residence_ticks_accumulated +=
584 accounting.last_observed_tick - agent.armed_tick;
585 }
586 clear_path_agent_goal(agent);
587}
588
596inline void observe_path_agent_flow_tick(PathAgentTickState& state,
597 std::span<const PathAgentState> agents,
598 std::uint64_t tick) noexcept {
599 if (state.flow_accounting == nullptr) {
600 return;
601 }
602 state.flow_accounting->observe_tick(tick);
603 const auto now = state.flow_accounting->last_observed_tick;
604 auto oldest = now;
605 auto any = false;
606 for (const auto& agent : agents) {
607 if (path_agent_goal_outstanding(agent)) {
608 any = true;
609 oldest = agent.armed_tick < oldest ? agent.armed_tick : oldest;
610 }
611 }
612 state.flow_accounting->counters.oldest_outstanding_age_ticks =
613 any ? now - oldest : 0;
614}
615
616// Scans agents ahead of a tick's path processing. NeedsPath agents request
617// processing with no manual dirty mark. Blocked agents consume one retry per
618// following tick. A retained Found route waits without path processing for
619// occupancy/reservations; invalid routes request a re-search. Exhausted agents
620// stop path processing at exhaustion; the configured policy decides whether
621// they remain honestly Blocked or become terminally Unreachable.
623inline auto prepare_path_agent_processing(
624 std::span<PathAgentState> agents, PathAgentTickOptions options,
625 PathAgentTickStats& stats,
626 diagnostics::FlowAccounting* accounting = nullptr) noexcept -> bool {
627 bool needs_processing = false;
628 for (auto& agent : agents) {
629 if (!agent.has_goal) {
630 continue;
631 }
632 if (agent.phase == PathAgentPhase::NeedsPath) {
633 needs_processing = true;
634 continue;
635 }
636 if (agent.phase != PathAgentPhase::Blocked) {
637 continue;
638 }
639 if (options.max_steps == 0) {
640 // A paused movement tick cannot prove whether the obstruction cleared.
641 // Do not spend the consecutive-block budget without attempting a step.
642 continue;
643 }
644 if (agent.blocked_retries < options.max_blocked_retries) {
645 ++agent.blocked_retries;
646 if (agent.last_result != PathStatus::Found) {
647 ++stats.repaths_requested;
648 needs_processing = true;
649 }
650 } else if (options.blocked_exhaustion_policy ==
651 BlockedAgentExhaustionPolicy::MarkUnreachable) {
652 agent.phase = PathAgentPhase::Unreachable;
653 agent.last_result.reset();
654 ++stats.repath_exhausted;
655 if (accounting != nullptr) {
656 ++accounting->counters.failed;
657 accounting->record_left_outstanding();
658 accounting->counters.residence_ticks_accumulated +=
659 accounting->last_observed_tick - agent.armed_tick;
660 }
661 }
662 }
663 return needs_processing;
664}
665
667template <typename World, typename ClassOrTag>
668[[nodiscard]] auto tick_unit_path_agents(
669 PathAgentTickState& state, const World& world,
670 std::span<PathAgentState> agents, PathRequestRuntime& runtime,
671 PathAgentTickOptions options = {},
672 const RegionGraphT<typename World::residency_type>* graph = nullptr)
673 -> PathAgentTickStats {
674 PathAgentTickStats stats;
675 stats.tick = advance_sim_tick(state.clock);
676
677 const bool repath_needed = prepare_path_agent_processing(
678 agents, options, stats, state.flow_accounting);
679 state.routes.ensure_size(agents.size());
680 if (state.pathing_dirty || repath_needed) {
681 const auto scope =
682 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
683 stats.pathing = process_unit_path_agents<World, ClassOrTag>(
684 world, agents, runtime, options.cache_policy, graph, scope,
685 &state.routes, state.flow_accounting);
686 stats.processed_paths = true;
687 state.pathing_dirty = false;
688 }
689
690 stats.movement = advance_path_agents(agents, state.routes, options.max_steps,
691 state.flow_accounting);
692 return stats;
693}
694
696template <typename World, typename ClassOrTag, typename Provider>
697[[nodiscard]] auto tick_unit_path_agents(
698 PathAgentTickState& state, const World& world,
699 std::span<PathAgentState> agents, PathRequestRuntime& runtime,
700 PathAgentTickOptions options,
701 const RegionGraphT<typename World::residency_type>* graph,
702 const Provider& provider) -> PathAgentTickStats {
703 PathAgentTickStats stats;
704 stats.tick = advance_sim_tick(state.clock);
705
706 const bool repath_needed = prepare_path_agent_processing(
707 agents, options, stats, state.flow_accounting);
708 state.routes.ensure_size(agents.size());
709 if (state.pathing_dirty || repath_needed) {
710 const auto scope =
711 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
712 stats.pathing = process_unit_path_agents<World, ClassOrTag>(
713 world, agents, runtime, options.cache_policy, graph, scope,
714 &state.routes, provider, state.flow_accounting);
715 stats.processed_paths = true;
716 state.pathing_dirty = false;
717 }
718
719 stats.movement = advance_path_agents(agents, state.routes, options.max_steps,
720 state.flow_accounting);
721 return stats;
722}
723
724template <typename World, typename ClassOrTag, typename OccupancyTag,
725 typename ReservationTag>
727[[nodiscard]] auto tick_unit_path_agents_with_movement(
728 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
729 PathRequestRuntime& runtime, PathAgentTickOptions options = {},
730 const RegionGraphT<typename World::residency_type>* graph = nullptr)
731 -> PathAgentTickStats {
732 PathAgentTickStats stats;
733 stats.tick = advance_sim_tick(state.clock);
734
735 const bool repath_needed = prepare_path_agent_processing(
736 agents, options, stats, state.flow_accounting);
737 state.routes.ensure_size(agents.size());
738 if (state.pathing_dirty || repath_needed) {
739 const auto scope =
740 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
741 stats.pathing = process_unit_path_agents<World, ClassOrTag>(
742 world, agents, runtime, options.cache_policy, graph, scope,
743 &state.routes, state.flow_accounting);
744 stats.processed_paths = true;
745 state.pathing_dirty = false;
746 }
747
748 stats.movement = advance_path_agents_with_movement<
749 World, ClassOrTag, OccupancyTag, ReservationTag>(
750 world, agents, state.routes,
751 PathAgentAdvanceOptions{options.max_steps, options.movement_dirty_mask},
752 state.flow_accounting);
753 return stats;
754}
755
757template <typename World, typename ClassOrTag, typename OccupancyTag,
758 typename ReservationTag, typename Provider>
759[[nodiscard]] auto tick_unit_path_agents_with_movement(
760 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
761 PathRequestRuntime& runtime, PathAgentTickOptions options,
762 const RegionGraphT<typename World::residency_type>* graph,
763 const Provider& provider) -> PathAgentTickStats {
764 PathAgentTickStats stats;
765 stats.tick = advance_sim_tick(state.clock);
766
767 const bool repath_needed = prepare_path_agent_processing(
768 agents, options, stats, state.flow_accounting);
769 state.routes.ensure_size(agents.size());
770 if (state.pathing_dirty || repath_needed) {
771 const auto scope =
772 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
773 stats.pathing = process_unit_path_agents<World, ClassOrTag>(
774 world, agents, runtime, options.cache_policy, graph, scope,
775 &state.routes, provider, state.flow_accounting);
776 stats.processed_paths = true;
777 state.pathing_dirty = false;
778 }
779
780 stats.movement = advance_path_agents_with_movement<
781 World, ClassOrTag, OccupancyTag, ReservationTag>(
782 world, agents, state.routes,
783 PathAgentAdvanceOptions{options.max_steps, options.movement_dirty_mask},
784 provider, state.flow_accounting);
785 return stats;
786}
787
788// Class forms: one movement class drives pathing, precheck, and (for the
789// movement variant) commit validation, so plan and commit provably agree.
791template <typename World, typename Class, std::uint32_t MaxCost>
792[[nodiscard]] auto tick_weighted_path_agents(
793 PathAgentTickState& state, const World& world,
794 std::span<PathAgentState> agents, PathRequestRuntime& runtime,
795 PathAgentTickOptions options = {},
796 const RegionGraphT<typename World::residency_type>* graph = nullptr)
797 -> PathAgentTickStats {
798 PathAgentTickStats stats;
799 stats.tick = advance_sim_tick(state.clock);
800
801 const bool repath_needed = prepare_path_agent_processing(
802 agents, options, stats, state.flow_accounting);
803 state.routes.ensure_size(agents.size());
804 if (state.pathing_dirty || repath_needed) {
805 const auto scope =
806 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
807 stats.pathing = process_weighted_path_agents<World, Class, MaxCost>(
808 world, agents, runtime, options.cache_policy, graph, scope,
809 &state.routes, state.flow_accounting);
810 stats.processed_paths = true;
811 state.pathing_dirty = false;
812 }
813
814 stats.movement = advance_path_agents(agents, state.routes, options.max_steps,
815 state.flow_accounting);
816 return stats;
817}
818
820template <typename World, typename Class, std::uint32_t MaxCost,
821 typename Provider>
822[[nodiscard]] auto tick_weighted_path_agents(
823 PathAgentTickState& state, const World& world,
824 std::span<PathAgentState> agents, PathRequestRuntime& runtime,
825 PathAgentTickOptions options,
826 const RegionGraphT<typename World::residency_type>* graph,
827 const Provider& provider) -> PathAgentTickStats {
828 PathAgentTickStats stats;
829 stats.tick = advance_sim_tick(state.clock);
830
831 const bool repath_needed = prepare_path_agent_processing(
832 agents, options, stats, state.flow_accounting);
833 state.routes.ensure_size(agents.size());
834 if (state.pathing_dirty || repath_needed) {
835 const auto scope =
836 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
837 stats.pathing = process_weighted_path_agents<World, Class, MaxCost>(
838 world, agents, runtime, options.cache_policy, graph, scope,
839 &state.routes, provider, state.flow_accounting);
840 stats.processed_paths = true;
841 state.pathing_dirty = false;
842 }
843
844 stats.movement = advance_path_agents(agents, state.routes, options.max_steps,
845 state.flow_accounting);
846 return stats;
847}
848
849template <typename World, typename Class, std::uint32_t MaxCost,
850 typename OccupancyTag, typename ReservationTag>
852[[nodiscard]] auto tick_weighted_path_agents_with_movement(
853 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
854 PathRequestRuntime& runtime, PathAgentTickOptions options = {},
855 const RegionGraphT<typename World::residency_type>* graph = nullptr)
856 -> PathAgentTickStats {
857 PathAgentTickStats stats;
858 stats.tick = advance_sim_tick(state.clock);
859
860 const bool repath_needed = prepare_path_agent_processing(
861 agents, options, stats, state.flow_accounting);
862 state.routes.ensure_size(agents.size());
863 if (state.pathing_dirty || repath_needed) {
864 const auto scope =
865 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
866 stats.pathing = process_weighted_path_agents<World, Class, MaxCost>(
867 world, agents, runtime, options.cache_policy, graph, scope,
868 &state.routes, state.flow_accounting);
869 stats.processed_paths = true;
870 state.pathing_dirty = false;
871 }
872
873 stats.movement = advance_path_agents_with_movement<World, Class, OccupancyTag,
874 ReservationTag>(
875 world, agents, state.routes,
876 PathAgentAdvanceOptions{options.max_steps, options.movement_dirty_mask},
877 state.flow_accounting);
878 return stats;
879}
880
882template <typename World, typename Class, std::uint32_t MaxCost,
883 typename OccupancyTag, typename ReservationTag, typename Provider>
884[[nodiscard]] auto tick_weighted_path_agents_with_movement(
885 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
886 PathRequestRuntime& runtime, PathAgentTickOptions options,
887 const RegionGraphT<typename World::residency_type>* graph,
888 const Provider& provider) -> PathAgentTickStats {
889 PathAgentTickStats stats;
890 stats.tick = advance_sim_tick(state.clock);
891
892 const bool repath_needed = prepare_path_agent_processing(
893 agents, options, stats, state.flow_accounting);
894 state.routes.ensure_size(agents.size());
895 if (state.pathing_dirty || repath_needed) {
896 const auto scope =
897 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
898 stats.pathing = process_weighted_path_agents<World, Class, MaxCost>(
899 world, agents, runtime, options.cache_policy, graph, scope,
900 &state.routes, provider, state.flow_accounting);
901 stats.processed_paths = true;
902 state.pathing_dirty = false;
903 }
904
905 stats.movement = advance_path_agents_with_movement<World, Class, OccupancyTag,
906 ReservationTag>(
907 world, agents, state.routes,
908 PathAgentAdvanceOptions{options.max_steps, options.movement_dirty_mask},
909 provider, state.flow_accounting);
910 return stats;
911}
912
913} // namespace tess
Definition path_agent_tick.h:135
Definition path_agent_tick.h:318
auto contains(std::size_t index) const noexcept -> bool
Definition path_agent_tick.h:383
Definition path.h:741
Definition world.h:22
Configures deterministic, bounded checks of persistently blocked agents.
Definition path_agent_tick.h:102
std::size_t max_probes_per_tick
Maximum number of indices returned from one collection pass.
Definition path_agent_tick.h:108
std::uint64_t jitter_seed
Caller-selected deterministic salt; no process-global RNG is consulted.
Definition path_agent_tick.h:110
std::uint32_t max_delay_ticks
Upper bound for later exponentially backed-off delays.
Definition path_agent_tick.h:106
std::uint32_t initial_delay_ticks
Upper bound for the first jittered delay after blockage is observed.
Definition path_agent_tick.h:104
Summarizes one blocked-agent recovery scheduling pass.
Definition path_agent_tick.h:114
Definition shape.h:46
Definition metadata_types.h:12
Summarizes path submission, results, movement, and failure outcomes.
Definition path_agent.h:50
Configures one bounded drain of an exact path-agent replan queue.
Definition path_agent_tick.h:293
MissingChunkPolicy missing_chunk_policy
Sparse-world boundary behavior passed through to exact A*.
Definition path_agent_tick.h:297
std::uint64_t equal_cost_tie_seed
Definition path_agent_tick.h:306
std::size_t max_requests
Maximum number of exact searches performed by one processing call.
Definition path_agent_tick.h:295
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
Configures per-tick movement, caching, and blocked-agent retry limits.
Definition path_agent_tick.h:84
std::uint32_t max_blocked_retries
Definition path_agent_tick.h:96
Definition path_agent_tick.h:17
diagnostics::FlowAccounting * flow_accounting
Definition path_agent_tick.h:36
Summarizes path planning and movement performed during one tick.
Definition path_agent_tick.h:531
Specifies inclusive start and goal coordinates for a path query.
Definition request.h:10
Definition path_runtime.h:70
Stores the authoritative monotonically increasing fixed-tick count.
Definition time.h:28
Definition diagnostics.h:505