/**
* ShowCaseStandalone
* Copyright (C) 2012 Kellerkindt <copyright at kellerkindt.com>
*
* 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 2 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 <http://www.gnu.org/licenses/>.
*/
package com.kellerkindt.scs.internals;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.ItemFrame;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import com.kellerkindt.scs.ShowCaseStandalone;
import com.kellerkindt.scs.events.ShowCaseOwnerSetEvent;
import com.kellerkindt.scs.events.ShowCaseShopHandlerChangedEvent;
import com.kellerkindt.scs.interfaces.ShopHandler;
import com.kellerkindt.scs.shops.Shop;
import com.kellerkindt.scs.utilities.ItemStackUtilities;
import com.kellerkindt.scs.utilities.Properties;
public class SimpleShopHandler implements ShopHandler {
// //I've converted this to a fixed array, because arrays are a lot quicker,
// //and lower mem impact. Since we have a sync repeating thread that loops
// through
// //the array, i've made it fixed. I use temp array lists to add/remove
// shops.
// private Shop[] shops = new Shop[0];
// --> HashMap is a lot faster for our needs
private HashMap<Item, Shop> itemShops = new HashMap<Item, Shop>();
private HashMap<Shop, Item> shopItems = new HashMap<Shop, Item>();
private HashMap<Block, Shop> blockShops = new HashMap<Block, Shop>();
private HashMap<UUID, Shop> uuidShops = new HashMap<UUID, Shop>();
private HashMap<String, Integer> shopOwners = new HashMap<String, Integer>();
private HashMap<Shop, List<ItemFrame>> shopFrames = new HashMap<Shop, List<ItemFrame>>();
private HashMap<ItemFrame, Shop> frameShops = new HashMap<ItemFrame, Shop>();
private ArrayList<Shop> shops = new ArrayList<Shop>(); // for fast iteration
private ShowCaseStandalone scs = null;
private boolean fireEvents = true;
private int syncTask;
private InternalShopChangeListener changeListener;
public SimpleShopHandler(ShowCaseStandalone scs) {
this.scs = scs;
this.changeListener = new InternalShopChangeListener();
scs.getServer().getPluginManager().registerEvents( this.changeListener, scs );
// set up scheduler
syncTask = scs.getServer().getScheduler().scheduleSyncRepeatingTask(scs, new Runnable() {
@Override
public void run() {
SimpleShopHandler.this.checkShopDisplayState();
}
}, 5L, Properties.intervall);
}
/**
* This method displays or hides the shops
* if the item disappeared, or the shop got
* inactive
* NOTE: this must be called from a synchronized Bukkit Thread
*/
public void checkShopDisplayState() {
long start = System.nanoTime();
// show some debug information
if (Properties.threadDebug) {
ShowCaseStandalone.dlog("Refreshing items. Thread exec start: " + start);
}
for (Shop p : this) {
// Regular chunk.isloaded causes bukkit to load the
// chunk.
if (!isChunkLoaded(p.getSpawnLocation()))
continue;
// shop Item
Item item = shopItems.get(p);
if (Properties.hideInactiveShops && (!p.isActive() && p.isVisible()))
hide(p);
// itemShops.remove(p.hide());
else if (item == null) {
if (!Properties.hideInactiveShops || p.isActive())
show(p);
// itemShops.put(p.show(), p);
else
hide(p);
// itemShops.remove(p.hide());
} else if (item.isDead()) {
if (!Properties.hideInactiveShops || p.isActive()) {
hide(p);
show(p);
// itemShops.remove(p.hide());
// itemShops.put(p.show(), p);
} else {
hide(p);
// itemShops.remove(p.hide());
}
}
}
if (Properties.threadDebug) {
long end = System.nanoTime();
long net = end - start;
ShowCaseStandalone.dlog("Thread exec end: " + end);
ShowCaseStandalone.dlog("Net time: " + net);
}
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#getShop(org.bukkit.entity.Item)
*/
@Override
public Shop getShop(Item i) {
return itemShops.get(i);
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#getShop(org.bukkit.block.Block)
*/
@Override
public Shop getShop(Block b) {
return blockShops.get(b);
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#getShop(java.util.UUID)
*/
@Override
public Shop getShop(UUID uuid) {
return uuidShops.get(uuid);
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#isShopItem(org.bukkit.entity.Item)
*/
@Override
public boolean isShopItem(Item item) {
return itemShops.containsKey(item);
}
@Override
public boolean isShopBlock(Block b) {
return blockShops.containsKey(b);
}
private void addRaw(Shop p, boolean overwrite) {
// if not already set a UUID, set it now
if (p.getUUID() == null) {
p.setUUID(UUID.randomUUID());
}
// be sure that the UUID is unique
while (uuidShops.containsKey(p.getUUID()) && !overwrite) {
p.setUUID(UUID.randomUUID());
}
if (uuidShops.containsKey(p.getUUID()) && overwrite) {
removeShop(uuidShops.get(p.getUUID()));
}
// add to lists
blockShops .put(p.getBlock(), p);
uuidShops .put(p.getUUID(), p);
shops .add(p);
this.setFrames(p);
this.incrementShopAmount( p.getOwner() );
}
/**
* Links all found ItemFrames
* @param shop Shop to search ItemFrames for
*/
private void setFrames (Shop shop) {
// clear
shopFrames.put(shop, new ArrayList<ItemFrame>());
// Iterate through the found frames
for (ItemFrame frame : getItemFrames(shop)) {
addFrame(shop, frame);
}
}
/**
* Removes the link between the given Shop
* and all known ItemFrames
* @param shop Shop to find ItemFrames for
*/
private void removeFrames (Shop shop) {
// get list
List<ItemFrame> frames = shopFrames.get(shop);
// is valid?
if (frames != null) {
// iterate through the links
for (ItemFrame frame : frames) {
// remove frame to shop link
frameShops.remove(frame);
}
// clear
frames.clear();
}
}
/**
* Adds a ItemFrame to the List for a shop
* @param shop Shop to attach to
* @param frame Frame to attach
*/
private void addFrame (Shop shop, ItemFrame frame) {
// get list
List<ItemFrame> list = shopFrames.get(shop);
// list does not exist yet?
if (list == null) {
list = new ArrayList<ItemFrame>();
shopFrames.put(shop, list);
}
// add frame
list.add(frame);
// add frame to shop link
frameShops.put(frame, shop);
}
/**
* Removes all links for the given ItemFrame
* @param frame ItemFrame to remove
*/
private void removeFrame (ItemFrame frame) {
// remove frame to shop link
Shop shop = frameShops.remove(frame);
// remove shop to frame link
shopFrames.get(shop).remove(frame);
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#addShop(com.kellerkindt.scs.shops.Shop)
*/
@Override
public void addShop(Shop p) {
addShop(p, false);
}
@Override
public void addShop(Shop p, boolean overwrite) {
addRaw(p, overwrite);
fireChangeEvent();
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#removeShop(com.miykeal.showCaseStandalone.shops.Shop)
*/
@Override
public void removeShop(Shop shop) {
// remove the main part
removeShopIteratorFriendly(shop);
// remove from iterator list
shops.remove(shop);
}
/**
* Removes the Shop from every Map but the List for the iterator
* This should allow the Iterator to remove shops while iterating
* @param shop Shop to remove
*/
public void removeShopIteratorFriendly (Shop shop) {
// hide
hide(shop);
blockShops .remove(shop.getBlock());
uuidShops .remove(shop.getUUID());
// shops .remove(shop);
shopItems .remove(shop);
this.removeFrames(shop);
this.decrementShopAmount( shop.getOwner() );
fireChangeEvent();
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#getShopAmount(java.lang.String)
*/
@Override
public int getShopAmount( String owner ) {
Integer amount = this.shopOwners.get( owner );
return ( amount != null ? amount : 0 );
}
private void incrementShopAmount( String owner ) {
String shopOwner = owner;
Integer amount = this.shopOwners.get( shopOwner );
if ( amount == null ) {
this.shopOwners.put( shopOwner, 1 );
} else {
this.shopOwners.put( shopOwner, amount + 1 );
}
}
private void decrementShopAmount( String owner ) {
String shopOwner = owner;
Integer amount = this.shopOwners.get( shopOwner );
if ( amount == null ) {
this.shopOwners.put( shopOwner, 1 );
} else if ( amount > 0 ){
this.shopOwners.put( shopOwner, amount - 1 );
}
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#loadChunk(org.bukkit.Chunk)
*/
@Override
public void loadChunk(Chunk k) {
if (Properties.chunkDebug) {
ShowCaseStandalone.dlog("Load chunk: " + k.toString() + ", " + k.getWorld().getName());
}
// fix for #263?
if (k == null || k.getWorld() == null) {
return;
}
if (Properties.chunkDebug) {
ShowCaseStandalone.dlog("Load chunk: " + k.toString() + ", " + k.getWorld().getName());
}
try {
for (Shop p : this) {
if (p.getWorld() == null) {
ShowCaseStandalone.slog(Level.SEVERE,
"Found showcase on not existing world! To remove perform: /scs purge w:"
+ p.getWorldName());
continue;
}
double kx = k.getX();
double kz = k.getZ();
World kw = k.getWorld();
Chunk ck = p.getLocation().getChunk();
double px = ck.getX();
double pz = ck.getZ();
World pw = ck.getWorld();
if (kx == px && kz == pz && kw.getName().equals(pw.getName())) {
if (Properties.chunkDebug)
ShowCaseStandalone.dlog("Found scs to load: " + p.getUUID());
if (!Properties.hideInactiveShops || p.isActive())
show(p);
// itemShops.put(p.show(), p);
else
hide(p);
// itemShops.remove(p.hide());
}
}
} catch (NullPointerException npe) {
ShowCaseStandalone.slog(Level.WARNING,
"NPE on load chunk shop enable.");
npe.printStackTrace();
}
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#unloadChunk(org.bukkit.Chunk)
*/
@Override
public void unloadChunk(Chunk k) {
if (Properties.chunkDebug) {
ShowCaseStandalone.dlog("Unload chunk: " + k.toString() + ", "
+ k.getWorld().getName());
}
try {
for (Shop p : this) {
if (p.getWorld() == null) {
ShowCaseStandalone.slog(Level.SEVERE, "Found showcase in not existing world! To remove perform: /scs purge w:" + p.getWorldName());
continue;
}
double kx = k.getX();
double kz = k.getZ();
World kw = k.getWorld();
Chunk ck = p.getLocation().getChunk();
double px = ck.getX();
double pz = ck.getZ();
World pw = ck.getWorld();
if (kx == px && kz == pz && kw.getName().equals(pw.getName())) {
if (Properties.chunkDebug) {
ShowCaseStandalone.dlog("Found scs to unload: " + p.getUUID());
}
// hide
hide(p);
}
}
} catch (NullPointerException npe) {
ShowCaseStandalone.slog(Level.WARNING, "NPE on unload chunk shop disable.");
}
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#hideAll()
*/
@Override
public void hideAll() {
for (Shop p : this)
hide(p);
// itemShops.remove(p.hide());
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#showAll()
*/
@Override
public void showAll() {
for (Shop p : this)
if (p.getBlock() != null)
if (p.getBlock().getChunk().isLoaded())
if (!Properties.hideInactiveShops || p.isActive())
show(p);
// itemShops.put(p.show(), p);
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#stop()
*/
@Override
public void stop() {
scs.getServer().getScheduler().cancelTask(syncTask);
ShowCaseStandalone.slog(Level.FINEST, "Stopped sync task.");
}
@Override
public void start() {
checkShopDisplayState();
ShowCaseStandalone.slog(Level.FINEST, "Started sync task.");
}
// /**
// * @see com.kellerkindt.scs.interfaces.ShopHandler#interact(org.bukkit.block.Block,
// * org.bukkit.entity.Player, int)
// */
// @Override
// public void interact(Block b, Player p, int amount) {
// BenchMark bm = null;
// if (Properties.interactDebug)
// bm = new BenchMark("Handler interact");
//
// Shop sp = this.getShopForBlock(b);
// if (Properties.interactDebug)
// bm.mark("getShopForBlock");
//
// sp.interact(p, amount);
// if (Properties.interactDebug)
// bm.mark("after interact");
//
// if (Properties.interactDebug)
// bm.end();
// }
// /**
// * @see com.kellerkindt.scs.interfaces.ShopHandler#info(org.bukkit.block.Block,
// * org.bukkit.entity.Player)
// */
// @Override
// public void info(Block b, Player p) {
// Shop sp = this.getShopForBlock(b);
// sp.info(p);
// }
private boolean isChunkLoaded(Location loc) {
int cx = loc.getBlockX() >> 4;
int cy = loc.getBlockZ() >> 4;
World cw = loc.getWorld();
// fix for #263 ?
if (cw == null)
return false;
return cw.isChunkLoaded(cx, cy);
}
@Override
public void removeAll() {
blockShops.clear();
uuidShops.clear();
itemShops.clear();
fireChangeEvent();
}
@Override
public Iterator<Shop> iterator() {
// DAFUQ, managing and keeping a ArrayList up to date, but iterating through the slow Map-Iterator - WHY!?
// return blockShops.values().iterator();
// iterate through the fast ArrayList-Iterator
return new ShopIterator(shops.iterator());
}
@Override
public void addAll(Collection<Shop> collection) {
addAll(collection, false);
}
@Override
public void addAll(Collection<Shop> shops, boolean overwrite) {
// deactivate event firing
fireEvents = false;
// add shops
for (Shop p : shops) {
addShop(p, overwrite);
}
// enable event firing
fireEvents = true;
// fire change event
fireChangeEvent();
}
/**
* Fires the ShopHandlerChangedValues event
*/
private void fireChangeEvent() {
// active?
if (!fireEvents)
return;
// perform the Event
scs.getServer().getPluginManager().callEvent(new ShowCaseShopHandlerChangedEvent(this));
}
private class InternalShopChangeListener implements Listener {
@EventHandler(ignoreCancelled=true, priority=EventPriority.MONITOR)
public void onShowCaseChange(ShowCaseOwnerSetEvent event) {
if (!event.getOldOwner().equals(event.getNewOwner())) {
// the old owner has now one shop less
decrementShopAmount( event.getOldOwner() );
// the new owner has now one shop more
incrementShopAmount( event.getNewOwner() );
}
}
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#hide(com.kellerkindt.scs.shops.Shop)
*/
@Override
public void hide(Shop shop) {
// get the Item for this shop
Item item = shopItems.get(shop);
if (item != null) {
// remove item
item.remove();
// prepare teleportation to the ground
int x = shop.getSpawnLocation().getBlockX();
int y = 0;
int z = shop.getSpawnLocation().getBlockZ();
World w = shop.getSpawnLocation().getWorld();
// teleport (fixed some client sided issues)
item.teleport(new Location(w, x, y, z));
// set invisible
shop.setVisible(false);
// remove item
shopItems.remove(shop);
itemShops.remove(item);
} else {
// Item Frame?
List<ItemFrame> frames = shopFrames.get(shop);
if (frames != null) {
// go through all frames
for (ItemFrame frame : frames) {
// set to no item
frame.setItem(null);
}
}
}
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#show(com.kellerkindt.scs.shops.Shop)
*/
@Override
public void show(Shop shop) {
// get ItemFrame
List<ItemFrame> frames = shopFrames.get(shop);
// is ItemFrame?
if (frames != null && frames.size() > 0) {
// hide - just to be sure
hide(shop);
// TODO: bad performance?
// set Item in the frame
for (ItemFrame frame : frames) {
frame.setItem(shop.getItemStack().clone());
}
// set visible
shop.setVisible(true);
}
else {
// spawn location set? world valid?
if (shop.getSpawnLocation() == null || shop.getWorld() == null) {
// no spawn location set
return;
}
// check for duplicate item
checkDuplicateItem(shop);
// spawn new item
Location spawnLocation = shop.getSpawnLocation();
ItemStack itemStack = shop.getItemStack().clone();
if (Properties.stackToMaxAmount) {
itemStack.setAmount(itemStack.getMaxStackSize());
} else {
itemStack.setAmount(1);
}
Item item = shop.getWorld().dropItem(spawnLocation, itemStack);
item.setVelocity(new Vector(0, 0.01, 0));
item.setPickupDelay(Properties.DEFAULT_PICKUP_DELAY); // less pickup events
// add to lists
shopItems.put(shop, item);
itemShops.put(item, shop);
// set visible
shop.setVisible(true);
}
}
/**
* @param shop Shop to check
* @return The ItemFrame above or null if there is not ItemFrame set
*/
public List<ItemFrame> getItemFrames (Shop shop) {
// list to return
List<ItemFrame> list = new ArrayList<ItemFrame>();
// block valid? world valid?
if (shop.getBlock() != null && shop.getWorld() != null) {
//
// get Blocks
Block blockUp = shop.getBlock().getRelative(BlockFace.UP);
Block blockDown = shop.getBlock();
Location locUp = blockUp .getLocation();
Location locDown= blockDown .getLocation();
// Search the ItemFrame, TODO: find a faster / better solution
for (ItemFrame frame : shop.getWorld().getEntitiesByClass(ItemFrame.class)) {
BlockFace face = frame.getFacing();
if (face == BlockFace.SOUTH || face == BlockFace.NORTH) {
face = face.getOppositeFace();
}
Block fBlock = frame.getLocation().getBlock();
Block aBlock = fBlock.getRelative(face);
// is frame faced to the upper or lower block?
if (aBlock.getLocation().equals(locDown) || aBlock.getLocation().equals(locUp) || fBlock.getLocation().equals(locUp)) {
list.add(frame);
}
}
}
// not found
return list;
}
/**
* Checks if there is already an item - server-crash?
* If found, they will be removed
* @return
*/
public void checkDuplicateItem (Shop shop) {
// max difference in height
double maxYDiff = 1.5;
Item shopItem = shopItems.get(shop);
// check in this chunk, not in whole world --> faster
for (Entity e : shop.getLocation().getChunk().getEntities())
{
double x = e.getLocation().getX();
double z = e.getLocation().getZ();
double yDiff = shop.getSpawnLocation().getY() - e.getLocation().getY();
if (yDiff < 0)
yDiff *= -1;
if (x == shop.getSpawnLocation().getX() && yDiff <= maxYDiff && z == shop.getSpawnLocation().getZ()) {
if (e instanceof Item) {
Item item = (Item)e;
// remove if not the current Item for this Shop
if (ItemStackUtilities.itemsEqual(item.getItemStack(), shop.getItemStack(), false) && shopItem != item)
item.remove();
}
}
}
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#onItemFramePlaced(org.bukkit.entity.ItemFrame)
*/
@Override
public void addItemFrame(final ItemFrame frame) {
// delayed, because the ItemFrame is added after this event
scs.getServer().getScheduler().scheduleSyncDelayedTask(scs, new Runnable() {
@Override
public void run() {
// blocks to check
List<Block> blocks = new ArrayList<Block>();
// get main blocks
Block frameBlock = frame.getLocation().getBlock();
Block frameBlockDown = frameBlock.getRelative(BlockFace.DOWN);
// add them
blocks.add(frameBlock);
blocks.add(frameBlockDown);
// allowed block faces
List<BlockFace> faces = new ArrayList<BlockFace>();
// add them
faces.add(BlockFace.NORTH);
faces.add(BlockFace.EAST);
faces.add(BlockFace.SOUTH);
faces.add(BlockFace.WEST);
faces.add(BlockFace.DOWN);
// iterate through the blocks
for (Block block : blocks) {
// iterate through all block faces
for (BlockFace face : faces) {
Block blockRelative = block.getRelative(face);
Shop shop = getShop(blockRelative);
// Shop found?
if (shop != null) {
// add the frames
setFrames(shop);
// apply changes
hide(shop);
show(shop);
}
}
}
}
});
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#onItemFrameDestroyed(org.bukkit.entity.ItemFrame)
*/
@Override
public void removeItemFrame(ItemFrame frame) {
final Shop shop = frameShops.get(frame);
// not above a shop?
if (shop == null) {
return;
}
// above a shop, hide item and show in ItemFrame
else {
// remove item
frame.setItem(null);
// remove frame
removeFrame(frame);
// // remove frame
// shopFrames.remove(shop);
// delayed, because the ItemFrame is removed after this event
scs.getServer().getScheduler().scheduleSyncDelayedTask(scs, new Runnable() {
@Override
public void run() {
hide(shop);
show(shop);
}
});
}
}
/**
* @see com.kellerkindt.scs.interfaces.ShopHandler#size()
*/
@Override
public int size() {
return shops.size();
}
/**
* This Iterator makes it possible to remove items with ".remove()"
* Got the suggestion in Ticket #465 (Phoenix_IV)
* @author kellerkindt <michael at kellerkindt.com>
*/
private class ShopIterator implements Iterator<Shop> {
private Iterator<Shop> iterator;
private Shop shop = null;
public ShopIterator (Iterator<Shop> iterator) {
this.iterator = iterator;
}
/**
* @see java.util.Iterator#hasNext()
*/
@Override
public boolean hasNext() {
return iterator.hasNext();
}
/**
* @see java.util.Iterator#next()
*/
@Override
public Shop next() {
return (shop = iterator.next());
}
/**
* @see java.util.Iterator#remove()
*/
@Override
public void remove() {
// remove the current shop from every map but the current list
// this iterator is iterating through
removeShopIteratorFriendly(shop);
// remove it from this list
iterator.remove();
}
}
}