blob: f18affa2daa873c4b59b8a9b20fa6f41c4b89558 [file] [log] [blame] [raw]
package net.glowstone.inventory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import net.glowstone.GlowServer;
import net.glowstone.entity.GlowPlayer;
import net.glowstone.util.InventoryUtil;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.inventory.InventoryType.SlotType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
/**
* A class which represents an inventory.
*/
public class GlowInventory implements Inventory {
@Override
public HashMap<Integer, ItemStack> removeItemAnySlot(
@NotNull ItemStack... items) throws IllegalArgumentException {
// TODO
return new HashMap<>();
}
/**
* This inventory's slots.
*/
private List<GlowInventorySlot> slots;
/**
* The list of humans viewing this inventory.
*/
private Set<HumanEntity> viewers;
/**
* The owner of this inventory.
*/
@Getter
private InventoryHolder holder;
/**
* The type of this inventory.
*/
@Getter
private InventoryType type;
/**
* The inventory's name.
*/
@Getter
private String title;
/**
* The inventory's maximum stack size.
*/
@Getter
@Setter
private int maxStackSize = 64;
protected GlowInventory() {
}
public GlowInventory(InventoryHolder holder, InventoryType type) {
this(holder, type, type.getDefaultSize(), type.getDefaultTitle());
}
public GlowInventory(InventoryHolder holder, InventoryType type, int size) {
this(holder, type, size, type.getDefaultTitle());
}
public GlowInventory(InventoryHolder holder, InventoryType type, int size, String title) {
initialize(GlowInventorySlot.createList(size), new HashSet<>(), holder, type, title);
}
/**
* Initializes some key components of this inventory.
*
* <p>This should be called in the constructor.
*
* @param slots List of slots this inventory has.
* @param viewers Set for storage of current inventory viewers.
* @param owner InventoryHolder which owns this Inventory.
* @param type The inventory type.
* @param title Inventory title, displayed in the client.
*/
protected void initialize(List<GlowInventorySlot> slots, Set<HumanEntity> viewers,
InventoryHolder owner, InventoryType type, String title) {
this.slots = slots;
this.viewers = viewers;
this.holder = owner;
this.type = type;
this.title = title;
}
////////////////////////////////////////////////////////////////////////////
// Internals
/**
* Add a viewer to the inventory.
*
* @param viewer The HumanEntity to add.
*/
public void addViewer(HumanEntity viewer) {
viewers.add(viewer);
}
/**
* Remove a viewer from the inventory.
*
* @param viewer The HumanEntity to remove.
*/
public void removeViewer(HumanEntity viewer) {
viewers.remove(viewer);
}
/**
* Returns the set which contains viewers.
*
* @return Viewers set.
*/
public Set<HumanEntity> getViewersSet() {
return Collections.synchronizedSet(viewers);
}
////////////////////////////////////////////////////////////////////////////
// Basic Stuff
/**
* Returns a certain slot.
*
* @param slot index.
* @return The requested slot.
*/
public GlowInventorySlot getSlot(int slot) {
if (slot < 0 || slot > slots.size()) {
GlowServer.logger.info("Out of bound slot: " + slot + " (max " + slots.size() + ")");
return null;
}
return slots.get(slot);
}
/**
* Get the type of the specified slot.
*
* @param slot The slot number.
* @return The SlotType of the slot.
*/
public SlotType getSlotType(int slot) {
if (slot < 0) {
return SlotType.OUTSIDE;
}
return slots.get(slot).getType();
}
/**
* Check whether it is allowed for a player to insert the given ItemStack at the slot,
* regardless of the slot's current contents.
*
* <p>Should return false for crafting output slots or armor slots which cannot accept the given
* item.
*
* @param slot The slot number.
* @param stack The stack to add.
* @return Whether the stack can be added there.
*/
public boolean itemPlaceAllowed(int slot, ItemStack stack) {
return getSlotType(slot) != SlotType.RESULT;
}
/**
* Check whether, in a shift-click operation, an item of the specified type may be placed in the
* given slot.
*
* @param slot The slot number.
* @param stack The stack to add.
* @return Whether the stack can be added there.
*/
public boolean itemShiftClickAllowed(int slot, ItemStack stack) {
return itemPlaceAllowed(slot, stack);
}
/**
* Handle a shift click in this inventory by the specified player.
*
* <p>The default implementation distributes items from the right to the left and from the
* bottom to the top.
*
* @param player The player who clicked
* @param view The inventory view in which was clicked
* @param clickedSlot The slot in the view
* @param clickedItem The item at which was clicked
*/
public void handleShiftClick(GlowPlayer player, InventoryView view, int clickedSlot,
ItemStack clickedItem) {
clickedItem = player.getInventory().tryToFillSlots(clickedItem, 8, -1, 35, 8);
view.setItem(clickedSlot, clickedItem);
}
/**
* Tries to put the given items into the specified slots of this inventory from the start slot
* (inclusive) to the end slot (exclusive).
*
* <p>The slots are supplied in pairs, first the start then the end slots.
*
* <p>This will first try to fill up all partial slots and if items are still left after doing
* so, it places them into the first empty slot.
*
* <p>If no empty slot was found and there are still items left, they're returned from this
* method.
*
* @param stack The items to place down
* @param slots Pairs of start/end slots
* @return The remaining items or {@code null} if non are remaining
*/
public ItemStack tryToFillSlots(ItemStack stack, int... slots) {
if (slots.length % 2 != 0) {
throw new IllegalArgumentException("Slots must be pairs.");
}
ItemStack maxStack = stack.clone();
maxStack.setAmount(stack.getMaxStackSize());
int firstEmpty = -1;
for (int s = 0; s < slots.length && stack.getAmount() > 0; s += 2) {
// Iterate through all pairs of start and end slots
int start = slots[s];
int end = slots[s + 1];
int delta = start < end ? 1 : -1;
for (int i = start; i != end && stack.getAmount() > 0; i += delta) {
// Check whether shift clicking is allowed in that slot of the inventory
if (!itemShiftClickAllowed(i, stack)) {
continue;
}
ItemStack currentStack = getItem(i);
// Store the first empty slot
if (firstEmpty == -1 && InventoryUtil.isEmpty(currentStack)) {
firstEmpty = i;
} else if (currentStack
.isSimilar(stack)) { // Non empty slot of similar items, try to fill stack
// Calculate the amount of transferable items
int amount = currentStack.getAmount();
int maxStackSize = Math.min(currentStack.getMaxStackSize(), getMaxStackSize());
int transfer = Math.min(stack.getAmount(), maxStackSize - amount);
if (transfer > 0) {
// And if there are any, transfer them
currentStack.setAmount(amount + transfer);
stack.setAmount(stack.getAmount() - transfer);
}
setItem(i, currentStack);
}
}
}
if (firstEmpty != -1) { // Fill empty slot
if (stack.getAmount() > stack.getMaxStackSize()) {
setItem(firstEmpty, maxStack);
stack.setAmount(stack.getAmount() - stack.getMaxStackSize());
} else {
ItemStack finalStack = stack.clone();
setItem(firstEmpty, finalStack);
stack.setAmount(0);
}
}
if (stack.getAmount() <= 0) {
stack = InventoryUtil.createEmptyStack();
}
return stack;
}
/**
* Gets the number of slots in this inventory according to the protocol.
*
* <p>Some inventories have 0 slots in the protocol, despite having slots.
*
* @return The numbers of slots
*/
public int getRawSlots() {
return getSize();
}
@Override
public int getSize() {
return slots.size();
}
////////////////////////////////////////////////////////////////////////////
// Basic Stuff
/**
* Returns the whole slot list.
*
* @return Slot list.
*/
public List<GlowInventorySlot> getSlots() {
return Collections.unmodifiableList(slots);
}
/**
* Set the custom title of this inventory or reset it to the default.
*
* @param title The new title, or null to reset.
*/
public void setTitle(String title) {
if (title == null) {
this.title = type.getDefaultTitle();
} else {
this.title = title;
}
}
@Override
public List<HumanEntity> getViewers() {
return new ArrayList<>(viewers);
}
@Override
public ListIterator<ItemStack> iterator() {
return new InventoryIterator(this);
}
@Override
public ListIterator<ItemStack> iterator(int index) {
if (index < 0) {
// negative indices go from back
index += getSize() + 1;
}
return new InventoryIterator(this, index);
}
@Override
public Location getLocation() {
return null;
}
////////////////////////////////////////////////////////////////////////////
// Get, Set, Add, Remove
@Override
public ItemStack getItem(int index) {
return slots.get(index).getItem();
}
@Override
public void setItem(int index, ItemStack item) {
if (index == -1) {
return;
}
slots.get(index).setItem(item);
}
@Override
public HashMap<Integer, ItemStack> addItem(ItemStack... items) {
HashMap<Integer, ItemStack> result = new HashMap<>();
for (int i = 0; i < items.length; ++i) {
ItemStack remaining = addItemStack(items[i], false);
if (!InventoryUtil.isEmpty(remaining)) {
result.put(i, remaining);
}
}
return result;
}
/**
* Adds the contents of the given ItemStack to the inventory.
*
* @param item the ItemStack to add
* @param ignoreMeta if true, can convert to items with different NBT data in order to stack
* with existing copies of those items, provided the material and damage value match
* @return the items that couldn't be added, or an empty stack if all were added
*/
public ItemStack addItemStack(ItemStack item, boolean ignoreMeta) {
int maxStackSize = item.getType() == null ? 64 : item.getType().getMaxStackSize();
int toAdd = item.getAmount();
Iterator<GlowInventorySlot> iterator = slots.iterator();
while (toAdd > 0 && iterator.hasNext()) {
GlowInventorySlot slot = iterator.next();
// Look for existing stacks to add to
ItemStack slotItem = InventoryUtil.itemOrEmpty(slot.getItem());
if (!InventoryUtil.isEmpty(slotItem) && compareItems(item, slotItem, ignoreMeta)) {
int space = maxStackSize - slotItem.getAmount();
if (space < 0) {
continue;
}
if (space > toAdd) {
space = toAdd;
}
slotItem.setAmount(slotItem.getAmount() + space);
toAdd -= space;
}
}
if (toAdd > 0) {
// Look for empty slots to add to
iterator = slots.iterator();
while (toAdd > 0 && iterator.hasNext()) {
GlowInventorySlot slot = iterator.next();
ItemStack slotItem = slot.getItem();
if (InventoryUtil.isEmpty(slotItem)
&& itemPlaceAllowed(slots.indexOf(slot), item)) {
int num = toAdd > maxStackSize ? maxStackSize : toAdd;
slotItem = item.clone();
slotItem.setAmount(num);
slot.setItem(slotItem);
toAdd -= num;
}
}
}
if (toAdd > 0) {
ItemStack remaining = new ItemStack(item);
remaining.setAmount(toAdd);
return remaining;
}
return InventoryUtil.createEmptyStack();
}
@Override
public HashMap<Integer, ItemStack> removeItem(ItemStack... items) {
HashMap<Integer, ItemStack> result = new HashMap<>();
for (int i = 0; i < items.length; ++i) {
ItemStack remaining = removeItemStack(items[i], true);
if (!InventoryUtil.isEmpty(remaining)) {
result.put(i, remaining);
}
}
return result;
}
/**
* Removes the given ItemStack from the inventory.
*
* @param item the ItemStack to remove
* @param ignoreMeta if true, can choose an item with different NBT data, provided the material
* and damage value match
* @return the items that couldn't be removed, or an empty stack if all were removed
*/
public ItemStack removeItemStack(ItemStack item, boolean ignoreMeta) {
int toRemove = item.getAmount();
Iterator<GlowInventorySlot> iterator = slots.iterator();
while (toRemove > 0 && iterator.hasNext()) {
GlowInventorySlot slot = iterator.next();
ItemStack slotItem = slot.getItem();
// Look for stacks to remove from.
if (!InventoryUtil.isEmpty(slotItem) && compareItems(item, slotItem, ignoreMeta)) {
if (slotItem.getAmount() > toRemove) {
slotItem.setAmount(slotItem.getAmount() - toRemove);
} else {
toRemove -= slotItem.getAmount();
item.setAmount(0);
slot.setItem(new ItemStack(Material.AIR, 0));
}
}
}
if (toRemove > 0) {
ItemStack remaining = new ItemStack(item);
remaining.setAmount(toRemove);
return remaining;
}
return InventoryUtil.createEmptyStack();
}
private boolean compareItems(ItemStack a, ItemStack b, boolean ignoreMeta) {
if (ignoreMeta) {
return a.getType() == b.getType() && a.getDurability() == b.getDurability();
}
return a.isSimilar(b);
}
@Override
public ItemStack[] getContents() {
ItemStack[] contents = new ItemStack[getSize()];
int i = 0;
for (ItemStack itemStack : this) {
contents[i] = InventoryUtil.itemOrEmpty(itemStack);
i++;
}
return contents;
}
@Override
public void setContents(ItemStack[] items) {
if (items.length != getSize()) {
throw new IllegalArgumentException("Length of items must be " + getSize());
}
Iterator<GlowInventorySlot> iterator = slots.iterator();
for (int i = 0; i < getSize(); i++) {
iterator.next().setItem(items[i]);
}
}
@Override
public ItemStack[] getStorageContents() {
return InventoryUtil.NO_ITEMS;
}
@Override
public void setStorageContents(ItemStack[] itemStacks) throws IllegalArgumentException {
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(getClass().getSimpleName());
sb.append(" for ").append(getHolder()).append(":\n");
for (GlowInventorySlot slot : slots) {
ItemStack item = slot.getItem();
SlotType type = slot.getType();
if (type != SlotType.CONTAINER || !InventoryUtil.isEmpty(item)) {
sb.append(item).append(" in ").append(slot.getType()).append('\n');
}
}
return sb.toString();
}
////////////////////////////////////////////////////////////////////////////
// Contains
@Override
public boolean contains(Material material) {
return first(material) >= 0;
}
@Override
public boolean contains(ItemStack item) {
return first(item) >= 0;
}
@Override
public boolean contains(Material material, int amount) {
HashMap<Integer, ? extends ItemStack> found = all(material);
int total = 0;
for (ItemStack stack : found.values()) {
total += stack.getAmount();
}
return total >= amount;
}
@Override
public boolean contains(ItemStack item, int amount) {
return contains(InventoryUtil.itemOrEmpty(item).getType(), amount);
}
@Override
public boolean containsAtLeast(ItemStack item, int amount) {
return false; // todo
}
////////////////////////////////////////////////////////////////////////////
// Find all
@Override
public HashMap<Integer, ItemStack> all(Material material) {
HashMap<Integer, ItemStack> result = new HashMap<>();
int i = 0;
for (ItemStack slotItem : this) {
if (!InventoryUtil.isEmpty(slotItem) && slotItem.getType() == material) {
result.put(i, InventoryUtil.itemOrEmpty(slotItem));
}
i++;
}
return result;
}
@Override
public HashMap<Integer, ItemStack> all(ItemStack item) {
HashMap<Integer, ItemStack> result = new HashMap<>();
int i = 0;
for (ItemStack slotItem : this) {
if (Objects.equals(slotItem, item)) {
result.put(i, slotItem);
}
i++;
}
return result;
}
////////////////////////////////////////////////////////////////////////////
// Find first
@Override
public int first(Material material) {
int i = 0;
for (ItemStack slotItem : this) {
if (slotItem == null) {
if (material == Material.AIR) {
return i;
}
} else if (slotItem.getType() == material) {
return i;
}
i++;
}
return -1;
}
@Override
public int first(ItemStack item) {
int i = 0;
for (ItemStack slotItem : this) {
if (Objects.equals(slotItem, item)) {
return i;
}
i++;
}
return -1;
}
@Override
public int firstEmpty() {
return first((Material) null);
}
////////////////////////////////////////////////////////////////////////////
// Remove
@Override
public void remove(Material material) {
HashMap<Integer, ? extends ItemStack> stacks = all(material);
stacks.keySet().forEach(this::clear);
}
@Override
public void remove(ItemStack item) {
HashMap<Integer, ? extends ItemStack> stacks = all(item);
stacks.keySet().forEach(this::clear);
}
////////////////////////////////////////////////////////////////////////////
// Clear
@Override
public void clear(int index) {
setItem(index, null);
}
@Override
public void clear() {
for (GlowInventorySlot slot : slots) {
slot.setItem(InventoryUtil.createEmptyStack());
}
}
/**
* Consumes an item or the full stack in the given slot.
*
* @param slot The slot to consume.
* @param wholeStack True if we should remove the complete stack.
* @return The number of item really consumed.
*/
public int consumeItem(int slot, boolean wholeStack) {
ItemStack item = InventoryUtil.itemOrEmpty(getItem(slot));
if (InventoryUtil.isEmpty(item)) {
return 0;
}
if (wholeStack || item.getAmount() == 1) {
setItem(slot, InventoryUtil.createEmptyStack());
} else {
item.setAmount(item.getAmount() - 1);
setItem(slot, item);
}
return wholeStack ? item.getAmount() : 1;
}
/**
* Consumes an item in the given slot.
*
* @param slot The slot to consume.
* @return The number of item really consumed.
*/
public int consumeItem(int slot) {
return this.consumeItem(slot, false);
}
}