diff --git a/src/main/java/world/bentobox/bentobox/BentoBox.java b/src/main/java/world/bentobox/bentobox/BentoBox.java index 89529ddb0..cfdcb8f8e 100644 --- a/src/main/java/world/bentobox/bentobox/BentoBox.java +++ b/src/main/java/world/bentobox/bentobox/BentoBox.java @@ -25,6 +25,7 @@ import world.bentobox.bentobox.managers.AddonsManager; import world.bentobox.bentobox.managers.CommandsManager; import world.bentobox.bentobox.managers.FlagsManager; import world.bentobox.bentobox.managers.HooksManager; +import world.bentobox.bentobox.managers.IslandDeletionManager; import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.LocalesManager; @@ -67,6 +68,8 @@ public class BentoBox extends JavaPlugin { private boolean isLoaded; + private IslandDeletionManager islandDeletionManager; + @Override public void onEnable(){ if (!ServerCompatibility.getInstance().checkCompatibility(this).isCanLaunch()) { @@ -142,10 +145,10 @@ public class BentoBox extends JavaPlugin { // Load islands from database - need to wait until all the worlds are loaded islandsManager.load(); - // Save islands & players data asynchronously every X minutes + // Save islands & players data every X minutes instance.getServer().getScheduler().runTaskTimer(instance, () -> { - playersManager.save(true); - islandsManager.save(true); + playersManager.saveAll(); + islandsManager.saveAll(); }, getSettings().getDatabaseBackupPeriod() * 20 * 60L, getSettings().getDatabaseBackupPeriod() * 20 * 60L); // Make sure all flag listeners are registered. @@ -169,13 +172,15 @@ public class BentoBox extends JavaPlugin { // Setup the Placeholders manager placeholdersManager = new PlaceholdersManager(this); - // Fire plugin ready event - isLoaded = true; - Bukkit.getServer().getPluginManager().callEvent(new BentoBoxReadyEvent()); - + // Show banner User.getInstance(Bukkit.getConsoleSender()).sendMessage("successfully-loaded", TextVariables.VERSION, instance.getDescription().getVersion(), "[time]", String.valueOf(System.currentTimeMillis() - startMillis)); + + // Fire plugin ready event - this should go last after everything else + isLoaded = true; + Bukkit.getServer().getPluginManager().callEvent(new BentoBoxReadyEvent()); + }); } @@ -196,6 +201,9 @@ public class BentoBox extends JavaPlugin { manager.registerEvents(new BannedVisitorCommands(this), this); // Death counter manager.registerEvents(new DeathListener(this), this); + // Island Delete Manager + islandDeletionManager = new IslandDeletionManager(this); + manager.registerEvents(islandDeletionManager, this); } @Override @@ -356,4 +364,11 @@ public class BentoBox extends JavaPlugin { public PlaceholdersManager getPlaceholdersManager() { return placeholdersManager; } + + /** + * @return the islandDeletionManager + */ + public IslandDeletionManager getIslandDeletionManager() { + return islandDeletionManager; + } } diff --git a/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminRegisterCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminRegisterCommand.java index 46a3654d0..a8c0a0461 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminRegisterCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminRegisterCommand.java @@ -51,6 +51,12 @@ public class AdminRegisterCommand extends ConfirmableCommand { return false; } + // Check if this spot is still being deleted + Location closestIsland = getClosestIsland(user.getLocation()); + if (getPlugin().getIslandDeletionManager().inDeletion(closestIsland)) { + user.sendMessage("commands.admin.register.in-deletion"); + return false; + } // Check if island is owned Optional island = getIslands().getIslandAt(user.getLocation()); if (island.map(i -> i.getOwner() != null).orElse(false)) { @@ -69,7 +75,7 @@ public class AdminRegisterCommand extends ConfirmableCommand { user.sendMessage("commands.admin.register.no-island-here"); this.askConfirmation(user, () -> { // Make island here - Island i = getIslands().createIsland(getClosestIsland(user.getLocation()), targetUUID); + Island i = getIslands().createIsland(closestIsland, targetUUID); getIslands().setOwner(user, targetUUID, i); getWorld().getBlockAt(i.getCenter()).setType(Material.BEDROCK); user.sendMessage("commands.admin.register.registered-island", "[xyz]", Util.xyz(i.getCenter().toVector())); diff --git a/src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangeResetCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangeResetCommand.java index e835559a5..e89da5cce 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangeResetCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangeResetCommand.java @@ -1,12 +1,15 @@ package world.bentobox.bentobox.api.commands.admin.range; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.util.Util; public class AdminRangeResetCommand extends CompositeCommand { @@ -50,4 +53,15 @@ public class AdminRangeResetCommand extends CompositeCommand { return true; } + + @Override + public Optional> tabComplete(User user, String alias, List args) { + String lastArg = !args.isEmpty() ? args.get(args.size()-1) : ""; + if (args.isEmpty()) { + // Don't show every player on the server. Require at least the first letter + return Optional.empty(); + } + List options = new ArrayList<>(Util.getOnlinePlayerList(user)); + return Optional.of(Util.tabLimit(options, lastArg)); + } } diff --git a/src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangeSetCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangeSetCommand.java index db420855a..7c28e3b68 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangeSetCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/admin/range/AdminRangeSetCommand.java @@ -1,6 +1,8 @@ package world.bentobox.bentobox.api.commands.admin.range; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.apache.commons.lang.StringUtils; @@ -9,6 +11,7 @@ import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.util.Util; public class AdminRangeSetCommand extends CompositeCommand { @@ -72,4 +75,15 @@ public class AdminRangeSetCommand extends CompositeCommand { return true; } + + @Override + public Optional> tabComplete(User user, String alias, List args) { + String lastArg = !args.isEmpty() ? args.get(args.size()-1) : ""; + if (args.isEmpty()) { + // Don't show every player on the server. Require at least the first letter + return Optional.empty(); + } + List options = new ArrayList<>(Util.getOnlinePlayerList(user)); + return Optional.of(Util.tabLimit(options, lastArg)); + } } diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteAcceptCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteAcceptCommand.java index cd6b84aad..cd324876d 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteAcceptCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteAcceptCommand.java @@ -106,7 +106,7 @@ public class IslandTeamInviteAcceptCommand extends CompositeCommand { if (inviter != null) { inviter.sendMessage("commands.island.team.invite.accept.name-joined-your-island", TextVariables.NAME, user.getName()); } - getIslands().save(false); + getIslands().save(island); return true; } diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamSetownerCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamSetownerCommand.java index 43c0e889c..d743818fc 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamSetownerCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamSetownerCommand.java @@ -10,6 +10,7 @@ import world.bentobox.bentobox.api.events.IslandBaseEvent; import world.bentobox.bentobox.api.events.team.TeamEvent; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; public class IslandTeamSetownerCommand extends CompositeCommand { @@ -59,9 +60,9 @@ public class IslandTeamSetownerCommand extends CompositeCommand { return false; } // Fire event so add-ons can run commands, etc. + Island island = getIslands().getIsland(getWorld(), playerUUID); IslandBaseEvent event = TeamEvent.builder() - .island(getIslands() - .getIsland(getWorld(), playerUUID)) + .island(island) .reason(TeamEvent.Reason.SETOWNER) .involvedPlayer(targetUUID) .build(); @@ -70,7 +71,7 @@ public class IslandTeamSetownerCommand extends CompositeCommand { return false; } getIslands().setOwner(getWorld(), user, targetUUID); - getIslands().save(true); + getIslands().save(island); return true; } diff --git a/src/main/java/world/bentobox/bentobox/api/events/IslandBaseEvent.java b/src/main/java/world/bentobox/bentobox/api/events/IslandBaseEvent.java index c0002f2fc..eb8395394 100644 --- a/src/main/java/world/bentobox/bentobox/api/events/IslandBaseEvent.java +++ b/src/main/java/world/bentobox/bentobox/api/events/IslandBaseEvent.java @@ -43,7 +43,7 @@ public class IslandBaseEvent extends PremadeEvent implements Cancellable { } /** - * @return the island involved in this event + * @return the island involved in this event. This may be null in the case of deleted islands, so use location instead */ public Island getIsland(){ return island; diff --git a/src/main/java/world/bentobox/bentobox/api/events/island/IslandEvent.java b/src/main/java/world/bentobox/bentobox/api/events/island/IslandEvent.java index 84fced873..89448eb1c 100644 --- a/src/main/java/world/bentobox/bentobox/api/events/island/IslandEvent.java +++ b/src/main/java/world/bentobox/bentobox/api/events/island/IslandEvent.java @@ -6,6 +6,7 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import world.bentobox.bentobox.api.events.IslandBaseEvent; +import world.bentobox.bentobox.database.objects.IslandDeletion; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.lists.Flags; @@ -58,9 +59,14 @@ public class IslandEvent extends IslandBaseEvent { */ CREATED, /** - * Fired just before any island chunks are to be deleted. + * Fired when an island is to be deleted. Note an island can be deleted without having + * chunks removed. */ DELETE, + /** + * Fired when island chunks are going to be deleted + */ + DELETE_CHUNKS, /** * Fired after all island chunks have been deleted or set for regeneration by the server */ @@ -168,14 +174,39 @@ public class IslandEvent extends IslandBaseEvent { super(island, player, admin, location); } } + /** + * Fired when an island chunks are going to be deleted. + * May be cancelled. + * + */ + public static class IslandDeleteChunksEvent extends IslandBaseEvent { + private final IslandDeletion deletedIslandInfo; + + private IslandDeleteChunksEvent(Island island, UUID player, boolean admin, Location location, IslandDeletion deletedIsland) { + // Final variables have to be declared in the constructor + super(island, player, admin, location); + this.deletedIslandInfo = deletedIsland; + } + + public IslandDeletion getDeletedIslandInfo() { + return deletedIslandInfo; + } + } /** * Fired when an island is deleted. * */ public static class IslandDeletedEvent extends IslandBaseEvent { - private IslandDeletedEvent(Island island, UUID player, boolean admin, Location location) { + private final IslandDeletion deletedIslandInfo; + + private IslandDeletedEvent(Island island, UUID player, boolean admin, Location location, IslandDeletion deletedIsland) { // Final variables have to be declared in the constructor super(island, player, admin, location); + this.deletedIslandInfo = deletedIsland; + } + + public IslandDeletion getDeletedIslandInfo() { + return deletedIslandInfo; } } /** @@ -256,6 +287,7 @@ public class IslandEvent extends IslandBaseEvent { private Reason reason = Reason.UNKNOWN; private boolean admin; private Location location; + private IslandDeletion deletedIslandInfo; public IslandEventBuilder island(Island island) { this.island = island; @@ -295,6 +327,11 @@ public class IslandEvent extends IslandBaseEvent { return this; } + public IslandEventBuilder deletedIslandInfo(IslandDeletion deletedIslandInfo) { + this.deletedIslandInfo = deletedIslandInfo; + return this; + } + public IslandBaseEvent build() { // Call the generic event for developers who just want one event and use the Reason enum Bukkit.getServer().getPluginManager().callEvent(new IslandEvent(island, player, admin, location, reason)); @@ -316,8 +353,12 @@ public class IslandEvent extends IslandBaseEvent { IslandDeleteEvent delete = new IslandDeleteEvent(island, player, admin, location); Bukkit.getServer().getPluginManager().callEvent(delete); return delete; + case DELETE_CHUNKS: + IslandDeleteChunksEvent deleteChunks = new IslandDeleteChunksEvent(island, player, admin, location, deletedIslandInfo); + Bukkit.getServer().getPluginManager().callEvent(deleteChunks); + return deleteChunks; case DELETED: - IslandDeletedEvent deleted = new IslandDeletedEvent(island, player, admin, location); + IslandDeletedEvent deleted = new IslandDeletedEvent(island, player, admin, location, deletedIslandInfo); Bukkit.getServer().getPluginManager().callEvent(deleted); return deleted; case ENTER: diff --git a/src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java index a56eae483..e94e867e9 100644 --- a/src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java @@ -4,6 +4,8 @@ import java.beans.IntrospectionException; import java.lang.reflect.InvocationTargetException; import java.util.List; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.Addon; @@ -38,12 +40,14 @@ public abstract class AbstractDatabaseHandler { /** * The addon that is accessing the database, if any. */ + @Nullable private Addon addon; /** * Get the addon that is accessing the database, if any. May be null. * @return the addon */ + @Nullable public Addon getAddon() { return addon; } @@ -52,7 +56,7 @@ public abstract class AbstractDatabaseHandler { * Set the addon that is accessing the database, if any. * @param addon the addon to set */ - public void setAddon(Addon addon) { + public void setAddon(@Nullable Addon addon) { this.addon = addon; } @@ -84,7 +88,8 @@ public abstract class AbstractDatabaseHandler { * @param uniqueId - unique ID * @return */ - public abstract T loadObject(String uniqueId) throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException; + @Nullable + public abstract T loadObject(@NonNull String uniqueId) throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException; /** * Save T into the corresponding database @@ -110,4 +115,12 @@ public abstract class AbstractDatabaseHandler { * Closes the database */ public abstract void close(); + + /** + * Attempts to delete the object with the uniqueId + * @param uniqueId - uniqueId of object + * @return true if successful, false if there is no such uniqueId + * @since 1.1 + */ + public abstract boolean deleteID(String uniqueId); } diff --git a/src/main/java/world/bentobox/bentobox/database/Database.java b/src/main/java/world/bentobox/bentobox/database/Database.java index 35f4d8d33..aadd5b5e9 100644 --- a/src/main/java/world/bentobox/bentobox/database/Database.java +++ b/src/main/java/world/bentobox/bentobox/database/Database.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; +import org.eclipse.jdt.annotation.NonNull; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.Addon; @@ -44,6 +45,7 @@ public class Database { * Load all the config objects and supply them as a list * @return list of config objects or an empty list if they cannot be loaded */ + @NonNull public List loadObjects() { List result = new ArrayList<>(); try { @@ -100,6 +102,16 @@ public class Database { return handler.objectExists(name); } + /** + * Attempts to delete the object with the uniqueId + * @param uniqueId - uniqueId of object + * @return true if successful, false if there is no such uniqueId + * @since 1.1 + */ + public boolean deleteID(String uniqueId) { + return handler.deleteID(uniqueId); + } + /** * Delete object from database * @param object - object to delete @@ -109,7 +121,7 @@ public class Database { handler.deleteObject(object); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | IntrospectionException e) { - logger.severe(() -> "Could not delete config! Error: " + e.getMessage()); + logger.severe(() -> "Could not delete object! Error: " + e.getMessage()); } } @@ -120,4 +132,6 @@ public class Database { handler.close(); } + + } \ No newline at end of file diff --git a/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java index 8f0ce46f2..4f3f8e9cf 100644 --- a/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java @@ -122,6 +122,28 @@ public class JSONDatabaseHandler extends AbstractJSONDatabaseHandler { } } + @Override + public boolean deleteID(String uniqueId) { + // The filename of the JSON file is the value of uniqueId field plus .json. Sometimes the .json is already appended. + if (!uniqueId.endsWith(JSON)) { + uniqueId = uniqueId + JSON; + } + // Get the database and table folders + File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME); + File tableFolder = new File(dataFolder, dataObject.getSimpleName()); + if (tableFolder.exists()) { + // Obtain the file and delete it + File file = new File(tableFolder, uniqueId); + try { + Files.delete(file.toPath()); + return true; + } catch (IOException e) { + plugin.logError("Could not delete json database object! " + file.getName() + " - " + e.getMessage()); + } + } + return false; + } + @Override public void deleteObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { // Null check @@ -137,25 +159,7 @@ public class JSONDatabaseHandler extends AbstractJSONDatabaseHandler { // Obtain the value of uniqueId within the instance (which must be a DataObject) PropertyDescriptor propertyDescriptor = new PropertyDescriptor("uniqueId", dataObject); Method method = propertyDescriptor.getReadMethod(); - String fileName = (String) method.invoke(instance); - - // The filename of the JSON file is the value of uniqueId field plus .json. Sometimes the .json is already appended. - if (!fileName.endsWith(JSON)) { - fileName = fileName + JSON; - } - - // Get the database and table folders - File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME); - File tableFolder = new File(dataFolder, dataObject.getSimpleName()); - if (tableFolder.exists()) { - // Obtain the file and delete it - File file = new File(tableFolder, fileName); - try { - Files.delete(file.toPath()); - } catch (IOException e) { - plugin.logError("Could not delete json database object! " + file.getName() + " - " + e.getMessage()); - } - } + deleteID((String) method.invoke(instance)); } @Override diff --git a/src/main/java/world/bentobox/bentobox/database/mongodb/MongoDBDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/mongodb/MongoDBDatabaseHandler.java index 05e031f61..b5a60efdb 100644 --- a/src/main/java/world/bentobox/bentobox/database/mongodb/MongoDBDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/mongodb/MongoDBDatabaseHandler.java @@ -106,6 +106,16 @@ public class MongoDBDatabaseHandler extends AbstractJSONDatabaseHandler { } } + @Override + public boolean deleteID(String uniqueId) { + try { + return collection.findOneAndDelete(new Document(MONGO_ID, uniqueId)) == null ? false : true; + } catch (Exception e) { + plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage()); + return false; + } + } + @Override public void deleteObject(T instance) { // Null check @@ -117,11 +127,7 @@ public class MongoDBDatabaseHandler extends AbstractJSONDatabaseHandler { plugin.logError("This class is not a DataObject: " + instance.getClass().getName()); return; } - try { - collection.findOneAndDelete(new Document(MONGO_ID, ((DataObject)instance).getUniqueId())); - } catch (Exception e) { - plugin.logError("Could not delete object " + instance.getClass().getName() + " " + e.getMessage()); - } + deleteID(((DataObject)instance).getUniqueId()); } @Override diff --git a/src/main/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseHandler.java index 6f1f5bf72..f332405b1 100644 --- a/src/main/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseHandler.java @@ -9,6 +9,8 @@ import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import org.bukkit.Bukkit; + import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; @@ -128,11 +130,18 @@ public class MySQLDatabaseHandler extends AbstractJSONDatabaseHandler { "`" + dataObject.getCanonicalName() + "` (json) VALUES (?) ON DUPLICATE KEY UPDATE json = ?"; - // Replace into is used so that any data in the table will be replaced with updated data - // The table name is the canonical name, so that add-ons can be sure of a unique table in the database + + Gson gson = getGson(); + String toStore = gson.toJson(instance); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> store(instance, toStore, sb)); + } else { + store(instance, toStore, sb); + } + } + + private void store(T instance, String toStore, String sb) { try (PreparedStatement preparedStatement = connection.prepareStatement(sb)) { - Gson gson = getGson(); - String toStore = gson.toJson(instance); preparedStatement.setString(1, toStore); preparedStatement.setString(2, toStore); preparedStatement.execute(); @@ -141,6 +150,22 @@ public class MySQLDatabaseHandler extends AbstractJSONDatabaseHandler { } } + @Override + public boolean deleteID(String uniqueId) { + String sb = "DELETE FROM `" + + dataObject.getCanonicalName() + + "` WHERE uniqueId = ?"; + try (PreparedStatement preparedStatement = connection.prepareStatement(sb)) { + // UniqueId needs to be placed in quotes + preparedStatement.setString(1, "\"" + uniqueId + "\""); + preparedStatement.execute(); + return preparedStatement.getUpdateCount() > 0; + } catch (Exception e) { + plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage()); + return false; + } + } + @Override public void deleteObject(T instance) { // Null check @@ -152,15 +177,9 @@ public class MySQLDatabaseHandler extends AbstractJSONDatabaseHandler { plugin.logError("This class is not a DataObject: " + instance.getClass().getName()); return; } - String sb = "DELETE FROM `" + - dataObject.getCanonicalName() + - "` WHERE uniqueId = ?"; - try (PreparedStatement preparedStatement = connection.prepareStatement(sb)) { + try { Method getUniqueId = dataObject.getMethod("getUniqueId"); - String uniqueId = (String) getUniqueId.invoke(instance); - // UniqueId needs to be placed in quotes - preparedStatement.setString(1, "\"" + uniqueId + "\""); - preparedStatement.execute(); + deleteID((String) getUniqueId.invoke(instance)); } catch (Exception e) { plugin.logError("Could not delete object " + instance.getClass().getName() + " " + e.getMessage()); } diff --git a/src/main/java/world/bentobox/bentobox/database/objects/Island.java b/src/main/java/world/bentobox/bentobox/database/objects/Island.java index 3cc5f7a5a..c8627c5e9 100644 --- a/src/main/java/world/bentobox/bentobox/database/objects/Island.java +++ b/src/main/java/world/bentobox/bentobox/database/objects/Island.java @@ -26,11 +26,11 @@ import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.configuration.WorldSettings; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.logs.LogEntry; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.adapters.Adapter; import world.bentobox.bentobox.database.objects.adapters.FlagSerializer; import world.bentobox.bentobox.database.objects.adapters.LogEntryListAdapter; -import world.bentobox.bentobox.api.logs.LogEntry; import world.bentobox.bentobox.lists.Flags; import world.bentobox.bentobox.managers.RanksManager; import world.bentobox.bentobox.util.Pair; @@ -62,6 +62,10 @@ public class Island implements DataObject { @Expose private int protectionRange; + // Maximum ever protection range - used in island deletion + @Expose + private int maxEverProtectionRange; + // World the island started in. This may be different from the island location @Expose private World world; @@ -115,6 +119,7 @@ public class Island implements DataObject { center = new Location(location.getWorld(), location.getX(), location.getY(), location.getZ()); range = BentoBox.getInstance().getIWM().getIslandDistance(world); this.protectionRange = protectionRange; + this.maxEverProtectionRange = protectionRange; } /** @@ -283,6 +288,20 @@ public class Island implements DataObject { return protectionRange; } + /** + * @return the maxEverProtectionRange or the protection range, whichever is larger + */ + public int getMaxEverProtectionRange() { + return Math.max(protectionRange, maxEverProtectionRange); + } + + /** + * @param maxEverProtectionRange the maxEverProtectionRange to set + */ + public void setMaxEverProtectionRange(int maxEverProtectionRange) { + this.maxEverProtectionRange = maxEverProtectionRange; + } + /** * @return true if the island is protected from the Purge, otherwise false */ @@ -532,6 +551,10 @@ public class Island implements DataObject { */ public void setProtectionRange(int protectionRange) { this.protectionRange = protectionRange; + // Ratchet up the maximum protection range + if (protectionRange > this.maxEverProtectionRange) { + this.maxEverProtectionRange = protectionRange; + } } /** @@ -659,14 +682,14 @@ public class Island implements DataObject { // Fixes #getLastPlayed() returning 0 when it is the owner's first connection. long lastPlayed = (plugin.getServer().getOfflinePlayer(owner).getLastPlayed() != 0) ? plugin.getServer().getOfflinePlayer(owner).getLastPlayed() : plugin.getServer().getOfflinePlayer(owner).getFirstPlayed(); - user.sendMessage("commands.admin.info.last-login","[date]", new Date(lastPlayed).toString()); + user.sendMessage("commands.admin.info.last-login","[date]", new Date(lastPlayed).toString()); - user.sendMessage("commands.admin.info.deaths", "[number]", String.valueOf(plugin.getPlayers().getDeaths(world, owner))); - String resets = String.valueOf(plugin.getPlayers().getResets(world, owner)); - String total = plugin.getIWM().getResetLimit(world) < 0 ? "Unlimited" : String.valueOf(plugin.getIWM().getResetLimit(world)); - user.sendMessage("commands.admin.info.resets-left", "[number]", resets, "[total]", total); - // Show team members - showMembers(user); + user.sendMessage("commands.admin.info.deaths", "[number]", String.valueOf(plugin.getPlayers().getDeaths(world, owner))); + String resets = String.valueOf(plugin.getPlayers().getResets(world, owner)); + String total = plugin.getIWM().getResetLimit(world) < 0 ? "Unlimited" : String.valueOf(plugin.getIWM().getResetLimit(world)); + user.sendMessage("commands.admin.info.resets-left", "[number]", resets, "[total]", total); + // Show team members + showMembers(user); } Vector location = center.toVector(); user.sendMessage("commands.admin.info.island-location", "[xyz]", Util.xyz(location)); @@ -674,6 +697,7 @@ public class Island implements DataObject { Vector to = center.toVector().add(new Vector(range-1, 0, range-1)).setY(center.getWorld().getMaxHeight()); user.sendMessage("commands.admin.info.island-coords", "[xz1]", Util.xyz(from), "[xz2]", Util.xyz(to)); user.sendMessage("commands.admin.info.protection-range", "[range]", String.valueOf(protectionRange)); + user.sendMessage("commands.admin.info.max-protection-range", "[range]", String.valueOf(maxEverProtectionRange)); Vector pfrom = center.toVector().subtract(new Vector(protectionRange, 0, protectionRange)).setY(0); Vector pto = center.toVector().add(new Vector(protectionRange-1, 0, protectionRange-1)).setY(center.getWorld().getMaxHeight()); user.sendMessage("commands.admin.info.protection-coords", "[xz1]", Util.xyz(pfrom), "[xz2]", Util.xyz(pto)); diff --git a/src/main/java/world/bentobox/bentobox/database/objects/IslandDeletion.java b/src/main/java/world/bentobox/bentobox/database/objects/IslandDeletion.java new file mode 100644 index 000000000..2f3a59aba --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/objects/IslandDeletion.java @@ -0,0 +1,178 @@ +package world.bentobox.bentobox.database.objects; + +import java.util.UUID; + +import org.bukkit.Location; +import org.bukkit.World; + +import com.google.gson.annotations.Expose; + +/** + * Data object to store islands in deletion + * @author tastybento + * @since 1.1 + */ +public class IslandDeletion implements DataObject { + + @Expose + private String uniqueId = ""; + + @Expose + private Location location; + + @Expose + private int minXChunk; + + @Expose + private int maxXChunk; + + @Expose + private int minZChunk; + + @Expose + private int maxZChunk; + + public IslandDeletion() {} + + public IslandDeletion(Island island) { + uniqueId = UUID.randomUUID().toString(); + location = island.getCenter(); + minXChunk = (location.getBlockX() - island.getMaxEverProtectionRange()) >> 4; + maxXChunk = (island.getMaxEverProtectionRange() + location.getBlockX() - 1) >> 4; + minZChunk = (location.getBlockZ() - island.getMaxEverProtectionRange()) >> 4; + maxZChunk = (island.getMaxEverProtectionRange() + location.getBlockZ() - 1) >> 4; + } + + public IslandDeletion(Location location, int minXChunk, int maxXChunk, int minZChunk, int maxZChunk) { + this.uniqueId = UUID.randomUUID().toString(); + this.location = location; + this.minXChunk = minXChunk; + this.maxXChunk = maxXChunk; + this.minZChunk = minZChunk; + this.maxZChunk = maxZChunk; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof IslandDeletion)) { + return false; + } + IslandDeletion other = (IslandDeletion) obj; + if (uniqueId == null) { + if (other.uniqueId != null) { + return false; + } + } else if (!uniqueId.equals(other.uniqueId)) { + return false; + } + return true; + } + + /** + * @return the location + */ + public Location getLocation() { + return location; + } + + /** + * @return the maxXChunk + */ + public int getMaxXChunk() { + return maxXChunk; + } + + /** + * @return the maxZChunk + */ + public int getMaxZChunk() { + return maxZChunk; + } + + /** + * @return the minXChunk + */ + public int getMinXChunk() { + return minXChunk; + } + + /** + * @return the minZChunk + */ + public int getMinZChunk() { + return minZChunk; + } + + @Override + public String getUniqueId() { + return uniqueId; + } + + /** + * @return the world + */ + public World getWorld() { + return location.getWorld(); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((uniqueId == null) ? 0 : uniqueId.hashCode()); + return result; + } + + /** + * @param location the location to set + */ + public void setLocation(Location location) { + this.location = location; + } + + /** + * @param maxXChunk the maxXChunk to set + */ + public void setMaxXChunk(int maxXChunk) { + this.maxXChunk = maxXChunk; + } + + /** + * @param maxZChunk the maxZChunk to set + */ + public void setMaxZChunk(int maxZChunk) { + this.maxZChunk = maxZChunk; + } + + /** + * @param minXChunk the minXChunk to set + */ + public void setMinXChunk(int minXChunk) { + this.minXChunk = minXChunk; + } + + /** + * @param minZChunk the minZChunk to set + */ + public void setMinZChunk(int minZChunk) { + this.minZChunk = minZChunk; + } + + @Override + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } +} + diff --git a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseConnector.java b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseConnector.java index 57c83e333..4e0c7f407 100644 --- a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseConnector.java +++ b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseConnector.java @@ -6,8 +6,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.nio.file.Files; -import java.nio.file.StandardCopyOption; import java.sql.Connection; import java.util.ArrayList; import java.util.List; @@ -18,6 +19,8 @@ import java.util.UUID; import org.bukkit.configuration.file.YamlConfiguration; +import com.google.common.base.Charsets; + import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.database.DatabaseConnector; @@ -78,29 +81,21 @@ public class YamlDatabaseConnector implements DatabaseConnector { return config; } - public void saveYamlFile(YamlConfiguration yamlConfig, String tableName, String fileName, Map commentMap) { - if (!fileName.endsWith(YML)) { - fileName = fileName + YML; - } + public void saveYamlFile(String data, String tableName, String fileName, Map commentMap) { + String name = fileName.endsWith(YML) ? fileName : fileName + YML; File tableFolder = new File(plugin.getDataFolder(), tableName); - File file = new File(tableFolder, fileName); + File file = new File(tableFolder, name); if (!tableFolder.exists()) { tableFolder.mkdirs(); } - try { - File tmpFile = new File(tableFolder, fileName + ".bak"); - if (file.exists()) { - // Make a backup of file - Files.copy(file.toPath(), tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - yamlConfig.save(file.toPath().toString()); - Files.deleteIfExists(tmpFile.toPath()); - } catch (Exception e) { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), Charsets.UTF_8)) { + writer.write(data); + } catch (IOException e) { plugin.logError("Could not save yml file: " + tableName + " " + fileName + " " + e.getMessage()); return; } if (commentMap != null && !commentMap.isEmpty()) { - commentFile(new File(tableFolder, fileName), commentMap); + commentFile(new File(tableFolder, name), commentMap); } } diff --git a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java index 3a80910e1..4e94eb09e 100644 --- a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java @@ -22,6 +22,7 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.configuration.MemorySection; @@ -307,18 +308,14 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // This is the Yaml Configuration that will be used and saved at the end YamlConfiguration config = new YamlConfiguration(); - // The file name of the Yaml file. - String filename = ""; - String path = DATABASE_FOLDER_NAME + File.separator + dataObject.getSimpleName(); // Comments for the file Map yamlComments = new HashMap<>(); // Only allow storing in an arbitrary place if it is a config object. Otherwise it is in the database StoreAt storeAt = instance.getClass().getAnnotation(StoreAt.class); - if (storeAt != null) { - path = storeAt.path(); - filename = storeAt.filename(); - } + String path = storeAt == null ? DATABASE_FOLDER_NAME + File.separator + dataObject.getSimpleName() : storeAt.path(); + String filename = storeAt == null ? "" : storeAt.filename(); + // See if there are any top-level comments // See if there are multiple comments ConfigComment.Line comments = instance.getClass().getAnnotation(ConfigComment.Line.class); @@ -443,7 +440,16 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { throw new IllegalArgumentException("No uniqueId in class"); } - ((YamlDatabaseConnector)databaseConnector).saveYamlFile(config, path, filename, yamlComments); + // Save + String name = filename; + String data = config.saveToString(); + if (plugin.isEnabled()) { + // Async + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> ((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments)); + } else { + // Sync for shutdown + ((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments); + } } private void setComment(ConfigComment comment, YamlConfiguration config, Map yamlComments, String parent) { @@ -553,6 +559,28 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { return value; } + @Override + public boolean deleteID(String uniqueId) { + // The filename of the YAML file is the value of uniqueId field plus .yml. Sometimes the .yml is already appended. + if (!uniqueId.endsWith(YML)) { + uniqueId = uniqueId + YML; + } + // Get the database and table folders + File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME); + File tableFolder = new File(dataFolder, dataObject.getSimpleName()); + if (tableFolder.exists()) { + // Obtain the file and delete it + File file = new File(tableFolder, uniqueId); + try { + Files.delete(file.toPath()); + return true; + } catch (IOException e) { + plugin.logError("Could not delete yml database object! " + file.getName() + " - " + e.getMessage()); + } + } + return false; + } + /* (non-Javadoc) * @see world.bentobox.bentobox.database.AbstractDatabaseHandler#deleteObject(java.lang.Object) */ @@ -571,23 +599,8 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // Obtain the value of uniqueId within the instance (which must be a DataObject) PropertyDescriptor propertyDescriptor = new PropertyDescriptor("uniqueId", dataObject); Method method = propertyDescriptor.getReadMethod(); - String fileName = (String) method.invoke(instance); - // The filename of the YAML file is the value of uniqueId field plus .yml. Sometimes the .yml is already appended. - if (!fileName.endsWith(YML)) { - fileName = fileName + YML; - } - // Get the database and table folders - File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME); - File tableFolder = new File(dataFolder, dataObject.getSimpleName()); - if (tableFolder.exists()) { - // Obtain the file and delete it - File file = new File(tableFolder, fileName); - try { - Files.delete(file.toPath()); - } catch (IOException e) { - plugin.logError("Could not delete yml database object! " + file.getName() + " - " + e.getMessage()); - } - } + deleteID((String) method.invoke(instance)); + } @Override diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandDeletionManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandDeletionManager.java new file mode 100644 index 000000000..5a1708dd8 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/managers/IslandDeletionManager.java @@ -0,0 +1,83 @@ +package world.bentobox.bentobox.managers; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bukkit.Location; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.events.BentoBoxReadyEvent; +import world.bentobox.bentobox.api.events.island.IslandEvent.IslandDeleteChunksEvent; +import world.bentobox.bentobox.api.events.island.IslandEvent.IslandDeletedEvent; +import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.objects.IslandDeletion; +import world.bentobox.bentobox.util.DeleteIslandChunks; +import world.bentobox.bentobox.util.Util; + +/** + * Listens for island deletions and adds them to the database. Removes them when the island is deleted. + * @author tastybento + * @since 1.1 + */ +public class IslandDeletionManager implements Listener { + + private BentoBox plugin; + /** + * Queue of islands to delete + */ + private Database handler; + private Set inDeletion; + + public IslandDeletionManager(BentoBox plugin) { + this.plugin = plugin; + handler = new Database<>(plugin, IslandDeletion.class); + inDeletion = new HashSet<>(); + } + + /** + * When BentoBox is fully loaded, load the islands that still need to be deleted and kick them off + * @param e + */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBentoBoxReady(BentoBoxReadyEvent e) { + // Load list of islands that were mid deletion and delete them + List toBeDeleted = handler.loadObjects(); + if (toBeDeleted != null && toBeDeleted.size() > 0) { + plugin.log("There are " + toBeDeleted.size() + " islands pending deletion."); + toBeDeleted.forEach(di -> { + plugin.log("Resuming deletion of island at " + di.getLocation().getWorld().getName() + " " + Util.xyz(di.getLocation().toVector())); + inDeletion.add(di.getLocation()); + new DeleteIslandChunks(plugin, di); + }); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onIslandDelete(IslandDeleteChunksEvent e) { + // Store location + inDeletion.add(e.getDeletedIslandInfo().getLocation()); + // Save to database + handler.saveObject(e.getDeletedIslandInfo()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onIslandDeleted(IslandDeletedEvent e) { + // Delete + inDeletion.remove(e.getDeletedIslandInfo().getLocation()); + // Delete from database + handler.deleteID(e.getDeletedIslandInfo().getUniqueId()); + } + + /** + * Check if an island location is in deletion + * @param location - center of location + * @return true if island is in the process of being deleted + */ + public boolean inDeletion(Location location) { + return inDeletion.contains(location); + } +} diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java index ec7f5cd90..a5153d206 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java @@ -24,9 +24,13 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.util.Vector; import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.events.IslandBaseEvent; +import world.bentobox.bentobox.api.events.island.IslandEvent; +import world.bentobox.bentobox.api.events.island.IslandEvent.Reason; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.objects.IslandDeletion; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.lists.Flags; import world.bentobox.bentobox.managers.island.IslandCache; @@ -60,9 +64,6 @@ public class IslandsManager { // Island Cache private IslandCache islandCache; - // Async database saving semaphore - private boolean midSave; - /** * Islands Manager * @param plugin - plugin @@ -244,6 +245,11 @@ public class IslandsManager { if (island == null) { return; } + // Fire event + IslandBaseEvent event = IslandEvent.builder().island(island).reason(Reason.DELETE).build(); + if (event.isCancelled()) { + return; + } // Set the owner of the island to no one. island.setOwner(null); island.setFlag(Flags.LOCK, RanksManager.VISITOR_RANK); @@ -255,7 +261,7 @@ public class IslandsManager { // Remove players from island removePlayersFromIsland(island); // Remove blocks from world - new DeleteIslandChunks(plugin, island); + new DeleteIslandChunks(plugin, new IslandDeletion(island)); } } @@ -685,8 +691,7 @@ public class IslandsManager { * @param user - user */ public void removePlayer(World world, User user) { - islandCache.removePlayer(world, user.getUniqueId()); - save(true); + removePlayer(world, user.getUniqueId()); } /** @@ -695,8 +700,10 @@ public class IslandsManager { * @param uuid - user's uuid */ public void removePlayer(World world, UUID uuid) { - islandCache.removePlayer(world, uuid); - save(true); + Island island = islandCache.removePlayer(world, uuid); + if (island != null) { + handler.saveObject(island); + } } /** @@ -727,36 +734,18 @@ public class IslandsManager { } /** - * Save the islands to the database - * @param async - if true, saving will be done async + * Save the all the islands to the database */ - public void save(boolean async){ - if (midSave) { - // If it's already saving, then do nothing - return; - } + public void saveAll(){ Collection collection = islandCache.getIslands(); - if(async) { - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - midSave = true; - for(Island island : collection){ - try { - handler.saveObject(island); - } catch (Exception e) { - plugin.logError("Could not save island to database when running async! " + e.getMessage()); - } - } - midSave = false; - }); - } else { - for(Island island : collection){ - try { - handler.saveObject(island); - } catch (Exception e) { - plugin.logError("Could not save island to database when running sync! " + e.getMessage()); - } + for(Island island : collection){ + try { + handler.saveObject(island); + } catch (Exception e) { + plugin.logError("Could not save island to database when running sync! " + e.getMessage()); } } + } /** @@ -768,8 +757,8 @@ public class IslandsManager { // Add player to new island teamIsland.addMember(playerUUID); islandCache.addPlayer(playerUUID, teamIsland); - // Save the database - save(false); + // Save the island + handler.saveObject(teamIsland); } public void setLast(Location last) { @@ -789,7 +778,7 @@ public class IslandsManager { public void shutdown(){ // Remove all coop associations islandCache.getIslands().stream().forEach(i -> i.getMembers().values().removeIf(p -> p == RanksManager.COOP_RANK)); - save(false); + saveAll(); islandCache.clear(); handler.close(); } @@ -867,4 +856,13 @@ public class IslandsManager { islandCache.getIslands().stream().forEach(i -> i.getMembers().entrySet().removeIf(e -> e.getKey().equals(uniqueId) && e.getValue() == rank)); } + /** + * Save the island to the database + * @param island - island + */ + public void save(Island island) { + handler.saveObject(island); + + } + } diff --git a/src/main/java/world/bentobox/bentobox/managers/PlayersManager.java b/src/main/java/world/bentobox/bentobox/managers/PlayersManager.java index 2a731a127..ddacfc833 100644 --- a/src/main/java/world/bentobox/bentobox/managers/PlayersManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/PlayersManager.java @@ -1,6 +1,5 @@ package world.bentobox.bentobox.managers; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -8,7 +7,6 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; @@ -63,20 +61,13 @@ public class PlayersManager { /** * Save all players - * @param async - if true, save async */ - public void save(boolean async){ - Collection set = Collections.unmodifiableCollection(playerCache.values()); - if(async) { - Runnable save = () -> set.forEach(handler::saveObject); - Bukkit.getScheduler().runTaskAsynchronously(plugin, save); - } else { - set.forEach(handler::saveObject); - } + public void saveAll(){ + Collections.unmodifiableCollection(playerCache.values()).forEach(handler::saveObject); } public void shutdown(){ - save(false); + saveAll(); playerCache.clear(); handler.close(); } diff --git a/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java b/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java index aa697d170..046e4dd63 100644 --- a/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java +++ b/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java @@ -169,8 +169,9 @@ public class IslandCache { * The island is removed from the islandsByUUID map, but kept in the location map. * @param world - world * @param uuid - player's UUID + * @return island player had or null if none */ - public void removePlayer(World world, UUID uuid) { + public Island removePlayer(World world, UUID uuid) { world = Util.getWorld(world); islandsByUUID.putIfAbsent(world, new HashMap<>()); Island island = islandsByUUID.get(world).get(uuid); @@ -185,6 +186,7 @@ public class IslandCache { } } islandsByUUID.get(world).remove(uuid); + return island; } /** diff --git a/src/main/java/world/bentobox/bentobox/managers/island/NewIsland.java b/src/main/java/world/bentobox/bentobox/managers/island/NewIsland.java index 9b4281fd3..e26702ab9 100644 --- a/src/main/java/world/bentobox/bentobox/managers/island/NewIsland.java +++ b/src/main/java/world/bentobox/bentobox/managers/island/NewIsland.java @@ -244,7 +244,7 @@ public class NewIsland { */ private Result isIsland(Location location){ location = Util.getClosestIsland(location); - if (plugin.getIslands().getIslandAt(location).isPresent()) { + if (plugin.getIslands().getIslandAt(location).isPresent() || plugin.getIslandDeletionManager().inDeletion(location)) { return Result.ISLAND_FOUND; } diff --git a/src/main/java/world/bentobox/bentobox/schems/Clipboard.java b/src/main/java/world/bentobox/bentobox/schems/Clipboard.java index 961103f0a..f5928200a 100644 --- a/src/main/java/world/bentobox/bentobox/schems/Clipboard.java +++ b/src/main/java/world/bentobox/bentobox/schems/Clipboard.java @@ -353,8 +353,8 @@ public class Clipboard { /** * Sets any entity that is in this location - * @param location - locaton - * @param config - config section + * @param location - location + * @param en - config section */ private void setEntity(Location location, ConfigurationSection en) { en.getKeys(false).forEach(k -> { diff --git a/src/main/java/world/bentobox/bentobox/util/DeleteIslandChunks.java b/src/main/java/world/bentobox/bentobox/util/DeleteIslandChunks.java index 8fac63fdc..41f3d5612 100644 --- a/src/main/java/world/bentobox/bentobox/util/DeleteIslandChunks.java +++ b/src/main/java/world/bentobox/bentobox/util/DeleteIslandChunks.java @@ -1,12 +1,12 @@ package world.bentobox.bentobox.util; -import org.bukkit.World; +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.events.IslandBaseEvent; import world.bentobox.bentobox.api.events.island.IslandEvent; import world.bentobox.bentobox.api.events.island.IslandEvent.Reason; -import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.database.objects.IslandDeletion; /** * Deletes islands fast using chunk regeneration @@ -17,40 +17,41 @@ import world.bentobox.bentobox.database.objects.Island; public class DeleteIslandChunks { /** - * Deletes the island - * @param plugin - plugin object - * @param island - island to delete + * This is how many chunks per world will be done in one tick. */ + private final static int SPEED = 5; + private int x; + private int z; + private BukkitTask task; + @SuppressWarnings("deprecation") - public DeleteIslandChunks(final BentoBox plugin, final Island island) { + public DeleteIslandChunks(BentoBox plugin, IslandDeletion di) { // Fire event - IslandBaseEvent event = IslandEvent.builder().island(island).reason(Reason.DELETE).build(); - if (event.isCancelled()) { - return; - } - final World world = island.getCenter().getWorld(); - if (world == null) { - return; - } - int minXChunk = island.getMinX() >> 4; - int maxXChunk = (island.getRange() * 2 + island.getMinX() - 1) >> 4; - int minZChunk = island.getMinZ() >> 4; - int maxZChunk = (island.getRange() * 2 + island.getMinZ() - 1) >> 4; - for (int x = minXChunk; x <= maxXChunk; x++) { - for (int z = minZChunk; z<=maxZChunk; z++) { - world.regenerateChunk(x, z); - //System.out.println("regenerating = " + x + "," + z); - if (plugin.getIWM().isNetherGenerate(world) && plugin.getIWM().isNetherIslands(world)) { - plugin.getIWM().getNetherWorld(world).regenerateChunk(x, z); + IslandEvent.builder().deletedIslandInfo(di).reason(Reason.DELETE_CHUNKS).build(); + x = di.getMinXChunk(); + z = di.getMinZChunk(); + task = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + for (int i = 0; i < SPEED; i++) { + di.getWorld().regenerateChunk(x, z); + if (plugin.getIWM().isNetherGenerate(di.getWorld()) && plugin.getIWM().isNetherIslands(di.getWorld())) { + plugin.getIWM().getNetherWorld(di.getWorld()).regenerateChunk(x, z); } - if (plugin.getIWM().isEndGenerate(world) && plugin.getIWM().isEndIslands(world)) { - plugin.getIWM().getEndWorld(world).regenerateChunk(x, z); + if (plugin.getIWM().isEndGenerate(di.getWorld()) && plugin.getIWM().isEndIslands(di.getWorld())) { + plugin.getIWM().getEndWorld(di.getWorld()).regenerateChunk(x, z); + } + z++; + if (z > di.getMaxZChunk()) { + z = di.getMinZChunk(); + x++; + if (x > di.getMaxXChunk()) { + task.cancel(); + // Fire event + IslandEvent.builder().deletedIslandInfo(di).reason(Reason.DELETED).build(); + } } } - } - // Fire event - IslandEvent.builder().island(island).reason(Reason.DELETED).build(); + }, 0L, 1L); } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 35bf52dbd..1823bb7a5 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -109,6 +109,7 @@ commands: registered-island: "&aRegistered player to island at [xyz]." already-owned: "&cIsland is already owned by another player!" no-island-here: "&cThere is no island here. Confirm to make one." + in-deletion: "&cThis island space is currently being deleted. Try later." unregister: parameters: "" description: "unregister owner from island, but keep island blocks" @@ -128,6 +129,7 @@ commands: island-location: "Island location: [xyz]" island-coords: "Island coordinates: [xz1] to [xz2]" protection-range: "Protection range: [range]" + max-protection-range: "Largest historical protection range: [range]" protection-coords: "Protection coordinates: [xz1] to [xz2]" is-spawn: "Island is a spawn island" banned-players: "Banned players:" diff --git a/src/test/java/world/bentobox/bentobox/api/commands/admin/AdminRegisterCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/admin/AdminRegisterCommandTest.java index 95e8e5cc8..794e08660 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/admin/AdminRegisterCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/admin/AdminRegisterCommandTest.java @@ -30,6 +30,7 @@ import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.managers.CommandsManager; +import world.bentobox.bentobox.managers.IslandDeletionManager; import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.LocalesManager; @@ -50,6 +51,7 @@ public class AdminRegisterCommandTest { private IslandsManager im; private PlayersManager pm; private UUID notUUID; + private IslandDeletionManager idm; /** * @throws java.lang.Exception @@ -111,11 +113,16 @@ public class AdminRegisterCommandTest { LocalesManager lm = mock(LocalesManager.class); when(lm.get(Mockito.any(), Mockito.any())).thenReturn("mock translation"); when(plugin.getLocalesManager()).thenReturn(lm); + + // Deletion Manager + idm = mock(IslandDeletionManager.class); + when(idm.inDeletion(Mockito.any())).thenReturn(false); + when(plugin.getIslandDeletionManager()).thenReturn(idm); } /** - * Test method for . + * Test method for {@link AdminRegisterCommand#execute(org.bukkit.command.CommandSender, String, String[])}. */ @Test public void testExecuteNoTarget() { @@ -125,7 +132,7 @@ public class AdminRegisterCommandTest { } /** - * Test method for . + * Test method for {@link AdminRegisterCommand#execute(org.bukkit.command.CommandSender, String, String[])}. */ @Test public void testExecuteUnknownPlayer() { @@ -137,7 +144,7 @@ public class AdminRegisterCommandTest { } /** - * Test method for . + * Test method for {@link AdminRegisterCommand#execute(org.bukkit.command.CommandSender, String, String[])}. */ @Test public void testExecutePlayerHasIsland() { @@ -151,7 +158,7 @@ public class AdminRegisterCommandTest { } /** - * Test method for . + * Test method for {@link AdminRegisterCommand#execute(org.bukkit.command.CommandSender, String, String[])}. */ @Test public void testExecuteInTeam() { @@ -165,7 +172,7 @@ public class AdminRegisterCommandTest { } /** - * Test method for . + * Test method for {@link AdminRegisterCommand#execute(org.bukkit.command.CommandSender, String, String[])}. */ @Test public void testExecuteAlreadyOwnedIsland() { @@ -186,6 +193,32 @@ public class AdminRegisterCommandTest { Mockito.verify(user).sendMessage("commands.admin.register.already-owned"); } + /** + * Test method for {@link AdminRegisterCommand#execute(org.bukkit.command.CommandSender, String, String[])}. + */ + @Test + public void testExecuteInDeletionIsland() { + when(idm.inDeletion(Mockito.any())).thenReturn(true); + when(im.inTeam(Mockito.any(), Mockito.any())).thenReturn(false); + when(im.hasIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(false); + String[] name = {"tastybento"}; + when(pm.getUUID(Mockito.any())).thenReturn(notUUID); + Location loc = mock(Location.class); + + // Island has owner + Island is = mock(Island.class); + when(is.getOwner()).thenReturn(uuid); + Optional opi = Optional.of(is); + when(im.getIslandAt(Mockito.any())).thenReturn(opi); + when(user.getLocation()).thenReturn(loc); + AdminRegisterCommand itl = new AdminRegisterCommand(ac); + assertFalse(itl.execute(user, itl.getLabel(), Arrays.asList(name))); + Mockito.verify(user).sendMessage("commands.admin.register.in-deletion"); + } + + /** + * Test method for {@link AdminRegisterCommand#execute(org.bukkit.command.CommandSender, String, String[])}. + */ @Test public void testExecuteSuccess() { when(im.inTeam(Mockito.any(), Mockito.any())).thenReturn(false); diff --git a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java index e04e2575b..fc0b020c3 100644 --- a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java @@ -23,6 +23,7 @@ import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.Server; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -37,6 +38,7 @@ import org.bukkit.entity.Player; import org.bukkit.entity.Slime; import org.bukkit.entity.Wither; import org.bukkit.entity.Zombie; +import org.bukkit.plugin.PluginManager; import org.bukkit.scheduler.BukkitScheduler; import org.junit.Before; import org.junit.Test; @@ -54,6 +56,7 @@ import com.google.common.collect.ImmutableSet.Builder; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.bentobox.api.events.island.IslandEvent.IslandDeleteEvent; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.lists.Flags; @@ -79,6 +82,7 @@ public class IslandsManagerTest { private IslandCache islandCache; private Optional optionalIsland; private Island is; + private PluginManager pim; /** * @throws java.lang.Exception @@ -122,8 +126,7 @@ public class IslandsManagerTest { pm = mock(PlayersManager.class); when(plugin.getPlayers()).thenReturn(pm); - // Server & Scheduler - + // Scheduler BukkitScheduler sch = mock(BukkitScheduler.class); PowerMockito.mockStatic(Bukkit.class); when(Bukkit.getScheduler()).thenReturn(sch); @@ -174,6 +177,11 @@ public class IslandsManagerTest { // User location when(user.getLocation()).thenReturn(location); + // Server for events + Server server = mock(Server.class); + when(Bukkit.getServer()).thenReturn(server); + pim = mock(PluginManager.class); + when(server.getPluginManager()).thenReturn(pim); } @@ -432,16 +440,37 @@ public class IslandsManagerTest { * Test method for {@link world.bentobox.bentobox.managers.IslandsManager#deleteIsland(world.bentobox.bentobox.database.objects.Island, boolean)}. */ @Test - public void testDeleteIslandIslandBoolean() { + public void testDeleteIslandIslandBooleanNull() { IslandsManager im = new IslandsManager(plugin); - im.deleteIsland((Island)null, true); + Mockito.verify(pim, Mockito.never()).callEvent(Mockito.any()); + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.IslandsManager#deleteIsland(world.bentobox.bentobox.database.objects.Island, boolean)}. + */ + @Test + public void testDeleteIslandIslandBooleanNoBlockRemoval() { + IslandsManager im = new IslandsManager(plugin); UUID owner = UUID.randomUUID(); Island island = im.createIsland(location, owner); im.deleteIsland(island, false); - island = im.createIsland(location, owner); + assertNull(island.getOwner()); + Mockito.verify(pim, Mockito.times(2)).callEvent(Mockito.any(IslandDeleteEvent.class)); + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.IslandsManager#deleteIsland(world.bentobox.bentobox.database.objects.Island, boolean)}. + */ + @Test + public void testDeleteIslandIslandBooleanRemoveBlocks() { + Mockito.verify(pim, Mockito.never()).callEvent(Mockito.any()); + IslandsManager im = new IslandsManager(plugin); + UUID owner = UUID.randomUUID(); + Island island = im.createIsland(location, owner); im.deleteIsland(island, true); - assertNull(island); + assertNull(island.getOwner()); + Mockito.verify(pim, Mockito.times(4)).callEvent(Mockito.any(IslandDeleteEvent.class)); } /** diff --git a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java index b42d47e79..e73adcd43 100644 --- a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java @@ -175,13 +175,12 @@ public class PlayersManagerTest { } /** - * Test method for {@link world.bentobox.bentobox.managers.PlayersManager#save(boolean)}. + * Test method for {@link world.bentobox.bentobox.managers.PlayersManager#saveAll()}. */ @Test public void testSaveBoolean() { PlayersManager pm = new PlayersManager(plugin); - pm.save(false); - pm.save(true); + pm.saveAll(); } /** diff --git a/src/test/java/world/bentobox/bentobox/util/DeleteIslandChunksTest.java b/src/test/java/world/bentobox/bentobox/util/DeleteIslandChunksTest.java deleted file mode 100644 index c914f493f..000000000 --- a/src/test/java/world/bentobox/bentobox/util/DeleteIslandChunksTest.java +++ /dev/null @@ -1,152 +0,0 @@ -/** - * - */ -package world.bentobox.bentobox.util; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Server; -import org.bukkit.World; -import org.bukkit.plugin.PluginManager; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandWorldManager; - -/** - * Tests the island delete class - * @author tastybento - * - */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({Bukkit.class}) -public class DeleteIslandChunksTest { - - private BentoBox plugin; - private Island island; - private Location location; - private World world; - - /** - * @throws java.lang.Exception - */ - @Before - public void setUp() throws Exception { - PowerMockito.mockStatic(Bukkit.class); - Server server = mock(Server.class); - PluginManager pim = mock(PluginManager.class); - when(server.getPluginManager()).thenReturn(pim); - when(Bukkit.getServer()).thenReturn(server); - plugin = mock(BentoBox.class); - IslandWorldManager iwm = mock(IslandWorldManager.class); - // No Nether or End by default - when(iwm.isNetherGenerate(Mockito.any())).thenReturn(false); - when(iwm.isNetherIslands(Mockito.any())).thenReturn(false); - when(iwm.isEndGenerate(Mockito.any())).thenReturn(false); - when(iwm.isEndIslands(Mockito.any())).thenReturn(false); - - when(plugin.getIWM()).thenReturn(iwm); - // World - //world = mock(World.class, Mockito.withSettings().verboseLogging()); - world = mock(World.class); - - island = new Island(); - island.setRange(64); - - location = mock(Location.class); - - when(location.getWorld()).thenReturn(world); - - } - - /** - * Test method for {@link world.bentobox.bentobox.util.DeleteIslandChunks#DeleteIslandChunks(world.bentobox.bentobox.BentoBox, world.bentobox.bentobox.database.objects.Island)}. - */ - @SuppressWarnings("deprecation") - @Test - public void testDeleteIslandChunksNegativeX() { - - // Island adjacent to an island at 0,0 - Location location2 = mock(Location.class); - - when(location2.getWorld()).thenReturn(world); - when(location2.getBlockX()).thenReturn(-128); - when(location2.getBlockY()).thenReturn(120); - when(location2.getBlockZ()).thenReturn(0); - island.setCenter(location2); - - new DeleteIslandChunks(plugin, island); - Mockito.verify(world, Mockito.times(64)).regenerateChunk(Mockito.anyInt(), Mockito.anyInt()); - } - - /** - * Test method for {@link world.bentobox.bentobox.util.DeleteIslandChunks#DeleteIslandChunks(world.bentobox.bentobox.BentoBox, world.bentobox.bentobox.database.objects.Island)}. - */ - @SuppressWarnings("deprecation") - @Test - public void testDeleteIslandChunksNegativeXX() { - - // Island adjacent to an island at 0,0 - Location location2 = mock(Location.class); - - when(location2.getWorld()).thenReturn(world); - when(location2.getBlockX()).thenReturn(-256); - when(location2.getBlockY()).thenReturn(120); - when(location2.getBlockZ()).thenReturn(0); - island.setCenter(location2); - - new DeleteIslandChunks(plugin, island); - Mockito.verify(world, Mockito.times(64)).regenerateChunk(Mockito.anyInt(), Mockito.anyInt()); - } - - /** - * Test method for {@link world.bentobox.bentobox.util.DeleteIslandChunks#DeleteIslandChunks(world.bentobox.bentobox.BentoBox, world.bentobox.bentobox.database.objects.Island)}. - */ - @SuppressWarnings("deprecation") - @Test - public void testDeleteIslandChunksIslandPositiveX() { - - // Island adjacent to an island at 0,0 - Location location2 = mock(Location.class); - - when(location2.getWorld()).thenReturn(world); - when(location2.getBlockX()).thenReturn(0); - when(location2.getBlockY()).thenReturn(120); - when(location2.getBlockZ()).thenReturn(0); - island.setCenter(location2); - - new DeleteIslandChunks(plugin, island); - Mockito.verify(world, Mockito.times(64)).regenerateChunk(Mockito.anyInt(), Mockito.anyInt()); - } - - /** - * Test method for {@link world.bentobox.bentobox.util.DeleteIslandChunks#DeleteIslandChunks(world.bentobox.bentobox.BentoBox, world.bentobox.bentobox.database.objects.Island)}. - */ - @SuppressWarnings("deprecation") - @Test - public void testDeleteIslandChunksPositiveXX() { - - // Island adjacent to an island at 0,0 - Location location2 = mock(Location.class); - - when(location2.getWorld()).thenReturn(world); - when(location2.getBlockX()).thenReturn(256); - when(location2.getBlockY()).thenReturn(120); - when(location2.getBlockZ()).thenReturn(0); - island.setCenter(location2); - - new DeleteIslandChunks(plugin, island); - Mockito.verify(world, Mockito.times(64)).regenerateChunk(Mockito.anyInt(), Mockito.anyInt()); - } - -}