Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
PlayerSelection.cpp
Go to the documentation of this file.
1#include "PlayerSelection.hpp"
4
5#include "Display.hpp"
7#include "IBar.hpp"
8#include "Messaging.hpp"
9#include "ChatRuntime.hpp"
10#include "RulesEngine.hpp"
11#include "LocalPlayers.hpp"
12#include "PlayerSetupFlow.hpp"
13#include "PlayerSetupSound.hpp"
14#include "UISound.hpp"
15#include "UserInterface.hpp"
16#include "RuleConfiguration.hpp"
17#include "RuleOptions.hpp"
18
19#include <SDL3/SDL.h>
20
21#include <algorithm>
22
24{
25 namespace
26 {
28
30
32 bool playbackAttached{};
34 std::vector<RuleHit> ruleHits;
37 bool loadRequested{};
38 bool networkPending{};
39 bool localPending{};
42 std::uint64_t pressSerial{};
43
45 {
46 return !history.highScores().empty();
47 }
48
50 {
52 setupSoundState, phase, globalState.playerInfo.aiLevel != 0,
53 globalState.numberOfPlayers);
54 for (std::size_t index = 0; index < update.count; ++index)
55 {
56 const auto& request = update.requests[index];
57 engine::playPennybagsVoice(request.voice, request.policy, false);
58 }
59 }
60
61 void syncSetupResourceContext() noexcept
62 {
63 const auto resources = startup::resources();
64 const data::ResourceContext context = resources
65 ? resources->context() : data::ResourceContext{};
66 setupFlowState.boardEdition = context.board;
67 setupFlowState.language = context.language;
68 }
69
71 {
72 return setupFlowState.playerLogCount != 0;
73 }
74
75 std::wstring messageString(
76 const actions::Message& message)
77 {
78 return std::wstring(message.stringA.data());
79 }
80
81
82 constexpr std::size_t EnterNameMaximumLength = 10;
83
84 bool pointInside(
85 std::int64_t x,
86 std::int64_t y,
87 int left,
88 int top,
89 int right,
90 int bottom)
91 {
92 // Win32 PtInRect() original :
93 // left/top inclus, right/bottom exclus.
94 return
95 x >= left &&
96 x < right &&
97 y >= top &&
98 y < bottom;
99 }
100
101 bool addCharacterToName(std::uint32_t character)
102 {
103 // udpsel_NameScreen_AddLetterToNameField(): retain the native
104 // wide-character limit, but never insert half a UTF-16 pair.
105 if (character < 32 || (character >= 128 && character < 160))
106 return false;
107
108 auto& name = globalState.playerInfo.name;
109 const std::size_t length = name.size() -
110 (!name.empty() && name.back() == L'_' ? 1u : 0u);
111 const std::size_t units = sizeof(wchar_t) == 2 && character > 0xFFFFu ? 2u : 1u;
113 return false;
114
115 name.resize(length);
116 if constexpr (sizeof(wchar_t) == 2)
117 {
118 if (character > 0xFFFFu)
119 {
120 character -= 0x10000u;
121 name.push_back(static_cast<wchar_t>(0xD800u + (character >> 10u)));
122 name.push_back(static_cast<wchar_t>(0xDC00u + (character & 0x3FFu)));
123 }
124 else name.push_back(static_cast<wchar_t>(character));
125 }
126 else name.push_back(static_cast<wchar_t>(character));
127 name.push_back(L'_');
128 return true;
129 }
130
131 void addTextToName(std::string_view utf8)
132 {
133 // Decode bounded input as scalars, as in ChatRuntime. Invalid UTF-8
134 // must not introduce surrogate code points or overlong characters.
135 for (std::size_t i = 0; i < utf8.size();)
136 {
137 const auto first = static_cast<std::uint8_t>(utf8[i]);
138 std::uint32_t code{};
139 std::size_t count{};
140 if (first < 0x80u) { code = first; count = 1; }
141 else if ((first & 0xE0u) == 0xC0u) { code = first & 0x1Fu; count = 2; }
142 else if ((first & 0xF0u) == 0xE0u) { code = first & 0x0Fu; count = 3; }
143 else if ((first & 0xF8u) == 0xF0u) { code = first & 0x07u; count = 4; }
144 else { ++i; continue; }
145 if (i + count > utf8.size()) break;
146 bool valid = true;
147 for (std::size_t j = 1; j < count; ++j)
148 {
149 const auto continuation = static_cast<std::uint8_t>(utf8[i + j]);
150 if ((continuation & 0xC0u) != 0x80u) { valid = false; break; }
151 code = (code << 6u) | (continuation & 0x3Fu);
152 }
153 if (!valid) { ++i; continue; }
154 const bool overlong = (count == 2 && code < 0x80u) ||
155 (count == 3 && code < 0x800u) || (count == 4 && code < 0x10000u);
156 if (overlong || code > 0x10FFFFu || (code >= 0xD800u && code <= 0xDFFFu))
157 { i += count; continue; }
158 if (!addCharacterToName(code)) break;
159 i += count;
160 }
161 }
162
164 {
165 auto& name = globalState.playerInfo.name;
166 if (name.empty() || name == L"_")
167 {
168 name = L"_";
169 return false;
170 }
171
172 std::size_t length = name.size() - (name.back() == L'_' ? 1u : 0u);
173 --length;
174 if constexpr (sizeof(wchar_t) == 2)
175 {
176 const auto last = static_cast<std::uint32_t>(name[length]);
177 if (length > 0 && last >= 0xDC00u && last <= 0xDFFFu)
178 {
179 const auto previous = static_cast<std::uint32_t>(name[length - 1]);
180 if (previous >= 0xD800u && previous <= 0xDBFFu) --length;
181 }
182 }
183 name.resize(length);
184 name.push_back(L'_');
185 return true;
186 }
187
188 bool enteredNameIsValid()
189 {
190 // Dans le source :
191 // wcslen(UDPSEL_PlayerInfo.name) > 1
192 return globalState.playerInfo.name.size() > 1;
193 }
196 {
197 // Les deux enums reprennent l'ordre exact
198 // UDPSEL_SetupPhase de UDPsel.h.
199 return static_cast<
201 >(
202 static_cast<std::uint8_t>(
203 phase
204 )
205 );
206 }
207
208
211 {
212 return static_cast<
214 >(
215 static_cast<std::uint8_t>(
216 phase
217 )
218 );
219 }
220
221
223 {
224 // Retail token names come from the selected LANG bank.
225 const auto resources = startup::resources();
226 if (resources && resources->language() && resources->language()->catalog)
227 {
228 for (std::uint8_t token = 0; token < rules::MaxTokens; ++token)
229 {
230 const auto value = resources->language()->catalog->message(920 + token);
231 if (value)
232 {
233 std::wstring name;
234 for (const auto unit : **value) name.push_back(static_cast<wchar_t>(unit));
236 }
237 }
238 return;
239 }
240 // Source US names remain available for resource-free rules tests.
241
245 L"Cannon"
246 );
247
251 L"Race Car"
252 );
253
257 L"Dog"
258 );
259
263 L"Top Hat"
264 );
265
269 L"Iron"
270 );
271
275 L"Horse"
276 );
277
281 L"Battleship"
282 );
283
287 L"Shoe"
288 );
289
293 L"Thimble"
294 );
295
299 L"Wheelbarrow"
300 );
301
305 L"Money Bag"
306 );
307 }
308
309
311 {
312 const auto& displayState =
314
315
316 setupFlowState.phase =
319 .currentPlayerSetupPhase
320 );
321
322
323 setupFlowState.desiredPhase =
326 .desiredPlayerSetupPhase
327 );
328
329
330 setupFlowState.name =
331 globalState.playerInfo.name;
332
333
334 setupFlowState.token =
335 globalState.playerInfo.token;
336
337
338 setupFlowState.aiLevel =
339 static_cast<std::uint8_t>(
340 globalState.playerInfo.aiLevel
341 );
342
343
344 setupFlowState.startButtonPressed =
345 globalState.playerInfo
346 .startButtonPressed;
347
348
349 setupFlowState.customRulesDesired =
350 globalState.playerInfo
351 .customRulesDesired;
352
353
354 setupFlowState.citySelected =
355 globalState.playerInfo.citySelected;
356
357
358 setupFlowState.serverMode =
360
361
362 setupFlowState.hasPlayerLogEntries =
364
366 }
367
368
370 {
371 globalState.playerInfo.name =
372 setupFlowState.name;
373
374
375 globalState.playerInfo.token =
376 setupFlowState.token;
377
378
379 globalState.playerInfo.aiLevel =
380 setupFlowState.aiLevel;
381
382
383 globalState.playerInfo.startButtonPressed =
384 setupFlowState.startButtonPressed;
385
386
387 globalState.playerInfo.customRulesDesired =
388 setupFlowState.customRulesDesired;
389
390
391 globalState.playerInfo.citySelected =
392 setupFlowState.citySelected;
393 }
394
395
396 bool tokenForButton(
398 std::uint8_t& token)
399 {
401
402
403 switch (button)
404 {
405 case Button::TokenGun:
406 token =
408 return true;
409
410 case Button::TokenIron:
411 token =
413 return true;
414
415 case Button::TokenThimble:
416 token =
418 return true;
419
420 case Button::TokenCar:
421 token =
423 return true;
424
425 case Button::TokenHorse:
426 token =
428 return true;
429
430 case Button::TokenBarrow:
431 token =
433 return true;
434
435 case Button::TokenDog:
436 token =
438 return true;
439
440 case Button::TokenShip:
441 token =
443 return true;
444
445 case Button::TokenMoneyBag:
446 token =
448 return true;
449
450 case Button::TokenHat:
451 token =
453 return true;
454
455 case Button::TokenShoe:
456 token =
458 return true;
459
460 default:
461 return false;
462 }
463 }
464
465
469 {
472
473
474 // SELECTPLAYER : seuls les slots presents et le bouton MORE
475 // avec plus de huit entrees sont hotspots.
476 if (setupFlowState.phase == Phase::SelectPlayer)
477 {
478 if (button == Button::SelectPlayerMore)
479 return setupFlowState.playerLogCount >
481
482 if (button >= Button::SelectPlayerCard1 &&
483 button <= Button::SelectPlayerCard8)
484 {
485 const std::size_t slot =
486 static_cast<std::size_t>(
487 static_cast<std::uint8_t>(button) -
488 static_cast<std::uint8_t>(Button::SelectPlayerCard1));
489 return setupFlowState.playerLogPageStart + slot <
490 setupFlowState.playerLogCount;
491 }
492 }
493
494
495 // SELECTTOKEN :
496 // seuls les pions disponibles sont hotspots.
497 if (
498 setupFlowState.phase ==
499 Phase::SelectToken)
500 {
501 std::uint8_t token = 0;
502
503
504 if (
506 button,
507 token
508 ))
509 {
510 return
513 uiState,
514 token
515 );
516 }
517 }
518
519
520 // STARTADDREMOVE : conditions exactes du
521 // DISPLAY_UDPSEL_Show() local hors MS Zone.
522 if (
523 setupFlowState.phase ==
524 Phase::StartAddRemove)
525 {
526 switch (button)
527 {
528 case Button::AddHuman:
529 case Button::AddComputer:
530 return
531 uiState.numberOfPlayers <
533
534
535 case Button::RemovePlayer:
536 return
538 0;
539
540
541 case Button::StartGame:
542 return
544 uiState.numberOfPlayers >= 2 &&
546 humanCount() >= 1;
547
548
549 default:
550 break;
551 }
552 }
553
554
555 if (
556 setupFlowState.phase == Phase::CustomizeRules &&
557 button == Button::RulesOkay)
558 {
559 return messaging::serverMode();
560 }
561
562
563 // ENTERNAME :
564 // le bouton NEXT n'existe qu'avec au moins
565 // un caractère + le curseur "_".
566 if (
567 setupFlowState.phase ==
568 Phase::EnterName &&
569 button ==
570 Button::EnterNameNext)
571 {
572 return enteredNameIsValid();
573 }
574
575
576 return true;
577 }
578
579
582 {
583 for (rules::PlayerNumber player = 0;
584 player < uiState.numberOfPlayers;
585 ++player)
586 {
588 return player;
589 }
590
591 return rules::NobodyPlayer;
592 }
593
594
597 bool interim)
598 {
600
601 if (interim)
602 {
603 const auto current =
605
606 if (current < uiState.numberOfPlayers &&
608 {
609 sender = current;
610 }
611 }
612 else
613 {
615 }
616
618 return false;
619
622 uiState.options,
623 sender,
624 interim,
625 acceptance))
626 {
627 return false;
628 }
629
631 }
632
633
635 const ui::playersetup::Command& command)
636 {
639
640
641 switch (command.type)
642 {
645 {
646 // UDPSEL :
647 // AddLocalPlayer(..., FALSE)
650 uiState,
651 command.name,
652 command.token,
653 command.colour,
654 command.aiLevel,
655 false
656 );
657
658 break;
659 }
660
661
664 {
667 uiState,
668 command.player
669 );
670
671 break;
672 }
673
674
677 {
682 uiState
683 );
684
685
686 if (
687 localPlayer !=
689 {
690 // UDPsel.cpp exact :
691 //
692 // ACTION_START_GAME,
693 // AnyLocalPlayer(),
694 // RULE_BANK_PLAYER.
699 );
700 }
701
702 break;
703 }
704
705
708 {
709 globalState.playerInfo.citySelected = command.city;
710 display::state().city = command.city;
711 display::state().system = command.system;
712 display::state().customBoardPath.clear();
713 break;
714 }
715
716
719 {
721 break;
722 }
723
724
727 {
730 globalState.forcedRefresh = true;
732 break;
733 }
734
735
738 {
741 false);
742 break;
743 }
744
745
748 {
751 globalState.forcedRefresh = true;
753 break;
754 }
755
756
759 {
762 globalState.forcedRefresh = true;
764 break;
765 }
766
767
770 {
773 mutableState.options,
774 command.setupRule,
775 command.ruleChoice))
776 {
777 globalState.forcedRefresh = true;
779 }
780 break;
781 }
782
783
784 default:
785 break;
786 }
787 }
788
789
791 {
793
794
795 const auto target =
797 setupFlowState.phase
798 );
799
800
801 if (
802 target !=
804 .desiredPlayerSetupPhase)
805 {
807 }
808
809
810 globalState.forcedRefresh = true;
811 }
812
813
815 std::int64_t x,
816 std::int64_t y)
817 {
819
820
823
825 {
826 const int px = static_cast<int>(x), py = static_cast<int>(y);
827 for (const auto& hit : ruleHits)
828 {
829 if (!hit.rect.contains(px, py)) continue;
830 const auto command = ui::playersetup::customRuleChoice(
831 setupFlowState, uiState, hit.rule, hit.choice);
832 executeSetupCommand(command);
833 globalState.forcedRefresh = true;
834 return true;
835 }
836 const auto button = restoreRuleRect.contains(px, py)
841 {
843 globalState.forcedRefresh = true;
844 return true;
845 }
846 }
847
848
849 const auto button =
851 setupFlowState.phase,
852 static_cast<int>(x),
853 static_cast<int>(y),
854 setupFlowState.boardEdition
855 );
856
857
858 if (
859 button ==
861 {
862 return false;
863 }
864
865
866 if (
868 button,
869 uiState
870 ))
871 {
872 return true;
873 }
874
875
876 const ui::playersetup::Command command =
879 uiState,
880 button
881 );
882
883 pressedButton = button;
884 ++pressSerial;
885
886
888
889
891 command
892 );
893
894
896
897
898 return true;
899 }
900
901 void clearPlayerState()
902 {
903 globalState.players = {};
904 globalState.numberOfPlayers = 0;
905 }
906 }
907
909 {
910 globalState = {};
912 ruleHits.clear();
913 loadRequested = false;
914 networkPending = false;
915 localPending = false;
916 customBoardRequested = false;
917 pressedButton = ui::playersetup::Button::None;
918 pressSerial = 0;
920
921
925 );
926
927
929 if (history.configured())
931 userinterface::ruleStateReadOnly(), history.names());
932
933
935
936 // DISPLAY_UDPSEL_Initialize() original :
937 display::state().previousPlayerSetupPhase =
939
940 display::state().currentPlayerSetupPhase =
942
943 display::state().desiredPlayerSetupPhase =
945
946 return true;
947 }
948
949 void shutdown()
950 {
951 globalState = {};
953 ruleHits.clear();
954 loadRequested = false;
955 customBoardRequested = false;
956
957 setupFlowState = {};
959
961 }
962
964 {
966
967 if (phase == displayState.currentPlayerSetupPhase)
968 {
969 return;
970 }
971
972 // Port direct de UDPSEL_SwitchPhase().
973
974 switch (phase)
975 {
977 {
979 {
980 displayState.desiredPlayerSetupPhase =
982 }
983 else
984 {
985 // Source originale :
986 // pas de hi-score -> LOCALORNETWORK.
987 displayState.desiredPlayerSetupPhase =
989 }
990
991 break;
992 }
993
995 {
996 if (history.configured())
998 userinterface::ruleStateReadOnly(), history.names());
1000 {
1001 displayState.desiredPlayerSetupPhase =
1003 }
1004 else
1005 {
1006 // Source originale :
1007 // aucune ancienne identité ->
1008 // ENTERNAME directement.
1009 displayState.desiredPlayerSetupPhase =
1011 }
1012
1013 break;
1014 }
1015
1016 default:
1017 {
1018 displayState.desiredPlayerSetupPhase = phase;
1019 break;
1020 }
1021 }
1022 }
1023
1024 void update()
1025 {
1026 if (networkPending)
1027 {
1029 {
1030 networkPending = false;
1031 if (!chat::stateReadOnly().boxActive) chat::toggle();
1033 }
1034 else if (!messaging::gameplayNetwork())
1035 {
1036 networkPending = false;
1038 }
1039 }
1042
1043
1044 if (
1045 displayState.currentPlayerSetupPhase ==
1046 displayState.desiredPlayerSetupPhase)
1047 {
1048 return;
1049 }
1050
1051
1052 displayState.previousPlayerSetupPhase =
1053 displayState.currentPlayerSetupPhase;
1054
1055
1056 displayState.currentPlayerSetupPhase =
1057 displayState.desiredPlayerSetupPhase;
1058
1059
1060 displayState.showOnlyLocalPlayersOnIBar =
1061 false;
1062
1063
1064 displayState.showOnlyLocalAIPlayersOnIBar =
1065 false;
1066
1067
1068 // udpsel_StartPhase() original :
1069 //
1070 // REMOVEPLAYER n'autorise les clics que sur nos
1071 // propres joueurs.
1072 //
1073 // MESS_GameStartedByLobby n'est pas encore porté,
1074 // donc nous suivons ici le chemin local normal.
1075 if (
1076 displayState.currentPlayerSetupPhase ==
1078 {
1079 displayState.showOnlyLocalPlayersOnIBar =
1080 true;
1081 }
1082
1083
1084 // ====================================================
1085 // udpsel_StartPhase().
1086 //
1087 // Si clickButton() a déjà effectué la transition dans
1088 // PlayerSetupFlow, ne pas la refaire : cela éviterait,
1089 // entre autres, d'avancer deux fois le pion par défaut
1090 // d'une IA.
1091 // ====================================================
1092
1093 setupFlowState.serverMode =
1095
1097
1098
1099 setupFlowState.hasPlayerLogEntries =
1101
1102
1103 setupFlowState.name =
1104 globalState.playerInfo.name;
1105
1106
1107 setupFlowState.token =
1108 globalState.playerInfo.token;
1109
1110
1111 setupFlowState.aiLevel =
1112 static_cast<std::uint8_t>(
1113 globalState.playerInfo.aiLevel
1114 );
1115
1116
1117 setupFlowState.startButtonPressed =
1118 globalState.playerInfo
1119 .startButtonPressed;
1120
1121
1122 const auto targetPhase =
1125 .currentPlayerSetupPhase
1126 );
1127
1128
1129 if (
1130 setupFlowState.phase !=
1132 {
1137 );
1138 }
1139 else
1140 {
1141 setupFlowState.desiredPhase =
1143 }
1144
1145
1147 // Phase sound is selected when playback starts the incoming objects.
1148
1149
1150 // The renderer retains its outgoing objects until their clocks finish.
1151 displayState.previousPlayerSetupPhase =
1152 displayState.currentPlayerSetupPhase;
1153 }
1154
1155 void show()
1156 {
1157 // The owner-thread PlayerSelectionPlayback realizes the visual phase.
1158
1160 display::stateReadOnly().desired2DView;
1161
1162
1165 {
1166 return;
1167 }
1168
1169
1170 update();
1171
1172
1173 // Visual state is reconciled every owner-thread frame.
1174 globalState.forcedRefresh = false;
1175 }
1176
1178 {
1181 display::stateReadOnly().desired2DView, static_cast<int>(ui::localplayers::count()),
1182 display::stateReadOnly().system, history.highScores(), pressedButton, pressSerial};
1183 }
1184
1185 std::expected<void, std::string> configureHistory(std::filesystem::path path)
1186 {
1187 if (auto result = history.open(std::move(path)); !result) return result;
1189 userinterface::ruleStateReadOnly(), history.names());
1190 return {};
1191 }
1192
1194 {
1195 const bool result = loadRequested;
1196 loadRequested = false;
1197 return result;
1198 }
1199
1201 {
1202 const bool result = customBoardRequested;
1203 customBoardRequested = false;
1204 return result;
1205 }
1206
1207 std::expected<void, std::string> commitCustomBoard(std::filesystem::path assetRoot)
1208 {
1209 if (!assetRoot.is_absolute())
1210 return std::unexpected("custom board asset root must be absolute");
1211
1212 setupFlowState.citySelected = -1;
1213 setupFlowState.currencySelectionIndex = 0;
1214 globalState.playerInfo.citySelected = -1;
1215
1216 auto& displayState = display::state();
1217 displayState.city = -1;
1218 displayState.customBoardPath = std::move(assetRoot);
1219 if (setupFlowState.boardEdition == data::BoardEdition::Europe)
1220 {
1221 // Legacy UDOpts forces a custom board back to the installed
1222 // language's default currency (iLangId - 2), not the country
1223 // that happened to be selected when Load Board was pressed.
1224 const int language = static_cast<int>(setupFlowState.language);
1225 displayState.system = language >= 2 && language <= 10
1226 ? language - 2 : 0;
1227 }
1228 else
1229 {
1231 }
1232
1237 return {};
1238 }
1239
1241 {
1242 // UDPsel.cpp:3937/4001: phase entry and returning from Options both
1243 // re-evaluate sound. PlayerSetupSound retains the retail desired/playing
1244 // deduplication, including the process-lifetime Welcome guard.
1246 }
1247
1248 void setPlaybackState(bool interactable, std::span<const RuleHit> hits,
1250 {
1251 playbackAttached = true;
1252 playbackInteractable = interactable;
1253 ruleHits.assign(hits.begin(), hits.end());
1256 }
1257
1259 {
1260 if (history.configured())
1261 {
1262 const auto& game = userinterface::ruleStateReadOnly();
1263 std::vector<HistoryPlayer> players;
1264 for (rules::PlayerNumber i=0;i<game.numberOfPlayers && i<rules::MaxPlayers;++i)
1265 players.push_back({game.players[i].name,game.players[i].aiPlayerLevel,
1266 ui::localplayers::slotIsLocalPlayer(i)});
1267 if (auto result=history.gameStarted(players);!result)
1268 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Player history: %s",result.error().c_str());
1269 }
1270 }
1271
1273 {
1276 if (history.configured() && message.action == actions::Type::NotifyGameOver)
1277 {
1278 const auto& game = userinterface::ruleStateReadOnly();
1279 if (message.numberA>=0 && message.numberA<game.numberOfPlayers && message.numberA<rules::MaxPlayers)
1280 {
1281 const auto& winner=game.players[static_cast<std::size_t>(message.numberA)];
1282 if (auto result=history.gameOver(winner.name,static_cast<int>(message.numberB),winner.aiPlayerLevel);!result)
1283 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Player history: %s",result.error().c_str());
1284 }
1285 }
1286 switch (message.action)
1287 {
1289 {
1290 // UDPSEL_ProcessMessageToPlayer():
1291 // NOTIFY_NAME_PLAYER provoque une mise à jour
1292 // forcée du player setup.
1293
1294 if (message.numberA < 0 ||
1295 message.numberA >=
1296 static_cast<std::int64_t>(
1298 {
1299 break;
1300 }
1301
1302 const auto playerNo =
1303 static_cast<std::size_t>(message.numberA);
1304
1305 PlayerSlot& slot =
1306 globalState.players[playerNo];
1307
1308 slot.occupied = true;
1309 slot.name = messageString(message);
1310
1311 slot.token =
1312 static_cast<std::uint8_t>(message.numberB);
1313
1314 slot.colour =
1315 static_cast<std::uint8_t>(message.numberC);
1316
1317 slot.aiLevel =
1318 static_cast<std::uint8_t>(message.numberD);
1319
1320 globalState.forcedRefresh = true;
1322
1323 break;
1324 }
1325
1327 {
1328 // Port du début de
1329 // UDPSEL_ProcessMessageToPlayer(
1330 // NOTIFY_NUMBER_OF_PLAYERS).
1331
1332 display::state().flashCurrentToken = false;
1333
1334 const auto count =
1335 std::clamp<std::int64_t>(
1336 message.numberA,
1337 0,
1338 static_cast<std::int64_t>(
1340 );
1341
1342 // Source UDPsel ignores repeated counts unless a fresh game
1343 // explicitly requests setup reset. A joining peer's resync is
1344 // not a new game for the already present machines.
1345 if (globalState.firstTimeInitializationDone && !localPending &&
1346 message.numberB == 0 && globalState.numberOfPlayers == count)
1347 break;
1348
1349 globalState.numberOfPlayers =
1350 static_cast<std::uint8_t>(count);
1351
1352 if (count == 0 ||
1353 !globalState.firstTimeInitializationDone)
1354 {
1355 globalState.firstTimeInitializationDone = true;
1356
1357 if (count == 0)
1358 {
1360 }
1361
1362 // numberB == 1 identifie le reset emis pendant un
1363 // chargement : l'original n'ouvre pas Player Select.
1364 if (count == 0 && message.numberB == 0)
1365 {
1368
1369 const bool setupRequested =
1370 displayState.desired2DView ==
1372 displayState.desired2DView ==
1374
1375
1376 if (setupRequested)
1377 {
1378 const auto phase =
1379 displayState.currentPlayerSetupPhase;
1380
1381 if (phase != display::PlayerSetupPhase::None &&
1384 {
1387 );
1388
1391 );
1392 }
1393 }
1394 else
1395 {
1398 );
1399
1402 );
1403 }
1404 }
1405 }
1406 else
1407 {
1408 globalState.forcedRefresh = true;
1409 }
1410
1411 if (localPending && count == 0)
1412 {
1413 localPending = false;
1415 }
1416 break;
1417 }
1418
1420 {
1421 globalState.forcedRefresh = true;
1423 break;
1424 }
1425
1426
1428 {
1429 const std::wstring deadName =
1430 messageString(message);
1431
1432
1433 for (auto& slot :
1434 globalState.players)
1435 {
1436 if (
1437 slot.occupied &&
1438 slot.name == deadName)
1439 {
1440 slot = {};
1441 break;
1442 }
1443 }
1444
1445
1446 globalState.forcedRefresh = true;
1448
1449 break;
1450 }
1451
1453 {
1454 globalState.forcedRefresh = true;
1455
1456 if (globalState.playerInfo.startButtonPressed && message.numberB > 0)
1457 {
1458 const auto playerSet = static_cast<std::uint32_t>(message.numberB);
1461
1462 const auto current = ui::localplayers::currentUIPlayer();
1463 if (current < rules::MaxPlayers && current < uiState.numberOfPlayers)
1464 globalState.playerInfo.token = uiState.players[current].token;
1465 }
1466
1467 break;
1468 }
1469
1470
1472 {
1473 // UDPSEL_ProcessMessageToPlayer() original.
1474 if (
1475 message.numberA ==
1476 static_cast<std::int64_t>(
1478 ) &&
1479 message.numberB != 0 &&
1481 {
1484 CustomizeRules
1485 );
1486 }
1487
1488 break;
1489 }
1490
1492 {
1493 // L'original route ce message vers UDPSEL,
1494 // mais UDPSEL_ProcessMessageToPlayer ne lui
1495 // associe aucun traitement supplémentaire.
1496 break;
1497 }
1498
1499 default:
1500 break;
1501 }
1502 }
1503
1504
1506 const uimsg::Message& message)
1507 {
1510
1511
1512 // UDPSEL_ProcessMessage() ne travaille que sur
1513 // Pselect / PselectRules.
1514 if (
1515 displayState.desired2DView !=
1517 displayState.desired2DView !=
1519 {
1520 return;
1521 }
1522
1523
1524 const display::PlayerSetupPhase phase =
1525 displayState.currentPlayerSetupPhase;
1526
1527 // ENTERNAME accepts typing during anim-in, as in UDPsel. Clicks cannot
1528 // operate controls while the previous visual phase is still leaving.
1530 displayState.currentPlayerSetupPhase != displayState.desiredPlayerSetupPhase) &&
1532 return;
1533
1534
1535 // ----------------------------------------------------
1536 // SOURIS
1537 // ----------------------------------------------------
1538
1539 if (
1540 message.type ==
1542 {
1543 const std::int64_t x =
1544 message.numberA;
1545
1546 const std::int64_t y =
1547 message.numberB;
1548
1550 pointInside(x, y, 341, 452, 468, 488))
1551 {
1553 return;
1554 }
1555
1556
1557 // ------------------------------------------------
1558 // LOCAL / NETWORK
1559 //
1560 // Ce panneau précède PlayerSetupFlow.
1561 // ------------------------------------------------
1562
1563 if (
1564 phase ==
1566 LocalOrNetwork)
1567 {
1568 // Local game.
1569 if (
1571 x,
1572 y,
1573 291,
1574 222,
1575 511,
1576 284
1577 ))
1578 {
1579 networkPending = false;
1581 if (chat::stateReadOnly().boxActive) chat::toggle();
1583 globalState.firstTimeInitializationDone = false;
1586
1587
1588 return;
1589 }
1590
1591
1592 // Native transport: remain here until connection/admission succeeds.
1593 if (
1595 x,
1596 y,
1597 291,
1598 294,
1599 511,
1600 356
1601 ))
1602 {
1603 if (chat::stateReadOnly().boxActive) chat::toggle();
1606
1607 return;
1608 }
1609
1610
1611 // The UI owner consumes this and opens its existing load workflow.
1612 if (
1614 x,
1615 y,
1616 291,
1617 366,
1618 511,
1619 428
1620 ))
1621 {
1622 loadRequested = true;
1623 return;
1624 }
1625 }
1626
1627
1628 // ------------------------------------------------
1629 // ENTERNAME / SELECTTOKEN / STARTADDREMOVE /
1630 // REMOVEPLAYER / SELECTAISTRENGTH
1631 // ------------------------------------------------
1632
1633 if (
1635 x,
1636 y
1637 ))
1638 {
1639 return;
1640 }
1641 }
1642
1643
1644 // ----------------------------------------------------
1645 // TEXTE
1646 // ----------------------------------------------------
1647
1648 if (
1649 phase ==
1651 message.type ==
1653 {
1655 message.text
1656 );
1657
1658
1659 globalState.forcedRefresh =
1660 true;
1661
1662 return;
1663 }
1664
1665
1666 // ----------------------------------------------------
1667 // CLAVIER
1668 // ----------------------------------------------------
1669
1670 if (
1671 phase ==
1673 message.type ==
1675 {
1676 const SDL_Scancode key =
1677 static_cast<SDL_Scancode>(
1678 message.numberA
1679 );
1680
1681
1682 if (
1683 key ==
1685 {
1686 if (
1688 {
1689 globalState.forcedRefresh =
1690 true;
1691 }
1692
1693 return;
1694 }
1695
1696
1697 if (
1698 key ==
1700 key ==
1702 {
1703 if (enteredNameIsValid())
1704 {
1707 SelectToken
1708 );
1709
1710 }
1711
1712 return;
1713 }
1714 }
1715 }
1716
1718 rules::PlayerNumber player)
1719 {
1720 // UDPSEL_PlayerButtonClicked() original.
1721
1723
1724
1725 const auto command =
1729 player
1730 );
1731
1732
1734
1735
1737 command
1738 );
1739
1740
1742 }
1743
1745 {
1746 return globalState;
1747 }
1748
1750 {
1751 return globalState;
1752 }
1753}
1754
1755
1756
1757
1758
std::size_t count
std::array< rules::PlayerNumber, rules::MaxPlayers > players
const char * name
std::atomic< float > left
std::atomic< float > right
int bottom
int top
PlayerNumber slot
std::uint64_t key
unsigned index
data::DataTag first
int token
float units
Button button
std::string path
bool valid
std::uint16_t value
Definition Timers.cpp:16
const State & stateReadOnly() noexcept
void toggle() noexcept
State & state()
Definition Display.cpp:699
void setBackdrop(Screen2D screen)
Definition Display.cpp:363
const State & stateReadOnly()
Definition Display.cpp:704
void playWarningSound() noexcept
Definition Engine.cpp:1889
void playPennybagsVoice(udsound::PennybagsVoice line, udsound::TokenVoiceClipPolicy policy, bool watchAfterStart) noexcept
Definition Engine.cpp:1940
void restoreRuleTracking() noexcept
Definition IBar.cpp:1200
bool startConfiguredNetwork()
bool sendAction(const actions::Message &message)
void visualPhaseStarted(ui::playersetup::Phase phase) noexcept
void setPlaybackState(bool interactable, std::span< const RuleHit > hits, ui::playersetup::Rect restore, ui::playersetup::Rect shortGame)
bool consumeCustomBoardRequest() noexcept
std::expected< void, std::string > configureHistory(std::filesystem::path path)
void processLibraryMessage(const uimsg::Message &message)
std::expected< void, std::string > commitCustomBoard(std::filesystem::path assetRoot)
void playerButtonClicked(rules::PlayerNumber player)
void processMessage(const actions::Message &message)
void switchPhase(display::PlayerSetupPhase phase)
bool acceptedConfigurationMessage(const GameOptions &options, PlayerNumber fromPlayer, bool interim, actions::Message &result)
void setShortGameRules(GameOptions &options)
void setStandardMonopolyRules(GameOptions &options)
bool applySetupRuleChoice(GameOptions &options, SetupRule rule, std::uint8_t choice) noexcept
constexpr PlayerNumber BankPlayer
Definition RuleTypes.hpp:15
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr std::size_t MaxTokens
Definition RuleTypes.hpp:30
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
constexpr PlayerNumber NobodyPlayer
Definition RuleTypes.hpp:16
std::shared_ptr< const data::ResourceSnapshot > resources() noexcept
bool requestRemoveLocalPlayer(const GameState &uiState, PlayerNumber player)
PlayerNumber anyLocalPlayer(const GameState &uiState)
void setCurrentUIPlayerFromPlayerSet(const GameState &uiState, std::uint32_t playerSet)
bool slotIsLocalHumanPlayer(PlayerNumber slot)
bool requestAddLocalPlayer(const GameState &uiState, std::wstring_view name, std::uint8_t token, std::uint8_t colour, std::uint8_t aiLevel, bool takeOverAI)
constexpr std::uint8_t TokenCar
constexpr std::uint8_t TokenThimble
void setPlayerLogEntries(State &state, const rules::GameState &uiState, std::span< const std::wstring > entries)
constexpr std::uint8_t TokenHat
Command playerBarClicked(State &state, rules::PlayerNumber player)
constexpr std::uint8_t TokenShoe
Command clickButton(State &state, const GameState &uiState, Button button)
constexpr std::uint8_t TokenIron
bool tokenAvailable(const GameState &uiState, std::uint8_t token)
constexpr std::uint8_t TokenGun
constexpr std::size_t PlayerHistoryPageSize
constexpr std::uint8_t TokenBarrow
void initialize(State &state, bool serverMode)
Command customRuleChoice(State &state, const GameState &, rules::options::SetupRule rule, std::uint8_t choice)
void requestPhase(State &state, const GameState &uiState, Phase requested)
constexpr std::uint8_t TokenHorse
constexpr std::uint8_t TokenShip
constexpr std::uint8_t TokenDog
void setTokenName(State &state, std::uint8_t token, std::wstring_view name)
constexpr std::uint8_t TokenMoneyBag
Button buttonAt(Phase phase, int x, int y, data::BoardEdition edition)
Update startPhase(State &state, display::PlayerSetupPhase phase, bool aiPlayer, std::uint8_t numberOfPlayers) noexcept
void resetPlayback(State &state) noexcept
const rules::GameState & ruleStateReadOnly()
rules::GameState & ruleState()
std::array< wchar_t, 80 > stringA
Definition Actions.hpp:140
rules::options::SetupRule setupRule