#include "testHarness.h" #include "vf/Advancement.h" #include "vf/Scoreboard.h" #include using namespace vf; namespace { using Event = AdvancementCriterion::Event; using Trigger = AdvancementCriterion::Trigger; ResourceId id(const std::string& text) { ResourceId parsed; ResourceId::parse(text, "voxelforge", parsed); return parsed; } const char* const rootAdvancement = R"json({ "display": {"title": "Getting started", "description": "Chop something down"}, "criteria": { "anyWood": {"trigger": "itemObtained", "item": "voxelforge:wood"} }, "rewards": {"experience": 5} })json"; const char* const toolsAdvancement = R"json({ "parent": "voxelforge:root", "display": {"title": "A workbench of your own"}, "criteria": { "sticks": {"trigger": "itemObtained", "item": "voxelforge:stick", "count": 4}, "table": {"trigger": "itemObtained", "item": "voxelforge:crafting_table"} }, "rewards": {"experience": 10, "recipes": ["voxelforge:redstone_torch"]} })json"; const char* const hunterAdvancement = R"json({ "parent": "voxelforge:tools", "display": {"title": "Monster hunter", "hidden": true}, "criteria": { "zombies": {"trigger": "mobKilled", "species": "zombie", "count": 3}, "creeper": {"trigger": "mobKilled", "species": "creeper"} }, "requirements": [["zombies", "creeper"]], "rewards": {"experience": 20, "loot": ["voxelforge:chests/starter"]} })json"; const char* const travellerAdvancement = R"json({ "parent": "voxelforge:root", "display": {"title": "Into the fire"}, "criteria": { "portal": {"trigger": "dimensionEntered", "dimension": "nether"}, "brew": {"trigger": "potionBrewed", "potion": "healing"}, "trade": {"trigger": "villagerTraded", "profession": "librarian", "item": "voxelforge:emerald_gem"}, "mine": {"trigger": "blockBroken", "block": "voxelforge:obsidian", "tool": "voxelforge:diamond_pickaxe"} }, "requirements": [["portal"], ["brew", "trade", "mine"]] })json"; DataPack contentPack() { DataPackMeta meta; meta.name = "story"; DataPack pack(meta); pack.addFile("data/voxelforge/advancements/root.json", rootAdvancement); pack.addFile("data/voxelforge/advancements/tools.json", toolsAdvancement); pack.addFile("data/voxelforge/advancements/hunter.json", hunterAdvancement); pack.addFile("data/voxelforge/advancements/traveller.json", travellerAdvancement); return pack; } AdvancementTree loadTree(const DataPack& pack, std::vector& errors) { DataPackLoader loader; loader.addPack(pack); loader.load(); return loadAdvancements(loader, errors); } Advancement makeAdvancement(const std::string& self, const std::string& parent) { Advancement node; node.id = id(self); if (!parent.empty()) node.parent = id(parent); AdvancementCriterion criterion; criterion.name = "tick"; criterion.trigger = Trigger::ItemObtained; node.criteria.push_back(criterion); return node; } } // namespace VF_TEST(advancementsLoadOutOfAPackAndKeepTheirTreeShape) { std::vector errors; AdvancementTree tree = loadTree(contentPack(), errors); CHECK(errors.empty()); CHECK_EQ(tree.size(), static_cast(4)); const Advancement* root = tree.find(id("voxelforge:root")); CHECK(root != nullptr); CHECK(root->isRoot()); CHECK_EQ(root->title, std::string("Getting started")); CHECK_EQ(root->description, std::string("Chop something down")); CHECK_EQ(root->criteria.size(), static_cast(1)); CHECK(root->criteria[0].item == ItemId::Wood); CHECK_EQ(root->criteria[0].matchMask, AdvancementCriterion::MatchItem); CHECK_EQ(root->reward.experience, 5); const Advancement* tools = tree.find(id("voxelforge:tools")); CHECK(tools != nullptr); CHECK(!tools->isRoot()); CHECK(tools->parent == id("voxelforge:root")); CHECK_EQ(tools->criteria.size(), static_cast(2)); CHECK_EQ(tools->findCriterion("sticks")->requiredCount, 4); CHECK(tools->findCriterion("absent") == nullptr); CHECK_EQ(tools->reward.recipes.size(), static_cast(1)); CHECK(tools->reward.recipes[0] == id("voxelforge:redstone_torch")); const Advancement* traveller = tree.find(id("voxelforge:traveller")); CHECK(traveller->findCriterion("portal")->dimension == DimensionId::Nether); CHECK(traveller->findCriterion("brew")->potion == PotionKind::Healing); CHECK(traveller->findCriterion("trade")->profession == VillagerProfession::Librarian); CHECK(traveller->findCriterion("trade")->item == ItemId::EmeraldGem); CHECK(traveller->findCriterion("mine")->block == Block::Obsidian); CHECK(traveller->findCriterion("mine")->tool == ItemId::DiamondPickaxe); CHECK_EQ(traveller->requirements.size(), static_cast(2)); CHECK_EQ(tree.roots().size(), static_cast(1)); CHECK(tree.roots()[0] == id("voxelforge:root")); CHECK_EQ(tree.childrenOf(id("voxelforge:root")).size(), static_cast(2)); CHECK_EQ(tree.childrenOf(id("voxelforge:tools")).size(), static_cast(1)); CHECK_EQ(tree.depthOf(id("voxelforge:root")), 0); CHECK_EQ(tree.depthOf(id("voxelforge:tools")), 1); CHECK_EQ(tree.depthOf(id("voxelforge:hunter")), 2); CHECK_EQ(tree.depthOf(id("voxelforge:absent")), -1); CHECK(!tree.hasCycle()); CHECK(tree.missingParents().empty()); CHECK_EQ(tree.ids().size(), static_cast(4)); } VF_TEST(anAdvancementIsGrantedOnlyWhenItsLastCriterionCompletes) { std::vector errors; AdvancementTree tree = loadTree(contentPack(), errors); AdvancementProgress progress(tree); const AdvancementId tools = id("voxelforge:tools"); CHECK(!progress.granted(tools)); CHECK_EQ(progress.grantedCount(), 0); std::vector granted = progress.notify(Event::itemObtained(ItemId::Stick, 2), 100); CHECK(granted.empty()); CHECK(!progress.criterionDone(tools, "sticks")); CHECK_EQ(progress.criterionProgress(tools, "sticks"), 2); CHECK_EQ(progress.criterionTick(tools, "sticks"), -1); granted = progress.notify(Event::itemObtained(ItemId::Stick, 2), 140); CHECK(granted.empty()); CHECK(progress.criterionDone(tools, "sticks")); CHECK_EQ(progress.criterionProgress(tools, "sticks"), 4); CHECK_EQ(progress.criterionTick(tools, "sticks"), 140); CHECK(!progress.granted(tools)); // An unrelated item moves nothing. granted = progress.notify(Event::itemObtained(ItemId::Dirt, 64), 150); CHECK(granted.empty()); CHECK(!progress.granted(tools)); granted = progress.notify(Event::itemObtained(ItemId::CraftingTable), 200); CHECK_EQ(granted.size(), static_cast(1)); if (!granted.empty()) CHECK(granted[0] == tools); CHECK(progress.granted(tools)); CHECK_EQ(progress.grantedTick(tools), 200); CHECK_EQ(progress.completedCriteria(tools).size(), static_cast(2)); CHECK_EQ(progress.totalExperience(), 10); // Granting is a one shot: repeating the last event awards nothing again. granted = progress.notify(Event::itemObtained(ItemId::CraftingTable), 260); CHECK(granted.empty()); CHECK_EQ(progress.grantedTick(tools), 200); CHECK_EQ(progress.totalExperience(), 10); CHECK_EQ(progress.grantedCount(), 1); } VF_TEST(requirementGroupsAreSatisfiedByAnyOneOfTheirCriteria) { std::vector errors; AdvancementTree tree = loadTree(contentPack(), errors); const Advancement* hunter = tree.find(id("voxelforge:hunter")); CHECK(hunter != nullptr); CHECK(!hunter->satisfiedBy({})); CHECK(hunter->satisfiedBy({"creeper"})); CHECK(hunter->satisfiedBy({"zombies"})); CHECK(hunter->danglingRequirements().empty()); // Without requirement groups every criterion is needed. const Advancement* tools = tree.find(id("voxelforge:tools")); CHECK(tools->requirements.empty()); CHECK(!tools->satisfiedBy({"sticks"})); CHECK(tools->satisfiedBy({"sticks", "table"})); const Advancement* traveller = tree.find(id("voxelforge:traveller")); CHECK(!traveller->satisfiedBy({"portal"})); CHECK(!traveller->satisfiedBy({"brew", "trade", "mine"})); CHECK(traveller->satisfiedBy({"portal", "mine"})); AdvancementProgress progress(tree); std::vector granted = progress.notify(Event::dimensionEntered(DimensionId::Nether), 10); CHECK(granted.empty()); granted = progress.notify(Event::dimensionEntered(DimensionId::End), 20); CHECK(granted.empty()); granted = progress.notify(Event::villagerTraded(VillagerProfession::Farmer, ItemId::EmeraldGem), 30); CHECK(granted.empty()); granted = progress.notify(Event::villagerTraded(VillagerProfession::Librarian, ItemId::EmeraldGem), 40); CHECK_EQ(granted.size(), static_cast(1)); if (!granted.empty()) CHECK(granted[0] == id("voxelforge:traveller")); } VF_TEST(countedCriteriaNeedEveryOneOfTheirEvents) { std::vector errors; AdvancementTree tree = loadTree(contentPack(), errors); AdvancementProgress progress(tree); const AdvancementId hunter = id("voxelforge:hunter"); progress.notify(Event::mobKilled(Species::Zombie), 1); progress.notify(Event::mobKilled(Species::Skeleton), 2); CHECK_EQ(progress.criterionProgress(hunter, "zombies"), 1); progress.notify(Event::mobKilled(Species::Zombie), 3); CHECK_EQ(progress.criterionProgress(hunter, "zombies"), 2); CHECK(!progress.granted(hunter)); std::vector granted = progress.notify(Event::mobKilled(Species::Zombie), 4); CHECK_EQ(granted.size(), static_cast(1)); if (!granted.empty()) CHECK(granted[0] == hunter); CHECK(progress.criterionDone(hunter, "zombies")); CHECK(!progress.criterionDone(hunter, "creeper")); CHECK_EQ(progress.grantedTick(hunter), 4); CHECK_EQ(progress.totalExperience(), 20); CHECK_EQ(progress.awardedLootTables().size(), static_cast(1)); CHECK(progress.awardedLootTables()[0] == id("voxelforge:chests/starter")); CHECK(progress.unlockedRecipes().empty()); } VF_TEST(hiddenAdvancementsStayOutOfSightUntilTheParentIsGranted) { std::vector errors; AdvancementTree tree = loadTree(contentPack(), errors); AdvancementProgress progress(tree); const AdvancementId hunter = id("voxelforge:hunter"); const AdvancementId tools = id("voxelforge:tools"); CHECK(progress.visible(id("voxelforge:root"))); CHECK(progress.visible(tools)); CHECK(!progress.visible(hunter)); progress.notify(Event::itemObtained(ItemId::Stick, 4), 10); CHECK(!progress.visible(hunter)); progress.notify(Event::itemObtained(ItemId::CraftingTable), 20); CHECK(progress.granted(tools)); CHECK(progress.visible(hunter)); // A hidden advancement is also its own reveal once it is earned. AdvancementTree lone; Advancement secret = makeAdvancement("voxelforge:secret", ""); secret.hidden = true; secret.criteria[0].matchMask = AdvancementCriterion::MatchItem; secret.criteria[0].item = ItemId::DiamondGem; CHECK(lone.add(secret)); AdvancementProgress hidden(lone); CHECK(!hidden.visible(secret.id)); hidden.notify(Event::itemObtained(ItemId::DiamondGem), 5); CHECK(hidden.granted(secret.id)); CHECK(hidden.visible(secret.id)); CHECK(!hidden.visible(id("voxelforge:nobody"))); } VF_TEST(aParentLoopIsReportedInsteadOfWalkedForever) { AdvancementTree tree; CHECK(tree.add(makeAdvancement("voxelforge:first", "voxelforge:third"))); CHECK(tree.add(makeAdvancement("voxelforge:second", "voxelforge:first"))); CHECK(tree.add(makeAdvancement("voxelforge:third", "voxelforge:second"))); CHECK(tree.add(makeAdvancement("voxelforge:selfish", "voxelforge:selfish"))); CHECK(tree.add(makeAdvancement("voxelforge:orphan", "voxelforge:nowhere"))); CHECK(tree.hasCycle()); CHECK(tree.cycleFrom(id("voxelforge:first"))); CHECK(tree.cycleFrom(id("voxelforge:second"))); CHECK(tree.cycleFrom(id("voxelforge:selfish"))); CHECK(!tree.cycleFrom(id("voxelforge:orphan"))); CHECK_EQ(tree.depthOf(id("voxelforge:first")), -1); CHECK_EQ(tree.depthOf(id("voxelforge:selfish")), -1); CHECK_EQ(tree.depthOf(id("voxelforge:orphan")), -1); CHECK_EQ(tree.missingParents().size(), static_cast(1)); CHECK(tree.missingParents()[0] == id("voxelforge:orphan")); CHECK(tree.roots().empty()); // A looping node still answers questions instead of hanging the caller. AdvancementProgress progress(tree); CHECK(progress.visible(id("voxelforge:first"))); std::vector granted = progress.notify(Event::itemObtained(ItemId::Dirt), 1); CHECK_EQ(granted.size(), static_cast(5)); CHECK(progress.granted(id("voxelforge:first"))); // Adding the same id twice is refused, so a duplicate cannot rewrite a node. CHECK(!tree.add(makeAdvancement("voxelforge:first", "voxelforge:third"))); Advancement nameless; CHECK(!tree.add(nameless)); CHECK_EQ(tree.size(), static_cast(5)); tree.clear(); CHECK_EQ(tree.size(), static_cast(0)); }