#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "procgen/GeneratedArtifact.h" #include #include #include #include using namespace eve::procgen; namespace { ArtifactId testId() { const auto parsed = ArtifactId::parse("018f0b7e-6e50-7a10-8c22-2c8f8e3dd001"); REQUIRE(parsed.has_value()); return *parsed; } } // namespace TEST_CASE("procgen.generatedArtifact.buildKeyIsCanonicalAndIdentityIsSeparate") { Params first; first.setSeed(17); first.setInt("width", 48); first.setFloat("radius", .75f); first.setBool("decorations", true); Params second; second.setBool("decorations", true); second.setFloat("radius", .75f); second.setInt("width", 48); second.setSeed(17); const auto firstKey = BuildKey::forRecipe("mesh.hexterrain", first); const auto secondKey = BuildKey::forRecipe("mesh.hexterrain", second); REQUIRE(firstKey.has_value()); REQUIRE(secondKey.has_value()); CHECK_EQ(firstKey->format(), secondKey->format()); second.setSeed(18); const auto changedKey = BuildKey::forRecipe("mesh.hexterrain", second); REQUIRE(changedKey.has_value()); CHECK_NE(firstKey->format(), changedKey->format()); const ArtifactId root = testId(); CHECK(!root.isNil()); CHECK(root.child("mesh") != root); CHECK(root.child("mesh") != root.child("collider")); CHECK(root.child("").isNil()); } TEST_CASE("procgen.generatedArtifact.hexTerrainPublishesCompositeProducts") { Params params; params.setInt("width", 8); params.setInt("height", 7); params.setInt("riverCount", 2); params.setInt("seed", 91); auto result = generateHexTerrainArtifact(params, testId()); REQUIRE(result.ok()); GeneratedArtifact artifact = std::move(result).takeValue(); CHECK_EQ(static_cast(artifact.type), static_cast(ArtifactType::Composite)); CHECK(!artifact.buildKey.empty()); CHECK(artifact.bounds.isValid()); const auto& composite = std::get(artifact.payload); REQUIRE_EQ(composite.children.size(), std::size_t(4)); const ArtifactPart* mesh = composite.find("mesh"); const ArtifactPart* collider = composite.find("collider"); const ArtifactPart* topology = composite.find("topology"); const ArtifactPart* anchors = composite.find("anchors"); REQUIRE(mesh != nullptr); REQUIRE(collider != nullptr); REQUIRE(topology != nullptr); REQUIRE(anchors != nullptr); CHECK_EQ(static_cast(mesh->type), static_cast(ArtifactType::MeshData)); CHECK(std::get(mesh->payload).getVertexCount() > 0); CHECK(std::get(collider->payload).isValid()); CHECK_EQ(std::get(topology->payload).getWidth(), 8); CHECK_EQ(std::get(anchors->payload).getCount(), 4); } TEST_CASE("procgen.generatedArtifact.castlePublishesCompositeProductsAndStoreProtectsIdentity") { Params params; params.setInt("rings", 2); params.setInt("keepFloors", 2); params.setInt("detail", 1); params.setInt("seed", 12); const ArtifactId id = testId(); auto result = generateCastleArtifact(params, id); REQUIRE(result.ok()); GeneratedArtifact artifact = std::move(result).takeValue(); const BuildKey key = artifact.buildKey; const auto& composite = std::get(artifact.payload); REQUIRE(composite.has("mesh")); REQUIRE(composite.has("collider")); REQUIRE(composite.has("topology")); REQUIRE(composite.has("anchors")); CHECK(std::get(composite.find("mesh")->payload).getIndexCount() > 0); CHECK(std::get(composite.find("collider")->payload).isValid()); CHECK_EQ(std::get(composite.find("topology")->payload).getWidth(), 2); CHECK_EQ(std::get(composite.find("anchors")->payload).getCount(), 5); ArtifactStore store; auto published = store.publish(std::move(artifact)); REQUIRE(published.ok()); CHECK_EQ(std::move(published).takeValue(), id); CHECK_EQ(store.size(), std::size_t(1)); CHECK(store.find(id) != nullptr); CHECK_EQ(store.findByBuildKey(key).size(), std::size_t(1)); Params sameParams = params; auto duplicate = generateCastleArtifact(sameParams, id); REQUIRE(duplicate.ok()); auto conflict = store.publish(std::move(duplicate).takeValue()); CHECK(!conflict.ok()); CHECK_EQ(static_cast(conflict.code()), static_cast(eve::StatusCode::Conflict)); auto removed = store.remove(id); CHECK(removed.ok()); CHECK(store.find(id) == nullptr); auto missing = store.remove(id); CHECK(!missing.ok()); CHECK_EQ(static_cast(missing.code()), static_cast(eve::StatusCode::NotFound)); } TEST_CASE("procgen.generatedArtifact.rejectsInvalidContracts") { const ArtifactId id = testId(); auto key = BuildKey::fromCanonical("procgen/v1/test/0000000000000001"); REQUIRE(key.has_value()); Bounds bounds; bounds.include(0.f, 0.f, 0.f); Grid2D grid; grid.resize(1, 1); auto nil = makeArtifact(ArtifactId::nil(), ArtifactType::Grid, eve::SchemaVersion(1), *key, bounds, {}, {}, GeneratedArtifact::Payload(grid)); CHECK(!nil.ok()); CHECK_EQ(static_cast(nil.code()), static_cast(eve::StatusCode::Rejected)); auto mismatch = makeArtifact(id, ArtifactType::MeshData, eve::SchemaVersion(1), *key, bounds, {}, {}, GeneratedArtifact::Payload(grid)); CHECK(!mismatch.ok()); CHECK_EQ(static_cast(mismatch.code()), static_cast(eve::StatusCode::Rejected)); } TEST_CASE("procgen.meshBuild.checkedGroupRestoreIsAtomic") { MeshBuild mesh; mesh.addVertex(0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f); mesh.addVertex(1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f); mesh.addVertex(0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f); mesh.addTriangle(0, 1, 2); auto valid = mesh.restoreGroupData({"main"}, {0}, 0); REQUIRE(valid.ok()); CHECK_EQ(mesh.getGroupCount(), 1); CHECK_EQ(mesh.activeGroup(), 0); auto invalid = mesh.restoreGroupData({""}, {0}, 0); REQUIRE(!invalid.ok()); CHECK_EQ(static_cast(invalid.error()->code()), static_cast(eve::DiagnosticCode::ProcgenGroupDataInvalid)); CHECK_EQ(mesh.getGroupName(0), std::string("main")); CHECK_EQ(mesh.getTriangleGroup(0), 0); } TEST_CASE("procgen.generatedArtifact.denseSnapshotRoundTripsEveryPayloadAndRebuildsIndexes") { const ArtifactId root = testId(); const auto key = BuildKey::fromCanonical("procgen/v1/roundtrip/0000000000000001"); REQUIRE(key.has_value()); Bounds rootBounds; rootBounds.include(0.f, 0.f, 0.f); rootBounds.include(4.f, 3.f, 2.f); Grid2D grid; grid.resize(2, 1); grid.setCell(0, 0, 7); grid.setCell(1, 0, 9); grid.setDetail(1, 0, 3); grid.setMeta("theme", "test"); grid.addObject("spawn", "unit", 1.f, 2.f, 3.f, 4.f, 5); Bounds gridBounds; gridBounds.include(0.f, 0.f, 0.f); gridBounds.include(2.f, 0.f, 1.f); PointSet points; const int point = points.add(1.f, 2.f, 3.f); points.setNormal(point, .1f, .9f, .2f); points.setRotation(point, .15f, .25f, .35f); points.setScale(point, 2.f, 3.f, 4.f); points.setBounds(point, -1.f, -2.f, -3.f, 1.f, 2.f, 3.f); points.setColor(point, .1f, .2f, .3f, .4f); points.setSteepness(point, .65f); points.setDensity(point, .75f); points.setPointSeed(point, 123u); points.trySetPointId(point, 18446744073709551614ull).expect("artifact snapshot point id"); points.setFloatAttribute(point, "weight", 1.5f); points.setIntAttribute(point, "variant", 4); points.setBoolAttribute(point, "enabled", true); points.setVectorAttribute(point, "wind", 5.f, 6.f, 7.f); points.setStringAttribute(point, "tag", "anchor"); Bounds pointBounds; pointBounds.include(1.f, 2.f, 3.f); MeshData mesh; mesh.setActiveGroup("main"); mesh.addVertex(0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f); mesh.addVertex(1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f); mesh.addVertex(0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f); mesh.addTriangle(0, 1, 2); mesh.setMeta("material", "debug"); Bounds meshBounds; meshBounds.include(0.f, 0.f, 0.f); meshBounds.include(1.f, 1.f, 0.f); ImageData image; image.width = 2; image.height = 1; image.channels = 4; image.format = "rgba8"; image.pixels = {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}; Bounds imageBounds; imageBounds.include(0.f, 0.f, 0.f); imageBounds.include(2.f, 1.f, 0.f); Collider collider; collider.vertices = mesh.positions(); collider.indices = mesh.indices(); collider.bounds = meshBounds; collider.shape = "triangle_mesh"; auto makePart = [&](std::string role, ArtifactId id, ArtifactType type, Bounds bounds, ArtifactLeafPayload payload, std::vector dependencies = {}) { return makeArtifactPart(std::move(role), id, type, eve::SchemaVersion(1), *key, bounds, std::move(dependencies), {}, std::move(payload)); }; std::vector children; auto addPart = [&](auto result) { REQUIRE(result.ok()); children.emplace_back(std::move(result).takeValue()); }; addPart(makePart("grid", root.child("grid"), ArtifactType::Grid, gridBounds, std::move(grid))); addPart(makePart("points", root.child("points"), ArtifactType::PointSet, pointBounds, std::move(points))); addPart(makePart("mesh", root.child("mesh"), ArtifactType::MeshData, meshBounds, std::move(mesh))); addPart(makePart("image", root.child("image"), ArtifactType::ImageData, imageBounds, std::move(image))); addPart(makePart("collider", root.child("collider"), ArtifactType::Collider, meshBounds, std::move(collider), {root.child("mesh")})); CompositeArtifact composite; composite.children = std::move(children); auto generated = makeArtifact(root, ArtifactType::Composite, eve::SchemaVersion(1), *key, rootBounds, {}, {}, std::move(composite)); REQUIRE(generated.ok()); ArtifactStore source; REQUIRE(source.publish(std::move(generated).takeValue()).ok()); auto snapshot = source.snapshotState(); REQUIRE(snapshot.ok()); ArtifactStore restored; REQUIRE(restored.restoreState(snapshot.value()).ok()); auto restoredSnapshot = restored.snapshotState(); REQUIRE(restoredSnapshot.ok()); CHECK_EQ(restoredSnapshot.value(), snapshot.value()); CHECK(restored.find(root) != nullptr); CHECK_EQ(restored.partCount(), std::size_t(5)); CHECK(restored.findPart(root.child("grid")) != nullptr); CHECK(restored.findPart(root.child("points")) != nullptr); CHECK(restored.findPart(root.child("mesh")) != nullptr); CHECK(restored.findPart(root.child("image")) != nullptr); CHECK(restored.findPart(root.child("collider")) != nullptr); REQUIRE(restored.typeOf(root.child("mesh")).has_value()); CHECK_EQ(static_cast(*restored.typeOf(root.child("mesh"))), static_cast(ArtifactType::MeshData)); const auto* restoredMesh = restored.findPart(root.child("mesh")); REQUIRE(restoredMesh != nullptr); CHECK_EQ(std::get(restoredMesh->payload).groupNames(), std::vector{"main"}); const auto* restoredPointsPart = restored.findPart(root.child("points")); REQUIRE(restoredPointsPart != nullptr); const auto& restoredPoints = std::get(restoredPointsPart->payload); CHECK_EQ(restoredPoints.getPitch(0), .15f); CHECK_EQ(restoredPoints.getRoll(0), .35f); CHECK_EQ(restoredPoints.getBoundsMinY(0), -2.f); CHECK_EQ(restoredPoints.getColorA(0), .4f); CHECK_EQ(restoredPoints.getSteepness(0), .65f); CHECK_EQ(restoredPoints.getPointId(0), 18446744073709551614ull); CHECK_EQ(restoredPoints.getIntAttribute(0, "variant", -1), 4); CHECK(restoredPoints.getBoolAttribute(0, "enabled", false)); CHECK_EQ(restoredPoints.getVectorAttributeZ(0, "wind", -1.f), 7.f); }