#include "procgen/biome/editing/BiomeTarget.h" #include "procgen/biome/editor/BiomeEditorModule.h" #include "common/Capability.h" #include "common/EditorAutomation.h" #include "editor/Editor.h" #include "procgen/PointSet.h" #include "procgen/SpatialData.h" #include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include using namespace eve::biome_editing; using namespace eve::editing; namespace { SelectionSnapshot select(const BiomeDocumentTarget& target, const ObjectId& id, const std::string& type) { SelectionSnapshot selection; selection.channel = "biome"; selection.items.push_back( {SelectionDomain::Asset, TargetId(target.targetId()), StableId(id.value()), type}); return selection; } void apply(BiomeDocumentTarget& target, EditorResult operation) { REQUIRE(operation.ok()); REQUIRE(target.applyDomainOperation(operation.value()).ok()); } class SpatialResolver final : public IBiomeSpatialResolver { public: EditorResult resolve(const std::string& asset) const override { const auto found = assets.find(asset); if (found == assets.end()) return eve::editing::failed( EditorStatus::NotFound, RuleId("test.biome.spatial"), "missing spatial asset"); return eve::editing::applied(found->second); } std::map assets; }; } // namespace TEST_CASE("editor.biome.properties_are_reversible_and_snapshot_load_is_atomic") { BiomeDocumentTarget target("world.biome"); BiomeLayerValue layer{ObjectId("forest"), "forest", "areas/forest.spatial", 1, 1.f, {}}; apply(target, target.makeCreateLayer(layer)); apply(target, target.makeCreateAsset( layer.id, {ObjectId("oak"), "models/oak.prefab", 2.f, .8f, 1.2f, true})); const auto selection = select(target, ObjectId("oak"), "biome.asset"); const auto scale = target.makeSet(selection, PropertyPath("asset.scale"), EditorValue::Array{.5, 1.5}, PropertySetMode::Absolute); REQUIRE(scale.ok()); REQUIRE(target.applyDomainOperation(scale.value()).ok()); CHECK_EQ(target.layers()[0].assets[0].minScale, .5f); DomainOperation undo = scale.value(); undo.payload = scale.value().inverse; REQUIRE(target.applyDomainOperation(undo).ok()); CHECK_EQ(target.layers()[0].assets[0].minScale, .8f); const EditorValue before = target.snapshotValue(); EditorValue invalid = before; auto* root = invalid.getIf(); auto* content = (*root)["content"].getIf(); auto* layers = (*content)["layers"].getIf(); auto* first = (*layers)[0].getIf(); (*first)["density"] = 2.0; CHECK_EQ(static_cast(target.loadSnapshot(invalid).code()), static_cast(EditorStatus::Rejected)); CHECK_EQ(target.snapshotValue(), before); } TEST_CASE("editor.biome.runtime_publishes_candidates_and_previews_deterministically") { BiomeDocumentTarget target("world.biome"); apply(target, target.makeCreateLayer( {ObjectId("forest"), "forest", "areas/forest.spatial", 1, 1.f, {}})); apply(target, target.makeCreateAsset( ObjectId("forest"), {ObjectId("oak"), "models/oak.prefab", 1.f, 1.f, 1.f, false})); eve::procgen::SpatialData domain = eve::procgen::SpatialData::box(0.f, 0.f, 0.f, 4.f, 0.f, 4.f); SpatialResolver resolver; resolver.assets["areas/forest.spatial"] = &domain; BiomeDocumentRuntime runtime; REQUIRE(runtime.publish(target, resolver).ok()); const Revision published = runtime.revision(); auto first = runtime.preview(&domain, 2.f, 42, 0.f, published); auto second = runtime.preview(&domain, 2.f, 42, 0.f, published); REQUIRE(first.ok()); REQUIRE(second.ok()); REQUIRE(bool(first.value())); REQUIRE(bool(second.value())); CHECK_EQ(first.value()->getCount(), second.value()->getCount()); for (int i = 0; i < first.value()->getCount(); ++i) { CHECK_EQ(first.value()->getStringAttribute(i, "asset", ""), second.value()->getStringAttribute(i, "asset", "")); CHECK_EQ(first.value()->getPointSeed(i), second.value()->getPointSeed(i)); } CHECK_EQ(static_cast(runtime.preview(&domain, 2.f, 42, 0.f, published + 1).code()), static_cast(EditorStatus::Conflict)); SpatialResolver missing; CHECK_EQ(static_cast(runtime.publish(target, missing).code()), static_cast(EditorStatus::NotFound)); CHECK_EQ(runtime.revision(), published); } TEST_CASE("editor.biome.adapter_registers_commands_and_owns_automation_targets") { using eve::editor::Editor; Editor editor; eve::biome_editor::BiomeEditorModule adapter; auto* automation = eve::cap::query(); REQUIRE(automation != nullptr); const std::string commands = automation->invoke("commands", "{}"); CHECK(commands.find("biome.layer.create.v1") != std::string::npos); CHECK(commands.find("biome.asset.create.v1") != std::string::npos); CHECK(commands.find("biome.property.set.v1") != std::string::npos); const std::string created = automation->invoke( "target-create", R"({"target":"agent.biome","type":"biome","layer":"forest","spatial":"areas/forest.spatial"})"); REQUIRE(created.find("\"status\":\"applied\"") != std::string::npos); const std::string asset = automation->invoke( "execute", R"({"target":"agent.biome","command":"biome.asset.create.v1","payload":{"layer":"forest","id":"oak","asset":"models/oak.prefab","weight":2,"minScale":0.8,"maxScale":1.2}})"); REQUIRE(asset.find("\"status\":\"applied\"") != std::string::npos); const std::string density = automation->invoke( "execute", R"({"target":"agent.biome","command":"biome.property.set.v1","payload":{"item":"forest","type":"biome.layer","path":"layer.density","value":0.5}})"); REQUIRE(density.find("\"status\":\"applied\"") != std::string::npos); const std::string inspected = automation->invoke("inspect", R"({"target":"agent.biome"})"); CHECK(inspected.find("forest") != std::string::npos); CHECK(inspected.find("oak") != std::string::npos); CHECK(inspected.find("0.5") != std::string::npos); const std::string undone = automation->invoke("undo", R"({"target":"agent.biome"})"); CHECK(undone.find("\"status\":\"applied\"") != std::string::npos); const std::string afterUndo = automation->invoke("inspect", R"({"target":"agent.biome"})"); CHECK(afterUndo.find("0.5") == std::string::npos); const std::string closed = automation->invoke("target-close", R"({"target":"agent.biome"})"); CHECK(closed.find("\"status\":\"applied\"") != std::string::npos); }