#include "testHarness.h" #include "vf/ContainerMenu.h" #include #include using namespace vf; namespace { const CraftingRegistry& sharedRegistry() { static CraftingRegistry registry; return registry; } // A recipe nothing in the default set uses: a tool sits in the grid as a proper // ingredient, which is the only way to exercise the remainder rule. CraftingRegistry registryWithToolRecipe() { CraftingRegistry registry; ShapedRecipe carving; carving.width = 1; carving.height = 2; carving.pattern = {ItemId::IronShovel, ItemId::Planks}; carving.result = {ItemId::Stick, 4}; carving.allowMirror = false; registry.addShaped(carving); return registry; } int countSlotsWithRole(const ContainerMenu& menu, MenuSlot::Role role) { return static_cast(menu.slotsWithRole(role).size()); } } // namespace VF_TEST(everyMenuKindLaysOutTheSectionsItsScreenNeeds) { for (int i = 0; i < static_cast(MenuKind::Count); ++i) { MenuKind kind = static_cast(i); ContainerMenu menu(kind, &sharedRegistry()); CHECK(menu.slotCount() > 0); CHECK_EQ(menu.slotCount(), menu.layout().totalSlots); CHECK_EQ(menu.layout().inventoryCount, playerInventorySlots); CHECK_EQ(menu.layout().hotbarCount, playerHotbarSlots); CHECK(std::string(menuKindName(kind)) != std::string("unknown")); } CHECK_EQ(ContainerMenu(MenuKind::Chest).slotCount(), 27 + 27 + 9); CHECK_EQ(ContainerMenu(MenuKind::LargeChest).slotCount(), 54 + 27 + 9); CHECK_EQ(ContainerMenu(MenuKind::Hopper).slotCount(), 5 + 27 + 9); CHECK_EQ(countSlotsWithRole(ContainerMenu(MenuKind::Hopper), MenuSlot::Role::Storage), 5); } VF_TEST(thePlayerInventoryKeepsItsTwoByTwoGridAndAnOffHandSlot) { ContainerMenu menu(MenuKind::PlayerInventory, &sharedRegistry()); const MenuLayout& layout = menu.layout(); CHECK_EQ(layout.craftWidth, 2); CHECK_EQ(layout.craftHeight, 2); CHECK_EQ(layout.craftCells(), 4); CHECK(layout.hasCrafting()); CHECK(layout.resultSlot >= 0); CHECK(layout.offHandSlot >= 0); CHECK_EQ(countSlotsWithRole(menu, MenuSlot::Role::CraftInput), 4); CHECK_EQ(countSlotsWithRole(menu, MenuSlot::Role::OffHand), 1); CHECK(menu.isPlayerSection(layout.offHandSlot)); CHECK(!menu.isPlayerSection(layout.resultSlot)); } VF_TEST(theCraftingTableGivesTheFullThreeByThreeGrid) { ContainerMenu menu(MenuKind::CraftingTable, &sharedRegistry()); CHECK_EQ(menu.layout().craftWidth, 3); CHECK_EQ(menu.layout().craftHeight, 3); CHECK_EQ(countSlotsWithRole(menu, MenuSlot::Role::CraftInput), 9); CHECK(menu.hasCrafting()); CHECK_EQ(menu.layout().offHandSlot, -1); } VF_TEST(theFurnaceFuelSlotRefusesAnythingThatWillNotBurn) { ContainerMenu menu(MenuKind::Furnace, &sharedRegistry()); std::vector fuelSlots = menu.slotsWithRole(MenuSlot::Role::FuelInput); CHECK_EQ(fuelSlots.size(), 1u); int fuel = fuelSlots[0]; CHECK(menu.canPlace(fuel, makeStack(ItemId::CoalLump, 8))); CHECK(menu.canPlace(fuel, makeStack(ItemId::Planks, 8))); CHECK(!menu.canPlace(fuel, makeStack(ItemId::Stone, 8))); CHECK(!menu.canPlace(fuel, makeStack(ItemId::IronIngot, 1))); MetaStack stone = makeStack(ItemId::Stone, 8); CHECK_EQ(menu.insertAt(fuel, stone), 0); CHECK_EQ(stone.count, 8); MetaStack coal = makeStack(ItemId::CoalLump, 8); CHECK_EQ(menu.insertAt(fuel, coal), 8); CHECK(coal.empty()); CHECK_EQ(menu.stackAt(fuel).count, 8); } VF_TEST(theFurnaceInputSlotOnlyTakesItemsWithASmeltingRecipe) { ContainerMenu menu(MenuKind::Furnace, &sharedRegistry()); int input = menu.slotsWithRole(MenuSlot::Role::SmeltInput)[0]; CHECK(menu.canPlace(input, makeStack(ItemId::IronOre, 4))); CHECK(menu.canPlace(input, makeStack(ItemId::RawBeef, 4))); CHECK(!menu.canPlace(input, makeStack(ItemId::Stick, 4))); CHECK(!menu.canPlace(input, makeStack(ItemId::Glowstone, 4))); // Without a registry there is nothing to consult, so nothing is smeltable. ContainerMenu blind(MenuKind::Furnace, nullptr); CHECK(!blind.canPlace(input, makeStack(ItemId::IronOre, 4))); } VF_TEST(theEnchantingTableTakesOnlyLapisForPaymentAndOnlyGearForTheItem) { ContainerMenu menu(MenuKind::EnchantingTable, &sharedRegistry()); int item = menu.slotsWithRole(MenuSlot::Role::EnchantItem)[0]; int lapis = menu.slotsWithRole(MenuSlot::Role::EnchantLapis)[0]; CHECK(menu.canPlace(lapis, makeStack(ItemId::LapisLazuli, 3))); CHECK(!menu.canPlace(lapis, makeStack(ItemId::DiamondGem, 3))); CHECK(!menu.canPlace(lapis, makeStack(ItemId::LapisOre, 3))); CHECK(menu.canPlace(item, makeTool(ItemId::DiamondSword))); CHECK(!menu.canPlace(item, makeStack(ItemId::Stone, 1))); CHECK(!menu.canPlace(item, makeStack(ItemId::LapisLazuli, 1))); } VF_TEST(theAnvilOnlyAcceptsItemsThatCarryDurability) { ContainerMenu menu(MenuKind::Anvil, &sharedRegistry()); std::vector inputs = menu.slotsWithRole(MenuSlot::Role::AnvilInput); CHECK_EQ(inputs.size(), 2u); CHECK(menu.canPlace(inputs[0], makeTool(ItemId::GoldHoe))); CHECK(menu.canPlace(inputs[1], makeTool(ItemId::DiamondAxe))); CHECK(!menu.canPlace(inputs[0], makeStack(ItemId::IronIngot, 4))); } VF_TEST(theBrewingStandFiltersIngredientsAndHoldsOneItemPerBottleSlot) { ContainerMenu menu(MenuKind::BrewingStand, &sharedRegistry()); int ingredient = menu.slotsWithRole(MenuSlot::Role::BrewIngredient)[0]; std::vector bottles = menu.slotsWithRole(MenuSlot::Role::BrewBottle); CHECK_EQ(bottles.size(), 3u); CHECK(menu.canPlace(ingredient, makeStack(ItemId::Gunpowder, 2))); CHECK(menu.canPlace(ingredient, makeStack(ItemId::RedstoneDust, 2))); CHECK(!menu.canPlace(ingredient, makeStack(ItemId::Stick, 2))); MetaStack pile = makeStack(ItemId::Glowstone, 20); int moved = menu.insertAt(bottles[0], pile); CHECK_EQ(moved, 1); CHECK_EQ(menu.stackAt(bottles[0]).count, 1); CHECK_EQ(pile.count, 19); CHECK_EQ(menu.insertAt(bottles[0], pile), 0); } VF_TEST(aResultSlotCanBeEmptiedButNeverFilledByHand) { ContainerMenu menu(MenuKind::CraftingTable, &sharedRegistry()); int result = menu.layout().resultSlot; CHECK(menu.slot(result).takeOnly); CHECK(!menu.canPlace(result, makeStack(ItemId::Stone, 1))); MetaStack stone = makeStack(ItemId::Stone, 1); CHECK_EQ(menu.insertAt(result, stone), 0); CHECK_EQ(stone.count, 1); CHECK(menu.stackAt(result).empty()); // The furnace, anvil and trade outputs answer the same way. ContainerMenu furnace(MenuKind::Furnace, &sharedRegistry()); int output = furnace.slotsWithRole(MenuSlot::Role::SmeltOutput)[0]; CHECK(!furnace.canPlace(output, makeStack(ItemId::IronIngot, 1))); ContainerMenu villager(MenuKind::Villager, &sharedRegistry()); int trade = villager.slotsWithRole(MenuSlot::Role::TradeResult)[0]; CHECK(!villager.canPlace(trade, makeStack(ItemId::EmeraldGem, 1))); } VF_TEST(theCraftingViewRecomputesTheResultAfterEveryGridChange) { ContainerMenu menu(MenuKind::CraftingTable, &sharedRegistry()); CraftingView grid = menu.crafting(); CHECK_EQ(grid.cellCount(), 9); CHECK(!grid.hasResult()); grid.inputAt(0) = makeStack(ItemId::Wood, 1); grid.refresh(); CHECK(grid.hasResult()); CHECK(grid.result().item == ItemId::Planks); CHECK_EQ(grid.result().count, 4); grid.inputAt(0).clear(); grid.refresh(); CHECK(!grid.hasResult()); grid.input(0, 0) = makeStack(ItemId::Planks, 3); grid.input(0, 1) = makeStack(ItemId::Planks, 3); grid.refresh(); CHECK(grid.result().item == ItemId::Stick); CHECK_EQ(grid.result().count, 4); } VF_TEST(takingTheResultSpendsExactlyOneItemFromEachFilledInputSlot) { ContainerMenu menu(MenuKind::CraftingTable, &sharedRegistry()); CraftingView grid = menu.crafting(); grid.input(0, 0) = makeStack(ItemId::Planks, 5); grid.input(0, 1) = makeStack(ItemId::Planks, 3); grid.refresh(); CHECK_EQ(grid.maxCrafts(), 3); MetaStack sticks = grid.takeResult(); CHECK(sticks.item == ItemId::Stick); CHECK_EQ(sticks.count, 4); CHECK_EQ(grid.input(0, 0).count, 4); CHECK_EQ(grid.input(0, 1).count, 2); CHECK(grid.hasResult()); grid.takeResult(); grid.takeResult(); CHECK_EQ(grid.input(0, 0).count, 2); CHECK(grid.input(0, 1).empty()); CHECK(!grid.hasResult()); CHECK(grid.takeResult().empty()); } VF_TEST(aDamageableIngredientStaysInTheGridOnePointWorse) { CraftingRegistry registry = registryWithToolRecipe(); ContainerMenu menu(MenuKind::CraftingTable, ®istry); CraftingView grid = menu.crafting(); grid.input(0, 0) = makeTool(ItemId::IronShovel); grid.input(0, 1) = makeStack(ItemId::Planks, 4); grid.refresh(); CHECK(grid.hasResult()); CHECK(grid.result().item == ItemId::Stick); MetaStack sticks = grid.takeResult(); CHECK_EQ(sticks.count, 4); CHECK(!grid.input(0, 0).empty()); CHECK(grid.input(0, 0).item == ItemId::IronShovel); CHECK_EQ(grid.input(0, 0).meta.durability.used, 1); CHECK_EQ(grid.input(0, 1).count, 3); // A shovel one point from death is spent by the craft instead of surviving. grid.input(0, 0).meta.durability.used = grid.input(0, 0).meta.durability.max - 1; grid.refresh(); grid.takeResult(); CHECK(grid.input(0, 0).empty()); } VF_TEST(shiftCraftingRepeatsUntilTheGridRunsDry) { ContainerMenu menu(MenuKind::CraftingTable, &sharedRegistry()); CraftingView grid = menu.crafting(); grid.input(0, 0) = makeStack(ItemId::Planks, 6); grid.input(0, 1) = makeStack(ItemId::Planks, 6); grid.refresh(); int rounds = grid.craftAll(grid.maxCrafts()); CHECK_EQ(rounds, 6); CHECK(grid.input(0, 0).empty()); CHECK(grid.input(0, 1).empty()); CHECK(!grid.hasResult()); CHECK_EQ(menu.totalOf(ItemId::Stick), 24); CHECK_EQ(menu.totalOf(ItemId::Planks), 0); } VF_TEST(craftingStopsWhenThePlayerSectionsHaveNoRoomLeft) { ContainerMenu menu(MenuKind::CraftingTable, &sharedRegistry()); for (int index : menu.playerSectionSlots()) { menu.slot(index).cell.stack = makeStack(ItemId::Obsidian, 64); } CraftingView grid = menu.crafting(); grid.input(0, 0) = makeStack(ItemId::Planks, 4); grid.input(0, 1) = makeStack(ItemId::Planks, 4); grid.refresh(); int rounds = grid.craftAll(grid.maxCrafts()); CHECK_EQ(rounds, 1); CHECK_EQ(menu.totalOf(ItemId::Stick), 4); CHECK_EQ(menu.stackAt(menu.layout().resultSlot).count, 4); CHECK_EQ(menu.totalOf(ItemId::Planks), 6); } VF_TEST(distributingFillsPartialStacksBeforeReachingForAnEmptySlot) { ContainerMenu menu(MenuKind::Chest, &sharedRegistry()); std::vector storage = menu.containerSectionSlots(); menu.slot(storage[0]).cell.stack = makeStack(ItemId::Sand, 60); menu.slot(storage[3]).cell.stack = makeStack(ItemId::Sand, 50); MetaStack incoming = makeStack(ItemId::Sand, 20); int moved = menu.distributeInto(storage, incoming); CHECK_EQ(moved, 20); CHECK(incoming.empty()); CHECK_EQ(menu.stackAt(storage[0]).count, 64); CHECK_EQ(menu.stackAt(storage[3]).count, 64); CHECK_EQ(menu.stackAt(storage[1]).count, 2); CHECK(menu.stackAt(storage[2]).empty()); } VF_TEST(distributingReportsWhatItCouldNotPlace) { ContainerMenu menu(MenuKind::Hopper, &sharedRegistry()); std::vector storage = menu.containerSectionSlots(); for (int index : storage) menu.slot(index).cell.stack = makeStack(ItemId::Obsidian, 64); MetaStack incoming = makeStack(ItemId::Sand, 30); CHECK_EQ(menu.distributeInto(storage, incoming), 0); CHECK_EQ(incoming.count, 30); } VF_TEST(aBoundContainerLoadsIntoTheStorageSectionAndIsWrittenBackOnClose) { Container chest(27); chest.insert(ItemId::Cobblestone, 90); chest.setSlot(5, ItemSlot{ItemId::IronPickaxe, 1, 100}); ContainerMenu menu(MenuKind::Chest, &sharedRegistry()); menu.bindContainer(chest); CHECK(menu.hasContainer()); CHECK_EQ(menu.totalOf(ItemId::Cobblestone), 90); std::vector storage = menu.containerSectionSlots(); CHECK_EQ(menu.stackAt(storage[5]).item, ItemId::IronPickaxe); CHECK_EQ(menu.stackAt(storage[5]).meta.durability.remaining(), 100); CHECK_EQ(menu.stackAt(storage[5]).meta.durability.max, toolMaxDurability(ItemId::IronPickaxe)); menu.slot(storage[10]).cell.stack = makeStack(ItemId::EmeraldGem, 7); MetaStack pulled = menu.takeFrom(storage[0], 30); CHECK_EQ(pulled.count, 30); menu.unbind(); CHECK(!menu.hasContainer()); CHECK_EQ(chest.slot(10).item, ItemId::EmeraldGem); CHECK_EQ(chest.slot(10).count, 7); CHECK_EQ(chest.slot(0).count, 34); CHECK_EQ(chest.slot(5).durability, 100); } VF_TEST(bindingNeverReachesPastTheContainersOwnSize) { Container tiny(3); tiny.insert(ItemId::Stick, 5); ContainerMenu menu(MenuKind::Chest, &sharedRegistry()); menu.bindContainer(tiny); CHECK_EQ(menu.totalOf(ItemId::Stick), 5); std::vector storage = menu.containerSectionSlots(); CHECK_EQ(menu.slot(storage[2]).backingIndex, 2); CHECK_EQ(menu.slot(storage[3]).backingIndex, -1); menu.slot(storage[9]).cell.stack = makeStack(ItemId::Stone, 4); menu.unbind(); CHECK_EQ(tiny.totalItems(), 5); } VF_TEST(theFurnaceMenuBindsAThreeSlotContainerOntoItsOwnRoles) { Container firebox(3); firebox.setSlot(0, ItemSlot{ItemId::IronOre, 8, 0}); firebox.setSlot(1, ItemSlot{ItemId::CoalLump, 4, 0}); firebox.setSlot(2, ItemSlot{ItemId::IronIngot, 2, 0}); ContainerMenu menu(MenuKind::Furnace, &sharedRegistry()); menu.bindContainer(firebox); CHECK_EQ(menu.stackAt(menu.slotsWithRole(MenuSlot::Role::SmeltInput)[0]).count, 8); CHECK_EQ(menu.stackAt(menu.slotsWithRole(MenuSlot::Role::FuelInput)[0]).count, 4); CHECK_EQ(menu.stackAt(menu.slotsWithRole(MenuSlot::Role::SmeltOutput)[0]).count, 2); menu.takeFrom(menu.slotsWithRole(MenuSlot::Role::SmeltOutput)[0], 2); menu.syncToContainer(); CHECK(firebox.slot(2).empty()); } VF_TEST(menuNbtRoundTripKeepsEnchantedAndNamedStacksThatAContainerWouldLose) { ContainerMenu menu(MenuKind::LargeChest, &sharedRegistry()); EnchantSet enchants; addEnchant(enchants, EnchantKind::Fortune, 3); addEnchant(enchants, EnchantKind::Unbreaking, 2); MetaStack pickaxe = makeEnchantedTool(ItemId::DiamondPickaxe, enchants); pickaxe.meta.customName = "Deep Cut"; pickaxe.meta.durability.used = 220; menu.slot(0).cell.stack = pickaxe; menu.slot(7).cell.stack = makeStack(ItemId::Gravel, 40); menu.setCreativeMode(true); ContainerMenu restored(MenuKind::Chest, &sharedRegistry()); CHECK(restored.fromNbt(menu.toNbt())); CHECK(restored.kind() == MenuKind::LargeChest); CHECK_EQ(restored.slotCount(), menu.slotCount()); CHECK(restored.creativeMode()); CHECK(stacksIdentical(restored.stackAt(0), pickaxe)); CHECK(restored.stackAt(0).meta.customName == std::string("Deep Cut")); CHECK_EQ(restored.stackAt(0).meta.enchants.levelOf(EnchantKind::Fortune), 3); CHECK_EQ(restored.stackAt(7).count, 40); } VF_TEST(menuNbtRejectsATagWhoseSlotCountDoesNotMatchTheKind) { ContainerMenu menu(MenuKind::Chest, &sharedRegistry()); NbtTag tag = menu.toNbt(); NbtTag* slots = nullptr; for (auto& entry : tag.asCompound()) { if (entry.first == "Slots") slots = &entry.second; } CHECK(slots != nullptr); if (slots == nullptr) return; slots->asList().pop_back(); ContainerMenu target(MenuKind::Chest, &sharedRegistry()); CHECK(!target.fromNbt(tag)); NbtTag notAMenu = NbtTag::makeCompound(); notAMenu.put("Kind", NbtTag::makeShort(99)); CHECK(!target.fromNbt(notAMenu)); } VF_TEST(slotAccountingAddsUpAcrossTheWholeMenu) { ContainerMenu menu(MenuKind::Chest, &sharedRegistry()); CHECK_EQ(menu.totalItems(), 0); CHECK_EQ(menu.usedSlots(), 0); menu.slot(0).cell.stack = makeStack(ItemId::Stone, 64); menu.slot(1).cell.stack = makeStack(ItemId::Stone, 12); menu.slot(2).cell.stack = makeTool(ItemId::GoldAxe); CHECK_EQ(menu.totalOf(ItemId::Stone), 76); CHECK_EQ(menu.totalOf(ItemId::GoldAxe), 1); CHECK_EQ(menu.totalOf(ItemId::Sand), 0); CHECK_EQ(menu.totalItems(), 77); CHECK_EQ(menu.usedSlots(), 3); CHECK(!menu.validIndex(-1)); CHECK(!menu.validIndex(menu.slotCount())); } VF_TEST(everyRoleAndFilterNameIsSpelledOut) { for (int i = 0; i < static_cast(MenuSlot::Role::Count); ++i) { CHECK(std::string(menuSlotRoleName(static_cast(i))) != std::string("unknown")); } for (int i = 0; i < static_cast(SlotFilter::Kind::Count); ++i) { CHECK(std::string(slotFilterKindName(static_cast(i))) != std::string("unknown")); } } // A result slot is guarded twice over: the takeOnly flag and a filter that lets // nothing through. Because canPlace checks takeOnly first, the filter would go // untested through the menu, so it is pinned here on its own. VF_TEST(everySlotFilterAnswersForItselfWithoutHelpFromTheTakeOnlyFlag) { const CraftingRegistry& registry = sharedRegistry(); SlotFilter nothing; nothing.kind = SlotFilter::Kind::Nothing; CHECK(!nothing.accepts(makeStack(ItemId::Stone, 1), ®istry)); CHECK(!nothing.accepts(makeStack(ItemId::CoalLump, 1), ®istry)); CHECK(!nothing.accepts(makeTool(ItemId::DiamondSword), ®istry)); CHECK(!nothing.accepts(makeStack(ItemId::LapisLazuli, 1), ®istry)); SlotFilter any; any.kind = SlotFilter::Kind::Any; CHECK(any.accepts(makeStack(ItemId::Stone, 1), nullptr)); CHECK(any.accepts(makeTool(ItemId::GoldHoe), nullptr)); SlotFilter fuel; fuel.kind = SlotFilter::Kind::Fuel; CHECK(fuel.accepts(makeStack(ItemId::CoalLump, 1), nullptr)); CHECK(fuel.accepts(makeStack(ItemId::Stick, 1), nullptr)); CHECK(!fuel.accepts(makeStack(ItemId::IronIngot, 1), nullptr)); SlotFilter smeltable; smeltable.kind = SlotFilter::Kind::Smeltable; CHECK(smeltable.accepts(makeStack(ItemId::GoldOre, 1), ®istry)); CHECK(!smeltable.accepts(makeStack(ItemId::GoldIngot, 1), ®istry)); CHECK(!smeltable.accepts(makeStack(ItemId::GoldOre, 1), nullptr)); SlotFilter lapis; lapis.kind = SlotFilter::Kind::Lapis; CHECK(lapis.accepts(makeStack(ItemId::LapisLazuli, 1), nullptr)); CHECK(!lapis.accepts(makeStack(ItemId::LapisOre, 1), nullptr)); SlotFilter enchantable; enchantable.kind = SlotFilter::Kind::Enchantable; CHECK(enchantable.accepts(makeTool(ItemId::GoldSword), nullptr)); CHECK(!enchantable.accepts(makeStack(ItemId::Obsidian, 1), nullptr)); SlotFilter repairable; repairable.kind = SlotFilter::Kind::Repairable; CHECK(repairable.accepts(makeTool(ItemId::StoneHoe), nullptr)); CHECK(!repairable.accepts(makeStack(ItemId::Planks, 1), nullptr)); SlotFilter brewing; brewing.kind = SlotFilter::Kind::BrewIngredient; CHECK(brewing.accepts(makeStack(ItemId::Gunpowder, 1), nullptr)); CHECK(brewing.accepts(makeStack(ItemId::Glowstone, 1), nullptr)); CHECK(brewing.accepts(makeStack(ItemId::RedstoneDust, 1), nullptr)); CHECK(!brewing.accepts(makeStack(ItemId::RedstoneBlock, 1), nullptr)); } VF_TEST(aFilterNeverAcceptsAnEmptyStack) { for (int i = 0; i < static_cast(SlotFilter::Kind::Count); ++i) { SlotFilter filter; filter.kind = static_cast(i); CHECK(!filter.accepts(MetaStack{}, &sharedRegistry())); } }