tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
flecs_adapter.h
1#pragma once
2
3#include <tess/ecs/adapter.h>
4
5#if defined(TESS_ENABLE_FLECS)
6
7#ifndef FLECS_H
8#error "Include <flecs.h> before <tess/ecs/flecs/flecs_adapter.h>"
9#endif
10
11#if FLECS_VERSION_MAJOR < 4 || \
12 (FLECS_VERSION_MAJOR == 4 && FLECS_VERSION_MINOR < 1) || \
13 (FLECS_VERSION_MAJOR == 4 && FLECS_VERSION_MINOR == 1 && \
14 FLECS_VERSION_PATCH < 5)
15#error "The tess Flecs adapter requires Flecs 4.1.5 or newer"
16#endif
17
18#include <algorithm>
19#include <cstddef>
20#include <cstdint>
21#include <type_traits>
22#include <vector>
23
24// The Flecs adapter: concrete PathAgentSource/PathAgentSink types
25// over a flecs::world, explicit lifecycle intents (spawn / despawn /
26// teleport / park / place -- raw entity destruction on an on-board agent
27// leaks a permanently impassable tile, so the intents are the only
28// sanctioned mutation paths), and tick_flecs_* drivers that are thin
29// instantiations of the generic tick_ecs_* pipeline. Compiled only when
30// the consumer defines TESS_ENABLE_FLECS and includes
31// <flecs.h> first; tess core never provides Flecs.
32namespace tess {
33
42 using entity_type = flecs::entity_t;
43
44 [[nodiscard]] static auto to_handle(flecs::entity_t entity) noexcept
45 -> EntityHandle {
46 if (entity == 0u) {
47 return kNullEntityHandle;
48 }
49 TESS_ASSERT_MSG(entity != kNullEntityHandle.value,
50 "the all-ones Flecs ID is reserved for tess null");
51 return EntityHandle{entity};
52 }
53
54 [[nodiscard]] static auto to_entity(EntityHandle handle) noexcept
55 -> flecs::entity_t {
56 return handle.is_null() ? 0u : handle.value;
57 }
58};
60
62class FlecsTilePositionAdapter {
63 public:
64 explicit FlecsTilePositionAdapter(flecs::world& world) noexcept
65 : world_(&world) {}
66
67 [[nodiscard]] auto position(flecs::entity_t entity) const -> Coord3 {
68 return world_->entity(entity).get<TilePosition>().coord;
69 }
70
71 void set_position(flecs::entity_t entity, Coord3 coord) {
72 world_->entity(entity).set<TilePosition>(TilePosition{coord});
73 }
74
75 private:
76 flecs::world* world_;
77};
79
82 std::uint64_t agent_id = 0;
83 flecs::entity_t entity = 0u;
84 // Cached during query iteration. Flecs component addresses remain stable
85 // because collection performs no structural mutation before fill ends.
86 PathState* state = nullptr;
87};
88
95struct FlecsPathAgentContext {
96 // Flecs copies each temporary term into builder-owned storage, but the
97 // analyzer follows its address through the upstream fluent builder and
98 // reports an escape at downstream constructor call sites.
99 // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
100 explicit FlecsPathAgentContext(flecs::world& world)
101 : world_identity(world.c_ptr()),
102 agent_query(
103 world.query_builder<PathState, const TilePosition, const AgentId>()
104 .build()) {}
105 // NOLINTEND(clang-analyzer-core.StackAddressEscape)
106
107 PathAgentTickState tick_state{};
108 PathAgentBatch batch{};
109 std::vector<FlecsAgentEntry> entries{};
110 std::uint64_t next_agent_id = 0;
111 // The query is owned by this exact world; equal component registrations in
112 // another world do not make its entity IDs or table storage interchangeable.
113 flecs::world_t* world_identity = nullptr;
114 flecs::query<PathState, const TilePosition, const AgentId> agent_query;
115
116 void reserve(std::size_t agent_capacity) {
117 batch.reserve(agent_capacity);
118 entries.reserve(agent_capacity);
119 }
120};
121
122namespace detail {
123
124template <typename World>
125[[nodiscard]] auto flecs_tile_resolves(const World& world, Coord3 tile) noexcept
126 -> bool {
127 const auto resolved = world.try_resolve(tile);
128 if (!resolved.has_value()) {
129 return false;
130 }
131 if constexpr (std::is_same_v<typename World::residency_type,
132 SparseResident>) {
133 if (!world.is_resident(resolved->chunk_key)) {
134 return false;
135 }
136 }
137 return true;
138}
139
140template <typename World>
141void flecs_mark_tile_dirty(World& world, Coord3 tile, DirtyMask mask) {
142 if (mask) {
143 world.mark_dirty(world.resolve(tile).chunk_key, mask,
144 Box3{tile, Extent3{1, 1, 1}});
145 }
146}
147
148inline void flecs_assert_immediate_lifecycle(
149 [[maybe_unused]] const flecs::world& world) noexcept {
150 // Flecs would queue structural commands inside a deferred scope while tess
151 // changes occupancy immediately. Refuse that split commit boundary.
152 TESS_ASSERT_MSG(!world.is_deferred() && !world.is_readonly(),
153 "Flecs lifecycle intents require immediate mode");
154}
155
156inline void flecs_assert_context_world(
157 [[maybe_unused]] const flecs::world& world,
158 [[maybe_unused]] const FlecsPathAgentContext& context) noexcept {
159 TESS_ASSERT_MSG(context.world_identity == world.c_ptr(),
160 "FlecsPathAgentContext must use the same Flecs world");
161}
162
163} // namespace detail
164
172class FlecsPathAgentSource {
173 public:
174 FlecsPathAgentSource(flecs::world& world,
175 FlecsPathAgentContext& context) noexcept
176 : world_(&world), context_(&context) {}
177
178 auto collect(PathAgentBatch& batch) -> PathAgentCollectInfo {
179 detail::flecs_assert_immediate_lifecycle(*world_);
180 detail::flecs_assert_context_world(*world_, *context_);
181 auto& entries = context_->entries;
182 entries.clear();
183 batch.clear();
184
185 context_->agent_query.each([&](flecs::entity entity, PathState& state,
186 const TilePosition&,
187 const AgentId& agent_id) {
188 // A negated builder term would avoid this check, but Flecs 4.1.5's
189 // fluent `without<T>()` produces StackAddressEscape false positives at
190 // every downstream constructor call under the required Clang analyzer
191 // gate. Keep the non-mutating filter inside the safe query phase until
192 // that upstream builder is analyzer-clean.
193 if (!entity.has<OffBoard>()) {
194 entries.push_back(FlecsAgentEntry{agent_id.value, entity.id(), &state});
195 }
196 });
197 std::sort(entries.begin(), entries.end(),
198 [](const FlecsAgentEntry& lhs, const FlecsAgentEntry& rhs) {
199 return lhs.agent_id < rhs.agent_id;
200 });
201
203 info.count = entries.size();
204 // Fill the batch strictly in sorted-entry order so batch index i and
205 // entries[i] stay one agent for the sink's write-back.
206 for (const auto& entry : entries) {
207 auto& state = *entry.state;
208 if (const auto* goal = world_->entity(entry.entity).try_get<PathGoal>()) {
209 if (!state.agent.has_goal || state.agent.goal != goal->coord) {
210 set_path_agent_goal(state.agent, goal->coord);
211 info.pathing_dirty = true;
212 }
213 } else if (state.agent.has_goal) {
214 clear_path_agent_goal(state.agent);
215 }
216 batch.push(FlecsHandleAdapter::to_handle(entry.entity), state.agent);
217 }
218 return info;
219 }
220
221 private:
222 flecs::world* world_;
223 FlecsPathAgentContext* context_;
224};
226
227template <typename Position = FlecsTilePositionAdapter>
235class FlecsPathAgentSink {
236 public:
237 FlecsPathAgentSink(flecs::world& world, FlecsPathAgentContext& context,
238 Position position) noexcept
239 : world_(&world),
240 context_(&context),
241 position_(static_cast<Position&&>(position)) {}
242
243 FlecsPathAgentSink(flecs::world& world,
244 FlecsPathAgentContext& context) noexcept
245 requires std::constructible_from<Position, flecs::world&>
246 : FlecsPathAgentSink(world, context, Position(world)) {}
247
248 void apply(const PathAgentBatch& batch) {
249 detail::flecs_assert_immediate_lifecycle(*world_);
250 detail::flecs_assert_context_world(*world_, *context_);
251 const auto agents = batch.agents();
252 const auto& entries = context_->entries;
253 TESS_ASSERT(agents.size() == entries.size());
254 for (std::size_t i = 0; i < entries.size(); ++i) {
255 const auto entity = entries[i].entity;
256 const auto& agent = agents[i];
257 auto flecs_entity = world_->entity(entity);
258 flecs_entity.set<PathState>(PathState{agent});
259 position_.set_position(entity, agent.position);
260 if (const auto* goal = flecs_entity.try_get<PathGoal>();
261 goal != nullptr && !agent.has_goal && agent.position == goal->coord) {
262 flecs_entity.remove<PathGoal>();
263 }
264 }
265 }
266
267 private:
268 flecs::world* world_;
269 FlecsPathAgentContext* context_;
270 Position position_;
271};
273
280template <typename World, typename OccupancyTag>
281[[nodiscard]] auto spawn_flecs_path_agent(
282 flecs::world& ecs, FlecsPathAgentContext& context, World& world,
283 TileOccupancyIndex& index, Coord3 position, DirtyMask dirty_mask = {},
284 DeltaCollector* render_deltas = nullptr) -> flecs::entity_t {
285 detail::flecs_assert_immediate_lifecycle(ecs);
286 detail::flecs_assert_context_world(ecs, context);
287 if (!detail::flecs_tile_resolves(world, position) ||
288 static_cast<bool>(world.template field<OccupancyTag>(position)) ||
289 !index.entity_at(position).is_null()) {
290 return 0u;
291 }
292 // Grow the only tess-owned allocating structure before creating an entity,
293 // advancing AgentId, or claiming the world tile. insert() cannot allocate
294 // after this preflight, so allocation failure is a semantic no-op.
295 index.reserve(index.size() + 1);
296 auto state = PathAgentState{};
297 state.position = position;
298 const auto flecs_entity = ecs.entity()
299 .set<AgentId>(AgentId{context.next_agent_id++})
300 .set<TilePosition>(TilePosition{position})
301 .set<PathState>(PathState{state});
302 const auto entity = flecs_entity.id();
303 world.template field<OccupancyTag>(position) = true;
304 const auto inserted =
305 index.insert(position, FlecsHandleAdapter::to_handle(entity));
306 TESS_ASSERT(inserted);
307 static_cast<void>(inserted);
308 detail::flecs_mark_tile_dirty(world, position, dirty_mask);
309 if (render_deltas != nullptr) {
310 render_deltas->record_spawn(FlecsHandleAdapter::to_handle(entity),
311 position);
312 }
313 return entity;
314}
315
321[[nodiscard]] inline auto spawn_flecs_path_agent_off_board(
322 flecs::world& world, FlecsPathAgentContext& context) -> flecs::entity_t {
323 detail::flecs_assert_immediate_lifecycle(world);
324 detail::flecs_assert_context_world(world, context);
325 return world.entity()
326 .set<AgentId>(AgentId{context.next_agent_id++})
327 .set<TilePosition>(TilePosition{})
328 .set<PathState>(PathState{})
329 .add<OffBoard>()
330 .id();
331}
332
339template <typename World, typename OccupancyTag>
340auto despawn_flecs_path_agent(flecs::world& ecs, World& world,
341 TileOccupancyIndex& index, flecs::entity_t entity,
342 DirtyMask dirty_mask = {},
343 DeltaCollector* render_deltas = nullptr) -> bool {
344 detail::flecs_assert_immediate_lifecycle(ecs);
345 if (!ecs.is_alive(entity)) {
346 return false;
347 }
348 auto flecs_entity = ecs.entity(entity);
349 const auto* state = flecs_entity.try_get<PathState>();
350 if (state == nullptr) {
351 return false;
352 }
353 if (!flecs_entity.has<OffBoard>()) {
354 const auto position = state->agent.position;
355 world.template field<OccupancyTag>(position) = false;
356 const auto erased = index.erase(position);
357 TESS_ASSERT(erased == FlecsHandleAdapter::to_handle(entity));
358 static_cast<void>(erased);
359 detail::flecs_mark_tile_dirty(world, position, dirty_mask);
360 if (render_deltas != nullptr) {
361 // This branch is on-board, so despawn must emit the release now.
362 // Parked entities skip the whole block because parking already emitted
363 // the release.
364 render_deltas->record_despawn(FlecsHandleAdapter::to_handle(entity),
365 position);
366 }
367 }
368 flecs_entity.destruct();
369 return true;
370}
371
378template <typename World, typename OccupancyTag>
379auto teleport_flecs_path_agent(flecs::world& ecs, World& world,
380 TileOccupancyIndex& index,
381 flecs::entity_t entity, Coord3 to,
382 DirtyMask dirty_mask = {},
383 DeltaCollector* render_deltas = nullptr)
384 -> bool {
385 detail::flecs_assert_immediate_lifecycle(ecs);
386 if (!ecs.is_alive(entity)) {
387 return false;
388 }
389 auto flecs_entity = ecs.entity(entity);
390 auto* state = flecs_entity.try_get_mut<PathState>();
391 if (state == nullptr || flecs_entity.has<OffBoard>()) {
392 return false;
393 }
394 if (!detail::flecs_tile_resolves(world, to) ||
395 static_cast<bool>(world.template field<OccupancyTag>(to)) ||
396 !index.entity_at(to).is_null()) {
397 return false;
398 }
399 const auto from = state->agent.position;
400 world.template field<OccupancyTag>(from) = false;
401 world.template field<OccupancyTag>(to) = true;
402 index.move(from, to, FlecsHandleAdapter::to_handle(entity));
403 auto fresh = PathAgentState{};
404 fresh.position = to;
405 flecs_entity.set<PathState>(PathState{fresh});
406 flecs_entity.set<TilePosition>(TilePosition{to});
407 detail::flecs_mark_tile_dirty(world, from, dirty_mask);
408 detail::flecs_mark_tile_dirty(world, to, dirty_mask);
409 if (render_deltas != nullptr) {
410 render_deltas->record_teleport(FlecsHandleAdapter::to_handle(entity), from,
411 to);
412 }
413 return true;
414}
415
422template <typename World, typename OccupancyTag>
423auto park_flecs_path_agent(flecs::world& ecs, World& world,
424 TileOccupancyIndex& index, flecs::entity_t entity,
425 DirtyMask dirty_mask = {},
426 DeltaCollector* render_deltas = nullptr) -> bool {
427 detail::flecs_assert_immediate_lifecycle(ecs);
428 if (!ecs.is_alive(entity)) {
429 return false;
430 }
431 auto flecs_entity = ecs.entity(entity);
432 auto* state = flecs_entity.try_get_mut<PathState>();
433 if (state == nullptr || flecs_entity.has<OffBoard>()) {
434 return false;
435 }
436 const auto position = state->agent.position;
437 world.template field<OccupancyTag>(position) = false;
438 const auto erased = index.erase(position);
439 TESS_ASSERT(erased == FlecsHandleAdapter::to_handle(entity));
440 static_cast<void>(erased);
441 clear_path_agent_goal(state->agent);
442 flecs_entity.modified<PathState>();
443 flecs_entity.add<OffBoard>();
444 detail::flecs_mark_tile_dirty(world, position, dirty_mask);
445 if (render_deltas != nullptr) {
446 render_deltas->record_park(FlecsHandleAdapter::to_handle(entity), position);
447 }
448 return true;
449}
450
457template <typename World, typename OccupancyTag>
458auto place_flecs_path_agent(flecs::world& ecs, World& world,
459 TileOccupancyIndex& index, flecs::entity_t entity,
460 Coord3 position, DirtyMask dirty_mask = {},
461 DeltaCollector* render_deltas = nullptr) -> bool {
462 detail::flecs_assert_immediate_lifecycle(ecs);
463 if (!ecs.is_alive(entity)) {
464 return false;
465 }
466 auto flecs_entity = ecs.entity(entity);
467 auto* state = flecs_entity.try_get_mut<PathState>();
468 if (state == nullptr || !flecs_entity.has<OffBoard>()) {
469 return false;
470 }
471 if (!detail::flecs_tile_resolves(world, position) ||
472 static_cast<bool>(world.template field<OccupancyTag>(position)) ||
473 !index.entity_at(position).is_null()) {
474 return false;
475 }
476 // Preflight index growth while the entity is still parked and the world
477 // tile is still free. The remaining index insertion is non-allocating.
478 index.reserve(index.size() + 1);
479 world.template field<OccupancyTag>(position) = true;
480 const auto inserted =
481 index.insert(position, FlecsHandleAdapter::to_handle(entity));
482 TESS_ASSERT(inserted);
483 static_cast<void>(inserted);
484 auto fresh = PathAgentState{};
485 fresh.position = position;
486 flecs_entity.set<PathState>(PathState{fresh});
487 flecs_entity.set<TilePosition>(TilePosition{position});
488 flecs_entity.remove<OffBoard>();
489 detail::flecs_mark_tile_dirty(world, position, dirty_mask);
490 if (render_deltas != nullptr) {
491 render_deltas->record_place(FlecsHandleAdapter::to_handle(entity),
492 position);
493 }
494 return true;
495}
496
498inline void set_flecs_path_agent_goal(flecs::world& world,
499 flecs::entity_t entity, Coord3 goal) {
500 world.entity(entity).set<PathGoal>(PathGoal{goal});
501}
502
504inline void clear_flecs_path_agent_goal(flecs::world& world,
505 flecs::entity_t entity) {
506 world.entity(entity).remove<PathGoal>();
507}
508
509template <typename World, typename ClassOrTag, typename OccupancyTag,
510 typename ReservationTag, typename Position = FlecsTilePositionAdapter>
517[[nodiscard]] auto tick_flecs_unit_path_agents(
518 flecs::world& ecs, FlecsPathAgentContext& context, World& world,
519 PathRequestRuntime& runtime, TileOccupancyIndex& index,
520 PathAgentTickOptions options = {},
521 const RegionGraphT<typename World::residency_type>* graph = nullptr,
522 DeltaCollector* render_deltas = nullptr) -> PathAgentTickStats {
523 detail::flecs_assert_immediate_lifecycle(ecs);
524 detail::flecs_assert_context_world(ecs, context);
525 FlecsPathAgentSource source(ecs, context);
526 FlecsPathAgentSink<Position> sink(ecs, context);
527 return tick_ecs_unit_path_agents<World, ClassOrTag, OccupancyTag,
528 ReservationTag>(
529 context.tick_state, world, source, sink, context.batch, runtime, index,
530 options, graph, render_deltas);
531}
532
533template <typename World, typename Class, std::uint32_t MaxCost,
534 typename OccupancyTag, typename ReservationTag,
535 typename Position = FlecsTilePositionAdapter>
537[[nodiscard]] auto tick_flecs_path_agents(
538 flecs::world& ecs, FlecsPathAgentContext& context, World& world,
539 PathRequestRuntime& runtime, TileOccupancyIndex& index,
540 PathAgentTickOptions options = {},
541 const RegionGraphT<typename World::residency_type>* graph = nullptr,
542 DeltaCollector* render_deltas = nullptr) -> PathAgentTickStats {
543 detail::flecs_assert_immediate_lifecycle(ecs);
544 detail::flecs_assert_context_world(ecs, context);
545 FlecsPathAgentSource source(ecs, context);
546 FlecsPathAgentSink<Position> sink(ecs, context);
547 return tick_ecs_path_agents<World, Class, MaxCost, OccupancyTag,
548 ReservationTag>(
549 context.tick_state, world, source, sink, context.batch, runtime, index,
550 options, graph, render_deltas);
551}
552
553} // namespace tess
554
555#endif // TESS_ENABLE_FLECS
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 flecs_adapter.h:81
Definition flecs_adapter.h:41
Definition flecs_adapter.h:95
Definition adapter.h:169
Definition adapter.h:55
Definition path_agent_tick.h:17
Definition adapter.h:150
Definition adapter.h:160
Definition adapter.h:145