tess 0.4.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
entt_adapter.h
1#pragma once
2
3#include <tess/ecs/adapter.h>
4
5#if defined(TESS_ENABLE_ENTT)
6
7#ifndef ENTT_VERSION
8#error \
9 "Include <entt/entity/registry.hpp> before <tess/ecs/entt/entt_adapter.h>"
10#endif
11
12#include <algorithm>
13#include <cstddef>
14#include <cstdint>
15#include <type_traits>
16#include <vector>
17
18// The EnTT adapter (M10): concrete PathAgentSource/PathAgentSink types
19// over an entt::registry, explicit lifecycle intents (spawn / despawn /
20// teleport / park / place -- raw registry.destroy on an on-board agent
21// leaks a permanently blocked tile, so the intents are the only
22// sanctioned mutation paths), and tick_entt_* drivers that are thin
23// instantiations of the generic tick_ecs_* pipeline. Compiled only when
24// the consumer defines TESS_ENABLE_ENTT and includes
25// <entt/entity/registry.hpp> first; tess core never provides EnTT.
26namespace tess {
27
35 using entity_type = entt::entity;
36
37 [[nodiscard]] static auto to_handle(entt::entity entity) noexcept
38 -> EntityHandle {
39 if (entity == entt::null) {
40 return kNullEntityHandle;
41 }
42 return EntityHandle{static_cast<std::uint64_t>(entt::to_integral(entity))};
43 }
44
45 [[nodiscard]] static auto to_entity(EntityHandle handle) noexcept
46 -> entt::entity {
47 if (handle.is_null()) {
48 return entt::null;
49 }
50 return static_cast<entt::entity>(static_cast<entt::id_type>(handle.value));
51 }
52};
54
56class EnttTilePositionAdapter {
57 public:
58 explicit EnttTilePositionAdapter(entt::registry& registry) noexcept
59 : registry_(&registry) {}
60
61 [[nodiscard]] auto position(entt::entity entity) const -> Coord3 {
62 return registry_->get<TilePosition>(entity).coord;
63 }
64
65 void set_position(entt::entity entity, Coord3 coord) {
66 registry_->get<TilePosition>(entity).coord = coord;
67 }
68
69 private:
70 entt::registry* registry_;
71};
73
76 std::uint64_t agent_id = 0;
77 entt::entity entity = entt::null;
78 // Cached during the collect view walk (EnTT component addresses are
79 // stable while no PathState is added/removed, and only the entry
80 // vector is sorted in between), saving a registry lookup per agent in
81 // the fill loop (audit 2026-07-11 low).
82 PathState* state = nullptr;
83};
84
92 PathAgentTickState tick_state{};
93 PathAgentBatch batch{};
94 std::vector<EnttAgentEntry> entries{};
95 std::uint64_t next_agent_id = 0;
96
97 void reserve(std::size_t agent_capacity) {
98 batch.reserve(agent_capacity);
99 entries.reserve(agent_capacity);
100 }
101};
102
103namespace detail {
104
105template <typename World>
106[[nodiscard]] auto ecs_tile_resolves(const World& world, Coord3 tile) noexcept
107 -> bool {
108 const auto resolved = world.try_resolve(tile);
109 if (!resolved.has_value()) {
110 return false;
111 }
112 if constexpr (std::is_same_v<typename World::residency_type,
113 SparseResident>) {
114 if (!world.is_resident(resolved->chunk_key)) {
115 return false;
116 }
117 }
118 return true;
119}
120
121template <typename World>
122void ecs_mark_tile_dirty(World& world, Coord3 tile, std::uint32_t mask) {
123 if (mask != 0) {
124 world.mark_dirty(world.resolve(tile).chunk_key, mask,
125 Box3{tile, Extent3{1, 1, 1}});
126 }
127}
128
129} // namespace detail
130
138class EnttPathAgentSource {
139 public:
140 EnttPathAgentSource(entt::registry& registry,
141 EnttPathAgentContext& context) noexcept
142 : registry_(&registry), context_(&context) {}
143
144 auto collect(PathAgentBatch& batch) -> PathAgentCollectInfo {
145 auto& entries = context_->entries;
146 entries.clear();
147 batch.clear();
148
149 auto view = registry_->view<PathState, TilePosition, AgentId>(
150 entt::exclude<OffBoard>);
151 for (const auto entity : view) {
152 entries.push_back(EnttAgentEntry{view.template get<AgentId>(entity).value,
153 entity,
154 &view.template get<PathState>(entity)});
155 }
156 std::sort(entries.begin(), entries.end(),
157 [](const EnttAgentEntry& lhs, const EnttAgentEntry& rhs) {
158 return lhs.agent_id < rhs.agent_id;
159 });
160
162 info.count = entries.size();
163 // Fill the batch strictly in sorted-entry order so batch index i and
164 // entries[i] stay one agent for the sink's write-back.
165 for (const auto& entry : entries) {
166 auto& state = *entry.state;
167 if (const auto* goal = registry_->try_get<PathGoal>(entry.entity)) {
168 if (!state.agent.has_goal || state.agent.goal != goal->coord) {
169 set_path_agent_goal(state.agent, goal->coord);
170 info.pathing_dirty = true;
171 }
172 } else if (state.agent.has_goal) {
173 clear_path_agent_goal(state.agent);
174 }
175 batch.push(EnttHandleAdapter::to_handle(entry.entity), state.agent);
176 }
177 return info;
178 }
179
180 private:
181 entt::registry* registry_;
182 EnttPathAgentContext* context_;
183};
185
186template <typename Position = EnttTilePositionAdapter>
194class EnttPathAgentSink {
195 public:
196 EnttPathAgentSink(entt::registry& registry, EnttPathAgentContext& context,
197 Position position) noexcept
198 : registry_(&registry),
199 context_(&context),
200 position_(static_cast<Position&&>(position)) {}
201
202 EnttPathAgentSink(entt::registry& registry,
203 EnttPathAgentContext& context) noexcept
204 requires std::constructible_from<Position, entt::registry&>
205 : EnttPathAgentSink(registry, context, Position(registry)) {}
206
207 void apply(const PathAgentBatch& batch) {
208 const auto agents = batch.agents();
209 const auto& entries = context_->entries;
210 TESS_ASSERT(agents.size() == entries.size());
211 for (std::size_t i = 0; i < entries.size(); ++i) {
212 const auto entity = entries[i].entity;
213 const auto& agent = agents[i];
214 registry_->get<PathState>(entity).agent = agent;
215 position_.set_position(entity, agent.position);
216 if (const auto* goal = registry_->try_get<PathGoal>(entity);
217 goal != nullptr && !agent.has_goal && agent.position == goal->coord) {
218 registry_->remove<PathGoal>(entity);
219 }
220 }
221 }
222
223 private:
224 entt::registry* registry_;
225 EnttPathAgentContext* context_;
226 Position position_;
227};
229
236template <typename World, typename OccupancyTag>
237[[nodiscard]] auto spawn_entt_path_agent(
238 entt::registry& registry, EnttPathAgentContext& context, World& world,
239 TileOccupancyIndex& index, Coord3 position, std::uint32_t dirty_mask = 0,
240 DeltaCollector* render_deltas = nullptr) -> entt::entity {
241 if (!detail::ecs_tile_resolves(world, position) ||
242 static_cast<bool>(world.template field<OccupancyTag>(position)) ||
243 !index.entity_at(position).is_null()) {
244 return entt::null;
245 }
246 const auto entity = registry.create();
247 registry.emplace<AgentId>(entity, AgentId{context.next_agent_id++});
248 registry.emplace<TilePosition>(entity, TilePosition{position});
249 auto state = PathAgentState{};
250 state.position = position;
251 registry.emplace<PathState>(entity, PathState{state});
252 world.template field<OccupancyTag>(position) = true;
253 const auto inserted =
254 index.insert(position, EnttHandleAdapter::to_handle(entity));
255 TESS_ASSERT(inserted);
256 static_cast<void>(inserted);
257 detail::ecs_mark_tile_dirty(world, position, dirty_mask);
258 if (render_deltas != nullptr) {
259 render_deltas->record_spawn(EnttHandleAdapter::to_handle(entity), position);
260 }
261 return entity;
262}
263
269[[nodiscard]] inline auto spawn_entt_path_agent_off_board(
270 entt::registry& registry, EnttPathAgentContext& context) -> entt::entity {
271 const auto entity = registry.create();
272 registry.emplace<AgentId>(entity, AgentId{context.next_agent_id++});
273 registry.emplace<TilePosition>(entity, TilePosition{});
274 registry.emplace<PathState>(entity, PathState{});
275 registry.emplace<OffBoard>(entity);
276 return entity;
277}
278
285template <typename World, typename OccupancyTag>
286auto despawn_entt_path_agent(entt::registry& registry, World& world,
287 TileOccupancyIndex& index, entt::entity entity,
288 std::uint32_t dirty_mask = 0,
289 DeltaCollector* render_deltas = nullptr) -> bool {
290 const auto* state = registry.try_get<PathState>(entity);
291 if (state == nullptr) {
292 return false;
293 }
294 if (!registry.all_of<OffBoard>(entity)) {
295 const auto position = state->agent.position;
296 world.template field<OccupancyTag>(position) = false;
297 const auto erased = index.erase(position);
298 TESS_ASSERT(erased == EnttHandleAdapter::to_handle(entity));
299 static_cast<void>(erased);
300 detail::ecs_mark_tile_dirty(world, position, dirty_mask);
301 if (render_deltas != nullptr) {
302 // A parked despawn records nothing: parking already released the
303 // tile and recorded it.
304 render_deltas->record_despawn(EnttHandleAdapter::to_handle(entity),
305 position);
306 }
307 }
308 registry.destroy(entity);
309 return true;
310}
311
318template <typename World, typename OccupancyTag>
319auto teleport_entt_path_agent(entt::registry& registry, World& world,
320 TileOccupancyIndex& index, entt::entity entity,
321 Coord3 to, std::uint32_t dirty_mask = 0,
322 DeltaCollector* render_deltas = nullptr) -> bool {
323 auto* state = registry.try_get<PathState>(entity);
324 if (state == nullptr || registry.all_of<OffBoard>(entity)) {
325 return false;
326 }
327 if (!detail::ecs_tile_resolves(world, to) ||
328 static_cast<bool>(world.template field<OccupancyTag>(to)) ||
329 !index.entity_at(to).is_null()) {
330 return false;
331 }
332 const auto from = state->agent.position;
333 world.template field<OccupancyTag>(from) = false;
334 world.template field<OccupancyTag>(to) = true;
335 index.move(from, to, EnttHandleAdapter::to_handle(entity));
336 auto fresh = PathAgentState{};
337 fresh.position = to;
338 state->agent = fresh;
339 registry.get<TilePosition>(entity).coord = to;
340 detail::ecs_mark_tile_dirty(world, from, dirty_mask);
341 detail::ecs_mark_tile_dirty(world, to, dirty_mask);
342 if (render_deltas != nullptr) {
343 render_deltas->record_teleport(EnttHandleAdapter::to_handle(entity), from,
344 to);
345 }
346 return true;
347}
348
355template <typename World, typename OccupancyTag>
356auto park_entt_path_agent(entt::registry& registry, World& world,
357 TileOccupancyIndex& index, entt::entity entity,
358 std::uint32_t dirty_mask = 0,
359 DeltaCollector* render_deltas = nullptr) -> bool {
360 auto* state = registry.try_get<PathState>(entity);
361 if (state == nullptr || registry.all_of<OffBoard>(entity)) {
362 return false;
363 }
364 const auto position = state->agent.position;
365 world.template field<OccupancyTag>(position) = false;
366 const auto erased = index.erase(position);
367 TESS_ASSERT(erased == EnttHandleAdapter::to_handle(entity));
368 static_cast<void>(erased);
369 clear_path_agent_goal(state->agent);
370 registry.emplace<OffBoard>(entity);
371 detail::ecs_mark_tile_dirty(world, position, dirty_mask);
372 if (render_deltas != nullptr) {
373 render_deltas->record_park(EnttHandleAdapter::to_handle(entity), position);
374 }
375 return true;
376}
377
384template <typename World, typename OccupancyTag>
385auto place_entt_path_agent(entt::registry& registry, World& world,
386 TileOccupancyIndex& index, entt::entity entity,
387 Coord3 position, std::uint32_t dirty_mask = 0,
388 DeltaCollector* render_deltas = nullptr) -> bool {
389 auto* state = registry.try_get<PathState>(entity);
390 if (state == nullptr || !registry.all_of<OffBoard>(entity)) {
391 return false;
392 }
393 if (!detail::ecs_tile_resolves(world, position) ||
394 static_cast<bool>(world.template field<OccupancyTag>(position)) ||
395 !index.entity_at(position).is_null()) {
396 return false;
397 }
398 world.template field<OccupancyTag>(position) = true;
399 const auto inserted =
400 index.insert(position, EnttHandleAdapter::to_handle(entity));
401 TESS_ASSERT(inserted);
402 static_cast<void>(inserted);
403 auto fresh = PathAgentState{};
404 fresh.position = position;
405 state->agent = fresh;
406 registry.get<TilePosition>(entity).coord = position;
407 registry.remove<OffBoard>(entity);
408 detail::ecs_mark_tile_dirty(world, position, dirty_mask);
409 if (render_deltas != nullptr) {
410 render_deltas->record_place(EnttHandleAdapter::to_handle(entity), position);
411 }
412 return true;
413}
414
416inline void set_entt_path_agent_goal(entt::registry& registry,
417 entt::entity entity, Coord3 goal) {
418 registry.emplace_or_replace<PathGoal>(entity, PathGoal{goal});
419}
420
422inline void clear_entt_path_agent_goal(entt::registry& registry,
423 entt::entity entity) {
424 registry.remove<PathGoal>(entity);
425}
426
427template <typename World, typename ClassOrTag, typename OccupancyTag,
428 typename ReservationTag, typename Position = EnttTilePositionAdapter>
435[[nodiscard]] auto tick_entt_unit_path_agents(
436 entt::registry& registry, EnttPathAgentContext& context, World& world,
437 PathRequestRuntime& runtime, TileOccupancyIndex& index,
438 PathAgentTickOptions options = {}, std::uint32_t movement_dirty_mask = 0,
439 const RegionGraphT<typename World::residency_type>* graph = nullptr,
440 DeltaCollector* render_deltas = nullptr) -> PathAgentTickStats {
441 EnttPathAgentSource source(registry, context);
442 EnttPathAgentSink<Position> sink(registry, context);
443 return tick_ecs_unit_path_agents<World, ClassOrTag, OccupancyTag,
444 ReservationTag>(
445 context.tick_state, world, source, sink, context.batch, runtime, index,
446 options, movement_dirty_mask, graph, render_deltas);
447}
448
449template <typename World, typename Class, std::uint32_t MaxCost,
450 typename OccupancyTag, typename ReservationTag,
451 typename Position = EnttTilePositionAdapter>
453[[nodiscard]] auto tick_entt_path_agents(
454 entt::registry& registry, EnttPathAgentContext& context, World& world,
455 PathRequestRuntime& runtime, TileOccupancyIndex& index,
456 PathAgentTickOptions options = {}, std::uint32_t movement_dirty_mask = 0,
457 const RegionGraphT<typename World::residency_type>* graph = nullptr,
458 DeltaCollector* render_deltas = nullptr) -> PathAgentTickStats {
459 EnttPathAgentSource source(registry, context);
460 EnttPathAgentSink<Position> sink(registry, context);
461 return tick_ecs_path_agents<World, Class, MaxCost, OccupancyTag,
462 ReservationTag>(
463 context.tick_state, world, source, sink, context.batch, runtime, index,
464 options, movement_dirty_mask, graph, render_deltas);
465}
466
467template <typename World, typename PassableTag, typename CostTag,
468 std::uint32_t MaxCost, typename OccupancyTag, typename ReservationTag,
469 typename Position = EnttTilePositionAdapter>
471[[nodiscard]] auto tick_entt_path_agents(
472 entt::registry& registry, EnttPathAgentContext& context, World& world,
473 PathRequestRuntime& runtime, TileOccupancyIndex& index,
474 PathAgentTickOptions options = {}, std::uint32_t movement_dirty_mask = 0,
475 const RegionGraphT<typename World::residency_type>* graph = nullptr,
476 DeltaCollector* render_deltas = nullptr) -> PathAgentTickStats {
477 EnttPathAgentSource source(registry, context);
478 EnttPathAgentSink<Position> sink(registry, context);
479 return tick_ecs_path_agents<World, PassableTag, CostTag, MaxCost,
480 OccupancyTag, ReservationTag>(
481 context.tick_state, world, source, sink, context.batch, runtime, index,
482 options, movement_dirty_mask, graph, render_deltas);
483}
484
485} // namespace tess
486
487#endif // TESS_ENABLE_ENTT
Definition delta_frame.h:226
Definition adapter.h:69
Definition adapter.h:177
Definition world.h:22
Definition adapter.h:36
Definition adapter.h:126
Definition adapter.h:115
Definition adapter.h:47
Definition adapter.h:139
Definition shape.h:30
Definition entity_handle.h:16
Definition entt_adapter.h:75
Definition entt_adapter.h:34
Definition entt_adapter.h:91
Definition adapter.h:54
Owns the shared clock, world-dirty flag, and retained routes across ticks.
Definition path_agent_tick.h:13
Definition adapter.h:149
Definition adapter.h:159
Definition adapter.h:144