tess 0.4.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
path_agent_tick.h
1#pragma once
2
3#include <tess/sim/path_agent.h>
4#include <tess/sim/time.h>
5
6#include <cstddef>
7#include <cstdint>
8#include <span>
9
10namespace tess {
11
14 SimClock clock{};
15 // WORLD-scoped pathing dirt: set it (via mark_pathing_dirty) after any
16 // world change that can invalidate existing routes; the next tick then
17 // replans EVERY agent. Agent-scoped needs (a newly armed goal, a Blocked
18 // retry) do not set it -- those agents alone replan while Following
19 // agents keep walking their retained routes (audit/optimization-log
20 // per-agent pathing-dirty item). Starts true so the first tick plans
21 // everyone.
22 bool pathing_dirty = true;
23 // Per-agent retained routes; see PathAgentRoutes for the index-pairing
24 // contract (reorder/remove agents => mark_pathing_dirty).
25 PathAgentRoutes routes{};
26};
27
30 std::size_t max_steps = 1;
31 PathRuntimeCachePolicy cache_policy{};
32 // Budget of consecutive failed re-path attempts for a Blocked agent.
33 // Exactly one attempt is consumed per processed tick spent Blocked, by
34 // prepare_path_agent_processing; blocked movement steps do not consume
35 // budget themselves. A Found re-path result resets the count to zero
36 // (apply_path_agent_results), so only consecutive non-Found results
37 // exhaust the budget and turn the agent terminally Unreachable. An
38 // agent whose re-paths keep planning Found but whose next step stays
39 // blocked -- e.g. a permanently parked agent on the route, since
40 // occupancy is not planning passability -- therefore re-paths every
41 // tick indefinitely by design and never becomes Unreachable.
42 std::uint32_t max_blocked_retries = 8;
43};
44
47 std::uint64_t tick = 0;
48 bool processed_paths = false;
49 PathAgentFrameStats pathing{};
50 PathAgentFrameStats movement{};
51 std::size_t repaths_requested = 0;
52 std::size_t repath_exhausted = 0;
53};
54
56inline void mark_pathing_dirty(PathAgentTickState& state) noexcept {
57 state.pathing_dirty = true;
58}
59
60// Arms a goal WITHOUT touching the world-scoped dirty flag: the agent
61// enters NeedsPath, which the next tick picks up as an agent-scoped
62// (NeedsOnly) processing pass. Before the per-agent split this marked the
63// shared flag and one new goal replanned the whole batch every tick
64// (optimization-log 2026-07-11, S11.4 soak observation).
66inline void set_path_agent_goal(PathAgentTickState& state,
67 PathAgentState& agent, Coord3 goal) noexcept {
68 static_cast<void>(state);
69 set_path_agent_goal(agent, goal);
70}
71
72// Scans agents ahead of a tick's path processing. NeedsPath agents (goals
73// assigned through the two-argument set_path_agent_goal) request processing
74// with no manual dirty mark. Blocked agents consume one re-path attempt per
75// processed tick until the retry budget runs out, at which point they turn
76// terminally Unreachable and stop requesting processing.
78inline auto prepare_path_agent_processing(std::span<PathAgentState> agents,
80 PathAgentTickStats& stats) noexcept
81 -> bool {
82 bool needs_processing = false;
83 for (auto& agent : agents) {
84 if (!agent.has_goal) {
85 continue;
86 }
87 if (agent.phase == PathAgentPhase::NeedsPath) {
88 needs_processing = true;
89 continue;
90 }
91 if (agent.phase != PathAgentPhase::Blocked) {
92 continue;
93 }
94 if (agent.blocked_retries < options.max_blocked_retries) {
95 ++agent.blocked_retries;
96 ++stats.repaths_requested;
97 needs_processing = true;
98 } else {
99 agent.phase = PathAgentPhase::Unreachable;
100 agent.status = PathStatus::NoPath;
101 ++stats.repath_exhausted;
102 }
103 }
104 return needs_processing;
105}
106
108template <typename World, typename ClassOrTag>
109[[nodiscard]] auto tick_unit_path_agents(
110 PathAgentTickState& state, const World& world,
111 std::span<PathAgentState> agents, PathRequestRuntime& runtime,
112 PathAgentTickOptions options = {},
115 PathAgentTickStats stats;
116 stats.tick = advance_sim_tick(state.clock);
117
118 const bool repath_needed =
119 prepare_path_agent_processing(agents, options, stats);
120 state.routes.ensure_size(agents.size());
121 if (state.pathing_dirty || repath_needed) {
122 const auto scope =
123 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
124 stats.pathing = process_unit_path_agents<World, ClassOrTag>(
125 world, agents, runtime, options.cache_policy, graph, scope,
126 &state.routes);
127 stats.processed_paths = true;
128 state.pathing_dirty = false;
129 }
130
131 stats.movement = advance_path_agents(agents, state.routes, options.max_steps);
132 return stats;
133}
134
135template <typename World, typename ClassOrTag, typename OccupancyTag,
136 typename ReservationTag>
138[[nodiscard]] auto tick_unit_path_agents_with_movement(
139 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
140 PathRequestRuntime& runtime, PathAgentTickOptions options = {},
141 std::uint32_t movement_dirty_mask = 0,
144 PathAgentTickStats stats;
145 stats.tick = advance_sim_tick(state.clock);
146
147 const bool repath_needed =
148 prepare_path_agent_processing(agents, options, stats);
149 state.routes.ensure_size(agents.size());
150 if (state.pathing_dirty || repath_needed) {
151 const auto scope =
152 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
153 stats.pathing = process_unit_path_agents<World, ClassOrTag>(
154 world, agents, runtime, options.cache_policy, graph, scope,
155 &state.routes);
156 stats.processed_paths = true;
157 state.pathing_dirty = false;
158 }
159
160 stats.movement =
161 advance_path_agents_with_movement<World, ClassOrTag, OccupancyTag,
162 ReservationTag>(
163 world, agents, state.routes, options.max_steps, movement_dirty_mask);
164 return stats;
165}
166
167// Class forms: one movement class drives pathing, precheck, and (for the
168// movement variant) commit validation, so plan and commit provably agree.
170template <typename World, typename Class, std::uint32_t MaxCost>
171[[nodiscard]] auto tick_weighted_path_agents(
172 PathAgentTickState& state, const World& world,
173 std::span<PathAgentState> agents, PathRequestRuntime& runtime,
174 PathAgentTickOptions options = {},
177 PathAgentTickStats stats;
178 stats.tick = advance_sim_tick(state.clock);
179
180 const bool repath_needed =
181 prepare_path_agent_processing(agents, options, stats);
182 state.routes.ensure_size(agents.size());
183 if (state.pathing_dirty || repath_needed) {
184 const auto scope =
185 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
186 stats.pathing = process_weighted_path_agents<World, Class, MaxCost>(
187 world, agents, runtime, options.cache_policy, graph, scope,
188 &state.routes);
189 stats.processed_paths = true;
190 state.pathing_dirty = false;
191 }
192
193 stats.movement = advance_path_agents(agents, state.routes, options.max_steps);
194 return stats;
195}
196
197template <typename World, typename Class, std::uint32_t MaxCost,
198 typename OccupancyTag, typename ReservationTag>
200[[nodiscard]] auto tick_weighted_path_agents_with_movement(
201 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
202 PathRequestRuntime& runtime, PathAgentTickOptions options = {},
203 std::uint32_t movement_dirty_mask = 0,
206 PathAgentTickStats stats;
207 stats.tick = advance_sim_tick(state.clock);
208
209 const bool repath_needed =
210 prepare_path_agent_processing(agents, options, stats);
211 state.routes.ensure_size(agents.size());
212 if (state.pathing_dirty || repath_needed) {
213 const auto scope =
214 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
215 stats.pathing = process_weighted_path_agents<World, Class, MaxCost>(
216 world, agents, runtime, options.cache_policy, graph, scope,
217 &state.routes);
218 stats.processed_paths = true;
219 state.pathing_dirty = false;
220 }
221
222 stats.movement = advance_path_agents_with_movement<World, Class, OccupancyTag,
223 ReservationTag>(
224 world, agents, state.routes, options.max_steps, movement_dirty_mask);
225 return stats;
226}
227
228template <typename World, typename PassableTag, typename CostTag,
229 std::uint32_t MaxCost>
231[[nodiscard]] auto tick_weighted_path_agents(
232 PathAgentTickState& state, const World& world,
233 std::span<PathAgentState> agents, PathRequestRuntime& runtime,
234 PathAgentTickOptions options = {},
237 PathAgentTickStats stats;
238 stats.tick = advance_sim_tick(state.clock);
239
240 const bool repath_needed =
241 prepare_path_agent_processing(agents, options, stats);
242 state.routes.ensure_size(agents.size());
243 if (state.pathing_dirty || repath_needed) {
244 const auto scope =
245 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
246 stats.pathing =
247 process_weighted_path_agents<World, PassableTag, CostTag, MaxCost>(
248 world, agents, runtime, options.cache_policy, graph, scope,
249 &state.routes);
250 stats.processed_paths = true;
251 state.pathing_dirty = false;
252 }
253
254 stats.movement = advance_path_agents(agents, state.routes, options.max_steps);
255 return stats;
256}
257
258template <typename World, typename PassableTag, typename CostTag,
259 std::uint32_t MaxCost, typename OccupancyTag, typename ReservationTag>
261[[nodiscard]] auto tick_weighted_path_agents_with_movement(
262 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
263 PathRequestRuntime& runtime, PathAgentTickOptions options = {},
264 std::uint32_t movement_dirty_mask = 0,
267 PathAgentTickStats stats;
268 stats.tick = advance_sim_tick(state.clock);
269
270 const bool repath_needed =
271 prepare_path_agent_processing(agents, options, stats);
272 state.routes.ensure_size(agents.size());
273 if (state.pathing_dirty || repath_needed) {
274 const auto scope =
275 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
276 stats.pathing =
277 process_weighted_path_agents<World, PassableTag, CostTag, MaxCost>(
278 world, agents, runtime, options.cache_policy, graph, scope,
279 &state.routes);
280 stats.processed_paths = true;
281 state.pathing_dirty = false;
282 }
283
284 stats.movement =
285 advance_path_agents_with_movement<World, PassableTag, OccupancyTag,
286 ReservationTag>(
287 world, agents, state.routes, options.max_steps, movement_dirty_mask);
288 return stats;
289}
290
291} // namespace tess
Definition path_runtime.h:92
Region graph storage specialized by dense or sparse residency policy.
Definition topology.h:309
Definition world.h:22
Summarizes path submission, results, movement, and failure outcomes.
Definition path_agent.h:44
Owns index-paired route copies retained across scoped processing passes.
Definition path_agent.h:88
Configures per-tick movement, caching, and blocked-agent retry limits.
Definition path_agent_tick.h:29
Owns the shared clock, world-dirty flag, and retained routes across ticks.
Definition path_agent_tick.h:13
Summarizes path planning and movement performed during one tick.
Definition path_agent_tick.h:46
Definition path_runtime.h:32
Stores the authoritative monotonically increasing fixed-tick count.
Definition time.h:28