VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Flow.hpp
Go to the documentation of this file.
1#pragma once
2
8#include <format>
9#include <functional>
10#include <memory>
11#include <optional>
12#include <string>
13#include <string_view>
14#include <utility>
15#include <vector>
16
17namespace VoltMod
18{
19
20/** Runs a sequence of menus while collecting state for one player. */
21template <class TState>
22class Flow : public std::enable_shared_from_this<Flow<TState>>
23{
24public:
25 using Ptr = std::shared_ptr<Flow>;
26 /** Menu factory for a custom step; call `flow.Advance()` after mutating `flow.State()`. */
27 using BuildFn = std::function<std::shared_ptr<Menu>(Flow& flow)>;
28 /** Step predicate over the current state; a false skips the step. */
29 using AppliesFn = std::function<bool(const TState&)>;
30
31 /** Duration presets with optional chat input. */
33 {
34 std::string Title;
35 std::vector<Labeled<int>> Presets;
36 std::function<void(TState&, int seconds)> Set;
37 /** Empty = no free-text row. */
38 std::string CustomLabel;
39 std::string CustomPrompt;
41 };
42
43 /** Labeled options with optional free-text input. */
45 {
46 std::string Title;
47 std::vector<Labeled<std::string>> Options;
48 std::function<void(TState&, const std::string& label, const std::string& value)> Set;
49 std::string CustomLabel;
50 std::string CustomPrompt;
51 std::string CustomValue;
53 };
54
55 /** Optional summary shown before finishing. */
57 {
58 std::string Title;
59 std::function<void(const TState& state, SummaryRows& rows)> Summary;
60 std::string ConfirmLabel;
61 std::string CancelLabel;
62 };
63
64 /** @p menus must outlive the flow. */
66 {
67 return Ptr(new Flow(menus, slot, std::move(initial)));
68 }
69
70 /** Append a custom step. */
72 {
73 _steps.push_back({.Build = std::move(build), .Applies = std::move(applies)});
74 return this->shared_from_this();
75 }
76
77 /** Append a duration-picker step, drawn by @ref VoltMod::BuildDurationMenu. */
79 {
80 auto weak = this->weak_from_this();
81 // Keep the predicate in Step instead of copying it into the callback.
82 auto applies = std::move(step.Applies);
83 return AddStep(
84 [weak, step = std::move(step)](Flow&) -> std::shared_ptr<Menu> {
85 auto self = weak.lock();
86 if (!self)
87 {
88 return nullptr;
89 }
90
91 return BuildDurationMenu({.Title = step.Title,
92 .Presets = step.Presets,
93 .Pick =
94 [self, set = step.Set](int, int seconds) {
95 if (set)
96 {
97 set(self->_state, seconds);
98 }
99 self->Advance();
100 },
101 .CustomLabel = step.CustomLabel,
102 .CustomPrompt = step.CustomPrompt});
103 },
104 std::move(applies));
105 }
106
107 /** Append an options step. */
109 {
110 auto weak = this->weak_from_this();
111 auto applies = std::move(step.Applies);
112 return AddStep(
113 [weak, step = std::move(step)](Flow&) -> std::shared_ptr<Menu> {
114 auto self = weak.lock();
115 return self ? self->BuildOptionsMenu(step) : nullptr;
116 },
117 std::move(applies));
118 }
119
120 /** Runs before each step and finish. Return a translation key to abort. */
121 Ptr Validate(std::function<std::optional<std::string>(const TState&)> check)
122 {
123 _validate = std::move(check);
124 return this->shared_from_this();
125 }
126
127 /** End with a summary confirm dialog instead of finishing straight away. */
129 {
130 _confirm = std::move(spec);
131 return this->shared_from_this();
132 }
133
134 Ptr Finish(std::function<void(TState&)> finish)
135 {
136 _finish = std::move(finish);
137 return this->shared_from_this();
138 }
139
140 /** Open the first applicable step (or the confirm/finish when there are none). */
141 void Begin() { OpenFrom(0); }
142
143 /** Move past the current step. Steps call this after writing their value into @ref State. */
144 void Advance() { OpenFrom(_stepIndex + 1); }
145
146 TState& State() { return _state; }
147
148private:
149 Flow(MenuSurface& menus, int slot, TState initial) : _menus(&menus), _slot(slot), _state(std::move(initial)) {}
150
151 struct Step
152 {
153 BuildFn Build;
154 AppliesFn Applies;
155 };
156
157 std::shared_ptr<Menu> BuildOptionsMenu(const OptionsStep& step)
158 {
159 auto self = this->shared_from_this();
160 MenuBuilder builder(step.Title);
161
162 // Share one setter because each open row retains the callback.
163 auto set = std::make_shared<const decltype(step.Set)>(step.Set);
164
165 for (const auto& [label, value] : step.Options)
166 {
167 builder.Button(label, [self, set, label, value](int) {
168 if (*set)
169 {
170 (*set)(self->_state, label, value);
171 }
172 self->Advance();
173 });
174 }
175
176 if (!step.CustomLabel.empty())
177 {
178 builder.Add(InputRow{.Label = step.CustomLabel,
179 .Prompt = step.CustomPrompt,
180 .Set = [self, set, customValue = step.CustomValue](int, std::string_view text) {
181 std::string typed = Strings::Trim(std::string(text));
182 if (typed.empty())
183 {
184 return false; // re-prompt
185 }
186 if (*set)
187 {
188 (*set)(self->_state, typed, customValue.empty() ? typed : customValue);
189 }
190 self->Advance();
191 return true;
192 }});
193 }
194
195 return builder.Build();
196 }
197
198 std::shared_ptr<Menu> BuildSummary()
199 {
200 auto self = this->shared_from_this();
201
202 SummaryRows rows;
203 if (_confirm.Summary)
204 {
205 _confirm.Summary(_state, rows);
206 }
207
208 // Resolved here because the flow knows which player it runs for.
209 std::string confirmLabel =
210 _confirm.ConfirmLabel.empty() ? _menus->Translate(_slot, "menu.confirm", "Confirm") : _confirm.ConfirmLabel;
211 std::string cancelLabel =
212 _confirm.CancelLabel.empty() ? _menus->Translate(_slot, "menu.cancel", "Cancel") : _confirm.CancelLabel;
213
214 return BuildConfirmMenu({.Title = _confirm.Title,
215 .Lines = std::move(rows).Take(),
216 .ConfirmLabel = std::move(confirmLabel),
217 .CancelLabel = std::move(cancelLabel),
218 .Confirm = [self](int) { self->RunFinish(); }});
219 }
220
221 void OpenFrom(std::size_t from)
222 {
223 if (!RunValidation())
224 {
225 return;
226 }
227
228 for (std::size_t i = from; i < _steps.size(); ++i)
229 {
230 if (_steps[i].Applies && !_steps[i].Applies(_state))
231 {
232 continue;
233 }
234 _stepIndex = i;
235 auto menu = _steps[i].Build(*this);
236 if (!menu)
237 {
238 // A step that cannot build has nothing to show, so abort like a validation failure
239 // instead of leaving the previous menu open.
240 _menus->CloseAll(_slot, "menu.stepFailed");
241 return;
242 }
243 _menus->Open(_slot, std::move(menu));
244 return;
245 }
246
247 if (_confirm.Summary)
248 {
249 _menus->Open(_slot, BuildSummary());
250 }
251 else
252 {
253 RunFinish();
254 }
255 }
256
257 void RunFinish()
258 {
259 // Check again after the confirmation dialog.
260 if (!RunValidation())
261 {
262 return;
263 }
264 if (_finish)
265 {
266 _finish(_state);
267 }
268 _menus->CloseAll(_slot);
269 }
270
271 /** False = aborted (error replied, menus closed). */
272 bool RunValidation()
273 {
274 if (!_validate)
275 {
276 return true;
277 }
278 auto error = _validate(_state);
279 if (!error)
280 {
281 return true;
282 }
283
284 _menus->CloseAll(_slot, *error);
285 return false;
286 }
287
288 MenuSurface* _menus;
289 int _slot;
290 TState _state;
291 std::vector<Step> _steps;
292 std::size_t _stepIndex = 0;
293 std::function<std::optional<std::string>(const TState&)> _validate;
294 ConfirmSpec _confirm;
295 std::function<void(TState&)> _finish;
296};
297
298} // namespace VoltMod
TState & State()
Definition Flow.hpp:146
std::function< bool(const TState &)> AppliesFn
Definition Flow.hpp:29
static Ptr Create(MenuSurface &menus, int slot, TState initial)
Definition Flow.hpp:65
Ptr AddStep(BuildFn build, AppliesFn applies={})
Definition Flow.hpp:71
void Advance()
Definition Flow.hpp:144
std::shared_ptr< Flow > Ptr
Definition Flow.hpp:25
std::function< std::shared_ptr< Menu >(Flow &flow)> BuildFn
Definition Flow.hpp:27
Ptr Finish(std::function< void(TState &)> finish)
Definition Flow.hpp:134
Ptr AddOptionsStep(OptionsStep step)
Definition Flow.hpp:108
Ptr Confirm(ConfirmSpec spec)
Definition Flow.hpp:128
Ptr Validate(std::function< std::optional< std::string >(const TState &)> check)
Definition Flow.hpp:121
Ptr AddDurationStep(DurationStep step)
Definition Flow.hpp:78
void Begin()
Definition Flow.hpp:141
virtual void Open(int slot, std::shared_ptr< Menu > menu)=0
virtual std::string Translate(int slot, std::string_view key, std::string_view fallback) const =0
virtual void CloseAll(int slot)=0
static std::string Trim(std::string_view str)
Definition Strings.cpp:107
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
std::shared_ptr< Menu > BuildDurationMenu(DurationMenu spec)
std::shared_ptr< Menu > BuildConfirmMenu(ConfirmMenu spec)
std::function< void(const TState &state, SummaryRows &rows)> Summary
Definition Flow.hpp:59
std::string CancelLabel
Definition Flow.hpp:61
std::string ConfirmLabel
Definition Flow.hpp:60
std::vector< Labeled< int > > Presets
Definition Flow.hpp:35
std::string CustomPrompt
Definition Flow.hpp:39
std::function< void(TState &, int seconds)> Set
Definition Flow.hpp:36
std::string CustomLabel
Definition Flow.hpp:38
std::string CustomValue
Definition Flow.hpp:51
std::function< void(TState &, const std::string &label, const std::string &value)> Set
Definition Flow.hpp:48
std::string CustomLabel
Definition Flow.hpp:49
std::string CustomPrompt
Definition Flow.hpp:50
std::vector< Labeled< std::string > > Options
Definition Flow.hpp:47