From c595b574eccf349c3a175fcc17c97ca5946d7627 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 21 Jan 2020 20:00:08 +0100 Subject: [PATCH] Load/unload shops on chunk load/unload This breaks shop limits, only loaded shops are counted at the moment --- .../java/de/epiceric/shopchest/ShopChest.java | 26 ++- .../command/ShopCommandExecutor.java | 40 ++++- .../shopchest/event/ShopInitializedEvent.java | 5 + .../shopchest/event/ShopsLoadedEvent.java | 33 ++++ .../shopchest/event/ShopsUnloadedEvent.java | 33 ++++ .../listeners/ShopUpdateListener.java | 74 ++++++-- .../de/epiceric/shopchest/sql/Database.java | 164 +++++++++++------- .../epiceric/shopchest/utils/ShopUtils.java | 84 +++++---- 8 files changed, 339 insertions(+), 120 deletions(-) create mode 100644 src/main/java/de/epiceric/shopchest/event/ShopsLoadedEvent.java create mode 100644 src/main/java/de/epiceric/shopchest/event/ShopsUnloadedEvent.java diff --git a/src/main/java/de/epiceric/shopchest/ShopChest.java b/src/main/java/de/epiceric/shopchest/ShopChest.java index dde3da7..0e00912 100644 --- a/src/main/java/de/epiceric/shopchest/ShopChest.java +++ b/src/main/java/de/epiceric/shopchest/ShopChest.java @@ -43,6 +43,8 @@ import me.wiefferink.areashop.AreaShop; import net.milkbowl.vault.economy.Economy; import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.RegisteredServiceProvider; @@ -65,6 +67,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; public class ShopChest extends JavaPlugin { @@ -437,13 +440,26 @@ public class ShopChest extends JavaPlugin { * Initializes the shops */ private void initializeShops() { - debug("Initializing Shops..."); - shopUtils.reloadShops(false, true, new Callback(this) { + getShopDatabase().connect(new Callback(this) { @Override public void onResult(Integer result) { - Bukkit.getServer().getPluginManager().callEvent(new ShopInitializedEvent(result)); - getLogger().info("Initialized " + result + " Shops"); - debug("Initialized " + result + " Shops"); + Chunk[] loadedChunks = getServer().getWorlds().stream().map(World::getLoadedChunks) + .flatMap(Stream::of).toArray(Chunk[]::new); + + shopUtils.loadShops(loadedChunks, new Callback(ShopChest.this) { + @Override + public void onResult(Integer result) { + getServer().getPluginManager().callEvent(new ShopInitializedEvent(result)); + getLogger().info("Loaded " + result + " shops in already loaded chunks"); + debug("Loaded " + result + " shops in already loaded chunks"); + } + + @Override + public void onError(Throwable throwable) { + getLogger().severe("Failed to load shops in already loaded chunks"); + if (throwable != null) getLogger().severe(throwable.getMessage()); + } + }); } @Override diff --git a/src/main/java/de/epiceric/shopchest/command/ShopCommandExecutor.java b/src/main/java/de/epiceric/shopchest/command/ShopCommandExecutor.java index 22896c1..eca599e 100644 --- a/src/main/java/de/epiceric/shopchest/command/ShopCommandExecutor.java +++ b/src/main/java/de/epiceric/shopchest/command/ShopCommandExecutor.java @@ -26,8 +26,10 @@ import de.epiceric.shopchest.utils.ClickType.CreateClickType; import de.epiceric.shopchest.utils.ClickType.SelectClickType; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.GameMode; import org.bukkit.OfflinePlayer; +import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -36,7 +38,9 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.stream.Stream; class ShopCommandExecutor implements CommandExecutor { @@ -191,12 +195,40 @@ class ShopCommandExecutor implements CommandExecutor { return; } - shopUtils.reloadShops(true, true, new Callback(plugin) { + // Reload configurations + plugin.getShopChestConfig().reload(false, true, true); + plugin.getHologramFormat().reload(); + plugin.getUpdater().restart(); + + // Remove all shops + Iterator iter = shopUtils.getShops().iterator(); + while (iter.hasNext()) { + shopUtils.removeShop(iter.next(), false); + } + + Chunk[] loadedChunks = Bukkit.getWorlds().stream().map(World::getLoadedChunks) + .flatMap(Stream::of).toArray(Chunk[]::new); + + // Reconnect to the database and re-load shops in loaded chunks + plugin.getShopDatabase().connect(new Callback(plugin) { @Override public void onResult(Integer result) { - sender.sendMessage(LanguageUtils.getMessage(Message.RELOADED_SHOPS, - new Replacement(Placeholder.AMOUNT, String.valueOf(result)))); - plugin.debug(sender.getName() + " has reloaded " + result + " shops"); + shopUtils.loadShops(loadedChunks, new Callback(plugin) { + @Override + public void onResult(Integer result) { + sender.sendMessage(LanguageUtils.getMessage(Message.RELOADED_SHOPS, + new Replacement(Placeholder.AMOUNT, String.valueOf(result)))); + plugin.debug(sender.getName() + " has reloaded " + result + " shops"); + } + + @Override + public void onError(Throwable throwable) { + sender.sendMessage(LanguageUtils.getMessage(Message.ERROR_OCCURRED, + new Replacement(Placeholder.ERROR, "Failed to load shops from database"))); + plugin.getLogger().severe("Failed to load shops"); + if (throwable != null) plugin.getLogger().severe(throwable.getMessage()); + } + }); } @Override diff --git a/src/main/java/de/epiceric/shopchest/event/ShopInitializedEvent.java b/src/main/java/de/epiceric/shopchest/event/ShopInitializedEvent.java index 1eb0a09..c18f2c4 100644 --- a/src/main/java/de/epiceric/shopchest/event/ShopInitializedEvent.java +++ b/src/main/java/de/epiceric/shopchest/event/ShopInitializedEvent.java @@ -3,6 +3,11 @@ package de.epiceric.shopchest.event; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; +/** + * @deprecated Use {@link ShopsLoadedEvent} instead since shops are loaded + * dynamically based on chunk loading + */ +@Deprecated public class ShopInitializedEvent extends Event { private static final HandlerList handlers = new HandlerList(); diff --git a/src/main/java/de/epiceric/shopchest/event/ShopsLoadedEvent.java b/src/main/java/de/epiceric/shopchest/event/ShopsLoadedEvent.java new file mode 100644 index 0000000..1fec3b6 --- /dev/null +++ b/src/main/java/de/epiceric/shopchest/event/ShopsLoadedEvent.java @@ -0,0 +1,33 @@ +package de.epiceric.shopchest.event; + +import java.util.Collection; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import de.epiceric.shopchest.shop.Shop; + +/** + * Called when shops have been loaded and added to the server + */ +public class ShopsLoadedEvent extends Event { + private static final HandlerList handlers = new HandlerList(); + private Collection shops; + + public ShopsLoadedEvent(Collection shops) { + this.shops = shops; + } + + public Collection getShops() { + return shops; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } +} diff --git a/src/main/java/de/epiceric/shopchest/event/ShopsUnloadedEvent.java b/src/main/java/de/epiceric/shopchest/event/ShopsUnloadedEvent.java new file mode 100644 index 0000000..e194aa1 --- /dev/null +++ b/src/main/java/de/epiceric/shopchest/event/ShopsUnloadedEvent.java @@ -0,0 +1,33 @@ +package de.epiceric.shopchest.event; + +import java.util.Collection; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import de.epiceric.shopchest.shop.Shop; + +/** + * Called when shops have been unloaded and removed from the server + */ +public class ShopsUnloadedEvent extends Event { + private static final HandlerList handlers = new HandlerList(); + private Collection shops; + + public ShopsUnloadedEvent(Collection shops) { + this.shops = shops; + } + + public Collection getShops() { + return shops; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } +} diff --git a/src/main/java/de/epiceric/shopchest/listeners/ShopUpdateListener.java b/src/main/java/de/epiceric/shopchest/listeners/ShopUpdateListener.java index bed4ec7..5dfced6 100644 --- a/src/main/java/de/epiceric/shopchest/listeners/ShopUpdateListener.java +++ b/src/main/java/de/epiceric/shopchest/listeners/ShopUpdateListener.java @@ -4,6 +4,10 @@ import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.shop.Shop; import de.epiceric.shopchest.utils.Callback; +import java.util.HashSet; +import java.util.Set; + +import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -12,12 +16,14 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.scheduler.BukkitRunnable; public class ShopUpdateListener implements Listener { - private ShopChest plugin; + private final ShopChest plugin; + private final Set newLoadedChunks = new HashSet<>(); public ShopUpdateListener(ShopChest plugin) { this.plugin = plugin; @@ -83,23 +89,55 @@ public class ShopUpdateListener implements Listener { } @EventHandler - public void onWorldLoad(WorldLoadEvent e) { - final String worldName = e.getWorld().getName(); + public void onChunkLoad(ChunkLoadEvent e) { + if (!plugin.getShopDatabase().isInitialized()) { + return; + } - plugin.getShopUtils().reloadShops(false, false, new Callback(plugin) { - @Override - public void onResult(Integer result) { - plugin.getLogger().info(String.format("Reloaded %d shops because a new world '%s' was loaded", result, worldName)); - plugin.debug(String.format("Reloaded %d shops because a new world '%s' was loaded", result, worldName)); - } + // Wait 10 ticks after first event is triggered, so that multiple + // chunk loads can be handled at the same time without having to + // send a database request for each chunk. + if (newLoadedChunks.isEmpty()) { + new BukkitRunnable(){ + @Override + public void run() { + int chunkCount = newLoadedChunks.size(); + plugin.getShopUtils().loadShops(newLoadedChunks.toArray(new Chunk[chunkCount]), new Callback(plugin) { + @Override + public void onResult(Integer result) { + if (result == 0) { + return; + } + plugin.debug("Loaded " + result + " shops in " + chunkCount + " chunks"); + } + + @Override + public void onError(Throwable throwable) { + // Database connection probably failed => disable plugin to prevent more errors + plugin.getLogger().severe("Failed to load shops in newly loaded chunks"); + plugin.debug("Failed to load shops in newly loaded chunks"); + if (throwable != null) plugin.debug(throwable); + } + }); + newLoadedChunks.clear(); + } + }.runTaskLater(plugin, 10L); + } - @Override - public void onError(Throwable throwable) { - // Database connection probably failed => disable plugin to prevent more errors - plugin.getLogger().severe("No database access. Disabling ShopChest"); - if (throwable != null) plugin.getLogger().severe(throwable.getMessage()); - plugin.getServer().getPluginManager().disablePlugin(plugin); - } - }); + newLoadedChunks.add(e.getChunk()); + } + + @EventHandler + public void onChunkUnload(ChunkUnloadEvent e) { + if (!plugin.getShopDatabase().isInitialized()) { + return; + } + + int num = plugin.getShopUtils().unloadShops(e.getChunk()); + + if (num > 0) { + String chunkStr = "[" + e.getChunk().getX() + "; " + e.getChunk().getZ() + "]"; + plugin.debug("Unloaded " + num + " shops in chunk " + chunkStr); + } } } diff --git a/src/main/java/de/epiceric/shopchest/sql/Database.java b/src/main/java/de/epiceric/shopchest/sql/Database.java index 9443be2..7fbe9c5 100644 --- a/src/main/java/de/epiceric/shopchest/sql/Database.java +++ b/src/main/java/de/epiceric/shopchest/sql/Database.java @@ -4,13 +4,13 @@ import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.config.Config; import de.epiceric.shopchest.event.ShopBuySellEvent; import de.epiceric.shopchest.event.ShopBuySellEvent.Type; -import de.epiceric.shopchest.exceptions.WorldNotFoundException; import de.epiceric.shopchest.shop.Shop; import de.epiceric.shopchest.shop.ShopProduct; import de.epiceric.shopchest.shop.Shop.ShopType; import de.epiceric.shopchest.utils.Callback; import de.epiceric.shopchest.utils.Utils; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.World; @@ -28,16 +28,20 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import com.zaxxer.hikari.HikariDataSource; public abstract class Database { - private final Set notFoundWorlds = new HashSet<>(); private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private boolean initialized; + String tableShops; String tableLogs; String tableLogouts; @@ -305,6 +309,7 @@ public abstract class Database { ResultSet rs = s.executeQuery("SELECT COUNT(id) FROM " + tableShops); if (rs.next()) { int count = rs.getInt(1); + initialized = true; plugin.debug("Initialized database with " + count + " entries"); @@ -362,74 +367,108 @@ public abstract class Database { } /** - * Get all shops from the database + * Get all shops from the database that are located in the given chunks * - * @param showConsoleMessages Whether console messages (errors or warnings) - * should be shown - * @param callback Callback that - if succeeded - returns a read-only - * collection of all shops (as - * {@code Collection}) + * @param chunks Shops in these chunks are retrieved + * @param callback Callback that returns an immutable collection of shops if succeeded */ - public void getShops(final boolean showConsoleMessages, final Callback> callback) { - new BukkitRunnable() { + public void getShopsInChunks(final Chunk[] chunks, final Callback> callback) { + // Split chunks into packages containing each {splitSize} chunks at max + int splitSize = 80; + int parts = (int) Math.ceil(chunks.length / (double) splitSize); + Chunk[][] splitChunks = new Chunk[parts][]; + for (int i = 0; i < parts; i++) { + int size = i < parts - 1 ? splitSize : chunks.length % splitSize; + Chunk[] tmp = new Chunk[size]; + System.arraycopy(chunks, i * splitSize, tmp, 0, size); + splitChunks[i] = tmp; + } + + new BukkitRunnable(){ @Override public void run() { - ArrayList shops = new ArrayList<>(); + List shops = new ArrayList<>(); - try (Connection con = dataSource.getConnection(); - PreparedStatement ps = con.prepareStatement("SELECT * FROM " + tableShops + "")) { - ResultSet rs = ps.executeQuery(); + // Send a request for each chunk package + for (Chunk[] newChunks : splitChunks) { - while (rs.next()) { - int id = rs.getInt("id"); - - plugin.debug("Getting Shop... (#" + id + ")"); - - String worldName = rs.getString("world"); - World world = Bukkit.getWorld(worldName); - - if (world == null) { - WorldNotFoundException ex = new WorldNotFoundException(worldName); - if (showConsoleMessages && !notFoundWorlds.contains(worldName)) { - plugin.getLogger().warning(ex.getMessage()); - notFoundWorlds.add(worldName); - } - plugin.debug("Failed to get shop (#" + id + ")"); - plugin.debug(ex); - continue; + // Map chunks by world + Map> chunksByWorld = new HashMap<>(); + for (Chunk chunk : newChunks) { + String world = chunk.getWorld().getName(); + Set chunksForWorld = chunksByWorld.getOrDefault(world, new HashSet<>()); + chunksForWorld.add(chunk); + chunksByWorld.put(world, chunksForWorld); + } + + // Create query dynamically + String query = "SELECT * FROM " + tableShops + " WHERE "; + for (String world : chunksByWorld.keySet()) { + query += "(world = ? AND ("; + int chunkNum = chunksByWorld.get(world).size(); + for (int i = 0; i < chunkNum; i++) { + query += "((x BETWEEN ? AND ?) AND (z BETWEEN ? AND ?)) OR "; } - - int x = rs.getInt("x"); - int y = rs.getInt("y"); - int z = rs.getInt("z"); - Location location = new Location(world, x, y, z); - OfflinePlayer vendor = Bukkit.getOfflinePlayer(UUID.fromString(rs.getString("vendor"))); - ItemStack itemStack = Utils.decode(rs.getString("product")); - int amount = rs.getInt("amount"); - ShopProduct product = new ShopProduct(itemStack, amount); - double buyPrice = rs.getDouble("buyprice"); - double sellPrice = rs.getDouble("sellprice"); - ShopType shopType = ShopType.valueOf(rs.getString("shoptype")); - - plugin.debug("Initializing new shop... (#" + id + ")"); - - shops.add(new Shop(id, plugin, vendor, product, location, buyPrice, sellPrice, shopType)); + query += "1=0)) OR "; } + query += "1=0"; + + try (Connection con = dataSource.getConnection(); + PreparedStatement ps = con.prepareStatement(query)) { + int index = 0; + for (String world : chunksByWorld.keySet()) { + ps.setString(++index, world); + for (Chunk chunk : chunksByWorld.get(world)) { + int minX = chunk.getX() * 16; + int minZ = chunk.getZ() * 16; + ps.setInt(++index, minX); + ps.setInt(++index, minX + 15); + ps.setInt(++index, minZ); + ps.setInt(++index, minZ + 15); + } + } + + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + int id = rs.getInt("id"); + + plugin.debug("Getting Shop... (#" + id + ")"); + + int x = rs.getInt("x"); + int y = rs.getInt("y"); + int z = rs.getInt("z"); + + World world = plugin.getServer().getWorld(rs.getString("world")); + Location location = new Location(world, x, y, z); + OfflinePlayer vendor = Bukkit.getOfflinePlayer(UUID.fromString(rs.getString("vendor"))); + ItemStack itemStack = Utils.decode(rs.getString("product")); + int amount = rs.getInt("amount"); + ShopProduct product = new ShopProduct(itemStack, amount); + double buyPrice = rs.getDouble("buyprice"); + double sellPrice = rs.getDouble("sellprice"); + ShopType shopType = ShopType.valueOf(rs.getString("shoptype")); + + plugin.debug("Initializing new shop... (#" + id + ")"); + + shops.add(new Shop(id, plugin, vendor, product, location, buyPrice, sellPrice, shopType)); + } + } catch (SQLException ex) { + if (callback != null) { + callback.callSyncError(ex); + } + + plugin.getLogger().severe("Failed to get shops from database"); + plugin.debug("Failed to get shops"); + plugin.debug(ex); - if (callback != null) { - callback.callSyncResult(Collections.unmodifiableCollection(shops)); + return; } - } catch (SQLException ex) { - if (callback != null) { - callback.callSyncError(ex); - } - - plugin.getLogger().severe("Failed to get shops from database"); - plugin.debug("Failed to get shops"); - plugin.debug(ex); } - - } + + if (callback != null) { + callback.callSyncResult(Collections.unmodifiableCollection(shops)); + } + }; }.runTaskAsynchronously(plugin); } @@ -730,6 +769,13 @@ public abstract class Database { } } + /** + * Returns whether a connection to the database has been established + */ + public boolean isInitialized() { + return initialized; + } + public enum DatabaseType { SQLite, MySQL } diff --git a/src/main/java/de/epiceric/shopchest/utils/ShopUtils.java b/src/main/java/de/epiceric/shopchest/utils/ShopUtils.java index 449149f..17ddbb0 100644 --- a/src/main/java/de/epiceric/shopchest/utils/ShopUtils.java +++ b/src/main/java/de/epiceric/shopchest/utils/ShopUtils.java @@ -2,8 +2,12 @@ package de.epiceric.shopchest.utils; import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.config.Config; +import de.epiceric.shopchest.event.ShopsLoadedEvent; +import de.epiceric.shopchest.event.ShopsUnloadedEvent; import de.epiceric.shopchest.shop.Shop; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.block.Chest; @@ -246,6 +250,7 @@ public class ShopUtils { * @return The amount of a shops a player has (if {@link Config#excludeAdminShops} is true, admin shops won't be counted) */ public int getShopAmount(OfflinePlayer p) { + // FIXME: currently only showing loaded shops float shopCount = 0; for (Shop shop : getShops()) { @@ -265,54 +270,65 @@ public class ShopUtils { } /** - * Reload the shops - * @param reloadConfig Whether the configuration should also be reloaded - * @param showConsoleMessages Whether messages about the language file should be shown in the console - * @param callback Callback that - if succeeded - returns the amount of shops that were reloaded (as {@code int}) + * Gets all shops in the given chunk from the database and adds them to the server + * @param chunk The chunk to load shops from + * @param callback Callback that returns the amount of shops added if succeeded + * @see ShopUtils#loadShops(Chunk[], Callback) */ - public void reloadShops(boolean reloadConfig, final boolean showConsoleMessages, final Callback callback) { - plugin.debug("Reloading shops..."); + public void loadShops(final Chunk chunk, final Callback callback) { + loadShops(new Chunk[] {chunk}, callback); + } - if (reloadConfig) { - plugin.getShopChestConfig().reload(false, true, showConsoleMessages); - plugin.getHologramFormat().reload(); - plugin.getUpdater().restart(); - } - - plugin.getShopDatabase().connect(new Callback(plugin) { + /** + * Gets all shops in the given chunks from the database and adds them to the server + * @param chunk The chunks to load shops from + * @param callback Callback that returns the amount of shops added if succeeded + * @see ShopUtils#loadShops(Chunk Callback) + */ + public void loadShops(final Chunk[] chunks, final Callback callback) { + plugin.getShopDatabase().getShopsInChunks(chunks, new Callback>(plugin) { @Override - public void onResult(Integer result) { - for (Shop shop : getShopsCopy()) { - removeShop(shop, false); - plugin.debug("Removed shop (#" + shop.getID() + ")"); + public void onResult(Collection result) { + for (Shop shop : result) { + if (shop.create(true)) { + addShop(shop, false); + } } - plugin.getShopDatabase().getShops(showConsoleMessages, new Callback>(plugin) { - @Override - public void onResult(Collection result) { - for (Shop shop : result) { - if (shop.create(showConsoleMessages)) { - addShop(shop, false); - } - } + if (callback != null) callback.onResult(result.size()); - if (callback != null) callback.callSyncResult(result.size()); - } - - @Override - public void onError(Throwable throwable) { - if (callback != null) callback.callSyncError(throwable); - } - }); + Bukkit.getPluginManager().callEvent(new ShopsLoadedEvent(result)); } @Override public void onError(Throwable throwable) { - if (callback != null) callback.callSyncError(throwable); + if (callback != null) callback.onError(throwable); } }); } + /** + * Removes all shops from the given chunk from the server + * @param chunk The chunk containing the shops to unload + * @return The amount of shops that were unloaded + */ + public int unloadShops(final Chunk chunk) { + Set unloadedShops = new HashSet<>(); + + Iterator iter = getShops().iterator(); + while(iter.hasNext()) { + Shop shop = iter.next(); + if (shop.getLocation().getChunk().equals(chunk)) { + removeShop(shop, false); + unloadedShops.add(shop); + plugin.debug("Unloaded shop (#" + shop.getID() + ")"); + } + } + + Bukkit.getPluginManager().callEvent(new ShopsUnloadedEvent(Collections.unmodifiableCollection(unloadedShops))); + return unloadedShops.size(); + } + /** * Update hologram and item of all shops for a player * @param player Player to show the updates