tess 1.0.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: 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 impassable 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.
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, DirtyMask mask) {
123 if (mask) {
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, DirtyMask dirty_mask = {},
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 // Preflight the Tess-owned index allocation before beginning lifecycle
247 // mutation. insert() cannot then fail from allocation after the ECS and
248 // world stores have changed; ECS pool exception semantics remain EnTT's.
249 index.reserve(index.size() + 1);
250 const auto entity = registry.create();
251 registry.emplace<AgentId>(entity, AgentId{context.next_agent_id++});
252 registry.emplace<TilePosition>(entity, TilePosition{position});
253 auto state = PathAgentState{};
254 state.position = position;
255 registry.emplace<PathState>(entity, PathState{state});
256 world.template field<OccupancyTag>(position) = true;
257 const auto inserted =
258 index.insert(position, EnttHandleAdapter::to_handle(entity));
259 TESS_ASSERT(inserted);
260 static_cast<void>(inserted);
261 detail::ecs_mark_tile_dirty(world, position, dirty_mask);
262 if (render_deltas != nullptr) {
263 render_deltas->record_spawn(EnttHandleAdapter::to_handle(entity), position);
264 }
265 return entity;
266}
267
273[[nodiscard]] inline auto spawn_entt_path_agent_off_board(
274 entt::registry& registry, EnttPathAgentContext& context) -> entt::entity {
275 const auto entity = registry.create();
276 registry.emplace<AgentId>(entity, AgentId{context.next_agent_id++});
277 registry.emplace<TilePosition>(entity, TilePosition{});
278 registry.emplace<PathState>(entity, PathState{});
279 registry.emplace<OffBoard>(entity);
280 return entity;
281}
282
289template <typename World, typename OccupancyTag>
290auto despawn_entt_path_agent(entt::registry& registry, World& world,
291 TileOccupancyIndex& index, entt::entity entity,
292 DirtyMask dirty_mask = {},
293 DeltaCollector* render_deltas = nullptr) -> bool {
294 const auto* state = registry.try_get<PathState>(entity);
295 if (state == nullptr) {
296 return false;
297 }
298 if (!registry.all_of<OffBoard>(entity)) {
299 const auto position = state->agent.position;
300 world.template field<OccupancyTag>(position) = false;
301 const auto erased = index.erase(position);
302 TESS_ASSERT(erased == EnttHandleAdapter::to_handle(entity));
303 static_cast<void>(erased);
304 detail::ecs_mark_tile_dirty(world, position, dirty_mask);
305 if (render_deltas != nullptr) {
306 // A parked despawn records nothing: parking already released the
307 // tile and recorded it.
308 render_deltas->record_despawn(EnttHandleAdapter::to_handle(entity),
309 position);
310 }
311 }
312 registry.destroy(entity);
313 return true;
314}
315
322template <typename World, typename OccupancyTag>
323auto teleport_entt_path_agent(entt::registry& registry, World& world,
324 TileOccupancyIndex& index, entt::entity entity,
325 Coord3 to, DirtyMask dirty_mask = {},
326 DeltaCollector* render_deltas = nullptr) -> bool {
327 auto* state = registry.try_get<PathState>(entity);
328 if (state == nullptr || registry.all_of<OffBoard>(entity)) {
329 return false;
330 }
331 if (!detail::ecs_tile_resolves(world, to) ||
332 static_cast<bool>(world.template field<OccupancyTag>(to)) ||
333 !index.entity_at(to).is_null()) {
334 return false;
335 }
336 const auto from = state->agent.position;
337 world.template field<OccupancyTag>(from) = false;
338 world.template field<OccupancyTag>(to) = true;
339 index.move(from, to, EnttHandleAdapter::to_handle(entity));
340 auto fresh = PathAgentState{};
341 fresh.position = to;
342 state->agent = fresh;
343 registry.get<TilePosition>(entity).coord = to;
344 detail::ecs_mark_tile_dirty(world, from, dirty_mask);
345 detail::ecs_mark_tile_dirty(world, to, dirty_mask);
346 if (render_deltas != nullptr) {
347 render_deltas->record_teleport(EnttHandleAdapter::to_handle(entity), from,
348 to);
349 }
350 return true;
351}
352
359template <typename World, typename OccupancyTag>
360auto park_entt_path_agent(entt::registry& registry, World& world,
361 TileOccupancyIndex& index, entt::entity entity,
362 DirtyMask dirty_mask = {},
363 DeltaCollector* render_deltas = nullptr) -> bool {
364 auto* state = registry.try_get<PathState>(entity);
365 if (state == nullptr || registry.all_of<OffBoard>(entity)) {
366 return false;
367 }
368 const auto position = state->agent.position;
369 world.template field<OccupancyTag>(position) = false;
370 const auto erased = index.erase(position);
371 TESS_ASSERT(erased == EnttHandleAdapter::to_handle(entity));
372 static_cast<void>(erased);
373 clear_path_agent_goal(state->agent);
374 registry.emplace<OffBoard>(entity);
375 detail::ecs_mark_tile_dirty(world, position, dirty_mask);
376 if (render_deltas != nullptr) {
377 render_deltas->record_park(EnttHandleAdapter::to_handle(entity), position);
378 }
379 return true;
380}
381
388template <typename World, typename OccupancyTag>
389auto place_entt_path_agent(entt::registry& registry, World& world,
390 TileOccupancyIndex& index, entt::entity entity,
391 Coord3 position, DirtyMask dirty_mask = {},
392 DeltaCollector* render_deltas = nullptr) -> bool {
393 auto* state = registry.try_get<PathState>(entity);
394 if (state == nullptr || !registry.all_of<OffBoard>(entity)) {
395 return false;
396 }
397 if (!detail::ecs_tile_resolves(world, position) ||
398 static_cast<bool>(world.template field<OccupancyTag>(position)) ||
399 !index.entity_at(position).is_null()) {
400 return false;
401 }
402 // Keep an allocation failure on the parked side of the commit boundary.
403 index.reserve(index.size() + 1);
404 world.template field<OccupancyTag>(position) = true;
405 const auto inserted =
406 index.insert(position, EnttHandleAdapter::to_handle(entity));
407 TESS_ASSERT(inserted);
408 static_cast<void>(inserted);
409 auto fresh = PathAgentState{};
410 fresh.position = position;
411 state->agent = fresh;
412 registry.get<TilePosition>(entity).coord = position;
413 registry.remove<OffBoard>(entity);
414 detail::ecs_mark_tile_dirty(world, position, dirty_mask);
415 if (render_deltas != nullptr) {
416 render_deltas->record_place(EnttHandleAdapter::to_handle(entity), position);
417 }
418 return true;
419}
420
422inline void set_entt_path_agent_goal(entt::registry& registry,
423 entt::entity entity, Coord3 goal) {
424 registry.emplace_or_replace<PathGoal>(entity, PathGoal{goal});
425}
426
428inline void clear_entt_path_agent_goal(entt::registry& registry,
429 entt::entity entity) {
430 registry.remove<PathGoal>(entity);
431}
432
433template <typename World, typename ClassOrTag, typename OccupancyTag,
434 typename ReservationTag, typename Position = EnttTilePositionAdapter>
441[[nodiscard]] auto tick_entt_unit_path_agents(
442 entt::registry& registry, EnttPathAgentContext& context, World& world,
443 PathRequestRuntime& runtime, TileOccupancyIndex& index,
444 PathAgentTickOptions options = {},
445 const RegionGraphT<typename World::residency_type>* graph = nullptr,
446 DeltaCollector* render_deltas = nullptr) -> PathAgentTickStats {
447 EnttPathAgentSource source(registry, context);
448 EnttPathAgentSink<Position> sink(registry, context);
449 return tick_ecs_unit_path_agents<World, ClassOrTag, OccupancyTag,
450 ReservationTag>(
451 context.tick_state, world, source, sink, context.batch, runtime, index,
452 options, graph, render_deltas);
453}
454
455template <typename World, typename Class, std::uint32_t MaxCost,
456 typename OccupancyTag, typename ReservationTag,
457 typename Position = EnttTilePositionAdapter>
459[[nodiscard]] auto tick_entt_path_agents(
460 entt::registry& registry, EnttPathAgentContext& context, World& world,
461 PathRequestRuntime& runtime, TileOccupancyIndex& index,
462 PathAgentTickOptions options = {},
463 const RegionGraphT<typename World::residency_type>* graph = nullptr,
464 DeltaCollector* render_deltas = nullptr) -> PathAgentTickStats {
465 EnttPathAgentSource source(registry, context);
466 EnttPathAgentSink<Position> sink(registry, context);
467 return tick_ecs_path_agents<World, Class, MaxCost, OccupancyTag,
468 ReservationTag>(
469 context.tick_state, world, source, sink, context.batch, runtime, index,
470 options, graph, render_deltas);
471}
472
473} // namespace tess
474
475#endif // TESS_ENABLE_ENTT
Definition adapter.h:70
Definition adapter.h:178
Definition world.h:22
Definition adapter.h:37
Definition adapter.h:127
Definition adapter.h:116
Definition adapter.h:48
Definition adapter.h:140
Definition shape.h:46
Definition metadata_types.h:12
Definition entity_handle.h:16
Definition entt_adapter.h:75
Definition entt_adapter.h:34
Definition entt_adapter.h:91
Definition adapter.h:55
Definition path_agent_tick.h:17
Definition adapter.h:150
Definition adapter.h:160
Definition adapter.h:145