265 lines
8.9 KiB
C++
265 lines
8.9 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <vector>
|
|
|
|
#include "Container.h"
|
|
#include "Crafting.h"
|
|
#include "ItemStack.h"
|
|
|
|
namespace vf {
|
|
|
|
// A menu is the screen an open container puts in front of the player. It does
|
|
// not replace Container: a Container is still the storage that lives in the
|
|
// world and that hoppers, droppers and comparators read. The menu binds one for
|
|
// the duration of the screen, adds the player's own sections next to it, and
|
|
// gives every cell a role and an admission rule.
|
|
|
|
// What a slot is willing to accept. The result of a craft, a smelt or a trade
|
|
// takes Nothing, which is what makes it take-only.
|
|
struct SlotFilter {
|
|
enum class Kind : uint8_t {
|
|
Any,
|
|
Nothing,
|
|
Fuel, // furnace fuel, per Crafting.h isFuel
|
|
Smeltable, // has a smelting recipe in the bound registry
|
|
Lapis, // enchanting table payment
|
|
Enchantable, // enchantabilityOf > 0
|
|
Repairable, // anvil input: tools carry durability, so they repair
|
|
BrewIngredient, // the brewing ingredients that exist as items here
|
|
Count
|
|
};
|
|
|
|
Kind kind = Kind::Any;
|
|
|
|
// `registry` is only consulted by Smeltable; every other kind answers from
|
|
// the item tables alone and accepts a null registry.
|
|
bool accepts(const MetaStack& stack, const CraftingRegistry* registry) const;
|
|
};
|
|
|
|
const char* slotFilterKindName(SlotFilter::Kind kind);
|
|
|
|
// One storage cell: a stack plus a cap. `capacity` of zero defers to the item's
|
|
// own limit, anything else clamps below it (a brewing bottle holds exactly one).
|
|
struct StackSlot {
|
|
MetaStack stack;
|
|
int capacity = 0;
|
|
|
|
bool empty() const { return stack.empty(); }
|
|
int limitFor(const MetaStack& candidate) const;
|
|
int room(const MetaStack& candidate) const;
|
|
|
|
// Merges what fits from `incoming`, leaving the rest in it. Returns moved.
|
|
int insert(MetaStack& incoming);
|
|
MetaStack take(int amount);
|
|
MetaStack takeAll();
|
|
MetaStack swapWith(const MetaStack& incoming);
|
|
};
|
|
|
|
enum class MenuKind : uint8_t {
|
|
PlayerInventory,
|
|
CraftingTable,
|
|
Chest,
|
|
LargeChest,
|
|
Furnace,
|
|
BrewingStand,
|
|
EnchantingTable,
|
|
Anvil,
|
|
Villager,
|
|
Hopper,
|
|
Count
|
|
};
|
|
|
|
const char* menuKindName(MenuKind kind);
|
|
|
|
struct MenuSlot {
|
|
// Which part of the screen the slot belongs to. Shift-clicking moves items
|
|
// between the container half and the player half, so the roles have to be
|
|
// distinguishable at runtime, not just visually.
|
|
enum class Role : uint8_t {
|
|
Storage, // the bound container, or the menu's own storage
|
|
Inventory, // the player's three rows
|
|
Hotbar, // the player's bottom row
|
|
CraftInput,
|
|
CraftResult,
|
|
FuelInput,
|
|
SmeltInput,
|
|
SmeltOutput,
|
|
BrewBottle,
|
|
BrewIngredient,
|
|
EnchantItem,
|
|
EnchantLapis,
|
|
AnvilInput,
|
|
AnvilResult,
|
|
TradeInput,
|
|
TradeResult,
|
|
OffHand,
|
|
Count
|
|
};
|
|
|
|
StackSlot cell;
|
|
Role role = Role::Storage;
|
|
SlotFilter filter;
|
|
// A take-only slot never accepts a placement, however empty it is.
|
|
bool takeOnly = false;
|
|
// Index into the bound Container, or -1 when the menu owns the stack.
|
|
int backingIndex = -1;
|
|
};
|
|
|
|
const char* menuSlotRoleName(MenuSlot::Role role);
|
|
|
|
// Where each section starts and how long it is, so callers can iterate a
|
|
// section without knowing the slot order by heart.
|
|
struct MenuLayout {
|
|
MenuKind kind = MenuKind::Chest;
|
|
|
|
int storageFirst = -1;
|
|
int storageCount = 0;
|
|
int inventoryFirst = -1;
|
|
int inventoryCount = 0;
|
|
int hotbarFirst = -1;
|
|
int hotbarCount = 0;
|
|
int craftFirst = -1;
|
|
int craftWidth = 0;
|
|
int craftHeight = 0;
|
|
int resultSlot = -1;
|
|
int offHandSlot = -1;
|
|
|
|
int totalSlots = 0;
|
|
|
|
int craftCells() const { return craftWidth * craftHeight; }
|
|
bool hasCrafting() const { return craftFirst >= 0 && craftCells() > 0; }
|
|
};
|
|
|
|
MenuLayout layoutFor(MenuKind kind);
|
|
|
|
constexpr int playerInventoryRows = 3;
|
|
constexpr int playerInventoryColumns = 9;
|
|
constexpr int playerInventorySlots = playerInventoryRows * playerInventoryColumns;
|
|
constexpr int playerHotbarSlots = 9;
|
|
|
|
class ContainerMenu;
|
|
|
|
// The crafting half of a menu, wired to the existing CraftingRegistry. It owns
|
|
// no stacks and no lifetime: it reads and writes the menu's craft slots in
|
|
// place, so the grid the player sees and the grid the recipe matcher sees are
|
|
// the same cells. A menu without a crafting section hands out a view whose
|
|
// cellCount is zero and whose operations therefore do nothing.
|
|
class CraftingView {
|
|
public:
|
|
explicit CraftingView(ContainerMenu& menu);
|
|
|
|
int width() const;
|
|
int height() const;
|
|
int cellCount() const;
|
|
|
|
MetaStack& input(int x, int y);
|
|
const MetaStack& input(int x, int y) const;
|
|
MetaStack& inputAt(int cell);
|
|
const MetaStack& inputAt(int cell) const;
|
|
|
|
// Recomputes the result slot from the current grid. Cheap enough to call
|
|
// after every single change, which is exactly what the menu does.
|
|
void refresh();
|
|
|
|
const MetaStack& result() const;
|
|
bool hasResult() const;
|
|
|
|
// How many times the current grid could be crafted before running dry.
|
|
int maxCrafts() const;
|
|
|
|
// Takes the result once. Each input loses one item; a damageable ingredient
|
|
// is not eaten but comes back one durability point worse, and a broken one
|
|
// vanishes. Returns the crafted stack, empty when there was no recipe.
|
|
MetaStack takeResult();
|
|
|
|
// Shift-clicking the result: crafts until the grid runs out or the player
|
|
// has no room, pushing every batch into the player's sections.
|
|
int craftAll(int limit);
|
|
|
|
private:
|
|
void consumeOneRound();
|
|
CraftGrid gridOfIds() const;
|
|
|
|
ContainerMenu* menu;
|
|
};
|
|
|
|
class ContainerMenu {
|
|
public:
|
|
explicit ContainerMenu(MenuKind kind, const CraftingRegistry* registry = nullptr);
|
|
|
|
MenuKind kind() const { return menuKind; }
|
|
const MenuLayout& layout() const { return menuLayout; }
|
|
const CraftingRegistry* registry() const { return craftingRegistry; }
|
|
|
|
int slotCount() const { return static_cast<int>(slots.size()); }
|
|
MenuSlot& slot(int index) { return slots[static_cast<std::size_t>(index)]; }
|
|
const MenuSlot& slot(int index) const { return slots[static_cast<std::size_t>(index)]; }
|
|
MetaStack& stackAt(int index) { return slot(index).cell.stack; }
|
|
const MetaStack& stackAt(int index) const { return slot(index).cell.stack; }
|
|
|
|
bool validIndex(int index) const { return index >= 0 && index < slotCount(); }
|
|
bool canPlace(int index, const MetaStack& incoming) const;
|
|
// True when the slot's contents may leave it. Every slot allows this; the
|
|
// asymmetry is deliberate, a result slot gives but never takes.
|
|
bool canTake(int index) const;
|
|
|
|
// Puts what fits into the slot, honouring the filter and the cap.
|
|
int insertAt(int index, MetaStack& incoming);
|
|
MetaStack takeFrom(int index, int amount);
|
|
|
|
// Section walkers used by shift-click and by the double-click gather.
|
|
std::vector<int> slotsWithRole(MenuSlot::Role role) const;
|
|
std::vector<int> playerSectionSlots() const;
|
|
std::vector<int> containerSectionSlots() const;
|
|
bool isPlayerSection(int index) const;
|
|
|
|
// Fills matching stacks first and only then empty slots, which is the order
|
|
// a shift-click has to follow to feel right.
|
|
int distributeInto(const std::vector<int>& targets, MetaStack& incoming);
|
|
|
|
int totalOf(ItemId item) const;
|
|
int totalItems() const;
|
|
int usedSlots() const;
|
|
|
|
// Binds a world Container as the storage section. The menu copies the
|
|
// container in, and syncToContainer writes it back. ItemSlot has no room
|
|
// for enchantments or names, so anything richer than durability survives
|
|
// only inside the menu (or through stackListToNbt).
|
|
void bindContainer(Container& container);
|
|
void syncFromContainer();
|
|
void syncToContainer();
|
|
bool hasContainer() const { return bound != nullptr; }
|
|
void unbind();
|
|
|
|
// Creative mode is the one thing allowed to conjure items: a middle click
|
|
// clones a stack instead of doing nothing.
|
|
void setCreativeMode(bool enabled) { creative = enabled; }
|
|
bool creativeMode() const { return creative; }
|
|
|
|
bool hasCrafting() const { return menuLayout.hasCrafting() && craftingRegistry != nullptr; }
|
|
// A two-pointer handle, cheap to hand out by value and never stale.
|
|
CraftingView crafting();
|
|
const MetaStack& resultStack() const;
|
|
|
|
// Called by CraftingView and by every slot action that touched a craft
|
|
// input, so the result slot is never stale.
|
|
void refreshResult();
|
|
|
|
// Persists every slot losslessly, unlike the Container projection.
|
|
NbtTag toNbt() const;
|
|
bool fromNbt(const NbtTag& tag);
|
|
|
|
private:
|
|
void buildSlots();
|
|
|
|
MenuKind menuKind;
|
|
MenuLayout menuLayout;
|
|
const CraftingRegistry* craftingRegistry = nullptr;
|
|
std::vector<MenuSlot> slots;
|
|
Container* bound = nullptr;
|
|
bool creative = false;
|
|
};
|
|
|
|
} // namespace vf
|