diff --git a/tests/testAdvancement.cpp b/tests/testAdvancement.cpp index 6c33626..35448c8 100644 --- a/tests/testAdvancement.cpp +++ b/tests/testAdvancement.cpp @@ -313,3 +313,162 @@ VF_TEST(aParentLoopIsReportedInsteadOfWalkedForever) { CHECK_EQ(tree.size(), static_cast(0)); } +VF_TEST(criteriaOnlyAcceptTheEventsTheyDescribe) { + AdvancementCriterion anyKill; + anyKill.trigger = Trigger::MobKilled; + CHECK(anyKill.accepts(Event::mobKilled(Species::Pig))); + CHECK(anyKill.accepts(Event::mobKilled(Species::Creeper))); + CHECK(!anyKill.accepts(Event::itemObtained(ItemId::Stick))); + + AdvancementCriterion creepersOnly = anyKill; + creepersOnly.matchMask = AdvancementCriterion::MatchSpecies; + creepersOnly.species = Species::Creeper; + CHECK(creepersOnly.accepts(Event::mobKilled(Species::Creeper))); + CHECK(!creepersOnly.accepts(Event::mobKilled(Species::Pig))); + + AdvancementCriterion minedWithIron; + minedWithIron.trigger = Trigger::BlockBroken; + minedWithIron.matchMask = AdvancementCriterion::MatchBlock | AdvancementCriterion::MatchTool; + minedWithIron.block = Block::DiamondOre; + minedWithIron.tool = ItemId::IronPickaxe; + CHECK(minedWithIron.accepts(Event::blockBroken(Block::DiamondOre, ItemId::IronPickaxe))); + CHECK(!minedWithIron.accepts(Event::blockBroken(Block::DiamondOre, ItemId::StonePickaxe))); + CHECK(!minedWithIron.accepts(Event::blockBroken(Block::CoalOre, ItemId::IronPickaxe))); + + AdvancementCriterion brewed; + brewed.trigger = Trigger::PotionBrewed; + brewed.matchMask = AdvancementCriterion::MatchPotion; + brewed.potion = PotionKind::Swiftness; + CHECK(brewed.accepts(Event::potionBrewed(PotionKind::Swiftness))); + CHECK(!brewed.accepts(Event::potionBrewed(PotionKind::Healing))); + CHECK(!brewed.accepts(Event::dimensionEntered(DimensionId::End))); + + for (int i = 0; i < static_cast(Trigger::Count); ++i) { + Trigger trigger = static_cast(i); + Trigger reparsed = Trigger::Count; + CHECK(parseAdvancementTriggerName(advancementTriggerName(trigger), reparsed)); + CHECK(reparsed == trigger); + } + Trigger unknown = Trigger::Count; + CHECK(!parseAdvancementTriggerName("telekinesis", unknown)); +} + +VF_TEST(oneUnreadableAdvancementDoesNotCostTheRest) { + DataPack pack = contentPack(); + pack.addFile("data/voxelforge/advancements/broken.json", + R"json({"criteria": {"x": {"trigger": "telekinesis"}}})json"); + pack.addFile("data/voxelforge/advancements/empty.json", R"json({"criteria": {}})json"); + pack.addFile("data/voxelforge/advancements/ghost.json", + R"json({"criteria": {"x": {"trigger": "itemObtained", + "item": "voxelforge:cheese"}}})json"); + pack.addFile("data/voxelforge/advancements/mismatch.json", + R"json({"criteria": {"x": {"trigger": "itemObtained"}}, + "requirements": [["y"]]})json"); + + std::vector errors; + AdvancementTree tree = loadTree(pack, errors); + CHECK_EQ(errors.size(), static_cast(4)); + CHECK_EQ(tree.size(), static_cast(4)); + CHECK(tree.find(id("voxelforge:root")) != nullptr); + CHECK(tree.find(id("voxelforge:broken")) == nullptr); + for (const DataPackError& failure : errors) { + CHECK(failure.file.find("data/voxelforge/advancements/") == 0u); + CHECK(!failure.reason.empty()); + } +} + +VF_TEST(advancementDocumentsAreCheckedFieldByField) { + Advancement parsed; + std::string problem; + const char* const rejected[] = { + R"json([])json", + R"json({})json", + R"json({"criteria": {}})json", + R"json({"criteria": {"x": {}}})json", + R"json({"criteria": {"x": {"trigger": "itemObtained", "count": 0}}})json", + R"json({"criteria": {"x": {"trigger": "itemObtained", "item": "NOT AN ID"}}})json", + R"json({"criteria": {"x": {"trigger": "mobKilled", "species": "dragon"}}})json", + R"json({"criteria": {"x": {"trigger": "dimensionEntered", "dimension": "attic"}}})json", + R"json({"criteria": {"x": {"trigger": "potionBrewed", "potion": "coffee"}}})json", + R"json({"criteria": {"x": {"trigger": "villagerTraded", "profession": "banker"}}})json", + R"json({"criteria": {"x": {"trigger": "blockBroken", "block": "voxelforge:cheese"}}})json", + R"json({"parent": "BAD ID", "criteria": {"x": {"trigger": "itemObtained"}}})json", + R"json({"criteria": {"x": {"trigger": "itemObtained"}}, "requirements": [[]]})json", + R"json({"criteria": {"x": {"trigger": "itemObtained"}}, + "rewards": {"experience": -1}})json", + R"json({"criteria": {"x": {"trigger": "itemObtained"}}, + "rewards": {"recipes": ["NOT AN ID"]}})json", + R"json({"criteria": {"x": {"trigger": "itemObtained"}}, "display": "a string"})json", + }; + for (const char* text : rejected) { + JsonParseResult document = parseJson(text); + CHECK(document.ok); + CHECK(!parseAdvancementJson(document.root, "voxelforge", parsed, problem)); + CHECK(!problem.empty()); + } + + JsonParseResult minimal = + parseJson(R"json({"criteria": {"x": {"trigger": "itemObtained"}}})json"); + CHECK(parseAdvancementJson(minimal.root, "voxelforge", parsed, problem)); + CHECK(parsed.isRoot()); + CHECK(!parsed.hidden); + CHECK_EQ(parsed.criteria.size(), static_cast(1)); + CHECK_EQ(parsed.criteria[0].matchMask, 0u); + CHECK_EQ(parsed.criteria[0].requiredCount, 1); + CHECK(parsed.reward.empty()); + + // A bare namespace default applies to every id inside the document. + JsonParseResult defaulted = parseJson( + R"json({"parent": "root", "criteria": {"x": {"trigger": "itemObtained", + "item": "stick"}}, "rewards": {"recipes": ["torch"]}})json"); + CHECK(parseAdvancementJson(defaulted.root, "extras", parsed, problem)); + CHECK(parsed.parent == ResourceId("extras", "root")); + CHECK(parsed.criteria[0].item == ItemId::Stick); + CHECK(parsed.reward.recipes[0] == ResourceId("extras", "torch")); +} + +VF_TEST(oneMiningRunFeedsBothTheAdvancementTreeAndTheStatSheet) { + DataPackMeta meta; + meta.name = "mining"; + DataPack pack(meta); + pack.addFile("data/voxelforge/advancements/quarry.json", + R"json({"display": {"title": "Quarry"}, + "criteria": {"stone": {"trigger": "blockBroken", + "block": "voxelforge:stone", "count": 5}}, + "rewards": {"experience": 15}})json"); + + std::vector errors; + AdvancementTree tree = loadTree(pack, errors); + CHECK(errors.empty()); + AdvancementProgress progress(tree); + + Scoreboard board; + ScoreObjective mined; + mined.name = "mined"; + mined.criterion = ScoreObjective::Criterion::Statistic; + mined.statKind = StatKind::BlocksBroken; + mined.statSubject = static_cast(Block::Stone); + CHECK(board.addObjective(mined)); + + const AdvancementId quarry = id("voxelforge:quarry"); + for (int strike = 1; strike <= 4; ++strike) { + progress.notify(Event::blockBroken(Block::Stone, ItemId::IronPickaxe), strike * 10); + board.stats("foksiu").addBlockBroken(Block::Stone); + CHECK_EQ(board.score("mined", "foksiu"), strike); + CHECK(!progress.granted(quarry)); + } + + std::vector granted = + progress.notify(Event::blockBroken(Block::Stone, ItemId::IronPickaxe), 50); + board.stats("foksiu").addBlockBroken(Block::Stone); + CHECK_EQ(granted.size(), static_cast(1)); + CHECK(progress.granted(quarry)); + CHECK_EQ(progress.totalExperience(), 15); + CHECK_EQ(board.score("mined", "foksiu"), 5); + CHECK_EQ(board.stats("foksiu").blocksBroken(Block::Stone), 5); + + // Breaking something else moves the sheet but not this advancement. + board.stats("foksiu").addBlockBroken(Block::Dirt, 3); + CHECK_EQ(board.score("mined", "foksiu"), 5); + CHECK_EQ(board.stats("foksiu").total(StatKind::BlocksBroken), 8); +}