3#include <tess/sim/path_agent.h>
4#include <tess/sim/time.h>
17struct PathAgentTickState {
26 bool pathing_dirty =
true;
38 PathAgentTickState() =
default;
39 PathAgentTickState(
const PathAgentTickState& other)
41 pathing_dirty{other.pathing_dirty},
42 routes{other.routes} {}
46 pathing_dirty = other.pathing_dirty;
47 routes = other.routes;
52 PathAgentTickState(PathAgentTickState&& other) noexcept
54 pathing_dirty{other.pathing_dirty},
55 routes{std::move(other.routes)},
57 other.flow_accounting =
nullptr;
59 auto operator=(PathAgentTickState&& other)
noexcept -> PathAgentTickState& {
62 pathing_dirty = other.pathing_dirty;
63 routes = std::move(other.routes);
65 other.flow_accounting =
nullptr;
69 ~PathAgentTickState() =
default;
73enum class BlockedAgentExhaustionPolicy : std::uint8_t {
85 std::size_t max_steps = 1;
97 BlockedAgentExhaustionPolicy blocked_exhaustion_policy =
98 BlockedAgentExhaustionPolicy::RemainBlocked;
115 std::size_t blocked = 0;
117 std::size_t selected = 0;
118 std::size_t deferred = 0;
137 void reserve(std::size_t agent_count) {
138 entries_.reserve(agent_count);
139 due_indices_.reserve(agent_count);
142 void clear()
noexcept {
144 due_indices_.clear();
148 [[nodiscard]]
auto collect_due(std::span<const PathAgentState> agents,
152 if (entries_.size() < agents.size()) {
153 entries_.resize(agents.size());
155 for (std::size_t i = agents.size(); i < entries_.size(); ++i) {
156 entries_[i].active =
false;
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;
172 if (!entry.active || entry.observed_position != agent.position) {
176 entry.observed_position = agent.position;
178 add_saturating(tick, jittered_delay(i, entry, options));
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) {
191 if (due_indices_.size() < options.max_probes_per_tick) {
192 due_indices_.push_back(i);
195 if (!due_indices_.empty()) {
196 scan_cursor_ = (due_indices_.back() + 1U) % agents.size();
199 stats.selected = due_indices_.size();
200 stats.deferred = stats.due - stats.selected;
204 [[nodiscard]]
auto due_agent_indices()
const noexcept
205 -> std::span<const std::size_t> {
209 void record_attempt(std::size_t agent_index, std::uint64_t tick,
211 if (agent_index >= entries_.size()) {
214 auto& entry = entries_[agent_index];
218 if (entry.attempt != std::numeric_limits<std::uint32_t>::max()) {
222 add_saturating(tick, jittered_delay(agent_index, entry, options));
227 std::uint64_t next_tick = 0;
228 std::uint64_t episode = 0;
229 std::uint32_t attempt = 0;
230 Coord3 observed_position{};
234 [[nodiscard]]
static constexpr auto mix(std::uint64_t value)
noexcept
236 value += 0x9e3779b97f4a7c15ULL;
237 value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL;
238 value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL;
239 return value ^ (value >> 31U);
242 [[nodiscard]]
static constexpr auto delay_cap(
251 for (std::uint32_t i = 0; i < attempt && cap < options.
max_delay_ticks;
262 [[nodiscard]]
static constexpr auto jittered_delay(
263 std::size_t agent_index,
const Entry& entry,
265 const auto cap = delay_cap(entry.attempt, options);
272 const auto floor = cap / 2U + cap % 2U;
273 const auto width = cap - floor + 1U;
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);
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;
287 std::vector<Entry> entries_;
288 std::vector<std::size_t> due_indices_;
289 std::size_t scan_cursor_ = 0;
298 MissingChunkPolicy::ReportIndeterminate;
320 void reserve(std::size_t agent_count) {
322 agent_count > std::numeric_limits<std::size_t>::max() / 2U
323 ? std::numeric_limits<std::size_t>::max()
325 indices_.reserve(doubled);
326 queued_.reserve(agent_count);
329 void clear()
noexcept {
330 for (
auto i = head_; i < indices_.size(); ++i) {
331 queued_[indices_[i]] = 0;
337 [[nodiscard]]
auto request(std::size_t index,
const PathAgentState& agent)
339 if (!agent.has_goal || agent.phase == PathAgentPhase::Unreachable) {
342 if (queued_.size() <= index) {
343 queued_.resize(index + 1U, 0);
345 if (queued_[index] != 0) {
348 if (head_ != 0 && head_ >= indices_.size() / 2U) {
349 indices_.erase(indices_.begin(),
350 indices_.begin() +
static_cast<std::ptrdiff_t
>(head_));
353 indices_.push_back(index);
358 void request_all(std::span<const PathAgentState> agents) {
359 if (queued_.size() < agents.size()) {
360 queued_.resize(agents.size(), 0);
362 for (std::size_t i = 0; i < agents.size(); ++i) {
363 (void)request(i, agents[i]);
367 [[nodiscard]]
auto empty()
const noexcept ->
bool {
368 return head_ == indices_.size();
371 [[nodiscard]]
auto pending()
const noexcept -> std::size_t {
372 return indices_.size() - head_;
383 [[nodiscard]]
auto contains(std::size_t index)
const noexcept ->
bool {
384 return index < queued_.size() && queued_[index] != 0;
387 [[nodiscard]]
auto front() const noexcept -> std::optional<std::
size_t> {
391 return indices_[head_];
394 void pop_front() noexcept {
398 const auto index = indices_[head_];
399 TESS_ASSERT(index < queued_.size());
400 if (index < queued_.size()) {
404 if (head_ == indices_.size()) {
411 std::vector<std::size_t> indices_;
412 std::vector<std::uint8_t> queued_;
413 std::size_t head_ = 0;
431template <
typename Search>
432[[nodiscard]]
auto process_path_agent_replans(
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()) {
443 const auto index = pending_index.value();
444 if (index >= agents.size()) {
448 auto& agent = agents[index];
449 if (!agent.has_goal || agent.phase == PathAgentPhase::Unreachable) {
453 if (agent.position == agent.goal) {
454 arrive_path_agent(agent, accounting);
455 routes.routes[index].clear();
461 const auto was_blocked = agent.phase == PathAgentPhase::Blocked;
462 const auto result = search(index,
PathRequest{agent.position, agent.goal});
465 stats.expanded_nodes += result.expanded_nodes;
466 record_path_agent_status(stats, result.status);
467 if (result.status == PathStatus::Found) {
470 routes.routes[index].assign(result.path.begin(), result.path.end());
471 agent.path_index = 0;
473 agent.phase = PathAgentPhase::Following;
475 agent.blocked_retries = 0;
478 routes.routes[index].clear();
479 agent.path_index = 0;
481 agent.phase = PathAgentPhase::Blocked;
489template <
typename World,
typename ClassOrTag>
490[[nodiscard]]
auto process_unit_path_agent_replans(
491 const World& world, std::span<PathAgentState> agents,
495 return process_path_agent_replans(
496 agents, routes, queue, options.max_requests,
498 return astar_path<World, ClassOrTag>(world, request, scratch,
499 options.missing_chunk_policy);
505template <
typename World,
typename Class>
506[[nodiscard]]
auto process_weighted_path_agent_replans(
507 const World& world, std::span<PathAgentState> agents,
511 return process_path_agent_replans(
512 agents, routes, queue, options.max_requests,
514 if (options.equal_cost_tie_seed != 0) {
515 auto seed = options.equal_cost_tie_seed +
516 static_cast<std::uint64_t>(index) + 1U;
518 seed = options.equal_cost_tie_seed;
520 return weighted_astar_path<World, Class>(
521 world, request, scratch, PathTieBreak{seed},
522 options.missing_chunk_policy);
524 return weighted_astar_path<World, Class>(world, request, scratch,
525 options.missing_chunk_policy);
532 std::uint64_t tick = 0;
533 bool processed_paths =
false;
537 std::size_t repaths_requested = 0;
540 std::size_t repath_exhausted = 0;
545 state.pathing_dirty =
true;
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;
569 ++accounting.counters.offered;
570 accounting.record_admitted();
571 agent.
armed_tick = accounting.last_observed_tick;
573 set_path_agent_goal(agent, goal);
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;
586 clear_path_agent_goal(agent);
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) {
602 state.flow_accounting->observe_tick(tick);
603 const auto now = state.flow_accounting->last_observed_tick;
606 for (
const auto& agent : agents) {
607 if (path_agent_goal_outstanding(agent)) {
612 state.flow_accounting->counters.oldest_outstanding_age_ticks =
613 any ? now - oldest : 0;
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) {
632 if (agent.phase == PathAgentPhase::NeedsPath) {
633 needs_processing =
true;
636 if (agent.phase != PathAgentPhase::Blocked) {
639 if (options.max_steps == 0) {
644 if (agent.blocked_retries < options.max_blocked_retries) {
645 ++agent.blocked_retries;
647 ++stats.repaths_requested;
648 needs_processing =
true;
650 }
else if (options.blocked_exhaustion_policy ==
651 BlockedAgentExhaustionPolicy::MarkUnreachable) {
652 agent.phase = PathAgentPhase::Unreachable;
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;
663 return needs_processing;
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);
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) {
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;
690 stats.movement = advance_path_agents(agents, state.routes, options.max_steps,
691 state.flow_accounting);
696template <
typename World,
typename ClassOrTag,
typename Prov
ider>
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);
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) {
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;
719 stats.movement = advance_path_agents(agents, state.routes, options.max_steps,
720 state.flow_accounting);
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);
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) {
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;
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);
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);
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) {
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;
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);
791template <
typename World,
typename Class, std::u
int32_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);
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) {
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;
814 stats.movement = advance_path_agents(agents, state.routes, options.max_steps,
815 state.flow_accounting);
820template <
typename World,
typename Class, std::uint32_t MaxCost,
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);
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) {
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;
844 stats.movement = advance_path_agents(agents, state.routes, options.max_steps,
845 state.flow_accounting);
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);
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) {
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;
873 stats.movement = advance_path_agents_with_movement<World, Class, OccupancyTag,
875 world, agents, state.routes,
876 PathAgentAdvanceOptions{options.max_steps, options.movement_dirty_mask},
877 state.flow_accounting);
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);
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) {
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;
905 stats.movement = advance_path_agents_with_movement<World, Class, OccupancyTag,
907 world, agents, state.routes,
908 PathAgentAdvanceOptions{options.max_steps, options.movement_dirty_mask},
909 provider, state.flow_accounting);
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
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 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