Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
TradeContractTextPlayback.cpp
Go to the documentation of this file.
2
3#include "FontRuntime.hpp"
4#include "IBarLayout.hpp"
9
10#include <algorithm>
11#include <array>
12#include <limits>
13#include <string_view>
14#include <utility>
15
16namespace monopoly::tradeui
17{
18 namespace
19 {
20 constexpr std::uint32_t PanelWidth = 200;
21 constexpr std::uint32_t PanelHeight = 225;
22 constexpr int PanelX = 600;
23 constexpr int WrapWidth = 176;
24 // GameInc.h includes Dat_Mon/dat_lang.h, not the obsolete Langids.h.
25 constexpr std::uint32_t OkayMessageId = 933;
26 constexpr std::uint32_t SquareNameBase = 1001;
27 constexpr std::uint32_t FutureHeadingId = 2016;
28 constexpr std::uint32_t ImmunityHeadingId = 2025;
29 constexpr std::uint32_t TextColour = 0x00DCDCDCU;
30 constexpr std::uint32_t SelectedTextColour = 0x00FFFFFFU;
31 constexpr std::uint32_t SelectedColour = 0x00B40000U; // COLORREF: (0,0,180).
32
33 [[nodiscard]] std::expected<std::u16string, std::string> messageText16(
34 const data::LanguageCatalog& catalog, std::uint32_t id)
35 {
36 const auto text = catalog.message(id);
37 if (!text) return std::unexpected(text.error().detail);
38 return **text;
39 }
40
41 [[nodiscard]] std::expected<std::string, std::string> messageText(
42 const data::LanguageCatalog& catalog, std::uint32_t id)
43 {
44 const auto text = messageText16(catalog, id);
45 if (!text) return std::unexpected(text.error());
46 const auto encoded = fonts::transcodeUtf8(std::u16string_view(*text));
47 if (!encoded) return std::unexpected(encoded.error().detail);
48 return *encoded;
49 }
50
51 [[nodiscard]] std::expected<std::u16string, std::string> squareName16(
52 const data::LanguageCatalog& catalog, int square,
53 int city, data::BoardEdition edition)
54 {
55 if (square < 0 || square >= static_cast<int>(rules::SquareCount))
56 return std::unexpected("Trade square-name index is out of range");
57
58 if (city == -1 && edition == data::BoardEdition::Europe)
59 city = static_cast<int>(catalog.language()) - 2;
60
61 const auto id64 = static_cast<std::uint64_t>(SquareNameBase) +
62 42ULL * static_cast<std::uint32_t>(std::max(city, 0)) +
63 static_cast<std::uint32_t>(square);
64 if (id64 > std::numeric_limits<std::uint32_t>::max())
65 return std::unexpected("Trade property-name index is out of range");
66
67 auto text = catalog.lookup(static_cast<std::uint32_t>(id64));
68 if (!text) return std::unexpected(text.error().detail);
69 if (!*text) return std::unexpected("Trade property name is missing from LANG");
70 if (!(**text)->empty() && (**text)->front() == u'*')
71 {
72 text = catalog.lookup(
73 SquareNameBase + static_cast<std::uint32_t>(square));
74 if (!text) return std::unexpected(text.error().detail);
75 if (!*text)
76 return std::unexpected(
77 "Trade base property name is missing from LANG");
78 }
79 return ***text;
80 }
81
82 [[nodiscard]] std::expected<std::string, std::string> propertyName(
83 const data::LanguageCatalog& catalog, std::uint32_t properties,
84 int city, data::BoardEdition edition)
85 {
86 int square = OffBoardSquare;
88 if ((ibar::layout::propertyBit(candidate) & properties) != 0)
89 {
90 square = candidate;
91 break;
92 }
93
94 const auto text = squareName16(catalog, square, city, edition);
95 if (!text) return std::unexpected(text.error());
96 const auto encoded = fonts::transcodeUtf8(std::u16string_view(*text));
97 if (!encoded) return std::unexpected(encoded.error().detail);
98 return *encoded;
99 }
100
101 [[nodiscard]] std::expected<std::u16string, std::string> propertySetNames16(
102 const data::LanguageCatalog& catalog, std::uint32_t properties,
103 int city, data::BoardEdition edition)
104 {
105 std::u16string output;
106 auto remaining = properties;
107 for (int candidate = 0;
108 candidate < OffBoardSquare && remaining != 0; ++candidate)
109 {
111 if (bit == 0 || (remaining & bit) == 0) continue;
112
113 const auto name = squareName16(catalog, candidate, city, edition);
114 if (!name) return std::unexpected(name.error());
115 if (!output.empty()) output += u", ";
116 output += *name;
117 remaining &= ~bit;
118 }
119 if (remaining != 0) output += u"...";
120 return output;
121 }
122
123 [[nodiscard]] std::expected<std::u16string, std::string> playerName16(
124 const rules::GameState& gameState,
125 const data::LanguageCatalog& catalog,
126 rules::PlayerNumber player)
127 {
128 if (player < rules::MaxPlayers)
130 std::wstring_view(gameState.players[player].name));
131
132 return messageText16(catalog,
133 player == rules::BankPlayer ? 900U :
134 player == rules::NobodyPlayer ? 902U : 901U);
135 }
136
137 [[nodiscard]] std::expected<std::string, std::string> confirmationText(
138 std::u16string_view pattern, std::int32_t amount,
140 const rules::GameState& gameState,
141 const data::LanguageCatalog& catalog,
142 int city, data::BoardEdition edition)
143 {
144 const language::LegacyFormatArguments arguments{
145 amount, recipient, 0, 0, {}};
146 language::LegacyFormatResolvers resolvers;
147 resolvers.playerName = [&](rules::PlayerNumber player)
148 {
149 return playerName16(gameState, catalog, player);
150 };
151 resolvers.squareName = [&](std::int64_t square)
152 {
153 return squareName16(
154 catalog, static_cast<int>(square), city, edition);
155 };
156 resolvers.propertySetNames = [&](std::uint32_t properties)
157 {
158 return propertySetNames16(
159 catalog, properties, city, edition);
160 };
161
164 if (!formatted) return std::unexpected(formatted.error());
165 const auto encoded = fonts::transcodeUtf8(
166 std::u16string_view(*formatted));
167 if (!encoded) return std::unexpected(encoded.error().detail);
168 return *encoded;
169 }
170
171 void fillRect(data::LegacyBitmapRGBA8& image, Rect rect,
172 std::uint32_t colour, std::uint8_t alpha = 255)
173 {
174 for (int y = std::max(0, rect.top);
176 for (int x = std::max(0, rect.left);
178 {
179 const auto offset = (static_cast<std::size_t>(y) * image.width +
180 static_cast<std::size_t>(x)) * 4U;
181 image.pixels[offset] = static_cast<std::uint8_t>(colour);
182 image.pixels[offset + 1] = static_cast<std::uint8_t>(colour >> 8U);
183 image.pixels[offset + 2] = static_cast<std::uint8_t>(colour >> 16U);
184 image.pixels[offset + 3] = alpha;
185 }
186 }
187
188 [[nodiscard]] std::expected<void, std::string> print(
189 data::LegacyBitmapRGBA8& image, fonts::Runtime& font,
190 std::string_view text, int x, int y, std::uint32_t colour)
191 {
192 if (text.empty()) return {};
193 const auto blitted = font.blitText(image, text, x, y, colour);
194 if (!blitted) return std::unexpected(blitted.error().detail);
195 return {};
196 }
197
198 [[nodiscard]] std::expected<void, std::string> printCentered(
199 data::LegacyBitmapRGBA8& image, fonts::Runtime& font,
200 std::string_view text, int y, std::uint32_t colour)
201 {
202 const auto metrics = font.measure(text);
203 if (!metrics) return std::unexpected(metrics.error().detail);
204 return print(image, font, text,
205 (static_cast<int>(PanelWidth) - metrics->width) / 2, y, colour);
206 }
207
208 void appendKey(std::string& key, std::string_view value)
209 {
210 key += std::to_string(value.size());
211 key.push_back(':');
212 key.append(value);
213 }
214
215 struct RestoreDefaultFont final
216 {
217 fonts::Runtime& runtime;
218 ~RestoreDefaultFont() { (void)runtime.restoreSettings(0); }
219 };
220 }
221
222 std::expected<void, std::string> ContractTextPlayback::sync(
223 const State& state, const rules::GameState& gameState,
226 {
228 state.contractDialogVisible;
229 if (!desired)
230 {
231 if (!visible_) return {};
232 if (!playback.commands().enqueue(sequence::StopSequenceCommand{
233 *surface_, TradeContractTextPriority, false}))
234 return std::unexpected("Trade contract-text stop queue is full");
235 visible_ = false;
236 return {};
237 }
238 const auto mode = state.contractDialogMode;
239 if (mode > 6 || state.contractDialogSide > 1 ||
240 (state.contractDialogKind != rules::TradeItemKind::FutureRent &&
241 state.contractDialogKind != rules::TradeItemKind::Immunity))
242 return std::unexpected("Trade contract-text state is invalid");
243 if (fontRuntime == nullptr || !fontRuntime->ready())
244 return std::unexpected("Trade contract-text font runtime is unavailable");
245 const auto resources = playback.resources();
246 const auto language = resources ? resources->language() : nullptr;
247 if (!language || !language->catalog)
248 return std::unexpected("Trade contract text has no language catalog");
249 if (!visible_ && playback.commands().pendingCount() >=
251 return std::unexpected("Trade contract-text start queue is full");
252
253 const auto& catalog = *language->catalog;
254 const auto edition = resources->context().board;
255 const auto headingId = state.contractDialogKind == rules::TradeItemKind::FutureRent
257 constexpr std::array<std::uint32_t, 7> PromptOffsets{{1, 2, 4, 5, 6, 7, 8}};
258 const auto promptId = headingId + PromptOffsets[mode] +
259 (mode == 1 ? state.contractDialogSide : 0U);
260 const auto heading = messageText(catalog, headingId);
261 if (!heading) return std::unexpected(heading.error());
262 const auto promptPattern = messageText16(catalog, promptId);
263 if (!promptPattern) return std::unexpected(promptPattern.error());
265 std::u16string_view(*promptPattern));
266 if (!promptEncoded)
267 return std::unexpected(promptEncoded.error().detail);
268 std::string prompt = *promptEncoded;
269 const auto okay = messageText(catalog, OkayMessageId);
270 if (!okay) return std::unexpected(okay.error());
271 std::string playerName;
272 if (mode != 0)
273 {
274 const auto player = state.contractDialogSide ? state.playerA : state.playerB;
275 if (player >= gameState.numberOfPlayers || player >= rules::MaxPlayers)
276 return std::unexpected("Trade contract recipient is out of range");
278 std::wstring_view(gameState.players[player].name));
280 return std::unexpected(encodedPlayerName.error().detail);
281 playerName = *encodedPlayerName;
282 if (mode == 3)
283 {
284 const auto formatted = confirmationText(
285 *promptPattern, state.contractAmount, player,
286 gameState, catalog, city, edition);
287 if (!formatted) return std::unexpected(formatted.error());
288 prompt = *formatted;
289 playerName.clear();
290 }
291 }
292 std::string amount;
293 if (mode == 2)
294 {
295 amount = std::to_string(state.contractAmount);
296 if (amount.size() < 2) amount.insert(amount.begin(), ' '); // %2i.
297 }
298
299 std::array<std::string, ContractListRects.size()> rows{};
300 std::array<bool, ContractListRects.size()> selected{};
301 const auto offset = state.contractList.empty() ? 0 : std::clamp(
302 state.contractListOffset, 0, static_cast<int>(state.contractList.size()) - 1);
303 for (std::size_t row = 0; mode != 0 && row < rows.size(); ++row)
304 {
305 const auto index = static_cast<std::size_t>(offset) + row;
306 if (index >= state.contractList.size()) break;
307 const auto& entry = state.contractList[index];
308 auto name = propertyName(catalog, entry.properties, city, edition);
309 if (!name) return std::unexpected(name.error());
310 if (mode >= 4)
311 {
312 rows[row] = std::to_string(entry.hitCount);
313 if (rows[row].size() < 2) rows[row].insert(rows[row].begin(), '0'); // %02d.
314 rows[row].push_back(' ');
315 }
316 rows[row] += *name;
317 selected[row] = entry.selected;
318 }
319 // The legacy line spacing is measured from PropertyList[0], even
320 // when the viewport starts further down the list.
321 std::string firstListText = rows.front();
322 if (mode != 0 && offset != 0)
323 {
324 const auto& first = state.contractList.front();
325 const auto name = propertyName(catalog, first.properties, city, edition);
326 if (!name) return std::unexpected(name.error());
327 firstListText.clear();
328 if (mode >= 4)
329 {
330 firstListText = std::to_string(first.hitCount);
331 if (firstListText.size() < 2) firstListText.insert(firstListText.begin(), '0');
332 firstListText.push_back(' ');
333 }
335 }
336 std::string key = std::to_string(mode) + ':' +
337 std::to_string(static_cast<int>(edition)) + ':';
339 for (const auto& text : {*heading, prompt, *okay, playerName, amount})
341 for (std::size_t row = 0; row < rows.size(); ++row)
342 {
343 appendKey(key, rows[row]);
344 key.push_back(selected[row] ? '1' : '0');
345 }
346
347 const bool contentChanged =
348 !contentKey_ || *contentKey_ != key;
349 if (visible_ && contentChanged && playback.commands().pendingCount() >=
351 return std::unexpected("sequence command queue cannot fit text redraw");
352 if (contentChanged)
353 {
354 RestoreDefaultFont restore{*fontRuntime};
355 const auto settings = fontRuntime->restoreSettings(7);
356 if (!settings) return std::unexpected(settings.error().detail);
358 image.pixels.assign(PanelWidth * PanelHeight * 4U, 0U);
359 const auto titleLines = fontRuntime->wrap(*heading, WrapWidth);
360 if (!titleLines) return std::unexpected(titleLines.error().detail);
361 const auto promptLines = fontRuntime->wrap(prompt, WrapWidth);
362 if (!promptLines) return std::unexpected(promptLines.error().detail);
363 const auto metrics = fontRuntime->measure(
364 titleLines->empty() ? std::string_view{} : std::string_view(titleLines->front()));
365 if (!metrics) return std::unexpected(metrics.error().detail);
366 int y = 15;
367 for (const auto& line : *titleLines)
368 {
369 if (auto result = printCentered(image, *fontRuntime, line, y, TextColour); !result)
370 return result;
371 y += metrics->height;
372 }
373 y += 3;
374 for (const auto& line : *promptLines)
375 {
376 if (auto result = printCentered(image, *fontRuntime, line, y, TextColour); !result)
377 return result;
378 y += metrics->height;
379 }
380
381 // UDTrade.cpp::DrawButton: 80x22, two-pixel bevel, black caption.
382 fillRect(image, {60,187,140,209}, 0x00BEBEBEU);
383 fillRect(image, {138,187,140,209}, 0x00646464U);
384 fillRect(image, {60,187,140,189}, 0x00DEDEDEU);
385 fillRect(image, {60,187,62,209}, 0x00DEDEDEU);
386 fillRect(image, {60,207,140,209}, 0x00646464U);
387 if (auto result = printCentered(image, *fontRuntime, *okay, 191, 0); !result)
388 return result;
389
390 if (mode != 0)
391 {
392 const bool usa = edition == data::BoardEdition::Usa;
393 fillRect(image, {14, usa ? 115 : 113, 166, usa ? 184 : 185}, 0, 0);
394 const auto lineMetrics = fontRuntime->measure(firstListText);
395 if (!lineMetrics) return std::unexpected(lineMetrics.error().detail);
396 y = 113;
397 for (std::size_t row = 0; row < rows.size(); ++row)
398 {
399 if (static_cast<std::size_t>(offset) + row >= state.contractList.size()) break;
400 auto rect = ContractListRects[row];
401 rect.left -= PanelX;
402 rect.right -= PanelX;
403 fillRect(image, rect, selected[row] ? SelectedColour : 0,
404 selected[row] ? 255 : 0);
405 if (auto result = print(image, *fontRuntime, rows[row], 16, y,
406 selected[row] ? SelectedTextColour : TextColour); !result)
407 return result;
408 y += lineMetrics->height;
409 }
410 // Retail clears the strip beside the list after printing;
411 // long property names must not overlap the arrow controls.
412 fillRect(image, {166, usa ? 115 : 113, 216, usa ? 184 : 185}, 0, 0);
413 }
414 if (auto result = print(image, *fontRuntime, playerName, 42, 101, TextColour); !result)
415 return result;
416 if (!amount.empty())
417 {
418 const auto size = fontRuntime->measure(amount);
419 if (!size) return std::unexpected(size.error().detail);
420 if (auto result = print(image, *fontRuntime, amount,
421 20 + (20 - size->width) / 2, 101, TextColour); !result)
422 return result;
423 }
424 if (!surface_)
425 {
426 const auto created = playback.runtimeBitmaps().create(PanelWidth, PanelHeight, true);
427 if (!created) return std::unexpected(created.error());
428 surface_ = *created;
429 }
430 const auto updated = playback.runtimeBitmaps().update(*surface_, std::move(image));
431 if (!updated) return std::unexpected(updated.error());
432 contentKey_ = std::move(key);
433 }
434 if (visible_)
435 {
436 if (contentChanged)
437 {
438 const auto forced = playback.forceRedraw(
439 *surface_, TradeContractTextPriority);
440 if (!forced) return forced;
441 }
442 return {};
443 }
445 if (!program) return std::unexpected(program.error().detail);
446 if (!playback.commands().enqueue(sequence::StartSequenceCommand{
447 *program, TradeContractTextPriority, {}, sequence::moveXYTransform(PanelX, 0)}))
448 return std::unexpected("Validated Trade contract-text start rejected");
449 visible_ = true;
450 return {};
451 }
452
453 void ContractTextPlayback::reset() noexcept
454 {
455 surface_.reset();
456 contentKey_.reset();
457 visible_ = false;
458 }
459}
std::size_t offset
const char * name
std::string_view text
LegacyBitmapRGBA8 image
fonts::Runtime & font
fonts::Metrics metrics
fonts::Runtime & runtime
std::uint64_t key
unsigned state
unsigned index
data::DataTag first
int colour
Rect rect
std::deque< Bytes > output
std::uint16_t value
Definition Timers.cpp:16
static std::expected< std::shared_ptr< const SequenceProgram >, RuntimeError > rawBitmap(data::DataId id, data::LegacyDataType sourceType, DescriptionLimits limits={})
std::expected< void, std::string > sync(const State &state, const rules::GameState &gameState, display::Screen2D desiredView, int city, fonts::Runtime *fontRuntime, engine::SequencePlayback &playback)
std::expected< std::string, Error > transcodeUtf8(std::u16string_view text)
PropertyMask propertyBit(int square) noexcept
std::expected< std::u16string, std::string > wideToUtf16(std::wstring_view text)
std::expected< std::u16string, std::string > formatLegacyMessage(std::u16string_view pattern, const LegacyFormatArguments &arguments, const LegacyFormatResolvers &resolvers, std::size_t limit)
std::vector< ObjectSpec > catalog(bool europe)
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
SequenceTransform moveXYTransform(std::int32_t x, std::int32_t y) noexcept
constexpr std::uint16_t TradeContractTextPriority
constexpr std::array< Rect, 5 > ContractListRects
Definition TradeUI.hpp:72
constexpr std::uint8_t OffBoardSquare
Definition TradeUI.hpp:25
std::vector< std::uint8_t > pixels