package net.scapeemulator.game.model.grounditem;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import net.scapeemulator.game.model.Entity;
import net.scapeemulator.game.model.Position;
import net.scapeemulator.game.model.definition.ItemDefinitions;
import net.scapeemulator.game.model.player.Item;
import net.scapeemulator.game.model.player.Player;
import net.scapeemulator.game.util.math.BasicMath;
/**
* @author Hadyn Richard
* @author David Insley
*/
public final class GroundItemList {
/**
* The owner identifier for a public item.
*/
public static final int PUBLIC_ITEM = -1;
/**
* The owner identifier for a public item that doesn't expire after a timer. TODO
*/
public static final int PUBLIC_ITEM_PERSIST = -2;
/**
* The UID counter for all the ground items that are created.
*/
private static final AtomicInteger counter = new AtomicInteger(1);
/**
* The ground item stacks in this list.
*/
private final Map<Position, GroundItemStack> stacks = new LinkedHashMap<>();
/**
* The list of listeners for this list.
*/
private final List<GroundItemListener> listeners = new LinkedList<>();
/**
* The class that represents a stack of ground items at a specific location.
*/
private class GroundItemStack {
/**
* The ground items that are contained in the stack.
*/
private final List<GroundItem> groundItems = new LinkedList<>();
/**
* The position at which the stack is located at.
*/
private final Position position;
/**
* Constructs a new {@link GroundItemStack};
*
* @param position The position of the stack.
*/
public GroundItemStack(Position position) {
this.position = position;
}
/**
* Appends a new ground item to the stack.
*
* @param itemId The id of the item to append.
* @param amount The amount of the item.
* @return the GroundItem modified or created
*/
public GroundItem add(int itemId, int amount, int owner) {
if (ItemDefinitions.forId(itemId).isStackable()) {
for (GroundItem groundItem : groundItems) {
if (groundItem.getItemId() != itemId || groundItem.getOwner() != owner) {
continue;
}
if (BasicMath.integerOverflow(groundItem.getAmount(), amount) != 0) {
continue;
}
/* update the ground item */
groundItem.setAmount(groundItem.getAmount() + amount);
groundItem.resetTimer();
return groundItem;
}
}
/* add the ground item to the ground item list */
GroundItem groundItem = new GroundItem(counter.getAndIncrement(), position, itemId, amount, owner);
groundItems.add(groundItem);
/* alert the listeners a new ground item was created */
for (GroundItemListener listener : listeners) {
if (listener.shouldFireEvents(groundItem)) {
listener.groundItemCreated(groundItem);
}
}
return groundItem;
}
/**
* Gets the first ground item in the list with the specified item id that is visible to the
* specified owner.
*
* @param itemId The item id for the ground item to get.
* @return The found ground item.
*/
public GroundItem get(int itemId, int owner) {
for (GroundItem groundItem : groundItems) {
if (groundItem.getItemId() != itemId) {
continue;
}
if (groundItem.getOwner() != PUBLIC_ITEM && groundItem.getOwner() != owner) {
continue;
}
return groundItem;
}
return null;
}
/**
* Removes the first ground item in the stack with the given item id that is visible to the
* specified owner.
*
* @param itemId the item id to check for
* @param owner the owner to check visibility for
* @return the GroundItem that was removed
*/
private GroundItem remove(int itemId, int owner) {
GroundItem groundItem = get(itemId, owner);
if (groundItem != null) {
groundItems.remove(groundItem);
for (GroundItemListener listener : listeners) {
if (listener.shouldFireEvents(groundItem)) {
listener.groundItemRemoved(groundItem);
}
}
}
return groundItem;
}
/**
* Gets if the stack contains a ground item with the specified item id visible to the
* specified player.
*
* @param player the player to check visibility for
* @param itemId the item id for the ground item to check
* @return true if the ground item with the item id exists
*/
public boolean contains(int itemId, int owner) {
return get(itemId, owner) != null;
}
/**
* Gets if the stack is empty.
*
* @return If the ground item map is empty.
*/
public boolean isEmpty() {
return groundItems.isEmpty();
}
/**
* Gets the ground items in the stack.
*
* @return the ground item list
*/
public List<GroundItem> getGroundItems() {
return groundItems;
}
}
/**
* The representation of a ground item for this list.
*/
public class GroundItem extends Entity {
/**
* The unique id of the ground item.
*/
private final int uid;
/**
* The item id that the ground item represents.
*/
private final int itemId;
/**
* The amount of the item that the ground item represents.
*/
private int amount;
/**
* The database id of the owner of this ground item, or -1 for a public item.
*/
private int owner;
private int timer;
public GroundItem(int uid, Position position, int itemId, int amount, int owner) {
this.uid = uid;
this.position = position;
this.itemId = itemId;
this.amount = amount;
this.owner = owner;
resetTimer();
}
/**
* Gets the unique id.
*
* @return The unique id.
*/
public int getUid() {
return uid;
}
/**
* Sets the amount of the item.
*
* @param newAmount The new amount.
*/
public void setAmount(int newAmount) {
if (newAmount != amount) {
int oldAmount = amount;
amount = newAmount;
for (GroundItemListener listener : listeners) {
if (listener.shouldFireEvents(this)) {
listener.groundItemUpdated(this, oldAmount);
}
}
}
}
@Override
public void setPosition(Position position) {
// Don't allow modification of the position
}
/**
* Gets the id of the item that the ground item represents.
*
* @return The item id.
*/
public int getItemId() {
return itemId;
}
/**
* Gets the amount of the item that the ground item represents.
*
* @return The item amount.
*/
public int getAmount() {
return amount;
}
public int getOwner() {
return owner;
}
public void setOwner(int owner) {
this.owner = owner;
}
public int getTimeLeft() {
return timer;
}
public void resetTimer() {
timer = owner != PUBLIC_ITEM ? 100 : 200;
}
public void decrementTimer() {
timer--;
}
/**
* Converts the ground item to a immutable item.
*
* @return The created immutable item.
*/
public Item toItem() {
return new Item(itemId, amount);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (getClass() != other.getClass()) {
return false;
}
GroundItem groundItem = (GroundItem) other;
if (itemId != groundItem.itemId) {
return false;
}
if (amount != groundItem.amount) {
return false;
}
if (!position.equals(groundItem.position)) {
return false;
}
return true;
}
}
/**
* Adds a listener for the list.
*/
public void addListener(GroundItemListener listener) {
listeners.add(listener);
}
/**
* Removes a listener from the list.
*/
public void removeListener(GroundItemListener listener) {
listeners.remove(listener);
}
/**
* Fires a ground item created event for each ground item in the list.
*/
public void fireEvents(GroundItemListener listener) {
for (Entry<Position, GroundItemStack> entry : stacks.entrySet()) {
for (GroundItem groundItem : entry.getValue().getGroundItems()) {
if (listener.shouldFireEvents(groundItem)) {
listener.groundItemCreated(groundItem);
}
}
}
}
/**
* Adds a ground item to the list.
*
* @param itemId The id of the item.
* @param amount The amount of the item.
* @param position The position of the ground item.
* @return
*/
public GroundItem add(int itemId, int amount, Position position, Player player) {
GroundItemStack stack = stacks.get(position);
if (stack == null) {
stack = new GroundItemStack(position);
stacks.put(position, stack);
}
return stack.add(itemId, amount, player != null ? player.getDatabaseId() : PUBLIC_ITEM);
}
/**
* Gets the first ground item at the given position that has the given item id and is visible to
* the specified player.
*
* @param itemId The item id.
* @param position The position of the item.
* @return The ground item.
*/
public GroundItem get(int itemId, Position position, Player player) {
GroundItemStack stack = stacks.get(position);
if (stack == null) {
return null;
}
return stack.get(itemId, player != null ? player.getDatabaseId() : PUBLIC_ITEM);
}
/**
* Checks if the list contains a ground item visible to the player.
*
* @param player the player to check visibility for
* @param itemId the id of the item to check for
* @param position the position of the item
* @return If the ground item exists.
*/
public boolean contains(int itemId, Position position, Player player) {
GroundItemStack stack = stacks.get(position);
if (stack == null) {
return false;
}
return stack.contains(itemId, player != null ? player.getDatabaseId() : PUBLIC_ITEM);
}
/**
* Removes the first ground item at the given position that has the given item id and is visible
* to the specified player.
*
* @param itemId
* @param position
* @param player
* @return the ground item that was removed
*/
public GroundItem remove(int itemId, Position position, Player player) {
GroundItemStack stack = stacks.get(position);
if (stack == null) {
return null;
}
GroundItem removed = stack.remove(itemId, player != null ? player.getDatabaseId() : PUBLIC_ITEM);
if (stack.isEmpty()) {
stacks.remove(position);
}
return removed;
}
public void tick() {
List<GroundItem> toRemove = new LinkedList<>();
for (GroundItemStack stack : stacks.values()) {
for (GroundItem groundItem : stack.getGroundItems()) {
groundItem.decrementTimer();
if (groundItem.getTimeLeft() < 1) {
toRemove.add(groundItem);
}
}
}
for (GroundItem groundItem : toRemove) {
if (groundItem.getOwner() == PUBLIC_ITEM) {
/*
* Because the remove ground item packet will remove the first item in the clients
* deque that matches the id and position, it is impossible to remove an item below
* another with the same id. TODO - Fix this somehow, in the meantime there may be
* visual inconsistencies if two stackable items with the same ID are dropped and
* the public item expires first
*/
GroundItem removed = remove(groundItem.getItemId(), groundItem.getPosition(), null);
} else {
groundItem.setOwner(PUBLIC_ITEM);
groundItem.resetTimer();
for (GroundItemListener listener : listeners) {
if (listener.shouldFireEvents(groundItem)) {
listener.groundItemCreated(groundItem);
}
}
}
}
}
}