34#include <system_error>
43 std::array<float, 3>
size{};
44 std::array<float, 3> center{};
52 result.size[
axis] = bounds.maximum[
axis] - bounds.minimum[
axis];
54 (bounds.maximum[
axis] + bounds.minimum[
axis]) * 0.5F;
61 std::cout << std::fixed << std::setprecision(3)
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";
96 std::uint64_t
hash=14695981039346656037ULL;
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);}
105 for(
const auto&
batch:mesh.batches)
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})
113 word(map->has_value());
if(!*map)
continue;
const auto&
binding=**map;
126 if(
argc!=5){std::cerr<<
"usage: MonopolyModernAssetProbe --environment-benchmark <assets-root> <workers:1|4|8> <runs:1..3>\n";
return 2;}
130 std::vector<std::uint64_t>
baseline;
134 environment.setDecodeWorkers(
workers);
135 const auto started=std::chrono::steady_clock::now();
137 const double milliseconds=std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-started).count();
140 std::size_t vertices=0,triangles=0;
141 for(
const auto& item:items)
146 <<
"\tfnv1a64\t"<<std::hex<<
signature<<std::dec<<
'\n';
150 <<
"\titems\t"<<items.size()<<
"\tvertices\t"<<vertices<<
"\ttriangles\t"<<triangles
151 <<
"\tscope\tdecode and ordered publish only; signature hashing excluded; no GPU\n";
162 std::cerr <<
"usage: MonopolyModernAssetProbe --gltf <path> <unitsPerMeter> "
163 "[yawDegrees [offsetX offsetY offsetZ]] [--keep-ground]\n";
170 std::cerr <<
"unitsPerMeter must be a finite positive number\n";
175 std::cerr <<
"yawDegrees must be a finite number\n";
182 std::cerr <<
"offsetX, offsetY and offsetZ must be finite numbers\n";
189 <<
'(' <<
static_cast<unsigned>(
loaded.error().code) <<
"): "
190 <<
loaded.error().detail <<
'\n';
193 const auto& mesh = **
loaded;
194 const auto measured = dimensions(mesh.bounds);
196 std::set<const ModernTextureImage*>
images;
197 for (
const auto&
batch : mesh.batches)
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};
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)
214 std::cout <<
"gltf\t" <<
argv[2] <<
"\nunits_per_meter\t" << options.
unitsPerMeter
215 <<
"\nyaw_degrees\t" << options.
yawDegrees <<
"\nlocal_offset\t";
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);
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;
235 bool within(
const std::filesystem::path&
path,
const std::filesystem::path&
root)
239 *
relative.begin() != std::filesystem::path(
"..");
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)
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';
268 for (std::size_t corner = 0; corner < 3; ++corner)
276 if (!file) { std::cerr <<
"cannot write diagnostic OBJ: " <<
output <<
'\n';
return false; }
281 std::ostringstream result;
286 else if (
value < 0x20U)
287 result <<
"\\u00" << std::hex << std::setw(2) << std::setfill(
'0')
288 <<
static_cast<unsigned>(
value) << std::dec;
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\":[";
311 if (!
text || !*
text || (***text).empty() || (***text).front() == u
'*')
312 { std::cerr <<
"missing canonical USA square name: " <<
messageId <<
'\n';
return false; }
314 if (!
name) { std::cerr <<
"invalid USA square UTF16\n";
return false; }
319 if (!
price) { std::cerr <<
price.error() <<
'\n';
return false; }
322 <<
",\"purchase_cost\":" << original.purchaseCost
325 <<
",\"ownable\":" << (
ownable ?
"true" :
"false") <<
'}';
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);
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';
368 { std::cerr <<
"invalid textured board batch coverage\n";
return false; }
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";
376 const auto&
image =
batch.texture->sourceImage;
380 { std::cerr <<
"textured board has invalid/missing decoded source image\n";
return false; }
381 const auto fileName =
name +
".bmp";
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; }
389 mtl <<
"map_Kd " << fileName <<
'\n';
392 obj <<
"usemtl " <<
name <<
'\n';
396 for (std::size_t corner = 0; corner < 3; ++corner)
399 if (vertex >= mesh.
vertices.size())
return false;
400 const auto number =
static_cast<std::uint64_t
>(vertex) + 1;
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\":"
416 <<
",\"batches\":" << mesh.
batches.size() <<
",\"minimum\":[";
419 proof <<
"],\"maximum\":[";
425 proof <<
",\"texture_resolution\":256,\"geometry_uv_verified_unchanged\":true,\"source_textures\":[";
433 [&](
const auto& item) { return item.coordinate.x == image.rawX && item.coordinate.y == image.rawY; });
436 <<
",\"raw_x\":" <<
image.rawX <<
",\"raw_y\":" <<
image.rawY
444 return static_cast<bool>(
proof);
453 if (
argc >= 2 && std::string_view(
argv[1]) ==
"--environment-benchmark")
455 if (
argc >= 2 && std::string_view(
argv[1]) ==
"--gltf")
462 (std::string_view(
argv[3]) ==
"--dump-retail" ||
boardOnly)))
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";
471 std::filesystem::absolute(
argv[1]);
473 std::filesystem::absolute(
argv[2]);
474 std::optional<std::filesystem::path>
dumpRoot;
477 std::error_code
error;
478 const auto build = std::filesystem::canonical(
argv[4],
error);
487 { std::cerr <<
"OBJ output requires an existing CMake build outside Source\n";
return 2; }
489 texturedBoard ?
"retail-textured-board" :
"retail-reference-obj");
491 if (
error) { std::cerr <<
"OBJ directory: " <<
error.message() <<
'\n';
return 1; }
494 { std::cerr <<
"OBJ output directory escapes the selected build\n";
return 2; }
501 std::cerr <<
"resource root: " << paths.error().detail <<
'\n';
509 std::cerr <<
"resource runtime: "
510 << initialized.error().detail <<
'\n';
514 auto resources =
runtime.snapshot();
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; }
524 if (!original || !(*original)->renderData) { std::cerr <<
"USA256 original board unavailable\n";
return 1; }
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;
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;
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;
546 const auto&
image =
b.texture->sourceImage;
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; });
553 if (!
identical) { std::cerr <<
"USA256 substitution changed board geometry/UVs or image mapping\n";
return 1; }
557 std::cout.imbue(std::locale::classic());
559 std::cout <<
"retail representative token bounds\n"
560 <<
"token\tslug\tlegacy_data_id\tminimum_xyz\tmaximum_xyz\tsize_xyz\tcenter_xyz\n";
564 const auto asset = legacy.
resolve(
id);
565 if (!asset || !(*asset)->renderData)
567 std::cerr << definition.slug <<
": representative legacy: "
568 << (asset ?
"missing render data" : asset.error().detail) <<
'\n';
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';
585 definition.slug,
id, *(*asset)->renderData))
failed =
true;
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"},
596 const auto id =
packDataId(LegacyGroupId::ThreeD, tag);
597 const auto asset = legacy.
resolve(
id);
598 if (!asset || !(*asset)->renderData)
600 std::cerr <<
name <<
": retail DataId " <<
id <<
": "
601 << (asset ?
"missing render data" : asset.error().detail) <<
'\n';
605 const auto& bounds = (*asset)->renderData->bounds;
606 const auto measured = dimensions(bounds);
607 std::cout <<
name <<
'\t' <<
id <<
'\t';
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";
631 modernRoot / std::filesystem::path(definition.relativeGlbPath)
632 .lexically_relative(
"assets/modern");
641 std::cerr << definition.slug <<
": modern file: " <<
filesystemError.message() <<
'\n';
652 std::cerr << definition.slug <<
": legacy: "
666 std::cerr << definition.slug <<
": modern: "
673 dimensions((*legacyAsset)->renderData->bounds);
675 dimensions((*modernAsset)->bounds);
677 std::array<float, 3> ratio{};
685 std::cout << static_cast<unsigned>(definition.token)
686 <<
'\t' << definition.slug
687 <<
'\t' << definition.unitsPerMeter
688 <<
'\t' << definition.yawDegrees <<
'\t';
702 std::array<std::size_t, ModernTokenCount>
hmdCounts{};
708 LegacyGroupId::ThreeD,
715 auto asset = legacy.
resolve(
id);
716 if (!asset || !(*asset)->mesh)
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);
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;
736 std::cout <<
"\nretail MIMe pose inventory\n"
737 <<
"token\tslug\thmd_count\tpose_counts\tdecode_failures\n";
740 const auto token =
static_cast<std::size_t
>(definition.token);
741 std::cout << static_cast<unsigned>(definition.token)
742 <<
'\t' << definition.slug
747 if (!
first) std::cout <<
',';
758 std::cout <<
"\nretail token sequence mesh inventory\n"
759 <<
"token\tslug\tidle_sequence\tidle_hmds\t"
760 "movement_hmd_count\tmovement_hmds\n";
764 const auto token =
static_cast<std::uint32_t
>(definition.token);
766 static_cast<std::uint32_t
>(
MoveBase) +
781 std::cerr <<
"legacy sequence " <<
sequenceId <<
": " << program.error().detail <<
'\n';
784 for (
const auto& description : (*program)->descriptions())
786 if (!description.contentsDataId)
803 LegacyGroupId::ThreeD,
805 static_cast<std::uint32_t
>(
MoveBase) +
813 LegacyGroupId::ThreeD,
823 for (
const auto tag :
tags)
825 if (!
first) std::cout <<
',';
826 std::cout <<
"0x" << std::hex
827 <<
static_cast<unsigned>(tag)
833 std::cout << static_cast<unsigned>(definition.token)
834 <<
'\t' << definition.slug
835 <<
"\t0x" << std::hex
836 <<
static_cast<unsigned>(
idleTag)
844 return failed || !std::cout ? 1 : 0;
int main(int argc, char **argv)
std::filesystem::path directory
std::deque< Bytes > output
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)
constexpr DataTag dataTag(DataId id) noexcept
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
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::vector< MeshRenderBatch > batches
std::vector< std::uint32_t > indices
std::vector< MeshVertex > vertices
std::array< float, 3 > localOffset
std::array< float, 16 > values
data::DataId contentsDataId
std::shared_ptr< const data::MeshRenderData > renderData