Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
ModernAssetProbe.cpp
Go to the documentation of this file.
1#include "MeshRuntime.hpp"
2#include "BoardRules.hpp"
4#include "FontRuntime.hpp"
5#include "MoneyFormat.hpp"
6#include "ModernGltfMesh.hpp"
9#include "ResourcePaths.hpp"
10#include "ResourceRuntime.hpp"
11#include "SequenceRuntime.hpp"
12
13#include <SDL3/SDL.h>
14
15#include <algorithm>
16#include <array>
17#include <bit>
18#include <chrono>
19#include <charconv>
20#include <cmath>
21#include <cwctype>
22#include <filesystem>
23#include <fstream>
24#include <iomanip>
25#include <iostream>
26#include <limits>
27#include <locale>
28#include <optional>
29#include <set>
30#include <sstream>
31#include <span>
32#include <string>
33#include <string_view>
34#include <system_error>
35#include <utility>
36
37namespace
38{
39 using namespace monopoly::data;
40
41 struct Dimensions
42 {
43 std::array<float, 3> size{};
44 std::array<float, 3> center{};
45 };
46
47 [[nodiscard]] Dimensions dimensions(const MeshBounds& bounds) noexcept
48 {
49 Dimensions result;
50 for (std::size_t axis = 0; axis < 3; ++axis)
51 {
52 result.size[axis] = bounds.maximum[axis] - bounds.minimum[axis];
53 result.center[axis] =
54 (bounds.maximum[axis] + bounds.minimum[axis]) * 0.5F;
55 }
56 return result;
57 }
58
59 void printVector(const std::array<float, 3>& value)
60 {
61 std::cout << std::fixed << std::setprecision(3)
62 << value[0] << ',' << value[1] << ',' << value[2];
63 }
64
65 bool parseFiniteFloat(std::string_view argument, float& value) noexcept
66 {
67 const auto parsed = std::from_chars(argument.data(), argument.data() + argument.size(), value);
68 return parsed.ec == std::errc{} && parsed.ptr == argument.data() + argument.size() &&
69 std::isfinite(value);
70 }
71
72 std::string_view meshErrorName(MeshRuntimeErrorCode code) noexcept
73 {
74 switch (code)
75 {
76 case MeshRuntimeErrorCode::MissingSource: return "MissingSource";
77 case MeshRuntimeErrorCode::MissingResources: return "MissingResources";
78 case MeshRuntimeErrorCode::SourceLoadFailed: return "SourceLoadFailed";
79 case MeshRuntimeErrorCode::ModernAssetInvalid: return "ModernAssetInvalid";
80 case MeshRuntimeErrorCode::ModernAssetUnsupported: return "ModernAssetUnsupported";
81 case MeshRuntimeErrorCode::TriangleDecodeFailed: return "TriangleDecodeFailed";
82 case MeshRuntimeErrorCode::TextureDecodeFailed: return "TextureDecodeFailed";
83 case MeshRuntimeErrorCode::MimeDecodeFailed: return "MimeDecodeFailed";
84 case MeshRuntimeErrorCode::InvalidPose: return "InvalidPose";
85 case MeshRuntimeErrorCode::InvalidTextureRegion: return "InvalidTextureRegion";
86 case MeshRuntimeErrorCode::VertexLimitExceeded: return "VertexLimitExceeded";
87 case MeshRuntimeErrorCode::GroupLimitExceeded: return "GroupLimitExceeded";
88 case MeshRuntimeErrorCode::IndexLimitExceeded: return "IndexLimitExceeded";
89 case MeshRuntimeErrorCode::NoRenderableGeometry: return "NoRenderableGeometry";
90 }
91 return "Unknown";
92 }
93
95 {
96 std::uint64_t hash=14695981039346656037ULL;
97 auto word=[&](std::uint64_t value){for(unsigned shift=0;shift<64;shift+=8){hash^=(value>>shift)&255;hash*=1099511628211ULL;}};
98 auto floats=[&](const auto& values){for(const float value:values)word(std::bit_cast<std::uint32_t>(value));};
100 const auto& mesh=*item.renderData;
101 floats(mesh.bounds.minimum);floats(mesh.bounds.maximum);
102 word(mesh.vertices.size());word(mesh.indices.size());word(mesh.batches.size());
103 for(const auto& vertex:mesh.vertices){floats(vertex.position);floats(vertex.normal);floats(vertex.uv);floats(vertex.tangent);}
104 for(const auto index:mesh.indices)word(index);
105 for(const auto& batch:mesh.batches)
106 {
107 word(batch.firstIndex);word(batch.indexCount);const auto& material=batch.material;
108 word(unsigned(material.model));word(material.rawDiffuse);floats(material.diffuse);floats(material.emissive);
109 floats(std::array{material.metallic,material.roughness,material.emissiveStrength,material.alphaCutoff});
110 word(material.doubleSided);word(unsigned(material.alphaMode));
111 for(const auto* map:std::array{&material.baseColorTexture,&material.metallicRoughnessTexture,&material.normalTexture,&material.emissiveTexture,&material.occlusionTexture})
112 {
113 word(map->has_value());if(!*map)continue;const auto& binding=**map;
114 word(binding.texCoord);word(unsigned(binding.colorSpace));floats(std::array{binding.scale});
115 word(unsigned(binding.sampler.wrapS));word(unsigned(binding.sampler.wrapT));
116 word(unsigned(binding.sampler.minFilter));word(unsigned(binding.sampler.magFilter));
117 word(bool(binding.image));if(!binding.image)continue;
118 word(binding.image->width);word(binding.image->height);
119 for(const auto byte:binding.image->rgba){hash^=byte;hash*=1099511628211ULL;}
120 }
121 }
122 return hash;
123 }
124 int benchmarkEnvironment(int argc,char** argv)
125 {
126 if(argc!=5){std::cerr<<"usage: MonopolyModernAssetProbe --environment-benchmark <assets-root> <workers:1|4|8> <runs:1..3>\n";return 2;}
127 unsigned workers{},runs{};
128 auto parse=[](const char* text,unsigned& result){const std::string_view value{text};const auto p=std::from_chars(value.data(),value.data()+value.size(),result);return p.ec==std::errc{}&&p.ptr==value.data()+value.size();};
129 if(!parse(argv[3],workers)||(workers!=1&&workers!=4&&workers!=8)||!parse(argv[4],runs)||runs<1||runs>3)return 2;
130 std::vector<std::uint64_t> baseline;
131 for(unsigned run=0;run<runs;++run)
132 {
133 monopoly::engine::ModernEnvironment environment(argv[2],true);
134 environment.setDecodeWorkers(workers);
135 const auto started=std::chrono::steady_clock::now();
136 const auto items=environment.items(monopoly::sequence::identity3D(),0,true,false,true);
137 const double milliseconds=std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-started).count();
138 if(items.size()!=monopoly::engine::ModernCityCount+2){std::cerr<<"incomplete environment benchmark\n";return 1;}
139 std::vector<std::uint64_t> signatures;
140 std::size_t vertices=0,triangles=0;
141 for(const auto& item:items)
142 {
143 const auto signature=environmentSignature(item);signatures.push_back(signature);
144 vertices+=item.renderData->vertices.size();triangles+=item.renderData->indices.size()/3;
145 std::cout<<"asset_signature\trun\t"<<run<<"\tnode\t"<<item.node<<"\tid\t"<<item.contentsDataId
146 <<"\tfnv1a64\t"<<std::hex<<signature<<std::dec<<'\n';
147 }
148 if(run==0)baseline=signatures;else if(signatures!=baseline){std::cerr<<"environment signatures changed between runs\n";return 1;}
149 std::cout<<"environment_decode_cpu\tworkers\t"<<workers<<"\trun\t"<<run<<"\telapsed_ms\t"<<milliseconds
150 <<"\titems\t"<<items.size()<<"\tvertices\t"<<vertices<<"\ttriangles\t"<<triangles
151 <<"\tscope\tdecode and ordered publish only; signature hashing excluded; no GPU\n";
152 }
153 return 0;
154 }
155
156 int probeGltf(int argc, char** argv)
157 {
158 const bool keepGround = argc > 2 && std::string_view(argv[argc - 1]) == "--keep-ground";
159 const int positionalCount = argc - (keepGround ? 1 : 0);
160 if (positionalCount != 4 && positionalCount != 5 && positionalCount != 8)
161 {
162 std::cerr << "usage: MonopolyModernAssetProbe --gltf <path> <unitsPerMeter> "
163 "[yawDegrees [offsetX offsetY offsetZ]] [--keep-ground]\n";
164 return 2;
165 }
166 ModernGltfLoadOptions options;
167 options.groundToZero = !keepGround;
168 if (!parseFiniteFloat(argv[3], options.unitsPerMeter) || options.unitsPerMeter <= 0.0F)
169 {
170 std::cerr << "unitsPerMeter must be a finite positive number\n";
171 return 2;
172 }
173 if (positionalCount >= 5 && !parseFiniteFloat(argv[4], options.yawDegrees))
174 {
175 std::cerr << "yawDegrees must be a finite number\n";
176 return 2;
177 }
178 if (positionalCount == 8)
179 for (std::size_t axis = 0; axis < options.localOffset.size(); ++axis)
180 if (!parseFiniteFloat(argv[5 + axis], options.localOffset[axis]))
181 {
182 std::cerr << "offsetX, offsetY and offsetZ must be finite numbers\n";
183 return 2;
184 }
185 auto loaded = loadModernGltfMesh(std::filesystem::path(argv[2]), options);
186 if (!loaded)
187 {
188 std::cerr << "glTF error_code=" << meshErrorName(loaded.error().code)
189 << '(' << static_cast<unsigned>(loaded.error().code) << "): "
190 << loaded.error().detail << '\n';
191 return 1;
192 }
193 const auto& mesh = **loaded;
194 const auto measured = dimensions(mesh.bounds);
195 std::array<std::size_t, 5> mapCounts{};
196 std::set<const ModernTextureImage*> images;
197 for (const auto& batch : mesh.batches)
198 {
199 const std::array<const std::optional<ModernTextureBinding>*, 5> maps{
200 &batch.material.baseColorTexture, &batch.material.metallicRoughnessTexture,
201 &batch.material.normalTexture, &batch.material.emissiveTexture,
202 &batch.material.occlusionTexture};
203 for (std::size_t index = 0; index < maps.size(); ++index)
204 if (*maps[index])
205 {
206 ++mapCounts[index];
207 if ((**maps[index]).image) images.insert((**maps[index]).image.get());
208 }
209 }
210 std::cout.imbue(std::locale::classic());
211 std::cout << std::defaultfloat << std::setprecision(std::numeric_limits<float>::max_digits10);
212 const auto vector = [](const std::array<float, 3>& value)
213 { std::cout << value[0] << ',' << value[1] << ',' << value[2]; };
214 std::cout << "gltf\t" << argv[2] << "\nunits_per_meter\t" << options.unitsPerMeter
215 << "\nyaw_degrees\t" << options.yawDegrees << "\nlocal_offset\t";
216 vector(options.localOffset);
217 std::cout << "\nground_to_zero\t" << (options.groundToZero ? 1 : 0)
218 << "\nminimum_xyz\t";
219 vector(mesh.bounds.minimum);
220 std::cout << "\nmaximum_xyz\t"; vector(mesh.bounds.maximum);
221 std::cout << "\nsize_xyz\t"; vector(measured.size);
222 std::cout << "\ncenter_xyz\t"; vector(measured.center);
223 std::cout << "\nvertices\t" << mesh.vertices.size()
224 << "\ntriangles\t" << mesh.indices.size() / 3U
225 << "\nbatches\t" << mesh.batches.size()
226 << "\nbase_color_map_bindings\t" << mapCounts[0]
227 << "\nmetallic_roughness_map_bindings\t" << mapCounts[1]
228 << "\nnormal_map_bindings\t" << mapCounts[2]
229 << "\nemissive_map_bindings\t" << mapCounts[3]
230 << "\nocclusion_map_bindings\t" << mapCounts[4]
231 << "\nunique_images\t" << images.size() << '\n';
232 return std::cout ? 0 : 1;
233 }
234
235 bool within(const std::filesystem::path& path, const std::filesystem::path& root)
236 {
237 const auto relative = path.lexically_relative(root);
238 return !relative.empty() && !relative.is_absolute() &&
239 *relative.begin() != std::filesystem::path("..");
240 }
241
242 bool exportObj(const std::filesystem::path& output, std::string_view name,
243 DataId id, const MeshRenderData& mesh)
244 {
245 std::error_code error;
246 if (std::filesystem::is_symlink(std::filesystem::symlink_status(output, error)))
247 { std::cerr << "refusing symlink OBJ output: " << output << '\n'; return false; }
248 if (mesh.indices.size() % 3 != 0)
249 { std::cerr << "OBJ mesh has incomplete triangle indices: " << id << '\n'; return false; }
250 for (const auto index : mesh.indices)
251 if (index >= mesh.vertices.size())
252 { std::cerr << "OBJ mesh index outside vertices: " << id << '\n'; return false; }
253 std::ofstream file(output, std::ios::trunc);
254 file.imbue(std::locale::classic());
255 file << std::setprecision(std::numeric_limits<float>::max_digits10)
256 << "# Diagnostic retail base mesh; production decoder coordinates and normals.\n"
257 << "# DataId " << id << "; no textures, materials or sequence transforms.\n"
258 << "o " << name << '\n';
259 for (const auto& vertex : mesh.vertices)
260 file << "v " << vertex.position[0] << ' ' << vertex.position[1] << ' '
261 << vertex.position[2] << '\n';
262 for (const auto& vertex : mesh.vertices)
263 file << "vn " << vertex.normal[0] << ' ' << vertex.normal[1] << ' '
264 << vertex.normal[2] << '\n';
265 for (std::size_t index = 0; index < mesh.indices.size(); index += 3)
266 {
267 file << 'f';
268 for (std::size_t corner = 0; corner < 3; ++corner)
269 {
270 const auto objIndex = static_cast<std::uint64_t>(mesh.indices[index + corner]) + 1;
271 file << ' ' << objIndex << "//" << objIndex;
272 }
273 file << '\n';
274 }
275 file.close();
276 if (!file) { std::cerr << "cannot write diagnostic OBJ: " << output << '\n'; return false; }
277 return true;
278 }
279 std::string jsonString(std::string_view text)
280 {
281 std::ostringstream result;
282 result << '"';
283 for (const unsigned char value : text)
284 {
286 else if (value < 0x20U)
287 result << "\\u00" << std::hex << std::setw(2) << std::setfill('0')
288 << static_cast<unsigned>(value) << std::dec;
290 }
291 result << '"';
292 return result.str();
293 }
294
295 bool exportSquareLabels(const std::filesystem::path& directory,
296 const ResourceSnapshot& resources)
297 {
298 const auto language = resources.language();
299 if (resources.context().board != BoardEdition::Usa || !language ||
300 language->language != LanguageId::EnglishUs || !language->catalog)
301 { std::cerr << "USA print labels require classic USA English resources\n"; return false; }
302 std::ostringstream json;
303 json.imbue(std::locale::classic());
304 json << "{\"label_contract_version\":1,\"edition\":\"USA\",\"language_id\":1,"
305 "\"city\":0,\"immutable_rules\":true,\"name_source\":\"LanguageCatalog::lookup\","
306 "\"price_source\":\"BoardRules::originalDefinition + money::format\",\"squares\":[";
307 for (std::uint32_t index = 0; index < 40; ++index)
308 {
309 const auto messageId = 1001U + index;
310 const auto text = language->catalog->lookup(messageId);
311 if (!text || !*text || (***text).empty() || (***text).front() == u'*')
312 { std::cerr << "missing canonical USA square name: " << messageId << '\n'; return false; }
313 const auto name = monopoly::fonts::transcodeUtf8(std::u16string_view(***text));
314 if (!name) { std::cerr << "invalid USA square UTF16\n"; return false; }
315 const auto square = static_cast<monopoly::rules::board::SquareType>(index);
316 const auto& original = monopoly::rules::board::originalDefinition(square);
317 const bool ownable = monopoly::rules::board::isOwnable(square);
318 const auto price = monopoly::money::format(original.purchaseCost,13,true,BoardEdition::Usa);
319 if (!price) { std::cerr << price.error() << '\n'; return false; }
320 json << (index ? "," : "") << "{\"index\":" << index << ",\"type\":" << index
321 << ",\"name_message_id\":" << messageId << ",\"name_utf8\":" << jsonString(*name)
322 << ",\"purchase_cost\":" << original.purchaseCost
323 << ",\"price_text_usd\":" << jsonString(ownable ? *price : std::string{})
324 << ",\"group\":" << static_cast<unsigned>(original.group)
325 << ",\"ownable\":" << (ownable ? "true" : "false") << '}';
326 }
327 json << "]}\n";
328 const auto output = directory / "boardmed_labels.json";
329 std::error_code error;
330 if (std::filesystem::is_symlink(std::filesystem::symlink_status(output,error)))
331 { std::cerr << "refusing symlink USA labels output\n"; return false; }
332 std::ofstream file(output,std::ios::trunc);
333 file << json.str(); file.close();
334 return static_cast<bool>(file);
335 }
336
337 bool exportTexturedBoard(const std::filesystem::path& directory,
338 DataId id, const MeshRenderData& mesh, const BoardTextureRecipe* recipe = nullptr)
339 {
340 // Refuse existing links before any write, including per-batch image outputs.
341 for (const auto& entry : std::filesystem::directory_iterator(directory))
343 { std::cerr << "refusing symlink textured-board output\n"; return false; }
344 std::ofstream obj(directory / "boardmed.obj", std::ios::trunc);
345 std::ofstream mtl(directory / "boardmed.mtl", std::ios::trunc);
346 obj.imbue(std::locale::classic());
347 mtl.imbue(std::locale::classic());
348 obj << std::setprecision(std::numeric_limits<float>::max_digits10)
349 << "# Production MeshRuntime ClassicMedium, default pose; raw engine Y-up units.\n"
350 << "# DataId " << id << "; UVs converted top-left to OBJ bottom-left.\n"
351 << "mtllib boardmed.mtl\no boardmed\n";
352 mtl << std::setprecision(std::numeric_limits<float>::max_digits10);
353 for (const auto& vertex : mesh.vertices)
354 obj << "v " << vertex.position[0] << ' ' << vertex.position[1] << ' '
355 << vertex.position[2] << '\n';
356 for (const auto& vertex : mesh.vertices)
357 obj << "vt " << vertex.uv[0] << ' ' << 1.0F - vertex.uv[1] << '\n';
358 for (const auto& vertex : mesh.vertices)
359 obj << "vn " << vertex.normal[0] << ' ' << vertex.normal[1] << ' '
360 << vertex.normal[2] << '\n';
361 std::size_t covered{};
362 for (std::size_t batchIndex = 0; batchIndex < mesh.batches.size(); ++batchIndex)
363 {
364 const auto& batch = mesh.batches[batchIndex];
365 if (batch.firstIndex != covered || batch.indexCount % 3 != 0 ||
366 batch.firstIndex > mesh.indices.size() ||
367 batch.indexCount > mesh.indices.size() - batch.firstIndex)
368 { std::cerr << "invalid textured board batch coverage\n"; return false; }
369 covered += batch.indexCount;
370 const auto name = "board_batch_" + std::to_string(batchIndex);
371 mtl << "newmtl " << name << "\nKd " << batch.material.diffuse[0] << ' '
372 << batch.material.diffuse[1] << ' ' << batch.material.diffuse[2]
373 << "\nd 1\nillum 2\nPr 0.72\nPm 0\n";
374 if (batch.texture)
375 {
376 const auto& image = batch.texture->sourceImage;
377 if (!image || !image->width || !image->height ||
378 image->width > 16384 || image->height > 16384 ||
379 image->rgba.size() != static_cast<std::size_t>(image->width) * image->height * 4)
380 { std::cerr << "textured board has invalid/missing decoded source image\n"; return false; }
381 const auto fileName = name + ".bmp";
382 auto* surface = SDL_CreateSurfaceFrom(static_cast<int>(image->width),
383 static_cast<int>(image->height), SDL_PIXELFORMAT_RGBA32,
384 const_cast<std::uint8_t*>(image->rgba.data()), static_cast<int>(image->width * 4));
385 if (!surface) { std::cerr << SDL_GetError() << '\n'; return false; }
386 const bool saved = SDL_SaveBMP(surface, (directory / fileName).string().c_str());
387 SDL_DestroySurface(surface);
388 if (!saved) { std::cerr << SDL_GetError() << '\n'; return false; }
389 mtl << "map_Kd " << fileName << '\n';
390 }
391 mtl << '\n';
392 obj << "usemtl " << name << '\n';
393 for (std::size_t index = batch.firstIndex; index < covered; index += 3)
394 {
395 obj << 'f';
396 for (std::size_t corner = 0; corner < 3; ++corner)
397 {
398 const auto vertex = mesh.indices[index + corner];
399 if (vertex >= mesh.vertices.size()) return false;
400 const auto number = static_cast<std::uint64_t>(vertex) + 1;
401 obj << ' ' << number << '/' << number << '/' << number;
402 }
403 obj << '\n';
404 }
405 }
406 if (covered != mesh.indices.size()) return false;
407 obj.close(); mtl.close();
408 if (!obj || !mtl) return false;
409 std::ofstream proof(directory / "boardmed_contract.json", std::ios::trunc);
410 proof.imbue(std::locale::classic());
411 proof << std::setprecision(std::numeric_limits<float>::max_digits10)
412 << "{\"production_geometry_decoder\":true,\"data_id\":" << id
413 << ",\"units_per_meter\":1,\"yaw_degrees\":0,\"offset\":[0,0,0],"
414 "\"ground_to_zero\":false,\"raw_engine_units\":true,\"vertices\":"
415 << mesh.vertices.size() << ",\"triangles\":" << mesh.indices.size()/3
416 << ",\"batches\":" << mesh.batches.size() << ",\"minimum\":[";
417 for (std::size_t axis=0; axis<3; ++axis)
418 proof << (axis ? "," : "") << mesh.bounds.minimum[axis];
419 proof << "],\"maximum\":[";
420 for (std::size_t axis=0; axis<3; ++axis)
421 proof << (axis ? "," : "") << mesh.bounds.maximum[axis];
422 proof << "]";
423 if (recipe)
424 {
425 proof << ",\"texture_resolution\":256,\"geometry_uv_verified_unchanged\":true,\"source_textures\":[";
426 bool first = true;
427 for (std::size_t index = 0; index < mesh.batches.size(); ++index)
428 {
429 const auto& texture = mesh.batches[index].texture;
430 if (!texture) continue;
431 const auto& image = *texture->sourceImage;
432 const auto reference = std::find_if(recipe->textures.begin(), recipe->textures.end(),
433 [&](const auto& item) { return item.coordinate.x == image.rawX && item.coordinate.y == image.rawY; });
434 if (reference == recipe->textures.end()) return false;
435 proof << (first ? "" : ",") << "{\"batch\":" << index << ",\"file\":" << jsonString(reference->fileName)
436 << ",\"raw_x\":" << image.rawX << ",\"raw_y\":" << image.rawY
437 << ",\"width\":" << image.width << ",\"height\":" << image.height << '}';
438 first = false;
439 }
440 proof << ']';
441 }
442 proof << "}\n";
443 proof.close();
444 return static_cast<bool>(proof);
445 }
446
447}
448
449int main(int argc, char** argv)
450{
451 using namespace monopoly::data;
452
453 if (argc >= 2 && std::string_view(argv[1]) == "--environment-benchmark")
455 if (argc >= 2 && std::string_view(argv[1]) == "--gltf")
456 return probeGltf(argc, argv);
457
458 const bool boardTextures256 = argc == 5 && std::string_view(argv[3]) == "--dump-textured-board-256";
459 const bool texturedBoard = boardTextures256 || (argc == 5 && std::string_view(argv[3]) == "--dump-textured-board");
460 const bool boardOnly = texturedBoard || (argc == 5 && std::string_view(argv[3]) == "--dump-board");
461 if (argc != 3 && !(argc == 5 &&
462 (std::string_view(argv[3]) == "--dump-retail" || boardOnly)))
463 {
464 std::cerr
465 << "usage: MonopolyModernAssetProbe <retail-root> <modern-assets-root> "
466 "[--dump-retail|--dump-board|--dump-textured-board|--dump-textured-board-256 <existing-cmake-build-dir>]\n";
467 return 2;
468 }
469
470 const std::filesystem::path retailRoot =
471 std::filesystem::absolute(argv[1]);
472 const std::filesystem::path modernRoot =
473 std::filesystem::absolute(argv[2]);
474 std::optional<std::filesystem::path> dumpRoot;
475 if (argc == 5)
476 {
477 std::error_code error;
478 const auto build = std::filesystem::canonical(argv[4], error);
479 bool underSource = false;
480 for (const auto& component : build)
481 {
482 auto text = component.wstring();
483 for (auto& character : text) character = static_cast<wchar_t>(std::towlower(character));
484 if (text == L"source") underSource = true;
485 }
486 if (error || underSource || !std::filesystem::is_regular_file(build / "CMakeCache.txt", error))
487 { std::cerr << "OBJ output requires an existing CMake build outside Source\n"; return 2; }
488 const auto destination = build / (boardTextures256 ? "retail-textured-board-256" :
489 texturedBoard ? "retail-textured-board" : "retail-reference-obj");
490 std::filesystem::create_directories(destination, error);
491 if (error) { std::cerr << "OBJ directory: " << error.message() << '\n'; return 1; }
492 dumpRoot = std::filesystem::canonical(destination, error);
493 if (error || !within(*dumpRoot, build))
494 { std::cerr << "OBJ output directory escapes the selected build\n"; return 2; }
495 }
496
497 const std::array roots{retailRoot};
498 auto paths = ResourcePaths::create(roots);
499 if (!paths)
500 {
501 std::cerr << "resource root: " << paths.error().detail << '\n';
502 return 1;
503 }
504
506 auto initialized = runtime.initialize(*paths);
507 if (!initialized)
508 {
509 std::cerr << "resource runtime: "
510 << initialized.error().detail << '\n';
511 return 1;
512 }
513
514 auto resources = runtime.snapshot();
515 MeshRuntimeCache legacy(resources);
516 std::optional<BoardTextureRecipe> boardRecipe;
518 {
519 auto recipe = buildUsaTextureRecipe(BoardMeshKind::ClassicMedium, TextureResolution::Pixels256);
520 if (!recipe) { std::cerr << recipe.error().detail << '\n'; return 1; }
522 if (!images) { std::cerr << "USA256 texture recipe unavailable: " << images.error() << '\n'; return 1; }
523 const auto original = legacy.resolve(recipe->meshDataId);
524 if (!original || !(*original)->renderData) { std::cerr << "USA256 original board unavailable\n"; return 1; }
525 const auto replaced = legacy.replaceTextureImages(recipe->meshDataId, *images);
526 if (!replaced) { std::cerr << replaced.error().detail << '\n'; return 1; }
527 const auto replacement = legacy.resolve(recipe->meshDataId);
528 if (!replacement || !(*replacement)->renderData) return 1;
529 const auto& before = *(*original)->renderData;
530 const auto& after = *(*replacement)->renderData;
531 bool identical = before.indices == after.indices && before.vertices.size() == after.vertices.size() &&
532 before.batches.size() == after.batches.size() && before.bounds.minimum == after.bounds.minimum &&
533 before.bounds.maximum == after.bounds.maximum;
534 for (std::size_t index = 0; identical && index < before.vertices.size(); ++index)
535 {
536 const auto& a = before.vertices[index]; const auto& b = after.vertices[index];
537 identical = a.position == b.position && a.normal == b.normal && a.uv == b.uv && a.tangent == b.tangent;
538 }
539 for (std::size_t index = 0; identical && index < before.batches.size(); ++index)
540 {
541 const auto& a = before.batches[index]; const auto& b = after.batches[index];
542 identical = a.firstIndex == b.firstIndex && a.indexCount == b.indexCount &&
543 a.material.rawDiffuse == b.material.rawDiffuse && a.material.diffuse == b.material.diffuse;
544 if (b.texture)
545 {
546 const auto& image = b.texture->sourceImage;
547 if (!image || image->width != 256 || image->height != 256) { identical = false; break; }
548 const auto matched = std::find_if(images->begin(), images->end(), [&](const auto& item)
549 { return item->texturePage == image->texturePage && item->rawX == image->rawX && item->rawY == image->rawY; });
550 identical = matched != images->end() && (*matched)->rgba == image->rgba;
551 }
552 }
553 if (!identical) { std::cerr << "USA256 substitution changed board geometry/UVs or image mapping\n"; return 1; }
554 boardRecipe = std::move(*recipe);
555 }
556 bool failed = false;
557 std::cout.imbue(std::locale::classic());
558
559 std::cout << "retail representative token bounds\n"
560 << "token\tslug\tlegacy_data_id\tminimum_xyz\tmaximum_xyz\tsize_xyz\tcenter_xyz\n";
561 if (!boardOnly) for (const auto& definition : modernTokenDefinitions())
562 {
563 const auto id = representativeLegacyMesh(definition.token);
564 const auto asset = legacy.resolve(id);
565 if (!asset || !(*asset)->renderData)
566 {
567 std::cerr << definition.slug << ": representative legacy: "
568 << (asset ? "missing render data" : asset.error().detail) << '\n';
569 failed = true;
570 continue;
571 }
572 const auto& bounds = (*asset)->renderData->bounds;
573 const auto measured = dimensions(bounds);
574 std::cout << static_cast<unsigned>(definition.token) << '\t' << definition.slug
575 << '\t' << id << '\t';
576 printVector(bounds.minimum);
577 std::cout << '\t';
578 printVector(bounds.maximum);
579 std::cout << '\t';
580 printVector(measured.size);
581 std::cout << '\t';
582 printVector(measured.center);
583 std::cout << '\n';
584 if (dumpRoot && !exportObj(*dumpRoot / (std::string(definition.slug) + ".obj"),
585 definition.slug, id, *(*asset)->renderData)) failed = true;
586 }
587
588 std::cout << "\nretail board and building bounds\n"
589 << "slug\tlegacy_data_id\tminimum_xyz\tmaximum_xyz\tsize_xyz\tcenter_xyz\n";
590 constexpr std::array<std::pair<DataTag, std::string_view>, 4> BoardAssets{{
591 {DataTag{0x0001}, "board_citymed"}, {DataTag{0x0003}, "boardmed"},
592 {DataTag{0x0004}, "hotel"}, {DataTag{0x0005}, "house"}}};
593 for (const auto& [tag, name] : BoardAssets)
594 {
595 if (texturedBoard && tag != DataTag{0x0003}) continue;
596 const auto id = packDataId(LegacyGroupId::ThreeD, tag);
597 const auto asset = legacy.resolve(id);
598 if (!asset || !(*asset)->renderData)
599 {
600 std::cerr << name << ": retail DataId " << id << ": "
601 << (asset ? "missing render data" : asset.error().detail) << '\n';
602 failed = true;
603 continue;
604 }
605 const auto& bounds = (*asset)->renderData->bounds;
606 const auto measured = dimensions(bounds);
607 std::cout << name << '\t' << id << '\t';
608 printVector(bounds.minimum); std::cout << '\t';
609 printVector(bounds.maximum); std::cout << '\t';
610 printVector(measured.size); std::cout << '\t';
611 printVector(measured.center); std::cout << '\n';
612 if (texturedBoard)
613 {
614 if (!exportTexturedBoard(*dumpRoot, id, *(*asset)->renderData, boardRecipe ? &*boardRecipe : nullptr)) failed = true;
615 if (!exportSquareLabels(*dumpRoot, *resources)) failed = true;
616 }
617 else if (dumpRoot && !exportObj(*dumpRoot / (std::string(name) + ".obj"),
618 name, id, *(*asset)->renderData)) failed = true;
619 }
620
621 if (boardOnly) return failed ? 1 : 0;
622
623 std::cout
624 << "\nmodern token calibration\n"
625 "token\tslug\tunits_per_meter\tyaw_degrees\tlegacy_xyz\t"
626 "modern_xyz\tscale_xyz(modern/legacy)\tlegacy_center\tmodern_center\n";
627
628 for (const auto& definition : modernTokenDefinitions())
629 {
630 const auto glb =
631 modernRoot / std::filesystem::path(definition.relativeGlbPath)
632 .lexically_relative("assets/modern");
633
634 std::error_code filesystemError;
635 if (!std::filesystem::is_regular_file(glb, filesystemError))
636 {
637 // Missing optional authoring assets are expected. Real filesystem
638 // failures (permissions, invalid path, etc.) still fail the probe.
639 if (filesystemError && filesystemError != std::errc::no_such_file_or_directory)
640 {
641 std::cerr << definition.slug << ": modern file: " << filesystemError.message() << '\n';
642 failed = true;
643 }
644 continue;
645 }
646
647 const auto legacyId =
648 representativeLegacyMesh(definition.token);
649 auto legacyAsset = legacy.resolve(legacyId);
650 if (!legacyAsset || !(*legacyAsset)->renderData)
651 {
652 std::cerr << definition.slug << ": legacy: "
653 << (legacyAsset ? "missing render data" : legacyAsset.error().detail) << '\n';
654 failed = true;
655 continue;
656 }
657
659 loadOptions.unitsPerMeter = definition.unitsPerMeter;
660 loadOptions.yawDegrees = definition.yawDegrees;
661 loadOptions.localOffset = definition.localOffset;
662 loadOptions.groundToZero = true;
664 if (!modernAsset)
665 {
666 std::cerr << definition.slug << ": modern: "
667 << modernAsset.error().detail << '\n';
668 failed = true;
669 continue;
670 }
671
672 const auto oldDimensions =
673 dimensions((*legacyAsset)->renderData->bounds);
674 const auto newDimensions =
675 dimensions((*modernAsset)->bounds);
676
677 std::array<float, 3> ratio{};
678 for (std::size_t axis = 0; axis < 3; ++axis)
679 {
680 ratio[axis] = oldDimensions.size[axis] != 0.0F
681 ? newDimensions.size[axis] / oldDimensions.size[axis]
682 : 0.0F;
683 }
684
685 std::cout << static_cast<unsigned>(definition.token)
686 << '\t' << definition.slug
687 << '\t' << definition.unitsPerMeter
688 << '\t' << definition.yawDegrees << '\t';
690 std::cout << '\t';
692 std::cout << '\t';
693 printVector(ratio);
694 std::cout << '\t';
696 std::cout << '\t';
698 std::cout << '\n';
699 }
700
701 std::array<std::set<std::size_t>, ModernTokenCount> poseCounts;
702 std::array<std::size_t, ModernTokenCount> hmdCounts{};
703 std::array<std::size_t, ModernTokenCount> decodeFailures{};
704
705 for (std::uint32_t rawTag = 0; rawTag <= 0x00E2U; ++rawTag)
706 {
707 const auto id = packDataId(
708 LegacyGroupId::ThreeD,
709 static_cast<DataTag>(rawTag));
710 const auto token = tokenForLegacyMesh(id);
711 if (!token)
712 continue;
713
714 ++hmdCounts[*token];
715 auto asset = legacy.resolve(id);
716 if (!asset || !(*asset)->mesh)
717 {
719 failed = true;
720 std::cerr << "legacy HMD " << id << ": "
721 << (asset ? "missing decoded mesh" : asset.error().detail) << '\n';
722 std::cout << "decode_error\t" << unsigned(*token) << "\t0x" << std::hex << id
723 << std::dec << '\t' << (asset ? "missing decoded mesh" : asset.error().detail);
724 if (!asset)
725 {
726 std::cout << "\terror_code=" << static_cast<unsigned>(asset.error().code);
727 if (asset.error().sourceError)
728 std::cout << "\tsource_error=" << asset.error().sourceError->detail;
729 }
730 std::cout << '\n';
731 continue;
732 }
733 poseCounts[*token].insert((*asset)->mesh->poseCount());
734 }
735
736 std::cout << "\nretail MIMe pose inventory\n"
737 << "token\tslug\thmd_count\tpose_counts\tdecode_failures\n";
738 for (const auto& definition : modernTokenDefinitions())
739 {
740 const auto token = static_cast<std::size_t>(definition.token);
741 std::cout << static_cast<unsigned>(definition.token)
742 << '\t' << definition.slug
743 << '\t' << hmdCounts[token] << '\t';
744 bool first = true;
745 for (const auto count : poseCounts[token])
746 {
747 if (!first) std::cout << ',';
748 std::cout << count;
749 first = false;
750 }
751 std::cout << '\t' << decodeFailures[token] << '\n';
752 }
753
754 constexpr DataTag CornerBase = 0x00F6;
755 constexpr DataTag MoveBase = 0x010D;
756 constexpr std::uint32_t AnimsPerToken = 0x0159 - 0x00F6;
757
758 std::cout << "\nretail token sequence mesh inventory\n"
759 << "token\tslug\tidle_sequence\tidle_hmds\t"
760 "movement_hmd_count\tmovement_hmds\n";
761
762 for (const auto& definition : modernTokenDefinitions())
763 {
764 const auto token = static_cast<std::uint32_t>(definition.token);
765 const auto idleTag = static_cast<DataTag>(
766 static_cast<std::uint32_t>(MoveBase) +
768 const auto idleId = packDataId(LegacyGroupId::ThreeD, idleTag);
769
770 std::set<DataTag> idleMeshes;
771 std::set<DataTag> movementMeshes;
772
773 const auto collectMeshes =
774 [&](DataId sequenceId, std::set<DataTag>& output)
775 {
777 resources, sequenceId);
778 if (!program)
779 {
780 failed = true;
781 std::cerr << "legacy sequence " << sequenceId << ": " << program.error().detail << '\n';
782 return;
783 }
784 for (const auto& description : (*program)->descriptions())
785 {
786 if (!description.contentsDataId)
787 continue;
788 const auto mapped =
789 tokenForLegacyMesh(*description.contentsDataId);
790 if (mapped && *mapped == definition.token)
791 output.insert(dataTag(*description.contentsDataId));
792 }
793 };
794
796
797 // Movement code uses move base + 0..10 and corner base + 0..4.
798 // Both bases are offset by the same 99-sequence token stride.
799 for (std::uint32_t offset = 0; offset <= 10; ++offset)
800 {
803 LegacyGroupId::ThreeD,
804 static_cast<DataTag>(
805 static_cast<std::uint32_t>(MoveBase) +
808 }
809 for (std::uint32_t offset = 0; offset <= 4; ++offset)
810 {
813 LegacyGroupId::ThreeD,
814 static_cast<DataTag>(
815 static_cast<std::uint32_t>(CornerBase) +
818 }
819
820 const auto printTags = [](const std::set<DataTag>& tags)
821 {
822 bool first = true;
823 for (const auto tag : tags)
824 {
825 if (!first) std::cout << ',';
826 std::cout << "0x" << std::hex
827 << static_cast<unsigned>(tag)
828 << std::dec;
829 first = false;
830 }
831 };
832
833 std::cout << static_cast<unsigned>(definition.token)
834 << '\t' << definition.slug
835 << "\t0x" << std::hex
836 << static_cast<unsigned>(idleTag)
837 << std::dec << '\t';
839 std::cout << '\t' << movementMeshes.size() << '\t';
841 std::cout << '\n';
842 }
843
844 return failed || !std::cout ? 1 : 0;
845}
std::size_t count
std::size_t offset
const char * name
fs::path relative
std::size_t root
std::uint16_t group
std::string_view text
LegacyBitmapRGBA8 image
data::DataId id
fonts::Settings saved
fonts::Runtime & runtime
SDL_GPUTexture * texture
int main(int argc, char **argv)
unsigned index
data::DataTag first
int token
std::array< data::DataTag, 3 > roots
std::filesystem::path directory
std::string path
std::string error
std::deque< Bytes > output
std::uint16_t value
Definition Timers.cpp:16
std::expected< void, MeshRuntimeError > replaceTextureImages(DataId id, std::span< const std::shared_ptr< const HmdTextureImage > > images)
std::expected< std::shared_ptr< const MeshRuntimeAsset >, MeshRuntimeError > resolve(DataId id, std::optional< DataId > rootSequenceDataId=std::nullopt, std::uint16_t priority=0)
static std::expected< ResourcePaths, DataError > create(std::span< const std::filesystem::path > roots)
std::expected< void, DataError > initialize(ResourcePaths paths, ResourceContext context={}, ArchiveOpenOptions options={}, std::span< const DataSourceOverride > overrides={})
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::expected< Profile, Error > parse(std::string_view text, int level)
Result run(const Request &request)
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
std::expected< std::shared_ptr< const MeshRenderData >, MeshRuntimeError > loadModernGltfMesh(const std::filesystem::path &path, ModernGltfLoadOptions options)
const std::array< ModernTokenDefinition, ModernTokenCount > & modernTokenDefinitions() noexcept
BoardTextureRecipeResult buildUsaTextureRecipe(BoardMeshKind mesh, TextureResolution resolution)
DataId representativeLegacyMesh(std::uint8_t token) noexcept
constexpr std::size_t ModernTokenCount
std::expected< std::vector< std::shared_ptr< const HmdTextureImage > >, std::string > loadBoardTextureImages(const ResourcePaths &paths, const BoardTextureRecipe &recipe, const BoardTextureContext &context)
std::optional< std::uint8_t > tokenForLegacyMesh(DataId id) noexcept
constexpr DataId packDataId(std::uint16_t group, DataTag tag) noexcept
Definition DataBanks.hpp:39
constexpr std::size_t ModernCityCount
std::expected< std::string, Error > transcodeUtf8(std::u16string_view text)
std::expected< std::string, std::string > format(std::int64_t defaultValue, int monetarySystem, bool printSymbol, data::BoardEdition edition)
bool isOwnable(SquareType square)
const SquareDefinition & originalDefinition(SquareType square)
Matrix3D identity3D() noexcept
std::shared_ptr< const data::ResourceSnapshot > resources() noexcept
std::size_t size()
std::vector< MeshRenderBatch > batches
std::vector< std::uint32_t > indices
std::vector< MeshVertex > vertices
std::array< float, 16 > values
std::shared_ptr< const data::MeshRenderData > renderData