#include "testHarness.h" #include "vf/SlotAction.h" #include #include #include using namespace vf; namespace { const CraftingRegistry& sharedRegistry() { static CraftingRegistry registry; return registry; } struct Session { ContainerMenu menu; CursorStack cursor; DragState drag; explicit Session(MenuKind kind) : menu(kind, &sharedRegistry()) {} SlotActionResult click(ClickKind kind, int slotIndex) { return applySlotAction(menu, cursor, drag, clickAt(kind, slotIndex)); } SlotActionResult hotbar(int slotIndex, int button) { return applySlotAction(menu, cursor, drag, hotbarSwapAt(slotIndex, button)); } }; // The seven items the random walk shuffles around. Tools are in the mix so the // walk also has stacks that refuse to merge. const std::array walkItems = {ItemId::Stone, ItemId::Planks, ItemId::Sand, ItemId::CoalLump, ItemId::Apple, ItemId::IronPickaxe, ItemId::RawBeef}; struct Ledger { std::array counts{}; void snapshot(const ContainerMenu& menu, const CursorStack& cursor) { for (std::size_t i = 0; i < walkItems.size(); ++i) { counts[i] = totalAcrossMenuAndCursor(menu, cursor, walkItems[i]); } } }; void seedMenu(ContainerMenu& menu, uint32_t& rng) { for (int i = 0; i < menu.slotCount(); ++i) { if (menu.slot(i).takeOnly) continue; if (stackRandom(rng) % 3u == 0u) continue; ItemId item = walkItems[stackRandom(rng) % walkItems.size()]; MetaStack stack = makeStack(item, 1 + static_cast(stackRandom(rng) % 40u)); if (!menu.canPlace(i, stack)) continue; menu.slot(i).cell.insert(stack); } } SlotAction randomAction(const ContainerMenu& menu, uint32_t& rng) { ClickKind kind = static_cast(stackRandom(rng) % static_cast(ClickKind::Count)); int slotIndex = static_cast(stackRandom(rng) % static_cast(menu.slotCount())); // Every eighth action aims outside the window, which is the only way to // reach the throw-away path with a full cursor. if (stackRandom(rng) % 8u == 0u) slotIndex = -1; if (kind == ClickKind::HotbarSwap) { return hotbarSwapAt(slotIndex, static_cast(stackRandom(rng) % 9u)); } return clickAt(kind, slotIndex); } struct WalkCoverage { int applied = 0; int dropped = 0; int distinctKindsApplied = 0; }; WalkCoverage runConservationWalk(MenuKind kind, uint32_t seed, int steps) { Session session(kind); uint32_t rng = seed; seedMenu(session.menu, rng); Ledger before; before.snapshot(session.menu, session.cursor); std::array thrown{}; std::array(ClickKind::Count)> perKind{}; WalkCoverage coverage; for (int step = 0; step < steps; ++step) { SlotAction action = randomAction(session.menu, rng); SlotActionResult outcome = applySlotAction(session.menu, session.cursor, session.drag, action); if (outcome.applied) { ++coverage.applied; ++perKind[static_cast(action.kind)]; } coverage.dropped += outcome.dropped; CHECK_EQ(outcome.created, 0); CHECK_EQ(outcome.dropped, static_cast([&] { int sum = 0; for (const MetaStack& lost : outcome.droppedStacks) sum += lost.count; return sum; }())); for (const MetaStack& lost : outcome.droppedStacks) { for (std::size_t i = 0; i < walkItems.size(); ++i) { if (lost.item == walkItems[i]) thrown[i] += lost.count; } } } Ledger after; after.snapshot(session.menu, session.cursor); for (std::size_t i = 0; i < walkItems.size(); ++i) { CHECK_EQ(after.counts[i] + thrown[i], before.counts[i]); } for (int hits : perKind) { if (hits > 0) ++coverage.distinctKindsApplied; } return coverage; } } // namespace VF_TEST(everyClickKindHasAName) { for (int i = 0; i < static_cast(ClickKind::Count); ++i) { CHECK(std::string(clickKindName(static_cast(i))) != std::string("unknown")); } } VF_TEST(aLeftClickPicksUpTheWholeStackAndPutsItDownAgain) { Session session(MenuKind::Chest); session.menu.slot(0).cell.stack = makeStack(ItemId::Stone, 37); SlotActionResult pickup = session.click(ClickKind::Left, 0); CHECK(pickup.applied); CHECK_EQ(pickup.moved, 37); CHECK(pickup.cursorChanged); CHECK_EQ(session.cursor.held.count, 37); CHECK(session.menu.stackAt(0).empty()); SlotActionResult drop = session.click(ClickKind::Left, 4); CHECK(drop.applied); CHECK_EQ(drop.moved, 37); CHECK(session.cursor.empty()); CHECK_EQ(session.menu.stackAt(4).count, 37); CHECK(!session.click(ClickKind::Left, 9).applied); CHECK(!session.click(ClickKind::Left, 999).applied); } VF_TEST(aLeftClickMergesIntoAMatchingStackAndSwapsWithAForeignOne) { Session session(MenuKind::Chest); session.menu.slot(0).cell.stack = makeStack(ItemId::Sand, 50); session.menu.slot(1).cell.stack = makeStack(ItemId::Sand, 60); session.menu.slot(2).cell.stack = makeStack(ItemId::Planks, 8); session.click(ClickKind::Left, 0); SlotActionResult merge = session.click(ClickKind::Left, 1); CHECK(merge.applied); CHECK_EQ(merge.moved, 4); CHECK_EQ(session.menu.stackAt(1).count, 64); CHECK_EQ(session.cursor.held.count, 46); SlotActionResult swap = session.click(ClickKind::Left, 2); CHECK(swap.applied); CHECK_EQ(swap.moved, 54); CHECK(session.cursor.held.item == ItemId::Planks); CHECK_EQ(session.cursor.held.count, 8); CHECK(session.menu.stackAt(2).item == ItemId::Sand); CHECK_EQ(session.menu.stackAt(2).count, 46); // A full matching stack accepts nothing and is not swapped either. session.menu.slot(3).cell.stack = makeStack(ItemId::Planks, 64); CHECK(!session.click(ClickKind::Left, 3).applied); CHECK_EQ(session.cursor.held.count, 8); } VF_TEST(aRightClickTakesTheLargerHalfAndThenPlacesOneAtATime) { Session session(MenuKind::Chest); session.menu.slot(0).cell.stack = makeStack(ItemId::CoalLump, 9); SlotActionResult half = session.click(ClickKind::Right, 0); CHECK(half.applied); CHECK_EQ(half.moved, 5); CHECK_EQ(session.cursor.held.count, 5); CHECK_EQ(session.menu.stackAt(0).count, 4); SlotActionResult one = session.click(ClickKind::Right, 6); CHECK(one.applied); CHECK_EQ(one.moved, 1); CHECK_EQ(session.menu.stackAt(6).count, 1); CHECK_EQ(session.cursor.held.count, 4); session.click(ClickKind::Right, 6); CHECK_EQ(session.menu.stackAt(6).count, 2); CHECK_EQ(session.cursor.held.count, 3); // A right click over an incompatible stack does nothing at all. session.menu.slot(7).cell.stack = makeStack(ItemId::Stone, 1); CHECK(!session.click(ClickKind::Right, 7).applied); CHECK_EQ(session.cursor.held.count, 3); CHECK_EQ(session.menu.stackAt(7).count, 1); } VF_TEST(shiftClickTopsUpExistingStacksBeforeItTouchesAnEmptySlot) { Session session(MenuKind::Chest); std::vector player = session.menu.playerSectionSlots(); session.menu.slot(0).cell.stack = makeStack(ItemId::Sand, 30); session.menu.slot(player[0]).cell.stack = makeStack(ItemId::Stone, 64); session.menu.slot(player[1]).cell.stack = makeStack(ItemId::Sand, 50); SlotActionResult moved = session.click(ClickKind::ShiftLeft, 0); CHECK(moved.applied); CHECK_EQ(moved.moved, 30); CHECK(session.menu.stackAt(0).empty()); CHECK_EQ(session.menu.stackAt(player[1]).count, 64); CHECK_EQ(session.menu.stackAt(player[2]).count, 16); CHECK(session.menu.stackAt(player[2]).item == ItemId::Sand); } VF_TEST(shiftClickPutsWhatDoesNotFitStraightBackWhereItCameFrom) { Session session(MenuKind::Hopper); for (int index : session.menu.playerSectionSlots()) { session.menu.slot(index).cell.stack = makeStack(ItemId::Obsidian, 64); } session.menu.slot(0).cell.stack = makeStack(ItemId::Sand, 12); SlotActionResult blocked = session.click(ClickKind::ShiftLeft, 0); CHECK(!blocked.applied); CHECK_EQ(session.menu.stackAt(0).count, 12); CHECK_EQ(session.menu.totalOf(ItemId::Sand), 12); } VF_TEST(shiftClickInAPlayerInventoryMovesBetweenTheRowsAndTheHotbar) { Session session(MenuKind::PlayerInventory); const MenuLayout& layout = session.menu.layout(); int firstRow = layout.inventoryFirst; int firstHotbar = layout.hotbarFirst; session.menu.slot(firstRow).cell.stack = makeStack(ItemId::Bread, 10); SlotActionResult down = session.click(ClickKind::ShiftLeft, firstRow); CHECK(down.applied); CHECK_EQ(down.moved, 10); CHECK(session.menu.stackAt(firstRow).empty()); CHECK_EQ(session.menu.stackAt(firstHotbar).count, 10); SlotActionResult up = session.click(ClickKind::ShiftRight, firstHotbar); CHECK(up.applied); CHECK_EQ(session.menu.stackAt(firstRow).count, 10); CHECK(session.menu.stackAt(firstHotbar).empty()); // The crafting grid is not a shift-click destination. CHECK_EQ(session.menu.totalOf(ItemId::Bread), 10); for (int cell = 0; cell < layout.craftCells(); ++cell) { CHECK(session.menu.stackAt(layout.craftFirst + cell).empty()); } } VF_TEST(shiftClickHonoursTheDestinationFilters) { Session session(MenuKind::Furnace); int hotbar = session.menu.layout().hotbarFirst; session.menu.slot(hotbar).cell.stack = makeStack(ItemId::Stone, 5); SlotActionResult refused = session.click(ClickKind::ShiftLeft, hotbar); CHECK(!refused.applied); CHECK_EQ(session.menu.stackAt(hotbar).count, 5); session.menu.slot(hotbar + 1).cell.stack = makeStack(ItemId::CoalLump, 5); SlotActionResult accepted = session.click(ClickKind::ShiftLeft, hotbar + 1); CHECK(accepted.applied); CHECK_EQ(session.menu.stackAt(session.menu.slotsWithRole(MenuSlot::Role::FuelInput)[0]).count, 5); } VF_TEST(shiftClickingTheResultSlotCraftsEverythingTheGridCanMake) { Session session(MenuKind::CraftingTable); CraftingView grid = session.menu.crafting(); grid.input(0, 0) = makeStack(ItemId::Planks, 4); grid.input(0, 1) = makeStack(ItemId::Planks, 4); grid.refresh(); SlotActionResult crafted = session.click(ClickKind::ShiftLeft, session.menu.layout().resultSlot); CHECK(crafted.applied); CHECK(crafted.craftedResult); CHECK_EQ(crafted.craftedCount, 4); CHECK_EQ(session.menu.totalOf(ItemId::Stick), 16); CHECK_EQ(session.menu.totalOf(ItemId::Planks), 0); } VF_TEST(aNumberKeyExchangesTheHoveredSlotWithThatHotbarSlot) { Session session(MenuKind::Chest); int hotbar = session.menu.layout().hotbarFirst; session.menu.slot(0).cell.stack = makeStack(ItemId::Sand, 20); session.menu.slot(hotbar + 3).cell.stack = makeStack(ItemId::Stone, 7); SlotActionResult swap = session.hotbar(0, 3); CHECK(swap.applied); CHECK_EQ(swap.moved, 27); CHECK(session.menu.stackAt(0).item == ItemId::Stone); CHECK_EQ(session.menu.stackAt(0).count, 7); CHECK(session.menu.stackAt(hotbar + 3).item == ItemId::Sand); CHECK_EQ(session.menu.stackAt(hotbar + 3).count, 20); CHECK(!session.hotbar(0, -1).applied); CHECK(!session.hotbar(0, 9).applied); CHECK(!session.hotbar(hotbar + 3, 3).applied); } VF_TEST(aNumberKeyRespectsTheTargetSlotsFilter) { Session session(MenuKind::Furnace); int hotbar = session.menu.layout().hotbarFirst; int fuel = session.menu.slotsWithRole(MenuSlot::Role::FuelInput)[0];