tess 0.4.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/diagnostics/diagnostics.h>
7#include <tess/path/node_index_space.h>
8#include <tess/path/path_view.h>
9#include <tess/topology/movement_class.h>
10
11#include <algorithm>
12#include <array>
13#include <concepts>
14#include <cstddef>
15#include <cstdint>
16#include <functional>
17#include <limits>
18#include <span>
19#include <type_traits>
20#include <utility>
21#include <vector>
22
23namespace tess {
24
26enum class PathStatus : std::uint8_t {
27 Found,
28 InvalidStart,
29 InvalidGoal,
30 NoPath,
31 // Sparse worlds only: the search reached the edge of the resident set and
32 // could not rule out a path through a non-resident chunk. Distinguished from
33 // NoPath so a caller never mistakes "not searched" for "no route exists" and
34 // can materialize the missing chunks and retry.
35 Indeterminate,
36};
37static_assert(sizeof(PathStatus) == sizeof(std::uint8_t));
38
39// How a search treats a step into a non-resident chunk of a sparse world.
40// Inert for dense worlds, where every chunk is resident.
42enum class MissingChunkPolicy : std::uint8_t {
43 // Treat a non-resident chunk as impassable. The search stays within the
44 // resident set and may report NoPath even when a route exists through
45 // chunks that are not currently materialized.
46 TreatAsBlocked,
47 // Never report a wrong NoPath across a non-resident boundary: if the search
48 // exhausts the resident set having skipped at least one non-resident
49 // neighbor, it returns Indeterminate instead of NoPath.
50 Indeterminate,
51};
52
55 Coord3 start;
56 Coord3 goal;
57};
58
63struct PathResult {
64 PathStatus status = PathStatus::NoPath;
65 std::uint32_t cost = 0;
66 std::size_t expanded_nodes = 0;
67 std::size_t reached_nodes = 0;
68 PathView path;
69};
70
73 PathStatus status = PathStatus::NoPath;
74 std::size_t expanded_nodes = 0;
75 std::size_t reached_nodes = 0;
76};
77
81 std::size_t requests = 0;
82 std::size_t unique_goals = 0;
83 std::size_t field_builds = 0;
84 std::size_t astar_fallbacks = 0;
85 std::size_t path_nodes = 0;
86};
87
89class PathScratch;
93class GoalSet;
102
103namespace detail {
104// Core behind weighted_distance_field_path; verify_residency lets
105// weighted_path_batch skip the O(resident_count) fingerprint recompute for
106// fields it reads against the same const world it just built them from.
107template <typename World, typename Class>
108auto weighted_distance_field_path_core(const World& world, Coord3 start,
109 Coord3 goal,
110 DistanceFieldScratch& scratch,
111 bool verify_residency) -> PathResult;
112
113// Core behind build_bounded_weighted_distance_field. settle_targets are
114// validated tile indices whose distances the caller will read; once every
115// target is settled the flood stops instead of exhausting the reachable
116// component (audit 2026-07-11 M3). Empty span = flood to exhaustion (the
117// public wrapper's behavior, byte-identical to the pre-M3 build).
118template <typename World, typename Class, std::uint32_t MaxCost>
119auto build_bounded_weighted_distance_field_core(
120 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
121 MissingChunkPolicy policy, std::span<const std::uint64_t> settle_targets)
123} // namespace detail
124
125// Declared here so the MissingChunkPolicy default lives on the first
126// declaration; the friend declarations and definitions below omit it.
131template <typename World, typename Tag>
132auto astar_path(const World& world, PathRequest request, PathScratch& scratch,
133 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
134 -> PathResult;
135
136// The weighted searches come in two forms: the core takes one MovementClass
137// fusing passability and entry cost; the legacy <PassableTag, CostTag> pair
138// forwards through movement::LegacyWeighted (identical semantics, including
139// the cost-agnostic passability asymmetry).
143template <typename World, typename Class>
144auto weighted_astar_path(
145 const World& world, PathRequest request, PathScratch& scratch,
146 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
147 -> PathResult;
148
149template <typename World, typename PassableTag, typename CostTag>
151auto weighted_astar_path(
152 const World& world, PathRequest request, PathScratch& scratch,
153 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
154 -> PathResult;
155
157template <typename World, typename Tag>
158auto build_distance_field_product(const World& world, const GoalSet& goals,
159 DistanceFieldScratch& scratch,
160 DistanceFieldProduct& product)
161 -> DistanceFieldResult;
162
164template <typename World, typename Tag>
165auto distance_field_product_path(const World& world, Coord3 start,
166 const DistanceFieldProduct& product,
167 DistanceFieldScratch& scratch) -> PathResult;
168
170template <typename World, typename Tag>
171auto nearest_target(const World& world, Coord3 start,
172 const DistanceFieldProduct& product,
173 DistanceFieldScratch& scratch) -> NearestTargetResult;
174
179template <typename WorldType, typename Tag>
180auto build_distance_field(
181 const WorldType& world, Coord3 goal, DistanceFieldScratch& scratch,
182 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
183 -> DistanceFieldResult;
184
189template <typename WorldType, typename Class>
190auto build_weighted_distance_field(
191 const WorldType& world, Coord3 goal, DistanceFieldScratch& scratch,
192 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
193 -> DistanceFieldResult;
194
195template <typename World, typename PassableTag, typename CostTag>
197auto build_weighted_distance_field(
198 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
199 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
200 -> DistanceFieldResult;
201
203template <typename World, typename Class>
204auto build_weighted_distance_field_in_box(
205 const World& world, Coord3 goal, Box3 domain, DistanceFieldScratch& scratch,
206 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
207 -> DistanceFieldResult;
208
209template <typename World, typename PassableTag, typename CostTag>
211auto build_weighted_distance_field_in_box(
212 const World& world, Coord3 goal, Box3 domain, DistanceFieldScratch& scratch,
213 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
214 -> DistanceFieldResult;
215
217template <typename World, typename Class, std::uint32_t MaxCost>
218auto build_bounded_weighted_distance_field(
219 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
220 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
221 -> DistanceFieldResult;
222
223template <typename World, typename PassableTag, typename CostTag,
224 std::uint32_t MaxCost>
226auto build_bounded_weighted_distance_field(
227 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
228 MissingChunkPolicy policy = MissingChunkPolicy::TreatAsBlocked)
229 -> DistanceFieldResult;
230
235 public:
237 ChunkKey key{};
238 std::uint32_t version = 0;
239 };
240
241 void reserve(std::size_t count) { chunks_.reserve(count); }
242
243 void clear() noexcept { chunks_.clear(); }
244
245 template <typename World>
246 void capture_all(const World& world) {
247 chunks_.clear();
248 chunks_.reserve(static_cast<std::size_t>(World::chunk_count));
249 // Keys are unique by construction: append directly instead of paying
250 // add_chunk's duplicate scan per key (which would be quadratic here).
251 for (std::uint64_t i = 0; i < World::chunk_count; ++i) {
252 chunks_.push_back(
253 ChunkVersionDependency{ChunkKey{i}, world.meta(ChunkKey{i}).version});
254 }
255 }
256
257 // Appends without add_chunk's duplicate scan; the caller must guarantee
258 // `key` is not already present (e.g. tracked via an external seen set).
259 template <typename World>
260 void add_chunk_unique(const World& world, ChunkKey key) {
261 chunks_.push_back(ChunkVersionDependency{key, world.meta(key).version});
262 }
263
264 template <typename World>
265 void add_chunk(const World& world, ChunkKey key) {
266 const auto version = world.meta(key).version;
267 // Path nodes are chunk-coherent: consecutive additions usually repeat
268 // the previous chunk, so check the last entry before scanning.
269 if (!chunks_.empty() && chunks_.back().key == key) {
270 chunks_.back().version = version;
271 return;
272 }
273 for (auto& chunk : chunks_) {
274 if (chunk.key == key) {
275 chunk.version = version;
276 return;
277 }
278 }
279 chunks_.push_back(ChunkVersionDependency{key, version});
280 }
281
282 template <typename World>
283 [[nodiscard]] auto is_valid(const World& world) const noexcept -> bool {
284 for (const auto chunk : chunks_) {
285 if (world.meta(chunk.key).version != chunk.version) {
286 return false;
287 }
288 }
289 return true;
290 }
291
292 [[nodiscard]] auto size() const noexcept -> std::size_t {
293 return chunks_.size();
294 }
295
296 [[nodiscard]] auto empty() const noexcept -> bool { return chunks_.empty(); }
297
298 [[nodiscard]] auto chunks() const noexcept
299 -> std::span<const ChunkVersionDependency> {
300 return chunks_;
301 }
302
303 private:
304 std::vector<ChunkVersionDependency> chunks_;
305};
306
307namespace detail {
308
309// Failure-dependency capture shared by the route/portal product builders.
310// NoPath depends on world content the search may never have touched (an
311// opening edit lands on a blocked tile; fast-path early-outs sample barriers
312// far from any expanded node), so precise capture is impractical: depend on
313// every chunk, making any edit invalidate the replay instead of it repeating
314// a stale failure forever. InvalidStart/InvalidGoal depend only on the
315// offending tiles; an out-of-bounds tile contributes nothing (bounds are
316// compile-time), which can leave the set empty -- the product is then
317// permanently invalid and callers rebuild, paying only the cheap bounds
318// rejection.
319template <typename Shape, typename World>
320void capture_failure_dependencies(const World& world, PathRequest request,
321 PathStatus status,
322 ChunkVersionDependencies& dependencies) {
323 if (status == PathStatus::NoPath) {
324 dependencies.capture_all(world);
325 return;
326 }
327 if (contains<Shape>(request.start)) {
328 dependencies.add_chunk(world,
329 chunk_key<Shape>(tile_key<Shape>(request.start)));
330 }
331 if (contains<Shape>(request.goal)) {
332 dependencies.add_chunk(world,
333 chunk_key<Shape>(tile_key<Shape>(request.goal)));
334 }
335}
336
337} // namespace detail
338
343 public:
344 void reserve_path_nodes(std::size_t node_count) { path_.reserve(node_count); }
345
346 // Dependency sets are bounded by the world's chunk count (failure
347 // products capture every chunk): reserve chunk_count to keep steady-state
348 // rebuilds allocation-free.
349 void reserve_dependencies(std::size_t count) { dependencies_.reserve(count); }
350
351 void clear() noexcept {
352 request_ = {};
353 status_ = PathStatus::NoPath;
354 cost_ = 0;
355 expanded_nodes_ = 0;
356 reached_nodes_ = 0;
357 path_.clear();
358 dependencies_.clear();
359 }
360
361 // An empty dependency set means "never validated", not "depends on
362 // nothing": cleared products and failure products that predate dependency
363 // capture must never replay as vacuously valid. Builders capture_all()
364 // for non-Found results, so any built product carries dependencies.
365 template <typename World>
366 [[nodiscard]] auto is_valid(const World& world) const noexcept -> bool {
367 return !dependencies_.empty() && dependencies_.is_valid(world);
368 }
369
370 [[nodiscard]] auto request() const noexcept -> PathRequest {
371 return request_;
372 }
373
374 [[nodiscard]] auto dependencies() const noexcept
375 -> std::span<const ChunkVersionDependencies::ChunkVersionDependency> {
376 return dependencies_.chunks();
377 }
378
379 private:
380 template <typename World, typename PassableTag, typename CostTag>
381 friend auto build_weighted_route_product(const World& world,
382 PathRequest request,
383 PathScratch& scratch,
384 WeightedRouteProduct& product)
385 -> PathResult;
386
387 template <typename World>
388 friend auto weighted_route_product_path(const World& world,
389 const WeightedRouteProduct& product)
390 -> PathResult;
391
392 PathRequest request_{};
393 PathStatus status_ = PathStatus::NoPath;
394 std::uint32_t cost_ = 0;
395 std::size_t expanded_nodes_ = 0;
396 std::size_t reached_nodes_ = 0;
397 std::vector<Coord3> path_;
398 ChunkVersionDependencies dependencies_;
399};
400
403
409 public:
410 void reserve_waypoints(std::size_t count) {
411 waypoints_.reserve(count);
412 candidate_waypoints_.reserve(count);
413 best_waypoints_.reserve(count);
414 }
415
416 void reserve_path_nodes(std::size_t node_count) {
417 path_.reserve(node_count);
418 segment_.reserve(node_count);
419 }
420
421 // Dependency sets are bounded by the world's chunk count (failure
422 // products capture every chunk): reserve chunk_count to keep steady-state
423 // rebuilds allocation-free.
424 void reserve_dependencies(std::size_t count) { dependencies_.reserve(count); }
425
426 void clear() noexcept {
427 request_ = {};
428 status_ = PathStatus::NoPath;
429 cost_ = 0;
430 expanded_nodes_ = 0;
431 reached_nodes_ = 0;
432 route_candidates_ = 0;
433 portal_scan_tiles_ = 0;
434 waypoints_.clear();
435 candidate_waypoints_.clear();
436 best_waypoints_.clear();
437 path_.clear();
438 segment_.clear();
439 dependencies_.clear();
440 }
441
442 // See WeightedRouteProduct::is_valid: empty dependencies are invalid by
443 // definition so failure/cleared products never replay vacuously.
444 template <typename World>
445 [[nodiscard]] auto is_valid(const World& world) const noexcept -> bool {
446 return !dependencies_.empty() && dependencies_.is_valid(world);
447 }
448
449 [[nodiscard]] auto request() const noexcept -> PathRequest {
450 return request_;
451 }
452
453 [[nodiscard]] auto waypoints() const noexcept -> std::span<const Coord3> {
454 return waypoints_;
455 }
456
457 [[nodiscard]] auto dependencies() const noexcept
458 -> std::span<const ChunkVersionDependencies::ChunkVersionDependency> {
459 return dependencies_.chunks();
460 }
461
462 [[nodiscard]] auto route_candidates() const noexcept -> std::size_t {
463 return route_candidates_;
464 }
465
466 [[nodiscard]] auto portal_scan_tiles() const noexcept -> std::size_t {
467 return portal_scan_tiles_;
468 }
469
470 private:
471 template <typename World, typename PassableTag, typename CostTag>
473 const World& world, PathRequest request,
474 std::span<const Coord3> waypoints, PathScratch& scratch,
476
477 template <typename World, typename PassableTag, typename CostTag>
479 const World& world, PathRequest request,
480 std::span<const Coord3> waypoints, PathScratch& scratch,
482 -> PathResult;
483
484 template <typename World, typename PassableTag, typename CostTag>
486 const World& world, PathRequest request, PathScratch& scratch,
488
489 template <typename World>
491 const World& world, const WeightedPortalRouteProduct& product)
492 -> PathResult;
493
494 // Rebuild calls may pass spans into this product's own storage --
495 // waypoints() or a previously returned PathResult.path -- and clear() plus
496 // segment stitching invalidate those. Copy product-owned input to `stash`
497 // first; the copy allocates, but only on the aliased rebuild path.
498 [[nodiscard]] auto stash_if_owned(std::span<const Coord3> input,
499 std::vector<Coord3>& stash) const
500 -> std::span<const Coord3> {
501 const auto owned = [&](const std::vector<Coord3>& storage) {
502 const auto* begin = storage.data();
503 const auto* end = begin + storage.size();
504 return !input.empty() &&
505 !std::less<const Coord3*>{}(input.data(), begin) &&
506 std::less<const Coord3*>{}(input.data(), end);
507 };
508 if (owned(waypoints_) || owned(path_) || owned(segment_) ||
509 owned(candidate_waypoints_) || owned(best_waypoints_)) {
510 stash.assign(input.begin(), input.end());
511 return std::span<const Coord3>{stash};
512 }
513 return input;
514 }
515
516 PathRequest request_{};
517 PathStatus status_ = PathStatus::NoPath;
518 std::uint32_t cost_ = 0;
519 std::size_t expanded_nodes_ = 0;
520 std::size_t reached_nodes_ = 0;
521 std::size_t route_candidates_ = 0;
522 std::size_t portal_scan_tiles_ = 0;
523 std::vector<Coord3> waypoints_;
524 std::vector<Coord3> candidate_waypoints_;
525 std::vector<Coord3> best_waypoints_;
526 std::vector<Coord3> path_;
527 std::vector<Coord3> segment_;
528 ChunkVersionDependencies dependencies_;
529};
530
536 public:
537 struct OpenNode {
538 std::uint64_t index = 0;
539 std::uint32_t g = 0;
540 std::uint32_t f = 0;
541 };
542
543 void reserve_nodes(std::size_t node_count) {
544 open_.reserve(node_count);
545 open_next_.reserve(node_count);
546 generation_.reserve(node_count);
547 state_.reserve(node_count);
548 g_.reserve(node_count);
549 parent_.reserve(node_count);
550 path_.reserve(node_count);
551 }
552
553 void clear() noexcept {
554 advance_epoch();
555 open_.clear();
556 open_next_.clear();
557 touched_count_ = 0;
558 path_.clear();
559 }
560
561 [[nodiscard]] auto capacity_nodes() const noexcept -> std::size_t {
562 return state_.capacity();
563 }
564
565 private:
566 template <typename World, typename Tag>
567 friend auto astar_path(const World& world, PathRequest request,
568 PathScratch& scratch, MissingChunkPolicy policy)
569 -> PathResult;
570
571 template <typename World, typename Class>
572 friend auto weighted_astar_path(const World& world, PathRequest request,
573 PathScratch& scratch,
574 MissingChunkPolicy policy) -> PathResult;
575
576 template <typename World, typename PassableTag, typename CostTag>
577 friend auto weighted_astar_path(const World& world, PathRequest request,
578 PathScratch& scratch,
579 MissingChunkPolicy policy) -> PathResult;
580
581 template <typename World, typename Tag>
582 friend auto cached_astar_path(const World& world, PathRequest request,
583 PathScratch& scratch, RouteCacheScratch& cache)
584 -> PathResult;
585
586 void advance_epoch() noexcept {
587 ++epoch_;
588 if (epoch_ == 0) {
589 std::fill(generation_.begin(), generation_.end(), 0);
590 epoch_ = 1;
591 }
592 }
593
594 [[nodiscard]] auto is_current(std::size_t offset) const noexcept -> bool {
595 return generation_[offset] == epoch_;
596 }
597
598 [[nodiscard]] auto state_at(std::size_t offset,
599 std::uint8_t unseen) const noexcept
600 -> std::uint8_t {
601 return is_current(offset) ? state_[offset] : unseen;
602 }
603
604 [[nodiscard]] auto g_at(std::size_t offset,
605 std::uint32_t infinite_cost) const noexcept
606 -> std::uint32_t {
607 return is_current(offset) ? g_[offset] : infinite_cost;
608 }
609
610 // offset is the node-array slot under the search's NodeIndexSpace; only
611 // the touched count survives for the expansion metric (audit 2026-07-11
612 // M10).
613 void touch_node(std::size_t offset) {
614 generation_[offset] = epoch_;
615 ++touched_count_;
616 }
617
618 std::vector<OpenNode> open_;
619 std::vector<OpenNode> open_next_;
620 // Parallel arrays deliberately: an interleaved {generation, g, state}
621 // record was tried (audit 2026-07-11 M9) and measured 3-9% SLOWER --
622 // partial-field visits (closed checks read generation+state only) waste
623 // bandwidth on a 12-byte record, while the packed arrays keep 16
624 // generations per cache line. See the optimization log, 2026-07-12.
625 std::vector<std::uint32_t> generation_;
626 std::uint32_t epoch_ = 1;
627 std::vector<std::uint8_t> state_;
628 std::vector<std::uint32_t> g_;
629 std::vector<std::uint64_t> parent_;
630 // Reached-node count only: unlike DistanceFieldScratch (whose touched
631 // list feeds dependency capture), no A* consumer reads the indices, so
632 // recording them cost an 8-byte store per reached node for nothing
633 // (audit 2026-07-11 M10).
634 std::size_t touched_count_ = 0;
635 std::vector<Coord3> path_;
636};
637
643 public:
644 void reserve_nodes(std::size_t node_count) {
645 frontier_.reserve(node_count);
646 weighted_frontier_.reserve(node_count);
647 weighted_bucket_capacity_ = node_count / 8u + 1u;
648 for (auto& bucket : weighted_buckets_) {
649 bucket.reserve(weighted_bucket_capacity_);
650 }
651 generation_.reserve(node_count);
652 distance_.reserve(node_count);
653 target_generation_.reserve(node_count);
654 touched_.reserve(node_count);
655 path_.reserve(node_count);
656 }
657
658 [[nodiscard]] auto capacity_nodes() const noexcept -> std::size_t {
659 return distance_.capacity();
660 }
661
662 private:
663 template <typename WorldType, typename Tag>
664 friend auto build_distance_field(const WorldType& world, Coord3 goal,
665 DistanceFieldScratch& scratch,
666 MissingChunkPolicy policy)
668
669 template <typename World, typename Tag>
670 friend auto distance_field_path(const World& world, Coord3 start, Coord3 goal,
671 DistanceFieldScratch& scratch) -> PathResult;
672
673 // The weighted friends name the single-Class cores; the legacy tag-pair
674 // overloads are thin forwarders and never touch scratch internals.
675 template <typename WorldType, typename Class>
676 friend auto build_weighted_distance_field(const WorldType& world, Coord3 goal,
677 DistanceFieldScratch& scratch,
678 MissingChunkPolicy policy)
680
681 template <typename World, typename Class>
683 const World& world, Coord3 goal, Box3 domain,
684 DistanceFieldScratch& scratch, MissingChunkPolicy policy)
686
687 template <typename World, typename Class, std::uint32_t MaxCost>
688 friend auto detail::build_bounded_weighted_distance_field_core(
689 const World& world, Coord3 goal, DistanceFieldScratch& scratch,
690 MissingChunkPolicy policy, std::span<const std::uint64_t> settle_targets)
692
693 // The public weighted_distance_field_path is a thin forwarder; the
694 // private access lives in the detail core it forwards to.
695 template <typename World, typename Class>
696 friend auto detail::weighted_distance_field_path_core(
697 const World& world, Coord3 start, Coord3 goal,
698 DistanceFieldScratch& scratch, bool verify_residency) -> PathResult;
699
700 // The batch skips the core's per-member residency verification and
701 // instead asserts the stamp once per group build.
702 template <typename World, typename Class, std::uint32_t MaxCost>
703 friend auto weighted_path_batch(const World& world,
704 std::span<const PathRequest> requests,
706 -> std::span<const PathResult>;
707
708 template <typename World, typename Tag>
709 friend auto build_distance_field_product(const World& world,
710 const GoalSet& goals,
711 DistanceFieldScratch& scratch,
712 DistanceFieldProduct& product)
714
715 template <typename World, typename Tag>
716 friend auto distance_field_product_path(const World& world, Coord3 start,
717 const DistanceFieldProduct& product,
718 DistanceFieldScratch& scratch)
719 -> PathResult;
720
721 template <typename World, typename Tag>
722 friend auto nearest_target(const World& world, Coord3 start,
723 const DistanceFieldProduct& product,
724 DistanceFieldScratch& scratch)
726
727 void clear_build() noexcept {
728 advance_epoch();
729 frontier_.clear();
730 weighted_frontier_.clear();
731 for (auto& bucket : weighted_buckets_) {
732 bucket.clear();
733 }
734 touched_.clear();
735 path_.clear();
736 has_goal_ = false;
737 }
738
739 void clear_path() noexcept { path_.clear(); }
740
741 void advance_epoch() noexcept {
742 ++epoch_;
743 if (epoch_ == 0) {
744 std::fill(generation_.begin(), generation_.end(), 0);
745 std::fill(target_generation_.begin(), target_generation_.end(), 0);
746 epoch_ = 1;
747 }
748 }
749
750 [[nodiscard]] auto is_current(std::size_t offset) const noexcept -> bool {
751 return generation_[offset] == epoch_;
752 }
753
754 [[nodiscard]] auto distance_at(std::size_t offset,
755 std::uint32_t infinite_distance) const noexcept
756 -> std::uint32_t {
757 return is_current(offset) ? distance_[offset] : infinite_distance;
758 }
759
760 void touch_node(std::size_t offset, std::uint64_t index) {
761 generation_[offset] = epoch_;
762 touched_.push_back(index);
763 }
764
765 // Dense-only convenience (offset == index) for the distance-field functions
766 // not yet ported to NodeIndexSpace; all of those static_assert
767 // AlwaysResident.
768 void touch_node(std::uint64_t index) {
769 touch_node(static_cast<std::size_t>(index), index);
770 }
771
772 // Settle-target marks share the build epoch: clear_build invalidates
773 // them wholesale, and advance_epoch's wrap path re-zeros both stamp
774 // arrays.
775 void mark_settle_target(std::size_t offset) {
776 target_generation_[offset] = epoch_;
777 }
778
779 [[nodiscard]] auto is_settle_target(std::size_t offset) const noexcept
780 -> bool {
781 return target_generation_[offset] == epoch_;
782 }
783
784 std::vector<std::uint64_t> frontier_;
785 std::vector<PathScratch::OpenNode> weighted_frontier_;
786 std::vector<std::vector<std::uint64_t>> weighted_buckets_;
787 std::size_t weighted_bucket_capacity_ = 0;
788 std::vector<std::uint32_t> generation_;
789 std::uint32_t epoch_ = 1;
790 std::vector<std::uint32_t> distance_;
791 std::vector<std::uint32_t> target_generation_;
792 std::vector<std::uint64_t> touched_;
793 std::vector<Coord3> path_;
794 // Per-chunk seen marks for build_distance_field_product's blocked-frontier
795 // dependency pass; sized to the world's chunk count on use.
796 std::vector<std::uint8_t> chunk_seen_;
797 Coord3 goal_{};
798 bool has_goal_ = false;
799 std::uint64_t residency_fingerprint_ = 0;
800
801 // Sparse residency staleness guard for the two-call build/read API. A built
802 // distance field is indexed by resident-slot offset; if the resident set
803 // changes between build_*distance_field and *distance_field_path (an
804 // eviction/reload can rebind a slot to a different chunk), the reader would
805 // descend a stale field and return a wrong path. build_* stamps the world's
806 // residency fingerprint (a content hash of the resident set, not a per-world
807 // counter -- so it also catches a scratch read against a different/copied/
808 // swapped world, which a bare epoch could alias) and the readers reject a
809 // mismatch (forcing a rebuild) instead of returning a wrong Found. Dense
810 // worlds never evict, so both methods compile to a no-op / constant true and
811 // keep dense byte-identical.
812 template <typename World>
813 void stamp_residency(const World& world) noexcept {
814 if constexpr (!std::is_same_v<typename World::residency_type,
816 residency_fingerprint_ = world.residency_fingerprint();
817 }
818 }
819 template <typename World>
820 [[nodiscard]] auto residency_matches(const World& world) const noexcept
821 -> bool {
822 if constexpr (std::is_same_v<typename World::residency_type,
824 return true;
825 } else {
826 return residency_fingerprint_ == world.residency_fingerprint();
827 }
828 }
829};
830
836 public:
837 void reserve_requests(std::size_t request_count) {
838 results_.reserve(request_count);
839 offsets_.reserve(request_count);
840 sizes_.reserve(request_count);
841 processed_.reserve(request_count);
842 request_goal_.reserve(request_count);
843 goal_coords_.reserve(request_count);
844 goal_counts_.reserve(request_count);
845 }
846
847 void reserve_path_nodes(std::size_t node_count) {
848 paths_.reserve(node_count);
849 }
850
851 void reserve_search_nodes(std::size_t node_count) {
852 field_scratch_.reserve_nodes(node_count);
853 astar_scratch_.reserve_nodes(node_count);
854 }
855
856 void clear() noexcept {
857 results_.clear();
858 offsets_.clear();
859 sizes_.clear();
860 processed_.clear();
861 paths_.clear();
862 stats_ = {};
863 }
864
865 [[nodiscard]] auto stats() const noexcept -> WeightedPathBatchStats {
866 return stats_;
867 }
868
869 private:
870 template <typename World, typename Class, std::uint32_t MaxCost>
871 friend auto weighted_path_batch(const World& world,
872 std::span<const PathRequest> requests,
874 -> std::span<const PathResult>;
875
876 DistanceFieldScratch field_scratch_;
877 PathScratch astar_scratch_;
878 std::vector<PathResult> results_;
879 std::vector<std::size_t> offsets_;
880 std::vector<std::size_t> sizes_;
881 std::vector<std::uint8_t> processed_;
882 std::vector<Coord3> paths_;
883 // Reusable goal -> request count flat map (open-addressed, power-of-two
884 // capacity, linear probing) built once per batch call.
885 std::vector<std::uint32_t> goal_slots_;
886 std::vector<Coord3> goal_coords_;
887 std::vector<std::uint32_t> goal_counts_;
888 std::vector<std::uint32_t> request_goal_;
889 // Counting-sort member buckets (group_offsets_[g]..group_offsets_[g+1]
890 // indexes group_members_), mirroring PathRequestRuntime's grouping, so
891 // scattering a group's results touches only its own members instead of
892 // rescanning every request per group (audit 2026-07-11 M1).
893 std::vector<std::uint32_t> group_offsets_;
894 std::vector<std::uint32_t> group_cursors_;
895 std::vector<std::uint32_t> group_members_;
896 // Per-group validated start tile indices handed to the field build as
897 // settle targets (audit 2026-07-11 M3).
898 std::vector<std::uint64_t> settle_targets_;
900};
901
902namespace detail {
903
904enum class Axis : std::uint8_t {
905 X,
906 Y,
907 Z,
908};
909
910struct PortalRouteCandidate {
911 bool found = false;
912 std::uint32_t score = 0;
913 std::size_t scan_tiles = 0;
914};
915
916// FNV-style lane combine with one final avalanche: cheap per coordinate,
917// well distributed for the power-of-two linear-probing flat hash maps used
918// by the batch planner and the request runtime (matching the route cache's
919// hashing style).
920[[nodiscard]] constexpr auto coord_hash(Coord3 coord) noexcept
921 -> std::uint64_t {
922 auto hash = std::uint64_t{0xcbf29ce484222325ull};
923 hash = (hash ^ static_cast<std::uint64_t>(coord.x)) * 0x100000001b3ull;
924 hash = (hash ^ static_cast<std::uint64_t>(coord.y)) * 0x100000001b3ull;
925 hash = (hash ^ static_cast<std::uint64_t>(coord.z)) * 0x100000001b3ull;
926 hash = (hash ^ (hash >> 30u)) * 0xbf58476d1ce4e5b9ull;
927 hash = (hash ^ (hash >> 27u)) * 0x94d049bb133111ebull;
928 return hash ^ (hash >> 31u);
929}
930
931[[nodiscard]] constexpr auto manhattan(Coord3 lhs, Coord3 rhs) noexcept
932 -> std::uint32_t {
933 const auto distance = manhattan_distance(lhs, rhs);
934 if (distance > std::numeric_limits<std::uint32_t>::max()) {
935 return std::numeric_limits<std::uint32_t>::max();
936 }
937 return static_cast<std::uint32_t>(distance);
938}
939
940template <typename World>
941[[nodiscard]] constexpr auto tile_count() noexcept -> std::size_t {
942 static_assert(World::chunk_count <= std::numeric_limits<std::size_t>::max() /
943 World::local_tile_count);
944 return static_cast<std::size_t>(World::chunk_count * World::local_tile_count);
945}
946
947template <typename Shape>
948[[nodiscard]] constexpr auto tile_index(Coord3 coord) noexcept
949 -> std::uint64_t {
950 static_assert(ShapeTraits<Shape>::tile_key_bits <= 64,
951 "MVP pathfinding supports shapes with u64 tile keys.");
952 return static_cast<std::uint64_t>(tile_key<Shape>(coord).value);
953}
954
955template <typename Shape>
956[[nodiscard]] constexpr auto tile_coord(std::uint64_t index) noexcept
957 -> Coord3 {
958 using Storage = typename ShapeTraits<Shape>::TileKeyStorage;
959 return coord<Shape>(TileKey<Shape>{static_cast<Storage>(index)});
960}
961
962// The passability/cost leaves take a movement class OR a raw passable tag
963// (normalized through movement_class_of, so every legacy <World, Tag> call
964// site compiles unchanged and the identity class keeps codegen byte-identical
965// to the raw field cast it replaces).
966template <typename World, typename ClassOrTag>
967[[nodiscard]] auto is_passable(const World& world, Coord3 coord) noexcept
968 -> bool {
969 using Class = movement::movement_class_of<ClassOrTag>;
970 const auto resolved = world.try_resolve(coord);
971 if (!resolved.has_value()) {
972 return false;
973 }
974 if constexpr (std::is_same_v<typename World::residency_type,
975 SparseResident>) {
976 const auto* page = world.try_chunk(resolved->chunk_key);
977 return page != nullptr && Class::passable(*page, resolved->local_tile_id);
978 } else {
979 return Class::passable(world.chunk(resolved->chunk_key),
980 resolved->local_tile_id);
981 }
982}
983
984template <typename World, typename ClassOrTag>
985[[nodiscard]] auto is_passable_index(const World& world,
986 std::uint64_t index) noexcept -> bool {
987 using Class = movement::movement_class_of<ClassOrTag>;
988 using Shape = typename World::shape_type;
989 using Storage = typename ShapeTraits<Shape>::TileKeyStorage;
990 const auto key = TileKey<Shape>{static_cast<Storage>(index)};
991 if constexpr (std::is_same_v<typename World::residency_type,
992 SparseResident>) {
993 // A non-resident chunk carries no data, so it reads as impassable. This
994 // keeps the unchecked hot-loop access safe on sparse worlds; the search's
995 // residency guard decides whether "impassable" means blocked or missing.
996 const auto* page = world.try_chunk(chunk_key<Shape>(key));
997 if (page == nullptr) {
998 return false;
999 }
1000 return Class::passable(*page, local_tile_id<Shape>(key));
1001 } else {
1002 return Class::passable(world.chunk(chunk_key<Shape>(key)),
1003 local_tile_id<Shape>(key));
1004 }
1005}
1006
1007// Unlike the passability leaves this REQUIRES a movement class: a raw tag
1008// would normalize to the unit-cost identity class and silently discard the
1009// cost field a legacy CostTag caller meant. Legacy <PassableTag, CostTag>
1010// entry points forward through movement::LegacyWeighted instead.
1011template <typename World, typename Class>
1012[[nodiscard]] auto tile_entry_cost_index(const World& world,
1013 std::uint64_t index) noexcept
1014 -> std::uint32_t {
1015 static_assert(std::derived_from<Class, movement::movement_class_tag>,
1016 "tile_entry_cost_index requires a MovementClass; wrap legacy "
1017 "tags in movement::LegacyWeighted<PassableTag, CostTag>.");
1018 using Shape = typename World::shape_type;
1019 using Storage = typename ShapeTraits<Shape>::TileKeyStorage;
1020 const auto key = TileKey<Shape>{static_cast<Storage>(index)};
1021 TESS_DIAG_EVENT(path_cost_read);
1022 return Class::entry_cost(world.chunk(chunk_key<Shape>(key)),
1023 local_tile_id<Shape>(key));
1024}
1025
1026[[nodiscard]] constexpr auto saturating_add(std::uint32_t lhs,
1027 std::uint32_t rhs) noexcept
1028 -> std::uint32_t {
1029 if (rhs > std::numeric_limits<std::uint32_t>::max() - lhs) {
1030 return std::numeric_limits<std::uint32_t>::max();
1031 }
1032 return lhs + rhs;
1033}
1034
1035template <typename World, typename Tag>
1036[[nodiscard]] auto is_full_axis_barrier(const World& world, Coord3 blocked,
1037 Axis axis) noexcept -> bool {
1038 using Shape = typename World::shape_type;
1039 constexpr auto size = ShapeTraits<Shape>::size;
1040
1041 if (axis == Axis::X) {
1042 for (std::int64_t z = 0; z < static_cast<std::int64_t>(size.z); ++z) {
1043 for (std::int64_t y = 0; y < static_cast<std::int64_t>(size.y); ++y) {
1044 const auto coord = Coord3{blocked.x, y, z};
1045 TESS_DIAG_EVENT(path_passability_check);
1046 if (is_passable_index<World, Tag>(world, tile_index<Shape>(coord))) {
1047 return false;
1048 }
1049 }
1050 }
1051 return true;
1052 }
1053
1054 if (axis == Axis::Y) {
1055 for (std::int64_t z = 0; z < static_cast<std::int64_t>(size.z); ++z) {
1056 for (std::int64_t x = 0; x < static_cast<std::int64_t>(size.x); ++x) {
1057 const auto coord = Coord3{x, blocked.y, z};
1058 TESS_DIAG_EVENT(path_passability_check);
1059 if (is_passable_index<World, Tag>(world, tile_index<Shape>(coord))) {
1060 return false;
1061 }
1062 }
1063 }
1064 return true;
1065 }
1066
1067 for (std::int64_t y = 0; y < static_cast<std::int64_t>(size.y); ++y) {
1068 for (std::int64_t x = 0; x < static_cast<std::int64_t>(size.x); ++x) {
1069 const auto coord = Coord3{x, y, blocked.z};
1070 TESS_DIAG_EVENT(path_passability_check);
1071 if (is_passable_index<World, Tag>(world, tile_index<Shape>(coord))) {
1072 return false;
1073 }
1074 }
1075 }
1076 return true;
1077}
1078
1079template <typename Shape>
1080[[nodiscard]] constexpr auto chunk_origin(ChunkCoord3 chunk) noexcept
1081 -> Coord3 {
1082 constexpr auto size = ShapeTraits<Shape>::chunk;
1083 return Coord3{
1084 static_cast<std::int64_t>(chunk.x * size.x),
1085 static_cast<std::int64_t>(chunk.y * size.y),
1086 static_cast<std::int64_t>(chunk.z * size.z),
1087 };
1088}
1089
1090template <typename Shape>
1091[[nodiscard]] constexpr auto adjacent_chunk(ChunkCoord3 from,
1092 ChunkCoord3 to) noexcept -> bool {
1093 const auto dx = from.x > to.x ? from.x - to.x : to.x - from.x;
1094 const auto dy = from.y > to.y ? from.y - to.y : to.y - from.y;
1095 const auto dz = from.z > to.z ? from.z - to.z : to.z - from.z;
1096 return dx + dy + dz == 1;
1097}
1098
1099template <typename World, typename PassableTag>
1100[[nodiscard]] auto best_chunk_portal(const World& world, ChunkCoord3 from,
1101 ChunkCoord3 to, Coord3 current,
1102 Coord3 goal, Coord3& portal,
1103 std::size_t* scan_tiles = nullptr) noexcept
1104 -> bool {
1105 using Shape = typename World::shape_type;
1106 constexpr auto chunk = ShapeTraits<Shape>::chunk;
1107 const auto origin = chunk_origin<Shape>(from);
1108
1109 if (!adjacent_chunk<Shape>(from, to)) {
1110 return false;
1111 }
1112
1113 auto found = false;
1114 auto best_score = std::numeric_limits<std::uint32_t>::max();
1115 const auto consider = [&](Coord3 source, Coord3 target) {
1116 if (scan_tiles != nullptr) {
1117 ++(*scan_tiles);
1118 }
1119 TESS_DIAG_EVENT(path_passability_check);
1120 if (!is_passable<World, PassableTag>(world, source)) {
1121 return;
1122 }
1123 TESS_DIAG_EVENT(path_passability_check);
1124 if (!is_passable<World, PassableTag>(world, target)) {
1125 return;
1126 }
1127 const auto score =
1128 saturating_add(manhattan(current, target), manhattan(target, goal));
1129 if (!found || score < best_score) {
1130 found = true;
1131 best_score = score;
1132 portal = target;
1133 }
1134 };
1135
1136 if (from.x != to.x) {
1137 const auto step = from.x < to.x ? std::int64_t{1} : std::int64_t{-1};
1138 const auto source_x =
1139 step > 0 ? origin.x + static_cast<std::int64_t>(chunk.x) - 1 : origin.x;
1140 for (std::int64_t z = origin.z;
1141 z < origin.z + static_cast<std::int64_t>(chunk.z); ++z) {
1142 for (std::int64_t y = origin.y;
1143 y < origin.y + static_cast<std::int64_t>(chunk.y); ++y) {
1144 consider(Coord3{source_x, y, z}, Coord3{source_x + step, y, z});
1145 }
1146 }
1147 return found;
1148 }
1149
1150 if (from.y != to.y) {
1151 const auto step = from.y < to.y ? std::int64_t{1} : std::int64_t{-1};
1152 const auto source_y =
1153 step > 0 ? origin.y + static_cast<std::int64_t>(chunk.y) - 1 : origin.y;
1154 for (std::int64_t z = origin.z;
1155 z < origin.z + static_cast<std::int64_t>(chunk.z); ++z) {
1156 for (std::int64_t x = origin.x;
1157 x < origin.x + static_cast<std::int64_t>(chunk.x); ++x) {
1158 consider(Coord3{x, source_y, z}, Coord3{x, source_y + step, z});
1159 }
1160 }
1161 return found;
1162 }
1163
1164 const auto step = from.z < to.z ? std::int64_t{1} : std::int64_t{-1};
1165 const auto source_z =
1166 step > 0 ? origin.z + static_cast<std::int64_t>(chunk.z) - 1 : origin.z;
1167 for (std::int64_t y = origin.y;
1168 y < origin.y + static_cast<std::int64_t>(chunk.y); ++y) {
1169 for (std::int64_t x = origin.x;
1170 x < origin.x + static_cast<std::int64_t>(chunk.x); ++x) {
1171 consider(Coord3{x, y, source_z}, Coord3{x, y, source_z + step});
1172 }
1173 }
1174 return found;
1175}
1176
1177template <typename World, typename PassableTag>
1178[[nodiscard]] auto build_chunk_portal_candidate(const World& world,
1179 PathRequest request,
1180 std::span<const Axis> order,
1181 std::vector<Coord3>& waypoints)
1182 -> PortalRouteCandidate {
1183 using Shape = typename World::shape_type;
1184
1185 waypoints.clear();
1186 auto current = request.start;
1187 auto current_chunk = chunk_coord<Shape>(request.start);
1188 const auto goal_chunk = chunk_coord<Shape>(request.goal);
1189 auto result = PortalRouteCandidate{true, 0, 0};
1190
1191 const auto append_portal = [&](ChunkCoord3 next_chunk) {
1192 auto portal = Coord3{};
1193 if (!best_chunk_portal<World, PassableTag>(world, current_chunk, next_chunk,
1194 current, request.goal, portal,
1195 &result.scan_tiles)) {
1196 result.found = false;
1197 return false;
1198 }
1199 result.score = saturating_add(result.score, manhattan(current, portal));
1200 waypoints.push_back(portal);
1201 current = portal;
1202 current_chunk = next_chunk;
1203 return true;
1204 };
1205
1206 for (const auto axis : order) {
1207 if (axis == Axis::X) {
1208 while (current_chunk.x != goal_chunk.x) {
1209 auto next = current_chunk;
1210 if (current_chunk.x < goal_chunk.x) {
1211 ++next.x;
1212 } else {
1213 --next.x;
1214 }
1215 if (!append_portal(next)) {
1216 return result;
1217 }
1218 }
1219 } else if (axis == Axis::Y) {
1220 while (current_chunk.y != goal_chunk.y) {
1221 auto next = current_chunk;
1222 if (current_chunk.y < goal_chunk.y) {
1223 ++next.y;
1224 } else {
1225 --next.y;
1226 }
1227 if (!append_portal(next)) {
1228 return result;
1229 }
1230 }
1231 } else {
1232 while (current_chunk.z != goal_chunk.z) {
1233 auto next = current_chunk;
1234 if (current_chunk.z < goal_chunk.z) {
1235 ++next.z;
1236 } else {
1237 --next.z;
1238 }
1239 if (!append_portal(next)) {
1240 return result;
1241 }
1242 }
1243 }
1244 }
1245
1246 result.score = saturating_add(result.score, manhattan(current, request.goal));
1247 return result;
1248}
1249
1250template <typename Fn>
1251void for_each_axis_neighbor(Coord3 coord, Fn&& fn) {
1252 fn(Coord3{coord.x + 1, coord.y, coord.z});
1253 fn(Coord3{coord.x - 1, coord.y, coord.z});
1254 fn(Coord3{coord.x, coord.y + 1, coord.z});
1255 fn(Coord3{coord.x, coord.y - 1, coord.z});
1256 fn(Coord3{coord.x, coord.y, coord.z + 1});
1257 fn(Coord3{coord.x, coord.y, coord.z - 1});
1258}
1259
1260template <typename Shape, typename Fn>
1261void for_each_indexed_axis_neighbor(Coord3 coord, std::uint64_t index,
1262 Fn&& fn) {
1263 using Traits = ShapeTraits<Shape>;
1264 constexpr auto size = Traits::size;
1265 constexpr auto chunk = Traits::chunk;
1266 constexpr auto local_bits = Traits::local_bits;
1267 constexpr auto chunk_index_stride =
1268 local_bits >= 64 ? std::uint64_t{0} : (std::uint64_t{1} << local_bits);
1269 constexpr auto chunk_y_stride = Traits::chunk_count_x * chunk_index_stride;
1270
1271 const auto local_x = static_cast<std::uint64_t>(coord.x) & (chunk.x - 1);
1272 const auto local_y = static_cast<std::uint64_t>(coord.y) & (chunk.y - 1);
1273
1274 if constexpr (!Traits::degenerate_x) {
1275 if (static_cast<std::uint64_t>(coord.x) + 1 < size.x) {
1276 const auto next_index = local_x + 1 < chunk.x
1277 ? index + 1
1278 : index + chunk_index_stride - local_x;
1279 fn(Coord3{coord.x + 1, coord.y, coord.z}, next_index);
1280 }
1281 if (coord.x > 0) {
1282 const auto next_index =
1283 local_x > 0 ? index - 1 : index - chunk_index_stride + (chunk.x - 1);
1284 fn(Coord3{coord.x - 1, coord.y, coord.z}, next_index);
1285 }
1286 }
1287
1288 if constexpr (!Traits::degenerate_y) {
1289 if (static_cast<std::uint64_t>(coord.y) + 1 < size.y) {
1290 const auto next_index = local_y + 1 < chunk.y
1291 ? index + chunk.x
1292 : index + chunk_y_stride - local_y * chunk.x;
1293 fn(Coord3{coord.x, coord.y + 1, coord.z}, next_index);
1294 }
1295 if (coord.y > 0) {
1296 const auto next_index =
1297 local_y > 0 ? index - chunk.x
1298 : index - chunk_y_stride + (chunk.y - 1) * chunk.x;
1299 fn(Coord3{coord.x, coord.y - 1, coord.z}, next_index);
1300 }
1301 }
1302
1303 if constexpr (!Traits::degenerate_z) {
1304 constexpr auto chunk_z_stride =
1305 Traits::chunk_count_x * Traits::chunk_count_y * chunk_index_stride;
1306 const auto local_xy = chunk.x * chunk.y;
1307 const auto local_z = static_cast<std::uint64_t>(coord.z) & (chunk.z - 1);
1308 if (static_cast<std::uint64_t>(coord.z) + 1 < size.z) {
1309 const auto next_index = local_z + 1 < chunk.z
1310 ? index + local_xy
1311 : index + chunk_z_stride - local_z * local_xy;
1312 fn(Coord3{coord.x, coord.y, coord.z + 1}, next_index);
1313 }
1314 if (coord.z > 0) {
1315 const auto next_index =
1316 local_z > 0 ? index - local_xy
1317 : index - chunk_z_stride + (chunk.z - 1) * local_xy;
1318 fn(Coord3{coord.x, coord.y, coord.z - 1}, next_index);
1319 }
1320 }
1321}
1322
1323[[nodiscard]] constexpr bool open_node_less(
1324 PathScratch::OpenNode lhs, PathScratch::OpenNode rhs) noexcept {
1325 if (lhs.f != rhs.f) {
1326 return lhs.f > rhs.f;
1327 }
1328 if (lhs.g != rhs.g) {
1329 return lhs.g < rhs.g;
1330 }
1331 return lhs.index > rhs.index;
1332}
1333
1334} // namespace detail
1335
1336#include <tess/path/detail/astar.h>
1337
1341template <typename World, typename PassableTag, typename CostTag>
1343 PathScratch& scratch,
1344 WeightedRouteProduct& product) -> PathResult {
1345 using Shape = typename World::shape_type;
1346 // Route products track chunk-version dependencies for cached replay
1347 // (weighted_route_product_path -> is_valid), which reads meta() for chunks a
1348 // sparse world may have since evicted. Dense-only until the sparse
1349 // route-cache slice defines dependency validity under eviction; weighted A*
1350 // itself runs natively on sparse worlds via weighted_astar_path.
1351 static_assert(
1352 std::is_same_v<typename World::residency_type, AlwaysResident>,
1353 "build_weighted_route_product is dense-only; call weighted_astar_path "
1354 "directly for sparse worlds, or await the sparse route-cache slice.");
1355
1356 product.clear();
1357 const auto result =
1358 weighted_astar_path<World, PassableTag, CostTag>(world, request, scratch);
1359 product.request_ = request;
1360 product.status_ = result.status;
1361 product.cost_ = result.cost;
1362 product.expanded_nodes_ = result.expanded_nodes;
1363 product.reached_nodes_ = result.reached_nodes;
1364 product.path_.assign(result.path.begin(), result.path.end());
1365 if (result.status == PathStatus::Found) {
1366 for (const auto coord : product.path_) {
1367 const auto key = tile_key<Shape>(coord);
1368 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
1369 }
1370 } else {
1371 // See detail::capture_failure_dependencies: a replayed failure must be
1372 // invalidated by any edit that could change the answer.
1373 detail::capture_failure_dependencies<Shape>(world, request, result.status,
1374 product.dependencies_);
1375 }
1376
1377 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
1378 product.reached_nodes_, product.path_};
1379}
1380
1382template <typename World>
1384 const WeightedRouteProduct& product)
1385 -> PathResult {
1386 if (!product.is_valid(world)) {
1387 return PathResult{PathStatus::NoPath, 0, 0, 0, {}};
1388 }
1389 return PathResult{product.status_, product.cost_, 0, 0, product.path_};
1390}
1391
1396template <typename World, typename PassableTag, typename CostTag>
1398 PathRequest request,
1399 std::span<const Coord3> waypoints,
1400 PathScratch& scratch,
1402 -> PathResult {
1403 using Shape = typename World::shape_type;
1404 // Same chunk-version dependency tracking as build_weighted_route_product, so
1405 // dense-only until the sparse route-cache slice. The per-segment weighted A*
1406 // it chains already supports sparse worlds via weighted_astar_path.
1407 static_assert(
1408 std::is_same_v<typename World::residency_type, AlwaysResident>,
1409 "build_weighted_portal_route_product is dense-only; chain "
1410 "weighted_astar_path directly for sparse worlds, or await the sparse "
1411 "route-cache slice.");
1412
1413 std::vector<Coord3> stash;
1414 const auto source = product.stash_if_owned(waypoints, stash);
1415
1416 product.clear();
1417 product.request_ = request;
1418 product.waypoints_.assign(source.begin(), source.end());
1419
1420 auto from = request.start;
1421 auto total_cost = std::uint32_t{0};
1422 auto total_expanded = std::size_t{0};
1423 auto total_reached = std::size_t{0};
1424 auto append_segment = [&](PathRequest segment_request) {
1425 const auto result = weighted_astar_path<World, PassableTag, CostTag>(
1426 world, segment_request, scratch);
1427 total_expanded += result.expanded_nodes;
1428 total_reached += result.reached_nodes;
1429 if (result.status != PathStatus::Found) {
1430 product.path_.clear();
1431 product.status_ = result.status;
1432 product.expanded_nodes_ = total_expanded;
1433 product.reached_nodes_ = total_reached;
1434 // Same failure-dependency contract as build_weighted_route_product;
1435 // the failing segment's endpoints are the offending tiles.
1436 detail::capture_failure_dependencies<Shape>(
1437 world, segment_request, result.status, product.dependencies_);
1438 return false;
1439 }
1440 total_cost = detail::saturating_add(total_cost, result.cost);
1441 product.segment_.assign(result.path.begin(), result.path.end());
1442 for (std::size_t i = product.path_.empty() ? 0u : 1u;
1443 i < product.segment_.size(); ++i) {
1444 product.path_.push_back(product.segment_[i]);
1445 }
1446 return true;
1447 };
1448
1449 for (const auto waypoint : source) {
1450 if (!append_segment(PathRequest{from, waypoint})) {
1451 return PathResult{product.status_, 0, total_expanded, total_reached,
1452 product.path_};
1453 }
1454 from = waypoint;
1455 }
1456 if (!append_segment(PathRequest{from, request.goal})) {
1457 return PathResult{product.status_, 0, total_expanded, total_reached,
1458 product.path_};
1459 }
1460
1461 product.status_ = PathStatus::Found;
1462 product.cost_ = total_cost;
1463 product.expanded_nodes_ = total_expanded;
1464 product.reached_nodes_ = total_reached;
1465 for (const auto coord : product.path_) {
1466 const auto key = tile_key<Shape>(coord);
1467 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
1468 }
1469 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
1470 product.reached_nodes_, product.path_};
1471}
1472
1474template <typename World>
1476 const World& world, const WeightedPortalRouteProduct& product)
1477 -> PathResult {
1478 if (!product.is_valid(world)) {
1479 return PathResult{PathStatus::NoPath, 0, 0, 0, {}};
1480 }
1481 return PathResult{product.status_, product.cost_, 0, 0, product.path_};
1482}
1483
1485template <typename WorldType, typename Tag>
1486auto build_distance_field(const WorldType& world, Coord3 goal,
1487 DistanceFieldScratch& scratch,
1488 [[maybe_unused]] MissingChunkPolicy policy)
1490 using Shape = typename WorldType::shape_type;
1491 using Space = detail::NodeIndexSpace<WorldType>;
1492 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
1493
1494 TESS_DIAG_EVENT_VALUE(path_clear, scratch.touched_.size());
1495 scratch.clear_build();
1496 if (!contains<Shape>(goal)) {
1497 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
1498 }
1499 if constexpr (!Space::is_dense) {
1500 // A non-resident goal cannot seed the flood: indexing its node-array slot
1501 // would be out of bounds. Under Indeterminate the field is simply unknown.
1502 const Space residency{world};
1503 if (!residency.is_resident_index(detail::tile_index<Shape>(goal))) {
1504 return DistanceFieldResult{policy == MissingChunkPolicy::Indeterminate
1505 ? PathStatus::Indeterminate
1506 : PathStatus::InvalidGoal,
1507 0, 0};
1508 }
1509 }
1510 TESS_DIAG_EVENT(path_goal_passability_check);
1511 if (!detail::is_passable<WorldType, Tag>(world, goal)) {
1512 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
1513 }
1514
1515 const Space space{world};
1516 const auto node_count = space.capacity_hint();
1517 if (scratch.distance_.size() != node_count) {
1518 TESS_DIAG_EVENT(path_initialize);
1519 scratch.generation_.assign(node_count, 0);
1520 scratch.distance_.assign(node_count, infinite_distance);
1521 }
1522
1523 const auto goal_index = detail::tile_index<Shape>(goal);
1524 const auto goal_offset = space.offset(goal_index);
1525 scratch.goal_ = goal;
1526 scratch.has_goal_ = true;
1527 scratch.stamp_residency(world);
1528 scratch.distance_[goal_offset] = 0;
1529 scratch.touch_node(goal_offset, goal_index);
1530 TESS_DIAG_EVENT(path_touch_node);
1531 scratch.frontier_.push_back(goal_index);
1532 TESS_DIAG_EVENT(path_heap_push);
1533
1534 std::size_t expanded_nodes = 0;
1535 std::size_t head = 0;
1536 // Sparse: set when the flood skips a non-resident neighbor, so a field
1537 // truncated by a missing chunk can report Indeterminate under policy.
1538 [[maybe_unused]] bool crossed_missing = false;
1539 while (head < scratch.frontier_.size()) {
1540 const auto current = scratch.frontier_[head];
1541 ++head;
1542 TESS_DIAG_EVENT(path_heap_pop);
1543 ++expanded_nodes;
1544
1545 const auto current_offset = space.offset(current);
1546 const auto current_distance =
1547 scratch.distance_at(current_offset, infinite_distance);
1548 const auto current_coord = detail::tile_coord<Shape>(current);
1549 detail::for_each_indexed_axis_neighbor<Shape>(
1550 current_coord, current, [&](Coord3, std::uint64_t neighbor_index) {
1551 TESS_DIAG_EVENT(path_neighbor_candidate);
1552 if constexpr (!Space::is_dense) {
1553 // A non-resident neighbor has no node-array slot; remember the
1554 // boundary and skip it before computing an out-of-bounds offset.
1555 if (!space.is_resident_index(neighbor_index)) {
1556 crossed_missing = true;
1557 return;
1558 }
1559 }
1560 const auto neighbor_offset = space.offset(neighbor_index);
1561 if (scratch.is_current(neighbor_offset)) {
1562 TESS_DIAG_EVENT(path_neighbor_closed);
1563 return;
1564 }
1565 TESS_DIAG_EVENT(path_passability_check);
1566 if (!detail::is_passable_index<WorldType, Tag>(world,
1567 neighbor_index)) {
1568 TESS_DIAG_EVENT(path_neighbor_blocked);
1569 return;
1570 }
1571
1572 scratch.distance_[neighbor_offset] = current_distance + 1;
1573 scratch.touch_node(neighbor_offset, neighbor_index);
1574 TESS_DIAG_EVENT(path_touch_node);
1575 scratch.frontier_.push_back(neighbor_index);
1576 TESS_DIAG_EVENT(path_heap_push);
1577 });
1578 }
1579
1580 if constexpr (!Space::is_dense) {
1581 if (crossed_missing && policy == MissingChunkPolicy::Indeterminate) {
1582 return DistanceFieldResult{PathStatus::Indeterminate, expanded_nodes,
1583 scratch.touched_.size()};
1584 }
1585 }
1586 return DistanceFieldResult{PathStatus::Found, expanded_nodes,
1587 scratch.touched_.size()};
1588}
1589
1594template <typename World, typename Tag>
1595auto distance_field_path(const World& world, Coord3 start, Coord3 goal,
1596 DistanceFieldScratch& scratch) -> PathResult {
1597 using Shape = typename World::shape_type;
1598 using Space = detail::NodeIndexSpace<World>;
1599 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
1600
1601 scratch.clear_path();
1602 if (!contains<Shape>(start)) {
1603 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
1604 }
1605 if constexpr (!Space::is_dense) {
1606 // A non-resident start is not in the field; its node-array slot would be
1607 // out of bounds. Report InvalidStart -- the caller learns whether the
1608 // field itself was truncated from build_distance_field's status.
1609 const Space residency{world};
1610 if (!residency.is_resident_index(detail::tile_index<Shape>(start))) {
1611 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
1612 }
1613 }
1614 TESS_DIAG_EVENT(path_start_passability_check);
1615 if (!detail::is_passable<World, Tag>(world, start)) {
1616 return PathResult{PathStatus::InvalidStart, 0, 0, 0, scratch.path_};
1617 }
1618 if (!contains<Shape>(goal)) {
1619 return PathResult{PathStatus::InvalidGoal, 0, 0, 0, scratch.path_};
1620 }
1621 if (!scratch.has_goal_ || scratch.goal_ != goal ||
1622 !scratch.residency_matches(world)) {
1623 return PathResult{PathStatus::NoPath, 0, 0, 0, scratch.path_};
1624 }
1625
1626 const Space space{world};
1627 const auto start_index = detail::tile_index<Shape>(start);
1628 auto current = start_index;
1629 auto current_offset = space.offset(current);
1630 auto current_distance =
1631 scratch.distance_at(current_offset, infinite_distance);
1632 if (current_distance == infinite_distance) {
1633 return PathResult{PathStatus::NoPath, 0, 0, scratch.touched_.size(),
1634 scratch.path_};
1635 }
1636
1637 scratch.path_.push_back(start);
1638 TESS_DIAG_EVENT(path_reconstruct_node);
1639 while (current_distance > 0) {
1640 const auto current_coord = detail::tile_coord<Shape>(current);
1641 auto next = current;
1642 auto next_distance = current_distance;
1643 detail::for_each_indexed_axis_neighbor<Shape>(
1644 current_coord, current, [&](Coord3, std::uint64_t neighbor_index) {
1645 if constexpr (!Space::is_dense) {
1646 // A non-resident neighbor was never touched by the flood, so its
1647 // distance is infinite and it cannot be the descent step; skip it
1648 // before computing an out-of-bounds offset.
1649 if (!space.is_resident_index(neighbor_index)) {
1650 return;
1651 }
1652 }
1653 const auto neighbor_offset = space.offset(neighbor_index);
1654 const auto neighbor_distance =
1655 scratch.distance_at(neighbor_offset, infinite_distance);
1656 if (neighbor_distance < next_distance) {
1657 next = neighbor_index;
1658 next_distance = neighbor_distance;
1659 }
1660 });
1661
1662 if (next == current || next_distance + 1 != current_distance) {
1663 scratch.path_.clear();
1664 return PathResult{PathStatus::NoPath, 0, 0, scratch.touched_.size(),
1665 scratch.path_};
1666 }
1667
1668 current = next;
1669 current_offset = space.offset(current);
1670 current_distance = scratch.distance_at(current_offset, infinite_distance);
1671 scratch.path_.push_back(detail::tile_coord<Shape>(current));
1672 TESS_DIAG_EVENT(path_reconstruct_node);
1673 }
1674
1675 return PathResult{
1676 PathStatus::Found, scratch.distance_[space.offset(start_index)],
1677 scratch.path_.size(), scratch.touched_.size(), scratch.path_};
1678}
1679
1681template <typename WorldType, typename Class>
1682auto build_weighted_distance_field(const WorldType& world, Coord3 goal,
1683 DistanceFieldScratch& scratch,
1684 [[maybe_unused]] MissingChunkPolicy policy)
1686 static_assert(std::derived_from<Class, movement::movement_class_tag>,
1687 "build_weighted_distance_field<World, Class> requires a "
1688 "MovementClass; legacy tag pairs go through the "
1689 "<World, PassableTag, CostTag> overload.");
1690 using Shape = typename WorldType::shape_type;
1691 using Space = detail::NodeIndexSpace<WorldType>;
1692 constexpr auto infinite_distance = std::numeric_limits<std::uint32_t>::max();
1693
1694 TESS_DIAG_EVENT_VALUE(path_clear, scratch.touched_.size());
1695 scratch.clear_build();
1696 if (!contains<Shape>(goal)) {
1697 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
1698 }
1699 if constexpr (!Space::is_dense) {
1700 // A non-resident goal cannot seed the flood; under Indeterminate the field
1701 // is simply unknown. Resolve it before is_passable/entry-cost read the
1702 // goal chunk.
1703 const Space residency{world};
1704 if (!residency.is_resident_index(detail::tile_index<Shape>(goal))) {
1705 return DistanceFieldResult{policy == MissingChunkPolicy::Indeterminate
1706 ? PathStatus::Indeterminate
1707 : PathStatus::InvalidGoal,
1708 0, 0};
1709 }
1710 }
1711 TESS_DIAG_EVENT(path_goal_passability_check);
1712 if (!detail::is_passable<WorldType, Class>(world, goal)) {
1713 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
1714 }
1715
1716 const auto goal_index = detail::tile_index<Shape>(goal);
1717 if (detail::tile_entry_cost_index<WorldType, Class>(world, goal_index) == 0) {
1718 return DistanceFieldResult{PathStatus::InvalidGoal, 0, 0};
1719 }
1720
1721 const Space space{world};
1722 const auto node_count = space.capacity_hint();
1723 if (scratch.distance_.size() != node_count) {
1724 TESS_DIAG_EVENT(path_initialize);
1725 scratch.generation_.assign(node_count, 0);
1726 scratch.distance_.assign(node_count, infinite_distance);
1727 }
1728
1729 const auto goal_offset = space.offset(goal_index);
1730 scratch.goal_ = goal;
1731 scratch.has_goal_ = true;
1732 scratch.stamp_residency(world);
1733 scratch.distance_[goal_offset] = 0;
1734 scratch.touch_node(goal_offset, goal_index);
1735 TESS_DIAG_EVENT(path_touch_node);
1736 scratch.weighted_frontier_.push_back(PathScratch::OpenNode{goal_index, 0, 0});
1737 std::push_heap(scratch.weighted_frontier_.begin(),
1738 scratch.weighted_frontier_.end(), detail::open_node_less);
1739 TESS_DIAG_EVENT(path_heap_push);
1740
1741 std::size_t expanded_nodes = 0;
1742 [[maybe_unused]] bool crossed_missing = false;
1743 while (!scratch.weighted_frontier_.empty()) {
1744 TESS_DIAG_EVENT(path_heap_pop);
1745 std::pop_heap(scratch.weighted_frontier_.begin(),
1746 scratch.weighted_frontier_.end(), detail::open_node_less);
1747 const auto current = scratch.weighted_frontier_.back();
1748 scratch.weighted_frontier_.pop_back();
1749
1750 const auto current_offset = space.offset(current.index);
1751 const auto current_distance =
1752 scratch.distance_at(current_offset, infinite_distance);
1753 if (current.g != current_distance) {
1754 TESS_DIAG_EVENT_VALUE(path_skip_pop, false);
1755 continue;
1756 }
1757 ++expanded_nodes;
1758
1759 const auto current_entry_cost =
1760 detail::tile_entry_cost_index<WorldType, Class>(world, current.index);
1761 if (current_entry_cost == 0) {
1762 continue;
1763 }
1764 const auto current_coord = detail::tile_coord<Shape>(current.index);
1765 detail::for_each_indexed_axis_neighbor<Shape>(
1766 current_coord, current.index,
1767 [&](Coord3, std::uint64_t neighbor_index) {
1768 TESS_DIAG_EVENT(path_neighbor_candidate);
1769 if constexpr (!Space::is_dense) {
1770 if (!space.is_resident_index(neighbor_index)) {
1771 crossed_missing = true;
1772 return;
1773 }
1774 }
1775 const auto neighbor_offset = space.offset(neighbor_index);
1776 TESS_DIAG_EVENT(path_relax_attempt);
1777 if (!scratch.is_current(neighbor_offset)) {
1778 TESS_DIAG_EVENT(path_passability_check);
1779 if (!detail::is_passable_index<WorldType, Class>(world,
1780 neighbor_index)) {
1781 TESS_DIAG_EVENT(path_neighbor_blocked);
1782 return;
1783 }
1784 if (detail::tile_entry_cost_index<WorldType, Class>(
1785 world, neighbor_index) == 0) {
1786 TESS_DIAG_EVENT(path_neighbor_blocked);
1787 return;
1788 }
1789 scratch.distance_[neighbor_offset] = infinite_distance;
1790 scratch.touch_node(neighbor_offset, neighbor_index);
1791 TESS_DIAG_EVENT(path_touch_node);
1792 }
1793
1794 const auto next_distance =
1795 detail::saturating_add(current_distance, current_entry_cost);
1796 if (next_distance == infinite_distance) {
1797 return;
1798 }
1799 if (next_distance <
1800 scratch.distance_at(neighbor_offset, infinite_distance)) {
1801 TESS_DIAG_EVENT(path_relax_success);
1802 scratch.distance_[neighbor_offset] = next_distance;
1803 scratch.weighted_frontier_.push_back(PathScratch::OpenNode{
1804 neighbor_index,
1805 next_distance,
1806 next_distance,
1807 });
1808 std::push_heap(scratch.weighted_frontier_.begin(),
1809 scratch.weighted_frontier_.end(),
1810 detail::open_node_less);
1811 TESS_DIAG_EVENT(path_heap_push);
1812 }
1813 });
1814 }
1815
1816 if constexpr (!Space::is_dense) {
1817 if (crossed_missing && policy == MissingChunkPolicy::Indeterminate) {
1818 return DistanceFieldResult{PathStatus::Indeterminate, expanded_nodes,
1819 scratch.touched_.size()};
1820 }
1821 }
1822 return DistanceFieldResult{PathStatus::Found, expanded_nodes,
1823 scratch.touched_.size()};
1824}
1825
1826template <typename World, typename PassableTag, typename CostTag>
1827auto build_weighted_distance_field(const World& world, Coord3 goal,
1828 DistanceFieldScratch& scratch,
1829 MissingChunkPolicy policy)
1830 -> DistanceFieldResult {
1831 return build_weighted_distance_field<
1832 World, movement::LegacyWeighted<PassableTag, CostTag>>(world, goal,
1833 scratch, policy);
1834}
1835
1836#include <tess/path/detail/weighted_batch.h>
1837
1838} // namespace tess
1839
1840#include <tess/path/route_cache.h>
Definition path.h:234
Definition field_product_cache.h:54
Definition path.h:642
friend auto build_weighted_distance_field_in_box(const World &world, Coord3 goal, Box3 domain, DistanceFieldScratch &scratch, MissingChunkPolicy policy) -> DistanceFieldResult
Builds a weighted field restricted to the intersection with domain.
Definition distance_field_box.h:18
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:526
friend auto distance_field_path(const World &world, Coord3 start, Coord3 goal, DistanceFieldScratch &scratch) -> PathResult
Definition path.h:1595
friend auto build_weighted_distance_field(const WorldType &world, Coord3 goal, DistanceFieldScratch &scratch, MissingChunkPolicy policy) -> DistanceFieldResult
Builds a movement-class weighted field into reusable caller scratch.
Definition path.h:1682
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:602
friend auto build_distance_field_product(const World &world, const GoalSet &goals, DistanceFieldScratch &scratch, DistanceFieldProduct &product) -> DistanceFieldResult
Builds a dense multi-goal field into caller-owned reusable storage.
Definition field_product_cache.h:365
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:1486
Owns an ordered set of goals used to build a reusable distance product.
Definition field_product_cache.h:18
Definition path.h:535
friend auto weighted_astar_path(const World &world, PathRequest request, PathScratch &scratch, MissingChunkPolicy policy) -> PathResult
Finds a weighted path using separate legacy passability and cost tags.
friend auto weighted_astar_path(const World &world, PathRequest request, PathScratch &scratch, MissingChunkPolicy policy) -> PathResult
Finds a weighted path using separate legacy passability and cost tags.
friend auto cached_astar_path(const World &world, PathRequest request, PathScratch &scratch, RouteCacheScratch &cache) -> PathResult
Runs unit A* with exact and same-goal suffix reuse from caller-owned cache.
Definition route_cache.h:432
friend auto astar_path(const World &world, PathRequest request, PathScratch &scratch, MissingChunkPolicy policy) -> PathResult
Definition path_view.h:21
Bounded scratch cache for exact and same-goal suffix unit routes.
Definition route_cache.h:42
Definition path.h:835
friend auto build_weighted_chunk_portal_route_product(const World &world, PathRequest request, PathScratch &scratch, WeightedPortalRouteProduct &product) -> PathResult
Definition portal_route.h:112
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:1475
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:1397
Bounded, movement-class-safe cache of verified weighted path segments.
Definition portal_segment_cache.h:48
Definition path.h:342
friend auto weighted_route_product_path(const World &world, const WeightedRouteProduct &product) -> PathResult
Replays a route product when all captured chunk versions still match.
Definition path.h:1383
friend auto build_weighted_route_product(const World &world, PathRequest request, PathScratch &scratch, WeightedRouteProduct &product) -> PathResult
Definition path.h:1342
Definition world.h:22
Definition world.h:18
Definition shape.h:75
Definition shape.h:67
Definition shape.h:30
Reports distance-field construction status and search work.
Definition path.h:72
Reports the closest reachable goal and a scratch-owned path to it.
Definition field_product_cache.h:41
Specifies inclusive start and goal coordinates for a path query.
Definition path.h:54
Definition path.h:63
Definition path.h:537
Definition shape.h:225
Definition path.h:80