Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
TradeUI.cpp
Go to the documentation of this file.
1#include "TradeUI.hpp"
2
3#include "IBarLayout.hpp"
4
5#include <SDL3/SDL_scancode.h>
6
7#include <algorithm>
8#include <limits>
9
10namespace monopoly::tradeui
11{
12 namespace
13 {
14 [[nodiscard]] bool validPlayer(
15 const rules::GameState& gameState,
16 rules::PlayerNumber player) noexcept
17 {
18 return player < gameState.numberOfPlayers &&
19 player < rules::MaxPlayers;
20 }
21
22 [[nodiscard]] bool activePlayer(
23 const rules::GameState& gameState,
24 rules::PlayerNumber player) noexcept
25 {
26 return validPlayer(gameState, player) &&
27 gameState.players[player].currentSquare < OffBoardSquare;
28 }
29
30 void updateJailCards(
31 State& state,
32 const rules::GameState& gameState) noexcept
33 {
34 state.jailCardDesired = {};
35 if (!validPlayer(gameState, state.playerA) ||
36 !validPlayer(gameState, state.playerB))
37 {
38 return;
39 }
40
41 for (std::size_t deck = 0; deck < gameState.cards.size(); ++deck)
42 {
43 const auto owner = gameState.cards[deck].jailOwner;
44 if (owner == state.playerA)
45 state.jailCardDesired[deck] = 1u << 0u;
46 else if (owner == state.playerB)
47 state.jailCardDesired[deck] = 1u << 1u;
48 }
49 }
50
52 const State& state,
53 const rules::GameState& gameState,
54 rules::PlayerNumber player) noexcept
55 {
56 return activePlayer(gameState, player) && player != state.playerA;
57 }
58
59 [[nodiscard]] std::optional<rules::TradeItemKind> itemKind(
60 std::int64_t value) noexcept
61 {
62 if (value < static_cast<std::int64_t>(rules::TradeItemKind::Cash) ||
63 value > static_cast<std::int64_t>(rules::TradeItemKind::FutureRent))
64 {
65 return std::nullopt;
66 }
67 return static_cast<rules::TradeItemKind>(value);
68 }
69
70 void recomputeCash(State& state, const rules::GameState& gameState) noexcept
71 {
72 if (!validPlayer(gameState, state.playerA) ||
73 !validPlayer(gameState, state.playerB))
74 {
75 state.cashDesired[0] = 0;
76 state.cashDesired[1] = 0;
77 return;
78 }
79 state.cashDesired[0] = gameState.players[state.playerA].cash -
80 state.cashDesired[2] + state.cashDesired[3];
81 state.cashDesired[1] = gameState.players[state.playerB].cash -
82 state.cashDesired[3] + state.cashDesired[2];
83 }
84
86 State& state, const rules::GameState& gameState) noexcept
87 {
88 // UDTrade_ProcessEverything rebuilds these fields from the retained
89 // items after a counteroffer swaps the two displayed participants.
90 state.cashDesired[2] = 0;
91 state.cashDesired[3] = 0;
93 for (const auto& item : state.items)
94 {
95 if (item.numberC == static_cast<std::int64_t>(rules::TradeItemKind::Cash))
96 {
97 if (item.numberA == state.playerA) state.cashDesired[2] = item.numberD;
98 else if (item.numberA == state.playerB) state.cashDesired[3] = item.numberD;
99 }
100 else if (item.numberC == static_cast<std::int64_t>(rules::TradeItemKind::JailCard) &&
101 item.numberD >= 0 &&
102 item.numberD < static_cast<std::int64_t>(gameState.cards.size()))
103 {
104 const auto deck = static_cast<std::size_t>(item.numberD);
105 const auto owner = gameState.cards[deck].jailOwner;
106 if (owner == state.playerA) state.jailCardDesired[deck] = 1u << 2u;
107 else if (owner == state.playerB) state.jailCardDesired[deck] = 1u << 3u;
108 }
109 }
111 }
112
113 [[nodiscard]] bool pressedKey(
114 const uimsg::Message& message, SDL_Scancode key) noexcept
115 {
116 return message.type == uimsg::Type::KeyboardPressed && message.numberA == key;
117 }
118
119 [[nodiscard]] std::optional<int> textDigit(const uimsg::Message& message) noexcept
120 {
121 if (message.type != uimsg::Type::TextInput || message.text.empty() ||
122 message.text.front() < '0' || message.text.front() > '9')
123 return std::nullopt;
124 return message.text.front() - '0';
125 }
126
127 [[nodiscard]] bool inEither(const Rect& a, const Rect& b, int x, int y) noexcept
128 {
129 return a.contains(x, y) || b.contains(x, y);
130 }
131
132 [[nodiscard]] Rect cashPopupRect(std::uint8_t side, const Rect& local) noexcept
133 {
134 const int x = 4 + static_cast<int>(side) * 600;
135 constexpr int y = 324;
136 return {local.left + x, local.top + y, local.right + x, local.bottom + y};
137 }
138
139 [[nodiscard]] bool removeFirstItem(
140 State& state, rules::TradeItemKind kind, std::optional<std::int64_t> value = std::nullopt)
141 {
142 const auto rawKind = static_cast<std::int64_t>(kind);
143 const auto it = std::find_if(state.items.begin(), state.items.end(),
144 [&](const actions::Message& item) {
145 return item.numberC == rawKind && (!value || item.numberD == *value);
146 });
147 if (it == state.items.end()) return false;
148 state.items.erase(it);
149 return true;
150 }
151
152 [[nodiscard]] bool writeCashItem(
153 State& state, const rules::GameState& gameState, std::uint8_t side, std::int64_t amount)
154 {
155 if (side > 1 || !validPlayer(gameState, state.playerA) ||
156 !validPlayer(gameState, state.playerB)) return false;
157 actions::Message item{};
158 item.numberA = side ? state.playerB : state.playerA;
159 item.numberB = side ? state.playerA : state.playerB;
160 item.numberC = static_cast<std::int64_t>(rules::TradeItemKind::Cash);
161 item.numberD = amount;
162 return addTradeItem(state, gameState, item);
163 }
164
165 void openCashDialog(State& state, std::uint8_t side) noexcept
166 {
167 state.cashOriginalOffers = {state.cashDesired[2], state.cashDesired[3]};
168 state.cashDialogSide = side;
169 state.cashTradeAmount = 0;
170 state.cashDialogFeedback = CashDialogFeedback::None;
171 state.cashDialogClosing = false;
172 state.cashDialogVisible = true;
173 }
174
175 constexpr int TradePropertyBasicWidth = 205;
176 constexpr int TradePropertyButtonWidth = 60;
177 constexpr int TradePropertyCardWidth = 36;
178 constexpr int TradePropertyCardHeight = 42;
179 constexpr int TradePropertyDeltaX = 3;
180 constexpr int TradePropertyDeltaY = 10;
181 constexpr std::array<std::array<int, 2>, 4> TradePropertyBoxOrigins{{
182 {{0, 225}}, {{600, 225}}, {{200, 244}}, {{400, 244}}
183 }};
184 constexpr std::array<int, 4> TradePropertyTopY{{40, 40, 45, 45}};
185 constexpr std::array<int, 28> TradePropertyHitOrder{{
186 5, 15, 25, 35, 12, 28, 1, 3,
187 6, 8, 9, 11, 13, 14, 16, 18,
188 19, 21, 23, 24, 26, 27, 29, 31,
189 32, 34, 37, 39
190 }};
191
193 PropertyProjection& projection,
194 int box,
195 PropertyMask visible,
196 bool modernHoldingsLayout) noexcept
197 {
198 if (box < 0 || box >= static_cast<int>(TradePropertyBoxOrigins.size())) return;
199
200 std::array<bool, 11> columns{};
201 int columnCount = 0;
202 int cardCount = 0;
203 for (int square = 0; square < static_cast<int>(rules::SquareCount); ++square)
204 {
205 const auto bit = ibar::layout::propertyBit(square);
206 if (bit == 0 || (visible & bit) == 0) continue;
207 const int order = ibar::layout::propertyBarOrder(square);
208 if (order < 0) continue;
209 ++cardCount;
210 const int column = order / 3;
211 if (!columns[static_cast<std::size_t>(column)])
212 {
213 columns[static_cast<std::size_t>(column)] = true;
214 ++columnCount;
215 }
216 }
217
218 std::array<int, 11> compactColumns{};
219 compactColumns.fill(-1);
220 int compact = 0;
221 for (std::size_t column = 0; column < columns.size(); ++column)
222 {
224 }
225
226 int widthApart = 0;
227 if (columnCount != 0)
228 {
229 if (box <= 1)
230 {
233 }
234 else
235 {
239 }
240 }
241
242 for (int square = 0; square < static_cast<int>(rules::SquareCount); ++square)
243 {
244 const auto bit = ibar::layout::propertyBit(square);
245 if (bit == 0 || (visible & bit) == 0) continue;
246 const int order = ibar::layout::propertyBarOrder(square);
247 if (order < 0) continue;
248
249 const int row = order % 3;
250 const int column = order / 3;
251 int x = 11 + TradePropertyDeltaX * row;
252 int y = TradePropertyTopY[static_cast<std::size_t>(box)] +
254 if (box <= 1)
255 {
256 x += column * widthApart;
257 if (order > 14)
258 {
259 x = 11 + (column - 5) * widthApart + TradePropertyDeltaX * row;
260 y += 65;
261 }
262 }
263 else
264 {
265 x += compactColumns[static_cast<std::size_t>(column)] * widthApart;
266 }
267
268 if (modernHoldingsLayout && box <= 1 && cardCount <= 8)
269 {
270 // The same geometry feeds rendering, picking and transfer
271 // endpoints. Keep retail geometry for crowded holdings/offers.
272 int gridIndex = 0;
274 {
276 if (otherBit && (visible & otherBit) &&
278 }
279 x = 11 + (gridIndex % 4) * 44;
280 y = TradePropertyTopY[static_cast<std::size_t>(box)] +
281 (gridIndex / 4) * 48;
282 }
283 const auto& origin = TradePropertyBoxOrigins[static_cast<std::size_t>(box)];
284 const int left = origin[0] + x;
285 const int top = origin[1] + y;
286 projection.hitRects[static_cast<std::size_t>(box)]
287 [static_cast<std::size_t>(square)] =
289
290 int priorityOrder = order;
291 if ((priorityOrder % 3) == 0) priorityOrder += 2;
292 else if (((priorityOrder - 2) % 3) == 0) priorityOrder -= 2;
293 projection.priorities[static_cast<std::size_t>(box)]
294 [static_cast<std::size_t>(square)] = 32 - priorityOrder;
295 }
296 }
297
298 [[nodiscard]] std::optional<std::size_t> contractIndex(
299 rules::TradeItemKind kind) noexcept
300 {
301 if (kind == rules::TradeItemKind::FutureRent) return 0u;
302 if (kind == rules::TradeItemKind::Immunity) return 1u;
303 return std::nullopt;
304 }
305
307 rules::TradeItemKind kind) noexcept
308 {
312 }
313
314 [[nodiscard]] Rect contractBoxBounds(std::size_t box) noexcept
315 {
316 const auto& origin = TradePropertyBoxOrigins[box];
317 return {origin[0], origin[1], origin[0] + 200, origin[1] + 225};
318 }
319
320 void clampContractListOffset(State& state) noexcept
321 {
322 if (state.contractList.empty()) state.contractListOffset = 0;
323 else state.contractListOffset = std::clamp(
324 state.contractListOffset, 0,
325 static_cast<int>(state.contractList.size()) - 1);
326 }
327
328 void rebuildContractPropertyList(State& state, std::uint32_t added)
329 {
330 // UDTrade_ProcessFutureTrade / ProcessImmunityTrade rebuild the
331 // selected deeds in property-bit order and scroll to the new one.
332 state.contractList.clear();
333 for (unsigned bit = 0; bit < 28; ++bit)
334 {
335 const auto mask = 1U << bit;
336 if ((state.contractProperties & mask) == 0) continue;
337 if (mask == added)
338 state.contractListOffset = static_cast<int>(state.contractList.size());
339 state.contractList.push_back({0, mask, false});
340 }
342 }
343
344 void closeContractDialog(State& state) noexcept
345 {
346 state.contractDialogVisible = false;
347 state.contractDialogMode = 0;
348 state.contractDialogSide = 0;
349 state.contractProperties = 0;
350 state.contractAmount = 0;
351 state.contractListOffset = 0;
352 state.contractList.clear();
353 }
354
355 void closeCashDialog(State& state) noexcept
356 {
357 state.cashDialogVisible = false;
358 state.cashDialogClosing = false;
359 state.cashDialogFeedback = CashDialogFeedback::None;
360 }
361
363 State& state,
364 const rules::GameState& gameState,
366 std::uint8_t mode,
367 std::uint8_t side)
368 {
369 if (!contractIndex(kind) || side > 1 ||
370 !validPlayer(gameState, state.playerA) ||
371 !validPlayer(gameState, state.playerB))
372 return false;
373
374 state.contractDialogVisible = true;
375 state.contractDialogKind = kind;
376 state.contractDialogMode = mode;
377 state.contractDialogSide = side;
378 state.contractListOffset = 0;
379 state.contractList.clear();
380 if (mode == 0)
381 {
382 state.contractProperties = 0;
383 state.contractAmount = 0;
384 return true;
385 }
386
387 if (mode == 4)
388 {
389 const auto to = side ? state.playerB : state.playerA;
390 const auto hitType = contractHitType(kind);
391 for (const auto& hit : gameState.countHits)
392 {
393 if (state.contractList.size() >= 28) break;
394 if (hit.toPlayer == to && hit.hitType == hitType &&
395 hit.hitCount != 0 && !hit.tradedItem)
396 state.contractList.push_back({hit.hitCount, hit.properties, false});
397 }
398 }
399 else if (mode == 5 || mode == 6)
400 {
401 const auto to = side ? state.playerA : state.playerB;
402 const auto rawKind = static_cast<std::int64_t>(kind);
403 for (const auto& item : state.items)
404 {
405 if (state.contractList.size() >= 28) break;
406 if (item.numberC == rawKind && item.numberD != 0 &&
407 item.numberB == to && item.numberE >= 0 &&
408 item.numberE <= std::numeric_limits<std::uint32_t>::max())
409 {
410 state.contractList.push_back({
411 static_cast<std::int32_t>(item.numberD),
412 static_cast<std::uint32_t>(item.numberE), false});
413 }
414 }
415 }
417 return true;
418 }
419
420 [[nodiscard]] std::pair<rules::PlayerNumber, rules::PlayerNumber>
421 contractDirection(const State& state, std::uint8_t side) noexcept
422 {
423 return side == 0
424 ? std::pair{state.playerA, state.playerB}
425 : std::pair{state.playerB, state.playerA};
426 }
427
429 State& state,
430 const rules::GameState& gameState,
434 std::int32_t count,
435 std::uint32_t properties)
436 {
437 actions::Message item{};
438 item.numberA = from;
439 item.numberB = to;
440 item.numberC = static_cast<std::int64_t>(kind);
441 item.numberD = count;
442 item.numberE = properties;
443 return addTradeItem(state, gameState, item);
444 }
445
447 State& state,
448 const rules::GameState& gameState,
449 const uimsg::Message& message)
450 {
451 const auto kind = state.contractDialogKind;
452 const int x = static_cast<int>(message.numberA);
453 const int y = static_cast<int>(message.numberB);
454
455 if (state.contractDialogMode == 2 &&
456 (message.type == uimsg::Type::TextInput ||
457 message.type == uimsg::Type::KeyboardPressed))
458 {
460 state.contractAmount /= 10;
461 else if (const auto digit = textDigit(message); digit && state.contractAmount < 10)
462 state.contractAmount = state.contractAmount * 10 + *digit;
463 else if (pressedKey(message, SDL_SCANCODE_RETURN) ||
465 {
466 if (state.contractAmount <= 0) closeContractDialog(state);
467 else state.contractDialogMode = 3;
468 }
469 return;
470 }
471
472 if (message.type != uimsg::Type::MouseLeftDown) return;
473 // Handle arrows before the early returns in modes 1/2/3, so the
474 // same five-row viewport works while creating and viewing rights.
476 {
477 state.contractListOffset += ContractUpRect.contains(x, y) ? -1 : 1;
479 return;
480 }
481 if (state.contractDialogMode == 0)
482 {
484 {
486 return;
487 }
488 for (std::size_t box = 0; box < TradePropertyBoxOrigins.size(); ++box)
489 {
490 if (!contractBoxBounds(box).contains(x, y)) continue;
491 state.contractDialogSide = static_cast<std::uint8_t>(1u - (box % 2u));
492 state.contractDialogMode = 1;
493 state.contractProperties = 0;
494 state.contractList.clear();
495 state.contractListOffset = 0;
496 return;
497 }
498 }
499 else if (state.contractDialogMode == 1)
500 {
502 {
503 if (state.contractProperties == 0) closeContractDialog(state);
504 else
505 {
506 state.contractDialogMode = 2;
507 state.contractAmount = 0;
508 }
509 return;
510 }
511 const auto projection = projectProperties(state, gameState);
512 const auto hit = propertyHit(projection, x, y);
513 if (!hit) return;
514 const int box = *hit / 100;
515 const int square = *hit % 100;
516 const auto bit = ibar::layout::propertyBit(square);
517 bool legal = false;
518 if (state.contractDialogSide == 0)
519 legal = (box == 0 || box == 3) &&
520 (((projection.after[0] | projection.afterMortgaged[0]) & bit) != 0);
521 else
522 legal = (box == 1 || box == 2) &&
523 (((projection.after[1] | projection.afterMortgaged[1]) & bit) != 0);
524 if (legal)
525 {
526 state.contractProperties ^= bit;
527 rebuildContractPropertyList(state, state.contractProperties & bit);
528 }
529 return;
530 }
531 else if (state.contractDialogMode == 2)
532 {
533 if (!ContractOkayRect.contains(x, y)) return;
534 if (state.contractAmount <= 0) closeContractDialog(state);
535 else state.contractDialogMode = 3;
536 return;
537 }
538 else if (state.contractDialogMode == 3)
539 {
540 if (!ContractOkayRect.contains(x, y)) return;
541 const auto [from, to] = contractDirection(state, state.contractDialogSide);
542 for (unsigned int bit = 0; bit < 28; ++bit)
543 {
544 const auto mask = 1u << bit;
545 if ((state.contractProperties & mask) == 0) continue;
547 state, gameState, kind, from, to,
548 state.contractAmount, mask);
549 }
552 return;
553 }
554 else if (state.contractDialogMode == 4 ||
555 state.contractDialogMode == 5 ||
556 state.contractDialogMode == 6)
557 {
559 {
560 if (state.contractDialogMode != 6)
561 {
562 const auto [from, to] =
563 contractDirection(state, state.contractDialogSide);
564 for (const auto& entry : state.contractList)
565 {
566 if (!entry.selected) continue;
567 if (state.contractDialogMode == 4)
568 {
570 from, to, entry.hitCount, entry.properties);
572 to, from, -entry.hitCount, entry.properties);
573 }
574 else
575 {
577 from, to, 0, entry.properties);
579 to, from, 0, entry.properties);
580 }
581 }
582 }
585 return;
586 }
587 if (state.contractDialogMode != 6)
588 {
589 for (std::size_t row = 0; row < ContractListRects.size(); ++row)
590 {
591 if (!ContractListRects[row].contains(x, y)) continue;
592 const auto index = static_cast<std::size_t>(
593 state.contractListOffset + static_cast<int>(row));
594 if (index < state.contractList.size())
595 state.contractList[index].selected =
596 !state.contractList[index].selected;
597 return;
598 }
599 }
600 }
601
603 }
604
605 void clearProjectedTradeContracts(rules::GameState& gameState) noexcept
606 {
607 for (auto& hit : gameState.countHits)
608 {
609 if (!hit.tradedItem) continue;
610 hit.toPlayer = rules::NobodyPlayer;
611 hit.tradedItem = false;
612 }
613 }
614
615 void initializeProjectedTradeSequence(rules::GameState& gameState) noexcept
616 {
617 for (auto& square : gameState.squares)
618 square.offeredInTradeTo = rules::NobodyPlayer;
619 for (auto& player : gameState.players)
620 player.cashGivenInTrade.fill(0);
621 for (auto& card : gameState.cards)
622 card.jailOfferedInTradeTo = rules::NobodyPlayer;
624 }
625
627 const rules::GameState& gameState,
628 const actions::Message& message) noexcept
629 {
630 const auto kind = itemKind(message.numberC);
631 if (!kind || message.numberA < 0 || message.numberB < 0 ||
632 message.numberA >= gameState.numberOfPlayers ||
633 message.numberB >= gameState.numberOfPlayers)
634 {
635 return false;
636 }
637
638 switch (*kind)
639 {
641 return message.numberD >= 0;
643 return message.numberD >= 0 &&
644 message.numberD < static_cast<std::int64_t>(rules::SquareCount);
646 return message.numberD >= 0 &&
647 message.numberD < static_cast<std::int64_t>(gameState.cards.size());
650 return message.numberD >= std::numeric_limits<std::int32_t>::min() &&
651 message.numberD <= std::numeric_limits<std::int32_t>::max() &&
652 message.numberE >= 0 &&
653 message.numberE <= std::numeric_limits<std::uint32_t>::max();
654 }
655 return false;
656 }
657 }
658
659 void reset(State& state) noexcept
660 {
661 state.playerA = rules::MaxPlayers;
662 state.playerB = rules::MaxPlayers;
663 state.tradeFrom = rules::MaxPlayers;
664 state.editMode = true;
665 state.playerSelectVisible = false;
666 state.ignoreEntryClick = false;
667 state.proposed = false;
668 state.showPropose = false;
669 state.aiProposing = false;
671 state.desiredTradePanels = -1;
672 state.cashDesired = {};
673 state.jailCardDesired = {};
674 state.immunityFutureDesired = {};
675 state.cashDialogVisible = false;
676 state.cashDialogSide = 0;
677 state.cashTradeAmount = 0;
678 state.cashOriginalOffers = {};
679 state.cashDialogFeedback = CashDialogFeedback::None;
680 state.cashDialogClosing = false;
681 state.contractDialogVisible = false;
682 state.contractDialogKind = rules::TradeItemKind::FutureRent;
683 state.contractDialogMode = 0;
684 state.contractDialogSide = 0;
685 state.contractProperties = 0;
686 state.contractAmount = 0;
687 state.contractListOffset = 0;
688 state.contractList.clear();
689 state.propertyMove.reset();
690 state.items.clear();
691 }
692
694 State& state,
697 {
698 if (gameState.numberOfPlayers < 2 ||
699 gameState.numberOfPlayers > rules::MaxPlayers ||
701 {
702 return false;
703 }
704
705 reset(state);
706 state.playerA = source;
707 state.tradeFrom = source;
708 state.ignoreEntryClick = true;
709
710 if (gameState.numberOfPlayers < 3)
711 {
712 state.playerB = static_cast<rules::PlayerNumber>(1u - source);
713 state.desiredTradePanels =
714 static_cast<int>(state.playerA) * 10 + state.playerB;
718 state.showPropose = true;
719 return true;
720 }
721
722 state.playerSelectVisible = true;
723 state.desiredTradePanels =
724 static_cast<int>(state.playerA) * 10 + rules::MaxPlayers;
725 return true;
726 }
727
728 std::optional<Rect> playerTokenRect(
729 const State& state,
731 rules::PlayerNumber player) noexcept
732 {
733 if (!state.playerSelectVisible || !partnerEligible(state, gameState, player))
734 return std::nullopt;
735
736 int row = 1;
737 for (rules::PlayerNumber candidate = 0; candidate < player; ++candidate)
738 {
740 ++row;
741 }
742
743 const int left = PlayerSelectX + 126;
744 const int top = PlayerSelectY - 5 + row * 32;
745 return Rect{left, top, left + 53, top + 29};
746 }
747
748 std::optional<rules::PlayerNumber> planPartnerSelection(
749 State& state,
752 const uimsg::Message& message) noexcept
753 {
754 if (desiredView != display::Screen2D::Trade || !state.playerSelectVisible)
755 return std::nullopt;
756
757 if (message.type == uimsg::Type::MouseLeftDown && state.ignoreEntryClick)
758 {
759 state.ignoreEntryClick = false;
760 return std::nullopt;
761 }
762
763 if (message.type == uimsg::Type::MouseLeftDown)
764 {
765 const int x = static_cast<int>(message.numberA);
766 const int y = static_cast<int>(message.numberB);
767 for (rules::PlayerNumber player = 0;
768 player < gameState.numberOfPlayers && player < rules::MaxPlayers;
769 ++player)
770 {
771 const auto rect = playerTokenRect(state, gameState, player);
772 if (rect && rect->contains(x, y))
773 return player;
774 }
775 return std::nullopt;
776 }
777
778 const auto choice = textDigit(message);
779 if (!choice || *choice < 1 || *choice > static_cast<int>(rules::MaxPlayers))
780 return std::nullopt;
781 const auto player = static_cast<rules::PlayerNumber>(*choice - 1);
782 return partnerEligible(state, gameState, player)
783 ? std::optional<rules::PlayerNumber>{player}
784 : std::nullopt;
785 }
786
788 State& state,
790 rules::PlayerNumber player) noexcept
791 {
792 if (!partnerEligible(state, gameState, player))
793 return false;
794
795 state.tradeFrom = state.playerA;
796 state.playerB = player;
797 state.playerSelectVisible = false;
798 state.ignoreEntryClick = false;
799 state.immunityFutureDesired = {};
800 state.cashDesired[1] = 0;
801 state.cashDesired[3] = 0;
802 state.desiredTradePanels =
803 static_cast<int>(state.playerA) * 10 + state.playerB;
807 state.showPropose = true;
808 return true;
809 }
810
812 const State& state,
813 const rules::GameState& gameState) noexcept
814 {
815 PropertyProjection projection{};
816 if (!validPlayer(gameState, state.playerA) ||
817 !validPlayer(gameState, state.playerB))
818 return projection;
819
820 for (int square = 0; square < static_cast<int>(rules::SquareCount); ++square)
821 {
822 const auto bit = ibar::layout::propertyBit(square);
823 if (bit == 0) continue;
824 const auto& squareState =
825 gameState.squares[static_cast<std::size_t>(square)];
826 std::optional<std::size_t> side;
827 if (squareState.owner == state.playerA) side = 0;
828 else if (squareState.owner == state.playerB) side = 1;
829 if (!side) continue;
830
831 if (squareState.mortgaged)
832 projection.beforeMortgaged[*side] |= bit;
833 else
834 projection.before[*side] |= bit;
835 }
836
837 for (const auto& item : state.items)
838 {
839 if (item.numberC != static_cast<std::int64_t>(rules::TradeItemKind::Square) ||
840 item.numberD < 0 ||
841 item.numberD >= static_cast<std::int64_t>(rules::SquareCount))
842 continue;
843
844 std::optional<std::size_t> side;
845 if (item.numberA == state.playerA) side = 0;
846 else if (item.numberA == state.playerB) side = 1;
847 if (!side) continue;
848
849 const int square = static_cast<int>(item.numberD);
850 const auto bit = ibar::layout::propertyBit(square);
851 if (bit == 0) continue;
852 projection.before[*side] &= ~bit;
853 projection.beforeMortgaged[*side] &= ~bit;
854 if (gameState.squares[static_cast<std::size_t>(square)].mortgaged)
855 projection.offeredMortgaged[*side] |= bit;
856 else
857 projection.offered[*side] |= bit;
858 }
859
860 projection.after[0] =
861 (projection.before[0] | projection.offered[1]) & ~projection.offered[0];
862 projection.after[1] =
863 (projection.before[1] | projection.offered[0]) & ~projection.offered[1];
864 projection.afterMortgaged[0] =
865 (projection.beforeMortgaged[0] | projection.offeredMortgaged[1]) &
866 ~projection.offeredMortgaged[0];
867 projection.afterMortgaged[1] =
868 (projection.beforeMortgaged[1] | projection.offeredMortgaged[0]) &
869 ~projection.offeredMortgaged[1];
870
872 projection, 0, projection.before[0] | projection.beforeMortgaged[0], state.modernHoldingsLayout);
874 projection, 1, projection.before[1] | projection.beforeMortgaged[1], state.modernHoldingsLayout);
876 projection, 2, projection.offered[0] | projection.offeredMortgaged[0], state.modernHoldingsLayout);
878 projection, 3, projection.offered[1] | projection.offeredMortgaged[1], state.modernHoldingsLayout);
879 return projection;
880 }
881
883 State& state,
886 {
887 // UDTrade_ProcessEverything() performs this guard continuously while
888 // the Trade screen is up: if either active trader becomes bankrupt,
889 // abandon the local editor/view and return to the main board.
891 state.playerA >= rules::MaxPlayers ||
892 state.playerB >= rules::MaxPlayers)
893 {
894 return false;
895 }
896
897 if (gameState.players[state.playerA].currentSquare < OffBoardSquare &&
898 gameState.players[state.playerB].currentSquare < OffBoardSquare)
899 {
900 return false;
901 }
902
903 gameState.tradeInProgress = false;
904 reset(state);
905 return true;
906 }
907
909 State& state,
910 const rules::GameState& gameState) noexcept
911 {
912 state.immunityFutureDesired = {};
913 if ((!gameState.options.futureRentTradingAllowed &&
914 !gameState.options.immunitiesTradingAllowed) ||
915 !validPlayer(gameState, state.playerA) ||
916 !validPlayer(gameState, state.playerB))
917 return;
918
919 const auto projection = projectProperties(state, gameState);
920 const std::array<PropertyMask, 2> after{{
921 projection.after[0] | projection.afterMortgaged[0],
922 projection.after[1] | projection.afterMortgaged[1]}};
923 constexpr std::array<rules::TradeItemKind, 2> kinds{{
926 for (std::size_t which = 0; which < kinds.size(); ++which)
927 {
928 const auto kind = kinds[which];
929 const auto hitType = contractHitType(kind);
930 for (std::size_t side = 0; side < 2; ++side)
931 {
932 const auto player = side ? state.playerB : state.playerA;
933 for (const auto& hit : gameState.countHits)
934 {
935 if (hit.toPlayer == player && hit.hitType == hitType &&
936 !hit.tradedItem)
937 {
938 state.immunityFutureDesired[which] |=
939 static_cast<std::uint8_t>(1u << side);
940 }
941 }
942
943 const auto target = side ? state.playerA : state.playerB;
944 const auto targetAfter = after[side ? 0u : 1u];
945 const auto otherAfter = after[side ? 1u : 0u];
946 const auto rawKind = static_cast<std::int64_t>(kind);
947 std::size_t itemIndex = 0;
948 while (itemIndex < state.items.size())
949 {
950 const auto& item = state.items[itemIndex];
951 if (item.numberC != rawKind || item.numberB != target ||
952 item.numberD <= 0 || item.numberE < 0 ||
953 item.numberE > std::numeric_limits<std::uint32_t>::max())
954 {
955 ++itemIndex;
956 continue;
957 }
958 const auto properties = static_cast<std::uint32_t>(item.numberE);
959 if ((properties & targetAfter) == 0)
960 {
961 state.immunityFutureDesired[which] |=
962 static_cast<std::uint8_t>(1u << (side + 2u));
963 ++itemIndex;
964 continue;
965 }
966
967 state.items[itemIndex] = std::move(state.items.back());
968 state.items.pop_back();
969 for (std::size_t q = 0; q < state.items.size(); ++q)
970 {
971 const auto& candidate = state.items[q];
972 if (candidate.numberC == rawKind &&
973 candidate.numberB == target && candidate.numberD < 0 &&
974 candidate.numberE >= 0 &&
975 candidate.numberE <= std::numeric_limits<std::uint32_t>::max() &&
976 (static_cast<std::uint32_t>(candidate.numberE) & otherAfter) != 0)
977 {
978 state.items[q] = std::move(state.items.back());
979 state.items.pop_back();
980 break;
981 }
982 }
983 }
984 }
985 }
986 }
987
988 std::optional<int> propertyHit(
989 const PropertyProjection& projection,
990 int x,
991 int y) noexcept
992 {
993 int box = -1;
994 for (std::size_t candidate = 0;
996 ++candidate)
997 {
998 const auto& origin = TradePropertyBoxOrigins[candidate];
999 const Rect bounds{
1000 origin[0], origin[1], origin[0] + 200, origin[1] + 225};
1001 if (bounds.contains(x, y))
1002 {
1003 box = static_cast<int>(candidate);
1004 break;
1005 }
1006 }
1007 if (box < 0) return std::nullopt;
1008
1009 for (const int square : TradePropertyHitOrder)
1010 {
1011 const auto& rect =
1012 projection.hitRects[static_cast<std::size_t>(box)]
1013 [static_cast<std::size_t>(square)];
1014 if (rect.right > rect.left && rect.bottom > rect.top &&
1015 rect.contains(x, y))
1016 return box * 100 + square;
1017 }
1018 return std::nullopt;
1019 }
1020
1022 State& state,
1025 const uimsg::Message& message)
1026 {
1027 InputUpdate result{};
1028 if (desiredView != display::Screen2D::Trade || state.playerSelectVisible ||
1029 !validPlayer(gameState, state.playerA) ||
1030 !validPlayer(gameState, state.playerB))
1031 return result;
1032
1034 const int x = static_cast<int>(message.numberA);
1035 const int y = static_cast<int>(message.numberB);
1036
1037 if (state.contractDialogVisible &&
1038 (message.type == uimsg::Type::MouseLeftDown ||
1039 message.type == uimsg::Type::TextInput ||
1041 {
1042 result.consumed = true;
1044 return result;
1045 }
1046
1047 if (!state.editMode)
1048 {
1049 if (message.type != uimsg::Type::MouseLeftDown) return result;
1050 if (FutureTradeAM.contains(x, y) &&
1051 (state.immunityFutureDesired[0] & (1u << 2u)))
1052 result.consumed = openContractDialog(
1054 else if (FutureTradeBM.contains(x, y) &&
1055 (state.immunityFutureDesired[0] & (1u << 3u)))
1056 result.consumed = openContractDialog(
1058 else if (ImmunityTradeAM.contains(x, y) &&
1059 (state.immunityFutureDesired[1] & (1u << 2u)))
1060 result.consumed = openContractDialog(
1062 else if (ImmunityTradeBM.contains(x, y) &&
1063 (state.immunityFutureDesired[1] & (1u << 3u)))
1064 result.consumed = openContractDialog(
1066 return result;
1067 }
1068
1069 if (state.cashDialogClosing)
1070 {
1071 result.consumed = true;
1072 return result;
1073 }
1074
1075 if (state.cashDialogVisible &&
1076 (message.type == uimsg::Type::MouseLeftDown ||
1077 message.type == uimsg::Type::TextInput ||
1079 {
1080 result.consumed = true;
1081 auto commitAmount = [&](std::int64_t amount) {
1082 state.cashTradeAmount = amount;
1083 (void)writeCashItem(state, gameState, state.cashDialogSide, amount);
1084 state.cashDesired[state.cashDialogSide + 2u] = amount;
1086 };
1087
1088 if (message.type == uimsg::Type::MouseLeftDown)
1089 {
1090 constexpr std::array<std::pair<Rect, std::int64_t>, 7> denominations{{
1091 {{7, 10, 31, 43}, 500}, {{32, 10, 56, 43}, 100},
1092 {{57, 10, 81, 43}, 50}, {{82, 10, 106, 43}, 20},
1093 {{107, 10, 131, 43}, 10}, {{132, 10, 156, 43}, 5},
1094 {{157, 10, 183, 43}, 1}}};
1095 for (const auto& [local, amount] : denominations)
1096 {
1097 if (!cashPopupRect(state.cashDialogSide, local).contains(x, y)) continue;
1098 if (state.cashTradeAmount + amount <= 999999)
1099 commitAmount(state.cashTradeAmount + amount);
1100 return result;
1101 }
1102
1103 if (cashPopupRect(state.cashDialogSide, {7, 43, 57, 59}).contains(x, y))
1104 {
1105 state.cashDesired[state.cashDialogSide + 2u] = state.cashTradeAmount;
1106 state.cashDesired[(1u - state.cashDialogSide) + 2u] = 0;
1107 (void)writeCashItem(state, gameState, state.cashDialogSide, state.cashTradeAmount);
1109 state.cashDialogFeedback = CashDialogFeedback::Okay;
1110 state.cashDialogClosing = true;
1111 state.cashDialogVisible = false;
1112 return result;
1113 }
1114 if (cashPopupRect(state.cashDialogSide, {68, 43, 118, 59}).contains(x, y))
1115 {
1116 state.cashTradeAmount = 0;
1118 state.cashDesired[state.cashDialogSide + 2u] = 0;
1120 state.cashDialogFeedback = CashDialogFeedback::Clear;
1121 return result;
1122 }
1123 if (cashPopupRect(state.cashDialogSide, {129, 43, 179, 59}).contains(x, y))
1124 {
1125 std::uint8_t restoredSide = state.cashDialogSide;
1126 std::int64_t restoredAmount = 0;
1127 if (state.cashOriginalOffers[0] == 0 && state.cashOriginalOffers[1] == 0)
1128 restoredAmount = 0;
1129 else if (state.cashOriginalOffers[0] == 0)
1130 {
1131 restoredAmount = state.cashOriginalOffers[1];
1132 if (restoredSide == 0) restoredSide = 1;
1133 }
1134 else
1135 {
1136 restoredAmount = state.cashOriginalOffers[0];
1137 if (restoredSide == 1) restoredSide = 0;
1138 }
1139 state.cashTradeAmount = restoredAmount;
1141 state.cashDesired[2] = state.cashOriginalOffers[0];
1142 state.cashDesired[3] = state.cashOriginalOffers[1];
1144 state.cashDialogFeedback = CashDialogFeedback::Cancel;
1145 state.cashDialogClosing = true;
1146 state.cashDialogVisible = false;
1147 return result;
1148 }
1149 return result;
1150 }
1151
1152 if (pressedKey(message, SDL_SCANCODE_BACKSPACE))
1153 commitAmount(state.cashTradeAmount / 10);
1154 else if (const auto digit = textDigit(message); digit && state.cashTradeAmount < 100000)
1155 commitAmount(state.cashTradeAmount * 10 + *digit);
1156 else if (pressedKey(message, SDL_SCANCODE_RETURN) ||
1158 {
1159 state.cashDesired[state.cashDialogSide + 2u] = state.cashTradeAmount;
1160 state.cashDesired[(1u - state.cashDialogSide) + 2u] = 0;
1161 (void)writeCashItem(state, gameState, state.cashDialogSide, state.cashTradeAmount);
1163 state.cashDialogFeedback = CashDialogFeedback::Okay;
1164 state.cashDialogClosing = true;
1165 state.cashDialogVisible = false;
1166 }
1167 return result;
1168 }
1169
1170 if (message.type != uimsg::Type::MouseLeftDown) return result;
1171 result.consumed = true;
1172
1173 if (inEither(CashTradeAT1, CashTradeAT2, x, y) && !state.cashDialogVisible)
1174 {
1176 return result;
1177 }
1178 if (inEither(CashTradeBT1, CashTradeBT2, x, y) && !state.cashDialogVisible)
1179 {
1181 return result;
1182 }
1183
1184 auto zeroCash = [&](rules::PlayerNumber sender, std::size_t offerIndex) {
1185 const auto kind = static_cast<std::int64_t>(rules::TradeItemKind::Cash);
1186 const auto it = std::find_if(state.items.begin(), state.items.end(),
1187 [&](const actions::Message& item) {
1188 return item.numberC == kind && item.numberA == sender;
1189 });
1190 if (it != state.items.end())
1191 {
1192 state.cashDesired[offerIndex] = 0;
1193 it->numberD = 0;
1195 }
1196 };
1198 {
1199 zeroCash(state.playerA, 2);
1200 return result;
1201 }
1203 {
1204 zeroCash(state.playerB, 3);
1205 return result;
1206 }
1207
1208 if (!state.propertyMove)
1209 {
1211 if (const auto hit = propertyHit(propertyProjection, x, y))
1212 {
1213 const int box = *hit / 100;
1214 const int square = *hit % 100;
1215 const auto bit = ibar::layout::propertyBit(square);
1216 const auto sourceRect =
1217 propertyProjection.hitRects[static_cast<std::size_t>(box)]
1218 [static_cast<std::size_t>(square)];
1219 bool changed = false;
1220 if (box == 0 || box == 1)
1221 {
1222 const std::size_t side = static_cast<std::size_t>(box);
1223 if (((propertyProjection.after[side] |
1224 propertyProjection.afterMortgaged[side]) & bit) != 0)
1225 {
1226 actions::Message item{};
1227 item.numberA =
1228 gameState.squares[static_cast<std::size_t>(square)].owner;
1229 item.numberB = side == 0 ? state.playerB : state.playerA;
1230 item.numberC =
1231 static_cast<std::int64_t>(rules::TradeItemKind::Square);
1232 item.numberD = square;
1233 changed = addTradeItem(state, gameState, item);
1234 if (changed)
1235 {
1236 const auto updated = projectProperties(state, gameState);
1237 state.propertyMove = PropertyMoveRequest{
1238 square, static_cast<std::uint8_t>(box),
1239 static_cast<std::uint8_t>(box + 2),
1240 gameState.squares[static_cast<std::size_t>(square)].mortgaged,
1241 sourceRect,
1242 updated.hitRects[static_cast<std::size_t>(box + 2)]
1243 [static_cast<std::size_t>(square)]};
1244 }
1245 }
1246 }
1247 else
1248 {
1249 changed = removeFirstItem(
1251 if (changed)
1252 {
1253 const auto updated = projectProperties(state, gameState);
1254 state.propertyMove = PropertyMoveRequest{
1255 square, static_cast<std::uint8_t>(box),
1256 static_cast<std::uint8_t>(box - 2),
1257 gameState.squares[static_cast<std::size_t>(square)].mortgaged,
1258 sourceRect,
1259 updated.hitRects[static_cast<std::size_t>(box - 2)]
1260 [static_cast<std::size_t>(square)]};
1261 }
1262 }
1264 return result;
1265 }
1266 }
1267
1268 auto toggleJail = [&](std::size_t deck, std::size_t slot) {
1269 const std::uint8_t bit = static_cast<std::uint8_t>(1u << slot);
1270 if ((state.jailCardDesired[deck] & bit) == 0) return;
1271 if (slot < 2)
1272 {
1273 const auto from = slot == 0 ? state.playerA : state.playerB;
1274 const auto to = slot == 0 ? state.playerB : state.playerA;
1275 state.jailCardDesired[deck] = static_cast<std::uint8_t>(1u << (slot + 2));
1276 actions::Message item{};
1277 item.numberA = from;
1278 item.numberB = to;
1279 item.numberC = static_cast<std::int64_t>(rules::TradeItemKind::JailCard);
1280 item.numberD = static_cast<std::int64_t>(deck);
1282 }
1283 else
1284 {
1285 state.jailCardDesired[deck] = static_cast<std::uint8_t>(1u << (slot - 2));
1287 static_cast<std::int64_t>(deck));
1288 }
1289 };
1290 for (std::size_t slot = 0; slot < ChanceJailRects.size(); ++slot)
1291 {
1292 if (ChanceJailRects[slot].contains(x, y))
1293 {
1294 toggleJail(0, slot);
1295 return result;
1296 }
1297 if (CommunityJailRects[slot].contains(x, y))
1298 {
1299 toggleJail(1, slot);
1300 return result;
1301 }
1302 }
1303
1304 if (FutureTradeAT.contains(x, y) &&
1305 (state.immunityFutureDesired[0] & (1u << 0u)))
1306 {
1309 return result;
1310 }
1311 if (FutureTradeBT.contains(x, y) &&
1312 (state.immunityFutureDesired[0] & (1u << 1u)))
1313 {
1316 return result;
1317 }
1318 if (FutureTradeAM.contains(x, y) &&
1319 (state.immunityFutureDesired[0] & (1u << 2u)))
1320 {
1323 return result;
1324 }
1325 if (FutureTradeBM.contains(x, y) &&
1326 (state.immunityFutureDesired[0] & (1u << 3u)))
1327 {
1330 return result;
1331 }
1332 if (ImmunityTradeAT.contains(x, y) &&
1333 (state.immunityFutureDesired[1] & (1u << 0u)))
1334 {
1337 return result;
1338 }
1339 if (ImmunityTradeBT.contains(x, y) &&
1340 (state.immunityFutureDesired[1] & (1u << 1u)))
1341 {
1344 return result;
1345 }
1346 if (ImmunityTradeAM.contains(x, y) &&
1347 (state.immunityFutureDesired[1] & (1u << 2u)))
1348 {
1351 return result;
1352 }
1353 if (ImmunityTradeBM.contains(x, y) &&
1354 (state.immunityFutureDesired[1] & (1u << 3u)))
1355 {
1358 return result;
1359 }
1360 if (FutureNewRect.contains(x, y) &&
1361 gameState.options.futureRentTradingAllowed)
1362 {
1365 return result;
1366 }
1367 if (ImmunityNewRect.contains(x, y) &&
1368 gameState.options.immunitiesTradingAllowed)
1369 {
1372 return result;
1373 }
1374
1375 if (ProposeRect.contains(x, y))
1376 {
1377 bool give = false;
1378 bool get = false;
1379 for (const auto& item : state.items)
1380 {
1381 const bool meaningful =
1382 item.numberC != static_cast<std::int64_t>(rules::TradeItemKind::Cash) ||
1383 item.numberD != 0;
1384 if (!meaningful) continue;
1385 if (item.numberA == state.playerA) give = true;
1386 if (item.numberB == state.playerA) get = true;
1387 }
1388 if (give && get && state.showPropose && state.tradeFrom < rules::MaxPlayers)
1389 {
1390 actions::Message action{};
1392 action.fromPlayer = state.tradeFrom;
1393 action.toPlayer = rules::BankPlayer;
1394 action.numberA = 1;
1395 result.outgoing.push_back(std::move(action));
1396 }
1397 else if (!give || !get)
1398 {
1399 result.proposeMissingOffer = true;
1400 }
1401 return result;
1402 }
1403
1404 if (CancelRect.contains(x, y))
1405 {
1406 reset(state);
1407 result.requestedBackdrop = display::Screen2D::Main;
1408 return result;
1409 }
1410
1411 return result;
1412 }
1413
1414 std::vector<actions::Message> planEditorSubmission(
1415 const State& state,
1417 std::uint32_t localHumanMask)
1418 {
1419 std::vector<actions::Message> batch;
1420 if (editor >= rules::MaxPlayers || state.tradeFrom != editor ||
1421 (localHumanMask & (1u << editor)) == 0) return batch;
1422 batch.reserve(state.items.size() + 1);
1423 batch.insert(batch.end(), state.items.begin(), state.items.end());
1426 done.fromPlayer = editor;
1427 done.toPlayer = rules::BankPlayer;
1428 done.numberA = 0;
1429 done.numberB = 1;
1430 batch.push_back(std::move(done));
1431 return batch;
1432 }
1433
1435 State& state,
1437 const actions::Message& message)
1438 {
1439 const auto kind = itemKind(message.numberC);
1440 if (!kind || state.tradeFrom >= rules::MaxPlayers)
1441 return false;
1442
1443 actions::Message stored = message;
1444 stored.binaryData.clear();
1445 stored.binaryDataA.clear();
1446 std::optional<std::size_t> overwrite;
1447 for (std::size_t index = 0; index < state.items.size(); ++index)
1448 {
1449 const auto existingKind = itemKind(state.items[index].numberC);
1450 if (!existingKind) continue;
1451 switch (*existingKind)
1452 {
1455 break;
1458 state.items[index].numberD == stored.numberD &&
1459 state.items[index].numberB == stored.numberB)
1460 overwrite = index;
1461 break;
1464 state.items[index].numberD == stored.numberD &&
1465 state.items[index].numberB == stored.numberB)
1466 overwrite = index;
1467 break;
1470 if (*existingKind == *kind &&
1471 state.items[index].numberB == stored.numberB &&
1472 state.items[index].numberE == stored.numberE)
1473 overwrite = index;
1474 break;
1475 }
1476 }
1477
1480 {
1481 const auto hitType = *kind == rules::TradeItemKind::FutureRent
1484 for (const auto& hit : gameState.countHits)
1485 {
1486 if (hit.toPlayer == stored.numberB &&
1487 hit.properties == static_cast<std::uint32_t>(stored.numberE) &&
1488 hit.hitType == hitType && !hit.tradedItem)
1489 {
1490 const auto maximum = static_cast<std::int64_t>(99 - hit.hitCount);
1491 if (stored.numberD > maximum) stored.numberD = maximum;
1492 break;
1493 }
1494 }
1495 }
1496
1498 stored.fromPlayer = state.tradeFrom;
1499 stored.toPlayer = rules::BankPlayer;
1500
1501 const bool removableContract =
1503 *kind == rules::TradeItemKind::FutureRent) && stored.numberD == 0;
1504
1505 if (!overwrite)
1506 {
1507 if (removableContract) return true;
1508 if (state.items.size() >= MaxTradeMessages) return false;
1509 state.items.push_back(std::move(stored));
1510 return true;
1511 }
1512
1514 {
1515 state.items[*overwrite] = std::move(state.items.back());
1516 state.items.pop_back();
1517 }
1518 else
1519 {
1520 state.items[*overwrite] = std::move(stored);
1521 }
1522 return true;
1523 }
1524
1529 rules::CountHitType hitType,
1530 std::int32_t hitCount,
1531 std::uint32_t properties,
1532 bool tradingItem) noexcept
1533 {
1534 if (from >= rules::MaxPlayers || to >= rules::MaxPlayers ||
1536 return false;
1537
1538 if (hitCount > 99) hitCount = 99;
1539 if (hitCount < 0 && !tradingItem) hitCount = 0;
1540
1541 std::optional<std::size_t> matching;
1542 std::optional<std::size_t> freeSpot;
1543 for (std::size_t i = 0; i < gameState.countHits.size(); ++i)
1544 {
1545 auto& hit = gameState.countHits[i];
1546 if (!freeSpot && hit.toPlayer == rules::NobodyPlayer) freeSpot = i;
1547 if (hit.toPlayer == to && hit.properties == properties &&
1548 hit.hitType == hitType && hit.tradedItem == tradingItem)
1549 {
1550 matching = i;
1551 break;
1552 }
1553 }
1554
1555 std::size_t target{};
1556 if (matching)
1557 target = *matching;
1558 else if (freeSpot)
1559 target = *freeSpot;
1560 else
1561 {
1562 for (auto& hit : gameState.countHits)
1563 {
1564 hit.toPlayer = rules::NobodyPlayer;
1565 hit.tradedItem = false;
1566 }
1567 target = 0;
1568 }
1569
1570 if (tradingItem)
1571 {
1572 for (const auto& hit : gameState.countHits)
1573 {
1574 if (hit.toPlayer == to && hit.properties == properties &&
1575 hit.hitType == hitType && !hit.tradedItem)
1576 {
1577 hitCount = std::min(hitCount, 99 - hit.hitCount);
1578 break;
1579 }
1580 }
1581 }
1582
1583 auto& hit = gameState.countHits[target];
1584 if (hitCount == 0)
1585 {
1586 hit.toPlayer = rules::NobodyPlayer;
1587 hit.tradedItem = false;
1588 return true;
1589 }
1590
1591 hit.tradedItem = tradingItem;
1592 hit.toPlayer = to;
1593 hit.fromPlayer = from;
1594 hit.hitCount = hitCount;
1595 hit.properties = properties;
1596 hit.hitType = hitType;
1597 return true;
1598 }
1599
1601 State& state,
1603 const actions::Message& message,
1605 std::uint32_t localHumanMask,
1607 {
1608 RuleUpdate result{};
1609
1611 {
1612 if (message.numberA == static_cast<std::int64_t>(actions::Type::NewGame))
1613 {
1614 gameState.tradeInProgress = false;
1615 reset(state);
1616 }
1617 else if (message.numberA == static_cast<std::int64_t>(actions::Type::StartTradeEditing) &&
1618 message.numberB != 0)
1619 {
1620 // UDTrade.cpp removes every editor-side modal immediately once
1621 // ACTION_START_TRADE_EDITING is accepted.
1622 state.playerSelectVisible = false;
1625 state.showPropose = false;
1626 }
1627 return result;
1628 }
1629
1630 switch (message.action)
1631 {
1633 {
1634 if (message.numberA < 0 || message.numberA >= gameState.numberOfPlayers ||
1635 message.numberA >= rules::MaxPlayers)
1636 return result;
1637 const auto proposer = static_cast<rules::PlayerNumber>(message.numberA);
1638 const bool wasInProgress = gameState.tradeInProgress;
1639 if (!wasInProgress)
1640 {
1642 }
1643 else if ((localHumanMask & (1u << proposer)) == 0)
1644 {
1646 const auto formerView = state.formerView;
1647 reset(state);
1648 state.formerView = formerView;
1649 }
1650 state.proposed = true;
1651 // Once a proposal is being viewed, UDTrade_ProcessEverything()
1652 // tears down editor modals. Read-only mode-6 contract viewers are
1653 // the sole exception and remain available during proposal review.
1654 state.playerSelectVisible = false;
1656 if (!state.contractDialogVisible || state.contractDialogMode != 6)
1658 state.showPropose = false;
1659 if (!wasInProgress)
1660 {
1661 state.formerView = currentView;
1662 result.requestedBackdrop = display::Screen2D::Trade;
1663 state.aiProposing = gameState.players[proposer].aiPlayerLevel > 0;
1664 }
1665 state.playerA = proposer;
1666 state.tradeFrom = proposer;
1667 state.playerB = rules::MaxPlayers;
1668 state.editMode = false;
1669 gameState.tradeInProgress = true;
1670 return result;
1671 }
1672
1674 if (message.numberA != -1)
1675 {
1676 // Presentation only: terminal autonomous trades may return to
1677 // the Portfolio they interrupted. Duplicate Started echoes can
1678 // reset aiProposing, so qualify the actual participants instead.
1679 const auto actualAI = [&](rules::PlayerNumber player)
1680 {
1681 return player < gameState.numberOfPlayers && player < rules::MaxPlayers &&
1682 gameState.players[player].aiPlayerLevel > 0;
1683 };
1685 state.formerView == display::Screen2D::Portfolio &&
1687 actualAI(state.playerA) && actualAI(state.playerB);
1688 gameState.tradeInProgress = false;
1689 reset(state);
1690 result.requestedBackdrop = restorePortfolio
1692 }
1693 else
1694 {
1695 const auto oldB = state.playerB;
1696 gameState.tradeInProgress = false;
1697 if (oldB < rules::MaxPlayers && (localHumanMask & (1u << oldB)) != 0)
1698 {
1699 state.tradeFrom = oldB;
1700 for (auto& item : state.items) item.fromPlayer = oldB;
1701 std::swap(state.playerA, state.playerB);
1702 state.desiredTradePanels =
1703 static_cast<int>(state.playerA) * 10 + state.playerB;
1704 state.editMode = true;
1706 // Retail rearms Propose/Cancel in its next Trade frame.
1707 // Modern projection must publish that editable state here.
1708 state.showPropose = true;
1709 result.requestedBackdrop = display::Screen2D::Trade;
1710 }
1711 else
1712 {
1713 reset(state);
1714 result.requestedBackdrop = display::Screen2D::Main;
1715 }
1716 }
1717 state.proposed = false;
1720 return result;
1721
1723 if (message.numberA < 0 || message.numberA >= gameState.numberOfPlayers ||
1724 message.numberA >= rules::MaxPlayers)
1725 return result;
1726 if (state.playerA < rules::MaxPlayers && state.playerA != message.numberA)
1727 {
1728 for (auto& item : state.items)
1729 item.fromPlayer = static_cast<rules::PlayerNumber>(message.numberA);
1730 state.showPropose = false;
1731 state.proposed = true;
1732 }
1733 state.playerA = static_cast<rules::PlayerNumber>(message.numberA);
1734 state.tradeFrom = state.playerA;
1736 return result;
1737
1739 state.playerSelectVisible = false;
1741 state.editMode = false;
1742 state.showPropose = false;
1743 // The retail viewer deliberately leaves mode-6 Future/Immunity
1744 // inspection dialogs open while acceptance is pending. Editing
1745 // modes 0..5 are dismissed.
1746 if (!state.contractDialogVisible || state.contractDialogMode != 6)
1749 return result;
1750
1752 {
1753 if (!validTradeItemForProjection(gameState, message) ||
1754 state.playerA >= rules::MaxPlayers)
1755 return result;
1756
1757 if (!addTradeItem(state, gameState, message)) return result;
1758 const auto from = static_cast<rules::PlayerNumber>(message.numberA);
1759 const auto to = static_cast<rules::PlayerNumber>(message.numberB);
1760 if (from == state.playerA) state.playerB = to;
1761 else if (to == state.playerA) state.playerB = from;
1762 state.desiredTradePanels =
1763 static_cast<int>(state.playerA) * 10 + state.playerB;
1764
1765 const auto kind = *itemKind(message.numberC);
1766 switch (kind)
1767 {
1769 gameState.players[from].cashGivenInTrade[to] = message.numberD;
1770 if (from == state.playerA) state.cashDesired[2] = message.numberD;
1771 if (from == state.playerB) state.cashDesired[3] = message.numberD;
1772 break;
1774 gameState.squares[static_cast<std::size_t>(message.numberD)]
1775 .offeredInTradeTo = to;
1776 break;
1778 gameState.cards[static_cast<std::size_t>(message.numberD)]
1779 .jailOfferedInTradeTo = to;
1780 break;
1784 gameState, from, to,
1788 static_cast<std::int32_t>(message.numberD),
1789 static_cast<std::uint32_t>(message.numberE), true);
1790 break;
1791 }
1793 return result;
1794 }
1795
1796 default:
1797 return result;
1798 }
1799 }
1800}
std::size_t count
std::array< rules::PlayerNumber, rules::MaxPlayers > players
std::atomic< float > left
int top
PlayerNumber slot
std::shared_ptr< const data::HmdTextureImage > source
std::uint64_t key
unsigned state
unsigned index
Kind kind
Rect rect
std::uint16_t value
Definition Timers.cpp:16
CardTag other
PropertyMask propertyBit(int square) noexcept
int propertyBarOrder(int square) noexcept
constexpr PlayerNumber BankPlayer
Definition RuleTypes.hpp:15
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr std::size_t SquareCount
Definition RuleTypes.hpp:24
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
constexpr PlayerNumber NobodyPlayer
Definition RuleTypes.hpp:16
constexpr Rect CashTradeAT2
Definition TradeUI.hpp:43
PropertyProjection projectProperties(const State &state, const rules::GameState &gameState) noexcept
Definition TradeUI.cpp:811
bool addUiImmunity(rules::GameState &gameState, rules::PlayerNumber from, rules::PlayerNumber to, rules::CountHitType hitType, std::int32_t hitCount, std::uint32_t properties, bool tradingItem) noexcept
Definition TradeUI.cpp:1525
constexpr Rect FutureTradeBT
Definition TradeUI.hpp:60
constexpr Rect CashTradeAT1
Definition TradeUI.hpp:42
constexpr int PlayerSelectY
Definition TradeUI.hpp:21
std::optional< int > propertyHit(const PropertyProjection &projection, int x, int y) noexcept
Definition TradeUI.cpp:988
constexpr Rect ImmunityTradeAM
Definition TradeUI.hpp:65
constexpr Rect CashTradeAM1
Definition TradeUI.hpp:46
constexpr std::array< Rect, 4 > CommunityJailRects
Definition TradeUI.hpp:56
constexpr Rect ImmunityTradeAT
Definition TradeUI.hpp:63
constexpr Rect ContractDownRect
Definition TradeUI.hpp:71
constexpr Rect CashTradeAM2
Definition TradeUI.hpp:47
bool abortIfParticipantOffBoard(State &state, rules::GameState &gameState, display::Screen2D desiredView) noexcept
Definition TradeUI.cpp:882
RuleUpdate processRuleMessage(State &state, rules::GameState &gameState, const actions::Message &message, display::Screen2D currentView, std::uint32_t localHumanMask, bool restoreAutonomousPortfolio)
Definition TradeUI.cpp:1600
constexpr Rect CancelRect
Definition TradeUI.hpp:51
bool beginLocalTrade(State &state, const rules::GameState &gameState, rules::PlayerNumber source) noexcept
Definition TradeUI.cpp:693
constexpr Rect ContractUpRect
Definition TradeUI.hpp:70
bool selectPartner(State &state, const rules::GameState &gameState, rules::PlayerNumber player) noexcept
Definition TradeUI.cpp:787
constexpr Rect FutureTradeBM
Definition TradeUI.hpp:62
constexpr Rect ImmunityTradeBT
Definition TradeUI.hpp:64
constexpr Rect ImmunityTradeBM
Definition TradeUI.hpp:66
constexpr Rect CashTradeBM1
Definition TradeUI.hpp:48
InputUpdate processInput(State &state, const rules::GameState &gameState, display::Screen2D desiredView, const uimsg::Message &message)
Definition TradeUI.cpp:1021
std::vector< actions::Message > planEditorSubmission(const State &state, rules::PlayerNumber editor, std::uint32_t localHumanMask)
Definition TradeUI.cpp:1414
constexpr std::array< Rect, 5 > ContractListRects
Definition TradeUI.hpp:72
void reset(State &state) noexcept
Definition TradeUI.cpp:659
constexpr Rect FutureTradeAM
Definition TradeUI.hpp:61
constexpr std::uint8_t OffBoardSquare
Definition TradeUI.hpp:25
std::uint32_t PropertyMask
Definition TradeUI.hpp:139
constexpr Rect CashTradeBM2
Definition TradeUI.hpp:49
constexpr Rect CashTradeBT1
Definition TradeUI.hpp:44
constexpr int PlayerSelectX
Definition TradeUI.hpp:20
void refreshContractProjection(State &state, const rules::GameState &gameState) noexcept
Definition TradeUI.cpp:908
std::optional< rules::PlayerNumber > planPartnerSelection(State &state, const rules::GameState &gameState, display::Screen2D desiredView, const uimsg::Message &message) noexcept
Definition TradeUI.cpp:748
constexpr Rect CashTradeBT2
Definition TradeUI.hpp:45
constexpr std::array< Rect, 4 > ChanceJailRects
Definition TradeUI.hpp:53
constexpr Rect ProposeRect
Definition TradeUI.hpp:50
constexpr Rect ContractOkayRect
Definition TradeUI.hpp:69
std::optional< Rect > playerTokenRect(const State &state, const rules::GameState &gameState, rules::PlayerNumber player) noexcept
Definition TradeUI.cpp:728
constexpr std::size_t MaxTradeMessages
Definition TradeUI.hpp:16
constexpr Rect FutureNewRect
Definition TradeUI.hpp:67
constexpr Rect FutureTradeAT
Definition TradeUI.hpp:59
constexpr Rect ImmunityNewRect
Definition TradeUI.hpp:68
bool addTradeItem(State &state, const rules::GameState &gameState, const actions::Message &message)
Definition TradeUI.cpp:1434
std::vector< std::uint8_t > binaryData
Definition Actions.hpp:142
constexpr bool contains(int x, int y) const noexcept
Definition TradeUI.hpp:34