Files
voxelforge/tests/testAdvancement.cpp
T
2026-09-10 17:57:13 +02:00

316 lines
13 KiB
C++

#include "testHarness.h"
#include "vf/Advancement.h"
#include "vf/Scoreboard.h"
#include <string>
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<DataPackError>& 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<DataPackError> errors;
AdvancementTree tree = loadTree(contentPack(), errors);
CHECK(errors.empty());
CHECK_EQ(tree.size(), static_cast<std::size_t>(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<std::size_t>(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<std::size_t>(2));
CHECK_EQ(tools->findCriterion("sticks")->requiredCount, 4);
CHECK(tools->findCriterion("absent") == nullptr);
CHECK_EQ(tools->reward.recipes.size(), static_cast<std::size_t>(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<std::size_t>(2));
CHECK_EQ(tree.roots().size(), static_cast<std::size_t>(1));
CHECK(tree.roots()[0] == id("voxelforge:root"));
CHECK_EQ(tree.childrenOf(id("voxelforge:root")).size(), static_cast<std::size_t>(2));
CHECK_EQ(tree.childrenOf(id("voxelforge:tools")).size(), static_cast<std::size_t>(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<std::size_t>(4));
}
VF_TEST(anAdvancementIsGrantedOnlyWhenItsLastCriterionCompletes) {
std::vector<DataPackError> 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<AdvancementId> 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<std::size_t>(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<std::size_t>(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<DataPackError> 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<AdvancementId> 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<std::size_t>(1));
if (!granted.empty()) CHECK(granted[0] == id("voxelforge:traveller"));
}
VF_TEST(countedCriteriaNeedEveryOneOfTheirEvents) {
std::vector<DataPackError> 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<AdvancementId> granted = progress.notify(Event::mobKilled(Species::Zombie), 4);
CHECK_EQ(granted.size(), static_cast<std::size_t>(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<std::size_t>(1));
CHECK(progress.awardedLootTables()[0] == id("voxelforge:chests/starter"));
CHECK(progress.unlockedRecipes().empty());
}
VF_TEST(hiddenAdvancementsStayOutOfSightUntilTheParentIsGranted) {
std::vector<DataPackError> 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<std::size_t>(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<AdvancementId> granted = progress.notify(Event::itemObtained(ItemId::Dirt), 1);
CHECK_EQ(granted.size(), static_cast<std::size_t>(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<std::size_t>(5));
tree.clear();
CHECK_EQ(tree.size(), static_cast<std::size_t>(0));
}