diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/HUDManager.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/HUDManager.java index 73fae80..de674ed 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/HUDManager.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/HUDManager.java @@ -104,7 +104,7 @@ public class HUDManager implements HUDWorkspace { InventoryComponent component = render.createComponent(inventory, this); openInventory(component); } catch (Exception e) { - throw CrashReports.report(null, "Could not open inventory %s", inventory.getId()); + throw CrashReports.report(e, "Could not open inventory %s", inventory.getId()); } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/InteractiveSlotComponent.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/InteractiveSlotComponent.java index bdf87d0..af030ae 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/InteractiveSlotComponent.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/InteractiveSlotComponent.java @@ -25,10 +25,9 @@ import ru.windcorp.progressia.client.graphics.input.KeyMatcher; import ru.windcorp.progressia.client.graphics.input.WheelScrollEvent; import ru.windcorp.progressia.common.Units; import ru.windcorp.progressia.common.world.item.ItemDataContainer; -import ru.windcorp.progressia.common.world.item.Items; import ru.windcorp.progressia.common.world.item.inventory.ItemContainer; -import ru.windcorp.progressia.common.world.item.inventory.ItemContainerMixed; import ru.windcorp.progressia.common.world.item.inventory.ItemSlot; +import ru.windcorp.progressia.common.world.item.inventory.Items; public class InteractiveSlotComponent extends Button { @@ -77,7 +76,7 @@ public class InteractiveSlotComponent extends Button { private void onMainAction() { ItemSlot handSlot = workspace.getHand().slot(); - ItemSlot invSlot = getSlotOrAllocate(); + ItemSlot invSlot = getSlot(); if (invSlot == null) { return; } @@ -108,13 +107,12 @@ public class InteractiveSlotComponent extends Button { if (success) { requestReassembly(); } - - cleanContainerUpIfNecessary(); } private void pickAll(ItemSlot handSlot) { - for (ItemSlot invSlot : getSlot().getContainer()) { - Items.pour(invSlot, handSlot); + int maxIndex = getSlot().getContainer().getMaxIndex(); + for (int index = 0; index < maxIndex; ++index) { + Items.pour(new ItemSlot(getSlot().getContainer(), index), handSlot); if (handSlot.isEmpty()) { break; } @@ -123,7 +121,7 @@ public class InteractiveSlotComponent extends Button { private void onAltAction() { ItemSlot handSlot = workspace.getHand().slot(); - ItemSlot invSlot = getSlotOrAllocate(); + ItemSlot invSlot = getSlot(); if (invSlot == null) { return; } @@ -135,7 +133,7 @@ public class InteractiveSlotComponent extends Button { } if (!success && handSlot.isEmpty()) { - success = Items.pour(invSlot, handSlot, invSlot.getAmount() / 2) != 0; + success = Items.pour(invSlot, handSlot, invSlot.getCount() / 2) != 0; } if (!success) { @@ -149,19 +147,17 @@ public class InteractiveSlotComponent extends Button { if (success) { requestReassembly(); } - - cleanContainerUpIfNecessary(); } private boolean tryToOpen(ItemSlot invSlot) { - if (invSlot.getAmount() != 1) { + if (invSlot.getCount() != 1) { return false; } - if (!(invSlot.getContents() instanceof ItemDataContainer)) { + if (!(invSlot.getItem() instanceof ItemDataContainer)) { return false; } - ItemDataContainer item = (ItemDataContainer) invSlot.getContents(); + ItemDataContainer item = (ItemDataContainer) invSlot.getItem(); return item.open(workspace.getPlayerEntity()) != null; } @@ -181,38 +177,4 @@ public class InteractiveSlotComponent extends Button { return slotComponent.getSlot(); } - private ItemSlot getSlotOrAllocate() { - ItemSlot slot = slotComponent.getSlot(); - - if (slot == null) { - - int slotIndex = slotComponent.getSlotIndex(); - ItemContainer uncastContainer = slotComponent.getSlotContainer(); - - assert slotIndex >= 0 : "Slot index is negative: " + slotIndex; - assert slotIndex >= uncastContainer.getSlotCount() - : "Slot index is valid (" + slotIndex + ") but container does not provide a slot"; - - if (uncastContainer instanceof ItemContainerMixed) { - ItemContainerMixed container = (ItemContainerMixed) uncastContainer; - container.addSlots(slotIndex - container.getSlotCount() + 1); - - slot = slotComponent.getSlot(); - assert slot != null : "Could not allocate slot for index " + slotIndex; - } else { - // Leave slot null - } - - } - - return slot; - } - - private void cleanContainerUpIfNecessary() { - ItemContainer container = slotComponent.getSlotContainer(); - if (container instanceof ItemContainerMixed) { - ((ItemContainerMixed) container).cleanUpSlots(); - } - } - } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/InventoryWindow.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/InventoryWindow.java index 926003d..e29a59c 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/InventoryWindow.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/InventoryWindow.java @@ -43,6 +43,8 @@ public class InventoryWindow extends Panel { private final InventoryComponent content; private final HUDWorkspace workspace; + + Object layoutCookie; public InventoryWindow(String name, InventoryComponent component, HUDWorkspace workspace) { super(name, new LayoutVertical(15, 15)); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/SlotComponent.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/SlotComponent.java index f3df4d1..7319794 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/SlotComponent.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/SlotComponent.java @@ -51,8 +51,7 @@ public class SlotComponent extends Component { private static Renderable containerOpenDecoration = null; private static Renderable containerOpenableDecoration = null; - private final ItemContainer container; - private final int index; + private final ItemSlot slot; private float scale = 2; @@ -66,8 +65,7 @@ public class SlotComponent extends Component { public SlotComponent(String name, ItemContainer container, int index) { super(name); - this.container = container; - this.index = index; + this.slot = new ItemSlot(container, index); setScale(2); @@ -95,18 +93,18 @@ public class SlotComponent extends Component { * @return the container */ public ItemContainer getSlotContainer() { - return container; + return slot.getContainer(); } /** * @return the index */ public int getSlotIndex() { - return index; + return slot.getIndex(); } public ItemSlot getSlot() { - return container.getSlot(index); + return slot; } public SlotComponent setScale(float scale) { @@ -142,14 +140,7 @@ public class SlotComponent extends Component { private void updateItemRenderer() { ItemData contents; - - ItemSlot slot = getSlot(); - - if (slot == null) { - contents = null; - } else { - contents = slot.getContents(); - } + contents = slot.getItem(); if (contents == null) { itemRenderer = null; @@ -160,7 +151,7 @@ public class SlotComponent extends Component { itemRenderer = ItemRenderRegistry.getInstance().get(contents.getId()).createRenderable(contents); } - int newAmount = slot.getAmount(); + int newAmount = slot.getCount(); if (newAmount != amountDisplayInt) { amountDisplayInt = newAmount; amountDisplayString = newAmount == 1 ? "" : Integer.toString(newAmount); @@ -195,7 +186,7 @@ public class SlotComponent extends Component { } private void renderDecorations(ShapeRenderHelper renderer) { - ItemData contents = getSlot().getContents(); + ItemData contents = getSlot().getItem(); if (contents instanceof ItemDataContainer) { ItemDataContainer asContainer = (ItemDataContainer) contents; diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/WindowedHUD.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/WindowedHUD.java index ff44d61..08041a8 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/WindowedHUD.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/hud/WindowedHUD.java @@ -17,13 +17,49 @@ */ package ru.windcorp.progressia.client.graphics.world.hud; +import glm.vec._2.Vec2; +import glm.vec._2.i.Vec2i; import ru.windcorp.progressia.client.graphics.gui.Component; +import ru.windcorp.progressia.client.graphics.gui.Layout; public class WindowedHUD extends Component { + + private static class Cookie { + private final Vec2 relPos = new Vec2(); + } public WindowedHUD(String name) { super(name); - setLayout(null); + setLayout(new Layout() { + @Override + public void layout(Component c) { + for (Component component : c.getChildren()) { + InventoryWindow window = (InventoryWindow) component; + + window.setSize(window.getPreferredSize()); + + Cookie cookie = (Cookie) window.layoutCookie; + + if (cookie == null) { + window.layoutCookie = cookie = new Cookie(); + cookie.relPos.x = 0.5f; + cookie.relPos.y = 2 / 3.0f; + } + + cookie.relPos.clamp(0, 1); + + window.setPosition( + (int) (cookie.relPos.x * c.getWidth() - window.getWidth() / 2.0f), + (int) (cookie.relPos.y * c.getHeight() - window.getHeight()) + ); + } + } + + @Override + public Vec2i calculatePreferredSize(Component c) { + throw new AssertionError("welp this wasnt supposed to hapen :("); + } + }); } public void addWindow(InventoryWindow window) { diff --git a/src/main/java/ru/windcorp/progressia/client/world/item/inventory/ContainerComponentSimple.java b/src/main/java/ru/windcorp/progressia/client/world/item/inventory/ContainerComponentSimple.java index ed3daa3..cec3702 100644 --- a/src/main/java/ru/windcorp/progressia/client/world/item/inventory/ContainerComponentSimple.java +++ b/src/main/java/ru/windcorp/progressia/client/world/item/inventory/ContainerComponentSimple.java @@ -19,6 +19,7 @@ package ru.windcorp.progressia.client.world.item.inventory; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import com.google.common.eventbus.Subscribe; @@ -33,19 +34,24 @@ import ru.windcorp.progressia.client.graphics.world.hud.Bar; import ru.windcorp.progressia.client.graphics.world.hud.HUDWorkspace; import ru.windcorp.progressia.client.graphics.world.hud.InteractiveSlotComponent; import ru.windcorp.progressia.common.world.item.inventory.ItemContainer; -import ru.windcorp.progressia.common.world.item.inventory.event.ItemSlotAddedEvent; -import ru.windcorp.progressia.common.world.item.inventory.event.ItemSlotRemovedEvent; +import ru.windcorp.progressia.common.world.item.inventory.event.ItemSlotChangedEvent; public class ContainerComponentSimple extends ContainerComponent { private final Group slots = new Group("Inventory.Slots", new LayoutGrid(0, 15)); - private final Collection slotCollection = new ArrayList<>(); + private final List slotCollection = new ArrayList<>(); private final ItemContainer container; + private final HUDWorkspace workspace; + + private int tmp__getSlotsPerRow() { + return 6; + } public ContainerComponentSimple(ItemContainer container, HUDWorkspace workspace) { super("Inventory"); this.container = container; + this.workspace = workspace; if (container.getInventory() != null) { container.getInventory().subscribe(this); @@ -77,14 +83,12 @@ public class ContainerComponentSimple extends ContainerComponent { addChild(slotsAndVolumeBar.setLayoutHint(LayoutBorderHorizontal.CENTER)); addChild(massBar.setLayoutHint(LayoutBorderHorizontal.LEFT)); - - for (int i = 0; i < container.getSlotCount(); ++i) { - addSlot(container, i, workspace); - } + + onSlotChanged(null); } - private void addSlot(ItemContainer container, int index, HUDWorkspace workspace) { - final int maxX = 6; + private void addSlot(int index) { + final int maxX = tmp__getSlotsPerRow(); InteractiveSlotComponent component = new InteractiveSlotComponent("Inventory.Slot." + index, container, index, workspace); @@ -92,6 +96,10 @@ public class ContainerComponentSimple extends ContainerComponent { slots.addChild(component.setLayoutHint(pos)); slotCollection.add(component); } + + private void removeSlot(int index) { + slots.removeChild(slotCollection.remove(index)); + } @Override public ItemContainer getContainer() { @@ -104,13 +112,26 @@ public class ContainerComponentSimple extends ContainerComponent { } @Subscribe - private void onSlotAdded(ItemSlotAddedEvent e) { + private void onSlotChanged(ItemSlotChangedEvent e) { + if (e != null && e.getContainer() != container) { + return; + } - } - - @Subscribe - private void onSlotAdded(ItemSlotRemovedEvent e) { + int wantSlots = container.getLastFilledSlot(); + int slotsPerRow = tmp__getSlotsPerRow(); + wantSlots = (wantSlots / slotsPerRow + 2) * slotsPerRow; + if (wantSlots < slotsPerRow) { + wantSlots = slotsPerRow; + } + + while (wantSlots > slotCollection.size()) { + addSlot(slotCollection.size()); + } + + while (wantSlots < slotCollection.size()) { + removeSlot(slotCollection.size() - 1); + } } } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/ItemDataContainer.java b/src/main/java/ru/windcorp/progressia/common/world/item/ItemDataContainer.java index 71dde1a..8f15675 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/ItemDataContainer.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/ItemDataContainer.java @@ -17,12 +17,18 @@ */ package ru.windcorp.progressia.common.world.item; +import java.util.Iterator; + +import com.google.common.collect.Iterators; + import ru.windcorp.progressia.common.state.ObjectStateField; +import ru.windcorp.progressia.common.world.item.inventory.InventoryOwner; import ru.windcorp.progressia.common.world.item.inventory.InventorySimple; import ru.windcorp.progressia.common.world.item.inventory.InventoryUser; +import ru.windcorp.progressia.common.world.item.inventory.ItemContainer; import ru.windcorp.progressia.common.world.item.inventory.ItemContainerMixedSimple; -public class ItemDataContainer extends ItemData { +public class ItemDataContainer extends ItemData implements InventoryOwner, ItemDataWithContainers { private final ObjectStateField inventory = field("Core:Contents").setShared().def(this::createInventory) .build(); @@ -53,13 +59,19 @@ public class ItemDataContainer extends ItemData { protected InventorySimple createInventory() { return new InventorySimple( getId(), - new ItemContainerMixedSimple(getId(), containerMassLimit, containerVolumeLimit, 10) + this, + new ItemContainerMixedSimple(getId(), containerMassLimit, containerVolumeLimit) ); } public InventorySimple getInventory() { return inventory.get(this); } + + @Override + public Iterator getAllContainers() { + return Iterators.forArray(getInventory().getContainers()); + } public boolean isOpen() { return !getInventory().getUsers().isEmpty(); diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotAddedEvent.java b/src/main/java/ru/windcorp/progressia/common/world/item/ItemDataWithContainers.java similarity index 74% rename from src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotAddedEvent.java rename to src/main/java/ru/windcorp/progressia/common/world/item/ItemDataWithContainers.java index fa07ac6..dcf2a33 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotAddedEvent.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/ItemDataWithContainers.java @@ -15,14 +15,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package ru.windcorp.progressia.common.world.item.inventory.event; +package ru.windcorp.progressia.common.world.item; -import ru.windcorp.progressia.common.world.item.inventory.ItemSlot; +import java.util.Iterator; -public class ItemSlotAddedEvent extends ItemSlotEvent { +import ru.windcorp.progressia.common.world.item.inventory.ItemContainer; - public ItemSlotAddedEvent(ItemSlot slot) { - super(slot); - } +public interface ItemDataWithContainers { + + Iterator getAllContainers(); } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/Items.java b/src/main/java/ru/windcorp/progressia/common/world/item/Items.java deleted file mode 100644 index d6e8324..0000000 --- a/src/main/java/ru/windcorp/progressia/common/world/item/Items.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Progressia - * Copyright (C) 2020-2021 Wind Corporation and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package ru.windcorp.progressia.common.world.item; - -import ru.windcorp.progressia.common.world.item.inventory.ItemSlot; - -public class Items { - - /** - * Attempts to transfer as many items as possible, but no more than - * {@code max}, between two slots. The origin of the items is {@code from}, - * the destination is {@code into}. This method does nothing if the items - * could not be removed from origin or could not be inserted into - * destination, such as when {@code into} contains different items. - * - * @param from the slot to transfer item from - * @param into the destination of the items - * @param max the maximum amount of items to transfer. Use - * {@code Integer.MAX_VALUE} to remove the limit - * @return the actual amount of items moved between slots - */ - public static int pour(ItemSlot from, ItemSlot into, int max) { - synchronized (from) { - synchronized (into) { - - ItemData item = from.getContents(); - - int originalAmount = from.getAmount(); - int transferAmount = Math.min(originalAmount, max); - - while (transferAmount > 0 && !into.canInsert(item, transferAmount)) { - transferAmount--; - } - - if (transferAmount == 0) { - return 0; - } - - if (!from.canRemove(transferAmount)) { - return 0; - } - - into.setContents(item, into.getAmount() + transferAmount); - from.setAmount(originalAmount - transferAmount); - - return transferAmount; - - } - } - } - - /** - * Attempts to transfer as many items as possible between two slots. The - * origin of the items is {@code from}, the destination is {@code into}. - * This method does nothing if the items could not be removed from origin or - * could not be inserted into destination, such as when {@code into} - * contains different items. - * - * @param from the slot to transfer item from - * @param into the destination of the items - * @return the actual amount of items moved between slots - */ - public static int pour(ItemSlot from, ItemSlot into) { - return pour(from, into, Integer.MAX_VALUE); - } - - /** - * Attempts to swap the contents of the two slots. - * - * @param a one of the slots - * @param b the other slot - * - * @return whether the swap succeeded - */ - public static boolean swap(ItemSlot a, ItemSlot b) { - synchronized (a) { - synchronized (b) { - - ItemData aItem = a.getContents(); - int aAmount = a.getAmount(); - - ItemData bItem = b.getContents(); - int bAmount = b.getAmount(); - - a.clear(); - b.clear(); - - if (a.canInsert(bItem, bAmount) && b.canInsert(aItem, aAmount)) { - a.setContents(bItem, bAmount); - b.setContents(aItem, aAmount); - return true; - } else { - a.setContents(aItem, aAmount); - b.setContents(bItem, bAmount); - return false; - } - - } - } - } - - private Items() { - } - -} diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/LinearItemProperty.java b/src/main/java/ru/windcorp/progressia/common/world/item/LinearItemProperty.java new file mode 100644 index 0000000..d144eca --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/world/item/LinearItemProperty.java @@ -0,0 +1,75 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.common.world.item; + +import ru.windcorp.progressia.common.world.item.inventory.ItemContainer; +import ru.windcorp.progressia.common.world.item.inventory.ItemSlot; + +/** + * A generalization of mass and volume. Not to be extended by mods. + */ +public enum LinearItemProperty { + + MASS, + VOLUME; + + public float get(ItemData item) { + switch (this) { + case MASS: + return item.getMass(); + case VOLUME: + return item.getVolume(); + default: + throw new AssertionError(); + } + } + + public float get(ItemSlot slot) { + switch (this) { + case MASS: + return slot.getMass(); + case VOLUME: + return slot.getVolume(); + default: + throw new AssertionError(); + } + } + + public float get(ItemContainer container) { + switch (this) { + case MASS: + return container.getMass(); + case VOLUME: + return container.getVolume(); + default: + throw new AssertionError(); + } + } + + public float getLimit(ItemContainer container) { + switch (this) { + case MASS: + return container.getMassLimit(); + case VOLUME: + return container.getVolumeLimit(); + default: + throw new AssertionError(); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/Inventory.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/Inventory.java index 250233b..2e2ddfc 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/Inventory.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/Inventory.java @@ -36,19 +36,25 @@ import ru.windcorp.progressia.common.world.item.inventory.event.InventoryOpenedE public class Inventory extends Namespaced implements Encodable { + private final InventoryOwner owner; private final ItemContainer[] containers; private final List users = new ArrayList<>(); private EventBus eventBus = null; - public Inventory(String id, ItemContainer... containers) { + public Inventory(String id, InventoryOwner owner, ItemContainer... containers) { super(id); + this.owner = owner; this.containers = containers; for (ItemContainer container : containers) { container.setInventory(this); } } + public InventoryOwner getOwner() { + return owner; + } + public synchronized void open(InventoryUser user) { users.add(user); subscribe(user); diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotRemovedEvent.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/InventoryOwner.java similarity index 74% rename from src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotRemovedEvent.java rename to src/main/java/ru/windcorp/progressia/common/world/item/inventory/InventoryOwner.java index cd2ead7..bcf6d01 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotRemovedEvent.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/InventoryOwner.java @@ -15,14 +15,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package ru.windcorp.progressia.common.world.item.inventory.event; +package ru.windcorp.progressia.common.world.item.inventory; -import ru.windcorp.progressia.common.world.item.inventory.ItemSlot; - -public class ItemSlotRemovedEvent extends ItemSlotEvent { - - public ItemSlotRemovedEvent(ItemSlot slot) { - super(slot); - } +public interface InventoryOwner { } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/InventorySimple.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/InventorySimple.java index f0e70c1..9f122d2 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/InventorySimple.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/InventorySimple.java @@ -19,8 +19,8 @@ package ru.windcorp.progressia.common.world.item.inventory; public class InventorySimple extends Inventory { - public InventorySimple(String id, ItemContainer container) { - super(id, container); + public InventorySimple(String id, InventoryOwner owner, ItemContainer container) { + super(id, owner, container); } public ItemContainer getContainer() { diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainer.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainer.java index 9255fdd..453b27e 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainer.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainer.java @@ -18,79 +18,335 @@ package ru.windcorp.progressia.common.world.item.inventory; import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Consumer; +import gnu.trove.iterator.TIntIterator; +import gnu.trove.set.TIntSet; +import gnu.trove.set.hash.TIntHashSet; import ru.windcorp.progressia.common.state.Encodable; import ru.windcorp.progressia.common.util.namespaces.Namespaced; +import ru.windcorp.progressia.common.world.item.ItemData; +import ru.windcorp.progressia.common.world.item.ItemDataWithContainers; +import ru.windcorp.progressia.common.world.item.inventory.event.ItemSlotChangedEvent; /** - * A collection of {@link ItemSlot}s representing a single storage unit. A - * container may impose limits on the maximum total mass and volume of its - * contents, although this is not enforced on data structure level. + * The base class of item containers. An item container is a set of slots with + * defined limits on mass and volume. Containers are typically grouped into an + * {@link Inventory} by in-game function. *

- * At any moment container has a definite amount of slots, each identified by a - * unique index. If a container has n slots, its slots are numbered 0 - * through n - 1. + * A container contains some number of slots indexed from 0. Users may inspect + * the contents of any slot. Slots may contain an {@link ItemData} and an item + * count. Item count is zero iff the {@link ItemData} is {@code null}, in which + * case the slot is considered empty. + *

+ * Item containers also implement mass and volume calculation. Both properties + * are determined by the corresponding {@link ItemData} methods, and combine + * linearly: + * {@code mass(2 * stick + 5 * apple) = 2 * mass(stick) + 5 * mass(apple)}, + * empty slots do not contribute any mass of volume. Users may query these + * computed values. In addition, a limit may be imposed on both properties. + * Containers will refuse operations which would lead to the violation of these + * limits. However, containers cannot detect excess mass or volume resulting + * from + * a implementation-triggered change of limits. + *

+ * As a safeguard against memory leaks, all containers must be provided with a + * maximum possible size. Item containers will refuse operations on slots with + * indices exceeding or equal to the maximum possible size. + *

+ * Implementors will typically find the following subclasses useful: + *

    + *
  • {@link ItemContainerMixed} for containers allowing an arbitrary amount of + * slots + *
      + *
    • see {@link ItemContainerMixedSimple} for a complete implementation
    • + *
    + *
  • + *
  • {@link ItemContainerSingle} for containers providing only one slot
  • + *
*/ -public abstract class ItemContainer extends Namespaced implements Encodable, Iterable { - - private Inventory inventory; +public abstract class ItemContainer extends Namespaced implements Encodable { - public ItemContainer(String id) { + private Inventory inventory; + private final int maxPossibleSize; + + private final TIntSet subContainersCache = new TIntHashSet(); + + public ItemContainer(String id, int maxPossibleSize) { super(id); + this.maxPossibleSize = maxPossibleSize; } - + + /** + * @return the inventory + */ public Inventory getInventory() { return inventory; } - - protected void setInventory(Inventory inventory) { + + /** + * @param inventory the inventory to set + */ + void setInventory(Inventory inventory) { this.inventory = inventory; } /** - * Retrieves the slot with the given index. + * Retrieves the item in the slot at the specified index. * - * @param index the index of the slot to retrieve - * @return the slot or {@code null} if the slot does not exist + * @param index the index of the slot to query + * @return the item or {@code null} if the slot is empty. */ - public abstract ItemSlot getSlot(int index); + public abstract ItemData getItem(int index); /** - * Returns the current slot count of this container. + * Retrieves the item count in the slot at the specified index. * - * @return the number of slots in this container + * @param index the index of the slot to query + * @return the item count or {@code 0} if the slot is empty */ - public abstract int getSlotCount(); - - @Override - public abstract void forEach(Consumer action); - - @Override - public Iterator iterator() { - return new Iterator() { - - private int index = 0; + public abstract int getCount(int index); - @Override - public boolean hasNext() { - return index < getSlotCount(); - } + /** + * Returns a slot index strictly greater than all indices containing + * items. Use this index as upper bound when iterating the container's + * items. The slot at this index is empty. + * + * @return a strict upper bound on the indices of filled slots + */ + public abstract int getMaxIndex(); - @Override - public ItemSlot next() { - if (!hasNext()) { - throw new NoSuchElementException("index = " + index + ", size = " + getSlotCount()); - } - ItemSlot slot = getSlot(index); - index++; - return slot; - } - - }; + /** + * Computes and returns the slot index of the last non-empty slot in this + * container. This operation is potentially slower than + * {@link #getMaxIndex()}, which is less precise. + * + * @return the index of the last filled slot, or -1 if no such slot exists + */ + public int getLastFilledSlot() { + int index = getMaxIndex(); + while (isEmpty(index) && index >= 0) { + index--; + } + return index; } + /** + * Determines whether items of the specified kind could be inserted into + * this container assuming a slot is available and mass and volume + * requirements of the container are satisfied. + *

+ * This method should only be a function of the configuration of the + * container and of the item; it must not depend on the contents of the + * container. + * + * @param item the item to check, not null + * @return {@code true} iff the item is allowed in this container + */ + protected boolean canAdd(ItemData item) { + if (item == null) { + return false; + } + + if (containsRecursively(item, this)) { + return false; + } + + return true; + } + + private boolean containsRecursively(ItemData haystack, ItemContainer needle) { + if (!(haystack instanceof ItemDataWithContainers)) { + return false; + } + + ItemDataWithContainers haystackWithTastyHay = (ItemDataWithContainers) haystack; + + Iterator iterator = haystackWithTastyHay.getAllContainers(); + while (iterator.hasNext()) { + ItemContainer container = iterator.next(); + + if (container == needle) { + return true; + } + + TIntIterator indexIterator = container.subContainersCache.iterator(); + + while (indexIterator.hasNext()) { + + int subHaystackIndex = indexIterator.next(); + ItemData subHaystackOrMaybeNot = container.getItem(subHaystackIndex); + if (containsRecursively(subHaystackOrMaybeNot, needle)) { + return true; + } + + } + } + + return false; + } + + /** + * Determines whether inventory users are allowed to manually remove items + * from this container. + *

+ * This method should only be a function of the configuration of the + * container; it must not depend on the contents of the container. + * + * @return {@code true} iff items could be removed from this container + */ + protected boolean isRemovingAllowed() { + return true; + } + + /** + * Determines whether the items can be added at the specified index. The + * slot must already contain items equal to {@code item} parameter or be + * empty. Neither total mass nor volume of the added items may exceed the + * available mass and volume of the container. Additional restrictions may + * apply. + *

+ * When index is valid, {@code item == null} and {@code count == 0}, this + * method returns {@code true}. Otherwise, when {@code count <= 0}, this + * method returns {@code false}. + * + * @param index the index of the slot to query + * @param item the item type + * @param count the amount of items to add + * @return {@code true} iff the operation is possible + */ + public boolean canAdd(int index, ItemData item, int count) { + if (index < 0 || index >= maxPossibleSize) { + return false; + } + + if (item == null && count == 0) { + return true; + } + + if (count < 0) { + return false; + } + + ItemData currentItem = getItem(index); + if (currentItem == null) { + // Pass + } else if (currentItem.equals(item)) { + // Pass + } else { + return false; + } + + if (!canAdd(item)) { + return false; + } + + float addedMass = item.getMass() * count; + if (getMass() + addedMass > getMassLimit()) { + return false; + } + + float addedVolume = item.getVolume() * count; + if (getVolume() + addedVolume > getVolumeLimit()) { + return false; + } + + return true; + } + + /** + * Determines whether the items can be removed from the specified index. The + * slot must already contain items equal to {@code item} parameter. The item + * count must be no lower than {@code count} parameter. Additional + * restrictions may apply. + *

+ * When index is valid, {@code item == null} and {@code count == 0}, this + * method returns {@code true}. Otherwise, when {@code count <= 0}, this + * method returns {@code false}. + * + * @param index the index of the slot to query + * @param item the item type + * @param count the amount of items to remove + * @return {@code true} iff the operation is possible + */ + public boolean canRemove(int index, ItemData item, int count) { + if (index < 0 || index >= maxPossibleSize) { + return false; + } + + if (item == null && count == 0) { + return true; + } + + if (count < 0) { + return false; + } + + if (!isRemovingAllowed()) { + return false; + } + + ItemData currentItem = getItem(index); + if (currentItem == null) { + return false; + } else if (currentItem.equals(item)) { + // Pass + } else { + return false; + } + + if (getCount(index) < count) { + return false; + } + + return true; + } + + /** + * Attempts to add the provided items to the specified slot in the + * container. This method modifies the data structure directly; use an + * appropriate {@link Items} method to add items in a safe and convenient + * way. + *

+ * A {@link #canAdd(int, ItemData, int)} check is performed. If the check + * fails, the method does not alter the contents of the container and + * returns {@code false}. If the check succeeds, the item type and item + * count of the referenced slot are altered appropriately and {@code true} + * is returned. + *

+ * When {@code item == null} or {@code count <= 0}, this method returns + * {@code false}. + * + * @param index the index of the slot to alter + * @param item the item type + * @param count the amount of items to add + * @return {@code true} iff the container was changed as the result of this + * operation + */ + protected abstract boolean add(int index, ItemData item, int count); + + /** + * Attempts to remove the provided items from the specified slot in the + * container. This method modifies the data structure directly; use an + * appropriate {@link Items} method to remove items in a safe and convenient + * way. + *

+ * A {@link #canRemove(int, ItemData, int)} check is performed. If the check + * fails, the method does not alter the contents of the container and + * returns {@code false}. If the check succeeds, the item type and item + * count of the referenced slot are altered appropriately and {@code true} + * is returned. + *

+ * When {@code item == null} or {@code count <= 0}, this method returns + * {@code false}. + * + * @param index the index of the slot to alter + * @param item the item type + * @param count the amount of items to remove + * @return {@code true} iff the container was changed as the result of this + * operation + */ + protected abstract boolean remove(int index, ItemData item, int count); + /** * Computes and returns the mass limit that the container imposes. * @@ -108,33 +364,162 @@ public abstract class ItemContainer extends Namespaced implements Encodable, Ite * boundary is set */ public abstract float getVolumeLimit(); - + public synchronized float getMass() { float sum = 0; - for (int i = 0; i < getSlotCount(); ++i) { - ItemSlot slot = getSlot(i); - - if (slot.isEmpty()) { + for (int i = 0; i < getMaxIndex(); ++i) { + ItemData data = getItem(i); + + if (data == null) { continue; } - - sum += slot.getContents().getMass() * slot.getAmount(); - } - return sum; - } - - public synchronized float getVolume() { - float sum = 0; - for (int i = 0; i < getSlotCount(); ++i) { - ItemSlot slot = getSlot(i); - - if (slot.isEmpty()) { - continue; - } - - sum += slot.getContents().getVolume() * slot.getAmount(); + + sum += data.getMass() * getCount(i); } return sum; } + public synchronized float getVolume() { + float sum = 0; + for (int i = 0; i < getMaxIndex(); ++i) { + ItemData data = getItem(i); + + if (data == null) { + continue; + } + + sum += data.getVolume() * getCount(i); + } + return sum; + } + + protected void fireSlotChangeEvent(int index) { + if (getItem(index) instanceof ItemDataWithContainers) { + subContainersCache.add(index); + } else { + subContainersCache.remove(index); + } + + Inventory inventory = this.inventory; + if (inventory == null || inventory.getEventBus() == null) { + return; + } + + inventory.getEventBus().post(new ItemSlotChangedEvent(this, index)); + } + + /** + * Checks class invariants and throws an {@link IllegalStateException} in + * case of discrepancies. + */ + protected synchronized void checkState() { + int maxIndex = getMaxIndex(); + for (int index = 0; index < maxIndex; ++index) { + + ItemData item = getItem(index); + int count = getCount(index); + + if ((item == null) != (count == 0)) { + if (item == null) { + throw new IllegalStateException("Item is null but count (" + count + ") != 0 in slot " + index); + } else { + throw new IllegalStateException("Item is " + item + " but count is zero in slot " + index); + } + } + + if (count < 0) { + throw new IllegalStateException("count is negative: " + count + " in slot " + index); + } + + boolean isContainer = item instanceof ItemDataWithContainers; + if (isContainer != subContainersCache.contains(index)) { + if (!isContainer) { + throw new IllegalStateException( + "subContainersCache is invalid: item in slot " + index + " (" + item + + ") is cached as a container" + ); + } else { + throw new IllegalStateException( + "subContainersCache is invalid: item in slot " + index + " (" + item + + ") is not cached as a container" + ); + } + } + + if (isContainer) { + if (containsRecursively(item, this)) { + throw new IllegalStateException("Recursion detected in slot " + index); + } + } + + } + + // Using negation in following checks to trigger errors if any value is + // NaN + // (since all comparisons return false if any operand is NaN) + + float mass = getMass(); + if (!(mass >= 0)) { + throw new IllegalStateException("Mass is negative: " + mass); + } + + float massLimit = getMassLimit(); + if (!(mass <= massLimit)) { + throw new IllegalStateException("Mass is greater than mass limit: " + mass + " > " + massLimit); + } + + float volume = getVolume(); + if (!(volume >= 0)) { + throw new IllegalStateException("Volume is negative: " + volume); + } + + float volumeLimit = getVolumeLimit(); + if (!(volume <= volumeLimit)) { + throw new IllegalStateException("Volume is greater than volume limit: " + volume + " > " + volumeLimit); + } + } + + @FunctionalInterface + public interface SlotConsumer { + void accept(ItemData item, int count); + } + + /** + * Invokes the provided action for each slot in this container. The action + * is run for empty slots, in which case call is invoked with parameters + * {@code (null, 0)} for each empty slot. The exact amount of invocations is + * determined by {@link #getMaxIndex()}. + * + * @param action the action to run + * @see #forEachItem(SlotConsumer) + */ + public synchronized void forEachSlot(SlotConsumer action) { + int maxIndex = getMaxIndex(); + for (int i = 0; i < maxIndex; ++i) { + action.accept(getItem(i), getCount(i)); + } + } + + /** + * Invokes the provided action for each non-empty slot in this container. + * + * @param action the action to run + * @see #forEachSlot(SlotConsumer) + */ + public synchronized void forEachItem(SlotConsumer action) { + int maxIndex = getMaxIndex(); + for (int i = 0; i < maxIndex; ++i) { + + int count = getCount(i); + if (count != 0) { + action.accept(getItem(i), count); + } + + } + } + + public boolean isEmpty(int index) { + return getCount(index) == 0; + } + } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerEquipment.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerEquipment.java index 1b356d0..577faaa 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerEquipment.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerEquipment.java @@ -28,10 +28,20 @@ public class ItemContainerEquipment extends ItemContainerSingle { private final SpeciesData.EquipmentSlot equipmentSlot; public ItemContainerEquipment(String id, SpeciesData.EquipmentSlot equipmentSlot) { - super(id, EQUIP_MASS_LIMIT, EQUIP_VOLUME_LIMIT); + super(id); this.equipmentSlot = equipmentSlot; } + @Override + public float getMassLimit() { + return EQUIP_MASS_LIMIT; + } + + @Override + public float getVolumeLimit() { + return EQUIP_VOLUME_LIMIT; + } + public SpeciesData.EquipmentSlot getEquipmentSlot() { return equipmentSlot; } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerHand.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerHand.java index 0671173..d3ff953 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerHand.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerHand.java @@ -29,10 +29,20 @@ public class ItemContainerHand extends ItemContainerSingle { private final SpeciesData.Hand hand; public ItemContainerHand(String id, Hand hand) { - super(id, HAND_MASS_LIMIT, HAND_VOLUME_LIMIT); + super(id); this.hand = hand; } + @Override + public float getMassLimit() { + return HAND_MASS_LIMIT; + } + + @Override + public float getVolumeLimit() { + return HAND_VOLUME_LIMIT; + } + public SpeciesData.Hand getHand() { return hand; } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerMixed.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerMixed.java index 63881e2..59388c9 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerMixed.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerMixed.java @@ -20,198 +20,214 @@ package ru.windcorp.progressia.common.world.item.inventory; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; - -import com.google.common.eventbus.Subscribe; - import ru.windcorp.progressia.common.state.Encodable; import ru.windcorp.progressia.common.state.IOContext; -import ru.windcorp.progressia.common.world.item.inventory.event.ItemSlotAddedEvent; -import ru.windcorp.progressia.common.world.item.inventory.event.ItemSlotChangedEvent; -import ru.windcorp.progressia.common.world.item.inventory.event.ItemSlotRemovedEvent; +import ru.windcorp.progressia.common.world.item.ItemData; +import ru.windcorp.progressia.common.world.item.ItemDataRegistry; -/** - * An {@link ItemContainer} capable of storing multiple item stacks. The set - * of slots is dynamic: new slots may be added and existing slots may be removed - * at will. - */ public abstract class ItemContainerMixed extends ItemContainer { + + public static final int MAX_SLOTS = 10000; - private final List slots = new ArrayList<>(); + private static final int GROWTH_STEP = 10; + private static final int MINIMUM_CAPACITY = 10; + + private ItemData[] items = new ItemData[MINIMUM_CAPACITY]; + private int[] counts = new int[MINIMUM_CAPACITY]; public ItemContainerMixed(String id) { - super(id); + super(id, MAX_SLOTS); } - @Override - protected void setInventory(Inventory inventory) { - if (getInventory() != null) { - getInventory().unsubscribe(this); + protected void setCapacity(int minimumCapacity) { + if (minimumCapacity < 0) { + return; } - super.setInventory(inventory); + int newCapacity = ((minimumCapacity - MINIMUM_CAPACITY - 1) / GROWTH_STEP + 1) * GROWTH_STEP + MINIMUM_CAPACITY; - if (getInventory() != null) { - getInventory().subscribe(this); - } - } - - /** - * Appends additional empty slots to the end of this container. - * - * @param amount the amount of slots to add - */ - public synchronized void addSlots(int amount) { - Inventory inventory = getInventory(); + ItemData[] newItems = new ItemData[newCapacity]; + int[] newCounts = new int[newCapacity]; - for (int i = 0; i < amount; ++i) { - ItemSlot slot = createSlot(slots.size()); - slots.add(slot); - slot.setContainer(this); - - if (inventory != null) { - inventory.getEventBus().post(new ItemSlotAddedEvent(slot)); - } - } + int length = Math.min(this.items.length, newItems.length); + System.arraycopy(this.items, 0, newItems, 0, length); + System.arraycopy(this.counts, 0, newCounts, 0, length); + + this.items = newItems; + this.counts = newCounts; } - /** - * Instantiates a new slot object that will be appended to the container. - * - * @param index the index that the new slot will receive - * @return the new slot - */ - protected abstract ItemSlot createSlot(int index); - - @Subscribe - private void onSlotChanged(ItemSlotChangedEvent e) { - cleanUpSlots(); - } - - public void cleanUpSlots() { - Inventory inventory = getInventory(); - Collection events = null; - - // Do not remove slot 0 - for (int i = slots.size() - 1; i > 0; --i) { - ItemSlot slot = slots.get(i); - if (slot.isEmpty()) { - slots.remove(i); - - if (inventory != null) { - if (events == null) { - events = new ArrayList<>(slots.size() - i); - } - events.add(new ItemSlotRemovedEvent(slot)); - } - } else { - break; - } + protected void ensureCapacity(int minimumCapacity) { + if (items.length >= minimumCapacity) { + return; } - if (events != null) { - // events != null only if inventory != null - events.forEach(inventory.getEventBus()::post); - } - } - - @Override - public ItemSlot getSlot(int index) { - if (index < 0 || index >= slots.size()) { - return null; - } - return slots.get(index); - } - - @Override - public int getSlotCount() { - return slots.size(); - } - - @Override - public void forEach(Consumer action) { - slots.forEach(action); + setCapacity(minimumCapacity); } @Override public synchronized void read(DataInput input, IOContext context) throws IOException { - synchronized (slots) { + int size = input.readInt(); + + ensureCapacity(size); - int needSlots = input.readInt(); - int hasSlots = slots.size(); + for (int index = 0; index < size; ++index) { - int costOfResetting = needSlots; - int costOfEditing = Math.abs(needSlots - hasSlots); + ItemData item; + int count = input.readInt(); - if (costOfResetting < costOfEditing) { - slots.clear(); - addSlots(needSlots); + if (count != 0) { + String id = input.readUTF(); + item = ItemDataRegistry.getInstance().create(id); + item.read(input, context); } else { - while (slots.size() > needSlots) { - slots.remove(slots.size() - 1); - } - - if (slots.size() < needSlots) { - addSlots(needSlots - slots.size()); - } + item = null; } - for (int i = 0; i < needSlots; ++i) { - slots.get(i).read(input, context); - } + items[index] = item; + counts[index] = count; + + fireSlotChangeEvent(index); } + + checkState(); } @Override public synchronized void write(DataOutput output, IOContext context) throws IOException { - synchronized (slots) { - output.writeInt(slots.size()); - for (int i = 0; i < slots.size(); ++i) { - slots.get(i).write(output, context); + int size = items.length; + output.writeInt(size); + + for (int index = 0; index < size; ++index) { + output.writeInt(counts[index]); + ItemData item = items[index]; + + if (item != null) { + output.writeUTF(item.getId()); + item.write(output, context); } - } + } @Override public void copy(Encodable destination) { - ItemContainerMixed container = (ItemContainerMixed) destination; - List mySlots = this.slots; - List containerSlots = container.slots; + ItemContainerMixed other = (ItemContainerMixed) destination; + int myLength = this.items.length; - synchronized (mySlots) { - synchronized (containerSlots) { + synchronized (this) { + synchronized (other) { + + other.setCapacity(myLength); + System.arraycopy(this.counts, 0, other.counts, 0, myLength); - int needSlots = mySlots.size(); - int hasSlots = containerSlots.size(); + for (int i = 0; i < myLength; ++i) { + ItemData myItem = this.items[i]; + ItemData otherItem; - int costOfResetting = needSlots; - int costOfEditing = Math.abs(needSlots - hasSlots); - - if (costOfResetting < costOfEditing) { - containerSlots.clear(); - container.addSlots(needSlots); - } else { - while (containerSlots.size() > needSlots) { - slots.remove(containerSlots.size() - 1); + if (myItem == null) { + otherItem = null; + } else { + otherItem = ItemDataRegistry.getInstance().create(myItem.getId()); + myItem.copy(otherItem); } - if (containerSlots.size() < needSlots) { - addSlots(needSlots - containerSlots.size()); - } - } - - for (int i = 0; i < needSlots; ++i) { - mySlots.get(i).copy(containerSlots.get(i)); + other.items[i] = otherItem; + other.fireSlotChangeEvent(i); } } } } + @Override + public ItemData getItem(int index) { + if (index < 0 || index >= items.length) { + return null; + } + return items[index]; + } + + @Override + public int getCount(int index) { + if (index < 0 || index >= counts.length) { + return 0; + } + return counts[index]; + } + + @Override + public int getMaxIndex() { + return items.length; + } + + @Override + protected boolean add(int index, ItemData item, int count) { + if (!canAdd(index, item, count)) { + return false; + } + + if (item != null) { + ensureCapacity(index + 1); + this.items[index] = item; + this.counts[index] += count; + fireSlotChangeEvent(index); + checkState(); + } + + return true; + } + + @Override + protected boolean remove(int index, ItemData item, int count) { + if (!canRemove(index, item, count)) { + return false; + } + + if (count != 0) { + this.counts[index] -= count; + + if (this.counts[index] == 0) { + this.items[index] = null; + shrinkIfPossible(); + } + + fireSlotChangeEvent(index); + checkState(); + } + + return true; + } + + protected void shrinkIfPossible() { + int upperBound; + + for (upperBound = counts.length; upperBound > MINIMUM_CAPACITY; --upperBound) { + if (counts[upperBound - 1] != 0) { + break; + } + } + + if (upperBound != counts.length) { + setCapacity(upperBound); + } + } + + @Override + protected synchronized void checkState() { + super.checkState(); + + if (items.length > MAX_SLOTS) { + throw new IllegalStateException("Container has more than " + MAX_SLOTS + " slots (items): " + items.length); + } + + if (counts.length > MAX_SLOTS) { + throw new IllegalStateException( + "Container has more than " + MAX_SLOTS + " slots (counts): " + counts.length + ); + } + } + } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerMixedSimple.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerMixedSimple.java index 7a3ef15..3b78911 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerMixedSimple.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerMixedSimple.java @@ -21,22 +21,11 @@ public class ItemContainerMixedSimple extends ItemContainerMixed { private final float massLimit; private final float volumeLimit; - - public ItemContainerMixedSimple(String id, float massLimit, float volumeLimit) { - this(id, massLimit, volumeLimit, 1); - } - public ItemContainerMixedSimple(String id, float massLimit, float volumeLimit, int startingSlots) { + public ItemContainerMixedSimple(String id, float massLimit, float volumeLimit) { super(id); this.massLimit = massLimit; this.volumeLimit = volumeLimit; - - addSlots(startingSlots); - } - - @Override - public ItemSlot createSlot(int index) { - return new ItemSlot(); } @Override diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerSingle.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerSingle.java index b8e95c2..7d5c4b3 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerSingle.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemContainerSingle.java @@ -20,48 +20,89 @@ package ru.windcorp.progressia.common.world.item.inventory; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; -import java.util.function.Consumer; import ru.windcorp.progressia.common.state.Encodable; import ru.windcorp.progressia.common.state.IOContext; +import ru.windcorp.progressia.common.world.item.ItemData; +import ru.windcorp.progressia.common.world.item.ItemDataRegistry; public abstract class ItemContainerSingle extends ItemContainer { - private final ItemSlot slot = new ItemSlot(); - - private final float massLimit; - private final float volumeLimit; + private ItemData item; + private int count; + private ItemSlot slot = new ItemSlot(this, 0); - public ItemContainerSingle(String id, float massLimit, float volumeLimit) { - super(id); - this.massLimit = massLimit; - this.volumeLimit = volumeLimit; + public ItemContainerSingle(String id) { + super(id, 1); + } + + @Override + public synchronized void read(DataInput input, IOContext context) throws IOException { + count = input.readInt(); + if (count != 0) { + String id = input.readUTF(); + item = ItemDataRegistry.getInstance().create(id); + item.read(input, context); + } else { + item = null; + } - slot.setContainer(this); + fireSlotChangeEvent(0); + checkState(); } @Override - public void read(DataInput input, IOContext context) throws IOException { - slot.read(input, context); - } - - @Override - public void write(DataOutput output, IOContext context) throws IOException { - slot.write(output, context); + public synchronized void write(DataOutput output, IOContext context) throws IOException { + output.writeInt(count); + if (item != null) { + output.writeUTF(item.getId()); + item.write(output, context); + } } @Override public void copy(Encodable destination) { - slot.copy(((ItemContainerSingle) destination).slot); + ItemContainerSingle other = (ItemContainerSingle) destination; + + synchronized (this) { + synchronized (other) { + other.count = this.count; + + if (this.item == null) { + other.item = null; + } else { + if (other.item == null || !other.item.isLike(this.item)) { + other.item = ItemDataRegistry.getInstance().create(this.item.getId()); + } + this.item.copy(other.item); + other.fireSlotChangeEvent(0); + } + } + } } @Override - public ItemSlot getSlot(int index) { - if (index == 0) { - return slot; - } else { + public ItemData getItem(int index) { + if (index != 0) { return null; } + return item; + } + + public ItemData getItem() { + return item; + } + + @Override + public int getCount(int index) { + if (index != 0) { + return 0; + } + return count; + } + + public int getCount() { + return count; } public ItemSlot slot() { @@ -69,23 +110,42 @@ public abstract class ItemContainerSingle extends ItemContainer { } @Override - public int getSlotCount() { + public int getMaxIndex() { return 1; } - + @Override - public void forEach(Consumer action) { - action.accept(slot); + protected boolean add(int index, ItemData item, int count) { + if (!canAdd(index, item, count)) { + return false; + } + + if (item != null) { + this.item = item; + this.count += count; + fireSlotChangeEvent(0); + checkState(); + } + + return true; } - + @Override - public float getMassLimit() { - return massLimit; - } - - @Override - public float getVolumeLimit() { - return volumeLimit; + protected boolean remove(int index, ItemData item, int count) { + if (!canRemove(index, item, count)) { + return false; + } + + if (count != 0) { + this.count -= count; + if (this.count == 0) { + this.item = null; + } + fireSlotChangeEvent(0); + checkState(); + } + + return true; } } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemSlot.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemSlot.java index 47860ba..e2e1df2 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemSlot.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/ItemSlot.java @@ -17,182 +17,124 @@ */ package ru.windcorp.progressia.common.world.item.inventory; -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; - -import ru.windcorp.progressia.common.state.Encodable; -import ru.windcorp.progressia.common.state.IOContext; import ru.windcorp.progressia.common.world.item.ItemData; -import ru.windcorp.progressia.common.world.item.ItemDataRegistry; -import ru.windcorp.progressia.common.world.item.inventory.event.ItemSlotChangedEvent; /** - * An entity optionally containing an {@link ItemData}. Item slots are typically - * found in {@link ItemContainerMixed}s. + * A reference to a slot in a container. The container and the index of an + * {@code ItemSlot} cannot be changed. + *

+ * {@code ItemSlot}s are wrapper objects; there may be multiple objects + * referencing a single slot. Slot objects are considered + * {@linkplain #equals(Object) equal} iff their indices are equal and they refer + * to the same container. + *

+ * This class provides public methods for fetching slot contents but not for + * changing them. To alter a slot, use an appropriate method from {@link Items}. */ -public class ItemSlot implements Encodable { +public class ItemSlot { + + private final ItemContainer container; + private final int index; + + public ItemSlot(ItemContainer container, int index) { + this.container = container; + this.index = index; + } - private ItemData contents; - private int amount; - - private ItemContainer container; - /** * @return the container */ public ItemContainer getContainer() { return container; } - + + /** + * @return the index + */ + public int getIndex() { + return index; + } + public Inventory getInventory() { - return getContainer().getInventory(); - } - - /** - * @param container the container to set - */ - void setContainer(ItemContainer container) { - this.container = container; + return container.getInventory(); } - /** - * Retrieves the contents of this slot. - * - * @return the stored {@link ItemData} or {@code null} - */ - public synchronized final ItemData getContents() { - return contents; + public ItemData getItem() { + return container.getItem(index); } - /** - * Sets the new contents of this slot. If an item stack was present - * previously, it is discarded. If the contents are {@code null}, the slot - * is emptied. - *

- * When the slot receives non-null contents, the new amount must be a - * positive integer. When the slot is emptied, {@code amount} must be 0. - * - * @param contents the new contents of this slot or {@code null} to clear - * the slot - * @param amount the amount of items to set. - * {@code (amount == 0) == (contents == null)} must be true. - */ - public synchronized final void setContents(ItemData contents, int amount) { - this.contents = contents; - this.amount = amount; - - checkState(); - - Inventory inventory = getInventory(); - if (inventory != null) { - inventory.getEventBus().post(new ItemSlotChangedEvent(this)); - } + public int getCount() { + return container.getCount(index); } - /** - * Sets the amount of items stored in this slot. - *

- * Setting the amount to zero also erases the slot's contents. - * - * @param amount the new amount - */ - public synchronized void setAmount(int amount) { - setContents(amount == 0 ? null : contents, amount); - } - - /** - * Clears this slot - */ - public synchronized void clear() { - setContents(null, 0); + public boolean isEmpty() { + return container.isEmpty(index); } - /** - * Retrieves the amount of items stored in this slot. If not items are - * present, this returns 0. - * - * @return the amount of items stored - */ - public synchronized int getAmount() { - return amount; + public boolean canAdd(ItemData item, int count) { + return container.canAdd(index, item, count); } - - public synchronized boolean isEmpty() { - return amount == 0; + + public boolean canRemove(ItemData item, int count) { + return container.canRemove(index, item, count); } - - public synchronized boolean canInsert(ItemData contents, int amount) { - if (contents == null) { - return false; - } - - if (container.getMass() + contents.getMass() * amount > container.getMassLimit()) { - return false; - } - - if (container.getVolume() + contents.getVolume() * amount > container.getVolumeLimit()) { - return false; - } - - return this.contents == null || this.contents.equals(contents); + + protected boolean add(ItemData item, int count) { + return container.add(index, item, count); } - - public synchronized boolean canRemove(int amount) { - return this.amount >= amount; + + protected boolean remove(ItemData item, int count) { + return container.remove(index, item, count); } - - private synchronized void checkState() { - if ((contents == null) != (amount == 0)) { - if (contents == null) { - throw new IllegalArgumentException("Contents is null but amount (" + amount + ") != 0"); - } else { - throw new IllegalArgumentException("Contents is " + contents + " but amount is zero"); + + public float getMass() { + synchronized (container) { + int count = getCount(); + if (count == 0) { + return 0; } - } - - if (amount < 0) { - throw new IllegalArgumentException("amount is negative: " + amount); + return count * getItem().getMass(); } } - @Override - public synchronized void read(DataInput input, IOContext context) throws IOException { - amount = input.readInt(); - if (amount != 0) { - String id = input.readUTF(); - contents = ItemDataRegistry.getInstance().create(id); - contents.read(input, context); - } else { - contents = null; - } - - checkState(); - } - - @Override - public synchronized void write(DataOutput output, IOContext context) throws IOException { - output.writeInt(amount); - if (contents != null) { - output.writeUTF(contents.getId()); - contents.write(output, context); - } - } - - @Override - public void copy(Encodable destination) { - ItemSlot slot = (ItemSlot) destination; - - slot.amount = this.amount; - - if (this.contents == null) { - slot.contents = null; - } else { - if (slot.contents == null || !slot.contents.isLike(this.contents)) { - slot.contents = ItemDataRegistry.getInstance().create(this.contents.getId()); + public float getVolume() { + synchronized (container) { + int count = getCount(); + if (count == 0) { + return 0; } - this.contents.copy(slot.contents); + return count * getItem().getVolume(); } } + /* + * For purposes of equality checking, all container instances are considered + * different + */ + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + System.identityHashCode(container); + result = prime * result + index; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ItemSlot other = (ItemSlot) obj; + if (container != other.container) + return false; + if (index != other.index) + return false; + return true; + } + } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/Items.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/Items.java new file mode 100644 index 0000000..c1a8ca2 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/Items.java @@ -0,0 +1,264 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.common.world.item.inventory; + +import ru.windcorp.progressia.common.world.item.ItemData; +import ru.windcorp.progressia.common.world.item.LinearItemProperty; + +public class Items { + + private static int pour(ItemContainer from, int fromIndex, ItemContainer into, int intoIndex, int max) { + synchronized (from) { + synchronized (into) { + + if (!from.isRemovingAllowed()) { + return 0; + } + + ItemData item = from.getItem(fromIndex); + + if (item == null) { + return 0; + } + + if (!into.canAdd(item)) { + return 0; + } + + if (!into.isEmpty(intoIndex) && !into.getItem(intoIndex).equals(item)) { + return 0; + } + + int originalCount = from.getCount(fromIndex); + int transferCount = Math.min(originalCount, max); + + for (LinearItemProperty prop : LinearItemProperty.values()) { + int canFitPropwise = (int) Math.floor((prop.getLimit(into) - prop.get(into)) / prop.get(item)); + if (canFitPropwise < transferCount) { + transferCount = canFitPropwise; + } + } + + if (transferCount == 0) { + return 0; + } + + if (!into.canAdd(intoIndex, item, transferCount)) { + return 0; + } + + if (!from.remove(fromIndex, item, transferCount)) { + return 0; + } + + boolean success = into.add(intoIndex, item, transferCount); + assert success : "Wait, canAdd and canRemove promised the operation would be safe!"; + + return transferCount; + + } + } + } + + /** + * Attempts to transfer as many items as possible, but no more than + * {@code max}, between two slots. The origin of the items is {@code from}, + * the destination is {@code into}. This method does nothing if the items + * could not be removed from origin or could not be inserted into + * destination, such as when {@code into} contains different items. + * + * @param from the slot to transfer item from + * @param into the destination of the items + * @param max the maximum amount of items to transfer. Use + * {@code Integer.MAX_VALUE} to remove the limit + * @return the actual amount of items moved between slots + */ + public static int pour(ItemSlot from, ItemSlot into, int max) { + return pour(from.getContainer(), from.getIndex(), into.getContainer(), into.getIndex(), max); + } + + /** + * Attempts to transfer as many items as possible between two slots. The + * origin of the items is {@code from}, the destination is {@code into}. + * This method does nothing if the items could not be removed from origin or + * could not be inserted into destination, such as when {@code into} + * contains different items. + * + * @param from the slot to transfer item from + * @param into the destination of the items + * @return the actual amount of items moved between slots + */ + public static int pour(ItemSlot from, ItemSlot into) { + return pour(from.getContainer(), from.getIndex(), into.getContainer(), into.getIndex(), Integer.MAX_VALUE); + } + + /** + * Attempts to swap the contents of the two slots. Swapping contents of two + * empty slots results in a no-op and is considered a success. + * + * @param a one of the slots + * @param b the other slot + * @return whether the swap succeeded + */ + public static boolean swap(ItemSlot a, ItemSlot b) { + synchronized (a.getContainer()) { + synchronized (b.getContainer()) { + + if (a.isEmpty() && b.isEmpty()) { + return true; + } + + if (!a.isEmpty() && !a.getContainer().isRemovingAllowed()) { + return false; + } + + if (!b.isEmpty() && !b.getContainer().isRemovingAllowed()) { + return false; + } + + ItemData aItem = a.getItem(); + int aCount = a.getCount(); + + ItemData bItem = b.getItem(); + int bCount = b.getCount(); + + a.remove(aItem, aCount); + b.remove(bItem, bCount); + + if (a.canAdd(bItem, bCount) && b.canAdd(aItem, aCount)) { + a.add(bItem, bCount); + b.add(aItem, aCount); + return true; + } else { + a.add(aItem, aCount); + b.add(bItem, bCount); + return false; + } + + } + } + } + + /** + * Attempts to place new items into the specified slot. Either all or none + * of the requested items will be spawned. + * + * @param into destination slot + * @param item the item to add + * @param count the item count + * @return whether the addition succeeded + */ + public static boolean spawn(ItemSlot into, ItemData item, int count) { + synchronized (into.getContainer()) { + return into.add(item, count); + } + } + + /** + * Attempts to remove items from the specified slot. Either all or none of + * the requested items will be destroyed. + * + * @param from the slot + * @param item the item to remove + * @param count the item count + * @return whether the removal succeeded + */ + public static boolean destroy(ItemSlot from, ItemData item, int count) { + synchronized (from.getContainer()) { + return from.remove(item, count); + } + } + + public static int pour(ItemSlot from, ItemContainer into, int max) { + synchronized (from.getContainer()) { + synchronized (into) { + + int totalPoured = 0; + + for (int index = 0; max > 0 && index <= into.getMaxIndex(); ++index) { + int poured = pour(from.getContainer(), from.getIndex(), into, index, max); + max -= poured; + totalPoured += poured; + } + + return totalPoured; + + } + } + } + + public static int pour(ItemSlot from, ItemContainer into) { + return pour(from, into, Integer.MAX_VALUE); + } + + public static boolean spawn(ItemContainer into, ItemData item, int count) { + synchronized (into) { + + if (item == null && count == 0) { + return true; + } + + if (count < 0) { + return false; + } + + if (!into.canAdd(item)) { + return false; + } + + for (LinearItemProperty prop : LinearItemProperty.values()) { + float requested = prop.get(item) * count; + float available = prop.getLimit(into) - prop.get(into); + if (requested > available) { + return false; + } + } + + int compatibleSlot = -1; + int firstEmptySlot = -1; + + for (int index = 0; index <= into.getMaxIndex(); ++index) { + ItemData inSlot = into.getItem(index); + if (inSlot == null) { + if (firstEmptySlot == -1) { + firstEmptySlot = index; + } + } else if (inSlot.equals(item)) { + compatibleSlot = index; + break; + } + } + + if (compatibleSlot == -1) { + compatibleSlot = firstEmptySlot; + } + + if (compatibleSlot == -1) { + // Means the inventory is full due to slot limit + return false; + } + + return into.add(compatibleSlot, item, count); + + } + } + + private Items() { + } + +} diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotChangedEvent.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotChangedEvent.java index 9b34114..9b40d25 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotChangedEvent.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotChangedEvent.java @@ -17,6 +17,7 @@ */ package ru.windcorp.progressia.common.world.item.inventory.event; +import ru.windcorp.progressia.common.world.item.inventory.ItemContainer; import ru.windcorp.progressia.common.world.item.inventory.ItemSlot; public class ItemSlotChangedEvent extends ItemSlotEvent { @@ -25,4 +26,8 @@ public class ItemSlotChangedEvent extends ItemSlotEvent { super(slot); } + public ItemSlotChangedEvent(ItemContainer container, int index) { + super(container, index); + } + } diff --git a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotEvent.java b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotEvent.java index 07521c6..8e307ea 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotEvent.java +++ b/src/main/java/ru/windcorp/progressia/common/world/item/inventory/event/ItemSlotEvent.java @@ -17,21 +17,38 @@ */ package ru.windcorp.progressia.common.world.item.inventory.event; +import ru.windcorp.progressia.common.world.item.inventory.ItemContainer; import ru.windcorp.progressia.common.world.item.inventory.ItemSlot; public abstract class ItemSlotEvent extends ItemContainerEvent { - private final ItemSlot slot; + private ItemSlot slot = null; + private final int index; + public ItemSlotEvent(ItemContainer container, int index) { + super(container); + this.index = index; + } + public ItemSlotEvent(ItemSlot slot) { - super(slot.getContainer()); + this(slot.getContainer(), slot.getIndex()); this.slot = slot; } + /** + * @return the index + */ + public int getIndex() { + return index; + } + /** * @return the slot */ public ItemSlot getSlot() { + if (slot == null) { + slot = new ItemSlot(getContainer(), index); + } return slot; } diff --git a/src/main/java/ru/windcorp/progressia/server/PlayerManager.java b/src/main/java/ru/windcorp/progressia/server/PlayerManager.java index f3bfed4..0236bc8 100644 --- a/src/main/java/ru/windcorp/progressia/server/PlayerManager.java +++ b/src/main/java/ru/windcorp/progressia/server/PlayerManager.java @@ -28,6 +28,7 @@ import ru.windcorp.progressia.common.world.entity.EntityData; import ru.windcorp.progressia.common.world.entity.EntityDataPlayer; import ru.windcorp.progressia.common.world.entity.EntityDataRegistry; import ru.windcorp.progressia.common.world.item.ItemDataRegistry; +import ru.windcorp.progressia.common.world.item.inventory.Items; import ru.windcorp.progressia.server.events.PlayerJoinedEvent; import ru.windcorp.progressia.test.TestContent; @@ -63,8 +64,8 @@ public class PlayerManager { private EntityDataPlayer spawnPlayerEntity(String login) { EntityDataPlayer player = (EntityDataPlayer) EntityDataRegistry.getInstance().create("Core:Player"); - player.getHand(0).slot().setContents(ItemDataRegistry.getInstance().create("Test:Stick"), 20); - player.getEquipmentSlot(0).slot().setContents(ItemDataRegistry.getInstance().create("Test:CardboardBackpack"), 1); + Items.spawn(player.getHand(0).slot(), ItemDataRegistry.getInstance().create("Test:Stick"), 5); + Items.spawn(player.getEquipmentSlot(0).slot(), ItemDataRegistry.getInstance().create("Test:CardboardBackpack"), 1); player.setPosition(getServer().getWorld().getGenerator().suggestSpawnLocation()); player.setUpVector(new Vec3(0, 0, 1));