/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.inventory.slot; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import org.lanternpowered.server.inventory.AbstractMutableInventory; import org.lanternpowered.server.inventory.FastOfferResult; import org.lanternpowered.server.inventory.LanternContainer; import org.lanternpowered.server.inventory.LanternItemStack; import org.lanternpowered.server.inventory.PeekOfferTransactionsResult; import org.lanternpowered.server.inventory.PeekPollTransactionsResult; import org.lanternpowered.server.inventory.PeekSetTransactionsResult; import org.lanternpowered.server.inventory.equipment.LanternEquipmentType; import org.spongepowered.api.data.property.item.EquipmentProperty; import org.spongepowered.api.item.ItemType; import org.spongepowered.api.item.inventory.Inventory; import org.spongepowered.api.item.inventory.ItemStack; import org.spongepowered.api.item.inventory.ItemStackSnapshot; import org.spongepowered.api.item.inventory.Slot; import org.spongepowered.api.item.inventory.equipment.EquipmentType; import org.spongepowered.api.item.inventory.property.AcceptsItems; import org.spongepowered.api.item.inventory.property.EquipmentSlotType; import org.spongepowered.api.item.inventory.transaction.InventoryTransactionResult; import org.spongepowered.api.item.inventory.transaction.SlotTransaction; import org.spongepowered.api.text.translation.Translation; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.WeakHashMap; import java.util.function.Predicate; import java.util.function.Supplier; import javax.annotation.Nullable; public class LanternSlot extends AbstractMutableInventory implements Slot { /** * The item stack the is stored in this slot. */ @Nullable private ItemStack itemStack; /** * The maximum stack size that can fit in this slot. */ private int maxStackSize = 64; /** * All the {@link LanternContainer}s this slot is attached to. */ private final Set<LanternContainer> containers = Collections.newSetFromMap(new WeakHashMap<>()); public LanternSlot(@Nullable Inventory parent) { super(parent, null); } public LanternSlot(@Nullable Inventory parent, @Nullable Translation name) { super(parent, name); } public void addContainer(LanternContainer container) { this.containers.add(container); } public void removeContainer(LanternContainer container) { this.containers.remove(container); } private void queueUpdate() { for (LanternContainer container : this.containers) { container.queueSlotChange(this); } } /** * Gets the raw {@link ItemStack} of this slot, without the need * to box/unbox the itemstack. * * @return The item stack */ @Nullable public ItemStack getRawItemStack() { return this.itemStack; } /** * Check whether the supplied item can be inserted into this slot. Returning * false from this method implies that {@link #offer} <b>would always return * false</b> for this item. * * @param stack ItemStack to check * @return true if the stack is valid for this slot */ @Override public boolean isValidItem(ItemStack stack) { return this.doesAllowEquipmentType(stack) && this.doesAcceptItemType(stack); } @Override public boolean isChild(Inventory child) { return false; } @Override public int slotCount() { return 0; } @Override public int getStackSize() { return this.itemStack != null ? this.itemStack.getQuantity() : 0; } @SuppressWarnings("unchecked") @Override public <T extends Inventory> Iterable<T> slots() { // A slot cannot contain slots return Collections.emptyList(); } @Override public Optional<ItemStack> poll(Predicate<ItemStack> matcher) { checkNotNull(matcher, "matcher"); if (this.itemStack == null || !matcher.test(this.itemStack)) { return Optional.empty(); } final ItemStack itemStack = this.itemStack; // Just remove the item, the complete stack was // being polled this.itemStack = null; this.queueUpdate(); return Optional.of(itemStack); } @Override public Optional<ItemStack> poll(int limit, Predicate<ItemStack> matcher) { checkNotNull(matcher, "matcher"); checkArgument(limit >= 0, "Limit may not be negative"); ItemStack itemStack = this.itemStack; // There is no item available if (itemStack == null || !matcher.test(itemStack)) { return Optional.empty(); } // Split the stack if needed if (limit < itemStack.getQuantity()) { itemStack.setQuantity(itemStack.getQuantity() - limit); // Clone the item to be returned itemStack = itemStack.copy(); itemStack.setQuantity(limit); } else { this.itemStack = null; } this.queueUpdate(); return Optional.of(itemStack); } @Override public Optional<ItemStack> peek(Predicate<ItemStack> matcher) { checkNotNull(matcher, "matcher"); return Optional.ofNullable(this.itemStack == null || !matcher.test(this.itemStack) ? null : this.itemStack.copy()); } @Override public Optional<PeekPollTransactionsResult> peekPollTransactions(Predicate<ItemStack> matcher) { checkNotNull(matcher, "matcher"); if (this.itemStack == null || !matcher.test(this.itemStack)) { return Optional.empty(); } final List<SlotTransaction> transactions = new ArrayList<>(); transactions.add(new SlotTransaction(this, this.itemStack.createSnapshot(), ItemStackSnapshot.NONE)); return Optional.of(new PeekPollTransactionsResult(transactions, this.itemStack.copy())); } @Override public Optional<ItemStack> peek(int limit, Predicate<ItemStack> matcher) { checkNotNull(matcher, "matcher"); checkArgument(limit >= 0, "Limit may not be negative"); ItemStack itemStack = this.itemStack; // There is no item available if (itemStack == null || !matcher.test(itemStack)) { return Optional.empty(); } itemStack = itemStack.copy(); // Split the stack if needed if (limit < itemStack.getQuantity()) { itemStack.setQuantity(limit); } return Optional.of(itemStack); } @Override public Optional<PeekPollTransactionsResult> peekPollTransactions(int limit, Predicate<ItemStack> matcher) { checkNotNull(matcher, "matcher"); checkArgument(limit >= 0, "Limit may not be negative"); ItemStack itemStack = this.itemStack; // There is no item available if (limit == 0 || itemStack == null || !matcher.test(itemStack)) { return Optional.empty(); } ItemStackSnapshot oldItem = itemStack.createSnapshot(); itemStack = itemStack.copy(); int quantity = itemStack.getQuantity(); ItemStackSnapshot newItem; if (limit >= quantity) { newItem = ItemStackSnapshot.NONE; } else { itemStack.setQuantity(quantity - limit); newItem = LanternItemStack.toSnapshot(itemStack); itemStack.setQuantity(limit); } final List<SlotTransaction> transactions = new ArrayList<>(); transactions.add(new SlotTransaction(this, oldItem, newItem)); return Optional.of(new PeekPollTransactionsResult(transactions, itemStack)); } @Override public PeekSetTransactionsResult peekSetTransactions(@Nullable ItemStack stack) { stack = LanternItemStack.toNullable(stack); boolean fail = false; if (stack != null) { if (stack.getQuantity() <= 0) { stack = null; } else { fail = !this.isValidItem(stack); } } final List<SlotTransaction> transactions = new ArrayList<>(); if (fail) { return new PeekSetTransactionsResult(transactions, InventoryTransactionResult.builder() .type(InventoryTransactionResult.Type.FAILURE) .reject(stack) .build()); } final InventoryTransactionResult.Builder resultBuilder = InventoryTransactionResult.builder() .type(InventoryTransactionResult.Type.SUCCESS); final ItemStackSnapshot oldItem = LanternItemStack.toSnapshot(this.itemStack); if (this.itemStack != null) { resultBuilder.replace(this.itemStack); } ItemStackSnapshot newItem = ItemStackSnapshot.NONE; if (stack != null) { final int maxStackSize = Math.min(stack.getMaxStackQuantity(), this.maxStackSize); final int quantity = stack.getQuantity(); if (quantity > maxStackSize) { stack = stack.copy(); stack.setQuantity(maxStackSize); newItem = LanternItemStack.toSnapshot(stack); // Create the rest stack that was rejected, // because the inventory doesn't allow so many items stack = stack.copy(); stack.setQuantity(quantity - maxStackSize); resultBuilder.reject(stack); } else { newItem = LanternItemStack.toSnapshot(stack); } } transactions.add(new SlotTransaction(this, oldItem, newItem)); return new PeekSetTransactionsResult(transactions, resultBuilder.build()); } protected boolean doesAllowItem(ItemType type) { return doesAllowEquipmentType(type) && doesAcceptItemType(type); } protected boolean doesAllowEquipmentType(ItemType type) { return doesAllowEquipmentTypeWithProperty(() -> type.getDefaultProperty(EquipmentProperty.class)); } protected boolean doesAllowEquipmentType(ItemStack stack) { return doesAllowEquipmentTypeWithProperty(() -> stack.getProperty(EquipmentProperty.class)); } protected boolean doesAllowEquipmentTypeWithProperty(Supplier<Optional<EquipmentProperty>> equipmentPropertySupplier) { return doesAllowEquipmentType(() -> equipmentPropertySupplier.get() .flatMap(property -> Optional.ofNullable(property.getValue()))); } protected boolean doesAllowEquipmentType(Supplier<Optional<EquipmentType>> equipmentTypeSupplier) { Collection<EquipmentSlotType> properties = getProperties(parent(), EquipmentSlotType.class); if (properties.isEmpty()) { return true; } Optional<EquipmentType> optEquipmentProperty = equipmentTypeSupplier.get(); if (optEquipmentProperty.isPresent()) { EquipmentType equipmentType = optEquipmentProperty.get(); for (EquipmentSlotType slotType : properties) { EquipmentType equipmentType1 = slotType.getValue(); if (equipmentType1 != null && ((LanternEquipmentType) equipmentType1).isChild(equipmentType)) { return true; } } } return false; } protected boolean doesAcceptItemType(ItemStack stack) { return doesAcceptItemType(stack.getItem()); } protected boolean doesAcceptItemType(ItemType itemType) { Collection<AcceptsItems> acceptsItemsProperties = getProperties(parent(), AcceptsItems.class); // All items will be accepted if there are no properties of this type if (acceptsItemsProperties.isEmpty()) { return true; } for (AcceptsItems acceptsItems : acceptsItemsProperties) { Collection<ItemType> itemTypes = acceptsItems.getValue(); if (itemTypes != null) { for (ItemType expectedType : itemTypes) { if (itemType.equals(expectedType)) { return true; } } } } return true; } @Override public FastOfferResult offerFast(ItemStack stack) { checkNotNull(stack, "stack"); if (LanternItemStack.toNullable(stack) == null) { return new FastOfferResult(stack, false); } final int maxStackSize = Math.min(stack.getMaxStackQuantity(), this.maxStackSize); if (this.itemStack != null && (!((LanternItemStack) this.itemStack).isSimilar(stack) || this.itemStack.getQuantity() >= maxStackSize) || !this.isValidItem(stack)) { return new FastOfferResult(stack, false); } // Get the amount of space we have left final int availableSpace = this.itemStack == null ? maxStackSize : maxStackSize - this.itemStack.getQuantity(); final int quantity = stack.getQuantity(); if (quantity > availableSpace) { if (this.itemStack == null) { this.itemStack = stack.copy(); } this.itemStack.setQuantity(maxStackSize); stack = stack.copy(); stack.setQuantity(quantity - availableSpace); queueUpdate(); return new FastOfferResult(stack, true); } else { if (this.itemStack == null) { this.itemStack = stack.copy(); } else { this.itemStack.setQuantity(this.itemStack.getQuantity() + quantity); } queueUpdate(); return FastOfferResult.SUCCESS; } } @Override public PeekOfferTransactionsResult peekOfferFastTransactions(ItemStack stack) { checkNotNull(stack, "stack"); final List<SlotTransaction> transactions = new ArrayList<>(); if (LanternItemStack.toNullable(stack) == null) { return new PeekOfferTransactionsResult(transactions, new FastOfferResult(stack, false)); } final int maxStackSize = Math.min(stack.getMaxStackQuantity(), this.maxStackSize); if (this.itemStack != null && (!((LanternItemStack) this.itemStack).isSimilar(stack) || this.itemStack.getQuantity() >= maxStackSize) || !this.isValidItem(stack)) { return new PeekOfferTransactionsResult(transactions, new FastOfferResult(stack, false)); } // Get the amount of space we have left final int availableSpace = this.itemStack == null ? maxStackSize : maxStackSize - this.itemStack.getQuantity(); final int quantity = stack.getQuantity(); if (quantity > availableSpace) { ItemStack newStack; if (this.itemStack == null) { newStack = stack.copy(); } else { newStack = this.itemStack.copy(); } newStack.setQuantity(maxStackSize); stack = stack.copy(); stack.setQuantity(quantity - availableSpace); transactions.add(new SlotTransaction(this, LanternItemStack.toSnapshot(this.itemStack), newStack.createSnapshot())); return new PeekOfferTransactionsResult(transactions, new FastOfferResult(stack, true)); } else { final ItemStack newStack; if (this.itemStack == null) { newStack = stack.copy(); } else { newStack = this.itemStack.copy(); newStack.setQuantity(newStack.getQuantity() + quantity); } transactions.add(new SlotTransaction(this, LanternItemStack.toSnapshot(this.itemStack), newStack.createSnapshot())); return new PeekOfferTransactionsResult(transactions, FastOfferResult.SUCCESS); } } @Override public InventoryTransactionResult set(@Nullable ItemStack stack) { stack = LanternItemStack.toNullable(stack); boolean fail = false; if (stack != null) { if (stack.getQuantity() <= 0) { stack = null; } else { fail = !isValidItem(stack); } } if (fail) { return InventoryTransactionResult.builder() .type(InventoryTransactionResult.Type.FAILURE) .reject(stack) .build(); } InventoryTransactionResult.Builder resultBuilder = InventoryTransactionResult.builder() .type(InventoryTransactionResult.Type.SUCCESS); if (this.itemStack != null) { resultBuilder.replace(this.itemStack); } if (stack != null) { stack = stack.copy(); final int maxStackSize = Math.min(stack.getMaxStackQuantity(), this.maxStackSize); final int quantity = stack.getQuantity(); if (quantity > maxStackSize) { stack.setQuantity(maxStackSize); // Create the rest stack that was rejected, // because the inventory doesn't allow so many items stack = stack.copy(); stack.setQuantity(quantity - maxStackSize); resultBuilder.reject(stack); } } this.itemStack = stack; queueUpdate(); return resultBuilder.build(); } @Override public void clear() { this.itemStack = null; } @Override public int size() { return this.itemStack == null ? 0 : 1; } @Override public int totalItems() { return this.itemStack == null ? 0 : this.itemStack.getQuantity(); } @Override public int capacity() { return 1; } @Override public boolean hasChildren() { return false; } @Override public boolean contains(ItemStack stack) { checkNotNull(stack, "stack"); return this.itemStack != null && ((LanternItemStack) this.itemStack).isSimilar(stack); } @Override public boolean contains(ItemType type) { checkNotNull(type, "type"); return this.itemStack != null && this.itemStack.getItem().equals(type); } @Override public int getMaxStackSize() { return this.maxStackSize; } @Override public void setMaxStackSize(int size) { checkArgument(size > 0, "Size must be greater then 0"); this.maxStackSize = size; } @SuppressWarnings("unchecked") @Override public <T extends Inventory> T query(Predicate<Inventory> matcher, boolean nested) { return (T) empty(); } /** * Gets whether the content of this slot should be offered * in the reverse offer to the main inventory when retrieving * the items through shift click. * * TODO: A cleaner way to implement this? * * @return Is reverse offer order */ public boolean isReverseShiftClickOfferOrder() { return true; } public boolean doesAllowShiftClickOffer() { return true; } }