Load/unload shops on chunk load/unload

This breaks shop limits, only loaded shops are counted at the moment
This commit is contained in:
Eric 2020-01-21 20:00:08 +01:00
parent f15fdc781f
commit c595b574ec
8 changed files with 339 additions and 120 deletions

View File

@ -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<Integer>(this) {
getShopDatabase().connect(new Callback<Integer>(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<Integer>(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

View File

@ -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<Integer>(plugin) {
// Reload configurations
plugin.getShopChestConfig().reload(false, true, true);
plugin.getHologramFormat().reload();
plugin.getUpdater().restart();
// Remove all shops
Iterator<Shop> 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<Integer>(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<Integer>(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

View File

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

View File

@ -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<Shop> shops;
public ShopsLoadedEvent(Collection<Shop> shops) {
this.shops = shops;
}
public Collection<Shop> getShops() {
return shops;
}
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -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<Shop> shops;
public ShopsUnloadedEvent(Collection<Shop> shops) {
this.shops = shops;
}
public Collection<Shop> getShops() {
return shops;
}
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

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

View File

@ -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<String> 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<Shop>})
* @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<Collection<Shop>> callback) {
new BukkitRunnable() {
public void getShopsInChunks(final Chunk[] chunks, final Callback<Collection<Shop>> 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<Shop> shops = new ArrayList<>();
List<Shop> 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<String, Set<Chunk>> chunksByWorld = new HashMap<>();
for (Chunk chunk : newChunks) {
String world = chunk.getWorld().getName();
Set<Chunk> 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
}

View File

@ -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<Integer> callback) {
plugin.debug("Reloading shops...");
public void loadShops(final Chunk chunk, final Callback<Integer> 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<Integer>(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<Integer> callback) {
plugin.getShopDatabase().getShopsInChunks(chunks, new Callback<Collection<Shop>>(plugin) {
@Override
public void onResult(Integer result) {
for (Shop shop : getShopsCopy()) {
removeShop(shop, false);
plugin.debug("Removed shop (#" + shop.getID() + ")");
public void onResult(Collection<Shop> result) {
for (Shop shop : result) {
if (shop.create(true)) {
addShop(shop, false);
}
}
plugin.getShopDatabase().getShops(showConsoleMessages, new Callback<Collection<Shop>>(plugin) {
@Override
public void onResult(Collection<Shop> 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<Shop> unloadedShops = new HashSet<>();
Iterator<Shop> 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