Rewrote ItemContainers from scratch

- ItemContainers have been entirely rewritten
  - ItemSlots are now wrappers, not owners
- Added InventoryOwners
  - Not yet used
- Added ItemDataWithContainers
- Added some basic windowing functionality to WindowedHUD
This commit is contained in:
OLEGSHA 2021-11-16 23:49:17 +03:00
parent be6203719a
commit 3641c4130b
Signed by: OLEGSHA
GPG Key ID: E57A4B08D64AFF7A
24 changed files with 1306 additions and 628 deletions

View File

@ -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());
}
}

View File

@ -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();
}
}
}

View File

@ -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));

View File

@ -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;

View File

@ -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) {

View File

@ -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<InteractiveSlotComponent> slotCollection = new ArrayList<>();
private final List<InteractiveSlotComponent> 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);
}
}
}

View File

@ -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<InventorySimple> 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<? extends ItemContainer> getAllContainers() {
return Iterators.forArray(getInventory().getContainers());
}
public boolean isOpen() {
return !getInventory().getUsers().isEmpty();

View File

@ -15,14 +15,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<? extends ItemContainer> getAllContainers();
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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() {
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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();
}
}
}

View File

@ -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<InventoryUser> 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);

View File

@ -15,14 +15,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 {
}

View File

@ -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() {

View File

@ -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.
* <p>
* At any moment container has a definite amount of slots, each identified by a
* unique index. If a container has <i>n</i> slots, its slots are numbered 0
* through <i>n</i> - 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* Implementors will typically find the following subclasses useful:
* <ul>
* <li>{@link ItemContainerMixed} for containers allowing an arbitrary amount of
* slots
* <ul>
* <li>see {@link ItemContainerMixedSimple} for a complete implementation</li>
* </ul>
* </li>
* <li>{@link ItemContainerSingle} for containers providing only one slot</li>
* </ul>
*/
public abstract class ItemContainer extends Namespaced implements Encodable, Iterable<ItemSlot> {
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<? super ItemSlot> action);
@Override
public Iterator<ItemSlot> iterator() {
return new Iterator<ItemSlot>() {
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.
* <p>
* 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<? extends ItemContainer> 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<ItemSlot> 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<ItemSlotRemovedEvent> 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<? super ItemSlot> 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<ItemSlot> mySlots = this.slots;
List<ItemSlot> 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
);
}
}
}

View File

@ -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

View File

@ -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<? super ItemSlot> 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;
}
}

View File

@ -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.
* <p>
* {@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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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() {
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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));