VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
CenterHtmlMenu.cpp
Go to the documentation of this file.
2#include "Menu/MenuCursor.hpp"
3
7#include <cstddef>
8#include <memory>
9#include <utility>
10
11namespace VoltMod
12{
13
14/** The rows of @p menu as the cursor sees them: how many, and which it may land on. */
16{
17 if (!menu)
18 {
19 return {};
20 }
21
22 return {.Count = static_cast<int>(menu->Items.size()), .Selectable = [menu, slot](int index) {
23 return IsRowActionable(menu->Items[static_cast<std::size_t>(index)], slot);
24 }};
25}
26
28 : _services(services),
29 _stack(*this, _services.Translations, services.Scheduler, services.Slots),
30 _cursors(services.Slots)
31{}
32
33bool CenterHtmlMenu::OpenSession(int slot, std::shared_ptr<Menu> menu, MenuOptions options)
34{
35 if (!IsValidSlot(slot) || !menu)
36 {
37 return false;
38 }
39
41
42 _services.Freeze.Open(slot, options.FreezeMovement);
43
44 Push(slot, std::move(menu));
45 return true;
46}
47
48void CenterHtmlMenu::Open(int slot, std::shared_ptr<Menu> menu)
49{
50 if (!IsValidSlot(slot) || !menu)
51 {
52 return;
53 }
54
55 if (!_stack.IsOpen(slot))
56 {
57 OpenSession(slot, std::move(menu), {});
58 return;
59 }
60
61 Push(slot, std::move(menu));
62}
63
64void CenterHtmlMenu::Push(int slot, std::shared_ptr<Menu> menu)
65{
66 _stack.Push(slot, std::move(menu));
67 ResetCursor(slot);
68
69 if (!_onFrame)
70 {
71 _onFrame = _services.Scheduler.EveryFrame([this] { OnGameFrame(); });
72 }
73}
74
75void CenterHtmlMenu::ResetCursor(int slot)
76{
77 Cursor& cursor = _cursors[slot];
78 cursor.LastSent = {};
79 cursor.LastInputTime = Time::MonotonicMs();
81}
82
83void CenterHtmlMenu::Select(int slot, int index)
84{
85 // Ignore stale or client-forged row indexes.
86 Menu* menu = _stack.Current(slot);
87 if (index < 0 || !menu || index >= static_cast<int>(menu->Items.size()))
88 {
89 return;
90 }
91
92 // Returning to a stepped row keeps its value pending.
93 if (!_stack.IsPending(slot, index))
94 {
95 _stack.ApplyPending(slot);
96 }
97
98 _cursors[slot].Selected = index;
99}
100
102{
103 if (!IsValidSlot(slot))
104 {
105 return;
106 }
107
108 // A prompt of a closing menu must not take later chat.
109 _services.ChatInput.CancelCapture(slot);
110
111 if (!_stack.IsOpen(slot))
112 {
113 return;
114 }
115
116 // A parent menu still showing is drawn next frame.
117 _stack.Pop(slot);
118 if (_stack.IsOpen(slot))
119 {
120 ResetCursor(slot);
121 return;
122 }
123
124 _cursors[slot] = {};
125 _services.Freeze.Close(slot);
126 _services.Messages.ClearCenterHtml(slot);
127}
128
130{
131 if (!IsValidSlot(slot))
132 {
133 return;
134 }
135
136 _services.ChatInput.CancelCapture(slot);
137
138 if (!_stack.IsOpen(slot))
139 {
140 return;
141 }
142
143 _stack.Clear(slot);
144 _cursors[slot] = {};
145 _services.Freeze.Close(slot);
146 _services.Messages.ClearCenterHtml(slot);
147}
148
149void CenterHtmlMenu::CloseAll(int slot, std::string_view replyKey)
150{
151 if (auto& reply = _services.Policy.Reply; reply)
152 {
153 reply(slot, _services.Translations.Get(std::string(replyKey), slot));
154 }
155
156 CloseAll(slot);
157}
158
159void CenterHtmlMenu::Prompt(int slot, std::string prompt, std::function<bool(int, std::string_view)> callback)
160{
161 _services.ChatInput.BeginCapture(slot, std::move(prompt), std::move(callback));
162 _cursors[slot].LastSent = {};
163}
164
165std::string CenterHtmlMenu::Translate(int slot, std::string_view key, std::string_view fallback) const
166{
167 return _services.Translations.GetOr(key, slot, fallback);
168}
169
171{
172 return _stack.IsOpen(slot);
173}
174
175void CenterHtmlMenu::Draw(int slot, bool changed)
176{
177 Menu* menu = _stack.Current(slot);
178 if (!menu)
179 {
180 return;
181 }
182
183 Page& page = _cursors[slot].LastSent;
184 const int64_t now = Time::MonotonicMs();
185 const bool stale = changed || page.Html.empty() || now - page.RenderedAt >= RenderIntervalMs;
186 if (stale)
187 {
188 std::string html = Render(slot, *menu);
189 page.RenderedAt = now;
190 if (html != page.Html)
191 {
192 page.Html = std::move(html);
193 page.SentAt = 0;
194 }
195 }
196
197 if (now - page.SentAt >= ResendIntervalMs)
198 {
199 _services.Messages.SendCenterHtml(slot, page.Html);
200 page.SentAt = now;
201 }
202}
203
204std::string CenterHtmlMenu::Render(int slot, Menu& menu)
205{
206 // A pending capture replaces the item list with its prompt.
207 if (auto prompt = _services.ChatInput.GetPrompt(slot))
208 {
209 return RenderCaptureOverlay(menu.Title, *prompt);
210 }
211
212 const CenterHtmlView view{
213 .Describe = [this, slot](int index) { return _stack.Describe(slot, index); },
214 .Breadcrumb = _stack.Breadcrumb(slot),
215 .Slot = slot,
216 .SelectedIndex = _cursors[slot].Selected,
217 .IsSubmenu = _stack.Depth(slot) > 1,
218 };
219 return RenderMenuHtml(&menu, view, _services.Translations);
220}
221
222void CenterHtmlMenu::OnGameFrame()
223{
224 for (int slot = 0; slot < MaxPlayers; ++slot)
225 {
226 if (!_stack.IsOpen(slot))
227 {
228 continue;
229 }
230
231 const bool pressed = ReadKeys(slot);
232
233 // Input may have activated a row that closed the menu it was about to draw.
234 if (_stack.IsOpen(slot))
235 {
236 Draw(slot, pressed);
237 }
238 }
239
240 // Slot reset clears stacks without going through Close, so stop per-frame work here.
241 if (!_stack.AnyOpen())
242 {
243 _onFrame.Reset();
244 }
245}
246
247bool CenterHtmlMenu::ReadKeys(int slot)
248{
249 if (!_stack.Current(slot))
250 {
251 return false;
252 }
253
254 Cursor& cursor = _cursors[slot];
255 const uint64_t buttons = _services.Entities.Controller(slot).Buttons();
256 const uint64_t pressed = buttons & ~cursor.PrevButtons;
257 cursor.PrevButtons = buttons;
258
259 if (pressed == 0)
260 {
261 return false;
262 }
263
264 const int64_t now = Time::MonotonicMs();
265 if (now - cursor.LastInputTime < PressGapMs)
266 {
267 return false;
268 }
269
270 if (_services.ChatInput.IsCapturing(slot))
271 {
272 if ((pressed & IN_RELOAD) == 0)
273 {
274 return false;
275 }
276
277 _services.ChatInput.CancelCapture(slot);
278 cursor.LastInputTime = now;
279 return true;
280 }
281
282 if (!RunKey(slot, pressed))
283 {
284 return false;
285 }
286
287 cursor.LastInputTime = now;
288 return true;
289}
290
291bool CenterHtmlMenu::RunKey(int slot, uint64_t pressed)
292{
293 if (pressed & IN_RELOAD)
294 {
295 Close(slot);
296 return true;
297 }
298
299 Menu* menu = _stack.Current(slot);
300 const int itemCount = menu ? static_cast<int>(menu->Items.size()) : 0;
301 if (itemCount == 0)
302 {
303 return false;
304 }
305
306 if (pressed & IN_FORWARD)
307 {
308 MoveCursor(slot, -1);
309 return true;
310 }
311 if (pressed & IN_BACK)
312 {
313 MoveCursor(slot, +1);
314 return true;
315 }
317 {
318 const int direction = (pressed & IN_MOVELEFT) ? -1 : +1;
319 if (_stack.Step(slot, _cursors[slot].Selected, direction))
320 {
321 return true;
322 }
324 {
325 return false;
326 }
327 JumpPage(slot, direction);
328 return true;
329 }
330 if (pressed & IN_USE)
331 {
332 _stack.Activate(slot, _cursors[slot].Selected);
333 return true;
334 }
335 return false;
336}
337
338void CenterHtmlMenu::MoveCursor(int slot, int step)
339{
340 Select(slot, MenuCursor::Step(CursorRowsFor(_stack.Current(slot), slot), _cursors[slot].Selected, step));
341}
342
343void CenterHtmlMenu::JumpPage(int slot, int delta)
344{
345 Menu* menu = _stack.Current(slot);
346 if (!menu || menu->Items.empty())
347 {
348 return;
349 }
350
351 Select(slot,
353}
354
355} // namespace VoltMod
bool OpenSession(int slot, std::shared_ptr< Menu > menu, MenuOptions options) override
void CloseAll(int slot) override
void Open(int slot, std::shared_ptr< Menu > menu) override
CenterHtmlMenu(const Services &services)
void Close(int slot) override
bool IsOpen(int slot) const override
void Prompt(int slot, std::string prompt, std::function< bool(int slot, std::string_view text)> callback) override
std::string Translate(int slot, std::string_view key, std::string_view fallback) const override
void CancelCapture(int slot)
Definition ChatInput.cpp:70
void BeginCapture(int slot, std::string prompt, Callback callback, int timeoutMs=60000)
Definition ChatInput.cpp:14
std::optional< std::string > GetPrompt(int slot) const
Definition ChatInput.cpp:80
bool IsCapturing(int slot) const
Definition ChatInput.cpp:32
uint64_t Buttons() const
VoltMod::Controller Controller(int slot)
void Close(int slot)
void Open(int slot, bool requested)
bool IsPending(int slot, int index) const
bool IsOpen(int slot) const
Definition MenuStack.cpp:48
void Pop(int slot)
Definition MenuStack.cpp:81
Menu * Current(int slot)
Definition MenuStack.cpp:25
void Activate(int slot, int index)
std::string_view Breadcrumb(int slot) const
Definition MenuStack.cpp:65
void Push(int slot, std::shared_ptr< Menu > menu)
Definition MenuStack.cpp:70
MenuRow Describe(int slot, int index)
bool AnyOpen() const
Definition MenuStack.cpp:53
void ApplyPending(int slot)
void Clear(int slot)
int Depth(int slot) const
Definition MenuStack.cpp:43
bool Step(int slot, int index, int direction)
void ClearCenterHtml(int slot)
Definition Messages.cpp:201
void SendCenterHtml(int slot, const std::string &html)
Definition Messages.cpp:75
std::function< void(int slot, std::string_view message)> Reply
Definition Policy.hpp:63
One-shot delays and repeating timers, run on the game thread.
Definition Scheduler.hpp:18
Subscription EveryFrame(std::function< void()> callback)
Definition Scheduler.cpp:35
static int64_t MonotonicMs()
Definition Durations.hpp:35
Localization system. Loads one JSON file per language; nested objects flatten into dotted keys (categ...
std::string Get(std::string_view key) const
std::string GetOr(std::string_view key, int slot, std::string_view fallback) const
int Step(const CursorRows &rows, int index, int step)
int First(const CursorRows &rows)
int JumpPage(const CursorRows &rows, int index, int rowsPerPage, int delta)
std::string RenderMenuHtml(const Menu *menu, const CenterHtmlView &view, Translations &translations)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
std::string RenderCaptureOverlay(const std::string &menuTitle, std::string_view prompt)
constexpr bool IsValidSlot(int slot)
Definition Slot.hpp:13
bool IsRowActionable(const MenuItem &item, int slot)
constexpr int CenterHtmlRowsPerPage
constexpr int MaxPlayers
Definition Slot.hpp:7
static CursorRows CursorRowsFor(Menu *menu, int slot)
VoltMod::Translations & Translations