293 lines
12 KiB
C++
293 lines
12 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "procgen/GeneratedArtifact.h"
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
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<int>(artifact.type), static_cast<int>(ArtifactType::Composite));
|
|
CHECK(!artifact.buildKey.empty());
|
|
CHECK(artifact.bounds.isValid());
|
|
|
|
const auto& composite = std::get<CompositeArtifact>(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<int>(mesh->type), static_cast<int>(ArtifactType::MeshData));
|
|
CHECK(std::get<MeshData>(mesh->payload).getVertexCount() > 0);
|
|
CHECK(std::get<Collider>(collider->payload).isValid());
|
|
CHECK_EQ(std::get<Grid2D>(topology->payload).getWidth(), 8);
|
|
CHECK_EQ(std::get<PointSet>(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<CompositeArtifact>(artifact.payload);
|
|
REQUIRE(composite.has("mesh"));
|
|
REQUIRE(composite.has("collider"));
|
|
REQUIRE(composite.has("topology"));
|
|
REQUIRE(composite.has("anchors"));
|
|
CHECK(std::get<MeshData>(composite.find("mesh")->payload).getIndexCount() > 0);
|
|
CHECK(std::get<Collider>(composite.find("collider")->payload).isValid());
|
|
CHECK_EQ(std::get<Grid2D>(composite.find("topology")->payload).getWidth(), 2);
|
|
CHECK_EQ(std::get<PointSet>(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<int>(conflict.code()), static_cast<int>(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<int>(missing.code()), static_cast<int>(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<int>(nil.code()), static_cast<int>(eve::StatusCode::Rejected));
|
|
|
|
auto mismatch = makeArtifact(id, ArtifactType::MeshData, eve::SchemaVersion(1), *key, bounds, {}, {},
|
|
GeneratedArtifact::Payload(grid));
|
|
CHECK(!mismatch.ok());
|
|
CHECK_EQ(static_cast<int>(mismatch.code()), static_cast<int>(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<int>(invalid.error()->code()), static_cast<int>(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<ArtifactId> dependencies = {}) {
|
|
return makeArtifactPart(std::move(role), id, type, eve::SchemaVersion(1), *key, bounds, std::move(dependencies),
|
|
{}, std::move(payload));
|
|
};
|
|
std::vector<ArtifactPart> 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<int>(*restored.typeOf(root.child("mesh"))), static_cast<int>(ArtifactType::MeshData));
|
|
const auto* restoredMesh = restored.findPart(root.child("mesh"));
|
|
REQUIRE(restoredMesh != nullptr);
|
|
CHECK_EQ(std::get<MeshData>(restoredMesh->payload).groupNames(), std::vector<std::string>{"main"});
|
|
const auto* restoredPointsPart = restored.findPart(root.child("points"));
|
|
REQUIRE(restoredPointsPart != nullptr);
|
|
const auto& restoredPoints = std::get<PointSet>(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);
|
|
}
|