tess 0.4.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
transition_provider.h
1#pragma once
2
3#include <tess/core/shape.h>
4#include <tess/storage/residency.h>
5
6#include <concepts>
7#include <cstdint>
8#include <type_traits>
9
10namespace tess {
11
12// A transition provider contributes EXTRA directed tile-to-tile transitions
13// to the region graph, beyond the built-in six-axis face adjacency (stairs,
14// ladders, and similar special movement for a class). The graph builders
15// enumerate a provider once per chunk and append one directed RegionPortal
16// per transition whose endpoints both resolve to labeled regions.
17//
18// Contract:
19// - `for_each_transition(world, chunk, sink)` invokes `sink(from, to)` once
20// per directed transition ORIGINATING in `chunk` (`from` must lie in that
21// chunk). Bidirectional passages emit both directions, each from its own
22// chunk's enumeration.
23// - `to` must lie in the same chunk or a face-neighbor chunk. Incremental
24// updates re-derive portals only for dirty chunks and their face
25// neighbors, so a longer-range transition would survive, stale, past an
26// edit to its landing chunk (asserted in debug builds).
27// - Enumeration must be deterministic for identical world content: the
28// incremental-equals-full-rebuild invariant compares portal order.
29// - Transitions whose endpoints are not passable for the graph's movement
30// class contribute nothing (their tiles hold no region label). On a sparse
31// world a transition landing in a NON-RESIDENT chunk marks the origin
32// region as reaching missing topology, so reachability degrades to
33// Indeterminate rather than a wrong Unreachable.
34//
35// The provider type and revision are stamped on the graph at build time
36// (mirroring the movement-class stamp). Empty providers have revision zero.
37// A stateful provider must expose
38// `std::uint64_t transition_revision() const noexcept` and change that value
39// whenever its emitted transition set can change. update_region_graph falls
40// back to a full rebuild when either stamp differs.
42template <typename P, typename World>
43concept TransitionProviderFor = requires(const P& provider, const World& world,
44 ChunkKey chunk,
45 void (*sink)(Coord3, Coord3)) {
46 provider.for_each_transition(world, chunk, sink);
47} && (std::is_empty_v<P> || requires(const P& provider) {
48 {
49 provider.transition_revision()
50 } noexcept -> std::same_as<std::uint64_t>;
51 });
52
53namespace detail {
54
55// Revision helper shared by graph construction and incremental updates.
56// Empty providers compile to the constant zero; stateful providers pay one
57// cheap revision read per graph build/update, never per transition.
58template <typename P>
59[[nodiscard]] constexpr auto transition_provider_revision(
60 const P& provider) noexcept -> std::uint64_t {
61 if constexpr (std::is_empty_v<P>) {
62 (void)provider;
63 return 0;
64 } else {
65 return provider.transition_revision();
66 }
67}
68
69} // namespace detail
70
71// The default provider: no transitions beyond the built-in face adjacency
72// the region graph already pairs. Building with it is byte-identical to the
73// providerless build.
76 template <typename World, typename Sink>
77 void for_each_transition(const World& /*world*/, ChunkKey /*chunk*/,
78 Sink&& /*sink*/) const noexcept {}
79};
80
81// Direction a stair's landing lies in, stored as the integral value of the
82// stair field (0 keeps "no stair" as the zero-initialized default).
84enum class StairDirection : std::uint8_t {
85 None = 0,
86 PositiveX = 1,
87 NegativeX = 2,
88 PositiveY = 3,
89 NegativeY = 4,
90};
91
92// Vertical provider: an integral `StairTag` field holds a StairDirection.
93// A non-None value marks the tile as the FOOT of a stair whose landing is
94// one step in that direction AND one z-level up -- deliberately offset,
95// because two vertically stacked passable tiles are already six-axis
96// adjacent, so a same-column "stair" would add nothing. Each stair
97// contributes both directions, each emitted from the chunk owning its
98// origin tile: the up transition from the foot's chunk, the down transition
99// from the landing's chunk -- the foot's chunk, its +z face neighbor (the
100// landing rose off the top layer), or a sideways face neighbor (the landing
101// stepped off the x/y edge at a lower local z). Whether either endpoint is
102// traversable stays a movement-class question -- the graph builders drop
103// transitions whose endpoints hold no region label, so stair edges are
104// automatically per-class. A stair field value outside the StairDirection
105// range reads as None.
106//
107// Limit: a landing must share the foot's chunk or a face-neighbor chunk
108// (the TransitionProviderFor contract). A stair whose landing would cross
109// two chunk boundaries at once -- sideways off the chunk's x/y edge AND up
110// off its top z layer -- contributes nothing; place the foot so the landing
111// stays within face-neighbor range.
113template <typename StairTag>
115 template <typename World, typename Sink>
116 void for_each_transition(const World& world, ChunkKey chunk,
117 Sink&& sink) const {
118 using Shape = typename World::shape_type;
119 using Traits = ShapeTraits<Shape>;
120 static_assert(
121 World::schema_type::template contains<StairTag>,
122 "StairTransitions references a field absent from the schema.");
123 emit_for_feet(world, chunk, chunk, sink);
124 // Down transitions originating here belong to stairs whose FOOT lies in
125 // a face-neighbor chunk: one z-level below (the landing rose off the
126 // foot chunk's top layer) or sideways at the same chunk z (the landing
127 // stepped off the foot chunk's x/y edge with local z below the top).
128 const auto coord_of_chunk = chunk_coord<Shape>(chunk);
129 const auto scan_foot_chunk = [&](ChunkCoord3 neighbor) {
130 const auto key = chunk_key<Shape>(neighbor);
131 if constexpr (std::is_same_v<typename World::residency_type,
133 // A non-resident foot chunk holds no readable stairs; the landing
134 // tile sits on this chunk's boundary toward it, whose boundary exit
135 // already marks the region as reaching missing topology.
136 if (!world.is_resident(key)) {
137 return;
138 }
139 }
140 emit_for_feet(world, key, chunk, sink);
141 };
142 if (coord_of_chunk.z > 0) {
143 auto below = coord_of_chunk;
144 --below.z;
145 scan_foot_chunk(below);
146 }
147 if (coord_of_chunk.x > 0) {
148 auto west = coord_of_chunk;
149 --west.x;
150 scan_foot_chunk(west);
151 }
152 if (coord_of_chunk.x + 1 < Traits::chunk_count_x) {
153 auto east = coord_of_chunk;
154 ++east.x;
155 scan_foot_chunk(east);
156 }
157 if (coord_of_chunk.y > 0) {
158 auto south = coord_of_chunk;
159 --south.y;
160 scan_foot_chunk(south);
161 }
162 if (coord_of_chunk.y + 1 < Traits::chunk_count_y) {
163 auto north = coord_of_chunk;
164 ++north.y;
165 scan_foot_chunk(north);
166 }
167 }
168
169 private:
170 // Emits the transitions of every stair FOOT in `foot_chunk` whose origin
171 // tile lies in `origin_chunk`: the up transition when the foot is the
172 // origin, the down transition when the landing is.
173 template <typename World, typename Sink>
174 static void emit_for_feet(const World& world, ChunkKey foot_chunk,
175 ChunkKey origin_chunk, Sink&& sink) {
176 using Shape = typename World::shape_type;
177 using Traits = ShapeTraits<Shape>;
178 const auto& page = world.chunk(foot_chunk);
179 const auto origin = chunk_coord<Shape>(foot_chunk);
180 const auto base = Coord3{
181 static_cast<std::int64_t>(origin.x * Traits::chunk.x),
182 static_cast<std::int64_t>(origin.y * Traits::chunk.y),
183 static_cast<std::int64_t>(origin.z * Traits::chunk.z),
184 };
185 for (std::uint64_t raw_id = 0; raw_id < Traits::local_tile_count;
186 ++raw_id) {
187 const auto value = page.template field<StairTag>(LocalTileId{raw_id});
188 static_assert(std::is_integral_v<std::remove_cvref_t<decltype(value)>>,
189 "StairTransitions requires an integral stair field.");
190 // Out-of-range values read as None. Compare BEFORE any narrowing: a
191 // wider field's 257 must not wrap into PositiveX, and a negative value
192 // must not wrap into the valid range.
193 if constexpr (std::is_signed_v<std::remove_cvref_t<decltype(value)>>) {
194 if (value <= 0) {
195 continue;
196 }
197 } else if (value == 0) {
198 continue;
199 }
200 if (static_cast<std::uint64_t>(value) >
201 static_cast<std::uint64_t>(StairDirection::NegativeY)) {
202 continue;
203 }
204 const auto direction =
205 static_cast<StairDirection>(static_cast<std::uint8_t>(value));
206 const auto local = local_coord_of<Shape>(raw_id);
207 const auto foot =
208 Coord3{base.x + local.x, base.y + local.y, base.z + local.z};
209 auto landing = foot;
210 ++landing.z;
211 switch (direction) {
212 case StairDirection::PositiveX:
213 ++landing.x;
214 break;
215 case StairDirection::NegativeX:
216 --landing.x;
217 break;
218 case StairDirection::PositiveY:
219 ++landing.y;
220 break;
221 case StairDirection::NegativeY:
222 --landing.y;
223 break;
224 case StairDirection::None:
225 continue;
226 }
227 if (!contains<Shape>(landing)) {
228 continue;
229 }
230 // Diagonal chunk crossings (sideways AND up at once) violate the
231 // face-neighbor contract; such a stair contributes nothing.
232 const auto foot_chunk_coord = chunk_coord<Shape>(foot);
233 const auto landing_chunk_coord = chunk_coord<Shape>(landing);
234 const auto dx = foot_chunk_coord.x != landing_chunk_coord.x ? 1 : 0;
235 const auto dy = foot_chunk_coord.y != landing_chunk_coord.y ? 1 : 0;
236 const auto dz = foot_chunk_coord.z != landing_chunk_coord.z ? 1 : 0;
237 if (dx + dy + dz > 1) {
238 continue;
239 }
240 const auto landing_chunk = chunk_key<Shape>(landing_chunk_coord);
241 if (foot_chunk.value == origin_chunk.value) {
242 sink(foot, landing); // up, originating at the foot
243 if (landing_chunk.value == origin_chunk.value) {
244 sink(landing, foot); // down, landing shares the chunk
245 }
246 } else if (landing_chunk.value == origin_chunk.value) {
247 sink(landing, foot); // down, originating at the landing
248 }
249 }
250 }
251
252 template <typename Shape>
253 [[nodiscard]] static auto local_coord_of(std::uint64_t raw_id) noexcept
254 -> Coord3 {
255 const auto chunk = ShapeTraits<Shape>::chunk;
256 const auto xy = static_cast<std::uint64_t>(chunk.x) * chunk.y;
257 const auto z = raw_id / xy;
258 const auto rest = raw_id % xy;
259 return Coord3{
260 static_cast<std::int64_t>(rest % chunk.x),
261 static_cast<std::int64_t>(rest / chunk.x),
262 static_cast<std::int64_t>(z),
263 };
264 }
265};
266
267} // namespace tess
Definition world.h:22
Constrains deterministic special-transition providers for a world type.
Definition transition_provider.h:43
Supplies no special transitions beyond ordinary face adjacency.
Definition transition_provider.h:75
Definition shape.h:39
Definition shape.h:67
Definition shape.h:30
Definition shape.h:59
Definition shape.h:244
Definition shape.h:225
Definition residency.h:17
Emits bidirectional stair transitions encoded by an integral world field.
Definition transition_provider.h:114