Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
ChatRuntime.cpp
Go to the documentation of this file.
1#include "ChatRuntime.hpp"
2
3#include "Messaging.hpp"
4
5#include <SDL3/SDL_scancode.h>
6
7#include <algorithm>
8#include <limits>
9
10namespace monopoly::chat
11{
12 namespace
13 {
14 State runtime{};
15 std::array<std::wstring, rules::MaxPlayers> playerNames{};
16 bool playerNamesReady{};
17
18 std::size_t outputLineCount() noexcept
19 {
20 return runtime.outputLayoutReady ? runtime.wrappedOutputLines : runtime.count;
21 }
22
23 [[nodiscard]] bool validSender(rules::PlayerNumber player) noexcept
24 {
25 return player < rules::MaxPlayers ||
26 player == rules::SpectatorPlayer;
27 }
28
29 [[nodiscard]] bool validTarget(rules::PlayerNumber player) noexcept
30 {
31 return player < rules::MaxPlayers ||
32 player == rules::AllPlayers;
33 }
34
35 struct ChatRect
36 {
37 int left{}, top{}, right{}, bottom{};
38 [[nodiscard]] constexpr bool contains(int x, int y) const noexcept
39 { return x >= left && x < right && y >= top && y < bottom; }
40 };
41
42 [[nodiscard]] constexpr ChatRect allButtonRect(const State& state) noexcept
43 {
44 return {state.windowX + state.windowWidth - 76, state.windowY + 2,
45 state.windowX + state.windowWidth - 60, state.windowY + 16};
46 }
47 [[nodiscard]] constexpr ChatRect optionsButtonRect(const State& state) noexcept
48 {
49 return {state.windowX + state.windowWidth - 58, state.windowY + 2,
50 state.windowX + state.windowWidth - 42, state.windowY + 16};
51 }
52 [[nodiscard]] constexpr ChatRect fluffButtonRect(const State& state) noexcept
53 {
54 return {state.windowX + state.windowWidth - 40, state.windowY + 2,
55 state.windowX + state.windowWidth - 24, state.windowY + 16};
56 }
57 [[nodiscard]] constexpr ChatRect shadeButtonRect(const State& state) noexcept
58 {
59 return {state.windowX + state.windowWidth - 21, state.windowY + 1,
60 state.windowX + state.windowWidth - 1, state.windowY + 18};
61 }
62 [[nodiscard]] constexpr ChatRect closeButtonRect(const State& state) noexcept
63 {
64 return {state.windowX, state.windowY,
65 state.windowX + 19, state.windowY + 18};
66 }
67 [[nodiscard]] constexpr ChatRect optionPanelRect(const State& state) noexcept
68 {
69 return {state.windowX + state.windowWidth - 120, state.windowY + 16,
70 state.windowX + state.windowWidth - 22, state.windowY + 68};
71 }
72 [[nodiscard]] constexpr ChatRect bgAlphaUpRect(const State& state) noexcept
73 {
74 return {state.windowX + state.windowWidth - 113, state.windowY + 18,
75 state.windowX + state.windowWidth - 97, state.windowY + 33};
76 }
77 [[nodiscard]] constexpr ChatRect bgAlphaDownRect(const State& state) noexcept
78 {
79 return {state.windowX + state.windowWidth - 43, state.windowY + 18,
80 state.windowX + state.windowWidth - 26, state.windowY + 33};
81 }
82 [[nodiscard]] constexpr ChatRect textAlphaUpRect(const State& state) noexcept
83 {
84 return {state.windowX + state.windowWidth - 113, state.windowY + 34,
85 state.windowX + state.windowWidth - 97, state.windowY + 49};
86 }
87 [[nodiscard]] constexpr ChatRect textAlphaDownRect(const State& state) noexcept
88 {
89 return {state.windowX + state.windowWidth - 43, state.windowY + 34,
90 state.windowX + state.windowWidth - 26, state.windowY + 49};
91 }
92 [[nodiscard]] constexpr ChatRect fontSizeUpRect(const State& state) noexcept
93 {
94 return {state.windowX + state.windowWidth - 113, state.windowY + 50,
95 state.windowX + state.windowWidth - 97, state.windowY + 65};
96 }
97 [[nodiscard]] constexpr ChatRect fontSizeDownRect(const State& state) noexcept
98 {
99 return {state.windowX + state.windowWidth - 43, state.windowY + 50,
100 state.windowX + state.windowWidth - 26, state.windowY + 65};
101 }
102 [[nodiscard]] constexpr ChatRect sizeButtonRect(const State& state) noexcept
103 {
104 return {state.windowX + state.windowWidth - 20,
105 state.windowY + state.windowHeight - 18,
106 state.windowX + state.windowWidth - 1,
107 state.windowY + state.windowHeight - 1};
108 }
109 [[nodiscard]] constexpr ChatRect chatBarRect(const State& state) noexcept
110 {
111 return {state.windowX, state.windowY,
112 state.windowX + state.windowWidth - 1, state.windowY + 18};
113 }
114 [[nodiscard]] constexpr ChatRect chatBoxRect(const State& state) noexcept
115 {
116 return {state.windowX, state.windowY + 18,
117 state.windowX + state.windowWidth - 1,
118 state.windowY + state.windowHeight - 1};
119 }
120 [[nodiscard]] constexpr ChatRect chatUpButtonRect(const State& state) noexcept
121 {
122 return {state.windowX + state.windowWidth - 18, state.windowY + 18,
123 state.windowX + state.windowWidth - 1, state.windowY + 35};
124 }
125 [[nodiscard]] constexpr ChatRect chatDownButtonRect(const State& state) noexcept
126 {
127 return {state.windowX + state.windowWidth - 18,
128 state.windowY + state.windowHeight - 35,
129 state.windowX + state.windowWidth - 1,
130 state.windowY + state.windowHeight - 18};
131 }
132 [[nodiscard]] constexpr ChatRect chatScrollbarRect(const State& state) noexcept
133 {
134 return {state.windowX + state.windowWidth - 17, state.windowY + 35,
135 state.windowX + state.windowWidth - 2,
136 state.windowY + state.windowHeight - 35};
137 }
138
139 [[nodiscard]] constexpr ChatRect fluffChatBarRect(const State& state) noexcept
140 {
141 return {state.fluffWindowX, state.fluffWindowY,
142 state.fluffWindowX + state.fluffWindowWidth - 1,
143 state.fluffWindowY + 18};
144 }
145 [[nodiscard]] constexpr ChatRect fluffChatBoxRect(const State& state) noexcept
146 {
147 return {state.fluffWindowX, state.fluffWindowY + 18,
148 state.fluffWindowX + state.fluffWindowWidth - 1,
149 state.fluffWindowY + state.fluffWindowHeight - 1};
150 }
151 [[nodiscard]] constexpr ChatRect fluffCloseButtonRect(const State& state) noexcept
152 {
153 return {state.fluffWindowX, state.fluffWindowY,
154 state.fluffWindowX + 19, state.fluffWindowY + 18};
155 }
156 [[nodiscard]] constexpr ChatRect fluffShadeButtonRect(const State& state) noexcept
157 {
158 return {state.fluffWindowX + state.fluffWindowWidth - 21,
159 state.fluffWindowY + 1,
160 state.fluffWindowX + state.fluffWindowWidth - 1,
161 state.fluffWindowY + 18};
162 }
163 [[nodiscard]] constexpr ChatRect fluffSizeButtonRect(const State& state) noexcept
164 {
165 return {state.fluffWindowX + state.fluffWindowWidth - 20,
166 state.fluffWindowY + state.fluffWindowHeight - 18,
167 state.fluffWindowX + state.fluffWindowWidth - 1,
168 state.fluffWindowY + state.fluffWindowHeight - 1};
169 }
170 [[nodiscard]] constexpr ChatRect fluffUpButtonRect(const State& state) noexcept
171 {
172 return {state.fluffWindowX + state.fluffWindowWidth - 18,
173 state.fluffWindowY + 18,
174 state.fluffWindowX + state.fluffWindowWidth - 1,
175 state.fluffWindowY + 35};
176 }
177 [[nodiscard]] constexpr ChatRect fluffDownButtonRect(const State& state) noexcept
178 {
179 return {state.fluffWindowX + state.fluffWindowWidth - 18,
180 state.fluffWindowY + state.fluffWindowHeight - 35,
181 state.fluffWindowX + state.fluffWindowWidth - 1,
182 state.fluffWindowY + state.fluffWindowHeight - 18};
183 }
184 [[nodiscard]] constexpr ChatRect fluffScrollbarRect(const State& state) noexcept
185 {
186 return {state.fluffWindowX + state.fluffWindowWidth - 17,
187 state.fluffWindowY + 35,
188 state.fluffWindowX + state.fluffWindowWidth - 2,
189 state.fluffWindowY + state.fluffWindowHeight - 35};
190 }
191 [[nodiscard]] constexpr ChatRect fluffCategoryRect(
192 const State& state, std::size_t category) noexcept
193 {
194 const int left = state.fluffWindowX + state.fluffWindowWidth -
195 143 + static_cast<int>(category) * 20;
196 return {left, state.fluffWindowY + 2,
197 left + 20, state.fluffWindowY + 17};
198 }
199 [[nodiscard]] constexpr ChatRect recipientButtonRect(
200 const State& state, std::size_t ordinal) noexcept
201 {
202 const int left = state.windowX + state.windowWidth -
203 (static_cast<int>(ordinal) + 5) * 19 + 3;
204 return {left, state.windowY + 3, left + 17, state.windowY + 15};
205 }
206 [[nodiscard]] bool processOptionPanelClick(int x, int y) noexcept
207 {
208 if (!runtime.optionsOpen || !optionPanelRect(runtime).contains(x, y))
209 return false;
210 if (fontSizeDownRect(runtime).contains(x, y) && runtime.fontSize < 14)
211 ++runtime.fontSize;
212 else if (fontSizeUpRect(runtime).contains(x, y) && runtime.fontSize > 7)
213 --runtime.fontSize;
214 else if (bgAlphaUpRect(runtime).contains(x, y) && runtime.backgroundAlphaIndex > 0)
215 --runtime.backgroundAlphaIndex;
216 else if (bgAlphaDownRect(runtime).contains(x, y) && runtime.backgroundAlphaIndex < 16)
217 ++runtime.backgroundAlphaIndex;
218 else if (textAlphaUpRect(runtime).contains(x, y) && runtime.textAlphaIndex > 1)
219 --runtime.textAlphaIndex;
220 else if (textAlphaDownRect(runtime).contains(x, y) && runtime.textAlphaIndex < 16)
221 ++runtime.textAlphaIndex;
222 return true;
223 }
224
226 const uimsg::Message& message, rules::PlayerNumber sender, std::uint32_t eligible, bool networkMode)
227 {
228 if (!runtime.fluffOpen) return false;
229 const int x = static_cast<int>(message.numberA);
230 const int y = static_cast<int>(message.numberB);
231
232 if (message.type == uimsg::Type::MouseMoved && runtime.fluffMoving)
233 {
234 runtime.fluffWindowX = x - runtime.fluffDragOffsetX;
235 runtime.fluffWindowY = y - runtime.fluffDragOffsetY;
236 return true;
237 }
238 if (message.type == uimsg::Type::MouseMoved &&
239 runtime.fluffScrolling && !runtime.fluffShaded)
240 {
241 const auto scrollbar = fluffScrollbarRect(runtime);
242 const int pos = std::clamp(y, scrollbar.top, scrollbar.bottom);
243 const int lineCount = FluffCategoryLineCounts[runtime.fluffCategory];
244 const int height = scrollbar.bottom - scrollbar.top;
245 runtime.fluffLineOffset = height > 0
246 ? ((pos - scrollbar.top) * (lineCount - 1)) / height
247 : 0;
248 return true;
249 }
250 if (message.type == uimsg::Type::MouseMoved &&
251 runtime.fluffSizing && !runtime.fluffShaded)
252 {
253 int width = x + runtime.fluffResizeOffsetX - runtime.fluffWindowX;
254 int height = y + runtime.fluffResizeOffsetY - runtime.fluffWindowY;
255 width = std::clamp(width, 246, 1601);
256 height = std::clamp(height, 99, 1201);
257 runtime.fluffWindowWidth = (width / 5) * 5 + 1;
258 runtime.fluffWindowHeight = ((height - 19) / 5) * 5 + 19;
259 return true;
260 }
261 if (message.type == uimsg::Type::MouseLeftUp && runtime.fluffMoving)
262 {
263 runtime.fluffMoving = false;
264 return true;
265 }
266 if (message.type == uimsg::Type::MouseLeftUp && runtime.fluffSizing)
267 {
268 runtime.fluffSizing = false;
269 return true;
270 }
271 if (message.type == uimsg::Type::MouseLeftUp && runtime.fluffScrolling)
272 {
273 runtime.fluffScrolling = false;
274 return true;
275 }
276 if (message.type == uimsg::Type::MouseMoved ||
277 message.type == uimsg::Type::MouseLeftUp)
278 {
279 return (!runtime.fluffShaded &&
280 fluffChatBoxRect(runtime).contains(x, y)) ||
281 fluffChatBarRect(runtime).contains(x, y);
282 }
283 if (message.type != uimsg::Type::MouseLeftDown) return false;
284
285 if (!runtime.fluffShaded && fluffScrollbarRect(runtime).contains(x, y))
286 {
287 runtime.fluffScrolling = true;
288 const auto scrollbar = fluffScrollbarRect(runtime);
289 const int pos = std::clamp(y, scrollbar.top, scrollbar.bottom);
290 const int lineCount = FluffCategoryLineCounts[runtime.fluffCategory];
291 const int height = scrollbar.bottom - scrollbar.top;
292 runtime.fluffLineOffset = height > 0
293 ? ((pos - scrollbar.top) * (lineCount - 1)) / height
294 : 0;
295 return true;
296 }
297 if (!runtime.fluffShaded && fluffUpButtonRect(runtime).contains(x, y))
298 {
299 if (runtime.fluffLineOffset > 0) --runtime.fluffLineOffset;
300 return true;
301 }
302 if (!runtime.fluffShaded && fluffDownButtonRect(runtime).contains(x, y))
303 {
304 const int lineCount = FluffCategoryLineCounts[runtime.fluffCategory];
305 if (runtime.fluffLineOffset < lineCount - 1) ++runtime.fluffLineOffset;
306 return true;
307 }
308 if (!runtime.fluffShaded && fluffSizeButtonRect(runtime).contains(x, y))
309 {
310 runtime.fluffSizing = true;
311 runtime.fluffResizeOffsetX = runtime.fluffWindowX + runtime.fluffWindowWidth - x;
312 runtime.fluffResizeOffsetY = runtime.fluffWindowY + runtime.fluffWindowHeight - y;
313 return true;
314 }
315 if (fluffShadeButtonRect(runtime).contains(x, y))
316 {
317 runtime.fluffShaded = !runtime.fluffShaded;
318 return true;
319 }
320 if (fluffCloseButtonRect(runtime).contains(x, y))
321 {
322 runtime.fluffOpen = false;
323 runtime.fluffMoving = false;
324 runtime.fluffSizing = false;
325 runtime.fluffScrolling = false;
326 return true;
327 }
328 for (std::size_t category = 0; category < 6; ++category)
329 {
330 if (!fluffCategoryRect(runtime, category).contains(x, y)) continue;
331 if (runtime.fluffCategory != category)
332 {
333 runtime.fluffCategory = category;
334 runtime.fluffLineOffset = 0;
335 runtime.fluffSelectedLine = -1;
336 runtime.draft.clear();
337 }
338 return true;
339 }
340 if (!runtime.fluffShaded && runtime.fontHeight > 0 &&
341 x >= runtime.fluffWindowX + 6 && x < runtime.fluffWindowX + runtime.fluffWindowWidth - 20 &&
342 y >= runtime.fluffWindowY + 18 && y < runtime.fluffWindowY + runtime.fluffWindowHeight - 4)
343 {
344 const int visibleLines = (runtime.fluffWindowHeight - 22) / runtime.fontHeight;
345 const int row = (y - runtime.fluffWindowY - 18) / runtime.fontHeight;
346 if (row < visibleLines)
347 (void)activateFluffLine(static_cast<std::size_t>(runtime.fluffLineOffset + row),
349 return true;
350 }
351 if (fluffChatBarRect(runtime).contains(x, y))
352 {
353 runtime.fluffMoving = true;
354 runtime.fluffDragOffsetX = x - runtime.fluffWindowX;
355 runtime.fluffDragOffsetY = y - runtime.fluffWindowY;
356 return true;
357 }
358 return (!runtime.fluffShaded &&
359 fluffChatBoxRect(runtime).contains(x, y)) ||
360 fluffChatBarRect(runtime).contains(x, y);
361 }
362
364 int x, int y, std::uint32_t eligible) noexcept
365 {
366 std::size_t ordinal{};
367 for (int player = static_cast<int>(rules::MaxPlayers) - 1;
368 player >= 0; --player)
369 {
370 const auto bit = 1u << static_cast<unsigned>(player);
371 if ((eligible & bit) == 0u) continue;
372 if (recipientButtonRect(runtime, ordinal).contains(x, y))
373 {
374 runtime.recipientMask ^= bit;
375 return true;
376 }
377 ++ordinal;
378 }
379 if (!allButtonRect(runtime).contains(x, y)) return false;
380 const bool enableAll = (runtime.recipientMask & eligible) != eligible;
381 runtime.recipientMask = enableAll
382 ? (1u << rules::MaxPlayers) - 1u : 0u;
383 return true;
384 }
385
386 [[nodiscard]] std::u16string decodeBlob(
387 const std::vector<std::uint8_t>& bytes)
388 {
389 std::u16string text;
390 if (bytes.empty() || (bytes.size() % 2u) != 0u)
391 return text;
392
393 const auto maxUnits = std::min(
394 bytes.size() / 2u, MaxInputCharacters + 1u);
395 for (std::size_t index = 0; index < maxUnits; ++index)
396 {
397 const auto unit = static_cast<char16_t>(
398 static_cast<std::uint16_t>(bytes[index * 2u]) |
399 (static_cast<std::uint16_t>(bytes[index * 2u + 1u]) << 8u));
400 if (unit == u'\0')
401 break;
402 text.push_back(unit);
403 }
404 return text;
405 }
406 [[nodiscard]] std::u16string decodeStringA(
407 const std::array<wchar_t, 80>& text)
408 {
409 std::u16string result;
410 for (const wchar_t value : text)
411 {
412 if (value == L'\0' || result.size() >= MaxInputCharacters)
413 break;
414 const auto code = static_cast<std::uint32_t>(value);
415 if constexpr (sizeof(wchar_t) == 2)
416 {
417 result.push_back(static_cast<char16_t>(code));
418 }
419 else if (code <= 0xFFFFu)
420 {
421 result.push_back(static_cast<char16_t>(code));
422 }
423 else if (code <= 0x10FFFFu && result.size() + 1u < MaxInputCharacters)
424 {
425 const auto adjusted = code - 0x10000u;
426 result.push_back(static_cast<char16_t>(
427 0xD800u + ((adjusted >> 10u) & 0x3FFu)));
428 result.push_back(static_cast<char16_t>(
429 0xDC00u + (adjusted & 0x3FFu)));
430 }
431 }
432 return result;
433 }
434
435 [[nodiscard]] bool appendUtf8(std::string_view utf8)
436 {
437 bool changed{};
438 for (std::size_t i = 0; i < utf8.size() &&
439 runtime.draft.size() < MaxInputCharacters;)
440 {
441 const auto first = static_cast<std::uint8_t>(utf8[i]);
442 std::uint32_t code{};
443 std::size_t count{};
444 if (first < 0x80u) { code = first; count = 1; }
445 else if ((first & 0xE0u) == 0xC0u) { code = first & 0x1Fu; count = 2; }
446 else if ((first & 0xF0u) == 0xE0u) { code = first & 0x0Fu; count = 3; }
447 else if ((first & 0xF8u) == 0xF0u) { code = first & 0x07u; count = 4; }
448 else { ++i; continue; }
449 if (i + count > utf8.size()) break;
450 bool valid = true;
451 for (std::size_t j = 1; j < count; ++j)
452 {
453 const auto continuation = static_cast<std::uint8_t>(utf8[i + j]);
454 if ((continuation & 0xC0u) != 0x80u) { valid = false; break; }
455 code = (code << 6u) | (continuation & 0x3Fu);
456 }
457 if (!valid) { ++i; continue; }
458 const bool overlong = (count == 2 && code < 0x80u) ||
459 (count == 3 && code < 0x800u) || (count == 4 && code < 0x10000u);
460 if (overlong || code > 0x10FFFFu || (code >= 0xD800u && code <= 0xDFFFu))
461 { i += count; continue; }
462 if (code <= 0xFFFFu)
463 {
464 runtime.draft.push_back(static_cast<char16_t>(code));
465 changed = true;
466 }
467 else if (runtime.draft.size() + 1u < MaxInputCharacters)
468 {
469 code -= 0x10000u;
470 runtime.draft.push_back(static_cast<char16_t>(0xD800u + (code >> 10u)));
471 runtime.draft.push_back(static_cast<char16_t>(0xDC00u + (code & 0x3FFu)));
472 changed = true;
473 }
474 i += count;
475 }
476 return changed;
477 }
478
479 [[nodiscard]] std::int64_t fluffMessageId(
480 std::size_t category, std::size_t line) noexcept
481 {
482 if (category >= FluffCategoryLineCounts.size() ||
483 line >= static_cast<std::size_t>(FluffCategoryLineCounts[category]))
484 return 0;
486 static_cast<std::int64_t>(line);
487 }
488
491 std::int64_t cannedTextId, actions::Message& result)
492 {
493 if (!validSender(from) || !validTarget(to) || cannedTextId <= 0)
494 return false;
495 actions::Message message{};
496 message.action = actions::Type::TextChat;
497 message.fromPlayer = from;
498 message.toPlayer = rules::BankPlayer;
499 message.numberA = to;
500 message.numberC = cannedTextId;
501 result = std::move(message);
502 return true;
503 }
504
505 void pushInputHistory(std::u16string_view text)
506 {
507 if (text.empty()) return;
508 if (runtime.inputHistoryCount < InputHistoryCapacity)
509 {
510 runtime.inputHistory[runtime.inputHistoryCount++] = text;
511 return;
512 }
513 std::move(runtime.inputHistory.begin() + 1,
514 runtime.inputHistory.end(), runtime.inputHistory.begin());
515 runtime.inputHistory.back().assign(text);
516 }
517
518 void pushEntry(Entry entry)
519 {
521 {
522 entry.displayName = playerNames[entry.from];
523 entry.senderNameCaptured = true;
524 }
525 std::size_t slot{};
526 if (runtime.count < HistoryCapacity)
527 {
528 slot = (runtime.first + runtime.count) % HistoryCapacity;
529 ++runtime.count;
530 }
531 else
532 {
533 slot = runtime.first;
534 runtime.first = (runtime.first + 1u) % HistoryCapacity;
535 }
536 runtime.history[slot] = std::move(entry);
537 ++runtime.historyRevision;
538 runtime.followLatest = true;
539 runtime.outputOffset = runtime.count == 0 ? 0 : runtime.count - 1u;
540 runtime.inputHistoryOffset = 0;
541 }
542 }
544 {
545 for (std::size_t player = 0; player < playerNames.size(); ++player)
546 playerNames[player] = gameState.players[player].name;
547 playerNamesReady = true;
548 }
549
551 std::size_t visibleLines) noexcept
552 {
553 runtime.wrappedOutputLines = wrappedLines;
554 runtime.fontHeight = std::max(height, 1);
555 runtime.outputLinesInWindow = visibleLines;
556 runtime.outputLayoutReady = true;
557 if (runtime.followLatest)
559 else
560 runtime.outputOffset = wrappedLines == 0 ? 0 :
561 std::min(runtime.outputOffset, wrappedLines - 1);
562 runtime.followLatest = false;
563 }
564
565 void setFluffSelectionText(std::int64_t cannedTextId, std::u16string_view text)
566 {
567 if (cannedTextId != 0 && cannedTextId == selectedFluffMessageId())
568 runtime.draft.assign(text);
569 }
570
572 {
573 runtime = {};
574 playerNames = {};
575 playerNamesReady = false;
576 }
577
579 {
580 // CHAT_Toggle() preserves CHAT_shade / CHAT_options across close/open.
581 runtime.boxActive = !runtime.boxActive;
582 }
583
584 void setRecipientMask(std::uint32_t mask) noexcept
585 {
586 runtime.recipientMask = mask & ((1u << rules::MaxPlayers) - 1u);
587 }
588
589 std::uint32_t recipientMask() noexcept
590 {
591 return runtime.recipientMask;
592 }
593
595 {
596 return runtime.eligibleRecipients;
597 }
598
600 {
601 if (runtime.fluffSelectedLine < 0) return 0;
602 return fluffMessageId(runtime.fluffCategory,
603 static_cast<std::size_t>(runtime.fluffSelectedLine));
604 }
605
607 std::size_t line, rules::PlayerNumber sender,
608 std::uint32_t eligibleRecipients, bool networkMode)
609 {
610 if (!networkMode || !runtime.fluffOpen ||
611 runtime.fluffCategory >= FluffCategoryLineCounts.size() ||
612 line >= static_cast<std::size_t>(
613 FluffCategoryLineCounts[runtime.fluffCategory]))
614 return false;
615
616 const int selectedLine = static_cast<int>(line);
617 if (runtime.fluffSelectedLine != selectedLine)
618 {
619 runtime.fluffSelectedLine = selectedLine;
620 runtime.draft.clear();
621 return true;
622 }
623
624 const auto cannedTextId = fluffMessageId(runtime.fluffCategory, line);
625 if (!validSender(sender) || cannedTextId == 0) return true;
626
627 const auto playerMask = (1u << rules::MaxPlayers) - 1u;
629 runtime.eligibleRecipients = eligibleRecipients;
630 const auto recipients = runtime.recipientMask & eligibleRecipients;
631
633 {
634 actions::Message action{};
636 cannedTextId, action) && messaging::sendAction(action))
637 {
639 runtime.fluffSelectedLine = -1;
640 runtime.draft.clear();
641 }
642 return true;
643 }
644
645 std::array<actions::Message, rules::MaxPlayers> batch{};
646 std::size_t count{};
647 for (rules::PlayerNumber player = 0; player < rules::MaxPlayers; ++player)
648 if ((recipients & (1u << player)) != 0u &&
649 buildCannedTextAction(sender, player, cannedTextId, batch[count]))
650 ++count;
651
652 const auto queued = messaging::queuedActionCount();
653 if (queued > messaging::MessageQueueCapacity ||
655 return true;
656 for (std::size_t index = 0; index < count; ++index)
658 return true;
659
660 Entry echo{};
661 echo.from = sender;
662 echo.to = sender;
663 echo.cannedTextId = cannedTextId;
664 echo.privateMessage = true;
665 pushEntry(std::move(echo));
667 runtime.fluffSelectedLine = -1;
668 runtime.draft.clear();
669 return true;
670 }
671
673 const uimsg::Message& message,
675 std::uint32_t eligibleRecipients,
676 bool networkMode)
677 {
678 const auto playerMask = (1u << rules::MaxPlayers) - 1u;
680 runtime.eligibleRecipients = networkMode ? eligibleRecipients : 0u;
681
682 if (message.type == uimsg::Type::KeyboardReleased &&
683 message.numberA == SDL_SCANCODE_TAB)
684 {
685 if (networkMode) toggle();
686 return networkMode;
687 }
688 if (!networkMode)
689 {
690 runtime.boxActive = false;
691 runtime.fluffOpen = false;
692 runtime.shaded = false;
693 runtime.fluffShaded = false;
694 return false;
695 }
696
697 const int pointerX = static_cast<int>(message.numberA);
698 const int pointerY = static_cast<int>(message.numberB);
699 if (!runtime.boxActive)
700 return false;
701
702 const bool activeFluffGesture = runtime.fluffMoving ||
703 runtime.fluffSizing || runtime.fluffScrolling;
705 return true;
706
707 if (message.type == uimsg::Type::MouseMoved && runtime.moving)
708 {
709 runtime.windowX = pointerX - runtime.dragOffsetX;
710 runtime.windowY = pointerY - runtime.dragOffsetY;
711 return true;
712 }
713 if (message.type == uimsg::Type::MouseMoved &&
714 runtime.scrolling && !runtime.shaded)
715 {
716 const auto scrollbar = chatScrollbarRect(runtime);
717 const int pos = std::clamp(pointerY, scrollbar.top, scrollbar.bottom);
718 const int height = scrollbar.bottom - scrollbar.top;
719 runtime.followLatest = false;
720 runtime.outputOffset = outputLineCount() > 1u && height > 0
721 ? static_cast<std::size_t>(
722 (static_cast<std::size_t>(pos - scrollbar.top) * (outputLineCount() - 1u)) / static_cast<std::size_t>(height))
723 : 0u;
724 return true;
725 }
726 if (message.type == uimsg::Type::MouseMoved &&
727 runtime.sizing && !runtime.shaded)
728 {
729 int width = pointerX + runtime.resizeOffsetX - runtime.windowX;
730 int height = pointerY + runtime.resizeOffsetY - runtime.windowY;
731 width = std::clamp(width, 246, 1601);
732 height = std::clamp(height, 99, 1201);
733 width = (width / 5) * 5 + 1;
734 height = ((height - 19) / 5) * 5 + 19;
735 runtime.windowWidth = width;
736 runtime.windowHeight = height;
737 return true;
738 }
739 if (message.type == uimsg::Type::MouseLeftUp && runtime.moving)
740 {
741 runtime.moving = false;
742 return true;
743 }
744 if (message.type == uimsg::Type::MouseLeftUp && runtime.sizing)
745 {
746 runtime.sizing = false;
747 return true;
748 }
749 if (message.type == uimsg::Type::MouseLeftUp && runtime.scrolling)
750 {
751 runtime.scrolling = false;
752 return true;
753 }
754
755 if (message.type == uimsg::Type::MouseLeftDown)
756 {
757 const int x = static_cast<int>(message.numberA);
758 const int y = static_cast<int>(message.numberB);
760 return true;
762 return true;
763 if (!runtime.shaded && chatScrollbarRect(runtime).contains(x, y))
764 {
765 runtime.scrolling = true;
766 const auto scrollbar = chatScrollbarRect(runtime);
767 const int pos = std::clamp(y, scrollbar.top, scrollbar.bottom);
768 const int height = scrollbar.bottom - scrollbar.top;
769 runtime.followLatest = false;
770 runtime.outputOffset = outputLineCount() > 1u && height > 0
771 ? static_cast<std::size_t>(
772 (static_cast<std::size_t>(pos - scrollbar.top) * (outputLineCount() - 1u)) / static_cast<std::size_t>(height))
773 : 0u;
774 return true;
775 }
776 if (!runtime.shaded && chatUpButtonRect(runtime).contains(x, y))
777 {
778 runtime.followLatest = false;
779 if (runtime.outputOffset > 0u) --runtime.outputOffset;
780 return true;
781 }
782 if (!runtime.shaded && chatDownButtonRect(runtime).contains(x, y))
783 {
784 runtime.followLatest = false;
785 if (outputLineCount() > 0u && runtime.outputOffset + 1u < outputLineCount())
786 ++runtime.outputOffset;
787 return true;
788 }
789 if (!runtime.shaded && sizeButtonRect(runtime).contains(x, y))
790 {
791 runtime.sizing = true;
792 runtime.resizeOffsetX = runtime.windowX + runtime.windowWidth - x;
793 runtime.resizeOffsetY = runtime.windowY + runtime.windowHeight - y;
794 return true;
795 }
796 if (optionsButtonRect(runtime).contains(x, y))
797 {
798 runtime.optionsOpen = !runtime.optionsOpen;
799 if (runtime.optionsOpen && runtime.shaded)
800 runtime.shaded = false;
801 return true;
802 }
803 if (shadeButtonRect(runtime).contains(x, y))
804 {
805 runtime.shaded = !runtime.shaded;
806 if (runtime.shaded) runtime.optionsOpen = false;
807 return true;
808 }
809 if (closeButtonRect(runtime).contains(x, y))
810 {
811 runtime.boxActive = false;
812 return true;
813 }
814 if (fluffButtonRect(runtime).contains(x, y))
815 {
816 runtime.fluffOpen = !runtime.fluffOpen;
817 return true;
818 }
819 if (chatBarRect(runtime).contains(x, y))
820 {
821 runtime.moving = true;
822 runtime.dragOffsetX = x - runtime.windowX;
823 runtime.dragOffsetY = y - runtime.windowY;
824 return true;
825 }
826 if (runtime.shaded) return false;
827 if (runtime.fontHeight > 0 && x >= runtime.windowX + 5 &&
828 x < runtime.windowX + runtime.windowWidth - 20 &&
829 y >= runtime.windowY + runtime.windowHeight - 4 - runtime.fontHeight &&
831 {
832 runtime.fluffSelectedLine = -1;
833 runtime.draft.clear();
834 return true;
835 }
836 }
837
838 if (message.type == uimsg::Type::MouseMoved ||
839 message.type == uimsg::Type::MouseLeftUp ||
841 {
842 if ((!runtime.shaded && chatBoxRect(runtime).contains(pointerX, pointerY)) ||
844 return true;
845 if (processFluffPointer(message, sender, eligibleRecipients, networkMode))
846 return true;
847 }
848
849 if (runtime.shaded)
850 return false;
851
852 if (message.type == uimsg::Type::TextInput)
853 {
854 if (runtime.fluffSelectedLine < 0)
855 (void)appendUtf8(message.text);
856 return true;
857 }
858 if (message.type != uimsg::Type::KeyboardPressed)
859 return false;
860
861 if (runtime.fluffSelectedLine >= 0)
863 static_cast<std::size_t>(runtime.fluffSelectedLine),
864 sender, eligibleRecipients, networkMode);
865
866 const auto key = static_cast<SDL_Scancode>(message.numberA);
868 {
869 runtime.followLatest = false;
870 if (runtime.outputOffset > 0u) --runtime.outputOffset;
871 return true;
872 }
874 {
875 runtime.followLatest = false;
876 if (outputLineCount() > 0u && runtime.outputOffset + 1u < outputLineCount())
877 ++runtime.outputOffset;
878 return true;
879 }
880 if (key == SDL_SCANCODE_UP)
881 {
882 if (runtime.inputHistoryOffset < runtime.inputHistoryCount)
883 {
884 ++runtime.inputHistoryOffset;
885 runtime.draft = runtime.inputHistory[
886 runtime.inputHistoryCount - runtime.inputHistoryOffset];
887 }
888 return true;
889 }
890 if (key == SDL_SCANCODE_DOWN)
891 {
892 if (runtime.inputHistoryOffset > 1)
893 {
894 --runtime.inputHistoryOffset;
895 runtime.draft = runtime.inputHistory[
896 runtime.inputHistoryCount - runtime.inputHistoryOffset];
897 }
898 return true;
899 }
901 {
902 if (!runtime.draft.empty())
903 {
904 const auto last = runtime.draft.back();
905 runtime.draft.pop_back();
906 if (last >= 0xDC00 && last <= 0xDFFF && !runtime.draft.empty() &&
907 runtime.draft.back() >= 0xD800 && runtime.draft.back() <= 0xDBFF)
908 runtime.draft.pop_back();
909 }
910 return true;
911 }
913 return true;
914 if (runtime.draft.empty() || !validSender(sender))
915 return true;
916
919 const auto selected = runtime.recipientMask & eligibleRecipients;
920 if (eligibleRecipients == 0u || selected == eligibleRecipients)
921 {
923 runtime.draft.clear();
924 return true;
925 }
926
927 std::array<actions::Message, rules::MaxPlayers> batch{};
928 std::size_t count{};
929 for (rules::PlayerNumber player = 0; player < rules::MaxPlayers; ++player)
930 if ((selected & (1u << player)) != 0u &&
931 buildTextAction(sender, player, runtime.draft, batch[count]))
932 ++count;
933
934 const auto queued = messaging::queuedActionCount();
935 if (queued > messaging::MessageQueueCapacity ||
937 return true;
938 for (std::size_t index = 0; index < count; ++index)
940 return true;
941
942 Entry echo{};
943 echo.from = sender;
944 echo.to = sender;
945 echo.privateMessage = true;
946 echo.text = runtime.draft;
947 pushEntry(std::move(echo));
948 runtime.draft.clear();
949 return true;
950 }
951
953 {
954 if (message.action != actions::Type::NotifyTextChat ||
955 message.numberA < 0 || message.numberA > rules::AllPlayers ||
956 message.numberB < 0 ||
957 (message.numberB >= rules::MaxPlayers &&
959 return false;
960
961 Entry entry{};
962 entry.to = static_cast<rules::PlayerNumber>(message.numberA);
963 entry.from = static_cast<rules::PlayerNumber>(message.numberB);
964 entry.cannedTextId = message.numberC;
965 entry.privateMessage = entry.to != rules::AllPlayers;
966 entry.text = decodeBlob(message.binaryDataA);
967 if (entry.text.empty())
968 entry.text = decodeStringA(message.stringA);
969
970 pushEntry(std::move(entry));
971 runtime.boxActive = true;
972 runtime.shaded = false;
973 return true;
974 }
978 std::u16string_view text,
979 actions::Message& result)
980 {
981 if (!validSender(from) || !validTarget(to) || text.empty() ||
982 text.size() > MaxInputCharacters)
983 return false;
984
985 actions::Message message{};
987 message.fromPlayer = from;
988 message.toPlayer = rules::BankPlayer;
989 message.numberA = to;
990 message.numberC = 0;
991 message.binaryDataA.reserve((text.size() + 1u) * 2u);
992 for (const char16_t unit : text)
993 {
994 const auto value = static_cast<std::uint16_t>(unit);
995 message.binaryDataA.push_back(static_cast<std::uint8_t>(value & 0xFFu));
996 message.binaryDataA.push_back(static_cast<std::uint8_t>(value >> 8u));
997 }
998 message.binaryDataA.push_back(0);
999 message.binaryDataA.push_back(0);
1000 result = std::move(message);
1001 return true;
1002 }
1003
1007 std::u16string_view text)
1008 {
1009 actions::Message message{};
1010 return buildTextAction(from, to, text, message) &&
1011 messaging::sendAction(message);
1012 }
1013
1015 {
1016 return runtime;
1017 }
1018
1019 const Entry* entryAt(std::size_t index) noexcept
1020 {
1021 if (index >= runtime.count)
1022 return nullptr;
1023 return &runtime.history[(runtime.first + index) % HistoryCapacity];
1024 }
1025}
std::size_t count
std::string_view text
std::atomic< float > left
std::atomic< float > right
std::vector< std::byte > bytes
int bottom
int top
fonts::Runtime & runtime
PlayerNumber slot
std::uint64_t key
std::uint32_t height
std::uint32_t width
unsigned state
unsigned index
data::DataTag first
bool valid
std::uint16_t value
Definition Timers.cpp:16
bool processInput(const uimsg::Message &message, rules::PlayerNumber sender, std::uint32_t eligibleRecipients, bool networkMode)
std::uint32_t recipientMask() noexcept
bool buildTextAction(rules::PlayerNumber from, rules::PlayerNumber to, std::u16string_view text, actions::Message &result)
constexpr std::size_t HistoryCapacity
std::uint32_t eligibleRecipients() noexcept
bool activateFluffLine(std::size_t line, rules::PlayerNumber sender, std::uint32_t eligibleRecipients, bool networkMode)
const State & stateReadOnly() noexcept
constexpr std::size_t MaxInputCharacters
std::int64_t selectedFluffMessageId() noexcept
void setPlayerNames(const rules::GameState &gameState)
void toggle() noexcept
void reset() noexcept
constexpr std::array< int, 6 > FluffCategoryLineCounts
constexpr std::size_t InputHistoryCapacity
void setOutputLayoutMetrics(std::size_t wrappedLines, int height, std::size_t visibleLines) noexcept
void setRecipientMask(std::uint32_t mask) noexcept
const Entry * entryAt(std::size_t index) noexcept
void setFluffSelectionText(std::int64_t cannedTextId, std::u16string_view text)
bool sendText(rules::PlayerNumber from, rules::PlayerNumber to, std::u16string_view text)
constexpr std::array< std::int64_t, 6 > FluffCategoryMessageStarts
bool processRuleMessage(const actions::Message &message)
std::size_t queuedActionCount()
constexpr std::size_t MessageQueueCapacity
Definition Messaging.hpp:14
bool sendAction(const actions::Message &message)
constexpr PlayerNumber BankPlayer
Definition RuleTypes.hpp:15
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr PlayerNumber SpectatorPlayer
Definition RuleTypes.hpp:22
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
constexpr PlayerNumber AllPlayers
Definition RuleTypes.hpp:17
std::vector< std::uint8_t > binaryDataA
Definition Actions.hpp:143
std::array< wchar_t, 80 > stringA
Definition Actions.hpp:140
rules::PlayerNumber from
rules::PlayerNumber to