Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
StatsTextPlayback.cpp
Go to the documentation of this file.
2
3#include "AIUtility.hpp"
4#include "FontRuntime.hpp"
5#include "MoneyFormat.hpp"
10
11#include <algorithm>
12#include <cmath>
13#include <iomanip>
14#include <limits>
15#include <locale>
16#include <numeric>
17#include <sstream>
18
19namespace monopoly::statsui
20{
21 namespace
22 {
23 std::string utf8(std::u16string_view value)
24 {
26 return encoded ? *encoded : std::string{"\xEF\xBF\xBD"};
27 }
28
29 std::string utf8(std::wstring_view value)
30 {
32 return encoded ? *encoded : std::string{"\xEF\xBF\xBD"};
33 }
34
35 std::string playerName(const rules::GameState& game, rules::PlayerNumber player)
36 {
37 return player < game.numberOfPlayers && player < rules::MaxPlayers ?
38 utf8(std::wstring_view(game.players[player].name)) : std::string{};
39 }
40 bool bssm(const PlayerPlaybackInputs& inputs)
41 {
42 return inputs.iBarPlayerLocalHuman && (inputs.mode == ibar::RuleMode::Build ||
45 }
46 void add(TextSurface& surface, std::string text, int x, int y,
47 int width, int height, int size = 8, int weight = 500,
48 std::uint32_t colour = 0xFFFFFF, TextAlignment alignment = TextAlignment::Left,
49 bool wrap = false, bool shrink = false)
50 {
51 surface.text.push_back({std::move(text), x, y, width, height,
52 size, weight, colour, alignment, wrap, shrink});
53 }
54 std::string titlePlayer(std::string_view pattern, std::string_view name,
56 {
57 std::string text;
58 for (std::size_t i = 0; i < pattern.size(); ++i)
59 {
60 if (pattern[i] != '^' || i + 1 == pattern.size())
61 { text += pattern[i]; continue; }
62 switch (pattern[++i])
63 {
64 case 'P': case 'p': text += name; break;
65 case '2': text += std::to_string(player); break;
66 default: text += "^^"; break;
67 }
68 }
69 return text;
70 }
71 }
72
73 std::expected<std::vector<TextSurface>, std::string> planStatsTextSurfaces(
74 const State& state, const rules::GameState& game, const PlayerPlaybackInputs& inputs,
76 const AccountState& accounts, int city, int system, display::Screen2D view,
77 const data::ResourceSnapshot& resources)
78 {
79 std::vector<TextSurface> result;
80 if (view != display::Screen2D::Portfolio) return result;
81 const auto language = resources.language();
82 const auto catalog = language ? language->catalog : nullptr;
83 const auto edition = resources.context().board;
84 std::string error;
85 const auto label = [&](std::uint32_t id)
86 {
87 if (!catalog) { error = "UDStats text has no LANG catalog"; return std::string{}; }
88 const auto value = catalog->lookup(id);
89 if (!value) { error = value.error().detail; return std::string{}; }
90 if (!*value) { error = "UDStats text is missing LANG ID " + std::to_string(id); return std::string{}; }
91 return utf8(std::u16string_view(***value));
92 };
93 const auto money = [&](std::int64_t value)
94 {
95 const auto text = money::format(value, system, false, edition);
96 if (!text) { error = text.error(); return std::string{}; }
97 return *text;
98 };
99 const auto property = [&](int square)
100 {
101 if (!catalog) { error = "UDStats property has no LANG catalog"; return std::string{}; }
102 const auto text = statsPropertyName(*catalog, edition, city, square);
103 if (!text) { error = text.error(); return std::string{}; }
104 return utf8(std::u16string_view(*text));
105 };
106
107 if (state.screen == Screen::Player)
108 {
109 const auto count = std::min<std::size_t>(state.playerCount,
110 std::min<std::size_t>(game.numberOfPlayers, rules::MaxPlayers));
111 const int width = count > 4 ? 130 : 198;
112 int gap = 3;
113 for (std::size_t column = 0; column < count; ++column)
114 {
115 const auto player = state.playerOrder[column];
116 if (player >= game.numberOfPlayers || player >= rules::MaxPlayers)
117 return std::unexpected("UDStats text player is invalid");
118 if (bssm(inputs) && inputs.iBarPlayer != player) continue;
119 // Retail prints money after compositing the coin into OthersID.
120 // Our separate foreground must paint above the cash bitmap;
121 // equal priorities insert newly started text before older coins.
122 TextSurface surface{100 + player, static_cast<int>(column) * width + gap,
123 224, width, count > 4 ? 226 : 222, PlayerCashPriority + 1};
124 gap += 3;
125 add(surface, playerName(game, player), 0, 5, width, 30, 14, 500,
126 0xFFFFFF, TextAlignment::Center, false, true);
127 add(surface, money(game.players[player].cash), 15, 62, width - 15, 20,
128 8, 500, 0xC8C8C8);
129 result.push_back(std::move(surface));
130 }
131 }
132 if (state.screen == Screen::Deed)
133 {
134 const auto grid = planDeedGrid(state, game, inputs);
135 if (!grid) return std::unexpected(grid.error());
136 for (const auto& item : *grid)
137 {
138 if (state.activeSort != 1)
139 {
140 const auto rank = std::find(state.deedOrder.begin(), state.deedOrder.end(), item.square);
141 if (rank == state.deedOrder.end()) return std::unexpected("UDStats value bar has no sorted metric");
142 TextSurface surface{200 + item.square, item.x + 42, item.y + 13, 52, 13, 511};
143 add(surface, money(state.deedMetric[static_cast<std::size_t>(rank - state.deedOrder.begin())]),
144 0, 0, 52, 13, 8, 500, 0xC8C8C8, TextAlignment::Right);
145 result.push_back(std::move(surface));
146 }
147 // The calculator popup owns its separate deed preview (IsPopUpIDOn).
148 if (calculator.picker == CalculatorPicker::Deed ||
149 !state.mouseKnown || state.mouseX < item.x || state.mouseX >= item.x + 36 ||
150 state.mouseY < item.y || state.mouseY >= item.y + 42) continue;
151 TextSurface surface{300, state.mouseX > 400 ? 10 : 410, 220, 400, 235, 601};
152 surface.blackRect = Rect{193, 5, 393, 235};
153 const auto& square = game.squares[static_cast<std::size_t>(item.square)];
154 const auto owner = square.owner;
155 add(surface, label(3205), 15, 10, 175, 18, 10, 700);
156 add(surface, owner < game.numberOfPlayers && owner < rules::MaxPlayers ? playerName(game, owner) : label(900),
157 10, 28, 164, 32, 14, 500, 0xFFFFFF, TextAlignment::Right, false, true);
158 const auto rent = ai::rentIfSteppedOn(game, static_cast<rules::board::SquareType>(item.square),
159 ai::propertiesOwnedByPlayer(game, owner));
160 add(surface, label(3206), 15, 65, 175, 18, 10, 700);
161 add(surface, money(rent), 0, 83, 164, 32, 14, 500, 0xFFFFFF, TextAlignment::Right);
162 add(surface, label(3207), 15, 118, 175, 18, 10, 700);
163 add(surface, money(square.gameEarnings), 0, 136, 164, 32, 14, 500, 0xFFFFFF, TextAlignment::Right);
164 add(surface, label(3208), 15, 167, 164, 33, 10, 700, 0xFFFFFF,
165 TextAlignment::Left, true);
166 surface.text.back().singleLineY = 182;
167 // Source repeats current rent here despite the future-value label.
168 add(surface, money(rent), 0, 200, 164, 32, 14, 500, 0xFFFFFF, TextAlignment::Right);
169 result.push_back(std::move(surface));
170 }
171 }
172 if (state.screen == Screen::Bank)
173 {
174 TextSurface surface{400, 7, 226, 786, 223, 51};
175 if (state.activeSort == 0)
176 {
177 const auto totalHouses = std::accumulate(state.bankPlayerHouses.begin(), state.bankPlayerHouses.end(), 0);
178 const auto totalHotels = std::accumulate(state.bankPlayerHotels.begin(), state.bankPlayerHotels.end(), 0);
179 add(surface, label(3107) + ": " + std::to_string(state.bankHousesRemaining), 20, 23, 274, 25, 8, 500, 0xFAFAFA, TextAlignment::Center);
180 add(surface, label(3109) + ": " + std::to_string(state.bankHotelsRemaining), 491, 23, 274, 25, 8, 500, 0xFAFAFA, TextAlignment::Center);
181 add(surface, label(3108) + ": " + std::to_string(totalHouses), 20, 56, 367, 25, 8, 500, 0xFAFAFA, TextAlignment::Center);
182 add(surface, label(3110) + ": " + std::to_string(totalHotels), 398, 56, 367, 25, 8, 500, 0xFAFAFA, TextAlignment::Center);
183 }
184 else if (state.activeSort == 1)
185 add(surface, label(3111), 26, 27, 734, 25, 10, 700, 0xFAFAFA, TextAlignment::Center);
186 else if (state.activeSort == 2)
187 {
188 add(surface, label(3112), 271, 20, 250, 25, 10, 700, 0xFAFAFA, TextAlignment::Center);
189 add(surface, label(3113), 38, 55, 320, 25, 10, 700, 0xFAFAFA);
190 add(surface, label(3114), 38, 139, 320, 25, 10, 700, 0xFAFAFA);
191 const auto cardDescription = [&](std::uint32_t id, int amount)
192 {
193 auto pattern = label(id);
194 if (edition == data::BoardEdition::Usa) return pattern;
195 auto value = monopoly::money::format(amount, system, true, edition);
196 if (!value) { error = value.error(); return std::string{}; }
197 std::replace(value->begin(), value->end(), ' ', '_');
198 for (std::size_t i = 0; i + 1 < pattern.size(); ++i)
199 if (pattern[i] == '^' && (pattern[i + 1] == 'A' || pattern[i + 1] == 'a'))
200 { pattern.replace(i, 2, *value); i += value->size() - 1; }
201 return pattern;
202 };
203 add(surface, cardDescription(3176, 50), 35, 85, 320, 52, 10, 700, 0xFFFFFF, TextAlignment::Left, true);
204 surface.text.back().maxLines = 2;
205 add(surface, cardDescription(3177, 200), 35, 170, 320, 50, 10, 700, 0xFFFFFF, TextAlignment::Left, true);
206 surface.text.back().maxLines = 2;
207 if (accounts.dividendCount > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max() / 50) ||
208 accounts.bankErrorCount > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max() / 200))
209 return std::unexpected("UDStats liability total overflows money range");
210 add(surface, money(static_cast<std::int64_t>(accounts.dividendCount * 50)), 560, 77, 225, 30, 8, 500, 0xFAFAFA);
211 add(surface, money(static_cast<std::int64_t>(accounts.bankErrorCount * 200)), 560, 155, 225, 30, 8, 500, 0xFAFAFA);
212 }
213 else
214 {
215 add(surface, label(3115), 39, 27, 189, 25, 10, 700, 0xFAFAFA, TextAlignment::Center);
216 add(surface, label(3116), 27, 59, 197, 20, 10, 700, 0xFAFAFA, TextAlignment::Center);
217 add(surface, label(3117), 228, 59, 58, 20, 10, 700, 0xFAFAFA, TextAlignment::Center);
218 add(surface, label(3118), 290, 59, 442, 20, 10, 700, 0xFAFAFA, TextAlignment::Center);
219 for (const auto& row : accounts.history)
220 surface.history.push_back({playerName(game, row.player), std::to_string(row.turn),
221 utf8(std::u16string_view(row.description))});
222 surface.scrollLines = accounts.scrollLines;
223 }
224 result.push_back(std::move(surface));
225 }
226 if (calculator.visible)
227 {
228 if (calculator.hoveredFunction)
229 {
230 constexpr std::array<std::uint32_t, 8> descriptions{2005,2006,2007,2008,2009,2011,2010,2012};
231 if (*calculator.hoveredFunction >= descriptions.size())
232 return std::unexpected("UDStats calculator function is invalid");
233 TextSurface surface{500, 611, 19, 172, 185, 100, true};
234 add(surface, label(descriptions[*calculator.hoveredFunction]), 5, 0, 162, 185,
235 8, 500, 0xFFFFFF, TextAlignment::Left, true);
236 result.push_back(std::move(surface));
237 }
238 if (calculator.activeFunction)
239 {
240 TextSurface instruction{501, 411, 17, 182, 22, 101, true};
241 std::string text;
242 if (!calculator.result)
243 {
244 if (calculator.picker == CalculatorPicker::Deed) text = label(2001);
245 else if (calculator.picker == CalculatorPicker::Player) text = label(2003);
246 else if (calculator.step == CalculatorStep::Third) text = label(3179);
247 }
248 const bool finnish = resources.context().language == data::LanguageId::Finnish;
249 add(instruction, text, 5, finnish ? -1 : 0, 177, 22, finnish ? 7 : 8, 500,
250 0xFFFFFF, TextAlignment::Left, finnish);
251 result.push_back(std::move(instruction));
252 }
253 if (calculator.result)
254 {
255 const auto value = calculator.result->value;
256 if (!std::isfinite(value) || value < static_cast<double>(std::numeric_limits<std::int64_t>::min()) ||
257 value >= static_cast<double>(std::numeric_limits<std::int64_t>::max()))
258 return std::unexpected("UDStats calculator result is not representable");
259 std::string text;
261 {
262 std::ostringstream output; output.imbue(std::locale::classic());
263 output << std::fixed << std::setprecision(1) << std::setw(3) << value;
264 text = output.str() + "%";
265 }
266 else text = money(static_cast<std::int64_t>(value));
267 TextSurface surface{502, 515, 54, 73, 14, 100, true};
268 add(surface, text, 0, 0, 73, 14, 8, 500, 0x9A9A9A, TextAlignment::Right);
269 result.push_back(std::move(surface));
270 }
271 }
272 if (future.open)
273 {
274 TextSurface surface{600, 601, 3, 197, 223, 612};
275 const auto title = titlePlayer(label(future.kind == FutureImmunityKind::Future ? 3180 : 3181),
276 playerName(game, future.player), future.player);
277 add(surface, title, 0, future.kind == FutureImmunityKind::Future ? 9 : 10, 197, 20,
278 8, 500, 0xFAFAFA, TextAlignment::Center);
279 add(surface, label(3182), 15, 30, 105, 30, 8, 500, 0xFAFAFA);
280 add(surface, label(3183), 110, 25, 80, 35, 8, 500, 0xFAFAFA, TextAlignment::Center, true);
281 surface.text.back().singleLineY = 30;
282 surface.text.back().wrappedX = resources.context().language == data::LanguageId::Danish ? 110 : 120;
283 surface.text.back().maxLines = 2;
284 for (std::size_t row = static_cast<std::size_t>(std::max(future.scrollIndex, 0)), shown = 0;
285 row < future.rows.size() && shown < 10; ++row, ++shown)
286 {
287 const int y = 60 + static_cast<int>(shown) * 15;
288 add(surface, property(future.rows[row].square), 15, y, 105, 15, 8, 500, 0xFAFAFA);
289 add(surface, std::to_string(future.rows[row].hitCount), 150, y, 40, 15, 8, 500, 0xFAFAFA);
290 }
291 result.push_back(std::move(surface));
292 }
293 if (!error.empty()) return std::unexpected(error);
294 return result;
295 }
296
297 std::expected<data::LegacyBitmapRGBA8, std::string> renderStatsTextSurface(
298 const TextSurface& surface, fonts::Runtime& font, int* historyScrollLimit, bool modernAA)
299 {
300 if (!font.ready()) return std::unexpected("UDStats font runtime is not ready");
301 if (surface.width <= 0 || surface.height <= 0 || surface.width > 800 || surface.height > 600)
302 return std::unexpected("UDStats text surface dimensions are invalid");
303 // This one opaque calculator description is presentation-only. Check
304 // the entire independent-face raster before publishing any new pixels;
305 // every failure continues through the complete original painter below.
306 if (modernAA && surface.key == 500 && surface.x == 611 && surface.y == 19 &&
307 surface.width == 172 && surface.height == 185 && surface.priority == 100 &&
308 surface.opaque && !surface.blackRect && surface.history.empty() && surface.text.size() == 1)
309 {
310 const auto& run = surface.text.front();
311 if (run.x == 5 && run.y == 0 && run.width == 162 && run.height == 185 &&
312 run.size == 8 && run.weight == 500 && run.colour == 0xFFFFFF &&
313 run.alignment == TextAlignment::Left && run.wrap && !run.shrink &&
314 run.singleLineY == -1 && run.maxLines == 0 && run.wrappedX == -1 &&
315 !run.text.empty() && run.text.size() <= 4096)
316 {
317 auto candidate = [&]() -> std::optional<data::LegacyBitmapRGBA8>
318 {
319 const auto measure = [&](std::string_view text)
320 {
321 return font.renderPresentation(text.empty() ? " " : text,
322 0xFFFFFF, 10, 400, false, false, false, false);
323 };
324 std::vector<data::LegacyBitmapRGBA8> lines;
325 std::string remaining = run.text;
326 unsigned logicalHeight{}, rasterHeight{};
327 for (;;)
328 {
329 const auto whole = measure(remaining);
330 const auto wholeRaster = font.renderPresentation(remaining,
331 0x00D7E8ED, 30, 400, false);
332 if (!whole || !wholeRaster) return {};
333 std::size_t cut = remaining.size(), resume = cut;
334 if (whole->width > unsigned(run.width) || wholeRaster->width > unsigned(run.width * 3))
335 {
336 // Ordinary word boundaries only. An unbreakable
337 // oversized word returns the whole native panel.
338 bool found{};
339 for (auto search = remaining.size(); search != 0;)
340 {
341 const auto space = remaining.rfind(' ', search - 1);
342 if (space == std::string::npos) break;
343 auto end = space;
344 while (end && remaining[end - 1] == ' ') --end;
345 if (end)
346 {
347 const auto prefix = measure(std::string_view(remaining).substr(0, end));
348 const auto prefixRaster = font.renderPresentation(
349 std::string_view(remaining).substr(0, end), 0x00D7E8ED, 30, 400, false);
350 if (!prefix || !prefixRaster) return {};
351 if (prefix->width <= unsigned(run.width) && prefixRaster->width <= unsigned(run.width * 3))
352 { cut = end; resume = space + 1; found = true; break; }
353 }
354 search = space;
355 }
356 if (!found) return {};
357 }
358 auto line = remaining.substr(0, cut);
359 if (resume != remaining.size()) std::replace(line.begin(), line.end(), '_', ' ');
360 const auto native = measure(line);
361 auto raster = font.renderPresentation(line.empty() ? " " : line,
362 0x00D7E8ED, 30, 400, false);
363 if (!native || !raster || native->width > unsigned(run.width) ||
364 raster->width > unsigned(run.width * 3) ||
365 raster->pixels.size() != std::size_t(raster->width) * raster->height * 4)
366 return {};
367 logicalHeight += native->height;
368 rasterHeight += raster->height;
369 if (logicalHeight > unsigned(run.height) || rasterHeight > unsigned(run.height * 3))
370 return {};
371 lines.push_back(std::move(*raster));
372 if (resume == remaining.size()) break;
373 remaining.erase(0, resume);
374 }
375 data::LegacyBitmapRGBA8 result{516, 555, {}};
376 result.pixels.resize(std::size_t(result.width) * result.height * 4);
377 for (std::size_t i = 0; i < result.pixels.size(); i += 4)
378 { result.pixels[i] = 22; result.pixels[i + 1] = 60; result.pixels[i + 2] = 61; result.pixels[i + 3] = 255; }
379 int y{};
380 for (const auto& line : lines)
381 {
382 if (!data::blitStraightRGBA8(result, line, 15, y, data::BitmapBlitMode::SourceOver)) return {};
383 y += static_cast<int>(line.height);
384 }
385 return result;
386 }();
387 if (candidate) return std::move(*candidate);
388 }
389 }
390 struct Restore
391 {
393 int size, weight;
394 bool italic, underline, strikeOut;
395 ~Restore()
396 {
397 (void)font.setSize(size); font.setWeight(weight);
398 font.setItalic(italic); font.setUnderline(underline); font.setStrikeOut(strikeOut);
399 }
400 } restore{font, font.settings().size, font.settings().weight,
401 font.settings().italic, font.settings().underline, font.settings().strikeOut};
402 font.setItalic(false); font.setUnderline(false); font.setStrikeOut(false);
403 if (historyScrollLimit) *historyScrollLimit = 0;
404 const int rasterScale = modernAA ? 3 : 1;
405 const auto blank = [rasterScale](int width, int height, bool opaque)
406 {
408 data::LegacyBitmapRGBA8 image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), {}};
409 image.pixels.resize(static_cast<std::size_t>(width) * height * 4, 0);
410 if (opaque) for (std::size_t i = 3; i < image.pixels.size(); i += 4) image.pixels[i] = 255;
411 return image;
412 };
413 auto image = blank(surface.width, surface.height, surface.opaque);
414 if (surface.blackRect)
415 for (int y = std::max(0, surface.blackRect->top) * rasterScale; y < std::min(surface.height, surface.blackRect->bottom) * rasterScale; ++y)
416 for (int x = std::max(0, surface.blackRect->left) * rasterScale; x < std::min(surface.width, surface.blackRect->right) * rasterScale; ++x)
417 image.pixels[(static_cast<std::size_t>(y) * image.width + x) * 4 + 3] = 255;
418
419 const auto draw = [&](data::LegacyBitmapRGBA8& target, const TextRun& run)
420 -> std::expected<void, std::string>
421 {
422 if (run.text.empty()) return {};
423 if (run.width <= 0 || run.height <= 0 || run.width > 800 || run.height > 600)
424 return std::unexpected("UDStats text clipping rectangle is invalid");
425 if (modernAA && surface.key >= 100 && surface.key < 100 + int(rules::MaxPlayers) &&
426 surface.y == 224 && surface.priority == PlayerCashPriority + 1 &&
427 !surface.opaque && !surface.blackRect && surface.history.empty() &&
428 ((surface.width == 130 && surface.height == 226) ||
429 (surface.width == 198 && surface.height == 222)) &&
430 surface.x >= 3 && (surface.x - 3) % (surface.width + 3) == 0 &&
431 (surface.x - 3) / (surface.width + 3) < (surface.width == 130 ? 6 : 4) &&
432 surface.text.size() == 2 && &run == &surface.text[1] &&
433 run.x == 15 && run.y == 62 && run.width == surface.width - 15 && run.height == 20 &&
434 run.size == 8 && run.weight == 500 && run.colour == 0xC8C8C8 &&
435 run.alignment == TextAlignment::Left && !run.wrap && !run.shrink &&
436 run.singleLineY == -1 && run.maxLines == 0 && run.wrappedX == -1 && run.text.size() <= 128)
437 {
438 const auto native = font.renderPresentation(run.text, 0x00D7E8ED, 12, 400, false);
439 const auto raster = font.renderPresentation(run.text, 0x00D7E8ED, 36, 400, false);
440 if (native && raster && native->width <= unsigned(run.width) && native->height <= unsigned(run.height) &&
441 raster->width <= unsigned(run.width * rasterScale) && raster->height <= unsigned(run.height * rasterScale) &&
442 raster->pixels.size() == std::size_t(raster->width) * raster->height * 4)
443 return data::blitStraightRGBA8(target, *raster, run.x * rasterScale, run.y * rasterScale,
444 data::BitmapBlitMode::SourceOver);
445 // No new pixels were painted: keep the entire native cash run,
446 // including its8-point layout and colour, on every failure.
447 }
448 if (const auto resized = font.setSize(run.size); !resized)
449 return std::unexpected(resized.error().detail);
450 font.setWeight(run.weight);
451 if (run.shrink)
452 for (int size = run.size; size > 1;)
453 {
454 const auto measured = font.measure(run.text);
455 if (!measured) return std::unexpected(measured.error().detail);
456 if (measured->width <= run.width) break;
457 const auto resized = font.setSize(--size);
458 if (!resized) return std::unexpected(resized.error().detail);
459 font.setWeight(700);
460 }
461 std::vector<std::string> lines{run.text};
462 if (run.wrap)
463 {
464 const auto wrapped = font.wrap(run.text, run.width);
465 if (!wrapped) return std::unexpected(wrapped.error().detail);
466 lines = *wrapped;
467 }
468 auto clipped = blank(run.width, run.height, false);
469 int y{};
470 int drawnLines{};
471 for (const auto& line : lines)
472 {
473 const auto metrics = font.measure(line.empty() ? " " : line);
474 if (!metrics) return std::unexpected(metrics.error().detail);
475 if (!line.empty())
476 {
477 // Native metrics above own layout; only glyph coverage uses
478 // the enlarged font, restored before the next measurement.
479 const int nativeSize = font.settings().size;
480 struct RestoreGlyphSize
481 {
483 int size;
484 ~RestoreGlyphSize() { (void)font.setSize(size); }
486 if (modernAA)
487 if (const auto resized = font.setSize(nativeSize * rasterScale); !resized)
488 return std::unexpected(resized.error().detail);
489 const auto raster = font.renderClipped(
490 line, run.colour,
491 {0, 0, static_cast<std::uint32_t>(run.width * rasterScale),
492 static_cast<std::uint32_t>(
493 std::max(0, run.height - y) * rasterScale)}, modernAA);
494 if (!raster) return std::unexpected(raster.error().detail);
495 int x{};
496 if (run.alignment == TextAlignment::Center && !(lines.size() > 1 && run.wrappedX >= 0))
497 x = (run.width - metrics->width) / 2;
498 else if (run.alignment == TextAlignment::Right) x = run.width - metrics->width;
499 const auto blit = data::blitStraightRGBA8(clipped, *raster, x * rasterScale, y * rasterScale, data::BitmapBlitMode::SourceOver);
500 if (!blit) return blit;
501 }
502 y += std::max(metrics->height, 1);
503 if (y >= run.height || (run.maxLines > 0 && ++drawnLines >= run.maxLines)) break;
504 }
505 return data::blitStraightRGBA8(target, clipped,
506 (lines.size() > 1 && run.wrappedX >= 0 ? run.wrappedX : run.x) * rasterScale,
507 (lines.size() == 1 && run.singleLineY >= 0 ? run.singleLineY : run.y) * rasterScale,
508 data::BitmapBlitMode::SourceOver);
509 };
510 for (const auto& run : surface.text)
511 if (const auto rendered = draw(image, run); !rendered) return std::unexpected(rendered.error());
512 if (!surface.history.empty())
513 {
514 if (surface.height <= 80) return std::unexpected("UDStats history viewport is too short");
515 if (const auto resized = font.setSize(8); !resized) return std::unexpected(resized.error().detail);
516 font.setWeight(500);
517 const auto metrics = font.measure("Test");
518 if (!metrics) return std::unexpected(metrics.error().detail);
519 const auto lineHeight = std::max(metrics->height, 1);
520 std::vector<int> rowHeights;
521 std::int64_t total{};
522 for (const auto& row : surface.history)
523 {
524 const auto lines = font.wrap(row.description, 442);
525 if (!lines) return std::unexpected(lines.error().detail);
526 const int height = static_cast<int>(std::max<std::size_t>(1, lines->size())) * lineHeight;
527 rowHeights.push_back(height);
528 total += height;
529 }
530 const int contentHeight = std::min(surface.height - 80, 120);
531 auto content = blank(surface.width, contentHeight, false);
532 if (historyScrollLimit)
533 *historyScrollLimit = static_cast<int>(std::min<std::int64_t>(
534 std::max<std::int64_t>(0, total - contentHeight + lineHeight - 1) / lineHeight,
535 std::numeric_limits<int>::max()));
536 const auto scroll = std::clamp<std::int64_t>(
537 static_cast<std::int64_t>(surface.scrollLines) * lineHeight, 0,
538 std::max<std::int64_t>(0, total - contentHeight));
539 std::int64_t y = -scroll;
540 int shown{};
541 for (std::size_t i = 0; i < surface.history.size(); ++i)
542 {
543 const auto& row = surface.history[i];
544 const int height = rowHeights[i];
545 if (y + height > 0)
546 {
547 const int clipHeight = std::min(height, 600);
548 const std::array<TextRun, 3> runs{{
549 {row.player,30,static_cast<int>(y),190,clipHeight,8,500,0xFAFAFA},
550 {row.turn,230,static_cast<int>(y),60,clipHeight,8,500,0xFAFAFA},
551 {row.description,290,static_cast<int>(y),442,clipHeight,8,500,0xFFFFFF,TextAlignment::Left,true}}};
552 for (const auto& run : runs)
553 if (const auto rendered = draw(content, run); !rendered) return std::unexpected(rendered.error());
554 if (++shown == 10) break;
555 }
556 y += height;
557 if (y >= contentHeight) break;
558 }
559 if (const auto blit = data::blitStraightRGBA8(image, content, 0, 80 * rasterScale, data::BitmapBlitMode::SourceOver); !blit)
560 return std::unexpected(blit.error());
561 }
562 return image;
563 }
564
565 void TextPlayback::reset() noexcept
566 {
567 surfaces_.clear(); published_.clear(); current_.clear(); fontSettings_.reset();
568 historyScrollLimit_ = 0;
569 modernAA_ = false;
570 }
571
572 std::expected<void, std::string> TextPlayback::sync(
573 const State& state, const rules::GameState& game, const PlayerPlaybackInputs& inputs,
575 const AccountState& accounts, int city, int system, display::Screen2D view,
577 {
578 std::vector<TextSurface> desired;
579 if (view == display::Screen2D::Portfolio)
580 {
581 const auto resources = playback.resources();
582 if (!resources) return std::unexpected("UDStats text has no resource snapshot");
584 city, system, view, *resources);
585 if (!plan) return std::unexpected(plan.error());
586 desired = std::move(*plan);
587 }
588 if (!desired.empty() && (!font || !font->ready()))
589 return std::unexpected("UDStats text requires a ready font runtime");
590 const bool unchangedPlan = desired == current_;
591 const bool unchangedFont = desired.empty() ||
592 (fontSettings_ && *fontSettings_ == font->settings());
593 if (unchangedPlan && unchangedFont && modernAA == modernAA_)
594 return {};
595 if (!unchangedPlan && published_.size() + desired.size() > sequence::SequenceCommandQueue::Capacity - playback.commands().pendingCount())
596 return std::unexpected("sequence command queue cannot fit UDStats text transition");
597 std::vector<data::LegacyBitmapRGBA8> images;
598 int historyLimit{};
599 for (const auto& surface : desired)
600 {
601 auto image = renderStatsTextSurface(surface, *font,
602 surface.key == 400 ? &historyLimit : nullptr, modernAA);
603 if (!image) return std::unexpected(image.error());
604 images.push_back(std::move(*image));
605 }
606 std::vector<Published> next;
607 std::vector<std::shared_ptr<const sequence::SequenceProgram>> programs;
608 for (std::size_t i = 0; i < desired.size(); ++i)
609 {
610 const auto& surface = desired[i];
611 const auto key = std::tuple{surface.key, surface.width, surface.height};
612 auto found = surfaces_.find(key);
613 if (found == surfaces_.end())
614 {
615 const auto created = playback.runtimeBitmaps().create(surface.width, surface.height, !surface.opaque);
616 if (!created) return std::unexpected(created.error());
617 found = surfaces_.emplace(key, *created).first;
618 }
619 const auto program = sequence::SequenceProgram::rawBitmap(found->second, data::LegacyDataType::Native);
620 if (!program) return std::unexpected(program.error().detail);
621 programs.push_back(*program);
622 next.push_back({found->second, surface.priority});
623 }
624 for (std::size_t i = 0; i < next.size(); ++i)
625 if (const auto updated = playback.runtimeBitmaps().update(next[i].id, std::move(images[i]),
626 modernAA ? std::optional<std::array<float, 4>>{{0, 0, float(desired[i].width), float(desired[i].height)}} : std::nullopt,
627 modernAA); !updated)
628 return updated;
629 if (unchangedPlan)
630 {
631 // A presentation-mode refresh changes pixels, never the CNK node,
632 // placement, priority, clock or native history geometry.
633 modernAA_ = modernAA;
634 historyScrollLimit_ = historyLimit;
635 if (font) fontSettings_ = font->settings();
636 return {};
637 }
638 for (const auto& object : published_)
639 if (!playback.commands().enqueue(sequence::StopSequenceCommand{object.id, object.priority, false}))
640 return std::unexpected("validated UDStats text stop rejected");
641 for (std::size_t i = 0; i < next.size(); ++i)
642 if (!playback.commands().enqueue(sequence::StartSequenceCommand{
643 programs[i], next[i].priority, {}, sequence::moveXYTransform(desired[i].x, desired[i].y)}))
644 return std::unexpected("validated UDStats text start rejected");
645 published_ = std::move(next);
646 current_ = std::move(desired);
647 historyScrollLimit_ = historyLimit;
648 modernAA_ = modernAA;
649 if (font) fontSettings_ = font->settings();
650 return {};
651 }
652}
std::size_t count
decision::EconomicActionPlan plan
const char * name
std::string_view text
LegacyBitmapRGBA8 image
fonts::Runtime & font
data::DataId id
std::uint16_t priority
fonts::Metrics metrics
std::uint64_t key
std::uint32_t height
std::uint32_t width
unsigned state
unsigned index
std::string_view label
int colour
std::string error
data::DataTag future
std::deque< Bytes > output
std::uint16_t value
Definition Timers.cpp:16
CardTag native
std::int64_t rentIfSteppedOn(const rules::GameState &state, SquareType square, rules::board::PropertySet propertiesOwned) noexcept
rules::board::PropertySet propertiesOwnedByPlayer(const rules::GameState &state, rules::PlayerNumber player) noexcept
Definition AIUtility.cpp:49
Result run(const Request &request)
std::expected< std::string, Error > transcodeUtf8(std::u16string_view text)
std::expected< std::string, std::string > format(std::int64_t defaultValue, int monetarySystem, bool printSymbol, data::BoardEdition edition)
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
std::shared_ptr< const data::ResourceSnapshot > resources() noexcept
std::expected< std::vector< TextSurface >, std::string > planStatsTextSurfaces(const State &state, const rules::GameState &game, const PlayerPlaybackInputs &inputs, const CalculatorUIState &calculator, const FutureImmunityState &future, const AccountState &accounts, int city, int system, display::Screen2D view, const data::ResourceSnapshot &resources)
std::expected< std::u16string, std::string > statsPropertyName(const data::LanguageCatalog &catalog, data::BoardEdition edition, int city, int square)
std::expected< std::vector< DeedPlayback::Published >, std::string > planDeedGrid(const State &state, const rules::GameState &gameState, const PlayerPlaybackInputs &inputs)
std::expected< data::LegacyBitmapRGBA8, std::string > renderStatsTextSurface(const TextSurface &surface, fonts::Runtime &font, int *historyScrollLimit, bool modernAA)
constexpr std::uint16_t PlayerCashPriority
std::size_t size()
std::vector< std::uint8_t > pixels
std::array< SquareState, SquareCount > squares
std::array< PlayerState, MaxPlayers > players
std::vector< HistoryRow > history