Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
ModernMenuSkin.cpp
Go to the documentation of this file.
1#include "ModernMenuSkin.hpp"
3#include <algorithm>
4#include <array>
5#include <optional>
6
7namespace monopoly::menu
8{
9 namespace
10 {
11 enum class Kind { Background, Pattern, Button, Title, Slot, SelectedSlot, Tab, SelectedTab, Toggle, SelectedToggle, PlayerCard, Confirmation, AuctionBackground, AuctionBottom, AuctionPlayer, AuctionBid, TokenPreview, TokenThumbnail, TokenFrame, TradeBackground, TradePanel, TradeOffer, TradeRail, TradeTitle, TradeButton, StatsPanel, CalculatorPanel, CalculatorDescription, StatsBackground, StatsBar, StatsBarHeading, StatsTab, StatsSelectedTab, CalculatorKey, CalculatorSelectedKey, BankSummary, DeedsFrame, CalculatorFunction, CalculatorSelectedFunction, PortfolioFrame, PortfolioAmount, CitySelector, TradeCashPanel, TradeCashButton, TradeCashPressed };
12 struct Descriptor { Kind kind; std::string_view label; int colour{-1}; int token{-1}; };
13 // StatsCalculatorLogic function order; exact Main DAT idle leaves, pressed leaf+1.
14 constexpr std::array<data::DataTag,8> CalculatorFunctionLeaves{
15 0x0347,0x0331,0x0327,0x0325,0x0329,0x032D,0x032B,0x032F};
16 constexpr std::array<std::string_view,8> CalculatorFunctionLabels{
17 "Odds","Net\nworth","Future\nto you","Future\nto other","Maximum\nexpense",
18 "Current\nincome","Maximum\nincome","Potential\nincome"};
19 // Canonical six identities, matching BoardLightingController and ModernIBarSkin.
20 constexpr std::array<std::array<unsigned, 3>, 6> PlayerColours{{
21 {255,0,0}, {0,0,255}, {60,150,60}, {255,255,0}, {255,0,255}, {255,128,0}}};
22 constexpr std::array<data::DataTag,11> TokenThumbnailFirstTags{
23 0x004A,0x0059,0x003B,0x003E,0x0056,0x0053,0x004D,0x0041,0x0047,0x0050,0x0044};
24 struct StatsControl { data::DataTag first, leaf; unsigned width, height; std::string_view label; };
25 constexpr std::array<StatsControl, 7> StatsControls{{
26 {0x019E,0x0E6C,114,43,"Players"}, {0x017F,0x0900,106,43,"Deeds"},
27 {0x0176,0x02F6,95,43,"Bank"}, {0x01A4,0x11BC,76,43,"Turn"},
28 {0x0198,0x0E3E,100,42,"Net worth"}, {0x0195,0x092E,105,42,"Future value"},
29 {0x017C,0x0308,107,42,"Cash"}
30 }};
31 struct StatsViewControl { std::array<data::DataTag,3> roots; data::DataTag leaf; unsigned width, height; std::string_view label; };
32 // Explicit idle/return/press roots: Bank's return tags precede its idle tags.
33 constexpr std::array<StatsViewControl,8> StatsViewControls{{
34 {{{0x00FE,0x00FD,0x00FF}},0x0C94,76,43,"houses hotels"},
35 {{{0x0104,0x0103,0x0105}},0x0CAE,100,42,"properties"},
36 {{{0x0101,0x0100,0x0102}},0x0CA1,105,42,"liabilities"},
37 {{{0x00FB,0x00FA,0x00FC}},0x0C87,107,42,"turn history"},
38 {{{0x01A1,0x01A3,0x01A2}},0x0E9B,76,43,"price"},
39 {{{0x019B,0x019D,0x019C}},0x0E5E,100,42,"owner"},
40 {{{0x0179,0x017B,0x017A}},0x07FC,105,42,"current rent"},
41 {{{0x0182,0x0184,0x0183}},0x0917,107,42,"game earnings"}
42 }};
43 struct StatsTabFrame { data::DataTag root, leaf; unsigned width, height; };
44 // Exact DAT-decoded variable footprints; no extension by nearby tag or idle size.
45 constexpr std::array<StatsTabFrame, 41> StatsAnimatedFrames{{
46 {0x01A0,0x124E,126,43},{0x01A0,0x124F,126,43},{0x01A0,0x1250,126,43},
47 {0x01A0,0x1251,126,43},{0x01A0,0x1252,126,43},{0x01A0,0x1253,126,43},
48 {0x01A0,0x1254,125,43},{0x01A0,0x1255,122,43},{0x01A0,0x1256,126,43},
49 {0x01A0,0x1257,130,43},{0x01A0,0x1258,133,78},{0x01A0,0x1259,133,78},
50 {0x01A6,0x1266,76,43},{0x01A6,0x1267,76,43},{0x01A6,0x1268,76,43},
51 {0x01A6,0x1269,76,43},{0x01A6,0x126A,76,43},{0x01A6,0x126B,76,43},
52 {0x01A6,0x126C,76,43},{0x01A6,0x126D,75,43},{0x01A6,0x126E,76,43},
53 {0x01A6,0x126F,78,43},{0x01A6,0x1270,83,43},{0x01A6,0x1271,83,43},
54 {0x01A5,0x11BD,76,43},{0x01A5,0x11BE,76,43},{0x01A5,0x11BF,76,43},
55 {0x01A5,0x11C0,76,43},{0x01A5,0x11C1,76,43},{0x01A5,0x11C2,76,43},
56 {0x01A5,0x11C3,75,43},{0x01A5,0x11C4,76,43},{0x01A5,0x11C5,78,43},
57 {0x01A5,0x11C6,83,43},{0x01A5,0x11C7,83,43},{0x01A5,0x11C8,72,41},
58 {0x01A5,0x11C9,74,43},{0x01A5,0x11CA,72,41},{0x01A5,0x11CB,72,41},
59 {0x01A5,0x11CC,72,41},{0x019F,0x0E77,119,52}
60 }};
61 constexpr std::array<StatsTabFrame, 5> StatsReturnEndFrames{{
62 {0x0181,0x1212,122,43},{0x0178,0x11EE,102,43},{0x019A,0x1236,100,42},
63 {0x0197,0x122A,105,42},{0x017E,0x11FA,107,42}
64 }};
65 // Measured first/last authored leaves: selected CNK branches can hold either.
66 // Intermediate frames require their own measured footprint before qualification.
67 constexpr std::array<StatsTabFrame, 33> StatsViewBoundaryFrames{{
68 {0x00FF,0x0EA7,72,43},{0x00FF,0x0CA0,83,43},
69 {0x0105,0x0E4A,72,42},{0x0105,0x0CBA,104,42},
70 {0x0102,0x0808,73,42},{0x0102,0x0CAD,110,50},
71 {0x00FC,0x080D,74,41},{0x00FC,0x0C93,116,50},
72 {0x01A2,0x0EA7,72,43},{0x01A2,0x0EA6,83,43},
73 {0x019C,0x0E59,71,41},{0x019C,0x0E69,104,42},
74 {0x017A,0x0808,73,42},{0x017A,0x0807,110,50},
75 {0x0183,0x0923,74,41},{0x0183,0x0922,116,50},
76 {0x00FD,0x0C9E,83,43},{0x0103,0x0CB8,104,42},
77 {0x0100,0x0CAB,110,50},{0x00FA,0x0C91,116,50},
78 {0x01A3,0x1265,83,43},{0x01A3,0x125A,76,43},
79 {0x019D,0x124D,104,42},{0x019D,0x1242,100,42},
80 {0x017B,0x1211,110,50},{0x017B,0x1206,105,42},
81 {0x0184,0x1229,116,50},{0x0184,0x121E,108,42},
82 {0x0177,0x0303,87,34},{0x0177,0x0301,101,51},
83 {0x0180,0x08FB,88,33},{0x0180,0x090B,111,52},{0x0181,0x121D,129,79}
84 }};
85 std::optional<Descriptor> describe(data::DataId root) noexcept
86 {
87 if (root >= 0x0003002D && root <= 0x00030037)
88 return Descriptor{Kind::TokenPreview, {}, -1, int(root - 0x0003002D)};
89 if (root >= 0x00030038 && root <= 0x0003003A) return Descriptor{Kind::TokenFrame, {}};
91 for (unsigned token=0;token<TokenThumbnailFirstTags.size();++token)
93 return Descriptor{Kind::TokenThumbnail, {}, -1, int(token)};
94 if (root >= 0x0002000B && root <= 0x0002000E) return Descriptor{Kind::BankSummary,{}};
95 if (root == 0x000200CD) return Descriptor{Kind::DeedsFrame,{}};
96 if (root == 0x000200C6) return Descriptor{Kind::PortfolioFrame,{}};
97 if (root == 0x000200D0) return Descriptor{Kind::PortfolioAmount,{}};
98 if (root >= 0x0002006D && root <= 0x00020074)
99 return Descriptor{Kind::CalculatorFunction,CalculatorFunctionLabels[root-0x0002006D]};
100 if (root >= 0x00020075 && root <= 0x0002007C)
101 return Descriptor{Kind::CalculatorSelectedFunction,CalculatorFunctionLabels[root-0x00020075]};
102 constexpr std::array<std::string_view,10> digits{"1","2","3","4","5","6","7","8","9","0"};
103 if (root >= 0x0002007D && root <= 0x00020086)
104 return Descriptor{Kind::CalculatorKey,digits[root-0x0002007D]};
105 if (root >= 0x00020087 && root <= 0x00020090)
106 return Descriptor{Kind::CalculatorSelectedKey,digits[root-0x00020087]};
107 // Measured direct Main bitmaps; player text/deeds remain separate owners.
108 if (root >= 0x00020349 && root <= 0x0002034E)
109 return Descriptor{Kind::StatsPanel, {}, int(root - 0x00020349)};
110 if (root >= 0x0002034F && root <= 0x00020354)
111 return Descriptor{Kind::StatsPanel, {}, int(root - 0x0002034F)};
112 if (root == 0x0002006A) return Descriptor{Kind::CalculatorPanel, {}};
113 if (root == 0x00020091) return Descriptor{Kind::CalculatorDescription, {}};
114 if (root == 0x00050089) return Descriptor{Kind::StatsBackground, {}};
115 // Only the measured static background qualifies; its real floor survives.
116 if (root == 0x00030000) return Descriptor{Kind::AuctionBackground,{}};
117 if (root == 0x0003036F) return Descriptor{Kind::AuctionBottom, {}};
118 if (root >= 0x00030370 && root <= 0x0003037B)
119 return Descriptor{Kind::AuctionPlayer, {}, int((root - 0x00030370) % 6)};
120 if (root == 0x00030384) return Descriptor{Kind::AuctionBid, {}};
121 // PlayerSelectionPlayback publishes names as separate native roots.
122 if (root >= 0x00030025 && root <= 0x00030027) return Descriptor{Kind::PlayerCard, {}};
123 // BoardBackdropPlayback's exact static bitmap owners.
124 if (root == 0x00050003) return Descriptor{Kind::Background, "MONOPOLY"};
125 if (root == 0x00030002) return Descriptor{Kind::Pattern, {}};
126 // OptionsSavePlayback's shells; runtime slot text is a separate root.
127 if (root == 0x0002006B) return Descriptor{Kind::Slot, {}};
128 if (root == 0x0002006C) return Descriptor{Kind::SelectedSlot, {}};
130 return {};
131 const auto tag = data::dataTag(root);
132 if (tag == 0x118A) return Descriptor{Kind::TradeCashPanel,{}};
133 if (tag >= 0x02D9 && tag <= 0x02DE)
134 {
135 constexpr std::array<std::string_view,3> labels{"Clear","Okay","Cancel"};
136 return Descriptor{(tag-0x02D9)%2 ? Kind::TradeCashPressed : Kind::TradeCashButton,
137 labels[(tag-0x02D9)/2]};
138 }
139 if (tag == 0x01FE) return Descriptor{Kind::CalculatorKey,"CLEAR"};
140 if (tag == 0x029B || tag == 0x0286 || tag == 0x022C || tag == 0x0006) return Descriptor{Kind::StatsBar,{}};
141 if (tag == 0x029C) return Descriptor{Kind::StatsBarHeading,"Display status by:"};
142 if (tag == 0x0287) return Descriptor{Kind::StatsBarHeading,"Sort Players by:"};
143 if (tag == 0x022D) return Descriptor{Kind::StatsBarHeading,"Sort deeds by:"};
144 if (tag == 0x0007) return Descriptor{Kind::StatsBarHeading,"Show Summary of:"};
145 for (const auto& control : StatsViewControls)
146 for (const auto owner : control.roots)
147 if (tag == owner)
148 return Descriptor{tag == control.roots[2] ? Kind::StatsSelectedTab : Kind::StatsTab, control.label};
149 for (const auto& control : StatsControls)
150 if (tag >= control.first && tag <= control.first + 2)
151 return Descriptor{tag == control.first + 1 ? Kind::StatsSelectedTab : Kind::StatsTab, control.label};
152 if (tag == 0x02CD) return Descriptor{Kind::TradeBackground,{}};
153 if (tag == 0x02BB) return Descriptor{Kind::TradePanel,{}};
154 if (tag == 0x02CF || tag == 0x02D0) return Descriptor{Kind::TradeOffer,{}};
155 if (tag >= 0x02AD && tag <= 0x02BA) return Descriptor{Kind::TradeRail,{},int((tag-0x02AD)%7)};
156 if (tag == 0x02DF) return Descriptor{Kind::TradeTitle,"Trade this for this"};
157 if (tag == 0x01AF || tag == 0x01B1) return Descriptor{Kind::TradeButton,"Cancel"};
158 if (tag == 0x01B8 || tag == 0x01BA) return Descriptor{Kind::TradeButton,"Propose"};
159 // EscapeConfirmationPlayback places separate Yes/No owners in the lower half.
160 if (tag == 0x02F4) return Descriptor{Kind::Confirmation, "Are you sure?"};
161 if (tag == 0x16BA) return Descriptor{Kind::Button, "Yes"};
162 if (tag == 0x0E4F) return Descriptor{Kind::Button, "No"};
163 struct Navigation { data::DataTag first; std::string_view label; };
164 constexpr std::array<Navigation, 4> navigation{{
165 {0x018E, "Files"}, {0x0192, "Options"}, {0x0186, "Credits"}, {0x018A, "Help"}}};
166 for (const auto& item : navigation)
167 if (tag >= item.first && tag <= item.first + 2)
168 return Descriptor{tag == item.first + 2 ? Kind::SelectedTab : Kind::Tab, item.label};
169
170 // Exact USA player-setup owners from PlayerSelectionCatalog.
171 struct Button { data::DataTag first; std::string_view label; };
172 constexpr std::array<Button, 24> buttons{{
173 {0x0279, "Local game"}, {0x0283, "Network game"},
174 {0x0256, "Load saved game"}, {0x027E, "Play"},
175 // OptionsFilePlayback/UDOpts: out, in, idle triples.
176 {0x0240, "New game"}, {0x023D, "Load game"},
177 {0x0243, "Save game"}, {0x0237, "Exit game"}, {0x0234, "Cancel"},
178 {0x025F, "New player"}, {0x0259, "More names"}, {0x025C, "Next"},
179 {0x0220, "Add human player"}, {0x021D, "Add computer player"},
180 {0x028A, "Remove player"}, {0x029D, "Start game"},
181 {0x0246, "First time buyer"}, {0x0231, "Entrepreneur"}, {0x02A8, "Tycoon"},
182 {0x0223, "Classic board"}, {0x0252, "Load board"},
183 {0x02A1, "Standard rules"}, {0x0229, "Custom rules"}, {0x026D, "OK"}
184 }};
185 for (const auto& button : buttons)
186 if (tag >= button.first && tag <= button.first + 2)
187 return Descriptor{Kind::Button, button.label};
188 if (tag >= 0x0214 && tag <= 0x0216) return Descriptor{Kind::CitySelector, "Select city"};
189 // Name entry is a shell; typed text belongs to a separate native surface.
190 if (tag >= 0x022E && tag <= 0x0230) return Descriptor{Kind::Slot, {}};
191 if (tag == 0x026B || tag == 0x026C)
192 return Descriptor{tag == 0x026C ? Kind::SelectedToggle : Kind::Toggle, "Off"};
193 if (tag == 0x0272 || tag == 0x0273)
194 return Descriptor{tag == 0x0273 ? Kind::SelectedToggle : Kind::Toggle, "On"};
195 struct Title { data::DataTag tag; std::string_view label; };
196 constexpr std::array<Title, 8> titles{{{0x0288, "Select player"}, {0x027D, "Enter name"},
197 {0x0289, "Choose token"}, {0x027C, "Computer difficulty"}, {0x0293, "Game rules"},
198 {0x0277, "Options"}, {0x0278, "Sound"}, {0x0262, "Display"}}};
199 for (const auto& title : titles)
200 if (tag == title.tag) return Descriptor{Kind::Title, title.label};
201 if (tag == 0x0249) return Descriptor{Kind::Title, "Help menu"};
202 if (tag >= 0x024A && tag <= 0x024C) return Descriptor{Kind::Button, "Full help\n(game will pause)"};
203 if (tag >= 0x024F && tag <= 0x0251) return Descriptor{Kind::Button, "Quick help"};
204 if (tag == 0x023A) return Descriptor{Kind::Title, "File"};
205 if (tag == 0x0255) return Descriptor{Kind::Title, "Load game"};
206 if (tag == 0x02A4) return Descriptor{Kind::Title, "Save game"};
207 return {};
208 }
209 bool compactCaption(data::LegacyBitmapRGBA8& target, const data::LegacyBitmapRGBA8& text, bool heading, bool calculator)
210 {
211 if (!text.width || !text.height || text.width > 4096 || text.height > 512 ||
212 text.pixels.size() != std::size_t(text.width) * text.height * 4 ||
213 target.width <= 36 || target.height < (heading ? 105U : 36U)) return false;
214 // Capture-measured header strip is native y24..35 inside the overlay.
215 // Explicit logical caps are independent of the high-resolution font provider.
216 const unsigned boxHeight = heading || calculator ? 33 : 30;
217 const double scale = std::min(double(target.width - 36) / text.width, double(boxHeight) / text.height);
218 const unsigned width = std::max(1U, unsigned(text.width * scale));
219 const unsigned height = std::max(1U, unsigned(text.height * scale));
220 const unsigned left = (target.width - width) / 2;
221 const unsigned top = heading ? 72 + (33 - height) / 2 : (target.height - height) / 2;
222 for (unsigned y = 0; y < height; ++y)
223 for (unsigned x = 0; x < width; ++x)
224 {
225 const auto src = (std::size_t(y * text.height / height) * text.width + x * text.width / width) * 4;
226 const auto dst = (std::size_t(y + top) * target.width + x + left) * 4;
227 const unsigned alpha = text.pixels[src + 3];
228 constexpr std::array<unsigned, 3> ink{245,235,211};
229 for (unsigned c = 0; c < 3; ++c)
230 target.pixels[dst + c] = std::uint8_t((ink[c] * alpha +
231 target.pixels[dst + c] * (255 - alpha) + 127) / 255);
232 }
233 return true;
234 }
235 bool functionCaption(data::LegacyBitmapRGBA8& target,
236 const std::array<data::LegacyBitmapRGBA8,2>& lines, unsigned count, unsigned lineHeight = 24)
237 {
238 // Default calculator cap is8 logical pixels; Help uses18. Keep a two-pixel line gap.
239 std::array<unsigned,2> widths{},heights{};
240 unsigned totalHeight = count == 2 ? 6 : 0;
241 for(unsigned i=0;i<count;++i)
242 {
243 const auto& text=lines[i];
244 if(!text.width || !text.height || text.width>4096 || text.height>512 ||
245 text.pixels.size()!=std::size_t(text.width)*text.height*4)return false;
246 const double scale=std::min(double(target.width-18)/text.width,double(lineHeight)/text.height);
247 widths[i]=std::max(1U,unsigned(text.width*scale));
248 heights[i]=std::max(1U,unsigned(text.height*scale));totalHeight+=heights[i];
249 }
250 unsigned top=(target.height-totalHeight)/2;
251 for(unsigned i=0;i<count;++i)
252 {
253 const auto& text=lines[i];const unsigned left=(target.width-widths[i])/2;
254 for(unsigned y=0;y<heights[i];++y)for(unsigned x=0;x<widths[i];++x)
255 {
256 const auto src=(std::size_t(y*text.height/heights[i])*text.width+x*text.width/widths[i])*4;
257 const auto dst=(std::size_t(y+top)*target.width+x+left)*4;
258 const unsigned alpha=text.pixels[src+3];
259 constexpr std::array<unsigned,3> ink{245,235,211};
260 for(unsigned c=0;c<3;++c)target.pixels[dst+c]=std::uint8_t(
261 (ink[c]*alpha+target.pixels[dst+c]*(255-alpha)+127)/255);
262 }
263 top+=heights[i]+6;
264 }
265 return true;
266 }
267 bool caption(data::LegacyBitmapRGBA8& target, const data::LegacyBitmapRGBA8& text,
268 bool heading, bool upperPanel = false, unsigned panelHeight = 0, unsigned panelTop = 0,
269 unsigned verticalPadding = 24, std::array<unsigned, 3> ink = {245, 235, 211})
270 {
271 if (!text.width || !text.height || text.width > 4096 || text.height > 512 ||
272 text.pixels.size() != std::size_t(text.width) * text.height * 4 ||
273 target.width < 48 || target.height < 36) return false;
274 const unsigned availableHeight = panelHeight ? panelHeight : target.height;
275 const float scale = std::min({heading ? 2.0F : 1.0F,
276 float(target.width - 48) / text.width,
278 const auto w = std::max(1U, unsigned(text.width * scale));
279 const auto h = std::max(1U, unsigned(text.height * scale));
280 const auto ox = (target.width - w) / 2;
281 const auto oy = panelTop + (heading ? availableHeight / 8 : upperPanel ? availableHeight / 4 : (availableHeight - h) / 2);
282 for (unsigned y = 0; y < h; ++y)
283 for (unsigned x = 0; x < w; ++x)
284 {
285 const auto src = (std::size_t(y * text.height / h) * text.width + x * text.width / w) * 4;
286 const auto dst = (std::size_t(y + oy) * target.width + x + ox) * 4;
287 const auto alpha = unsigned(text.pixels[src + 3]);
288 for (unsigned c = 0; c < 3; ++c)
289 target.pixels[dst + c] = std::uint8_t((ink[c] * alpha +
290 target.pixels[dst + c] * (255 - alpha) + 127) / 255);
291 }
292 return true;
293 }
294 }
295
296 void ModernMenuSkin::configureBackground(std::shared_ptr<const data::LegacyBitmapRGBA8> image)
297 {
298 if (image && (!image->width || !image->height || image->width > 4096 || image->height > 4096 ||
299 image->pixels.size() != std::size_t(image->width) * image->height * 4)) image.reset();
300 if (background_ == image) return;
301 background_ = std::move(image);
302 cache_.clear(); cachedBytes_ = 0;
303 }
304
306 {
307 tokenImages_ = std::move(provider);
308 cache_.clear(); cachedBytes_ = 0;
309 }
310
312 {
313 tradeCashPresentation_ = std::move(qualified);
314 cache_.clear(); cachedBytes_ = 0;
315 }
316
318 {
319 const auto descriptor = describe(root);
320 if (edition_ != data::BoardEdition::Usa || language_ != data::LanguageId::EnglishUs || !descriptor) return false;
321 const bool token = descriptor->kind == Kind::TokenPreview || descriptor->kind == Kind::TokenThumbnail ||
322 descriptor->kind == Kind::TokenFrame;
323 return token ? bool(tokenImages_) : bool(text_);
324 }
325
326 std::shared_ptr<const data::BitmapRuntimeAsset> ModernMenuSkin::substitute(
327 data::DataId root, std::shared_ptr<const data::BitmapRuntimeAsset> original, bool principal)
328 {
329 if (!original || !supports(root)) return original;
330 const auto descriptor = *describe(root);
331 const bool selectedToggle = descriptor.kind == Kind::SelectedToggle;
332 if (selectedToggle)
333 {
334 if (!principal || original->sourceType != data::LegacyDataType::Uap || !original->source) return original;
335 const auto metadata = data::inspectLegacyUap(*original->source);
336 const auto leaf = original->dataId;
337 const bool measured = root == 0x0005026C ?
338 leaf == 0x00051062 || leaf == 0x00051064 || leaf == 0x00051066 || leaf == 0x00051067 :
339 leaf == 0x0005106D || leaf == 0x0005106F || leaf == 0x00051071 || leaf == 0x00051072;
340 if (!measured || !metadata || metadata->width != 59 || metadata->height != 33 ||
341 metadata->originX != 0 || metadata->originY != 0 || metadata->flags != 6 ||
342 original->image.width != 59 || original->image.height != 33) return original;
343 }
344 const bool auctionBackdrop = descriptor.kind == Kind::AuctionBackground;
345 if (auctionBackdrop)
346 {
347 if (!principal || !tradeCashPresentation_ || !tradeCashPresentation_() ||
348 original->dataId != root || original->sourceType != data::LegacyDataType::Bitmap ||
349 !original->source || original->image.width != 800 || original->image.height != 450) return original;
350 const auto metadata=data::inspectLegacyBitmap(*original->source);
351 if (!metadata || metadata->width != 800 || metadata->height != 450 ||
352 metadata->bitsPerPixel != 24 || metadata->compression != 0) return original;
353 }
354 const bool tradeCash = descriptor.kind == Kind::TradeCashPanel ||
355 descriptor.kind == Kind::TradeCashButton || descriptor.kind == Kind::TradeCashPressed;
356 if (tradeCash)
357 {
358 if (!principal || !tradeCashPresentation_ || !tradeCashPresentation_() ||
359 original->sourceType != data::LegacyDataType::Uap || !original->source) return original;
360 const auto metadata = data::inspectLegacyUap(*original->source);
361 if (!metadata || metadata->flags != 6) return original;
362 unsigned leaf=0x118A,width=191,height=70,x=307,y=352;
363 if (descriptor.kind != Kind::TradeCashPanel)
364 {
365 const unsigned control=(data::dataTag(root)-0x02D9)/2;
366 constexpr std::array<unsigned,3> first{0x1160,0x1175,0x118B};
367 constexpr std::array<unsigned,3> widths{58,59,58}, origins{374,312,435};
368 const auto actual=data::dataTag(original->dataId);
369 if (actual < first[control] || actual > first[control]+15 ||
370 (descriptor.kind == Kind::TradeCashButton && actual != first[control])) return original;
371 const unsigned pose=actual-first[control];
373 // Exact16 DAT principal poses. Five local-origin glow siblings stay retail.
374 if (control<2 && pose>=6 && pose<=11) { --width; if(control==1) ++x; }
375 if (pose>=7 && pose<=10) { height=16; y=396; }
376 }
378 original->image.width != width || original->image.height != height ||
379 metadata->width != width || metadata->height != height ||
380 metadata->originX != int(x) || metadata->originY != int(y)) return original;
381 }
382 // Retail card hit width is97, but incoming/outgoing UAP poses426..42A
383 // are96x110. Qualify only those measured leaves; idle425 stays unchanged.
384 bool playerCardTransition = false;
385 if (descriptor.kind == Kind::PlayerCard &&
386 (root == 0x00030025 || root == 0x00030027) &&
387 original->dataId >= 0x00030426 && original->dataId <= 0x0003042A &&
388 original->sourceType == data::LegacyDataType::Uap && original->source &&
389 original->image.width == 96 && original->image.height == 110)
390 {
391 const auto metadata = data::inspectLegacyUap(*original->source);
392 playerCardTransition = metadata && metadata->width == 96 && metadata->height == 110 &&
393 metadata->originX == 26 && metadata->originY == 25;
394 }
395 if (descriptor.kind == Kind::PlayerCard &&
397 (original->image.width != 97 || original->image.height != 110)))) return original;
398 const auto& source = original->image;
399 const bool citySelector = descriptor.kind == Kind::CitySelector;
400 if (citySelector)
401 {
402 // Production USA CITY_STATIC: in/out F03..F05; settled idle F03 only.
403 // Dynamic city text and both arrow owners are separate runtime surfaces.
404 const bool leaf = original->dataId >= 0x00050F03 && original->dataId <= 0x00050F05 &&
405 (root != 0x00050215 || original->dataId == 0x00050F03);
406 if (!principal || !leaf || original->sourceType != data::LegacyDataType::Uap ||
407 !original->source || source.width != 210 || source.height != 133) return original;
408 const auto metadata = data::inspectLegacyUap(*original->source);
409 if (!metadata || metadata->width != 210 || metadata->height != 133 ||
410 metadata->originX != 0 || metadata->originY != 0) return original;
411 }
412 // Options authored box y30..108 overlaps subtitles starting y83/85.
413 // Keep its full footprint, but restrict the opaque panel to y30..75.
414 const bool optionsHeader = root == 0x00050277;
415 const auto helpTag = data::dataTag(root);
416 const bool helpChrome = root == 0x00050249 ||
418 ((helpTag >= 0x024A && helpTag <= 0x024C) || (helpTag >= 0x024F && helpTag <= 0x0251)));
419 if (optionsHeader && (!principal || original->dataId != 0x0005108C || source.width != 619 || source.height != 78))
420 return original;
421 if (helpChrome)
422 {
423 const auto leaf = data::dataTag(original->dataId);
424 const bool title = helpTag == 0x0249;
425 // UDOpts: state0 is out,1 in,2 idle; both transitions use eight measured leaves.
426 const bool full = helpTag >= 0x024A && helpTag <= 0x024C;
427 const bool qualifiedLeaf = title ? leaf == 0x1028 :
428 full ? (helpTag == 0x024C ? leaf == 0x1034 : leaf >= 0x1034 && leaf <= 0x103B) :
429 (helpTag == 0x0251 ? leaf == 0x104A : leaf >= 0x104A && leaf <= 0x1051);
431 !qualifiedLeaf || source.width != (title ? 619U : 220U) || source.height != (title ? 78U : 62U))
432 return original;
433 }
434 const bool trade = descriptor.kind == Kind::TradeBackground || descriptor.kind == Kind::TradePanel ||
435 descriptor.kind == Kind::TradeOffer || descriptor.kind == Kind::TradeRail ||
436 descriptor.kind == Kind::TradeTitle || descriptor.kind == Kind::TradeButton;
437 if (trade)
438 {
439 const auto tag=data::dataTag(root);
441 if(descriptor.kind==Kind::TradeBackground)
442 {
443 const auto contents=data::dataTag(original->dataId);
444 if(contents!=0x11B2 && contents!=0x11B3 && contents!=0x11B4)return original;
446 }
447 else
448 {
449 if(!principal)return original;
450 if(descriptor.kind==Kind::TradePanel){expectedTag=0x1159;expectedWidth=200;expectedHeight=225;}
451 if(descriptor.kind==Kind::TradeOffer)
452 {expectedTag=tag==0x02CF?0x115A:0x115B;expectedWidth=tag==0x02CF?194:197;expectedHeight=tag==0x02CF?218:223;}
453 if(descriptor.kind==Kind::TradeRail){expectedTag=0x114A+tag-0x02AD;expectedWidth=397;expectedHeight=222;}
454 if(descriptor.kind==Kind::TradeTitle){expectedTag=0x11EC;expectedWidth=390;expectedHeight=36;}
455 if(descriptor.kind==Kind::TradeButton){expectedTag=tag==0x01AF||tag==0x01B1?0x11DC:0x11E0;expectedWidth=101;expectedHeight=29;}
456 }
458 source.width!=expectedWidth || source.height!=expectedHeight)return original;
459 }
460 const bool statsPanel = descriptor.kind == Kind::StatsPanel || descriptor.kind == Kind::CalculatorPanel ||
461 descriptor.kind == Kind::CalculatorDescription || descriptor.kind == Kind::StatsBackground ||
462 descriptor.kind == Kind::BankSummary || descriptor.kind == Kind::DeedsFrame ||
463 descriptor.kind == Kind::PortfolioFrame || descriptor.kind == Kind::PortfolioAmount;
464 if (statsPanel)
465 {
466 data::DataId expected = root;
467 unsigned width = 0, height = 0;
468 if (descriptor.kind == Kind::StatsBackground)
469 {
470 // Only measured leaves qualify, including their authored secondary instances.
471 // Other decorations and the separate board render slot stay untouched.
472 if (original->dataId == 0x00050914) { width = 800; height = 225; }
473 else if (original->dataId == 0x00050913) { width = 400; height = 225; }
474 else if (original->dataId == 0x00050916) { width = 399; height = 3; }
475 else return original;
476 expected = original->dataId;
477 }
478 else
479 {
480 if (!principal) return original;
481 if (descriptor.kind == Kind::BankSummary)
482 { constexpr std::array<data::DataId,4> leaves{0x0002009E,0x0002009F,0x000200A0,0x000200A2};
483 expected = leaves[root-0x0002000B]; width = 786; height = 223; }
484 else if (descriptor.kind == Kind::DeedsFrame)
485 { width = 790; height = 215; }
486 else if (descriptor.kind == Kind::PortfolioFrame)
487 { width = 400; height = 235; }
488 else if (descriptor.kind == Kind::PortfolioAmount)
489 { width = 52; height = 13; }
490 else if (descriptor.kind == Kind::StatsPanel)
491 { const bool large = root <= 0x0002034E; width = large ? 198 : 130; height = large ? 222 : 226; }
492 else if (descriptor.kind == Kind::CalculatorPanel)
493 { expected = 0x00020324; width = 199; height = 208; }
494 else { expected = 0x0002035A; width = 188; height = 209; }
495 }
496 if (original->dataId != expected || source.width != width || source.height != height) return original;
497 }
498 const bool statsControl = descriptor.kind == Kind::StatsBar || descriptor.kind == Kind::StatsBarHeading ||
499 descriptor.kind == Kind::StatsTab || descriptor.kind == Kind::StatsSelectedTab;
500 if (statsControl)
501 {
502 if (!principal) return original;
503 unsigned leaf = 0, width = 0, height = 0;
504 const auto tag = data::dataTag(root);
505 if (descriptor.kind == Kind::StatsTab || descriptor.kind == Kind::StatsSelectedTab)
506 {
507 for (const auto& control : StatsControls)
508 if (tag >= control.first && tag <= control.first + 2)
509 { leaf = control.leaf; width = control.width; height = control.height; break; }
510 for (const auto& control : StatsViewControls)
511 for (const auto owner : control.roots)
512 if (tag == owner)
513 { leaf = control.leaf; width = control.width; height = control.height; }
514 const auto actualLeaf = data::dataTag(original->dataId);
515 const auto acceptMeasured = [&](const auto& frames) {
516 for (const auto& frame : frames)
517 if (tag == frame.root && actualLeaf == frame.leaf)
518 { leaf = frame.leaf; width = frame.width; height = frame.height; }
519 };
523 // Unmeasured animation leaves keep their original pixels and placement.
524 }
525 else if (tag == 0x029B) { leaf = 0x0912; width = 328; height = 42; }
526 else if (tag == 0x029C) { leaf = 0x0911; width = 337; height = 80; }
527 else if (tag == 0x0286 || tag == 0x022C || tag == 0x0006) { leaf = 0x0CBB; width = 313; height = 42; }
528 else if (tag == 0x0287) { leaf = 0x0E7E; width = 333; height = 80; }
529 else if (tag == 0x022D) { leaf = 0x090F; width = 333; height = 80; }
530 else { leaf = 0x0CBC; width = 334; height = 80; }
532 source.width != width || source.height != height) return original;
533 }
534 const bool calculatorFunction = descriptor.kind == Kind::CalculatorFunction || descriptor.kind == Kind::CalculatorSelectedFunction;
535 const bool calculatorKey = descriptor.kind == Kind::CalculatorKey || descriptor.kind == Kind::CalculatorSelectedKey || calculatorFunction;
536 if (calculatorKey)
537 {
538 if (!principal) return original;
539 data::DataId expected{};
540 unsigned width = 24, height = 25;
542 {
543 const bool pressed=descriptor.kind==Kind::CalculatorSelectedFunction;
544 expected=0x00020000+CalculatorFunctionLeaves[root-(pressed?0x00020075:0x0002006D)]+unsigned(pressed);
545 width=47;height=29;
546 }
547 else if (root == 0x000501FE) { expected = 0x00050F14; width = 50; }
548 // Bitmap banks store zero first; CNK button roots run1..9 then0.
549 else if (descriptor.kind == Kind::CalculatorSelectedKey) expected = 0x0002033D + (root - 0x00020087 + 1) % 10;
550 else expected = 0x00020333 + (root - 0x0002007D + 1) % 10;
551 if (original->dataId != expected || source.width != width || source.height != height) return original;
552 }
553 const bool tokenOwner = descriptor.kind == Kind::TokenPreview || descriptor.kind == Kind::TokenThumbnail ||
554 descriptor.kind == Kind::TokenFrame;
555 if (tokenOwner && !principal) return original;
556 if (descriptor.kind == Kind::TokenThumbnail && (source.width != 100 || source.height != 41)) return original;
557 if (descriptor.kind == Kind::TokenFrame && (source.width != 254 || source.height != 194)) return original;
558 std::uint8_t tokenFrame = 255;
559 if (descriptor.kind == Kind::TokenPreview)
560 {
561 const unsigned first = 0x0450 + 29 * descriptor.token;
563 data::dataTag(original->dataId) < first || data::dataTag(original->dataId) > first + 27) return original;
564 tokenFrame = std::uint8_t(data::dataTag(original->dataId) - first);
565 }
566 const bool auction = descriptor.kind == Kind::AuctionBackground || descriptor.kind == Kind::AuctionBottom ||
567 descriptor.kind == Kind::AuctionPlayer || descriptor.kind == Kind::AuctionBid;
568 if (auction)
569 {
570 if (!principal) return original;
571 // Exact retail DAT raster extents vary by player colour; layout width is independent.
572 constexpr std::array<std::array<unsigned,2>,12> playerSizes{{
573 {200,92},{201,90},{200,91},{200,91},{200,92},{200,91},
574 {133,90},{134,90},{134,90},{134,90},{134,90},{134,90}}};
575 const unsigned expectedWidth = descriptor.kind == Kind::AuctionBackground || descriptor.kind == Kind::AuctionBottom ? 800 :
576 descriptor.kind == Kind::AuctionPlayer ? playerSizes[root-0x00030370][0] : 110;
577 const unsigned expectedHeight = descriptor.kind == Kind::AuctionBackground ? 450 :
578 descriptor.kind == Kind::AuctionBottom ? 150 : descriptor.kind == Kind::AuctionPlayer ? playerSizes[root-0x00030370][1] : 27;
579 if (source.width != expectedWidth || source.height != expectedHeight) return original;
580 }
581 if (!source.width || !source.height || source.width > 800 || source.height > 600 ||
582 source.pixels.size() != std::size_t(source.width) * source.height * 4) return original;
583 const Key key{root, original.get(), principal};
584 if (const auto found = cache_.find(key); found != cache_.end())
585 { found->second.lastUsed = ++cacheClock_; return found->second.replacement; }
586 std::shared_ptr<const data::LegacyBitmapRGBA8> tokenImage;
587 if (descriptor.kind == Kind::TokenPreview || descriptor.kind == Kind::TokenThumbnail)
588 {
589 tokenImage = tokenImages_(std::uint8_t(descriptor.token),tokenFrame);
590 if (!tokenImage || tokenImage->width != 768 || tokenImage->height != 640 ||
591 tokenImage->pixels.size() != std::size_t(768)*640*4) return original;
592 }
593 auto replacement = std::make_shared<data::BitmapRuntimeAsset>();
594 replacement->dataId = original->dataId;
595 replacement->sourceType = data::LegacyDataType::Native;
596 replacement->preferLinearFiltering = true;
597 auto& image = replacement->image;
598 if (auctionBackdrop)
599 {
600 // Raw BMP_auctiona is opaque. Check once per immutable source, not
601 // on cache hits. No native alpha or geometry may be reconstructed.
602 for(std::size_t at=3;at<source.pixels.size();at+=4)
603 if(source.pixels[at]!=255)return original;
604 *replacement=*original;
605 replacement->sourceType=data::LegacyDataType::Native;
606 constexpr std::array<unsigned,3> teal{80,133,136};
607 for(unsigned y=0;y<280;++y)
608 {
609 // Source-derived detail remains; no synthetic gradient or flat fill.
610 const unsigned weight=y<240 ? 255U : (280-y)*255U/40U;
611 for(unsigned x=0;x<800;++x)
612 {
613 const auto at=(std::size_t(y)*800+x)*4;
614 const unsigned luminance=(54U*source.pixels[at]+183U*source.pixels[at+1]+
615 19U*source.pixels[at+2]+128U)/256U;
616 for(unsigned c=0;c<3;++c)
617 {
618 const unsigned tinted=std::min(255U,(luminance*teal[c]+64U)/128U);
619 image.pixels[at+c]=std::uint8_t((tinted*weight+
620 source.pixels[at+c]*(255U-weight)+127U)/255U);
621 }
622 }
623 }
624 // Every RGBA byte in rows280..449 remains copied from retail:
625 // perspective runway, bright lamps and auctioneer ground contact.
626 }
627 else if (descriptor.kind == Kind::TokenPreview)
628 {
629 image = *tokenImage;
630 replacement->presentationRect = std::array<float,4>{527.0F,275.5F,747.0F,458.5F};
631 }
632 else
633 {
634 image = {source.width * 3, source.height * 3, std::vector<std::uint8_t>(source.pixels.size() * 9, 0)};
635 if (principal || descriptor.kind == Kind::TradeBackground || descriptor.kind == Kind::StatsBackground)
636 {
637 const auto w = image.width, h = image.height;
638 const bool selectedThumbnail = descriptor.kind == Kind::TokenThumbnail &&
640 const auto* photo = descriptor.kind == Kind::Background ? background_.get() : nullptr;
641 const double cover = photo ? std::max(double(w) / photo->width, double(h) / photo->height) : 1;
642 const double photoOffsetX = photo ? (photo->width * cover - w) / 2 : 0;
643 const double photoOffsetY = photo ? (photo->height * cover - h) / 2 : 0;
644 const bool selected = descriptor.kind == Kind::SelectedSlot || descriptor.kind == Kind::SelectedTab ||
645 descriptor.kind == Kind::SelectedToggle || descriptor.kind == Kind::StatsSelectedTab ||
646 descriptor.kind == Kind::CalculatorSelectedKey ||
647 descriptor.kind == Kind::CalculatorSelectedFunction ||
648 descriptor.kind == Kind::TradeCashPressed ||
650 const bool background = descriptor.kind == Kind::Background || descriptor.kind == Kind::Pattern ||
651 descriptor.kind == Kind::AuctionBackground || descriptor.kind == Kind::TradeBackground ||
652 descriptor.kind == Kind::StatsBackground;
653 const bool roundedKind = descriptor.kind == Kind::Button || descriptor.kind == Kind::TradeButton;
654 const bool playerCard = descriptor.kind == Kind::PlayerCard;
655 const bool auctionPlayer = descriptor.kind == Kind::AuctionPlayer;
656 const bool playerPanel = descriptor.kind == Kind::StatsPanel;
657 const bool tradeRail = descriptor.kind == Kind::TradeRail;
658 const bool sourceAlpha = selectedToggle || descriptor.kind == Kind::TradeOffer || statsPanel || statsControl || calculatorKey ||
660 const unsigned auctionBandX = root < 0x00030376 ? 99 : 0;
661 const std::array<unsigned, 3> fill = selectedToggle ? std::array<unsigned, 3>{188, 157, 94} :
662 background ? std::array<unsigned, 3>{13, 35, 38} :
663 selected ? std::array<unsigned, 3>{42, 85, 82} : std::array<unsigned, 3>{22, 60, 61};
664 constexpr std::array<unsigned, 3> brass{188, 157, 94};
665 // Actual F03/F04 skyline uses gray69..116; the premultiplied
666 // F05 fade decodes to67..115. The measured art is wholly grayscale
667 // inside native[x7,202)×[12,80), above baked caption/arrows.
668 const int citySkyBackground = original->dataId == 0x00050F05 ? 67 : 69;
669 const int citySkyForeground = original->dataId == 0x00050F05 ? 115 : 116;
670 constexpr std::array<unsigned,3> citySkyInk{117,139,123};
671 unsigned rowGradient = 0;
672 std::size_t sourceRow = 0;
673 paintMenuRaster(image, !photo && descriptor.kind != Kind::TokenThumbnail,
674 [&](unsigned x, unsigned y, std::size_t offset)
675 {
676 // Evaluate every output row, including rows within one native pixel.
677 if (x == 0)
678 { rowGradient = 6 * (h - y) / h; sourceRow = std::size_t(y/3) * source.width; }
679 const bool rim = background ? x < 9 || y < 9 || x + 9 >= w || y + 9 >= h :
680 x < 3 || y < 3 || x + 3 >= w || y + 3 >= (optionsHeader ? 135U : h);
681 const bool rounded = roundedKind &&
683 if (rounded || (optionsHeader && y >= 135)) return;
684 for (unsigned c = 0; c < 3; ++c)
685 image.pixels[offset + c] = std::uint8_t(rim ? brass[c] : fill[c] + (selectedToggle ? 0 : rowGradient));
686 image.pixels[offset + 3] = 255;
687 // Native black name glyphs are published separately at x13/y16,
688 // 67x14. Give that untouched surface a cream band with two pixels padding.
689 if (playerCard && x >= 33 && x < 246 && y >= 42 && y < 96)
690 {
691 image.pixels[offset] = 245; image.pixels[offset+1] = 235;
692 image.pixels[offset+2] = 211;
693 }
694 if (citySelector && x >= 21 && x < 606 && y >= 36 && y < 240)
695 {
696 const auto native = (sourceRow+x/3)*4;
697 if (source.pixels[native] == source.pixels[native+1] &&
698 source.pixels[native+1] == source.pixels[native+2])
699 {
700 const unsigned coverage = unsigned(std::clamp(
701 int(source.pixels[native])-citySkyBackground,0,
704 for (unsigned c=0;c<3;++c)
705 image.pixels[offset+c]=std::uint8_t((citySkyInk[c]*coverage+
706 image.pixels[offset+c]*(255-coverage)+127)/255);
707 }
708 }
709 // Native white city name remains at local40,114,130x13.
710 // Keep its well dark without painting either independent arrow.
711 if (citySelector && x >= 120 && x < 510 && y >= 336 && y < 387)
712 {
713 image.pixels[offset] = 13; image.pixels[offset+1] = 35;
714 image.pixels[offset+2] = 38;
715 }
716 if (auctionPlayer)
717 {
718 if (x >= auctionBandX && x < auctionBandX + 402 && ((y >= 30 && y < 120) || (y >= 165 && y < 255)))
719 {
720 image.pixels[offset] = 245; image.pixels[offset+1] = 235;
721 image.pixels[offset+2] = 211;
722 }
723 // Full-width top stripe leaves both dynamic text bands readable.
724 if (y >= 3 && y < 18 && x >= 3 && x + 3 < w)
725 for (unsigned c = 0; c < 3; ++c)
726 image.pixels[offset+c] = std::uint8_t(PlayerColours[descriptor.colour][c]);
727 }
728 if (playerPanel && y >= 3 && y < 18 && x >= 3 && x + 3 < w)
729 for (unsigned c = 0; c < 3; ++c)
730 image.pixels[offset+c] = std::uint8_t(PlayerColours[descriptor.colour][c]);
731 if (tradeRail && y >= 3 && y < 18 && x >= 3 && x + 3 < w)
732 for (unsigned c = 0; c < 3; ++c) image.pixels[offset+c] = std::uint8_t(
733 descriptor.colour < 6 ? PlayerColours[descriptor.colour][c] : std::array<unsigned,3>{130,145,143}[c]);
734 if (sourceAlpha)
735 image.pixels[offset+3] = source.pixels[(sourceRow+x/3)*4+3];
736 // These banknotes are the real denomination controls. Preserve
737 // their authored ink/colours and locations; amounts remain native text.
738 if (descriptor.kind == Kind::TradeCashPanel &&
739 x>=24 && x<552 && y>=48 && y<135)
740 {
741 const auto native=(sourceRow+x/3)*4;
742 for(unsigned c=0;c<3;++c)image.pixels[offset+c]=source.pixels[native+c];
743 }
744 if (!rim && photo)
745 {
746 const auto px = std::min(photo->width - 1, unsigned((x + photoOffsetX) / cover));
747 const auto py = std::min(photo->height - 1, unsigned((y + photoOffsetY) / cover));
748 const auto src = (std::size_t(py) * photo->width + px) * 4;
749 // Teal scrim keeps the existing foreground controls legible.
750 const unsigned alpha = unsigned(photo->pixels[src + 3]) * 110 / 255;
751 for (unsigned c = 0; c < 3; ++c)
752 image.pixels[offset + c] = std::uint8_t((unsigned(photo->pixels[src + c]) * alpha +
753 unsigned(image.pixels[offset + c]) * (255 - alpha) + 127) / 255);
754 }
755 });
756 if (descriptor.kind == Kind::TokenThumbnail)
757 {
758 const double fit = std::min(double(w-18)/tokenImage->width,double(h-18)/tokenImage->height);
759 const unsigned iw=unsigned(tokenImage->width*fit), ih=unsigned(tokenImage->height*fit);
760 const unsigned ox=(w-iw)/2, oy=(h-ih)/2;
761 for(unsigned y=0;y<ih;++y) for(unsigned x=0;x<iw;++x)
762 {
763 const auto src=(std::size_t(y*tokenImage->height/ih)*tokenImage->width+x*tokenImage->width/iw)*4;
764 const auto dst=(std::size_t(y+oy)*w+x+ox)*4;
765 const unsigned alpha=tokenImage->pixels[src+3];
766 for(unsigned c=0;c<3;++c) image.pixels[dst+c]=std::uint8_t((unsigned(tokenImage->pixels[src+c])*alpha+
767 unsigned(image.pixels[dst+c])*(255-alpha)+127)/255);
768 }
769 }
770 if(calculatorFunction || (helpChrome && descriptor.label.find('\n') != std::string_view::npos))
771 {
772 const auto split=descriptor.label.find('\n');
773 const unsigned count=split==std::string_view::npos?1:2;
774 std::array<data::LegacyBitmapRGBA8,2> lines;
775 const auto first=text_(descriptor.label.substr(0,split));
776 if(!first)return original;
777 lines[0]=*first;
778 if(count==2)
779 {
780 const auto second=text_(descriptor.label.substr(split+1));
781 if(!second)return original;
782 lines[1]=*second;
783 }
784 if(!functionCaption(image,lines,count,helpChrome ? 54U : 24U))return original;
785 }
786 else if (!descriptor.label.empty())
787 {
788 const auto text = text_(descriptor.label);
789 if (!text) return original;
790 const bool statsLabel = descriptor.kind == Kind::StatsTab || descriptor.kind == Kind::StatsSelectedTab ||
791 descriptor.kind == Kind::StatsBarHeading;
793 descriptor.kind == Kind::StatsBarHeading, calculatorKey) :
794 caption(image, *text, descriptor.kind == Kind::Background, descriptor.kind == Kind::Confirmation,
795 optionsHeader ? 135U : citySelector ? 60U : 0U, citySelector ? 264U : 0U,
796 citySelector ? 6U : 24U, selectedToggle ? std::array<unsigned,3>{22,60,61} :
797 std::array<unsigned,3>{245,235,211});
798 if (!painted) return original;
799
800 }
801 }
802 }
803 // Identity retains its owner so recycled pointers cannot return stale
804 // replacements. Bound animated frame retention and decoded raster bytes.
805 // Retire the least recently displayed derivative, rather than flushing
806 // the live background and all thumbnails when an animation fills the cache.
807 while (!cache_.empty() && (cache_.size() >= 64 ||
808 cachedBytes_ + image.pixels.size() > 96U * 1024U * 1024U))
809 {
810 const auto oldest = std::ranges::min_element(cache_, {},
811 [](const auto& entry) { return entry.second.lastUsed; });
812 cachedBytes_ -= oldest->second.replacement->image.pixels.size();
813 cache_.erase(oldest);
814 }
815 cachedBytes_ += image.pixels.size();
816 cache_.emplace(key, Entry{original, replacement, ++cacheClock_});
817 return replacement;
818 }
819}
std::size_t count
std::size_t offset
std::size_t root
std::string_view text
std::atomic< float > left
LegacyBitmapMetadata metadata
LegacyBitmapRGBA8 image
int top
std::shared_ptr< const data::HmdTextureImage > source
std::uint64_t key
std::uint32_t height
std::uint32_t width
unsigned index
std::uint16_t leaf
std::string_view label
data::DataTag first
int token
int colour
std::array< data::DataTag, 3 > roots
Kind kind
std::size_t frame
Button button
int boxHeight
std::size_t at
CardTag native
void configureTokenImages(TokenImageProvider provider)
void configureBackground(std::shared_ptr< const data::LegacyBitmapRGBA8 > image)
bool supports(data::DataId root) const noexcept
std::shared_ptr< const data::BitmapRuntimeAsset > substitute(data::DataId root, std::shared_ptr< const data::BitmapRuntimeAsset > original, bool principal=true)
std::function< std::shared_ptr< const data::LegacyBitmapRGBA8 >(std::uint8_t token, std::uint8_t frame)> TokenImageProvider
void configureTradeCashPresentation(std::function< bool()> qualified)
std::uint32_t DataId
Definition DataBanks.hpp:10
constexpr DataTag dataTag(DataId id) noexcept
Definition DataBanks.hpp:64
std::uint16_t DataTag
Definition DataBanks.hpp:11
constexpr std::uint16_t legacyGroupValue(LegacyGroupId group) noexcept
Definition DataBanks.hpp:30
std::expected< LegacyUapMetadata, BitmapError > inspectLegacyUap(std::span< const std::byte > bytes)
constexpr DataId packDataId(std::uint16_t group, DataTag tag) noexcept
Definition DataBanks.hpp:39
std::expected< LegacyBitmapMetadata, BitmapError > inspectLegacyBitmap(std::span< const std::byte > bytes)
constexpr std::uint16_t dataGroup(DataId id) noexcept
Definition DataBanks.hpp:57
void paintMenuRaster(data::LegacyBitmapRGBA8 &image, bool horizontalTriplets, PaintPixel &&paintPixel)