#include #include #include "testHarness.h" #include "vf/LootTables.h" using namespace vf; namespace { using Species = MobDropRule::Species; using Structure = ChestFill::Structure; uint32_t seedFor(int index) { uint32_t mixed = static_cast(index) + 1u; mixed ^= mixed >> 16; mixed *= 0x7feb352du; mixed ^= mixed >> 15; mixed *= 0x846ca68bu; mixed ^= mixed >> 16; return mixed == 0u ? 1u : mixed; } LootContext contextWith(ItemId tool, uint32_t seed) { LootContext context; context.tool = tool; context.seed = seed; return context; } LootContext enchantedWith(ItemId tool, EnchantKind kind, int level, uint32_t seed) { LootContext context = contextWith(tool, seed); addEnchant(context.toolEnchants, kind, level); return context; } bool sameRoll(const LootRoll& left, const LootRoll& right) { if (left.items.size() != right.items.size()) return false; for (std::size_t i = 0; i < left.items.size(); ++i) { if (left.items[i].item != right.items[i].item) return false; if (left.items[i].count != right.items[i].count) return false; if (left.items[i].durability != right.items[i].durability) return false; } return true; } ItemId bestToolFor(ToolType type) { switch (type) { case ToolType::Pickaxe: return ItemId::DiamondPickaxe; case ToolType::Axe: return ItemId::DiamondAxe; case ToolType::Shovel: return ItemId::DiamondShovel; case ToolType::Sword: return ItemId::DiamondSword; case ToolType::Hoe: return ItemId::DiamondHoe; case ToolType::None: return ItemId::None; } return ItemId::None; } const std::vector& toolPalette() { static const std::vector tools = { ItemId::None, ItemId::WoodPickaxe, ItemId::WoodAxe, ItemId::WoodShovel, ItemId::WoodSword, ItemId::WoodHoe, ItemId::StonePickaxe, ItemId::StoneAxe, ItemId::StoneShovel, ItemId::StoneSword, ItemId::StoneHoe, ItemId::IronPickaxe, ItemId::IronAxe, ItemId::IronShovel, ItemId::IronSword, ItemId::IronHoe, ItemId::GoldPickaxe, ItemId::GoldAxe, ItemId::GoldShovel, ItemId::GoldSword, ItemId::GoldHoe, ItemId::DiamondPickaxe, ItemId::DiamondAxe, ItemId::DiamondShovel, ItemId::DiamondSword, ItemId::DiamondHoe, }; return tools; } bool ruleCanYield(const BlockDropRule& rule, ItemId item) { return item == rule.drop || item == rule.silkTouchDrop() || item == rule.firedDrop || item == rule.bonusDrop; } bool poolCanYield(const LootPool& pool, ItemId item) { for (const LootEntry& entry : pool.entries) { if (entry.item == item) return true; } return false; } ItemId firstEntryOfPool(const LootTable& table, std::size_t poolIndex) { if (poolIndex >= table.pools.size()) return ItemId::None; if (table.pools[poolIndex].entries.empty()) return ItemId::None; return table.pools[poolIndex].entries[0].item; } } // namespace VF_TEST(theRegistrysPublishedConstantsAreTheOnesTheBoundsWereDerivedFrom) { CHECK_EQ(fishingBaseWaitTicks, 600); CHECK_EQ(fishingMinWaitTicks, 100); CHECK_EQ(fishingLureTicksPerLevel, 100); CHECK_EQ(fishingFishWeight, 85); CHECK_EQ(fishingTreasureWeight, 5); CHECK_EQ(fishingJunkWeight, 10); CHECK_EQ(fishingLuckWeightShift, 2); CHECK_EQ(fishingFishWeight + fishingTreasureWeight + fishingJunkWeight, 100); CHECK_EQ(shearToolType, ToolType::Hoe); CHECK_EQ(static_cast(Block::Count), 130); CHECK_EQ(static_cast(Species::Count), 17); CHECK_EQ(static_cast(Structure::Count), 6); CHECK_EQ(static_cast(FishingLoot::Pool::Count), 3); } VF_TEST(everyBlockInThePaletteCarriesAnAuthoredRuleInEnumOrder) { const LootRegistry& registry = lootRegistry(); CHECK(registry.coversEveryBlock()); CHECK_EQ(registry.blockRules().size(), static_cast(Block::Count)); int outOfOrder = 0; for (int index = 0; index < static_cast(Block::Count); ++index) { Block block = static_cast(index); const BlockDropRule& rule = registry.blockRule(block); if (rule.block != block) ++outOfOrder; CHECK(rule.minCount >= 0); CHECK(rule.maxCount >= rule.minCount); CHECK(rule.dropChance > 0.0f); CHECK(rule.dropChance <= 1.0f); } CHECK_EQ(outOfOrder, 0); CHECK(registry.coversEveryMob()); CHECK_EQ(registry.mobRules().size(), static_cast(Species::Count)); CHECK(registry.coversEveryStructure()); CHECK_EQ(registry.chestFills().size(), static_cast(Structure::Count)); } VF_TEST(everyRuleNamesTheSameToolClassTheItemModuleAlreadyDeclares) { const LootRegistry& registry = lootRegistry(); int mismatches = 0; int constrained = 0; for (int index = 0; index < static_cast(Block::Count); ++index) { Block block = static_cast(index); ToolType declared = correctToolFor(block); if (declared == ToolType::None) continue; ++constrained; if (registry.blockRule(block).harvestTool != declared) ++mismatches; } CHECK_EQ(mismatches, 0); CHECK_EQ(constrained, 28); } VF_TEST(theRegistryIsNeverMorePermissiveThanTheSalvagedToolCheck) { const LootRegistry& registry = lootRegistry(); int leaks = 0; int gatedCases = 0; for (int index = 0; index < static_cast(Block::Count); ++index) { Block block = static_cast(index); for (ItemId tool : toolPalette()) { if (toolQualifiesFor(block, tool)) continue; ++gatedCases; if (!registry.rollBlock(block, contextWith(tool, 1234u)).empty()) ++leaks; } } CHECK_EQ(leaks, 0); CHECK(gatedCases > 100); } VF_TEST(harvestLevelsGateTheOresExactlyWhereTheTiersSay) { CHECK_EQ(toolHarvestLevel(ToolTier::None), 0); CHECK_EQ(toolHarvestLevel(ToolTier::Wood), 1); CHECK_EQ(toolHarvestLevel(ToolTier::Gold), 1); CHECK_EQ(toolHarvestLevel(ToolTier::Stone), 2); CHECK_EQ(toolHarvestLevel(ToolTier::Iron), 3); CHECK_EQ(toolHarvestLevel(ToolTier::Diamond), 4); const LootRegistry& registry = lootRegistry(); const struct { Block block; ItemId tool; bool allowed; } cases[] = { {Block::Stone, ItemId::None, false}, {Block::Stone, ItemId::DiamondShovel, false}, {Block::Stone, ItemId::WoodPickaxe, true}, {Block::IronOre, ItemId::WoodPickaxe, false}, {Block::IronOre, ItemId::GoldPickaxe, false}, {Block::IronOre, ItemId::StonePickaxe, true}, {Block::DiamondOre, ItemId::StonePickaxe, false}, {Block::DiamondOre, ItemId::IronPickaxe, true}, {Block::Obsidian, ItemId::IronPickaxe, false}, {Block::Obsidian, ItemId::DiamondPickaxe, true}, {Block::CryingObsidian, ItemId::IronPickaxe, false}, {Block::CopperOre, ItemId::WoodPickaxe, false}, {Block::CopperOre, ItemId::StonePickaxe, true}, {Block::Dirt, ItemId::None, true}, {Block::Wood, ItemId::None, true}, }; for (const auto& item : cases) { CHECK_EQ(toolAllows(registry.blockRule(item.block), item.tool), item.allowed); } CHECK(registry.rollBlock(Block::Obsidian, contextWith(ItemId::IronPickaxe, 9u)).empty()); CHECK_EQ(registry.rollBlock(Block::Obsidian, contextWith(ItemId::DiamondPickaxe, 9u)) .countOf(ItemId::Obsidian), 1); } VF_TEST(stoneBecomesCobblestoneUnlessItIsTakenWholeOrBrokenInsideFire) { const LootRegistry& registry = lootRegistry(); LootContext plain = contextWith(ItemId::IronPickaxe, 17u); CHECK_EQ(registry.rollBlock(Block::Stone, plain).countOf(ItemId::Cobblestone), 1); LootContext silk = enchantedWith(ItemId::IronPickaxe, EnchantKind::SilkTouch, 1, 17u); LootRoll harvested = registry.rollBlock(Block::Stone, silk); CHECK_EQ(harvested.countOf(ItemId::Stone), 1); CHECK_EQ(harvested.totalCount(), 1); LootContext burning = contextWith(ItemId::IronPickaxe, 17u); burning.fiery = true; LootRoll melted = registry.rollBlock(Block::Stone, burning); CHECK_EQ(melted.countOf(ItemId::Stone), 1); CHECK_EQ(melted.countOf(ItemId::Cobblestone), 0); // Cobblestone bakes back into stone the same way, and iron and gold ore // come out as ingots. LootRoll cobble = registry.rollBlock(Block::Cobblestone, burning); CHECK_EQ(cobble.countOf(ItemId::Stone), 1); CHECK_EQ(registry.rollBlock(Block::IronOre, burning).countOf(ItemId::IronIngot), 1); CHECK_EQ(registry.rollBlock(Block::GoldOre, burning).countOf(ItemId::GoldIngot), 1); CHECK_EQ(registry.rollBlock(Block::IronOre, plain).countOf(ItemId::IronOre), 1); } VF_TEST(oreRulesMultiplyWithFortuneAndHandBackTheOreBlockUnderSilkTouch) { const LootRegistry& registry = lootRegistry(); const struct { Block block; ItemId raw; int lowest; int highest; } ores[] = { {Block::CoalOre, ItemId::CoalLump, 1, 1}, {Block::DiamondOre, ItemId::DiamondGem, 1, 1}, {Block::RedstoneOre, ItemId::RedstoneDust, 4, 5}, {Block::LapisOre, ItemId::LapisLazuli, 4, 9}, {Block::Glowstone, ItemId::Glowstone, 2, 4}, }; for (const auto& ore : ores) { int seenLowest = 1000; int seenHighest = 0; int fortuneHighest = 0; for (int i = 0; i < 500; ++i) { LootContext plain = contextWith(ItemId::DiamondPickaxe, seedFor(i)); int count = registry.rollBlock(ore.block, plain).countOf(ore.raw); seenLowest = std::min(seenLowest, count); seenHighest = std::max(seenHighest, count); LootContext lucky = enchantedWith(ItemId::DiamondPickaxe, EnchantKind::Fortune, 3, seedFor(i)); fortuneHighest = std::max(fortuneHighest, registry.rollBlock(ore.block, lucky).countOf(ore.raw)); } CHECK_EQ(seenLowest, ore.lowest); CHECK_EQ(seenHighest, ore.highest); CHECK_EQ(fortuneHighest, ore.highest * 4); LootContext silk = enchantedWith(ItemId::DiamondPickaxe, EnchantKind::SilkTouch, 1, 5u); LootRoll whole = registry.rollBlock(ore.block, silk); CHECK_EQ(whole.countOf(itemForBlock(ore.block)), 1); CHECK_EQ(whole.totalCount(), 1); } } VF_TEST(shearClassToolsTakeTheBlockItselfWithoutNeedingSilkTouch) { const LootRegistry& registry = lootRegistry(); for (ItemId hoe : {ItemId::WoodHoe, ItemId::DiamondHoe}) { LootRoll sheared = registry.rollBlock(Block::Leaves, contextWith(hoe, 42u)); CHECK_EQ(sheared.countOf(ItemId::Leaves), 1); CHECK_EQ(sheared.totalCount(), 1); } // A pickaxe falls through to the chance table instead. int leafBlocks = 0; int cuttings = 0; for (int i = 0; i < 800; ++i) { LootRoll roll = registry.rollBlock(Block::Leaves, contextWith(ItemId::IronPickaxe, seedFor(i))); leafBlocks += roll.countOf(ItemId::Leaves); cuttings += roll.countOf(ItemId::Wood); } CHECK_EQ(leafBlocks, 0); CHECK(cuttings > 0); // Shearing a block the palette has no item for still yields nothing. for (Block block : {Block::Wool, Block::Cobweb, Block::Vine, Block::Kelp, Block::Moss}) { CHECK(registry.blockRule(block).shearHarvest); CHECK(registry.rollBlock(block, contextWith(ItemId::DiamondHoe, 8u)).empty()); } } VF_TEST(everyBlockRuleOnlyEverProducesItemsItDeclaredUpFront) { const LootRegistry& registry = lootRegistry(); int strayItems = 0; int rollsMade = 0; for (int index = 0; index < static_cast(Block::Count); ++index) { Block block = static_cast(index); const BlockDropRule& rule = registry.blockRule(block); for (int sample = 0; sample < 12; ++sample) { LootContext context = contextWith(bestToolFor(rule.harvestTool), seedFor(sample)); if (sample % 3 == 1) addEnchant(context.toolEnchants, EnchantKind::SilkTouch, 1); if (sample % 3 == 2) addEnchant(context.toolEnchants, EnchantKind::Fortune, 3); if (sample % 4 == 3) context.fiery = true; LootRoll roll = registry.rollBlock(block, context); ++rollsMade; for (const ItemSlot& stack : roll.items) { if (!ruleCanYield(rule, stack.item)) ++strayItems; if (stack.count <= 0) ++strayItems; } } } CHECK_EQ(strayItems, 0); CHECK_EQ(rollsMade, static_cast(Block::Count) * 12); } VF_TEST(everyBlockThatDeclaresACertainDropActuallyHandsItOver) { const LootRegistry& registry = lootRegistry(); int certainDrops = 0; int missed = 0;