Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
OptionsVisualPlayback.cpp
Go to the documentation of this file.
2#include "FontRuntime.hpp"
5#include <algorithm>
6#include <cmath>
7#include <memory>
8#include <utility>
9
10namespace monopoly::optionsui
11{
12 namespace
13 {
14
15 data::LegacyBitmapRGBA8 blank(int width, int height)
16 {
17 data::LegacyBitmapRGBA8 image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), {}};
18 image.pixels.assign(static_cast<std::size_t>(width) * height * 4U, 0);
19 return image;
20 }
21 std::expected<std::string, std::string> label(const engine::SequencePlayback& playback, std::uint32_t id)
22 {
23 const auto resources = playback.resources();
24 if (!resources || !resources->language() || !resources->language()->catalog)
25 return std::unexpected("Options text language catalog unavailable");
26 const auto text = resources->language()->catalog->message(id);
27 if (!text) return std::unexpected(text.error().detail);
28 const auto encoded = fonts::transcodeUtf8(std::u16string_view(**text));
29 if (!encoded) return std::unexpected(encoded.error().detail);
30 return *encoded;
31 }
32 std::expected<void, std::string> draw(data::LegacyBitmapRGBA8& image,
33 fonts::Runtime& font, std::string_view text, int y, std::uint32_t color = 0x00FFFFFFU,
34 fonts::Runtime* presentation = nullptr)
35 {
36 if (text.empty()) return {};
37 if (presentation)
38 {
39 const auto native = font.measure(text);
40 if (!native) return std::unexpected(native.error().detail);
41 const auto rendered = presentation->render(text, color, true);
42 if (!rendered) return std::unexpected(rendered.error().detail);
43 const unsigned width = std::max(1, native->width)*3U;
44 const unsigned height = std::max(1, native->height)*3U;
45 if (rendered->width == width && rendered->height == height)
47 // Hinting is size-dependent:3x font advances need not equal3x
48 // native advances. Fit coverage to the native measured extent,
49 // rather than rewrapping lines or clipping their final glyphs.
50 auto fitted = blank(width, height);
51 for (unsigned row=0; row<height; ++row)
52 {
53 const double sy=std::clamp((row+0.5)*rendered->height/height-0.5,0.0,double(rendered->height-1));
54 const unsigned y0=unsigned(sy),y1=std::min(y0+1,rendered->height-1);
55 const double fy=sy-y0;
56 for (unsigned column=0; column<width; ++column)
57 {
58 const double sx=std::clamp((column+0.5)*rendered->width/width-0.5,0.0,double(rendered->width-1));
59 const unsigned x0=unsigned(sx),x1=std::min(x0+1,rendered->width-1);
60 const double fx=sx-x0;
61 const auto alpha=[&](unsigned x,unsigned line)
62 { return rendered->pixels[(std::size_t(line)*rendered->width+x)*4+3]; };
63 const double top=alpha(x0,y0)*(1-fx)+alpha(x1,y0)*fx;
64 const double bottom=alpha(x0,y1)*(1-fx)+alpha(x1,y1)*fx;
65 const auto offset=(std::size_t(row)*width+column)*4;
66 for (unsigned channel=0;channel<3;++channel)
67 fitted.pixels[offset+channel]=std::uint8_t((color>>(channel*8))&255U);
68 fitted.pixels[offset+3]=std::uint8_t(std::lround(top*(1-fy)+bottom*fy));
69 }
70 }
72 }
73 const auto blitted = font.blitText(image, text, 0, y, color);
74 if (!blitted) return std::unexpected(blitted.error().detail);
75 return {};
76 }
77 struct RestoreFont
78 {
79 fonts::Runtime* font;
80 ~RestoreFont() { if (font) (void)font->restoreSettings(0); }
81 };
82 }
83
84 std::expected<std::vector<std::string>, std::string> wrapOptionsText(
85 fonts::Runtime& font, std::string_view text, int width, bool replaceUnderscores)
86 {
87 if (width <= 0) return std::unexpected("Options wrap width must be positive");
88 std::vector<std::string> lines;
89 while (!text.empty())
90 {
91 if (static_cast<unsigned char>(text.front()) < 32)
92 {
93 if (text.front() != '\n') lines.emplace_back();
94 text.remove_prefix(1);
95 continue;
96 }
97 std::size_t end = 0, lastSpace = std::string_view::npos;
98 while (end < text.size() && static_cast<unsigned char>(text[end]) >= 32)
99 {
100 if (text[end] == ' ') lastSpace = end;
101 ++end;
102 while (end < text.size() && (static_cast<unsigned char>(text[end]) & 0xC0U) == 0x80U) ++end;
103 const auto measured = font.measure(text.substr(0, end));
104 if (!measured) return std::unexpected(measured.error().detail);
105 if (measured->width >= width)
106 {
107 // Retail backs up to and includes the last space. Keep a
108 // bounded UTF-8 hard break for words with no space instead
109 // of reproducing the legacy backward buffer overrun.
110 if (lastSpace != std::string_view::npos) end = lastSpace + 1;
111 break;
112 }
113 }
114 std::string line(text.substr(0, end));
115 if (replaceUnderscores) std::replace(line.begin(), line.end(), '_', ' ');
116 lines.push_back(std::move(line));
117 text.remove_prefix(end);
118 }
119 return lines;
120 }
121
122 std::expected<void, std::string> VisualPlayback::sync(State& state,
123 display::Screen2D view, std::uint64_t tick, fonts::Runtime* font,
125 {
126 std::optional<Screen> screen;
127 if (view == display::Screen2D::Options && state.active &&
128 ((state.currentScreen == Screen::Option && state.optionSnapshotLoaded) ||
129 state.currentScreen == Screen::Credits ||
130 (state.currentScreen == Screen::Help && state.quickHelpVisible)))
131 screen = state.currentScreen;
132 const bool presentation = modernPresentation && screen && *screen != Screen::Credits;
133 const unsigned scale = presentation ? 3 : 1;
134 const bool viewChanged = screen != lastScreen_ || state.quickHelpVisible != lastQuickHelp_;
135 const bool changed = viewChanged || (screen && modernPresentation != lastPresentation_);
136 // Opaque modern Options backdrop owns layer10. These foreground panels
137 // use11 without altering native defaults or the Credits scroll clock.
138 const std::uint16_t foregroundPriority = modernPresentation ? 11 : 10;
139 const std::size_t count = !screen ? 0 : (*screen == Screen::Option ? 14 : (*screen == Screen::Credits ? 1 : 4));
140 if (changed && count + published_.size() > sequence::SequenceCommandQueue::Capacity - playback.commands().pendingCount())
141 return std::unexpected("sequence command queue cannot fit Options visual transition");
142 std::vector<Object> desired;
143 std::vector<data::DataId> changedSurfaces;
144 std::vector<std::pair<data::DataId, data::LegacyBitmapRGBA8>> pendingImages;
145 auto nextCreditY = lastCreditY_;
146 auto publishImage = [&](std::size_t index, data::LegacyBitmapRGBA8 image, int x, int y, std::uint16_t priority)
147 -> std::expected<void, std::string>
148 {
149 if (!surfaces_[index])
150 {
151 const unsigned imageScale = index == 14 ? 1 : scale;
152 const auto created = playback.runtimeBitmaps().create(image.width/imageScale, image.height/imageScale, true);
153 if (!created) return std::unexpected(created.error());
154 surfaces_[index] = *created;
155 }
156 pendingImages.emplace_back(*surfaces_[index], std::move(image));
157 changedSurfaces.push_back(*surfaces_[index]);
158 desired.push_back({*surfaces_[index], priority, x, y});
159 return {};
160 };
161 RestoreFont restore{nullptr};
162 fonts::Runtime* rasterFont = nullptr;
163 if (screen && *screen != Screen::Credits)
164 {
165 if (!font || !font->ready()) return std::unexpected("Options font runtime unavailable");
166 if (auto selected = font->restoreSettings(0); !selected) return std::unexpected(selected.error().detail);
167 restore.font = font;
168 const auto resources = playback.resources();
169 if (!resources) return std::unexpected("Options resources unavailable");
170 if (resources->context().board != data::BoardEdition::Usa)
171 {
172 if (auto sized = font->setSize(*screen == Screen::Help ? 12 : 10); !sized)
173 return std::unexpected(sized.error().detail);
174 font->setWeight(700);
175 }
176 if (presentation)
177 {
178 auto settings = font->settings(); settings.size *= 3;
179 if (!presentationFont_) presentationFont_ = std::make_shared<fonts::Runtime>();
180 if (!presentationFont_->ready() || presentationFont_->settings().fontPath != settings.fontPath)
181 if (auto selected = presentationFont_->setFont(settings.fontPath, settings.familyName); !selected)
182 return std::unexpected(selected.error().detail);
183 if (presentationFont_->settings().size != settings.size)
184 if (auto sized = presentationFont_->setSize(settings.size); !sized)
185 return std::unexpected(sized.error().detail);
186 presentationFont_->setWeight(settings.weight);
187 presentationFont_->setItalic(settings.italic);
188 presentationFont_->setUnderline(settings.underline);
189 presentationFont_->setStrikeOut(settings.strikeOut);
190 rasterFont = presentationFont_.get();
191 }
192 }
193 if (screen == Screen::Option)
194 {
195 if (!changed && lastTune_ == state.musicTuneIndex) return {};
196 const bool usa = playback.resources()->context().board == data::BoardEdition::Usa;
197 for (std::size_t index = 0; index < OptionLabelTextIds.size(); ++index)
198 {
200 if (!text) return std::unexpected(text.error());
201 const auto metrics = font->measure(*text);
202 if (!metrics) return std::unexpected(metrics.error().detail);
203 auto image = blank(200*scale, std::max(1, metrics->height * 2)*scale);
204 if (usa)
205 {
206 if (auto result = draw(image, *font, *text, 0, index == 8 ? 0x00808080U : 0x00FFFFFFU, rasterFont); !result) return result;
207 }
208 else
209 {
210 const auto lines = wrapOptionsText(*font, *text, 200, true);
211 if (!lines) return std::unexpected(lines.error());
212 int y = lines->size() > 1 ? 0 : 5;
213 for (const auto& line : *lines)
214 {
215 if (auto result = draw(image, *font, line, y, index == 8 ? 0x00808080U : 0x00FFFFFFU, rasterFont); !result) return result;
216 y += metrics->height;
217 }
218 }
219 const auto rect = optionToggleRect(static_cast<OptionToggle>(index), false);
220 if (auto result = publishImage(index, std::move(image), rect.right + 10, rect.top + (usa ? 5 : 0), 50); !result)
221 return result;
222 }
223 int y = 253;
224 for (std::size_t index = 0; index < MusicTuneCount; ++index)
225 {
226 const auto text = usa ? std::string(MusicNames[index]) : std::to_string(index + 1);
227 auto image = font->render(text, index == state.musicTuneIndex ? 0x00FF0000U : 0x00FFFFFFU);
228 if (!image) return std::unexpected(image.error().detail);
229 state.musicChoiceRects[index] = {163, y, 163 + static_cast<int>(image->width), y + static_cast<int>(image->height)};
230 const int height = static_cast<int>(image->height);
231 if (presentation)
232 {
233 auto high = blank(image->width*3, image->height*3);
234 if (auto result = draw(high, *font, text, 0,
235 index == state.musicTuneIndex ? 0x005E9DBCU : 0x00FFFFFFU, rasterFont); !result) return result;
236 *image = std::move(high);
237 }
238 if (auto result = publishImage(9 + index, std::move(*image), 163, y, 50); !result) return result;
239 y += height;
240 }
241 }
242 else if (screen == Screen::Credits)
243 {
244 const auto resources = playback.resources();
245 if (!resources) return std::unexpected("Options credits resources unavailable");
246 const auto id = creditsBitmap(resources->context().board);
247 if (!credits_ || credits_->dataId != id)
248 {
249 const auto metadata = resources->data().metadata(id);
250 if (!metadata) return std::unexpected(metadata.error().detail);
251 const auto bytes = resources->data().load(id);
252 if (!bytes) return std::unexpected(bytes.error().detail);
253 const auto asset = bitmapCache_.resolve(id, metadata->type, *bytes);
254 if (!asset) return std::unexpected(asset.error().detail);
255 credits_ = *asset;
256 }
257 if (viewChanged) creditsStart_ = tick;
258 int y = creditsScrollY(tick >= creditsStart_ ? tick - creditsStart_ : 0);
259 if (y + static_cast<int>(credits_->image.height) <= 0)
260 {
261 creditsStart_ = tick;
262 y = 486;
263 }
264 if (!changed && y == lastCreditY_) return {};
265 auto image = blank(static_cast<int>(credits_->image.width), 486);
267 return copied;
268 if (auto result = publishImage(14, std::move(image), 400 - static_cast<int>(credits_->image.width) / 2, 0, foregroundPriority); !result)
269 return result;
270 nextCreditY = y;
271 }
272 else if (screen == Screen::Help)
273 {
274 if (!changed && lastHelpLine_ == state.quickHelpFirstLine && !state.quickHelpLines.empty()) return {};
275 const auto metrics = font->measure("TEST");
276 if (!metrics) return std::unexpected(metrics.error().detail);
277 const int lineHeight = std::max(1, metrics->height);
278 if (state.quickHelpLines.empty())
279 {
280 const auto lines = wrapOptionsText(*font, state.quickHelpText, 600,
281 playback.resources()->context().board != data::BoardEdition::Usa);
282 if (!lines) return std::unexpected(lines.error());
283 state.quickHelpLines = *lines;
284 state.quickHelpFirstLine = 0;
285 state.quickHelpPageIndex = 0;
286 state.quickHelpPageOffsets.assign(2, 0);
287 state.quickHelpInitialPage = true;
288 }
289 state.quickHelpLinesPerPage = quickHelpPageLineCount(
290 state.quickHelpLines.size(), state.quickHelpFirstLine,
291 lineHeight, state.quickHelpInitialPage);
292 if (state.quickHelpPageOffsets.size() < state.quickHelpPageIndex + 2)
293 state.quickHelpPageOffsets.resize(state.quickHelpPageIndex + 2);
294 auto image = blank(600*scale, 450*scale);
295 int y = 0;
296 for (std::size_t index = state.quickHelpFirstLine;
297 index < state.quickHelpLines.size() && index < state.quickHelpFirstLine + state.quickHelpLinesPerPage; ++index)
298 {
299 if (auto result = draw(image, *font, state.quickHelpLines[index], y, 0x00FFFFFFU, rasterFont); !result) return result;
300 y += lineHeight;
301 }
302 if (auto result = publishImage(15, std::move(image), 100, 0, foregroundPriority); !result) return result;
303 constexpr std::array<std::uint32_t, 3> ids{3170, 3169, 932};
304 constexpr std::array<int, 3> xs{100, 650, 400};
305 for (std::size_t index = 0; index < ids.size(); ++index)
306 {
307 const auto text = label(playback, ids[index]);
308 if (!text) return std::unexpected(text.error());
309 auto button = font->render(*text, 0x00FFFFFFU);
310 if (!button) return std::unexpected(button.error().detail);
311 const bool enabled = index == 2 || (index == 0 ? state.quickHelpPageIndex > 0 :
312 state.quickHelpLinesPerPage > 0 &&
313 state.quickHelpFirstLine + state.quickHelpLinesPerPage < state.quickHelpLines.size());
314 state.quickHelpButtonRects[index] = enabled ? Rect{xs[index],455,
315 xs[index]+static_cast<int>(button->width),455+static_cast<int>(button->height)} : Rect{};
316 if (!enabled) std::fill(button->pixels.begin(), button->pixels.end(), 0);
317 if (presentation)
318 {
319 auto high = blank(button->width*3, button->height*3);
320 if (enabled)
321 if (auto result = draw(high, *font, *text, 0, 0x00FFFFFFU, rasterFont); !result) return result;
322 *button = std::move(high);
323 }
324 if (auto result = publishImage(16 + index, std::move(*button), xs[index], 455, 50); !result) return result;
325 }
326 }
327 const std::size_t requiredCommands = desired == published_
328 ? changedSurfaces.size() : published_.size() + desired.size();
330 return std::unexpected("sequence command queue cannot fit Options visual update");
331 for (auto& [id, image] : pendingImages)
332 {
333 std::optional<std::array<float,4>> rect;
334 if (presentation)
335 {
336 const auto extent = playback.runtimeBitmaps().extent(id);
337 rect = std::array<float,4>{0,0,float(extent->width),float(extent->height)};
338 }
339 if (auto updated = playback.runtimeBitmaps().update(id, std::move(image), rect, presentation); !updated)
340 return updated;
341 }
342 lastCreditY_ = nextCreditY;
343
344 if (desired == published_)
345 {
346 for (const auto id : changedSurfaces)
347 {
348 const auto object = std::find_if(desired.begin(), desired.end(),
349 [id](const auto& value) { return value.id == id; });
350 if (object == desired.end()) continue;
351 const auto forced = playback.forceRedraw(id, object->priority);
352 if (!forced) return forced;
353 }
354 }
355 else
356 {
357 if (desired.size() + published_.size() > sequence::SequenceCommandQueue::Capacity - playback.commands().pendingCount())
358 return std::unexpected("sequence command queue cannot fit Options visual objects");
359 std::vector<std::shared_ptr<const sequence::SequenceProgram>> programs;
360 for (const auto& object : desired)
361 {
363 if (!program) return std::unexpected(program.error().detail);
364 programs.push_back(*program);
365 }
366 for (const auto& object : published_)
367 if (!playback.commands().enqueue(sequence::StopSequenceCommand{object.id, object.priority, false}))
368 return std::unexpected("validated Options visual stop rejected");
369 for (std::size_t index = 0; index < desired.size(); ++index)
370 {
371 const auto& object = desired[index];
372 if (!playback.commands().enqueue(sequence::StartSequenceCommand{programs[index], object.priority, {},
373 sequence::moveXYTransform(object.x, object.y)}))
374 return std::unexpected("validated Options visual start rejected");
375 }
376 published_ = std::move(desired);
377 }
378 lastScreen_ = screen;
379 lastQuickHelp_ = state.quickHelpVisible;
380 lastPresentation_ = modernPresentation;
381 lastTune_ = state.musicTuneIndex;
382 lastHelpLine_ = state.quickHelpFirstLine;
383 return {};
384 }
385
386 void VisualPlayback::reset() noexcept
387 {
388 surfaces_.fill(std::nullopt);
389 published_.clear();
390 lastScreen_.reset();
391 lastQuickHelp_ = false;
392 lastPresentation_ = false;
393 presentationFont_.reset();
394 lastTune_ = -1;
395 lastHelpLine_ = static_cast<std::size_t>(-1);
396 creditsStart_ = 0;
397 lastCreditY_ = -1;
398 credits_.reset();
399 bitmapCache_.clear();
400 }
401}
std::size_t count
std::size_t offset
std::string_view text
LegacyBitmapMetadata metadata
LegacyBitmapRGBA8 image
std::vector< std::byte > bytes
int bottom
int top
fonts::Runtime & font
bool enabled
Definition Engine.cpp:170
std::uint16_t priority
fonts::Metrics metrics
std::uint32_t height
std::uint32_t width
unsigned state
unsigned index
std::string_view label
Rect rect
Button button
std::uint16_t value
Definition Timers.cpp:16
CardTag native
std::expected< std::shared_ptr< const BitmapRuntimeAsset >, BitmapError > resolve(DataId id, LegacyDataType sourceType, SharedDataBytes bytes)
std::expected< void, std::string > sync(State &state, display::Screen2D view, std::uint64_t tick, fonts::Runtime *fontRuntime, engine::SequencePlayback &playback, bool modernPresentation=false)
static std::expected< std::shared_ptr< const SequenceProgram >, RuntimeError > rawBitmap(data::DataId id, data::LegacyDataType sourceType, DescriptionLimits limits={})
std::expected< void, std::string > blitStraightRGBA8(LegacyBitmapRGBA8 &destination, const LegacyBitmapRGBA8 &source, std::int32_t destinationX, std::int32_t destinationY, BitmapBlitMode mode)
std::expected< std::string, Error > transcodeUtf8(std::u16string_view text)
Rect optionToggleRect(OptionToggle toggle, bool onSide) noexcept
Definition OptionsUI.cpp:77
constexpr std::uint8_t MusicTuneCount
Definition OptionsUI.hpp:66
constexpr std::array< std::string_view, 5 > MusicNames
constexpr int creditsScrollY(std::uint64_t elapsedTicks) noexcept
std::size_t quickHelpPageLineCount(std::size_t totalLines, std::size_t firstLine, int lineHeight, bool initialPage) noexcept
Definition OptionsUI.cpp:8
constexpr std::array< std::uint32_t, 9 > OptionLabelTextIds
std::expected< std::vector< std::string >, std::string > wrapOptionsText(fonts::Runtime &font, std::string_view text, int width, bool replaceUnderscores)
constexpr data::DataId creditsBitmap(data::BoardEdition edition) noexcept
SequenceTransform moveXYTransform(std::int32_t x, std::int32_t y) noexcept
std::shared_ptr< const data::ResourceSnapshot > resources() noexcept