#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; for (int index = 0; index < static_cast(Block::Count); ++index) { Block block = static_cast(index); const BlockDropRule& rule = registry.blockRule(block); if (rule.drop == ItemId::None || rule.dropChance < 1.0f) continue; ++certainDrops; LootContext context = contextWith(bestToolFor(rule.harvestTool), seedFor(index)); LootRoll roll = registry.rollBlock(block, context); if (roll.countOf(rule.drop) < rule.minCount) ++missed; } CHECK_EQ(missed, 0); CHECK_EQ(certainDrops, 62); } VF_TEST(blocksTheItemPaletteCannotExpressStillCarryARuleThatYieldsNothing) { const LootRegistry& registry = lootRegistry(); int barren = 0; for (int index = 0; index < static_cast(Block::Count); ++index) { Block block = static_cast(index); const BlockDropRule& rule = registry.blockRule(block); if (!rule.yieldsNothing()) continue; ++barren; CHECK_EQ(rule.block, block); CHECK(blockRuleTable(rule).empty()); LootContext generous = enchantedWith(bestToolFor(rule.harvestTool), EnchantKind::Fortune, 3, seedFor(index)); addEnchant(generous.toolEnchants, EnchantKind::SilkTouch, 1); CHECK(registry.rollBlock(block, generous).empty()); } CHECK_EQ(barren, static_cast(Block::Count) - 64); } VF_TEST(theSameSeedReproducesTheSameDropForEveryBlockInThePalette) { const LootRegistry& registry = lootRegistry(); int divergences = 0; for (int index = 0; index < static_cast(Block::Count); ++index) { Block block = static_cast(index); LootContext context = enchantedWith(ItemId::DiamondPickaxe, EnchantKind::Fortune, 2, seedFor(index)); LootRoll first = registry.rollBlock(block, context); LootRoll second = registry.rollBlock(block, context); if (!sameRoll(first, second)) ++divergences; } CHECK_EQ(divergences, 0); } VF_TEST(everyMobRuleOnlyEverProducesItemsItDeclaredUpFront) { const LootRegistry& registry = lootRegistry(); int strayItems = 0; for (int index = 0; index < static_cast(Species::Count); ++index) { Species species = static_cast(index); const MobDropRule& rule = registry.mobRule(species); CHECK_EQ(rule.species, species); CHECK(rule.maxCount >= rule.minCount); for (int sample = 0; sample < 40; ++sample) { LootContext context = contextWith(ItemId::IronSword, seedFor(sample)); context.playerKill = sample % 2 == 0; context.fiery = sample % 3 == 0; if (sample % 5 == 0) addEnchant(context.toolEnchants, EnchantKind::Looting, 3); for (const ItemSlot& stack : registry.rollMob(species, context).items) { bool declared = stack.item == rule.drop || stack.item == rule.firedDrop || stack.item == rule.rareDrop; if (!declared) ++strayItems; } } } CHECK_EQ(strayItems, 0); CHECK(registry.rollMob(Species::Count, contextWith(ItemId::None, 1u)).empty()); } VF_TEST(theRegistryAgreesWithTheSalvagedMobTablesOnEverySharedSpecies) { const LootRegistry& registry = lootRegistry(); const struct { Species species; LootTable::MobKind kind; } shared[] = { {Species::Zombie, LootTable::MobKind::Zombie}, {Species::Skeleton, LootTable::MobKind::Skeleton}, {Species::Creeper, LootTable::MobKind::Creeper}, {Species::Sheep, LootTable::MobKind::Sheep}, {Species::Cow, LootTable::MobKind::Cow}, {Species::Pig, LootTable::MobKind::Pig}, }; for (const auto& pair : shared) { const MobDropRule& rule = registry.mobRule(pair.species); const LootTable& salvaged = mobLootTable(pair.kind); CHECK_EQ(rule.drop, firstEntryOfPool(salvaged, 0)); if (rule.rareDrop != ItemId::None) { CHECK_EQ(rule.rareDrop, firstEntryOfPool(salvaged, 1)); } } } VF_TEST(livestockComesOutCookedWhenTheKillWasFieryAndRawOtherwise) { const LootRegistry& registry = lootRegistry(); const struct { Species species; ItemId raw; ItemId cooked; } herd[] = { {Species::Sheep, ItemId::RawMutton, ItemId::CookedMutton}, {Species::Cow, ItemId::RawBeef, ItemId::CookedBeef}, {Species::Pig, ItemId::RawPorkchop, ItemId::CookedPorkchop}, }; for (const auto& animal : herd) { CHECK(!registry.mobRule(animal.species).hostile); int raw = 0; int cooked = 0; for (int i = 0; i < 300; ++i) { LootContext cold = contextWith(ItemId::IronSword, seedFor(i)); LootRoll rawRoll = registry.rollMob(animal.species, cold); raw += rawRoll.countOf(animal.raw); CHECK_EQ(rawRoll.countOf(animal.cooked), 0); LootContext burning = cold; burning.fiery = true; LootRoll cookedRoll = registry.rollMob(animal.species, burning); cooked += cookedRoll.countOf(animal.cooked); CHECK_EQ(cookedRoll.countOf(animal.raw), 0); } CHECK(raw > 0); CHECK_EQ(raw, cooked); } } VF_TEST(dropsThatNeedAPlayerKillNeverShowUpFromAnEnvironmentalDeath) { const LootRegistry& registry = lootRegistry(); CHECK(registry.mobRule(Species::Blaze).dropNeedsPlayerKill); int fromPlayers = 0; int fromTheWorld = 0; int witchGlowstoneFromTheWorld = 0; for (int i = 0; i < 2000; ++i) { LootContext byPlayer = contextWith(ItemId::IronSword, seedFor(i)); byPlayer.playerKill = true; LootContext byWorld = contextWith(ItemId::None, seedFor(i)); fromPlayers += registry.rollMob(Species::Blaze, byPlayer).countOf(ItemId::CoalLump); fromTheWorld += registry.rollMob(Species::Blaze, byWorld).countOf(ItemId::CoalLump); witchGlowstoneFromTheWorld += registry.rollMob(Species::Witch, byWorld).countOf(ItemId::Glowstone); } CHECK(fromPlayers > 0); CHECK_EQ(fromTheWorld, 0); // A witch is the counter example: her glowstone is not player gated. CHECK(!registry.mobRule(Species::Witch).rareNeedsPlayerKill); CHECK(witchGlowstoneFromTheWorld > 0); } VF_TEST(ironGolemsIgnoreLootingWhileZombiesDoNot) { const LootRegistry& registry = lootRegistry(); CHECK(!registry.mobRule(Species::IronGolem).lootingRaisesCount); int golemLowest = 100; int golemHighest = 0; int golemLootedHighest = 0; int zombieHighest = 0; int zombieLootedHighest = 0; for (int i = 0; i < 1500; ++i) { LootContext plain = contextWith(ItemId::IronSword, seedFor(i)); plain.playerKill = true; LootContext looted = plain; addEnchant(looted.toolEnchants, EnchantKind::Looting, 3);