Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
PlayerSelectionPlayback.cpp
Go to the documentation of this file.
3#include "MoneyFormat.hpp"
4#include "FontRuntime.hpp"
5
6#include <algorithm>
7#include <SDL3/SDL.h>
8
10{
11 namespace detail
12 {
13 std::expected<std::vector<std::string>, std::string> wrapRuleDescription(
14 fonts::Runtime& font, std::string_view value, int maxPixelWidth, std::size_t maxLines)
15 {
16 if (maxPixelWidth <= 0)
17 return std::unexpected("Player selection rule wrap width must be positive");
18
19 auto nextBoundary = [](std::string_view text, std::size_t offset) noexcept
20 {
21 if (offset >= text.size()) return text.size();
22 ++offset;
23 while (offset < text.size() &&
24 (static_cast<unsigned char>(text[offset]) & 0xC0U) == 0x80U)
25 ++offset;
26 return offset;
27 };
28
29 std::vector<std::string> lines;
30 std::string remaining(value);
31 while (!remaining.empty() && lines.size() < maxLines)
32 {
33 const auto whole = font.measure(remaining);
34 if (!whole) return std::unexpected(whole.error().detail);
35 if (whole->width <= maxPixelWidth)
36 {
37 lines.push_back(std::move(remaining));
38 break;
39 }
40
41 std::size_t cut = std::string::npos;
42 std::size_t search = remaining.size();
43 while (search != 0)
44 {
45 const auto space = remaining.rfind(' ', search - 1);
46 if (space == std::string::npos) break;
47 const auto prefix = font.measure(std::string_view(remaining).substr(0, space));
48 if (!prefix) return std::unexpected(prefix.error().detail);
49 if (prefix->width <= maxPixelWidth)
50 {
51 cut = space;
52 break;
53 }
54 search = space;
55 }
56
57 if (cut != std::string::npos)
58 {
59 lines.push_back(remaining.substr(0, cut));
60 remaining.erase(0, cut + 1);
61 continue;
62 }
63
64 // udpsel_WordWrap hard-breaks the first word when no space-delimited
65 // prefix fits. Consume at least one whole UTF-8 code point.
66 auto wordEnd = remaining.find(' ');
67 if (wordEnd == std::string::npos) wordEnd = remaining.size();
68 std::size_t best{};
69 for (std::size_t end = nextBoundary(remaining, 0);
70 end != 0 && end <= wordEnd;)
71 {
72 const auto prefix = font.measure(std::string_view(remaining).substr(0, end));
73 if (!prefix) return std::unexpected(prefix.error().detail);
74 if (prefix->width > maxPixelWidth)
75 {
76 if (best == 0) best = end;
77 break;
78 }
79 best = end;
80 if (end == wordEnd) break;
81 const auto next = nextBoundary(remaining, end);
82 if (next == end) break;
83 end = next;
84 }
85 if (best == 0) best = nextBoundary(remaining, 0);
86 lines.push_back(remaining.substr(0, best));
87 remaining.erase(0, best);
88 }
89 return lines;
90 }
91 }
92
93 namespace
94 {
96 using O = detail::Object;
97 using Spec = detail::ObjectSpec;
98 constexpr std::array<int, 8> CardX{35,141,241,344,36,141,242,345};
99 constexpr std::array<int, 8> CardY{253,253,253,253,370,371,371,372};
100 constexpr std::array<std::array<int,4>,16> Values{{
101 {{4,5,0,0}}, {{12,32,60,88}}, {{4,12,16,22}}, {{0,250,500,750}},
102 {{500,1000,1500,2000}}, {{0,100,200,400}}, {{0,5,10,15}},
103 {{0,100,200,400}}, {{0,75,150,300}}, {{1,2,3,4}}, {{0,50,100,200}},
104 {{0,1,6,12}}, {{0,1,3,6}}, {{0,5,10,20}}, {{3,4,5,10}}, {{0,2,4,28}}
105 }};
106
107 template<class Char> std::string utf8(std::basic_string_view<Char> value)
108 {
109 const char* encoding = sizeof(Char) == 2 ? "UTF-16LE" : "UTF-32LE";
110 const std::basic_string<Char> terminated(value);
111 char* converted = SDL_iconv_string("UTF-8", encoding,
112 reinterpret_cast<const char*>(terminated.c_str()), (value.size()+1)*sizeof(Char));
113 if (!converted) return {};
114 std::string result(converted);
116 return result;
117 }
118
119 std::expected<std::string,std::string> text(
120 const engine::SequencePlayback& playback, std::uint32_t id)
121 {
122 const auto resources = playback.resources();
123 if (!resources || !resources->language() || !resources->language()->catalog)
124 return std::unexpected("Player selection language catalog unavailable");
125 auto result = resources->language()->catalog->message(id);
126 if (!result) return std::unexpected(result.error().detail);
127 return utf8<char16_t>(**result);
128 }
129
130 struct RestoreFont
131 {
132 fonts::Runtime& font;
133 ~RestoreFont() { (void)font.restoreSettings(9); }
134 };
135
136 bool enabled(O object, const RenderState& s)
137 {
138 switch(object)
139 {
140 case O::ENTERNAME_BUTTON_NEXT: return s.setup.name.size()>1;
141 case O::SELECTPLAYER_BUTTON_MORE: return s.setup.playerLogCount>8;
142 case O::START_BUTTON_ADDHUMAN:
143 case O::START_BUTTON_ADDCOMPUTER: return s.game.numberOfPlayers<rules::MaxPlayers;
144 case O::START_BUTTON_REMOVEPLAYER: return s.localPlayers>0;
145 case O::START_BUTTON_STARTGAME:
146 return s.setup.serverMode && s.game.numberOfPlayers>=2 &&
147 std::any_of(s.game.players.begin(), s.game.players.begin()+
148 std::min<std::size_t>(s.game.numberOfPlayers,rules::MaxPlayers),
149 [](const auto& p){return p.aiPlayerLevel==0;});
150 case O::RULES_BUTTON_OKAY: return s.setup.serverMode;
151 default: break;
152 }
153 constexpr std::array<O,11> buttons{O::PICKTOKEN_BUTTON_CANNON,
154 O::PICKTOKEN_BUTTON_RACECAR,O::PICKTOKEN_BUTTON_DOG,O::PICKTOKEN_BUTTON_TOPHAT,
155 O::PICKTOKEN_BUTTON_IRON,O::PICKTOKEN_BUTTON_HORSE,O::PICKTOKEN_BUTTON_BATTLESHIP,
156 O::PICKTOKEN_BUTTON_SHOE,O::PICKTOKEN_BUTTON_THIMBLE,
157 O::PICKTOKEN_BUTTON_WHEELBARROW,O::PICKTOKEN_BUTTON_SACKOFMONEY};
158 constexpr std::array<O,11> rotating{O::PICKTOKEN_ROTATING_CANNON,
159 O::PICKTOKEN_ROTATING_RACECAR,O::PICKTOKEN_ROTATING_DOG,O::PICKTOKEN_ROTATING_TOPHAT,
160 O::PICKTOKEN_ROTATING_IRON,O::PICKTOKEN_ROTATING_HORSE,O::PICKTOKEN_ROTATING_BATTLESHIP,
161 O::PICKTOKEN_ROTATING_SHOE,O::PICKTOKEN_ROTATING_THIMBLE,
162 O::PICKTOKEN_ROTATING_WHEELBARROW,O::PICKTOKEN_ROTATING_SACKOFMONEY};
163 for (std::uint8_t i=0;i<11;++i)
164 {
165 if (object==buttons[i]) return ui::playersetup::tokenAvailable(s.game,i);
166 if (object==rotating[i]) return s.setup.token==i;
167 }
168 return true;
169 }
170 }
171
172 std::expected<void,std::string> PlayerSelectionPlayback::sync(
174 {
175 const bool visible = s.view==display::Screen2D::PlayerSelect ||
177 const P target = visible ? s.setup.phase : P::None;
178 const bool europe = s.setup.boardEdition==data::BoardEdition::Europe;
179 auto next = live_;
180 std::vector<sequence::SequenceCommand> commands;
181 auto change = [&](Live& object, data::DataId id) -> std::expected<void,std::string>
182 {
183 if (object.id==id) return {};
184 std::shared_ptr<const sequence::SequenceProgram> program;
185 if (id)
186 {
190 if (!loaded) return std::unexpected(loaded.error().detail);
191 program=*loaded;
192 }
193 if (object.id) commands.emplace_back(sequence::StopSequenceCommand{object.id,object.priority,false});
194 if (id)
195 {
197 options.endingAction=object.loop ? 3 : 2;
198 commands.emplace_back(sequence::StartSequenceCommand{program,object.priority,options,
199 sequence::moveXYTransform(object.x,object.y)});
200 }
201 object.id=id;
202 return {};
203 };
204 // Only runtime clock completion may promote an animation to idle.
205 for(auto& [key, object]:next)
206 {
207 if (!visible)
208 {
209 if (auto r=change(object,0);!r) return r;
210 continue;
211 }
212 if (!object.animating) continue;
213 const auto info=playback.runtime().info(object.id,object.priority);
214 if (!info || info->sequenceClock<info->endTime) continue;
215 if(auto r=change(object,object.leaving ? 0 : object.idle);!r) return r;
216 object.animating=false;
217 }
218 std::erase_if(next,[](const auto& item){return item.second.id==0;});
219 const bool phaseChange=phase_!=target;
220 if(phaseChange)
221 {
222 for(auto& [key,object]:next)
223 {
224 if(object.leaving) continue;
225 if(auto r=change(object,object.out);!r) return r;
226 object.leaving=true;
227 object.animating=object.id!=0;
228 }
229 std::erase_if(next,[](const auto& item){return item.second.id==0;});
230 }
231
232 std::map<int,Spec> desired;
233 std::optional<fonts::Settings> generatedPlayerFont;
234 std::map<int,CachedPlayerName> generatedPlayerNames;
235 auto nextHits=ruleHits_;
236 auto nextRestore=restoreRect_, nextShort=shortRect_;
237 if(target!=P::None && (!phaseChange || next.empty()))
238 {
239 const bool incomingSettled=!phaseChange && std::none_of(next.begin(),next.end(),
240 [](const auto& item){return item.second.animating;});
241 for(auto spec:detail::catalog(europe))
242 {
243 if(spec.phase!=target || !spec.idle || !enabled(spec.object,s)) continue;
244 if(spec.object>=O::PICKTOKEN_ROTATING_CANNON && spec.object<=O::PICKTOKEN_ROTATING_SHOE &&
245 !incomingSettled && !next.contains(static_cast<int>(spec.object))) continue;
246 if(europe && (spec.object==O::CITY_STATIC || spec.object==O::CITY_BUTTON_LEFT || spec.object==O::CITY_BUTTON_RIGHT)) continue;
247 if(!europe && (spec.object==O::COUNTRY_STATIC || spec.object==O::COUNTRY_BUTTON_LEFT || spec.object==O::COUNTRY_BUTTON_RIGHT || spec.object==O::CURRENCY_STATIC || spec.object==O::CURRENCY_BUTTON_LEFT || spec.object==O::CURRENCY_BUTTON_RIGHT)) continue;
248 desired.emplace(static_cast<int>(spec.object),spec);
249 }
250 const bool textNeeded=target==P::HiScore || target==P::EnterName || target==P::SelectPlayer ||
251 target==P::RemovePlayer || target==P::SelectCity || target==P::CustomizeRules;
252 if(textNeeded && (!font || !font->ready()))
253 return std::unexpected("Player selection font runtime unavailable");
254 bool cachedPlayerText=target==P::SelectPlayer && !phaseChange && textCacheFont_ &&
255 *textCacheFont_==font->settings() && textCacheResources_==playback.resources() &&
256 textCacheBoard_==s.setup.boardEdition && textCachePage_==s.setup.playerLogPageStart &&
257 textCacheCount_==s.setup.playerLogCount;
258 if(target==P::SelectPlayer)
259 {
260 std::size_t visibleNames{};
261 for(std::size_t i=0;i<8 && s.setup.playerLogPageStart+i<s.setup.playerLogCount;++i)
262 {
263 ++visibleNames;
264 const int key=220+static_cast<int>(i);
265 const auto cached=textCache_.find(key);
266 const auto surface=surfaces_.find(key);
267 const auto live=next.find(key);
268 cachedPlayerText=cachedPlayerText && cached!=textCache_.end() &&
269 cached->second.text==s.setup.playerLog[s.setup.playerLogPageStart+i] &&
270 surface!=surfaces_.end() && live!=next.end() && !live->second.leaving &&
271 live->second.id==surface->second &&
272 playback.runtimeBitmaps().asset(surface->second)==cached->second.asset;
273 const auto priority=static_cast<std::uint16_t>(1500+i);
274 desired.emplace(210+static_cast<int>(i),Spec{O{},target,0x00030026U,0x00030025U,0x00030027U,CardX[i]-358,CardY[i]-255,priority});
275 }
276 cachedPlayerText=cachedPlayerText && textCache_.size()==visibleNames;
278 for(std::size_t i=0;i<visibleNames;++i)
279 {
280 const int key=220+static_cast<int>(i);
281 desired.emplace(key,Spec{O{},target,surfaces_.at(key),0,0,CardX[i]+13,CardY[i]+16,
282 static_cast<std::uint16_t>(1501+i)});
283 }
284 }
286 {
287 if(target==P::SelectPlayer) generatedPlayerFont=font->settings();
288 if(auto r=font->saveSettings(9);!r) return std::unexpected(r.error().detail);
289 RestoreFont restore{*font};
290 auto setFont=[&](int size,int weight)->std::expected<void,std::string>
291 {
292 auto r=font->setSize(size); if(!r) return std::unexpected(r.error().detail);
293 font->setWeight(weight); return {};
294 };
295 auto draw=[&](data::LegacyBitmapRGBA8& image,const std::string& value,int x,int y,bool center,std::uint32_t color=0xFFFFFFU)->std::expected<void,std::string>
296 {
297 if(value.empty()) return {};
298 auto metrics=font->measure(value); if(!metrics) return std::unexpected(metrics.error().detail);
299 if(center){x-=metrics->width/2;y-=metrics->height/2;}
300 auto rendered=font->blitText(image,value,x,y,color);
301 if(!rendered) return std::unexpected(rendered.error().detail);
302 return {};
303 };
304 auto blank=[](int w,int h){data::LegacyBitmapRGBA8 image{static_cast<std::uint32_t>(w),static_cast<std::uint32_t>(h),{}};image.pixels.assign(static_cast<std::size_t>(w)*h*4,0);return image;};
305 auto surface=[&](int key,data::LegacyBitmapRGBA8 image,int x,int y,int priority)->std::expected<void,std::string>
306 {
307 auto found=surfaces_.find(key);
308 if(found==surfaces_.end())
309 {
310 auto id=playback.runtimeBitmaps().create(image.width,image.height,true);
311 if(!id) return std::unexpected(id.error());
312 found=surfaces_.emplace(key,*id).first;
313 }
314 const auto previous=playback.runtimeBitmaps().asset(found->second);
315 const bool changed=!previous || previous->image.pixels!=image.pixels;
316 if(changed)
317 {
318 auto r=playback.runtimeBitmaps().update(found->second,std::move(image));
319 if(!r) return r;
320 const auto live=next.find(key);
321 if(!phaseChange && live!=next.end() &&
322 live->second.id==found->second && !live->second.leaving)
323 {
324 auto redraw=playback.forceRedraw(
325 found->second,static_cast<std::uint16_t>(priority));
326 if(!redraw) return redraw;
327 }
328 }
329 desired.emplace(key,Spec{O{},target,found->second,0,0,x,y,static_cast<std::uint16_t>(priority)});
330 return {};
331 };
332 if(target==P::HiScore)
333 {
334 if(auto r=setFont(16,700);!r)return r;
335 auto image=blank(779,279);
336 for(std::size_t i=0;i<std::min<std::size_t>(8,s.highScores.size());++i)
337 {
338 const auto& entry=s.highScores[i];
339 const auto name=utf8<wchar_t>(entry.name);
340 const auto wins=std::to_string(entry.wins);
341 auto worth=money::format(entry.greatestNetWorth,s.monetarySystem,true,s.setup.boardEdition);
342 if(!worth)return std::unexpected(worth.error());
343 const int y=100+static_cast<int>(i)*21;
344 for(int column=0;column<3;++column)
345 {
346 const auto& value=column==0?name:column==1?wins:*worth;
347 auto metrics=font->measure(value);if(!metrics)return std::unexpected(metrics.error().detail);
348 const int x=column==0?125-metrics->width/2:column==1?365-metrics->width/2:660-metrics->width;
349 if(auto r=draw(image,value,x,y,false);!r)return r;
350 }
351 }
352 if(auto r=surface(202,std::move(image),9,170,1500);!r)return r;
353 }
354 if(target==P::EnterName)
355 {
356 auto image=blank(496,54);
357 if(auto r=draw(image,utf8<wchar_t>(s.setup.name),248,27,true);!r)return r;
358 if(auto r=surface(200,std::move(image),153,361,1500);!r)return r;
359 }
360 if(target==P::RemovePlayer)
361 {
362 if(auto r=setFont(16,700);!r)return r;
363 auto image=blank(600,100);
364 for(int i=0;i<2;++i)
365 {
366 auto value=text(playback,i==0?3174:3173);if(!value)return std::unexpected(value.error());
367 auto metrics=font->measure(*value);if(!metrics)return std::unexpected(metrics.error().detail);
368 if(auto r=draw(image,*value,(600-metrics->width)/2,i*60,false);!r)return r;
369 }
370 if(auto r=surface(201,std::move(image),100,281,1500);!r)return r;
371 }
372 if(target==P::SelectPlayer)
373 {
374 for(std::size_t i=0;i<8 && s.setup.playerLogPageStart+i<s.setup.playerLogCount;++i)
375 {
376 auto value=utf8<wchar_t>(s.setup.playerLog[s.setup.playerLogPageStart+i]);
377 ++playerNameRasterizations_;
378 if(auto r=setFont(8,700);!r)return r;
379 for(int size=8;size>1;--size)
380 {
381 auto metrics=font->measure(value);if(!metrics)return std::unexpected(metrics.error().detail);
382 if(metrics->width<67)break;
383 if(auto r=setFont(size-1,700);!r)return r;
384 }
385 auto image=blank(67,14);
386 if(auto r=draw(image,value,33,7,true,0);!r)return r;
387 const auto priority=static_cast<std::uint16_t>(1500+i);
388 if(incomingSettled || next.contains(220+static_cast<int>(i)))
389 {
390 if(auto r=surface(220+static_cast<int>(i),std::move(image),CardX[i]+13,CardY[i]+16,priority+1);!r)return r;
391 const int key=220+static_cast<int>(i);
392 generatedPlayerNames.emplace(key,CachedPlayerName{
393 s.setup.playerLog[s.setup.playerLogPageStart+i],
394 playback.runtimeBitmaps().asset(surfaces_.at(key))});
395 }
396 }
397 }
398 if(target==P::SelectCity)
399 {
400 constexpr std::array<const char*,11> Cities{"Classic","Atlanta","Boston","Chicago","Dallas","Los Angeles","New York","San Francisco","Seattle","Toronto","Washington"};
401 if(auto r=setFont(8,400);!r)return r;
402 for(int field=0;field<(europe?2:1);++field)
403 {
404 const int selected=field==0?s.setup.citySelected:s.setup.currencySelection[std::clamp(s.setup.currencySelectionIndex,0,2)];
405 if(selected<0 || selected>=(europe?(field==0?12:13):11))return std::unexpected("Player selection city/currency out of range");
406 auto value=europe?text(playback,static_cast<std::uint32_t>((field==0?3144:3156)+selected)):std::expected<std::string,std::string>{Cities[selected]};
407 if(!value)return std::unexpected(value.error());
408 const int width=europe?(field==0?113:155):130;
409 if(auto r=setFont(8,400);!r)return r;
410 auto metrics=font->measure(*value);if(!metrics)return std::unexpected(metrics.error().detail);
411 if(field==1 && metrics->width>=width){if(auto r=setFont(7,400);!r)return r;}
412 auto image=blank(width,13);
413 if(auto r=draw(image,*value,width/2,6,true);!r)return r;
414 if(incomingSettled || next.contains(230+field))
415 if(auto r=surface(230+field,std::move(image),europe?(field==0?168:508):335,europe?442:408,1500);!r)return r;
416 }
417 }
418 if(target==P::CustomizeRules)
419 {
420 nextHits.clear();
421 auto image=blank(793,375);
422 int x=7,y=100;
423 const auto& o=s.game.options;
424 const std::array<int,21> selected{o.housesPerHotel,o.maximumHouses,o.maximumHotels,o.freeParkingSeed,o.initialCash,o.passingGoAmount,o.taxRate,o.flatTaxFee,o.luxuryTaxAmount,o.maximumTurnsInJail,o.getOutOfJailFee,o.houseShortageLevel,o.hotelShortageLevel,o.interestRate,o.auctionGoingTimeDelay,o.dealNPropertiesAtStartup,o.evenBuildRule,o.doubleSalaryOnGo,o.freeParkingPot,o.futureRentTradingAllowed&&o.immunitiesTradingAllowed,o.dealFreePropertiesAtStartup};
425 constexpr std::array<int,16> DefaultChoice{1,3,3,2,2,2,2,2,1,2,1,2,2,2,2,0};
426 for(int row=0;row<21;++row)
427 {
428 auto label=text(playback,3119+row);if(!label)return std::unexpected(label.error());
429 if(auto r=setFont(8,400);!r)return r;
430 auto metrics=font->measure(*label);if(!metrics)return std::unexpected(metrics.error().detail);
431 const int advance=(1+metrics->width/211)*22;
432 if(auto r=setFont(8,700);!r)return r;
433 auto wrapped=detail::wrapRuleDescription(*font,*label,211,2);if(!wrapped)return std::unexpected(wrapped.error());
434 for(std::size_t line=0;line<wrapped->size();++line)
435 if(auto r=draw(image,(*wrapped)[line],x+175,y-100+static_cast<int>(line)*18,false);!r)return r;
436 const int count=row==0?2:row<16?4:1;
437 for(int choice=0;choice<count;++choice)
438 {
439 const int bx=x+(row==0?choice+2:row<16?choice:3)*43;
440 nextHits.push_back({{bx,y,bx+43,y+16},static_cast<rules::options::SetupRule>(row),static_cast<std::uint8_t>(choice)});
441 const int key=300+static_cast<int>(nextHits.size());
443 if(row<16)
446 const bool on=row<16?selectedChoice==choice:selected[row]!=0;
447 desired.emplace(key,Spec{O{},target,on?0x00030092U:0x00030091U,on?0x00030094U:0x00030093U,0,bx,y,static_cast<std::uint16_t>(1500+(europe?82:78)+nextHits.size()-1)});
448 if(row<16)
449 {
450 std::expected<std::string,std::string> value=std::to_string(Values[row][choice]);
451 if(row==3||row==4||row==5||row==7||row==8||row==10)value=money::format(Values[row][choice],s.monetarySystem,false,s.setup.boardEdition);
452 if(row==15&&choice==3)value=text(playback,3172);
453 if(!value)return std::unexpected(value.error());
454 if(auto r=draw(image,*value,bx-7+21,y-100+8,true);!r)return r;
455 }
456 }
457 y+=advance;
458 if(y>450){x=407;y=100;}
459 }
460 const int bx=540;
461 const int y1=y+(450-y-72)/3,y2=y+2*(450-y-72)/3+36;
462 nextRestore={bx,y1,bx+127,y1+36};nextShort={bx,y2,bx+127,y2+36};
463 desired[static_cast<int>(O::RULES_BUTTON_RESTORESTANDARD)].x=bx;
464 desired[static_cast<int>(O::RULES_BUTTON_RESTORESTANDARD)].y=y1;
465 desired[static_cast<int>(O::RULES_BUTTON_SHORTGAME)].x=bx;
466 desired[static_cast<int>(O::RULES_BUTTON_SHORTGAME)].y=y2;
467 if(!s.setup.serverMode)
468 {
469 auto label=text(playback,3171);if(!label)return std::unexpected(label.error());
470 if(auto r=draw(image,*label,396,360,true);!r)return r;
471 }
472 if(auto r=surface(240,std::move(image),7,100,europe?1658:1654);!r)return r;
473 }
474 }
475 for(const auto& [key,spec]:desired)
476 {
477 auto found=next.find(key);
479 const bool pressed=s.pressSerial!=pressSerial_ &&
480 ((spec.object==O::CITY_BUTTON_LEFT && s.pressedButton==B::CityLeft) ||
481 (spec.object==O::CITY_BUTTON_RIGHT && s.pressedButton==B::CityRight) ||
482 (spec.object==O::COUNTRY_BUTTON_LEFT && s.pressedButton==B::CountryLeft) ||
483 (spec.object==O::COUNTRY_BUTTON_RIGHT && s.pressedButton==B::CountryRight) ||
484 (spec.object==O::CURRENCY_BUTTON_LEFT && s.pressedButton==B::CurrencyLeft) ||
485 (spec.object==O::CURRENCY_BUTTON_RIGHT && s.pressedButton==B::CurrencyRight));
486 if(found!=next.end() && found->second.idle==spec.idle && !found->second.leaving && !pressed)continue;
487 Live object=found!=next.end()?found->second:Live{};
488 object.priority=spec.priority;object.x=spec.x;object.y=spec.y;
489 object.idle=spec.idle;object.out=spec.out;object.leaving=false;
490 object.loop=spec.object>=O::PICKTOKEN_ROTATING_CANNON && spec.object<=O::PICKTOKEN_ROTATING_SHOE;
491 object.animating=spec.in!=0;
492 if(auto r=change(object,spec.in?spec.in:spec.idle);!r)return r;
493 next[key]=object;
494 }
495 for(auto& [key,object]:next)
496 {
497 if(desired.contains(key)||object.leaving)continue;
498 if(auto r=change(object,object.out);!r)return r;
499 object.animating=object.id!=0;object.leaving=true;
500 }
501 }
502 std::erase_if(next,[](const auto& item){return item.second.id==0;});
503 if(commands.size()>sequence::SequenceCommandQueue::Capacity-playback.commands().pendingCount())
504 return std::unexpected("Sequence queue cannot fit player selection transition");
505 for(auto& command:commands)
506 if(!std::visit([&](auto item){return playback.commands().enqueue(std::move(item));},std::move(command)))
507 return std::unexpected("Validated player selection command rejected");
508 live_=std::move(next);
510 {
511 textCacheFont_=std::move(generatedPlayerFont);
512 textCache_=std::move(generatedPlayerNames);
513 textCacheResources_=playback.resources();
514 textCacheBoard_=s.setup.boardEdition;
515 textCachePage_=s.setup.playerLogPageStart;
516 textCacheCount_=s.setup.playerLogCount;
517 }
518 pressSerial_=s.pressSerial;
519 if(!phaseChange || !desired.empty() || live_.empty())
520 {
521 if (phaseChange && target != P::None) startedPhase_ = target;
522 phase_ = target;
523 }
524 if (target == P::None) startedPhase_.reset();
525 interactable_ = target != P::None && phase_ == target;
526 ready_=phase_==target && std::none_of(live_.begin(),live_.end(),[](const auto& item){return item.second.animating;});
527 ruleHits_=std::move(nextHits);restoreRect_=nextRestore;shortRect_=nextShort;
528 return {};
529 }
530
532 {
533 live_.clear();surfaces_.clear();textCache_.clear();ruleHits_.clear();
534 textCacheFont_.reset();textCacheResources_.reset();
535 textCachePage_=textCacheCount_=0;playerNameRasterizations_=0;
536 restoreRect_={};shortRect_={};phase_=P::None;ready_=false;
537 pressSerial_=0;interactable_=false;startedPhase_.reset();
538 }
539}
std::size_t count
std::size_t offset
const char * name
std::string_view text
LegacyBitmapRGBA8 image
fonts::Runtime & font
bool enabled
Definition Engine.cpp:170
data::DataId id
std::uint16_t priority
fonts::Metrics metrics
std::uint64_t key
std::uint32_t width
unsigned index
std::string_view label
std::shared_ptr< const sequence::SequenceProgram > on
std::string error
std::uint16_t value
Definition Timers.cpp:16
data::DataId out
std::expected< void, std::string > sync(const RenderState &, fonts::Runtime *, engine::SequencePlayback &)
static std::expected< std::shared_ptr< const SequenceProgram >, RuntimeError > rawBitmap(data::DataId id, data::LegacyDataType sourceType, DescriptionLimits limits={})
static std::expected< std::shared_ptr< const SequenceProgram >, RuntimeError > load(const data::DataSource &source, data::DataId id, std::size_t offset=0, DescriptionLimits limits={})
std::uint32_t DataId
Definition DataBanks.hpp:10
bool isRuntimeBitmapDataId(DataId id) noexcept
std::expected< std::string, std::string > format(std::int64_t defaultValue, int monetarySystem, bool printSymbol, data::BoardEdition edition)
std::expected< std::vector< std::string >, std::string > wrapRuleDescription(fonts::Runtime &font, std::string_view value, int maxPixelWidth, std::size_t maxLines)
std::vector< ObjectSpec > catalog(bool europe)
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
SequenceTransform moveXYTransform(std::int32_t x, std::int32_t y) noexcept
std::shared_ptr< const data::ResourceSnapshot > resources() noexcept
bool tokenAvailable(const GameState &uiState, std::uint8_t token)
std::vector< std::uint8_t > pixels