Squid::Tasks 1.0.0
C++14 coroutine-based task library for games
TaskFSM.h
1#pragma once
2
8
9#include "Task.h"
10
11NAMESPACE_SQUID_BEGIN
12
13class TaskFSM;
14
15namespace FSM
16{
17
18struct StateId
19{
20 StateId() = default;
21 StateId(int32_t in_idx) : idx(in_idx) {}
22 StateId(size_t in_idx) : idx((int32_t)in_idx) {}
23 bool operator==(const StateId& other) const { return (other.idx == idx); }
24 bool operator!=(const StateId& other) const { return (other.idx != idx); }
25 bool IsValid() const { return idx != INT32_MAX; }
26
27 int32_t idx = INT32_MAX; // Default to invalid idx
28};
29
30//--- Transition functions ---//
32{
34 std::string oldStateName;
36 std::string newStateName;
37};
38
39using tOnStateTransitionFn = std::function<void()>;
40using tDebugStateTransitionFn = std::function<void(TransitionDebugData)>;
41
42#include "Private/TaskFSMPrivate.h" // Internal use only! Do not include elsewhere!
43
44//--- State Handle ---//
45template<class tStateInput, class tStateConstructorFn>
47{
48 using tPredicateRet = typename std::conditional<!std::is_void<tStateInput>::value, std::optional<tStateInput>, bool>::type;
49 using tPredicateFn = std::function<tPredicateRet()>;
50public:
51 StateHandle(StateHandle&& in_other) = default;
52 StateHandle& operator=(StateHandle&& in_other) = default;
53
54 StateId GetId() const
55 {
56 return m_state ? m_state->idx : StateId{};
57 }
58
59// SFINAE Template Declaration Macros (#defines)
61#define NONVOID_ONLY_WITH_PREDICATE template <class tPredicateFn, typename tPayload = tStateInput, typename std::enable_if_t<!std::is_void<tPayload>::value>* = nullptr>
62#define VOID_ONLY_WITH_PREDICATE template <class tPredicateFn, typename tPayload = tStateInput, typename std::enable_if_t<std::is_void<tPayload>::value>* = nullptr>
63#define NONVOID_ONLY template <typename tPayload = tStateInput, typename std::enable_if_t<!std::is_void<tPayload>::value && !std::is_convertible<tPayload, tPredicateFn>::value>* = nullptr>
64#define VOID_ONLY template <typename tPayload = tStateInput, typename std::enable_if_t<std::is_void<tPayload>::value>* = nullptr>
65#define PREDICATE_ONLY template <typename tPredicateFn, typename std::enable_if_t<!std::is_convertible<tStateInput, tPredicateFn>::value>* = nullptr>
67
68 // Link methods
69 VOID_ONLY LinkHandle Link() //< Empty predicate link (always follow link)
70 {
71 return _InternalLink([] { return true; }, LinkHandle::eType::Normal);
72 }
73 NONVOID_ONLY LinkHandle Link(tPayload in_payload) //< Empty predicate link w/ payload (always follow link, using provided payload)
74 {
75 return _InternalLink([payload = std::move(in_payload)]() -> tPredicateRet { return payload; }, LinkHandle::eType::Normal);
76 }
77 PREDICATE_ONLY LinkHandle Link(tPredicateFn in_predicate) //< Predicate link w/ implicit payload (follow link when predicate returns a value; use return value as payload)
78 {
79 return _InternalLink(in_predicate, LinkHandle::eType::Normal);
80 }
81 NONVOID_ONLY_WITH_PREDICATE LinkHandle Link(tPredicateFn in_predicate, tPayload in_payload) //< Predicate link w/ explicit payload (follow link when predicate returns true; use provided payload)
82 {
83 return _InternalLink(in_predicate, std::move(in_payload), LinkHandle::eType::Normal);
84 }
85
86 // OnCompleteLink methods
87 VOID_ONLY LinkHandle OnCompleteLink() //< Empty predicate link (always follow link)
88 {
89 return _InternalLink([] { return true; }, LinkHandle::eType::OnComplete);
90 }
91 NONVOID_ONLY LinkHandle OnCompleteLink(tPayload in_payload) //< Empty predicate link w/ payload (always follow link, using provided payload)
92 {
93 return _InternalLink([payload = std::move(in_payload)]() -> tPredicateRet { return payload; }, LinkHandle::eType::OnComplete);
94 }
95 PREDICATE_ONLY LinkHandle OnCompleteLink(tPredicateFn in_predicate) //< Predicate link w/ implicit payload (follow link when predicate returns a value; use return value as payload)
96 {
97 return _InternalLink(in_predicate, LinkHandle::eType::OnComplete, true);
98 }
99 NONVOID_ONLY_WITH_PREDICATE LinkHandle OnCompleteLink(tPredicateFn in_predicate, tPayload in_payload) //< Predicate link w/ explicit payload (follow link when predicate returns true; use provided payload)
100 {
101 return _InternalLink(in_predicate, std::move(in_payload), LinkHandle::eType::OnComplete, true);
102 }
103
104private:
105 friend class NAMESPACE_SQUID::TaskFSM;
106
107 StateHandle() = delete;
108 StateHandle(std::shared_ptr<State<tStateInput, tStateConstructorFn>> InStatePtr)
109 : m_state(InStatePtr)
110 {
111 }
112 StateHandle(const StateHandle& Other) = delete;
113 StateHandle& operator=(const StateHandle& Other) = delete;
114
115 // Internal link function implementations
116 VOID_ONLY_WITH_PREDICATE LinkHandle _InternalLink(tPredicateFn in_predicate, LinkHandle::eType in_linkType, bool in_isConditional = false) // bool-returning predicate
117 {
118 static_assert(std::is_same<bool, decltype(in_predicate())>::value, "This link requires a predicate function returning bool");
119 std::shared_ptr<LinkBase> link = std::make_shared<FSM::Link<tStateInput, tStateConstructorFn, tPredicateFn>>(m_state, in_predicate);
120 return LinkHandle(link, in_linkType, in_isConditional);
121 }
122 NONVOID_ONLY_WITH_PREDICATE LinkHandle _InternalLink(tPredicateFn in_predicate, LinkHandle::eType in_linkType, bool in_isConditional = false) // optional-returning predicate
123 {
124 static_assert(std::is_same<std::optional<tStateInput>, decltype(in_predicate())>::value, "This link requires a predicate function returning std::optional<tStateInput>");
125 std::shared_ptr<LinkBase> link = std::make_shared<FSM::Link<tStateInput, tStateConstructorFn, tPredicateFn>>(m_state, in_predicate);
126 return LinkHandle(link, in_linkType, in_isConditional);
127 }
128 NONVOID_ONLY_WITH_PREDICATE LinkHandle _InternalLink(tPredicateFn in_predicate, tPayload in_payload, LinkHandle::eType in_linkType, bool in_isConditional = false) // bool-returning predicate w/ fixed payload
129 {
130 static_assert(std::is_same<bool, decltype(in_predicate())>::value, "This link requires a predicate function returning bool");
131 auto predicate = [in_predicate, in_payload]() -> std::optional<tStateInput>
132 {
133 return in_predicate() ? std::optional<tStateInput>(in_payload) : std::optional<tStateInput>{};
134 };
135 return _InternalLink(predicate, in_linkType, in_isConditional);
136 }
137
138 // SFINAE Template Declaration Macros (#undefs)
139#undef NONVOID_ONLY_WITH_PREDICATE
140#undef VOID_ONLY_WITH_PREDICATE
141#undef NONVOID_ONLY
142#undef VOID_ONLY
143#undef PREDICATE_ONLY
144
145 std::shared_ptr<State<tStateInput, tStateConstructorFn>> m_state; // Internal state object
146};
147
148} // namespace FSM
149
150using StateId = FSM::StateId;
151template<class tStateInput, class tStateConstructorFn>
154using tOnStateTransitionFn = FSM::tOnStateTransitionFn;
155using tDebugStateTransitionFn = FSM::tDebugStateTransitionFn;
156
157//--- TaskFSM ---//
159{
160public:
161 // Create a new FSM state [fancy param-deducing version (hopefully) coming soon!]
162 template<typename tStateConstructorFn>
163 auto State(std::string in_name, tStateConstructorFn in_stateCtorFn)
164 {
165 typedef FSM::function_traits<tStateConstructorFn> tFnTraits;
166 using tStateInput = typename tFnTraits::tArg;
167 const FSM::StateId newStateId = m_states.size();
168 m_states.push_back(InternalStateData(in_name));
169 auto state = std::make_shared<FSM::State<tStateInput, tStateConstructorFn>>(std::move(in_stateCtorFn), newStateId, in_name);
171 }
172
173 // Create a new FSM exit state (immediately terminates the FSM when executed)
174 FSM::StateHandle<void, void> State(std::string in_name)
175 {
176 const FSM::StateId newStateId = m_states.size();
177 m_states.push_back(InternalStateData(in_name));
178 m_exitStates.push_back(newStateId);
179 auto state = std::make_shared<FSM::State<void, void>>(newStateId, in_name);
180 return FSM::StateHandle<void, void>{ state };
181 }
182
183 // Define the initial entry links into the state machine
184 void EntryLinks(std::vector<FSM::LinkHandle> in_entryLinks);
185
186 // Define all outgoing links from a given state (may only be called once per state)
187 template<class tStateInput, class tStateConstructorFn>
188 void StateLinks(const FSM::StateHandle<tStateInput, tStateConstructorFn>& in_originState, std::vector<FSM::LinkHandle> in_outgoingLinks);
189
190 // Begins execution of the state machine (returns id of final exit state)
191 Task<FSM::StateId> Run(tOnStateTransitionFn in_onTransitionFn = {}, tDebugStateTransitionFn in_debugStateTransitionFn = {}) const;
192
193private:
194 // Evaluates all possible outgoing links from the current state, returning the first valid transition (if any transitions are valid)
195 std::optional<FSM::TransitionEvent> EvaluateLinks(FSM::StateId in_curStateId, bool in_isCurrentStateComplete, const tOnStateTransitionFn& in_onTransitionFn) const;
196
197 // Internal state
198 struct InternalStateData
199 {
200 InternalStateData(std::string in_debugName)
201 : debugName(in_debugName)
202 {
203 }
204 std::vector<FSM::LinkHandle> outgoingLinks;
205 std::string debugName;
206 };
207 std::vector<InternalStateData> m_states;
208 std::vector<FSM::LinkHandle> m_entryLinks;
209 std::vector<FSM::StateId> m_exitStates;
210};
211
213
214//--- TaskFSM Methods ---//
215template<class tStateInput, class tStateConstructorFn>
216void TaskFSM::StateLinks(const FSM::StateHandle<tStateInput, tStateConstructorFn>& in_originState, std::vector<FSM::LinkHandle> in_outgoingLinks)
217{
218 const int32_t stateIdx = in_originState.m_state->stateId.idx;
219 SQUID_RUNTIME_CHECK(m_states[stateIdx].outgoingLinks.size() == 0, "Cannot set outgoing links more than once for each state");
220
221 // Validate that there are exactly 0 or 1 unconditional OnComplete links (there may be any number of other OnComplete links, but only one with no condition)
222 int32_t numOnCompleteLinks = 0;
223 int32_t numOnCompleteLinks_Unconditional = 0;
224 for(const FSM::LinkHandle& link : in_outgoingLinks)
225 {
226 if(link.IsOnCompleteLink())
227 {
228 SQUID_RUNTIME_CHECK(numOnCompleteLinks_Unconditional == 0, "Cannot call OnCompleteLink() after calling OnCompleteLink() with no conditions (unreachable link)");
229 ++numOnCompleteLinks;
230 if(!link.HasCondition())
231 {
232 numOnCompleteLinks_Unconditional++;
233 }
234 }
235 }
236 SQUID_RUNTIME_CHECK(numOnCompleteLinks == 0 || numOnCompleteLinks_Unconditional > 0, "More than one unconditional OnCompleteLink() was set");
237
238 // Set the outgoing links for the origin state
239 m_states[stateIdx].outgoingLinks = std::move(in_outgoingLinks);
240}
241inline void TaskFSM::EntryLinks(std::vector<FSM::LinkHandle> in_entryLinks)
242{
243 // Validate to ensure there are no OnComplete links set as entry links
244 int32_t numOnCompleteLinks = 0;
245 for(const FSM::LinkHandle& link : in_entryLinks)
246 {
247 if(link.IsOnCompleteLink())
248 {
249 ++numOnCompleteLinks;
250 }
251 }
252 SQUID_RUNTIME_CHECK(numOnCompleteLinks == 0, "EntryLinks() list may not contain any OnCompleteLink() links");
253
254 // Set the entry links list for this FSM
255 m_entryLinks = std::move(in_entryLinks);
256}
257inline std::optional<FSM::TransitionEvent> TaskFSM::EvaluateLinks(FSM::StateId in_curStateId, bool in_isCurrentStateComplete, const tOnStateTransitionFn& in_onTransitionFn) const
258{
259 // Determine whether to use entry links or state-specific outgoing links
260 const std::vector<FSM::LinkHandle>& links = (in_curStateId.idx < m_states.size()) ? m_states[in_curStateId.idx].outgoingLinks : m_entryLinks;
261
262 // Find the first valid transition from the current state
263 for(const FSM::LinkHandle& link : links)
264 {
265 if(!link.IsOnCompleteLink() || in_isCurrentStateComplete) // Skip link evaluation check for OnComplete links unless current state is complete
266 {
267 if(auto result = link.EvaluateLink(in_onTransitionFn)) // Check if the transition to this state is valid
268 {
269 return result;
270 }
271 }
272 }
273 return {}; // No valid transition was found
274}
275inline Task<FSM::StateId> TaskFSM::Run(tOnStateTransitionFn in_onTransitionFn, tDebugStateTransitionFn in_debugStateTransitionFn) const
276{
277 // Task-local variables
278 FSM::StateId curStateId; // The current state's ID
279 Task<> task; // The current state's task
280
281 // Custom debug task name logic
282 TASK_NAME(__FUNCTION__, [this, &curStateId, &task]
283 {
284 const std::string stateName = (curStateId.idx < m_states.size()) ? m_states[curStateId.idx].debugName : "";
285 return stateName + " -- " + task.GetDebugStack();
286 });
287
288 // Debug state transition lambda
289 auto DebugStateTransition = [this, in_debugStateTransitionFn](FSM::StateId in_oldStateId, FSM::StateId in_newStateId) {
290 if(in_debugStateTransitionFn)
291 {
292 std::string oldStateName = in_oldStateId.IsValid() ? m_states[in_oldStateId.idx].debugName : std::string("<ENTRY>");
293 std::string newStateName = m_states[in_newStateId.idx].debugName;
294 in_debugStateTransitionFn({ in_oldStateId, std::move(oldStateName), in_newStateId, std::move(newStateName) });
295 }
296 };
297
298 // Main FSM loop
299 while(true)
300 {
301 // Evaluate links, checking for a valid transition
302 if(std::optional<FSM::TransitionEvent> transition = EvaluateLinks(curStateId, task.IsDone(), in_onTransitionFn))
303 {
304 auto newStateId = transition->newStateId;
305 DebugStateTransition(curStateId, newStateId); // Call state-transition debug function
306
307 // If the transition is to an exit state, return that state ID (terminating the FSM)
308 auto Found = std::find(m_exitStates.begin(), m_exitStates.end(), newStateId.idx);
309 if(Found != m_exitStates.end())
310 {
311 co_return newStateId;
312 }
313 SQUID_RUNTIME_CHECK(newStateId.idx < m_states.size(), "It should be logically impossible to get an invalid state to this point");
314
315 // Begin running new state (implicitly killing old state)
316 curStateId = newStateId;
317 co_await RemoveStopTask(task);
318 task = std::move(transition->newTask); // NOTE: Initial call to Resume() happens below
319 co_await AddStopTask(task);
320 }
321
322 // Resume current state
323 task.Resume();
324
325 // Suspend until next frame
326 co_await Suspend();
327 }
328}
329
330NAMESPACE_SQUID_END
Control handle.
Definition: TaskFSM.h:47
StateId GetId() const
< Get the ID of this state
Definition: TaskFSM.h:54
Definition: TaskFSM.h:159
Definition: Task.h:204
eTaskStatus Resume()
Resumes the task (Task/WeakTask only)
Definition: Task.h:309
std::string GetDebugStack(std::optional< TaskDebugStackFormatter > in_formatter={}) const
Gets this task's debug stack (use TASK_NAME to set a task's debug name)
Definition: Task.h:322
bool IsDone() const
Returns whether the task has terminated.
Definition: Task.h:281
#define TASK_NAME(...)
Macro that instruments a task with a debug name string. Usually at the top of every task coroutine as...
Definition: Task.h:25
State ID handle, representing a unique state ID with "invalid id" semantics.
Definition: TaskFSM.h:19
Debug state transition data (used by debug-state-transition callbacks)
Definition: TaskFSM.h:32
FSM::StateId oldStateId
Outgoing state's id.
Definition: TaskFSM.h:33
std::string newStateName
Incoming state's name.
Definition: TaskFSM.h:36
std::string oldStateName
Outgoing state's name.
Definition: TaskFSM.h:34
FSM::StateId newStateId
Incoming state's id.
Definition: TaskFSM.h:35
Awaiter class that suspends unconditionally.
Definition: Task.h:91