tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
path.h
1#pragma once
2
3#define TESS_PATH_PATH_H_INCLUDED 1
4
5#include <tess/core/shape.h>
6#include <tess/core/tag_identity.h>
7#include <tess/diagnostics/diagnostics.h>
8#include <tess/path/detail/portal_memo.h>
9#include <tess/path/node_index_space.h>
10#include <tess/path/path_view.h>
11#include <tess/path/request.h>
12#include <tess/topology/movement_class.h>
13#include <tess/topology/transition_model.h>
14
15#include <algorithm>
16#include <array>
17#include <concepts>
18#include <cstddef>
19#include <cstdint>
20#include <cstdlib>
21#include <functional>
22#include <limits>
23#include <span>
24#include <type_traits>
25#include <utility>
26#include <vector>
27
28namespace tess {
29
31enum class PathStatus : std::uint8_t {
32 // No operation has produced a current result. Used by default, cleared,
33 // stale, or model-mismatched products; never a reachability conclusion.
34 NotComputed,
35 Found,
36 InvalidStart,
37 InvalidGoal,
38 // No route exists in the graph considered under the selected missing-chunk
39 // policy. It is a whole-world conclusion only when no unknown boundary was
40 // assumed impassable.
41 NoPath,
42 // Sparse worlds only: the search reached the edge of the resident set and
43 // could not rule out a path through a non-resident chunk. Distinguished from
44 // NoPath so a caller never mistakes "not searched" for "no route exists" and
45 // can materialize the missing chunks and retry.
46 Indeterminate,
47 // A legal route step or accumulated exact cost reached the reserved
48 // uint32_t infinity sentinel and therefore cannot be represented.
49 CostOverflow,
50 // A bounded or heuristic strategy found no candidate. Callers requiring a
51 // reachability conclusion must run an authoritative search.
52 NoCandidate,
53};
54static_assert(sizeof(PathStatus) == sizeof(std::uint8_t));
55
60struct PathResult {
61 PathStatus status = PathStatus::NotComputed;
62 std::uint32_t cost = 0;
63 std::size_t expanded_nodes = 0;
64 std::size_t reached_nodes = 0;
65 PathView path;
66 std::uint32_t cost_scale = 1;
67};
68
71 PathStatus status = PathStatus::NotComputed;
72 std::size_t expanded_nodes = 0;
73 std::size_t reached_nodes = 0;
74};
75
79 std::size_t requests = 0;
80 std::size_t unique_goals = 0;
81 std::size_t field_builds = 0;
82 std::size_t astar_fallbacks = 0;
83 std::size_t path_nodes = 0;
84};
85
87class PathScratch;
91class GoalSet;
95class UnitRouteCache;
100
102template <typename World, typename Class, std::uint32_t MaxCost>
103[[nodiscard]] auto weighted_path_batch(
104 const World& world, std::span<const PathRequest> requests,
106 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
107 -> std::span<const PathResult>;
108
110template <typename World, typename Class, std::uint32_t MaxCost,
111 typename Provider>
112[[nodiscard]] auto weighted_path_batch(const World& world,
113 std::span<const PathRequest> requests,
115 MissingChunkPolicy policy,
116 const Provider& provider)
117 -> std::span<const PathResult>;
118
119namespace detail {
120// Core behind weighted_distance_field_path; verify_residency lets
121// weighted_path_batch skip the O(resident_count) fingerprint recompute for
122// fields it reads against the same const world it just built them from.
123template <typename World, typename Class>
124[[nodiscard]] auto weighted_distance_field_path_core(
125 const World& world, PathRequest request, DistanceFieldScratch& scratch,
126 bool verify_residency) -> PathResult;
127
128template <typename World, typename Class, typename Provider>
129[[nodiscard]] auto weighted_distance_field_path_core(
130 const World& world, PathRequest request, DistanceFieldScratch& scratch,
131 bool verify_residency, const Provider& provider) -> PathResult;
132
133// Core behind build_bounded_weighted_distance_field. settle_targets are
134// validated tile indices whose distances the caller will read; once every
135// target is settled the flood stops instead of exhausting the reachable
136// component. Empty span means flood to exhaustion, matching the public
137// wrapper.
138template <typename World, typename Class, std::uint32_t MaxCost>
139[[nodiscard]] auto build_bounded_weighted_distance_field_core(
140 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
141 MissingChunkPolicy policy, std::span<const std::uint64_t> settle_targets)
143} // namespace detail
144
145// Declared here so the MissingChunkPolicy default lives on the first
146// declaration; the friend declarations and definitions below omit it.
151template <typename World, typename Tag>
152[[nodiscard]] auto astar_path(
153 const World& world, PathRequest request, PathScratch& scratch,
154 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
155 -> PathResult;
156
158template <typename World, typename Tag, typename Provider>
159[[nodiscard]] auto astar_path(const World& world, PathRequest request,
160 PathScratch& scratch, MissingChunkPolicy policy,
161 const Provider& provider) -> PathResult;
162
166template <typename World, typename Class>
167[[nodiscard]] auto weighted_astar_path(
168 const World& world, PathRequest request, PathScratch& scratch,
169 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
170 -> PathResult;
171
173template <typename World, typename Class>
174[[nodiscard]] auto weighted_astar_path(
175 const World& world, PathRequest request, PathScratch& scratch,
176 PathTieBreak tie_break,
177 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
178 -> PathResult;
179
181template <typename World, typename Class, typename Provider>
182[[nodiscard]] auto weighted_astar_path(const World& world, PathRequest request,
183 PathScratch& scratch,
184 MissingChunkPolicy policy,
185 const Provider& provider) -> PathResult;
186
188template <typename World, typename Class, typename Provider>
189[[nodiscard]] auto weighted_astar_path(const World& world, PathRequest request,
190 PathScratch& scratch,
191 MissingChunkPolicy policy,
192 const Provider& provider,
193 PathTieBreak tie_break) -> PathResult;
194
196template <typename World, typename Tag>
197[[nodiscard]] auto build_distance_field_product(const World& world,
198 const GoalSet& goals,
199 DistanceFieldProduct& product,
200 DistanceFieldScratch& scratch)
201 -> DistanceFieldResult;
202
204template <typename World, typename Tag, typename Provider>
205[[nodiscard]] auto build_distance_field_product(const World& world,
206 const GoalSet& goals,
207 DistanceFieldProduct& product,
208 DistanceFieldScratch& scratch,
209 const Provider& provider)
210 -> DistanceFieldResult;
211
213template <typename World, typename Tag>
214[[nodiscard]] auto distance_field_product_path(
215 const World& world, Coord3 start, const DistanceFieldProduct& product,
216 DistanceFieldScratch& scratch) -> PathResult;
217
219template <typename World, typename Tag, typename Provider>
220[[nodiscard]] auto distance_field_product_path(
221 const World& world, Coord3 start, const DistanceFieldProduct& product,
222 DistanceFieldScratch& scratch, const Provider& provider) -> PathResult;
223
225template <typename World, typename Tag>
226[[nodiscard]] auto nearest_target(const World& world, Coord3 start,
227 const DistanceFieldProduct& product,
228 DistanceFieldScratch& scratch)
229 -> NearestTargetResult;
230
235template <typename WorldType, typename Tag>
236[[nodiscard]] auto build_distance_field(
237 const WorldType& world, Coord3 goal, DistanceFieldScratch& scratch,
238 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
239 -> DistanceFieldResult;
240
242template <typename WorldType, typename Tag, typename Provider>
243[[nodiscard]] auto build_distance_field(const WorldType& world, Coord3 goal,
244 DistanceFieldScratch& scratch,
245 MissingChunkPolicy policy,
246 const Provider& provider)
247 -> DistanceFieldResult;
248
253template <typename WorldType, typename Class>
254[[nodiscard]] auto build_weighted_distance_field(
255 const WorldType& world, Coord3 goal, DistanceFieldScratch& scratch,
256 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
257 -> DistanceFieldResult;
258
260template <typename WorldType, typename Class, typename Provider>
261[[nodiscard]] auto build_weighted_distance_field(const WorldType& world,
262 Coord3 goal,
263 DistanceFieldScratch& scratch,
264 MissingChunkPolicy policy,
265 const Provider& provider)
266 -> DistanceFieldResult;
267
269template <typename World, typename Class>
270[[nodiscard]] auto build_weighted_distance_field_in_box(
271 const World& world, Coord3 goal, Box3 domain, DistanceFieldScratch& scratch,
272 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
273 -> DistanceFieldResult;
274
276template <typename World, typename Class, typename Provider>
277[[nodiscard]] auto build_weighted_distance_field_in_box(
278 const World& world, Coord3 goal, Box3 domain, DistanceFieldScratch& scratch,
279 MissingChunkPolicy policy, const Provider& provider) -> DistanceFieldResult;
280
282template <typename World, typename Class, std::uint32_t MaxCost>
283[[nodiscard]] auto build_bounded_weighted_distance_field(
284 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
285 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
286 -> DistanceFieldResult;
287
289template <typename World, typename Class, std::uint32_t MaxCost,
290 typename Provider>
291[[nodiscard]] auto build_bounded_weighted_distance_field(
292 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
293 MissingChunkPolicy policy, const Provider& provider) -> DistanceFieldResult;
294
299 public:
301 ChunkKey key{};
302 ContentVersion content_version{};
303 };
304
305 void reserve(std::size_t count) { chunks_.reserve(count); }
306
307 void clear() noexcept { chunks_.clear(); }
308
309 template <typename World>
310 void capture_all(const World& world) {
311 chunks_.clear();
312 chunks_.reserve(static_cast<std::size_t>(World::chunk_count));
313 // Keys are unique by construction: append directly instead of paying
314 // add_chunk's duplicate scan per key (which would be quadratic here).
315 for (std::uint64_t i = 0; i < World::chunk_count; ++i) {
316 chunks_.push_back(ContentVersionDependency{
317 ChunkKey{i}, world.meta(ChunkKey{i}).content_version});
318 }
319 }
320
321 // Appends without add_chunk's duplicate scan; the caller must guarantee
322 // `key` is not already present (e.g. tracked via an external seen set).
323 template <typename World>
324 void add_chunk_unique(const World& world, ChunkKey key) {
325 chunks_.push_back(
326 ContentVersionDependency{key, world.meta(key).content_version});
327 }
328
329 template <typename World>
330 void add_chunk(const World& world, ChunkKey key) {
331 const auto content_version = world.meta(key).content_version;
332 // Path nodes are chunk-coherent: consecutive additions usually repeat
333 // the previous chunk, so check the last entry before scanning.
334 if (!chunks_.empty() && chunks_.back().key == key) {
335 chunks_.back().content_version = content_version;
336 return;
337 }
338 for (auto& chunk : chunks_) {
339 if (chunk.key == key) {
340 chunk.content_version = content_version;
341 return;
342 }
343 }
344 chunks_.push_back(ContentVersionDependency{key, content_version});
345 }
346
347 template <typename World>
348 [[nodiscard]] auto is_valid(const World& world) const noexcept -> bool {
349 for (const auto chunk : chunks_) {
350 if (world.meta(chunk.key).content_version != chunk.content_version) {
351 return false;
352 }
353 }
354 return true;
355 }
356
357 [[nodiscard]] auto size() const noexcept -> std::size_t {
358 return chunks_.size();
359 }
360
361 [[nodiscard]] auto empty() const noexcept -> bool { return chunks_.empty(); }
362
363 [[nodiscard]] auto chunks() const noexcept
364 -> std::span<const ContentVersionDependency> {
365 return chunks_;
366 }
367
368 private:
369 std::vector<ContentVersionDependency> chunks_;
370};
371
372namespace detail {
373
374// Failure-dependency capture shared by the route/portal product builders.
375// NoPath and CostOverflow depend on world content the search may never have
376// touched (an
377// opening edit lands on an impassable tile; fast-path early-outs sample
378// barriers far from any expanded node), so precise capture is impractical:
379// depend on every chunk, making any edit invalidate the replay instead of it
380// repeating a stale failure forever. InvalidStart/InvalidGoal depend only on
381// the offending tiles; an out-of-bounds tile contributes nothing (bounds are
382// compile-time), which can leave the set empty -- the product is then
383// permanently invalid and callers rebuild, paying only the cheap bounds
384// rejection.
385template <typename Shape, typename World>
386void capture_failure_dependencies(const World& world, PathRequest request,
387 PathStatus status,
388 ContentVersionDependencies& dependencies) {
389 if (status == PathStatus::NoPath || status == PathStatus::NoCandidate ||
390 status == PathStatus::CostOverflow) {
391 dependencies.capture_all(world);
392 return;
393 }
394 if (contains<Shape>(request.start)) {
395 dependencies.add_chunk(world,
396 chunk_key<Shape>(tile_key<Shape>(request.start)));
397 }
398 if (contains<Shape>(request.goal)) {
399 dependencies.add_chunk(world,
400 chunk_key<Shape>(tile_key<Shape>(request.goal)));
401 }
402}
403
404} // namespace detail
405
410 public:
411 void reserve_path_nodes(std::size_t node_count) { path_.reserve(node_count); }
412
413 // Dependency sets are bounded by the world's chunk count (failure
414 // products capture every chunk): reserve chunk_count to keep steady-state
415 // rebuilds allocation-free.
416 void reserve_dependencies(std::size_t count) { dependencies_.reserve(count); }
417
418 void clear() noexcept {
419 request_ = {};
420 status_ = PathStatus::NotComputed;
421 cost_ = 0;
422 expanded_nodes_ = 0;
423 reached_nodes_ = 0;
424 path_.clear();
425 dependencies_.clear();
426 }
427
428 // An empty dependency set means "never validated", not "depends on
429 // nothing": cleared products and failure products that predate dependency
430 // capture must never replay as vacuously valid. Builders capture_all()
431 // for non-Found results, so any built product carries dependencies.
432 template <typename World>
433 [[nodiscard]] auto is_valid(const World& world) const noexcept -> bool {
434 return !dependencies_.empty() && dependencies_.is_valid(world);
435 }
436
437 [[nodiscard]] auto request() const noexcept -> PathRequest {
438 return request_;
439 }
440
441 [[nodiscard]] auto dependencies() const noexcept
442 -> std::span<const ContentVersionDependencies::ContentVersionDependency> {
443 return dependencies_.chunks();
444 }
445
446 private:
447 template <typename World, typename Class>
448 friend auto build_weighted_route_product(const World& world,
449 PathRequest request,
450 PathScratch& scratch,
451 WeightedRouteProduct& product)
452 -> PathResult;
453
454 template <typename World>
455 friend auto weighted_route_product_path(const World& world,
456 const WeightedRouteProduct& product)
457 -> PathResult;
458
459 PathRequest request_{};
460 PathStatus status_ = PathStatus::NotComputed;
461 std::uint32_t cost_ = 0;
462 std::size_t expanded_nodes_ = 0;
463 std::size_t reached_nodes_ = 0;
464 std::vector<Coord3> path_;
465 ContentVersionDependencies dependencies_;
466};
467
470
472
473namespace detail {
474
475template <typename World, typename PassableTag>
476[[nodiscard]] auto select_chunk_portal_waypoints(
477 const World& world, PathRequest request,
478 WeightedPortalRouteProduct& product) -> bool;
479
480} // namespace detail
481
487 public:
488 void reserve_waypoints(std::size_t count) {
489 waypoints_.reserve(count);
490 candidate_waypoints_.reserve(count);
491 best_waypoints_.reserve(count);
492 }
493
494 void reserve_path_nodes(std::size_t node_count) {
495 path_.reserve(node_count);
496 segment_.reserve(node_count);
497 }
498
499 // Dependency sets are bounded by the world's chunk count (failure
500 // products capture every chunk): reserve chunk_count to keep steady-state
501 // rebuilds allocation-free.
502 void reserve_dependencies(std::size_t count) { dependencies_.reserve(count); }
503
504 void clear() noexcept {
505 request_ = {};
506 status_ = PathStatus::NotComputed;
507 cost_ = 0;
508 expanded_nodes_ = 0;
509 reached_nodes_ = 0;
510 route_candidates_ = 0;
511 portal_scan_tiles_ = 0;
512 waypoints_.clear();
513 candidate_waypoints_.clear();
514 best_waypoints_.clear();
515 path_.clear();
516 segment_.clear();
517 dependencies_.clear();
518 }
519
520 // See WeightedRouteProduct::is_valid: empty dependencies are invalid by
521 // definition so failure/cleared products never replay vacuously.
522 template <typename World>
523 [[nodiscard]] auto is_valid(const World& world) const noexcept -> bool {
524 return !dependencies_.empty() && dependencies_.is_valid(world);
525 }
526
527 [[nodiscard]] auto request() const noexcept -> PathRequest {
528 return request_;
529 }
530
531 [[nodiscard]] auto waypoints() const noexcept -> std::span<const Coord3> {
532 return waypoints_;
533 }
534
535 [[nodiscard]] auto dependencies() const noexcept
536 -> std::span<const ContentVersionDependencies::ContentVersionDependency> {
537 return dependencies_.chunks();
538 }
539
540 [[nodiscard]] auto route_candidates() const noexcept -> std::size_t {
541 return route_candidates_;
542 }
543
544 [[nodiscard]] auto portal_scan_tiles() const noexcept -> std::size_t {
545 return portal_scan_tiles_;
546 }
547
548 private:
549 template <typename World, typename Class>
551 const World& world, PathRequest request,
552 std::span<const Coord3> waypoints, PathScratch& scratch,
554
555 template <typename World, typename Class>
557 const World& world, PathRequest request,
558 std::span<const Coord3> waypoints, PathScratch& scratch,
560 -> PathResult;
561
562 template <typename World, typename Class>
564 const World& world, PathRequest request, PathScratch& scratch,
566
567 template <typename World, typename Class>
569 const World& world, PathRequest request, PathScratch& scratch,
571 -> PathResult;
572
573 template <typename World, typename PassableTag>
574 friend auto detail::select_chunk_portal_waypoints(
575 const World& world, PathRequest request,
576 WeightedPortalRouteProduct& product) -> bool;
577
578 template <typename World>
580 const World& world, const WeightedPortalRouteProduct& product)
581 -> PathResult;
582
583 // Rebuild calls may pass spans into this product's own storage --
584 // waypoints() or a previously returned PathResult.path -- and clear() plus
585 // segment stitching invalidate those. Copy product-owned input to `stash`
586 // first; the copy allocates, but only on the aliased rebuild path.
587 [[nodiscard]] auto stash_if_owned(std::span<const Coord3> input,
588 std::vector<Coord3>& stash) const
589 -> std::span<const Coord3> {
590 const auto owned = [&](const std::vector<Coord3>& storage) {
591 const auto* begin = storage.data();
592 const auto* end = begin + storage.size();
593 return !input.empty() &&
594 !std::less<const Coord3*>{}(input.data(), begin) &&
595 std::less<const Coord3*>{}(input.data(), end);
596 };
597 if (owned(waypoints_) || owned(path_) || owned(segment_) ||
598 owned(candidate_waypoints_) || owned(best_waypoints_)) {
599 stash.assign(input.begin(), input.end());
600 return std::span<const Coord3>{stash};
601 }
602 return input;
603 }
604
605 PathRequest request_{};
606 PathStatus status_ = PathStatus::NotComputed;
607 std::uint32_t cost_ = 0;
608 std::size_t expanded_nodes_ = 0;
609 std::size_t reached_nodes_ = 0;
610 std::size_t route_candidates_ = 0;
611 std::size_t portal_scan_tiles_ = 0;
612 std::vector<Coord3> waypoints_;
613 std::vector<Coord3> candidate_waypoints_;
614 std::vector<Coord3> best_waypoints_;
615 std::vector<Coord3> path_;
616 std::vector<Coord3> segment_;
617 ContentVersionDependencies dependencies_;
618};
619
620// Declared here, ahead of the PathScratch friend declarations below, so the
621// default MissingChunkPolicy has exactly one home: defaults may only appear
622// on a template's first declaration, and a friend declaration may not
623// introduce them.
624//
648template <typename World, typename Tag>
649[[nodiscard]] auto cached_astar_path(
650 const World& world, PathRequest request, PathScratch& scratch,
651 UnitRouteCache& cache,
652 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
653 -> PathResult;
654
655template <typename World, typename Tag, typename Provider>
656[[nodiscard]] auto cached_astar_path(
657 const World& world, PathRequest request, PathScratch& scratch,
658 UnitRouteCache& cache, const Provider& provider,
659 MissingChunkPolicy policy = MissingChunkPolicy::ReportIndeterminate)
660 -> PathResult;
661
662namespace detail {
663
664// Packed open-list node: the sort key concatenates the ordering's two
665// lexicographic fields so one compare decides whenever (f, g) differ.
666// Order-isomorphic to open_node_less over (f asc, g desc, index asc) by
667// default. Seeded searches store a reversible permutation of the index as the
668// tertiary key, preserving this 16-byte representation. UINT32_MAX - g is
669// defined and invertible for every g, so the key mapping is injective over the
670// full field range. f is retained in the high word solely for the unit
671// search's two-bucket dial test.
672struct PackedOpenNode {
673 std::uint64_t key = 0;
674 std::uint64_t index = 0;
675
676 [[nodiscard]] static constexpr auto mix_tie(std::uint64_t value) noexcept
677 -> std::uint64_t {
678 value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL;
679 value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL;
680 return value ^ (value >> 31U);
681 }
682
683 [[nodiscard]] static constexpr auto unmix_tie(std::uint64_t value) noexcept
684 -> std::uint64_t {
685 value ^= value >> 31U;
686 value ^= value >> 62U;
687 value *= 0x319642b2d24d8ec3ULL;
688 value ^= value >> 27U;
689 value ^= value >> 54U;
690 value *= 0x96de1b173f119089ULL;
691 value ^= value >> 30U;
692 return value ^ (value >> 60U);
693 }
694
695 [[nodiscard]] static constexpr auto make(std::uint64_t index, std::uint32_t g,
696 std::uint32_t f,
697 PathTieBreak tie_break = {}) noexcept
698 -> PackedOpenNode {
699 return PackedOpenNode{
700 (static_cast<std::uint64_t>(f) << 32u) |
701 (std::numeric_limits<std::uint32_t>::max() - g),
702 tie_break.seed == 0 ? index : mix_tie(index ^ tie_break.seed)};
703 }
704
705 [[nodiscard]] constexpr auto node_index(
706 PathTieBreak tie_break = {}) const noexcept -> std::uint64_t {
707 return tie_break.seed == 0 ? index : unmix_tie(index) ^ tie_break.seed;
708 }
709
710 [[nodiscard]] constexpr auto g() const noexcept -> std::uint32_t {
711 return std::numeric_limits<std::uint32_t>::max() -
712 static_cast<std::uint32_t>(key);
713 }
714
715 [[nodiscard]] constexpr auto f() const noexcept -> std::uint32_t {
716 return static_cast<std::uint32_t>(key >> 32u);
717 }
718};
719
720[[nodiscard]] constexpr bool packed_open_node_less(
721 PackedOpenNode lhs, PackedOpenNode rhs) noexcept {
722 if (lhs.key != rhs.key) {
723 return lhs.key > rhs.key;
724 }
725 return lhs.index > rhs.index;
726}
727
728static_assert(PackedOpenNode::unmix_tie(PackedOpenNode::mix_tie(0)) == 0);
729static_assert(PackedOpenNode::unmix_tie(PackedOpenNode::mix_tie(1)) == 1);
730static_assert(PackedOpenNode::unmix_tie(PackedOpenNode::mix_tie(
731 std::numeric_limits<std::uint64_t>::max())) ==
732 std::numeric_limits<std::uint64_t>::max());
733static_assert(sizeof(PackedOpenNode) == 2 * sizeof(std::uint64_t));
734
735} // namespace detail
736
742 public:
743 struct OpenNode {
744 std::uint64_t index = 0;
745 std::uint32_t g = 0;
746 std::uint32_t f = 0;
747 };
748
749 void reserve_nodes(std::size_t node_count) {
750 open_.reserve(node_count);
751 open_next_.reserve(node_count);
752 generation_.reserve(node_count);
753 state_.reserve(node_count);
754 g_.reserve(node_count);
755 parent_.reserve(node_count);
756 path_.reserve(node_count);
757 }
758
759 void clear() noexcept {
760 advance_epoch();
761 open_.clear();
762 open_next_.clear();
763 touched_count_ = 0;
764 path_.clear();
765 }
766
767 [[nodiscard]] auto capacity_nodes() const noexcept -> std::size_t {
768 return state_.capacity();
769 }
770
771 private:
772 template <typename World, typename Tag>
773 friend auto astar_path(const World& world, PathRequest request,
774 PathScratch& scratch, MissingChunkPolicy policy)
775 -> PathResult;
776
777 template <typename World, typename Tag, typename Provider>
778 friend auto astar_path(const World& world, PathRequest request,
779 PathScratch& scratch, MissingChunkPolicy policy,
780 const Provider& provider) -> PathResult;
781
782 template <typename World, typename Class>
783 friend auto weighted_astar_path(const World& world, PathRequest request,
784 PathScratch& scratch,
785 MissingChunkPolicy policy) -> PathResult;
786
787 template <typename World, typename Class, typename Provider>
788 friend auto weighted_astar_path(const World& world, PathRequest request,
789 PathScratch& scratch,
790 MissingChunkPolicy policy,
791 const Provider& provider) -> PathResult;
792
793 template <typename World, typename Class, typename Provider>
794 friend auto weighted_astar_path(const World& world, PathRequest request,
795 PathScratch& scratch,
796 MissingChunkPolicy policy,
797 const Provider& provider,
798 PathTieBreak tie_break) -> PathResult;
799
800 template <typename World, typename Class>
801 friend auto weighted_astar_path(const World& world, PathRequest request,
802 PathScratch& scratch, PathTieBreak tie_break,
803 MissingChunkPolicy policy) -> PathResult;
804
805 template <typename World, typename Tag>
806 friend auto cached_astar_path(const World& world, PathRequest request,
807 PathScratch& scratch, UnitRouteCache& cache,
808 MissingChunkPolicy policy) -> PathResult;
809
810 template <typename World, typename Tag, typename Provider>
811 friend auto cached_astar_path(const World& world, PathRequest request,
812 PathScratch& scratch, UnitRouteCache& cache,
813 const Provider& provider,
814 MissingChunkPolicy policy) -> PathResult;
815
816 void advance_epoch() noexcept {
817 ++epoch_;
818 if (epoch_ == 0) {
819 std::fill(generation_.begin(), generation_.end(), 0);
820 epoch_ = 1;
821 }
822 }
823
824 [[nodiscard]] auto is_current(std::size_t offset) const noexcept -> bool {
825 return generation_[offset] == epoch_;
826 }
827
828 [[nodiscard]] auto state_at(std::size_t offset,
829 std::uint8_t unseen) const noexcept
830 -> std::uint8_t {
831 return is_current(offset) ? state_[offset] : unseen;
832 }
833
834 [[nodiscard]] auto g_at(std::size_t offset,
835 std::uint32_t infinite_cost) const noexcept
836 -> std::uint32_t {
837 return is_current(offset) ? g_[offset] : infinite_cost;
838 }
839
840 // offset is the node-array slot under the search's NodeIndexSpace; only
841 // the touched count survives for the expansion metric.
842 void touch_node(std::size_t offset) {
843 generation_[offset] = epoch_;
844 ++touched_count_;
845 }
846
847 std::vector<detail::PackedOpenNode> open_;
848 std::vector<detail::PackedOpenNode> open_next_;
849 // Parallel arrays deliberately: an interleaved {generation, g, state}
850 // record measured 3-9% slower --
851 // partial-field visits (closed checks read generation+state only) waste
852 // bandwidth on a 12-byte record, while the packed arrays keep 16
853 // generations per cache line. See the optimization log, 2026-07-12.
854 std::vector<std::uint32_t> generation_;
855 std::uint32_t epoch_ = 1;
856 std::vector<std::uint8_t> state_;
857 std::vector<std::uint32_t> g_;
858 std::vector<std::uint64_t> parent_;
859 // Reached-node count only: unlike DistanceFieldScratch (whose touched
860 // list feeds dependency capture), no A* consumer reads the indices, so
861 // recording them cost an 8-byte store per reached node for nothing.
862 std::size_t touched_count_ = 0;
863 std::vector<Coord3> path_;
864};
865
873 public:
874 void reserve_nodes(std::size_t node_count) {
875 frontier_.reserve(node_count);
876 weighted_frontier_.reserve(node_count);
877 weighted_bucket_capacity_ = node_count / 8u + 1u;
878 for (auto& bucket : weighted_buckets_) {
879 bucket.reserve(weighted_bucket_capacity_);
880 }
881 generation_.reserve(node_count);
882 distance_.reserve(node_count);
883 target_generation_.reserve(node_count);
884 touched_.reserve(node_count);
885 path_.reserve(node_count);
886 }
887
888 [[nodiscard]] auto capacity_nodes() const noexcept -> std::size_t {
889 return distance_.capacity();
890 }
891
892 private:
893 template <typename WorldType, typename Tag>
894 friend auto build_distance_field(const WorldType& world, Coord3 goal,
895 DistanceFieldScratch& scratch,
896 MissingChunkPolicy policy)
898
899 template <typename World, typename Class, typename Provider>
901 const World& world, Coord3 goal, Box3 domain,
902 DistanceFieldScratch& scratch, MissingChunkPolicy policy,
903 const Provider& provider) -> DistanceFieldResult;
904
905 template <typename World, typename Tag>
906 friend auto distance_field_path(const World& world, PathRequest request,
907 DistanceFieldScratch& scratch) -> PathResult;
908
909 // The weighted friends name the movement-class cores.
910 template <typename WorldType, typename Class>
911 friend auto build_weighted_distance_field(const WorldType& world, Coord3 goal,
912 DistanceFieldScratch& scratch,
913 MissingChunkPolicy policy)
915
916 template <typename WorldType, typename Class, typename Provider>
917 friend auto build_weighted_distance_field(const WorldType& world, Coord3 goal,
918 DistanceFieldScratch& scratch,
919 MissingChunkPolicy policy,
920 const Provider& provider)
922
923 template <typename World, typename Class>
925 const World& world, Coord3 goal, Box3 domain,
926 DistanceFieldScratch& scratch, MissingChunkPolicy policy)
928
929 template <typename World, typename Class, std::uint32_t MaxCost>
930 friend auto detail::build_bounded_weighted_distance_field_core(
931 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
932 MissingChunkPolicy policy, std::span<const std::uint64_t> settle_targets)
934
935 // The public weighted_distance_field_path is a thin forwarder; the
936 // private access lives in the detail core it forwards to.
937 template <typename World, typename Class>
938 friend auto detail::weighted_distance_field_path_core(
939 const World& world, PathRequest request, DistanceFieldScratch& scratch,
940 bool verify_residency) -> PathResult;
941
942 template <typename World, typename Class, typename Provider>
943 friend auto detail::weighted_distance_field_path_core(
944 const World& world, PathRequest request, DistanceFieldScratch& scratch,
945 bool verify_residency, const Provider& provider) -> PathResult;
946
947 // The batch skips the core's per-member residency verification and
948 // instead asserts the stamp once per group build.
949 template <typename World, typename Class, std::uint32_t MaxCost>
950 friend auto weighted_path_batch(const World& world,
951 std::span<const PathRequest> requests,
953 MissingChunkPolicy policy)
954 -> std::span<const PathResult>;
955
956 template <typename World, typename Class, std::uint32_t MaxCost,
957 typename Provider>
958 friend auto weighted_path_batch(const World& world,
959 std::span<const PathRequest> requests,
961 MissingChunkPolicy policy,
962 const Provider& provider)
963 -> std::span<const PathResult>;
964
965 template <typename World, typename Tag>
966 friend auto build_distance_field_product(const World& world,
967 const GoalSet& goals,
968 DistanceFieldProduct& product,
969 DistanceFieldScratch& scratch)
971
972 template <typename World, typename Tag, typename Provider>
973 friend auto build_distance_field_product(const World& world,
974 const GoalSet& goals,
975 DistanceFieldProduct& product,
976 DistanceFieldScratch& scratch,
977 const Provider& provider)
979
980 template <typename World, typename Tag>
981 friend auto distance_field_product_path(const World& world, Coord3 start,
982 const DistanceFieldProduct& product,
983 DistanceFieldScratch& scratch)
984 -> PathResult;
985
986 template <typename World, typename Tag, typename Provider>
987 friend auto distance_field_product_path(const World& world, Coord3 start,
988 const DistanceFieldProduct& product,
989 DistanceFieldScratch& scratch,
990 const Provider& provider)
991 -> PathResult;
992
993 template <typename World, typename Tag>
994 friend auto nearest_target(const World& world, Coord3 start,
995 const DistanceFieldProduct& product,
996 DistanceFieldScratch& scratch)
998
999 template <typename World, typename Tag, typename Provider>
1000 friend auto nearest_target(const World& world, Coord3 start,
1001 const DistanceFieldProduct& product,
1002 DistanceFieldScratch& scratch,
1003 const Provider& provider) -> NearestTargetResult;
1004
1005 template <typename World, typename Class, typename Provider>
1007 const World& world, const GoalSet& goals, DistanceFieldProduct& product,
1008 DistanceFieldScratch& scratch, const Provider& provider)
1010
1011 template <typename World, typename Class, typename Provider>
1013 const World& world, Coord3 start, const DistanceFieldProduct& product,
1014 DistanceFieldScratch& scratch, const Provider& provider) -> PathResult;
1015
1016 void clear_build() noexcept {
1017 advance_epoch();
1018 frontier_.clear();
1019 weighted_frontier_.clear();
1020 for (auto& bucket : weighted_buckets_) {
1021 bucket.clear();
1022 }
1023 touched_.clear();
1024 path_.clear();
1025 // These fields are the public-result validity sentinels. Per-node
1026 // distances and predecessors intentionally retain old bytes: the epoch
1027 // stamps invalidate them without an O(world-size) clearing pass.
1028 has_goal_ = false;
1029 build_status_ = PathStatus::NotComputed;
1030 model_class_identity_ = 0;
1031 model_provider_identity_ = 0;
1032 model_provider_instance_identity_ = nullptr;
1033 model_provider_revision_ = 0;
1034 }
1035
1036 // Preserve node labels and diagnostics for the just-failed build while
1037 // withdrawing the public two-call validity stamp. Without this distinction,
1038 // a caller that inspects CostOverflow and later reuses the scratch could
1039 // reconstruct a plausible path through only the finite prefix.
1040 void discard_build_result() noexcept {
1041 has_goal_ = false;
1042 build_status_ = PathStatus::NotComputed;
1043 model_class_identity_ = 0;
1044 model_provider_identity_ = 0;
1045 model_provider_instance_identity_ = nullptr;
1046 model_provider_revision_ = 0;
1047 }
1048
1049 void clear_path() noexcept { path_.clear(); }
1050
1051 void publish_build_status(PathStatus status) noexcept {
1052 build_status_ = status;
1053 }
1054
1055 [[nodiscard]] auto unresolved_path_status() const noexcept -> PathStatus {
1056 if (build_status_ == PathStatus::Indeterminate) {
1057 return PathStatus::Indeterminate;
1058 }
1059 return build_status_ == PathStatus::Found ? PathStatus::NoPath
1060 : PathStatus::NotComputed;
1061 }
1062
1063 void advance_epoch() noexcept {
1064 ++epoch_;
1065 if (epoch_ == 0) {
1066 std::fill(generation_.begin(), generation_.end(), 0);
1067 std::fill(target_generation_.begin(), target_generation_.end(), 0);
1068 epoch_ = 1;
1069 }
1070 }
1071
1072 [[nodiscard]] auto is_current(std::size_t offset) const noexcept -> bool {
1073 return generation_[offset] == epoch_;
1074 }
1075
1076 [[nodiscard]] auto distance_at(std::size_t offset,
1077 std::uint32_t infinite_distance) const noexcept
1078 -> std::uint32_t {
1079 return is_current(offset) ? distance_[offset] : infinite_distance;
1080 }
1081
1082 void touch_node(std::size_t offset, std::uint64_t index) {
1083 generation_[offset] = epoch_;
1084 touched_.push_back(index);
1085 }
1086
1087 // Dense-only convenience (offset == index) for distance-field functions
1088 // whose contracts require AlwaysResident worlds.
1089 void touch_node(std::uint64_t index) {
1090 touch_node(static_cast<std::size_t>(index), index);
1091 }
1092
1093 // Settle-target marks share the build epoch: clear_build invalidates
1094 // them wholesale, and advance_epoch's wrap path re-zeros both stamp
1095 // arrays.
1096 void mark_settle_target(std::size_t offset) {
1097 target_generation_[offset] = epoch_;
1098 }
1099
1100 [[nodiscard]] auto is_settle_target(std::size_t offset) const noexcept
1101 -> bool {
1102 return target_generation_[offset] == epoch_;
1103 }
1104
1105 std::vector<std::uint64_t> frontier_;
1106 std::vector<detail::PackedOpenNode> weighted_frontier_;
1107 std::vector<std::vector<std::uint64_t>> weighted_buckets_;
1108 std::size_t weighted_bucket_capacity_ = 0;
1109 std::vector<std::uint32_t> generation_;
1110 std::uint32_t epoch_ = 1;
1111 std::vector<std::uint32_t> distance_;
1112 std::vector<std::uint32_t> target_generation_;
1113 std::vector<std::uint64_t> touched_;
1114 std::vector<Coord3> path_;
1115 // Per-chunk seen marks for build_distance_field_product's blocked-frontier
1116 // dependency pass; sized to the world's chunk count on use.
1117 std::vector<std::uint8_t> chunk_seen_;
1118 Coord3 goal_{};
1119 bool has_goal_ = false;
1120 PathStatus build_status_ = PathStatus::NotComputed;
1121 std::uint64_t residency_fingerprint_ = 0;
1122
1123 // Sparse residency staleness guard for the two-call build/read API. A built
1124 // distance field is indexed by resident-slot offset; if the resident set
1125 // changes between build_*distance_field and *distance_field_path (an
1126 // eviction/rematerialization can rebind a slot to a different chunk), the
1127 // reader would descend a stale field and return a wrong path. build_* stamps
1128 // the world's residency fingerprint (a content hash of the resident set, not
1129 // a per-world counter -- so it also catches a scratch read against a
1130 // different/copied/ swapped world, which a bare epoch could alias) and the
1131 // readers reject a mismatch (forcing a rebuild) instead of returning a wrong
1132 // Found. Dense worlds never evict, so both methods compile to a no-op /
1133 // constant true and keep dense byte-identical.
1134 template <typename World>
1135 void stamp_residency(const World& world) noexcept {
1136 if constexpr (!std::is_same_v<typename World::residency_type,
1137 AlwaysResident>) {
1138 residency_fingerprint_ = world.residency_fingerprint();
1139 }
1140 }
1141 template <typename World>
1142 [[nodiscard]] auto residency_matches(const World& world) const noexcept
1143 -> bool {
1144 if constexpr (std::is_same_v<typename World::residency_type,
1145 AlwaysResident>) {
1146 return true;
1147 } else {
1148 return residency_fingerprint_ == world.residency_fingerprint();
1149 }
1150 }
1151
1152 template <typename Model>
1153 void stamp_model(const Model& model = Model{},
1154 const void* provider_instance = nullptr) noexcept {
1155 model_class_identity_ = detail::tag_identity<typename Model::class_type>();
1156 model_lattice_identity_ =
1157 static_cast<std::uint32_t>(Model::lattice_identity);
1158 model_lattice_version_ = Model::lattice_version;
1159 model_step_identity_ =
1160 static_cast<std::uint32_t>(Model::step_policy_identity);
1161 model_cost_scale_ = Model::cost_scale;
1162 model_provider_identity_ =
1163 detail::tag_identity<typename Model::provider_type>();
1164 model_provider_instance_identity_ = provider_instance;
1165 model_provider_revision_ = model.revision();
1166 }
1167
1168 template <typename Model>
1169 [[nodiscard]] auto model_matches(
1170 const Model& model = Model{},
1171 const void* provider_instance = nullptr) const noexcept -> bool {
1172 return model_class_identity_ ==
1173 detail::tag_identity<typename Model::class_type>() &&
1174 model_lattice_identity_ ==
1175 static_cast<std::uint32_t>(Model::lattice_identity) &&
1176 model_lattice_version_ == Model::lattice_version &&
1177 model_step_identity_ ==
1178 static_cast<std::uint32_t>(Model::step_policy_identity) &&
1179 model_cost_scale_ == Model::cost_scale &&
1180 model_provider_identity_ ==
1181 detail::tag_identity<typename Model::provider_type>() &&
1182 model_provider_instance_identity_ == provider_instance &&
1183 model_provider_revision_ == model.revision();
1184 }
1185
1186 std::uintptr_t model_class_identity_ = 0;
1187 std::uint32_t model_lattice_identity_ = 0;
1188 std::uint32_t model_lattice_version_ = 0;
1189 std::uint32_t model_step_identity_ = 0;
1190 std::uint32_t model_cost_scale_ = 0;
1191 std::uintptr_t model_provider_identity_ = 0;
1192 const void* model_provider_instance_identity_ = nullptr;
1193 std::uint64_t model_provider_revision_ = 0;
1194};
1195
1201 public:
1202 void reserve_requests(std::size_t request_count) {
1203 results_.reserve(request_count);
1204 offsets_.reserve(request_count);
1205 sizes_.reserve(request_count);
1206 processed_.reserve(request_count);
1207 request_goal_.reserve(request_count);
1208 goal_coords_.reserve(request_count);
1209 goal_counts_.reserve(request_count);
1210 }
1211
1212 void reserve_path_nodes(std::size_t node_count) {
1213 paths_.reserve(node_count);
1214 }
1215
1216 void reserve_search_nodes(std::size_t node_count) {
1217 field_scratch_.reserve_nodes(node_count);
1218 astar_scratch_.reserve_nodes(node_count);
1219 }
1220
1221 void clear() noexcept {
1222 results_.clear();
1223 offsets_.clear();
1224 sizes_.clear();
1225 processed_.clear();
1226 paths_.clear();
1227 stats_ = {};
1228 }
1229
1230 [[nodiscard]] auto stats() const noexcept -> WeightedPathBatchStats {
1231 return stats_;
1232 }
1233
1234 private:
1235 template <typename World, typename Class, std::uint32_t MaxCost>
1236 friend auto weighted_path_batch(const World& world,
1237 std::span<const PathRequest> requests,
1238 WeightedPathBatchScratch& scratch,
1239 MissingChunkPolicy policy)
1240 -> std::span<const PathResult>;
1241
1242 template <typename World, typename Class, std::uint32_t MaxCost,
1243 typename Provider>
1244 friend auto weighted_path_batch(const World& world,
1245 std::span<const PathRequest> requests,
1246 WeightedPathBatchScratch& scratch,
1247 MissingChunkPolicy policy,
1248 const Provider& provider)
1249 -> std::span<const PathResult>;
1250
1251 DistanceFieldScratch field_scratch_;
1252 PathScratch astar_scratch_;
1253 std::vector<PathResult> results_;
1254 std::vector<std::size_t> offsets_;
1255 std::vector<std::size_t> sizes_;
1256 std::vector<std::uint8_t> processed_;
1257 std::vector<Coord3> paths_;
1258 // Reusable goal -> request count flat map (open-addressed, power-of-two
1259 // capacity, linear probing) built once per batch call.
1260 std::vector<std::uint32_t> goal_slots_;
1261 std::vector<Coord3> goal_coords_;
1262 std::vector<std::uint32_t> goal_counts_;
1263 std::vector<std::uint32_t> request_goal_;
1264 // Counting-sort member buckets (group_offsets_[g]..group_offsets_[g+1]
1265 // indexes group_members_), mirroring PathRequestRuntime's grouping, so
1266 // scattering a group's results touches only its own members instead of
1267 // rescanning every request per group.
1268 std::vector<std::uint32_t> group_offsets_;
1269 std::vector<std::uint32_t> group_cursors_;
1270 std::vector<std::uint32_t> group_members_;
1271 // Per-group validated start tile indices handed to the field build as
1272 // settle targets.
1273 std::vector<std::uint64_t> settle_targets_;
1275};
1276
1277namespace detail {
1278
1279enum class Axis : std::uint8_t {
1280 X,
1281 Y,
1282 Z,
1283};
1284
1285struct PortalRouteCandidate {
1286 bool found = false;
1287 std::uint32_t score = 0;
1288 std::size_t scan_tiles = 0;
1289};
1290
1291// FNV-style lane combine with one final avalanche: cheap per coordinate,
1292// well distributed for the power-of-two linear-probing flat hash maps used
1293// by the batch planner and the request runtime (matching the route cache's
1294// hashing style).
1295[[nodiscard]] constexpr auto coord_hash(Coord3 coord) noexcept
1296 -> std::uint64_t {
1297 auto hash = std::uint64_t{0xcbf29ce484222325ull};
1298 hash = (hash ^ static_cast<std::uint64_t>(coord.x)) * 0x100000001b3ull;
1299 hash = (hash ^ static_cast<std::uint64_t>(coord.y)) * 0x100000001b3ull;
1300 hash = (hash ^ static_cast<std::uint64_t>(coord.z)) * 0x100000001b3ull;
1301 hash = (hash ^ (hash >> 30u)) * 0xbf58476d1ce4e5b9ull;
1302 hash = (hash ^ (hash >> 27u)) * 0x94d049bb133111ebull;
1303 return hash ^ (hash >> 31u);
1304}
1305
1306[[nodiscard]] constexpr auto manhattan(Coord3 lhs, Coord3 rhs) noexcept
1307 -> std::uint32_t {
1308 const auto distance = manhattan_distance(lhs, rhs);
1309 if (distance > std::numeric_limits<std::uint32_t>::max()) {
1310 return std::numeric_limits<std::uint32_t>::max();
1311 }
1312 return static_cast<std::uint32_t>(distance);
1313}
1314
1315template <typename World>
1316[[nodiscard]] constexpr auto tile_count() noexcept -> std::size_t {
1317 static_assert(World::chunk_count <= std::numeric_limits<std::size_t>::max() /
1318 World::local_tile_count);
1319 return static_cast<std::size_t>(World::chunk_count * World::local_tile_count);
1320}
1321
1322template <typename Shape>
1323[[nodiscard]] constexpr auto tile_index(Coord3 coord) noexcept
1324 -> std::uint64_t {
1325 static_assert(ShapeTraits<Shape>::tile_key_bits <= 64,
1326 "pathfinding requires shapes with at most 64-bit tile keys");
1327 return static_cast<std::uint64_t>(tile_key<Shape>(coord).value);
1328}
1329
1330template <typename Shape>
1331[[nodiscard]] constexpr auto tile_coord(std::uint64_t index) noexcept
1332 -> Coord3 {
1333 using Storage = typename ShapeTraits<Shape>::TileKeyStorage;
1334 return coord<Shape>(TileKey<Shape>{static_cast<Storage>(index)});
1335}
1336
1337// The passability/cost leaves take a movement class OR a raw passable tag
1338// (normalized through movement_class_of, so every legacy <World, Tag> call
1339// site compiles unchanged and the identity class keeps codegen byte-identical
1340// to the raw field cast it replaces).
1341template <typename World, typename ClassOrTag>
1342[[nodiscard]] auto is_passable(const World& world, Coord3 coord) noexcept
1343 -> bool {
1344 using Class = movement::movement_class_of<ClassOrTag>;
1345 const auto resolved = world.try_resolve(coord);
1346 if (!resolved.has_value()) {
1347 return false;
1348 }
1349 if constexpr (std::is_same_v<typename World::residency_type,
1350 SparseResident>) {
1351 const auto* page = world.try_chunk(resolved->chunk_key);
1352 return page != nullptr && Class::passable(*page, resolved->local_tile_id);
1353 } else {
1354 return Class::passable(world.chunk(resolved->chunk_key),
1355 resolved->local_tile_id);
1356 }
1357}
1358
1359template <typename World, typename ClassOrTag>
1360[[nodiscard]] auto is_passable_index(const World& world,
1361 std::uint64_t index) noexcept -> bool {
1362 using Class = movement::movement_class_of<ClassOrTag>;
1363 using Shape = typename World::shape_type;
1364 using Storage = typename ShapeTraits<Shape>::TileKeyStorage;
1365 const auto key = TileKey<Shape>{static_cast<Storage>(index)};
1366 if constexpr (std::is_same_v<typename World::residency_type,
1367 SparseResident>) {
1368 // A non-resident chunk carries no data, so it reads as impassable. This
1369 // keeps the unchecked hot-loop access safe on sparse worlds; the search's
1370 // residency guard decides whether "impassable" means blocked or missing.
1371 const auto* page = world.try_chunk(chunk_key<Shape>(key));
1372 if (page == nullptr) {
1373 return false;
1374 }
1375 return Class::passable(*page, local_tile_id<Shape>(key));
1376 } else {
1377 return Class::passable(world.chunk(chunk_key<Shape>(key)),
1378 local_tile_id<Shape>(key));
1379 }
1380}
1381
1382// Unlike the passability leaves this requires a movement class: a raw tag
1383// would normalize to unit-cost movement and silently discard weighted cost.
1384template <typename World, typename Class>
1385[[nodiscard]] auto tile_entry_cost_index(const World& world,
1386 std::uint64_t index) noexcept
1387 -> std::uint32_t {
1388 static_assert(std::derived_from<Class, movement::movement_class_tag>,
1389 "tile_entry_cost_index requires a MovementClass; pass an "
1390 "explicit MovementClass.");
1391 using Shape = typename World::shape_type;
1392 using Storage = typename ShapeTraits<Shape>::TileKeyStorage;
1393 const auto key = TileKey<Shape>{static_cast<Storage>(index)};
1394 TESS_DIAG_EVENT(path_cost_read);
1395 return Class::entry_cost(world.chunk(chunk_key<Shape>(key)),
1396 local_tile_id<Shape>(key));
1397}
1398
1399[[nodiscard]] constexpr auto saturating_add(std::uint32_t lhs,
1400 std::uint32_t rhs) noexcept
1401 -> std::uint32_t {
1402 if (rhs > std::numeric_limits<std::uint32_t>::max() - lhs) {
1403 return std::numeric_limits<std::uint32_t>::max();
1404 }
1405 return lhs + rhs;
1406}
1407
1408template <typename World, typename Tag>
1409[[nodiscard]] auto is_full_axis_barrier(const World& world, Coord3 blocked,
1410 Axis axis) noexcept -> bool {
1411 using Shape = typename World::shape_type;
1412 constexpr auto size = ShapeTraits<Shape>::size;
1413
1414 if (axis == Axis::X) {
1415 for (std::int64_t z = 0; z < static_cast<std::int64_t>(size.z); ++z) {
1416 for (std::int64_t y = 0; y < static_cast<std::int64_t>(size.y); ++y) {
1417 const auto coord = Coord3{blocked.x, y, z};
1418 TESS_DIAG_EVENT(path_passability_check);
1419 if (is_passable_index<World, Tag>(world, tile_index<Shape>(coord))) {
1420 return false;
1421 }
1422 }
1423 }
1424 return true;
1425 }
1426
1427 if (axis == Axis::Y) {
1428 for (std::int64_t z = 0; z < static_cast<std::int64_t>(size.z); ++z) {
1429 for (std::int64_t x = 0; x < static_cast<std::int64_t>(size.x); ++x) {
1430 const auto coord = Coord3{x, blocked.y, z};
1431 TESS_DIAG_EVENT(path_passability_check);
1432 if (is_passable_index<World, Tag>(world, tile_index<Shape>(coord))) {
1433 return false;
1434 }
1435 }
1436 }
1437 return true;
1438 }
1439
1440 for (std::int64_t y = 0; y < static_cast<std::int64_t>(size.y); ++y) {
1441 for (std::int64_t x = 0; x < static_cast<std::int64_t>(size.x); ++x) {
1442 const auto coord = Coord3{x, y, blocked.z};
1443 TESS_DIAG_EVENT(path_passability_check);
1444 if (is_passable_index<World, Tag>(world, tile_index<Shape>(coord))) {
1445 return false;
1446 }
1447 }
1448 }
1449 return true;
1450}
1451
1452template <typename Shape>
1453[[nodiscard]] constexpr auto chunk_origin(ChunkCoord3 chunk) noexcept
1454 -> Coord3 {
1455 constexpr auto size = ShapeTraits<Shape>::chunk;
1456 return Coord3{
1457 static_cast<std::int64_t>(chunk.x * size.x),
1458 static_cast<std::int64_t>(chunk.y * size.y),
1459 static_cast<std::int64_t>(chunk.z * size.z),
1460 };
1461}
1462
1463template <typename Shape>
1464[[nodiscard]] constexpr auto adjacent_chunk(ChunkCoord3 from,
1465 ChunkCoord3 to) noexcept -> bool {
1466 const auto dx = from.x > to.x ? from.x - to.x : to.x - from.x;
1467 const auto dy = from.y > to.y ? from.y - to.y : to.y - from.y;
1468 const auto dz = from.z > to.z ? from.z - to.z : to.z - from.z;
1469 return dx + dy + dz == 1;
1470}
1471
1472template <typename World, typename PassableTag>
1473[[nodiscard]] auto best_chunk_portal(const World& world, ChunkCoord3 from,
1474 ChunkCoord3 to, Coord3 current,
1475 Coord3 goal, Coord3& portal,
1476 std::size_t* scan_tiles = nullptr) noexcept
1477 -> bool {
1478 using Shape = typename World::shape_type;
1479 using Class = movement::movement_class_of<PassableTag>;
1480 using Traits = ShapeTraits<Shape>;
1481 constexpr auto chunk = Traits::chunk;
1482 const auto origin = chunk_origin<Shape>(from);
1483
1484 if (!adjacent_chunk<Shape>(from, to)) {
1485 return false;
1486 }
1487
1488 auto found = false;
1489 auto best_score = std::numeric_limits<std::uint32_t>::max();
1490 const auto score_target = [&](Coord3 target) {
1491 const auto score =
1492 saturating_add(manhattan(current, target), manhattan(target, goal));
1493 if (!found || score < best_score) {
1494 found = true;
1495 best_score = score;
1496 portal = target;
1497 }
1498 };
1499
1500 // Fast path: when both chunk coordinates lie inside the chunk grid and
1501 // both pages are acquirable, per-tile coordinate resolution hoists to
1502 // two page lookups and the seam walks local tile ids directly — the
1503 // same predicate, iteration order, scoring, tie-breaking, and
1504 // scan/diagnostic accounting as the generic loop below, which remains
1505 // the authority for out-of-shape or non-resident chunks.
1506 const auto in_grid = [](ChunkCoord3 coord) {
1507 return coord.x < Traits::chunk_count_x && coord.y < Traits::chunk_count_y &&
1508 coord.z < Traits::chunk_count_z;
1509 };
1510 if (in_grid(from) && in_grid(to)) {
1511 const auto acquire = [&](ChunkCoord3 coord) {
1512 if constexpr (std::is_same_v<typename World::residency_type,
1513 SparseResident>) {
1514 return world.try_chunk(chunk_key<Shape>(coord));
1515 } else {
1516 return &world.chunk(chunk_key<Shape>(coord));
1517 }
1518 };
1519 const auto* from_page = acquire(from);
1520 const auto* to_page = acquire(to);
1521 if (from_page != nullptr && to_page != nullptr) {
1522 const auto consider_local = [&](LocalCoord3 source_local,
1523 LocalCoord3 target_local, Coord3 target) {
1524 if (scan_tiles != nullptr) {
1525 ++(*scan_tiles);
1526 }
1527 TESS_DIAG_EVENT(path_passability_check);
1528 if (!Class::passable(*from_page, local_tile_id<Shape>(source_local))) {
1529 return;
1530 }
1531 TESS_DIAG_EVENT(path_passability_check);
1532 if (!Class::passable(*to_page, local_tile_id<Shape>(target_local))) {
1533 return;
1534 }
1535 score_target(target);
1536 };
1537
1538 if (from.x != to.x) {
1539 const auto step = from.x < to.x ? std::int64_t{1} : std::int64_t{-1};
1540 const auto source_x =
1541 step > 0 ? static_cast<std::int64_t>(chunk.x) - 1 : std::int64_t{0};
1542 const auto target_x =
1543 step > 0 ? std::int64_t{0} : static_cast<std::int64_t>(chunk.x) - 1;
1544 const auto world_target_x = origin.x + source_x + step;
1545 for (std::int64_t z = 0; z < static_cast<std::int64_t>(chunk.z); ++z) {
1546 for (std::int64_t y = 0; y < static_cast<std::int64_t>(chunk.y);
1547 ++y) {
1548 consider_local(LocalCoord3{static_cast<std::uint64_t>(source_x),
1549 static_cast<std::uint64_t>(y),
1550 static_cast<std::uint64_t>(z)},
1551 LocalCoord3{static_cast<std::uint64_t>(target_x),
1552 static_cast<std::uint64_t>(y),
1553 static_cast<std::uint64_t>(z)},
1554 Coord3{world_target_x, origin.y + y, origin.z + z});
1555 }
1556 }
1557 return found;
1558 }
1559 if (from.y != to.y) {
1560 const auto step = from.y < to.y ? std::int64_t{1} : std::int64_t{-1};
1561 const auto source_y =
1562 step > 0 ? static_cast<std::int64_t>(chunk.y) - 1 : std::int64_t{0};
1563 const auto target_y =
1564 step > 0 ? std::int64_t{0} : static_cast<std::int64_t>(chunk.y) - 1;
1565 const auto world_target_y = origin.y + source_y + step;
1566 for (std::int64_t z = 0; z < static_cast<std::int64_t>(chunk.z); ++z) {
1567 for (std::int64_t x = 0; x < static_cast<std::int64_t>(chunk.x);
1568 ++x) {
1569 consider_local(LocalCoord3{static_cast<std::uint64_t>(x),
1570 static_cast<std::uint64_t>(source_y),
1571 static_cast<std::uint64_t>(z)},
1572 LocalCoord3{static_cast<std::uint64_t>(x),
1573 static_cast<std::uint64_t>(target_y),
1574 static_cast<std::uint64_t>(z)},
1575 Coord3{origin.x + x, world_target_y, origin.z + z});
1576 }
1577 }
1578 return found;
1579 }
1580 const auto step = from.z < to.z ? std::int64_t{1} : std::int64_t{-1};
1581 const auto source_z =
1582 step > 0 ? static_cast<std::int64_t>(chunk.z) - 1 : std::int64_t{0};
1583 const auto target_z =
1584 step > 0 ? std::int64_t{0} : static_cast<std::int64_t>(chunk.z) - 1;
1585 const auto world_target_z = origin.z + source_z + step;
1586 for (std::int64_t y = 0; y < static_cast<std::int64_t>(chunk.y); ++y) {
1587 for (std::int64_t x = 0; x < static_cast<std::int64_t>(chunk.x); ++x) {
1588 consider_local(LocalCoord3{static_cast<std::uint64_t>(x),
1589 static_cast<std::uint64_t>(y),
1590 static_cast<std::uint64_t>(source_z)},
1591 LocalCoord3{static_cast<std::uint64_t>(x),
1592 static_cast<std::uint64_t>(y),
1593 static_cast<std::uint64_t>(target_z)},
1594 Coord3{origin.x + x, origin.y + y, world_target_z});
1595 }
1596 }
1597 return found;
1598 }
1599 }
1600
1601 const auto consider = [&](Coord3 source, Coord3 target) {
1602 if (scan_tiles != nullptr) {
1603 ++(*scan_tiles);
1604 }
1605 TESS_DIAG_EVENT(path_passability_check);
1606 if (!is_passable<World, PassableTag>(world, source)) {
1607 return;
1608 }
1609 TESS_DIAG_EVENT(path_passability_check);
1610 if (!is_passable<World, PassableTag>(world, target)) {
1611 return;
1612 }
1613 score_target(target);
1614 };
1615
1616 if (from.x != to.x) {
1617 const auto step = from.x < to.x ? std::int64_t{1} : std::int64_t{-1};
1618 const auto source_x =
1619 step > 0 ? origin.x + static_cast<std::int64_t>(chunk.x) - 1 : origin.x;
1620 for (std::int64_t z = origin.z;
1621 z < origin.z + static_cast<std::int64_t>(chunk.z); ++z) {
1622 for (std::int64_t y = origin.y;
1623 y < origin.y + static_cast<std::int64_t>(chunk.y); ++y) {
1624 consider(Coord3{source_x, y, z}, Coord3{source_x + step, y, z});
1625 }
1626 }
1627 return found;
1628 }
1629
1630 if (from.y != to.y) {
1631 const auto step = from.y < to.y ? std::int64_t{1} : std::int64_t{-1};
1632 const auto source_y =
1633 step > 0 ? origin.y + static_cast<std::int64_t>(chunk.y) - 1 : origin.y;
1634 for (std::int64_t z = origin.z;
1635 z < origin.z + static_cast<std::int64_t>(chunk.z); ++z) {
1636 for (std::int64_t x = origin.x;
1637 x < origin.x + static_cast<std::int64_t>(chunk.x); ++x) {
1638 consider(Coord3{x, source_y, z}, Coord3{x, source_y + step, z});
1639 }
1640 }
1641 return found;
1642 }
1643
1644 const auto step = from.z < to.z ? std::int64_t{1} : std::int64_t{-1};
1645 const auto source_z =
1646 step > 0 ? origin.z + static_cast<std::int64_t>(chunk.z) - 1 : origin.z;
1647 for (std::int64_t y = origin.y;
1648 y < origin.y + static_cast<std::int64_t>(chunk.y); ++y) {
1649 for (std::int64_t x = origin.x;
1650 x < origin.x + static_cast<std::int64_t>(chunk.x); ++x) {
1651 consider(Coord3{x, y, source_z}, Coord3{x, y, source_z + step});
1652 }
1653 }
1654 return found;
1655}
1656
1657// Answers a chunk-portal query, serving it from the selection's memo
1658// when the same seam has already been walked from the same tile. The six
1659// axis orders and the greedy walk re-walk shared seams, and a census of
1660// two portal workloads measured 66.7-67.1% of calls repeating a key
1661// already answered in the same selection.
1662//
1663// Three properties keep this equivalent to calling best_chunk_portal
1664// directly:
1665//
1666// - A hit adds nothing to scan_tiles. The counter measures tiles
1667// actually examined, and a served query examines none.
1668// - A hit that carries a failure leaves the caller's portal untouched,
1669// matching best_chunk_portal, which assigns only on success.
1670// - The key omits the goal, which is invariant across one selection.
1671// It also omits `from`, which is redundant because `from` is the
1672// chunk containing `current` at every call site. That redundancy is
1673// enforced rather than assumed: a caller passing an inconsistent
1674// pair bypasses the memo instead of colliding with an unrelated
1675// entry.
1676template <typename World, typename PassableTag>
1677[[nodiscard]] auto memoized_chunk_portal(const World& world, ChunkCoord3 from,
1678 ChunkCoord3 to, Coord3 current,
1679 Coord3 goal, Coord3& portal,
1680 std::size_t* scan_tiles) -> bool {
1681 using Shape = typename World::shape_type;
1682 auto& memo = active_portal_memo();
1683 const auto step = portal_step_code(from, to);
1684 const auto containing = chunk_coord<Shape>(current);
1685 const auto keyable = step != 0 && containing.x == from.x &&
1686 containing.y == from.y && containing.z == from.z;
1687 if (!keyable) {
1688 return best_chunk_portal<World, PassableTag>(world, from, to, current, goal,
1689 portal, scan_tiles);
1690 }
1691
1692 const auto current_index = tile_index<Shape>(current);
1693 const auto lookup = memo.probe(current_index, step);
1694 if (lookup.hit) {
1695 if (lookup.found) {
1696 portal = tile_coord<Shape>(lookup.portal_index);
1697 }
1698 return lookup.found;
1699 }
1700 const auto found = best_chunk_portal<World, PassableTag>(
1701 world, from, to, current, goal, portal, scan_tiles);
1702 memo.store(lookup, current_index, step,
1703 found ? tile_index<Shape>(portal) : std::uint64_t{0}, found);
1704 return found;
1705}
1706
1707template <typename World, typename PassableTag>
1708[[nodiscard]] auto build_chunk_portal_candidate(const World& world,
1709 PathRequest request,
1710 std::span<const Axis> order,
1711 std::vector<Coord3>& waypoints)
1712 -> PortalRouteCandidate {
1713 using Shape = typename World::shape_type;
1714
1715 waypoints.clear();
1716 auto current = request.start;
1717 auto current_chunk = chunk_coord<Shape>(request.start);
1718 const auto goal_chunk = chunk_coord<Shape>(request.goal);
1719 auto result = PortalRouteCandidate{true, 0, 0};
1720
1721 const auto append_portal = [&](ChunkCoord3 next_chunk) {
1722 auto portal = Coord3{};
1723 if (!memoized_chunk_portal<World, PassableTag>(
1724 world, current_chunk, next_chunk, current, request.goal, portal,
1725 &result.scan_tiles)) {
1726 result.found = false;
1727 return false;
1728 }
1729 result.score = saturating_add(result.score, manhattan(current, portal));
1730 waypoints.push_back(portal);
1731 current = portal;
1732 current_chunk = next_chunk;
1733 return true;
1734 };
1735
1736 for (const auto axis : order) {
1737 if (axis == Axis::X) {
1738 while (current_chunk.x != goal_chunk.x) {
1739 auto next = current_chunk;
1740 if (current_chunk.x < goal_chunk.x) {
1741 ++next.x;
1742 } else {
1743 --next.x;
1744 }
1745 if (!append_portal(next)) {
1746 return result;
1747 }
1748 }
1749 } else if (axis == Axis::Y) {
1750 while (current_chunk.y != goal_chunk.y) {
1751 auto next = current_chunk;
1752 if (current_chunk.y < goal_chunk.y) {
1753 ++next.y;
1754 } else {
1755 --next.y;
1756 }
1757 if (!append_portal(next)) {
1758 return result;
1759 }
1760 }
1761 } else {
1762 while (current_chunk.z != goal_chunk.z) {
1763 auto next = current_chunk;
1764 if (current_chunk.z < goal_chunk.z) {
1765 ++next.z;
1766 } else {
1767 --next.z;
1768 }
1769 if (!append_portal(next)) {
1770 return result;
1771 }
1772 }
1773 }
1774 }
1775
1776 result.score = saturating_add(result.score, manhattan(current, request.goal));
1777 return result;
1778}
1779
1780template <typename Fn>
1781void for_each_axis_neighbor(Coord3 coord, Fn&& fn) {
1782 fn(Coord3{coord.x + 1, coord.y, coord.z});
1783 fn(Coord3{coord.x - 1, coord.y, coord.z});
1784 fn(Coord3{coord.x, coord.y + 1, coord.z});
1785 fn(Coord3{coord.x, coord.y - 1, coord.z});
1786 fn(Coord3{coord.x, coord.y, coord.z + 1});
1787 fn(Coord3{coord.x, coord.y, coord.z - 1});
1788}
1789
1790template <typename Shape, typename Fn>
1791// Keep this forced inline. Provider-aware readers made some Clang versions
1792// outline the helper even though every call is in a per-node reconstruction
1793// loop. That codegen cliff made the gated field-product replay and
1794// nearest-target workloads about 2.4x slower; inlining restores the original
1795// loop shape. MSVC needs its spelling while Clang/GCC share the GNU attribute.
1796#if defined(_MSC_VER)
1797__forceinline void
1798#elif defined(__GNUC__) || defined(__clang__)
1799__attribute__((always_inline)) inline void
1800#else
1801inline void
1802#endif
1803for_each_indexed_axis_neighbor(Coord3 coord, std::uint64_t index, Fn&& fn) {
1804 using Traits = ShapeTraits<Shape>;
1805 constexpr auto size = Traits::size;
1806 constexpr auto chunk = Traits::chunk;
1807 constexpr auto local_bits = Traits::local_bits;
1808 constexpr auto chunk_index_stride =
1809 local_bits >= 64 ? std::uint64_t{0} : (std::uint64_t{1} << local_bits);
1810 constexpr auto chunk_y_stride = Traits::chunk_count_x * chunk_index_stride;
1811
1812 const auto local_x = static_cast<std::uint64_t>(coord.x) & (chunk.x - 1);
1813 const auto local_y = static_cast<std::uint64_t>(coord.y) & (chunk.y - 1);
1814
1815 if constexpr (!Traits::degenerate_x) {
1816 if (static_cast<std::uint64_t>(coord.x) + 1 < size.x) {
1817 const auto next_index = local_x + 1 < chunk.x
1818 ? index + 1
1819 : index + chunk_index_stride - local_x;
1820 fn(Coord3{coord.x + 1, coord.y, coord.z}, next_index);
1821 }
1822 if (coord.x > 0) {
1823 const auto next_index =
1824 local_x > 0 ? index - 1 : index - chunk_index_stride + (chunk.x - 1);
1825 fn(Coord3{coord.x - 1, coord.y, coord.z}, next_index);
1826 }
1827 }
1828
1829 if constexpr (!Traits::degenerate_y) {
1830 if (static_cast<std::uint64_t>(coord.y) + 1 < size.y) {
1831 const auto next_index = local_y + 1 < chunk.y
1832 ? index + chunk.x
1833 : index + chunk_y_stride - local_y * chunk.x;
1834 fn(Coord3{coord.x, coord.y + 1, coord.z}, next_index);
1835 }
1836 if (coord.y > 0) {
1837 const auto next_index =
1838 local_y > 0 ? index - chunk.x
1839 : index - chunk_y_stride + (chunk.y - 1) * chunk.x;
1840 fn(Coord3{coord.x, coord.y - 1, coord.z}, next_index);
1841 }
1842 }
1843
1844 if constexpr (!Traits::degenerate_z) {
1845 constexpr auto chunk_z_stride =
1846 Traits::chunk_count_x * Traits::chunk_count_y * chunk_index_stride;
1847 const auto local_xy = chunk.x * chunk.y;
1848 const auto local_z = static_cast<std::uint64_t>(coord.z) & (chunk.z - 1);
1849 if (static_cast<std::uint64_t>(coord.z) + 1 < size.z) {
1850 const auto next_index = local_z + 1 < chunk.z
1851 ? index + local_xy
1852 : index + chunk_z_stride - local_z * local_xy;
1853 fn(Coord3{coord.x, coord.y, coord.z + 1}, next_index);
1854 }
1855 if (coord.z > 0) {
1856 const auto next_index =
1857 local_z > 0 ? index - local_xy
1858 : index - chunk_z_stride + (chunk.z - 1) * local_xy;
1859 fn(Coord3{coord.x, coord.y, coord.z - 1}, next_index);
1860 }
1861 }
1862}
1863
1864[[nodiscard]] constexpr bool open_node_less(
1865 PathScratch::OpenNode lhs, PathScratch::OpenNode rhs) noexcept {
1866 if (lhs.f != rhs.f) {
1867 return lhs.f > rhs.f;
1868 }
1869 if (lhs.g != rhs.g) {
1870 return lhs.g < rhs.g;
1871 }
1872 return lhs.index > rhs.index;
1873}
1874
1875} // namespace detail
1876
1877#include <tess/path/detail/astar.h>
1878
1882template <typename World, typename Class>
1883[[nodiscard]] auto build_weighted_route_product(const World& world,
1884 PathRequest request,
1885 PathScratch& scratch,
1886 WeightedRouteProduct& product)
1887 -> PathResult {
1888 using Shape = typename World::shape_type;
1889 // Route products track content-version dependencies for cached replay
1890 // (weighted_route_product_path -> is_valid), which reads meta() for chunks a
1891 // sparse world may have since evicted. Route products therefore require a
1892 // dense world; weighted A* itself runs natively on sparse worlds.
1893 static_assert(
1894 std::is_same_v<typename World::residency_type, AlwaysResident>,
1895 "build_weighted_route_product is dense-only; call weighted_astar_path "
1896 "directly for sparse worlds.");
1897
1898 product.clear();
1899 const auto result =
1900 weighted_astar_path<World, Class>(world, request, scratch);
1901 product.request_ = request;
1902 product.status_ = result.status;
1903 product.cost_ = result.cost;
1904 product.expanded_nodes_ = result.expanded_nodes;
1905 product.reached_nodes_ = result.reached_nodes;
1906 product.path_.assign(result.path.begin(), result.path.end());
1907 if (result.status == PathStatus::Found) {
1908 for (const auto coord : product.path_) {
1909 const auto key = tile_key<Shape>(coord);
1910 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
1911 }
1912 } else {
1913 // See detail::capture_failure_dependencies: a replayed failure must be
1914 // invalidated by any edit that could change the answer.
1915 detail::capture_failure_dependencies<Shape>(world, request, result.status,
1916 product.dependencies_);
1917 }
1918
1919 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
1920 product.reached_nodes_, product.path_};
1921}
1922
1924template <typename World>
1926 const World& world, const WeightedRouteProduct& product) -> PathResult {
1927 if (!product.is_valid(world)) {
1928 return PathResult{PathStatus::NotComputed, 0, 0, 0, {}};
1929 }
1930 return PathResult{product.status_, product.cost_, 0, 0, product.path_};
1931}
1932
1937template <typename World, typename Class>
1939 const World& world, PathRequest request, std::span<const Coord3> waypoints,
1940 PathScratch& scratch, WeightedPortalRouteProduct& product) -> PathResult {
1941 using Shape = typename World::shape_type;
1942 // Same content-version dependency tracking as build_weighted_route_product,
1943 // so the product requires a dense world. The per-segment weighted A* it
1944 // chains already supports sparse worlds.
1945 static_assert(std::is_same_v<typename World::residency_type, AlwaysResident>,
1946 "build_weighted_portal_route_product is dense-only; chain "
1947 "weighted_astar_path directly for sparse worlds.");
1948
1949 std::vector<Coord3> stash;
1950 const auto source = product.stash_if_owned(waypoints, stash);
1951
1952 product.clear();
1953 product.request_ = request;
1954 product.waypoints_.assign(source.begin(), source.end());
1955
1956 auto from = request.start;
1957 auto total_cost = std::uint64_t{0};
1958 auto total_expanded = std::size_t{0};
1959 auto total_reached = std::size_t{0};
1960 auto append_segment = [&](PathRequest segment_request) {
1961 const auto result =
1962 weighted_astar_path<World, Class>(world, segment_request, scratch);
1963 total_expanded += result.expanded_nodes;
1964 total_reached += result.reached_nodes;
1965 if (result.status != PathStatus::Found) {
1966 product.path_.clear();
1967 product.status_ = result.status;
1968 product.expanded_nodes_ = total_expanded;
1969 product.reached_nodes_ = total_reached;
1970 // Same failure-dependency contract as build_weighted_route_product;
1971 // the failing segment's endpoints are the offending tiles.
1972 detail::capture_failure_dependencies<Shape>(
1973 world, segment_request, result.status, product.dependencies_);
1974 return false;
1975 }
1976 total_cost += result.cost;
1977 if (total_cost >= std::numeric_limits<std::uint32_t>::max()) {
1978 product.path_.clear();
1979 product.status_ = PathStatus::CostOverflow;
1980 product.expanded_nodes_ = total_expanded;
1981 product.reached_nodes_ = total_reached;
1982 detail::capture_failure_dependencies<Shape>(
1983 world, request, product.status_, product.dependencies_);
1984 return false;
1985 }
1986 product.segment_.assign(result.path.begin(), result.path.end());
1987 for (std::size_t i = product.path_.empty() ? 0u : 1u;
1988 i < product.segment_.size(); ++i) {
1989 product.path_.push_back(product.segment_[i]);
1990 }
1991 return true;
1992 };
1993
1994 for (const auto waypoint : source) {
1995 if (!append_segment(PathRequest{from, waypoint})) {
1996 return PathResult{product.status_, 0, total_expanded, total_reached,
1997 product.path_};
1998 }
1999 from = waypoint;
2000 }
2001 if (!append_segment(PathRequest{from, request.goal})) {
2002 return PathResult{product.status_, 0, total_expanded, total_reached,
2003 product.path_};
2004 }
2005
2006 product.status_ = PathStatus::Found;
2007 product.cost_ = static_cast<std::uint32_t>(total_cost);
2008 product.expanded_nodes_ = total_expanded;
2009 product.reached_nodes_ = total_reached;
2010 for (const auto coord : product.path_) {
2011 const auto key = tile_key<Shape>(coord);
2012 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
2013 }
2014 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
2015 product.reached_nodes_, product.path_};
2016}
2017
2019template <typename World>
2021 const World& world, const WeightedPortalRouteProduct& product)
2022 -> PathResult {
2023 if (!product.is_valid(world)) {
2024 return PathResult{PathStatus::NotComputed, 0, 0, 0, {}};
2025 }
2026 return PathResult{product.status_, product.cost_, 0, 0, product.path_};
2027}
2028
2030template <typename WorldType, typename Tag>
2031[[nodiscard]] auto build_distance_field(
2032 const WorldType& world, Coord3 goal, DistanceFieldScratch& scratch,
2033 [[maybe_unused]] MissingChunkPolicy policy) -> DistanceFieldResult {
2034 using Shape = typename WorldType::shape_type;
2035 using Space = detail::NodeIndexSpace<WorldType>;
2036 using Class = movement::movement_class_of<Tag>;
2037 using UnitClass = movement::detail::UnitMovementClass<Class>;
2039 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
2040
2041 if constexpr (Model::cost_scale != 1) {
2043 scratch, policy);
2044 }
2045
2046 TESS_DIAG_EVENT_VALUE(path_clear, scratch.touched_.size());
2047 scratch.clear_build();
2048 if (!contains<Shape>(goal)) {
2049 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
2050 }
2051 if constexpr (!Space::is_dense) {
2052 // A non-resident goal cannot seed the flood: indexing its node-array slot
2053 // would be out of bounds. Under Indeterminate the field is simply unknown.
2054 const Space residency{world};
2055 if (!residency.is_resident_index(detail::tile_index<Shape>(goal))) {
2056 return DistanceFieldResult{
2057 policy == MissingChunkPolicy::ReportIndeterminate
2058 ? PathStatus::Indeterminate
2059 : PathStatus::InvalidGoal,
2060 0, 0};
2061 }
2062 }
2063 TESS_DIAG_EVENT(path_goal_passability_check);
2064 if (!detail::is_passable<WorldType, Tag>(world, goal)) {
2065 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
2066 }
2067
2068 const Space space{world};
2069 const auto node_count = space.capacity_hint();
2070 if (scratch.distance_.size() != node_count) {
2071 TESS_DIAG_EVENT(path_initialize);
2072 scratch.generation_.assign(node_count, 0);
2073 scratch.distance_.assign(node_count, infinite_distance);
2074 }
2075
2076 const auto goal_index = detail::tile_index<Shape>(goal);
2077 const auto goal_offset = space.offset(goal_index);
2078 scratch.goal_ = goal;
2079 scratch.has_goal_ = true;
2080 scratch.template stamp_model<Model>();
2081 scratch.stamp_residency(world);
2082 scratch.distance_[goal_offset] = 0;
2083 scratch.touch_node(goal_offset, goal_index);
2084 TESS_DIAG_EVENT(path_touch_node);
2085 scratch.frontier_.push_back(goal_index);
2086 TESS_DIAG_EVENT(path_heap_push);
2087
2088 std::size_t expanded_nodes = 0;
2089 std::size_t head = 0;
2090 // Sparse: set when the flood skips a non-resident neighbor, so a field
2091 // truncated by a missing chunk can report Indeterminate under policy.
2092 [[maybe_unused]] bool crossed_missing = false;
2093 const auto model = Model{};
2094 while (head < scratch.frontier_.size()) {
2095 const auto current = scratch.frontier_[head];
2096 ++head;
2097 TESS_DIAG_EVENT(path_heap_pop);
2098 ++expanded_nodes;
2099
2100 const auto current_offset = space.offset(current);
2101 const auto current_distance =
2102 scratch.distance_at(current_offset, infinite_distance);
2103 const auto current_coord = detail::tile_coord<Shape>(current);
2104 const auto visit_neighbor = [&](std::uint64_t neighbor_index) {
2105 if constexpr (!Space::is_dense) {
2106 // A non-resident neighbor has no node-array slot; remember the
2107 // boundary and skip it before computing an out-of-bounds offset.
2108 if (!space.is_resident_index(neighbor_index)) {
2109 crossed_missing = true;
2110 return;
2111 }
2112 }
2113 const auto neighbor_offset = space.offset(neighbor_index);
2114 if (scratch.is_current(neighbor_offset)) {
2115 TESS_DIAG_EVENT(path_neighbor_closed);
2116 return;
2117 }
2118 scratch.distance_[neighbor_offset] = current_distance + 1;
2119 scratch.touch_node(neighbor_offset, neighbor_index);
2120 TESS_DIAG_EVENT(path_touch_node);
2121 scratch.frontier_.push_back(neighbor_index);
2122 TESS_DIAG_EVENT(path_heap_push);
2123 };
2124 if constexpr (Model::preserves_default_connectivity &&
2125 std::is_same_v<typename Model::step_policy,
2127 detail::for_each_indexed_axis_neighbor<Shape>(
2128 current_coord, current, [&](Coord3, std::uint64_t neighbor_index) {
2129 TESS_DIAG_EVENT(path_neighbor_candidate);
2130 if constexpr (!Space::is_dense) {
2131 if (!space.is_resident_index(neighbor_index)) {
2132 crossed_missing = true;
2133 return;
2134 }
2135 }
2136 TESS_DIAG_EVENT(path_passability_check);
2137 if (!detail::is_passable_index<WorldType, Tag>(world,
2138 neighbor_index)) {
2139 TESS_DIAG_EVENT(path_neighbor_blocked);
2140 return;
2141 }
2142 visit_neighbor(neighbor_index);
2143 });
2144 } else {
2145 model.for_each_reverse(world, current_coord, current, [&](auto probe) {
2146 TESS_DIAG_EVENT(path_neighbor_candidate);
2147 if (probe.availability == TransitionAvailability::MissingTopology) {
2148 crossed_missing = true;
2149 return;
2150 }
2151 visit_neighbor(probe.to_index);
2152 });
2153 }
2154 }
2155
2156 if constexpr (!Space::is_dense) {
2157 if (crossed_missing && policy == MissingChunkPolicy::ReportIndeterminate) {
2158 scratch.publish_build_status(PathStatus::Indeterminate);
2159 return DistanceFieldResult{PathStatus::Indeterminate, expanded_nodes,
2160 scratch.touched_.size()};
2161 }
2162 }
2163 scratch.publish_build_status(PathStatus::Found);
2164 return DistanceFieldResult{PathStatus::Found, expanded_nodes,
2165 scratch.touched_.size()};
2166}
2167
2172template <typename World, typename Tag>
2173[[nodiscard]] auto distance_field_path(const World& world, PathRequest request,
2174 DistanceFieldScratch& scratch)
2175 -> PathResult {
2176 using Shape = typename World::shape_type;
2177 using Space = detail::NodeIndexSpace<World>;
2178 using Class = movement::movement_class_of<Tag>;
2179 using UnitClass = movement::detail::UnitMovementClass<Class>;
2181 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
2182
2183 if constexpr (Model::cost_scale != 1) {
2184 return detail::weighted_distance_field_path_core<World, UnitClass>(
2185 world, request, scratch, /*verify_residency=*/true);
2186 }
2187
2188 scratch.clear_path();
2189 if (!contains<Shape>(request.start)) {
2190 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
2191 }
2192 if (!contains<Shape>(request.goal)) {
2193 return PathResult{PathStatus::InvalidGoal, 0, 0, 0, scratch.path_};
2194 }
2195 if (!scratch.has_goal_ || scratch.goal_ != request.goal ||
2196 !scratch.template model_matches<Model>() ||
2197 !scratch.residency_matches(world)) {
2198 return PathResult{PathStatus::NotComputed, 0, 0, 0, scratch.path_};
2199 }
2200 if constexpr (!Space::is_dense) {
2201 const Space residency{world};
2202 if (!residency.is_resident_index(
2203 detail::tile_index<Shape>(request.start))) {
2204 const auto status = scratch.build_status_ == PathStatus::Indeterminate
2205 ? PathStatus::Indeterminate
2206 : PathStatus::InvalidStart;
2207 return PathResult{status, 0, 0, 0, scratch.path_};
2208 }
2209 }
2210 TESS_DIAG_EVENT(path_start_passability_check);
2211 if (!detail::is_passable<World, Tag>(world, request.start)) {
2212 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
2213 }
2214
2215 const Space space{world};
2216 const auto start_index = detail::tile_index<Shape>(request.start);
2217 auto current = start_index;
2218 auto current_offset = space.offset(current);
2219 auto current_distance =
2220 scratch.distance_at(current_offset, infinite_distance);
2221 if (current_distance == infinite_distance) {
2222 return PathResult{scratch.unresolved_path_status(), 0, 0,
2223 scratch.touched_.size(), scratch.path_};
2224 }
2225
2226 scratch.path_.push_back(request.start);
2227 TESS_DIAG_EVENT(path_reconstruct_node);
2228 const auto model = Model{};
2229 while (current_distance > 0) {
2230 const auto current_coord = detail::tile_coord<Shape>(current);
2231 auto next = current;
2232 auto next_distance = current_distance;
2233 const auto consider_neighbor = [&](std::uint64_t neighbor_index) {
2234 if constexpr (!Space::is_dense) {
2235 // A non-resident neighbor was never touched by the flood, so its
2236 // distance is infinite and it cannot be the descent step; skip it
2237 // before computing an out-of-bounds offset.
2238 if (!space.is_resident_index(neighbor_index)) {
2239 return;
2240 }
2241 }
2242 if (!detail::is_passable_index<World, Tag>(world, neighbor_index)) {
2243 return;
2244 }
2245 const auto neighbor_offset = space.offset(neighbor_index);
2246 const auto neighbor_distance =
2247 scratch.distance_at(neighbor_offset, infinite_distance);
2248 if (neighbor_distance < next_distance) {
2249 next = neighbor_index;
2250 next_distance = neighbor_distance;
2251 }
2252 };
2253 if constexpr (Model::preserves_default_connectivity &&
2254 std::is_same_v<typename Model::step_policy,
2256 detail::for_each_indexed_axis_neighbor<Shape>(
2257 current_coord, current, [&](Coord3, std::uint64_t neighbor_index) {
2258 consider_neighbor(neighbor_index);
2259 });
2260 } else {
2261 model.for_each_forward(world, current_coord, current, [&](auto probe) {
2262 if (probe.availability == TransitionAvailability::Legal) {
2263 consider_neighbor(probe.to_index);
2264 }
2265 });
2266 }
2267
2268 if (next == current || next_distance + 1 != current_distance) {
2269 scratch.path_.clear();
2270 return PathResult{PathStatus::NotComputed, 0, 0, scratch.touched_.size(),
2271 scratch.path_};
2272 }
2273
2274 current = next;
2275 current_offset = space.offset(current);
2276 current_distance = scratch.distance_at(current_offset, infinite_distance);
2277 scratch.path_.push_back(detail::tile_coord<Shape>(current));
2278 TESS_DIAG_EVENT(path_reconstruct_node);
2279 }
2280
2281 return PathResult{
2282 PathStatus::Found, scratch.distance_[space.offset(start_index)],
2283 scratch.path_.size(), scratch.touched_.size(), scratch.path_};
2284}
2285
2287template <typename WorldType, typename Tag, typename Provider>
2288[[nodiscard]] auto build_distance_field(const WorldType& world, Coord3 goal,
2289 DistanceFieldScratch& scratch,
2290 MissingChunkPolicy policy,
2291 const Provider& provider)
2293 using Class = movement::movement_class_of<Tag>;
2294 using UnitClass = movement::detail::UnitMovementClass<Class>;
2295 return build_weighted_distance_field<WorldType, UnitClass, Provider>(
2296 world, goal, scratch, policy, provider);
2297}
2298
2300template <typename World, typename Tag, typename Provider>
2301[[nodiscard]] auto distance_field_path(const World& world, PathRequest request,
2302 DistanceFieldScratch& scratch,
2303 const Provider& provider) -> PathResult {
2304 using Class = movement::movement_class_of<Tag>;
2305 using UnitClass = movement::detail::UnitMovementClass<Class>;
2306 return detail::weighted_distance_field_path_core<World, UnitClass, Provider>(
2307 world, request, scratch, /*verify_residency=*/true, provider);
2308}
2309
2311template <typename WorldType, typename Class, typename Provider>
2313 const WorldType& world, Coord3 goal, DistanceFieldScratch& scratch,
2314 [[maybe_unused]] MissingChunkPolicy policy, const Provider& provider)
2316 static_assert(std::derived_from<Class, movement::movement_class_tag>,
2317 "build_weighted_distance_field<World, Class> requires a "
2318 "MovementClass; pass a movement class such as "
2319 "PositiveCostFieldMovement.");
2320 using Shape = typename WorldType::shape_type;
2321 using Space = detail::NodeIndexSpace<WorldType>;
2323 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
2324
2325 TESS_DIAG_EVENT_VALUE(path_clear, scratch.touched_.size());
2326 scratch.clear_build();
2327 if (!contains<Shape>(goal)) {
2328 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
2329 }
2330 if constexpr (!Space::is_dense) {
2331 // A non-resident goal cannot seed the flood; under Indeterminate the field
2332 // is simply unknown. Resolve it before is_passable/entry-cost read the
2333 // goal chunk.
2334 const Space residency{world};
2335 if (!residency.is_resident_index(detail::tile_index<Shape>(goal))) {
2336 return DistanceFieldResult{
2337 policy == MissingChunkPolicy::ReportIndeterminate
2338 ? PathStatus::Indeterminate
2339 : PathStatus::InvalidGoal,
2340 0, 0};
2341 }
2342 }
2343 TESS_DIAG_EVENT(path_goal_passability_check);
2344 if (!detail::is_passable<WorldType, Class>(world, goal)) {
2345 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
2346 }
2347
2348 const auto goal_index = detail::tile_index<Shape>(goal);
2349 if (detail::tile_entry_cost_index<WorldType, Class>(world, goal_index) == 0) {
2350 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
2351 }
2352
2353 const Space space{world};
2354 const auto node_count = space.capacity_hint();
2355 if (scratch.distance_.size() != node_count) {
2356 TESS_DIAG_EVENT(path_initialize);
2357 scratch.generation_.assign(node_count, 0);
2358 scratch.distance_.assign(node_count, infinite_distance);
2359 }
2360
2361 const auto goal_offset = space.offset(goal_index);
2362 const auto model = Model{provider};
2363 scratch.goal_ = goal;
2364 scratch.has_goal_ = true;
2365 scratch.template stamp_model<Model>(
2366 model, detail::transition_provider_instance_identity(provider));
2367 scratch.stamp_residency(world);
2368 scratch.distance_[goal_offset] = 0;
2369 scratch.touch_node(goal_offset, goal_index);
2370 TESS_DIAG_EVENT(path_touch_node);
2371 scratch.weighted_frontier_.push_back(
2372 detail::PackedOpenNode::make(goal_index, 0, 0));
2373 std::push_heap(scratch.weighted_frontier_.begin(),
2374 scratch.weighted_frontier_.end(),
2375 detail::packed_open_node_less);
2376 TESS_DIAG_EVENT(path_heap_push);
2377
2378 std::size_t expanded_nodes = 0;
2379 [[maybe_unused]] bool crossed_missing = false;
2380 auto cost_overflow = false;
2381 while (!scratch.weighted_frontier_.empty()) {
2382 TESS_DIAG_EVENT(path_heap_pop);
2383 std::pop_heap(scratch.weighted_frontier_.begin(),
2384 scratch.weighted_frontier_.end(),
2385 detail::packed_open_node_less);
2386 const auto current = scratch.weighted_frontier_.back();
2387 scratch.weighted_frontier_.pop_back();
2388
2389 const auto current_offset = space.offset(current.index);
2390 const auto current_distance =
2391 scratch.distance_at(current_offset, infinite_distance);
2392 if (current.g() != current_distance) {
2393 TESS_DIAG_EVENT_VALUE(path_skip_pop, false);
2394 continue;
2395 }
2396 ++expanded_nodes;
2397
2398 const auto current_coord = detail::tile_coord<Shape>(current.index);
2399 model.for_each_reverse(
2400 world, current_coord, current.index, [&](auto probe) {
2401 TESS_DIAG_EVENT(path_neighbor_candidate);
2402 if (probe.availability == TransitionAvailability::MissingTopology) {
2403 crossed_missing = true;
2404 return;
2405 }
2406 if (probe.cost_overflow) {
2407 cost_overflow = true;
2408 return;
2409 }
2410 const auto neighbor_index = probe.to_index;
2411 if constexpr (!Space::is_dense) {
2412 if (!space.is_resident_index(neighbor_index)) {
2413 crossed_missing = true;
2414 return;
2415 }
2416 }
2417 const auto neighbor_offset = space.offset(neighbor_index);
2418 TESS_DIAG_EVENT(path_relax_attempt);
2419 if (!scratch.is_current(neighbor_offset)) {
2420 scratch.distance_[neighbor_offset] = infinite_distance;
2421 scratch.touch_node(neighbor_offset, neighbor_index);
2422 TESS_DIAG_EVENT(path_touch_node);
2423 }
2424
2425 const auto next_distance =
2426 detail::saturating_add(current_distance, probe.cost);
2427 if (next_distance == infinite_distance) {
2428 cost_overflow = true;
2429 return;
2430 }
2431 if (next_distance <
2432 scratch.distance_at(neighbor_offset, infinite_distance)) {
2433 TESS_DIAG_EVENT(path_relax_success);
2434 scratch.distance_[neighbor_offset] = next_distance;
2435 scratch.weighted_frontier_.push_back(detail::PackedOpenNode::make(
2436 neighbor_index, next_distance, next_distance));
2437 std::push_heap(scratch.weighted_frontier_.begin(),
2438 scratch.weighted_frontier_.end(),
2439 detail::packed_open_node_less);
2440 TESS_DIAG_EVENT(path_heap_push);
2441 }
2442 });
2443 }
2444
2445 if constexpr (!Space::is_dense) {
2446 if (crossed_missing && policy == MissingChunkPolicy::ReportIndeterminate) {
2447 scratch.publish_build_status(PathStatus::Indeterminate);
2448 return DistanceFieldResult{PathStatus::Indeterminate, expanded_nodes,
2449 scratch.touched_.size()};
2450 }
2451 }
2452 if (cost_overflow) {
2453 scratch.discard_build_result();
2454 return DistanceFieldResult{PathStatus::CostOverflow, expanded_nodes,
2455 scratch.touched_.size()};
2456 }
2457 scratch.publish_build_status(PathStatus::Found);
2458 return DistanceFieldResult{PathStatus::Found, expanded_nodes,
2459 scratch.touched_.size()};
2460}
2461
2462template <typename WorldType, typename Class>
2463[[nodiscard]] auto build_weighted_distance_field(const WorldType& world,
2464 Coord3 goal,
2465 DistanceFieldScratch& scratch,
2466 MissingChunkPolicy policy)
2469 world, goal, scratch, policy, AdjacentTransitions{});
2470}
2471
2472#include <tess/path/detail/weighted_batch.h>
2473
2474} // namespace tess
2475
2476#include <tess/path/route_cache.h>
Definition field_product_cache.h:60
Definition path.h:872
friend auto build_distance_field(const WorldType &world, Coord3 goal, DistanceFieldScratch &scratch, MissingChunkPolicy policy) -> DistanceFieldResult
Builds an unweighted goal-rooted field into caller-owned scratch.
Definition path.h:2031
friend auto build_weighted_distance_field(const WorldType &world, Coord3 goal, DistanceFieldScratch &scratch, MissingChunkPolicy policy) -> DistanceFieldResult
Definition path.h:2463
friend auto weighted_path_batch(const World &world, std::span< const PathRequest > requests, WeightedPathBatchScratch &scratch, MissingChunkPolicy policy, const Provider &provider) -> std::span< const PathResult >
Solves a provider-aware bounded weighted batch.
friend auto weighted_path_batch(const World &world, std::span< const PathRequest > requests, WeightedPathBatchScratch &scratch, MissingChunkPolicy policy) -> std::span< const PathResult >
Solves a bounded weighted batch without special transitions.
friend auto distance_field_product_path(const World &world, Coord3 start, const DistanceFieldProduct &product, DistanceFieldScratch &scratch) -> PathResult
Reconstructs a borrowed path from a valid multi-goal product.
Definition field_product_cache.h:1281
friend auto build_weighted_distance_field_product(const World &world, const GoalSet &goals, DistanceFieldProduct &product, DistanceFieldScratch &scratch, const Provider &provider) -> DistanceFieldResult
Builds a dense multi-goal weighted field into a reusable product.
Definition field_product_cache.h:1016
friend auto weighted_distance_field_product_path(const World &world, Coord3 start, const DistanceFieldProduct &product, DistanceFieldScratch &scratch, const Provider &provider) -> PathResult
Reconstructs an exact weighted path through a valid reusable product.
Definition field_product_cache.h:1290
friend auto nearest_target(const World &world, Coord3 start, const DistanceFieldProduct &product, DistanceFieldScratch &scratch) -> NearestTargetResult
Finds the nearest reachable goal represented by a valid product.
Definition field_product_cache.h:1413
friend auto build_weighted_distance_field_in_box(const World &world, Coord3 goal, Box3 domain, DistanceFieldScratch &scratch, MissingChunkPolicy policy, const Provider &provider) -> DistanceFieldResult
Builds a boxed weighted field composed with a special provider.
Definition distance_field_box.h:18
friend auto build_distance_field_product(const World &world, const GoalSet &goals, DistanceFieldProduct &product, DistanceFieldScratch &scratch) -> DistanceFieldResult
Builds a dense multi-goal field into caller-owned reusable storage.
Definition field_product_cache.h:1005
friend auto distance_field_path(const World &world, PathRequest request, DistanceFieldScratch &scratch) -> PathResult
Definition path.h:2173
Owns an ordered set of goals used to build a reusable distance product.
Definition field_product_cache.h:19
Definition path.h:741
friend auto weighted_astar_path(const World &world, PathRequest request, PathScratch &scratch, MissingChunkPolicy policy) -> PathResult
friend auto weighted_astar_path(const World &world, PathRequest request, PathScratch &scratch, MissingChunkPolicy policy, const Provider &provider, PathTieBreak tie_break) -> PathResult
Finds a provider-aware weighted path with seeded equal-cost tie-breaking.
friend auto astar_path(const World &world, PathRequest request, PathScratch &scratch, MissingChunkPolicy policy, const Provider &provider) -> PathResult
Finds a minimum-step path composed with a special-transition provider.
friend auto astar_path(const World &world, PathRequest request, PathScratch &scratch, MissingChunkPolicy policy) -> PathResult
friend auto cached_astar_path(const World &world, PathRequest request, PathScratch &scratch, UnitRouteCache &cache, MissingChunkPolicy policy) -> PathResult
Finds a cached empty-provider route or computes and stores one.
Definition route_cache.h:921
friend auto weighted_astar_path(const World &world, PathRequest request, PathScratch &scratch, PathTieBreak tie_break, MissingChunkPolicy policy) -> PathResult
Finds an optimal weighted path with seeded equal-cost tie-breaking.
friend auto weighted_astar_path(const World &world, PathRequest request, PathScratch &scratch, MissingChunkPolicy policy, const Provider &provider) -> PathResult
Finds a weighted path composed with a special-transition provider.
Definition path_view.h:21
Definition transition_model.h:380
Definition route_cache.h:94
Definition path.h:1200
friend auto weighted_path_batch(const World &world, std::span< const PathRequest > requests, WeightedPathBatchScratch &scratch, MissingChunkPolicy policy, const Provider &provider) -> std::span< const PathResult >
Solves a provider-aware bounded weighted batch.
friend auto weighted_path_batch(const World &world, std::span< const PathRequest > requests, WeightedPathBatchScratch &scratch, MissingChunkPolicy policy) -> std::span< const PathResult >
Solves a bounded weighted batch without special transitions.
friend auto build_weighted_portal_route_product(const World &world, PathRequest request, std::span< const Coord3 > waypoints, PathScratch &scratch, WeightedPortalRouteProduct &product) -> PathResult
Definition path.h:1938
friend auto build_weighted_chunk_portal_route_product_cached(const World &world, PathRequest request, PathScratch &scratch, WeightedPortalSegmentCache &cache, WeightedPortalRouteProduct &product) -> PathResult
Builds a chunk-portal weighted route through the segment cache.
Definition portal_route.h:299
friend auto weighted_portal_route_product_path(const World &world, const WeightedPortalRouteProduct &product) -> PathResult
Replays a portal-route product when its dependencies remain current.
Definition path.h:2020
friend auto build_weighted_chunk_portal_route_product(const World &world, PathRequest request, PathScratch &scratch, WeightedPortalRouteProduct &product) -> PathResult
Definition portal_route.h:193
Definition portal_segment_cache.h:61
Definition path.h:409
friend auto weighted_route_product_path(const World &world, const WeightedRouteProduct &product) -> PathResult
Replays a route product when all captured content versions still match.
Definition path.h:1925
friend auto build_weighted_route_product(const World &world, PathRequest request, PathScratch &scratch, WeightedRouteProduct &product) -> PathResult
Definition path.h:1883
Definition world.h:22
Supplies no special transitions beyond ordinary face adjacency.
Definition transition_provider.h:132
Definition shape.h:94
Definition shape.h:86
Definition metadata_types.h:86
Definition shape.h:46
Reports distance-field construction status and search work.
Definition path.h:70
Reports the closest reachable goal and a scratch-owned path to it.
Definition field_product_cache.h:42
Specifies inclusive start and goal coordinates for a path query.
Definition request.h:10
Definition path.h:60
Definition path.h:743
Definition request.h:34
Definition shape.h:296
Definition path.h:78
Definition step_policy.h:26