79 std::vector<TextSurface> result;
81 const auto language = resources.language();
82 const auto catalog = language ? language->catalog :
nullptr;
83 const auto edition = resources.context().board;
85 const auto label = [&](std::uint32_t
id)
87 if (!catalog) {
error =
"UDStats text has no LANG catalog";
return std::string{}; }
88 const auto value = catalog->lookup(
id);
90 if (!*
value) {
error =
"UDStats text is missing LANG ID " + std::to_string(
id);
return std::string{}; }
91 return utf8(std::u16string_view(***
value));
93 const auto money = [&](std::int64_t
value)
99 const auto property = [&](
int square)
101 if (!catalog) {
error =
"UDStats property has no LANG catalog";
return std::string{}; }
104 return utf8(std::u16string_view(*
text));
109 const auto count = std::min<std::size_t>(
state.playerCount,
117 return std::unexpected(
"UDStats text player is invalid");
125 add(surface, playerName(game, player), 0, 5,
width, 30, 14, 500,
129 result.push_back(std::move(surface));
135 if (!
grid)
return std::unexpected(
grid.error());
136 for (
const auto& item : *
grid)
138 if (
state.activeSort != 1)
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())]),
145 result.push_back(std::move(surface));
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};
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);
160 add(surface,
label(3206), 15, 65, 175, 18, 10, 700);
162 add(surface,
label(3207), 15, 118, 175, 18, 10, 700);
164 add(surface,
label(3208), 15, 167, 164, 33, 10, 700, 0xFFFFFF,
166 surface.text.back().singleLineY = 182;
169 result.push_back(std::move(surface));
175 if (
state.activeSort == 0)
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);
184 else if (
state.activeSort == 1)
186 else if (
state.activeSort == 2)
189 add(surface,
label(3113), 38, 55, 320, 25, 10, 700, 0xFAFAFA);
190 add(surface,
label(3114), 38, 139, 320, 25, 10, 700, 0xFAFAFA);
197 std::replace(
value->begin(),
value->end(),
' ',
'_');
198 for (std::size_t
i = 0;
i + 1 <
pattern.size(); ++
i)
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);
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;
224 result.push_back(std::move(surface));
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};
236 result.push_back(std::move(surface));
240 TextSurface
instruction{501, 411, 17, 182, 22, 101,
true};
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");
262 std::ostringstream
output;
output.imbue(std::locale::classic());
263 output << std::fixed << std::setprecision(1) << std::setw(3) <<
value;
266 else text = money(
static_cast<std::int64_t
>(
value));
267 TextSurface surface{502, 515, 54, 73, 14, 100,
true};
269 result.push_back(std::move(surface));
274 TextSurface surface{600, 601, 3, 197, 223, 612};
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;
283 surface.text.back().maxLines = 2;
284 for (std::size_t
row =
static_cast<std::size_t
>(std::max(
future.scrollIndex, 0)), shown = 0;
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);
291 result.push_back(std::move(surface));
293 if (!
error.empty())
return std::unexpected(
error);
300 if (!
font.ready())
return std::unexpected(
"UDStats font runtime is not ready");
302 return std::unexpected(
"UDStats text surface dimensions are invalid");
306 if (
modernAA && surface.
key == 500 && surface.
x == 611 && surface.
y == 19 &&
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)
317 auto candidate = [&]() -> std::optional<data::LegacyBitmapRGBA8>
319 const auto measure = [&](std::string_view
text)
321 return font.renderPresentation(
text.empty() ?
" " :
text,
322 0xFFFFFF, 10, 400,
false,
false,
false,
false);
324 std::vector<data::LegacyBitmapRGBA8>
lines;
325 std::string remaining = run.text;
329 const auto whole = measure(remaining);
331 0x00D7E8ED, 30, 400,
false);
334 if (
whole->width >
unsigned(run.width) ||
wholeRaster->width >
unsigned(run.width * 3))
341 const auto space = remaining.rfind(
' ',
search - 1);
342 if (
space == std::string::npos)
break;
344 while (end && remaining[end - 1] ==
' ') --end;
347 const auto prefix = measure(std::string_view(remaining).
substr(0, end));
349 std::string_view(remaining).
substr(0, end), 0x00D7E8ED, 30, 400,
false);
351 if (
prefix->width <=
unsigned(run.width) &&
prefixRaster->width <=
unsigned(run.width * 3))
356 if (!
found)
return {};
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);
372 if (
resume == remaining.size())
break;
373 remaining.erase(0,
resume);
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; }
380 for (
const auto& line :
lines)
382 if (!data::blitStraightRGBA8(result, line, 15,
y, data::BitmapBlitMode::SourceOver))
return {};
383 y +=
static_cast<int>(line.height);
394 bool italic, underline, strikeOut;
398 font.setItalic(italic);
font.setUnderline(underline);
font.setStrikeOut(strikeOut);
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;
410 if (opaque)
for (std::size_t
i = 3;
i <
image.pixels.size();
i += 4)
image.pixels[
i] = 255;
417 image.pixels[(
static_cast<std::size_t
>(
y) *
image.width +
x) * 4 + 3] = 255;
420 -> std::expected<void, std::string>
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) &&
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)
438 const auto native =
font.renderPresentation(run.text, 0x00D7E8ED, 12, 400,
false);
439 const auto raster =
font.renderPresentation(run.text, 0x00D7E8ED, 36, 400,
false);
444 data::BitmapBlitMode::SourceOver);
449 return std::unexpected(
resized.error().detail);
450 font.setWeight(run.weight);
452 for (
int size = run.size; size > 1;)
456 if (
measured->width <= run.width)
break;
461 std::vector<std::string>
lines{run.text};
464 const auto wrapped =
font.wrap(run.text, run.width);
471 for (
const auto& line :
lines)
473 const auto metrics =
font.measure(line.empty() ?
" " : line);
488 return std::unexpected(
resized.error().detail);
491 {0, 0,
static_cast<std::uint32_t
>(run.width *
rasterScale),
492 static_cast<std::uint32_t
>(
494 if (!
raster)
return std::unexpected(
raster.error().detail);
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;
500 if (!blit)
return blit;
503 if (
y >= run.height || (run.maxLines > 0 && ++
drawnLines >= run.maxLines))
break;
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);
510 for (
const auto& run : surface.
text)
512 if (!surface.history.empty())
514 if (surface.height <= 80)
return std::unexpected(
"UDStats history viewport is too short");
521 std::int64_t
total{};
522 for (
const auto&
row : surface.history)
525 if (!
lines)
return std::unexpected(
lines.error().detail);
530 const int contentHeight = std::min(surface.height - 80, 120);
532 if (historyScrollLimit)
533 *historyScrollLimit =
static_cast<int>(std::min<std::int64_t>(
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,
539 std::int64_t
y = -scroll;
541 for (std::size_t
i = 0;
i < surface.history.size(); ++
i)
543 const auto&
row = surface.history[
i];
548 const std::array<TextRun, 3>
runs{{
549 {
row.player,30,
static_cast<int>(
y),190,
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)
554 if (++shown == 10)
break;
559 if (
const auto blit = data::blitStraightRGBA8(
image,
content, 0, 80 *
rasterScale, data::BitmapBlitMode::SourceOver); !blit)
560 return std::unexpected(blit.error());