diff --git a/pom.xml b/pom.xml index 6154cbe6..7746d1ad 100644 --- a/pom.xml +++ b/pom.xml @@ -58,8 +58,8 @@ 2.0.9 - 1.20.1-R0.1-SNAPSHOT - 1.24.0-SNAPSHOT + 1.20.4-R0.1-SNAPSHOT + 2.0.0-SNAPSHOT 2.6.2 1.3.0 @@ -67,7 +67,7 @@ -LOCAL - 1.14.2 + 1.15.0 BentoBoxWorld_AOneBlock bentobox-world @@ -292,26 +292,19 @@ --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED - --add-opens - java.base/java.util.stream=ALL-UNNAMED + --add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED - --add-opens - java.base/java.util.regex=ALL-UNNAMED - --add-opens - java.base/java.nio.channels.spi=ALL-UNNAMED + --add-opens java.base/java.util.regex=ALL-UNNAMED + --add-opens java.base/java.nio.channels.spi=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED - --add-opens - java.base/java.util.concurrent=ALL-UNNAMED + --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/sun.nio.fs=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/java.nio.file=ALL-UNNAMED - --add-opens - java.base/java.nio.charset=ALL-UNNAMED - --add-opens - java.base/java.lang.reflect=ALL-UNNAMED - --add-opens - java.logging/java.util.logging=ALL-UNNAMED + --add-opens java.base/java.nio.charset=ALL-UNNAMED + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + --add-opens java.logging/java.util.logging=ALL-UNNAMED --add-opens java.base/java.lang.ref=ALL-UNNAMED --add-opens java.base/java.util.jar=ALL-UNNAMED --add-opens java.base/java.util.zip=ALL-UNNAMED @@ -373,7 +366,9 @@ **/*Names* - + + org/bukkit/Material* + diff --git a/src/main/java/world/bentobox/aoneblock/AOneBlock.java b/src/main/java/world/bentobox/aoneblock/AOneBlock.java index ea491759..2a63c99e 100644 --- a/src/main/java/world/bentobox/aoneblock/AOneBlock.java +++ b/src/main/java/world/bentobox/aoneblock/AOneBlock.java @@ -24,7 +24,9 @@ import world.bentobox.aoneblock.listeners.ItemsAdderListener; import world.bentobox.aoneblock.listeners.JoinLeaveListener; import world.bentobox.aoneblock.listeners.NoBlockHandler; +import world.bentobox.aoneblock.oneblocks.OneBlockCustomBlockCreator; import world.bentobox.aoneblock.oneblocks.OneBlocksManager; +import world.bentobox.aoneblock.oneblocks.customblock.ItemsAdderCustomBlock; import world.bentobox.aoneblock.requests.IslandStatsHandler; import world.bentobox.aoneblock.requests.LocationStatsHandler; import world.bentobox.bentobox.api.addons.GameModeAddon; @@ -39,281 +41,299 @@ */ public class AOneBlock extends GameModeAddon { - private static final String NETHER = "_nether"; - private static final String THE_END = "_the_end"; - private boolean hasItemsAdder = false; - - // Settings - private Settings settings; - private ChunkGeneratorWorld chunkGenerator; - private final Config configObject = new Config<>(this, Settings.class); - private BlockListener blockListener; - private OneBlocksManager oneBlockManager; - private PlaceholdersManager phManager; - private HoloListener holoListener; - - @Override - public void onLoad() { - // Check if ItemsAdder exists, if yes register listener - if (Bukkit.getPluginManager().getPlugin("ItemsAdder") != null) { - registerListener(new ItemsAdderListener(this)); - hasItemsAdder = true; - } - // Save the default config from config.yml - saveDefaultConfig(); - // Load settings from config.yml. This will check if there are any issues with it too. - if (loadSettings()) { - // Chunk generator - chunkGenerator = settings.isUseOwnGenerator() ? null : new ChunkGeneratorWorld(this); - // Register commands - playerCommand = new PlayerCommand(this); - adminCommand = new AdminCommand(this); - } - } - - private boolean loadSettings() { - // Load settings again to get worlds - settings = configObject.loadConfigObject(); - if (settings == null) { - // Disable - logError("AOneBlock settings could not load! Addon disabled."); - setState(State.DISABLED); - return false; - } else { - // Save the settings - configObject.saveConfigObject(settings); - } - return true; - } - - @Override - public void onEnable() { - oneBlockManager = new OneBlocksManager(this); - if (loadData()) { - // Failed to load - don't register anything - return; - } - blockListener = new BlockListener(this); - registerListener(blockListener); - registerListener(new NoBlockHandler(this)); - registerListener(new BlockProtect(this)); - registerListener(new JoinLeaveListener(this)); - registerListener(new InfoListener(this)); - // Register placeholders - registerPlaceholders(); - - // Register request handlers - registerRequestHandler(new IslandStatsHandler(this)); - registerRequestHandler(new LocationStatsHandler(this)); - - // Register Holograms - holoListener = new HoloListener(this); - registerListener(holoListener); - } - - // Load phase data - public boolean loadData() { - try { - oneBlockManager.loadPhases(); - } catch (IOException e) { - // Disable - logError("AOneBlock settings could not load (oneblock.yml error)! Addon disabled."); - logError(e.getMessage()); - setState(State.DISABLED); - return true; - } - return false; - } - - private void registerPlaceholders() { - phManager = new PlaceholdersManager(this); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_phase", phManager::getPhaseByLocation); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_count", phManager::getCountByLocation); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_phase", phManager::getPhase); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_count", phManager::getCount); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_next_phase", phManager::getNextPhaseByLocation); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_next_phase", phManager::getNextPhase); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_blocks_to_next_phase", phManager::getNextPhaseBlocks); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_blocks_to_next_phase", phManager::getNextPhaseBlocksByLocation); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_percent_done", phManager::getPercentDone); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_percent_done", phManager::getPercentDoneByLocation); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_done_scale", phManager::getDoneScale); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_done_scale", phManager::getDoneScaleByLocation); - // Since 1.10 - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_lifetime_count", phManager::getLifetimeByLocation); - getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_lifetime_count", phManager::getLifetime); - } - - @Override - public void onDisable() { - // save cache - blockListener.saveCache(); - - // Clear holograms - holoListener.clear(); - } - - @Override - public void onReload() { - // save cache - blockListener.saveCache(); - if (loadSettings()) { - log("Reloaded AOneBlock settings"); - } - } - - /** - * @return the settings - */ - public Settings getSettings() { - return settings; - } - - @Override - public void createWorlds() { - String worldName = settings.getWorldName().toLowerCase(); - if (getServer().getWorld(worldName) == null) { - log("Creating AOneBlock world ..."); - } - - // Create the world if it does not exist - islandWorld = getWorld(worldName, World.Environment.NORMAL, chunkGenerator); - // Make the nether if it does not exist - if (settings.isNetherGenerate()) { - if (getServer().getWorld(worldName + NETHER) == null) { - log("Creating AOneBlock's Nether..."); - } - netherWorld = settings.isNetherIslands() ? getWorld(worldName, World.Environment.NETHER, chunkGenerator) : getWorld(worldName, World.Environment.NETHER, null); - } - // Make the end if it does not exist - if (settings.isEndGenerate()) { - if (getServer().getWorld(worldName + THE_END) == null) { - log("Creating AOneBlock's End World..."); - } - endWorld = settings.isEndIslands() ? getWorld(worldName, World.Environment.THE_END, chunkGenerator) : getWorld(worldName, World.Environment.THE_END, null); - } - } - - /** - * Gets a world or generates a new world if it does not exist - * - * @param worldName2 - the overworld name - * @param env - the environment - * @param chunkGenerator2 - the chunk generator. If null then the generator will not be specified - * @return world loaded or generated - */ - private World getWorld(String worldName2, Environment env, ChunkGeneratorWorld chunkGenerator2) { - // Set world name - worldName2 = env.equals(World.Environment.NETHER) ? worldName2 + NETHER : worldName2; - worldName2 = env.equals(World.Environment.THE_END) ? worldName2 + THE_END : worldName2; - WorldCreator wc = WorldCreator.name(worldName2).type(WorldType.FLAT).environment(env); - World w = settings.isUseOwnGenerator() ? wc.createWorld() : wc.generator(chunkGenerator2).createWorld(); - // Set spawn rates - if (w != null) { - setSpawnRates(w); - } - return w; - - } - - private void setSpawnRates(World w) { - if (getSettings().getSpawnLimitMonsters() > 0) { - w.setSpawnLimit(SpawnCategory.MONSTER, getSettings().getSpawnLimitMonsters()); - } - if (getSettings().getSpawnLimitAmbient() > 0) { - w.setSpawnLimit(SpawnCategory.AMBIENT, getSettings().getSpawnLimitAmbient()); - } - if (getSettings().getSpawnLimitAnimals() > 0) { - w.setSpawnLimit(SpawnCategory.ANIMAL, getSettings().getSpawnLimitAnimals()); - } - if (getSettings().getSpawnLimitWaterAnimals() > 0) { - w.setSpawnLimit(SpawnCategory.WATER_ANIMAL, getSettings().getSpawnLimitWaterAnimals()); - } - if (getSettings().getTicksPerAnimalSpawns() > 0) { - w.setTicksPerSpawns(SpawnCategory.ANIMAL, getSettings().getTicksPerAnimalSpawns()); - } - if (getSettings().getTicksPerMonsterSpawns() > 0) { - w.setTicksPerSpawns(SpawnCategory.MONSTER, getSettings().getTicksPerMonsterSpawns()); - } - - } - - @Override - public WorldSettings getWorldSettings() { - return getSettings(); - } - - @Override - public @Nullable ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { - return chunkGenerator; - } - - @Override - public void saveWorldSettings() { - if (settings != null) { - configObject.saveConfigObject(settings); - } - } - - - @Override - public void saveDefaultConfig() - { - super.saveDefaultConfig(); - // Save default phases panel - this.saveResource("panels/phases_panel.yml", false); - } - - - /* (non-Javadoc) - * @see world.bentobox.bentobox.api.addons.Addon#allLoaded() - */ - @Override - public void allLoaded() { - // save settings. This will occur after all addons have loaded - this.saveWorldSettings(); - } - - /** - * @param i - island - * @return one block island data - */ - @NonNull - public OneBlockIslands getOneBlocksIsland(@NonNull Island i) { - return blockListener.getIsland(Objects.requireNonNull(i)); - } - - public OneBlocksManager getOneBlockManager() { - return oneBlockManager; - } - - /** - * @return the blockListener - */ - public BlockListener getBlockListener() { - return blockListener; - } - - /** - * Get the placeholder manager - * - * @return the phManager - */ - public PlaceholdersManager getPlaceholdersManager() { - return phManager; - } - - /** - * @return the holoListener - */ - public HoloListener getHoloListener() { - return holoListener; - } - - /** - * @return true if ItemsAdder is on the server - */ - public boolean hasItemsAdder() { - return hasItemsAdder; - } + private static final String NETHER = "_nether"; + private static final String THE_END = "_the_end"; + private boolean hasItemsAdder = false; + + // Settings + private Settings settings; + private ChunkGeneratorWorld chunkGenerator; + private final Config configObject = new Config<>(this, Settings.class); + private BlockListener blockListener; + private OneBlocksManager oneBlockManager; + private PlaceholdersManager phManager; + private HoloListener holoListener; + + @Override + public void onLoad() { + // Check if ItemsAdder exists, if yes register listener + if (Bukkit.getPluginManager().getPlugin("ItemsAdder") != null) { + registerListener(new ItemsAdderListener(this)); + OneBlockCustomBlockCreator.register(ItemsAdderCustomBlock::fromId); + OneBlockCustomBlockCreator.register("itemsadder", ItemsAdderCustomBlock::fromMap); + hasItemsAdder = true; + } + // Save the default config from config.yml + saveDefaultConfig(); + // Load settings from config.yml. This will check if there are any issues with + // it too. + if (loadSettings()) { + // Chunk generator + chunkGenerator = settings.isUseOwnGenerator() ? null : new ChunkGeneratorWorld(this); + // Register commands + playerCommand = new PlayerCommand(this); + adminCommand = new AdminCommand(this); + } + } + + private boolean loadSettings() { + // Load settings again to get worlds + settings = configObject.loadConfigObject(); + if (settings == null) { + // Disable + logError("AOneBlock settings could not load! Addon disabled."); + setState(State.DISABLED); + return false; + } else { + // Save the settings + configObject.saveConfigObject(settings); + } + return true; + } + + @Override + public void onEnable() { + oneBlockManager = new OneBlocksManager(this); + if (loadData()) { + // Failed to load - don't register anything + return; + } + blockListener = new BlockListener(this); + registerListener(blockListener); + registerListener(new NoBlockHandler(this)); + registerListener(new BlockProtect(this)); + registerListener(new JoinLeaveListener(this)); + registerListener(new InfoListener(this)); + // Register placeholders + registerPlaceholders(); + + // Register request handlers + registerRequestHandler(new IslandStatsHandler(this)); + registerRequestHandler(new LocationStatsHandler(this)); + + // Register Holograms + holoListener = new HoloListener(this); + registerListener(holoListener); + } + + // Load phase data + public boolean loadData() { + try { + oneBlockManager.loadPhases(); + } catch (IOException e) { + // Disable + logError("AOneBlock settings could not load (oneblock.yml error)! Addon disabled."); + logError(e.getMessage()); + setState(State.DISABLED); + return true; + } + return false; + } + + private void registerPlaceholders() { + phManager = new PlaceholdersManager(this); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_phase", + phManager::getPhaseByLocation); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_count", + phManager::getCountByLocation); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_phase", phManager::getPhase); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_count", phManager::getCount); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_next_phase", + phManager::getNextPhaseByLocation); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_next_phase", phManager::getNextPhase); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_blocks_for_phase", + phManager::getPhaseBlocks); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_blocks_to_next_phase", + phManager::getNextPhaseBlocks); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_blocks_to_next_phase", + phManager::getNextPhaseBlocksByLocation); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_percent_done", + phManager::getPercentDone); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_percent_done", + phManager::getPercentDoneByLocation); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_done_scale", phManager::getDoneScale); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_done_scale", + phManager::getDoneScaleByLocation); + // Since 1.10 + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "visited_island_lifetime_count", + phManager::getLifetimeByLocation); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, "my_island_lifetime_count", + phManager::getLifetime); + } + + @Override + public void onDisable() { + // save cache + blockListener.saveCache(); + + // Clear holograms + holoListener.clear(); + } + + @Override + public void onReload() { + // save cache + blockListener.saveCache(); + if (loadSettings()) { + log("Reloaded AOneBlock settings"); + loadData(); + } + } + + /** + * @return the settings + */ + public Settings getSettings() { + return settings; + } + + @Override + public void createWorlds() { + String worldName = settings.getWorldName().toLowerCase(); + if (getServer().getWorld(worldName) == null) { + log("Creating AOneBlock world ..."); + } + + // Create the world if it does not exist + islandWorld = getWorld(worldName, World.Environment.NORMAL, chunkGenerator); + // Make the nether if it does not exist + if (settings.isNetherGenerate()) { + if (getServer().getWorld(worldName + NETHER) == null) { + log("Creating AOneBlock's Nether..."); + } + netherWorld = settings.isNetherIslands() ? getWorld(worldName, World.Environment.NETHER, chunkGenerator) + : getWorld(worldName, World.Environment.NETHER, null); + } + // Make the end if it does not exist + if (settings.isEndGenerate()) { + if (getServer().getWorld(worldName + THE_END) == null) { + log("Creating AOneBlock's End World..."); + } + endWorld = settings.isEndIslands() ? getWorld(worldName, World.Environment.THE_END, chunkGenerator) + : getWorld(worldName, World.Environment.THE_END, null); + } + } + + /** + * Gets a world or generates a new world if it does not exist + * + * @param worldName2 - the overworld name + * @param env - the environment + * @param chunkGenerator2 - the chunk generator. If null then the + * generator will not be specified + * @return world loaded or generated + */ + private World getWorld(String worldName2, Environment env, ChunkGeneratorWorld chunkGenerator2) { + // Set world name + worldName2 = env.equals(World.Environment.NETHER) ? worldName2 + NETHER : worldName2; + worldName2 = env.equals(World.Environment.THE_END) ? worldName2 + THE_END : worldName2; + WorldCreator wc = WorldCreator.name(worldName2).type(WorldType.FLAT).environment(env); + World w = settings.isUseOwnGenerator() ? wc.createWorld() : wc.generator(chunkGenerator2).createWorld(); + // Set spawn rates + if (w != null) { + setSpawnRates(w); + } + return w; + + } + + private void setSpawnRates(World w) { + if (getSettings().getSpawnLimitMonsters() > 0) { + w.setSpawnLimit(SpawnCategory.MONSTER, getSettings().getSpawnLimitMonsters()); + } + if (getSettings().getSpawnLimitAmbient() > 0) { + w.setSpawnLimit(SpawnCategory.AMBIENT, getSettings().getSpawnLimitAmbient()); + } + if (getSettings().getSpawnLimitAnimals() > 0) { + w.setSpawnLimit(SpawnCategory.ANIMAL, getSettings().getSpawnLimitAnimals()); + } + if (getSettings().getSpawnLimitWaterAnimals() > 0) { + w.setSpawnLimit(SpawnCategory.WATER_ANIMAL, getSettings().getSpawnLimitWaterAnimals()); + } + if (getSettings().getTicksPerAnimalSpawns() > 0) { + w.setTicksPerSpawns(SpawnCategory.ANIMAL, getSettings().getTicksPerAnimalSpawns()); + } + if (getSettings().getTicksPerMonsterSpawns() > 0) { + w.setTicksPerSpawns(SpawnCategory.MONSTER, getSettings().getTicksPerMonsterSpawns()); + } + + } + + @Override + public WorldSettings getWorldSettings() { + return getSettings(); + } + + @Override + public @Nullable ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { + return chunkGenerator; + } + + @Override + public void saveWorldSettings() { + if (settings != null) { + configObject.saveConfigObject(settings); + } + } + + @Override + public void saveDefaultConfig() { + super.saveDefaultConfig(); + // Save default phases panel + this.saveResource("panels/phases_panel.yml", false); + } + + /* + * (non-Javadoc) + * + * @see world.bentobox.bentobox.api.addons.Addon#allLoaded() + */ + @Override + public void allLoaded() { + // save settings. This will occur after all addons have loaded + this.saveWorldSettings(); + } + + /** + * @param i - island + * @return one block island data + */ + @NonNull + public OneBlockIslands getOneBlocksIsland(@NonNull Island i) { + return blockListener.getIsland(Objects.requireNonNull(i)); + } + + public OneBlocksManager getOneBlockManager() { + return oneBlockManager; + } + + /** + * @return the blockListener + */ + public BlockListener getBlockListener() { + return blockListener; + } + + /** + * Get the placeholder manager + * + * @return the phManager + */ + public PlaceholdersManager getPlaceholdersManager() { + return phManager; + } + + /** + * @return the holoListener + */ + public HoloListener getHoloListener() { + return holoListener; + } + + /** + * @return true if ItemsAdder is on the server + */ + public boolean hasItemsAdder() { + return hasItemsAdder; + } } diff --git a/src/main/java/world/bentobox/aoneblock/PlaceholdersManager.java b/src/main/java/world/bentobox/aoneblock/PlaceholdersManager.java index 5ea6dfa1..05750d79 100644 --- a/src/main/java/world/bentobox/aoneblock/PlaceholdersManager.java +++ b/src/main/java/world/bentobox/aoneblock/PlaceholdersManager.java @@ -10,215 +10,241 @@ public class PlaceholdersManager { private static final TreeMap SCALE; + private static final String INFINITE = "aoneblock.placeholders.infinite"; static { - SCALE = new TreeMap<>(); - SCALE.put(0D, "&c╍╍╍╍╍╍╍╍"); - SCALE.put(12.5, "&a╍&c╍╍╍╍╍╍╍"); - SCALE.put(25.0, "&a╍╍&c╍╍╍╍╍╍"); - SCALE.put(37.5, "&a╍╍╍&c╍╍╍╍╍"); - SCALE.put(50D, "&a╍╍╍╍&c╍╍╍╍"); - SCALE.put(62.5, "&a╍╍╍╍╍&c╍╍╍"); - SCALE.put(75.0, "&a╍╍╍╍╍╍&c╍╍"); - SCALE.put(87.5, "&a╍╍╍╍╍╍╍&c╍"); - SCALE.put(100D, "&a╍╍╍╍╍╍╍╍"); + SCALE = new TreeMap<>(); + SCALE.put(0D, "&c╍╍╍╍╍╍╍╍"); + SCALE.put(12.5, "&a╍&c╍╍╍╍╍╍╍"); + SCALE.put(25.0, "&a╍╍&c╍╍╍╍╍╍"); + SCALE.put(37.5, "&a╍╍╍&c╍╍╍╍╍"); + SCALE.put(50D, "&a╍╍╍╍&c╍╍╍╍"); + SCALE.put(62.5, "&a╍╍╍╍╍&c╍╍╍"); + SCALE.put(75.0, "&a╍╍╍╍╍╍&c╍╍"); + SCALE.put(87.5, "&a╍╍╍╍╍╍╍&c╍"); + SCALE.put(100D, "&a╍╍╍╍╍╍╍╍"); } private final AOneBlock addon; public PlaceholdersManager(AOneBlock addon) { - this.addon = addon; + this.addon = addon; } - /** * Get phase by location of user + * * @param user - user * @return Phase name */ public String getPhaseByLocation(User user) { - if (user == null || user.getUniqueId() == null) return ""; - return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) - .map(addon::getOneBlocksIsland) - .map(OneBlockIslands::getPhaseName) - .orElse(""); + if (user == null || user.getUniqueId() == null) + return ""; + return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) + .map(addon::getOneBlocksIsland).map(OneBlockIslands::getPhaseName).orElse(""); } /** * Get block count by user location + * * @param user - user * @return String of count */ public String getCountByLocation(User user) { - if (user == null || user.getUniqueId() == null) return ""; - return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) - .map(addon::getOneBlocksIsland) - .map(OneBlockIslands::getBlockNumber) - .map(String::valueOf) - .orElse(""); + if (user == null || user.getUniqueId() == null) + return ""; + return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) + .map(addon::getOneBlocksIsland).map(OneBlockIslands::getBlockNumber).map(String::valueOf).orElse(""); } /** * Get user's island phase + * * @param user - island owner or team member * @return phase name */ public String getPhase(User user) { - if (user == null || user.getUniqueId() == null) return ""; - Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); - return i == null ? "" : addon.getOneBlocksIsland(i).getPhaseName(); + if (user == null || user.getUniqueId() == null) + return ""; + Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); + return i == null ? "" : addon.getOneBlocksIsland(i).getPhaseName(); } /** * Get island block count + * * @param user island owner or team member * @return string of block count */ public String getCount(User user) { - if (user == null || user.getUniqueId() == null) return ""; - Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); - return i == null ? "" : String.valueOf(addon.getOneBlocksIsland(i).getBlockNumber()); + if (user == null || user.getUniqueId() == null) + return ""; + Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); + return i == null ? "" : String.valueOf(addon.getOneBlocksIsland(i).getBlockNumber()); } /** * Get the next phase based on user's location + * * @param user - user * @return next phase */ public String getNextPhaseByLocation(User user) { - if (user == null || user.getUniqueId() == null) return ""; - return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) - .map(addon::getOneBlocksIsland) - .map(addon.getOneBlockManager()::getNextPhase) - .orElse(""); + if (user == null || user.getUniqueId() == null) + return ""; + return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) + .map(addon::getOneBlocksIsland).map(addon.getOneBlockManager()::getNextPhase).orElse(""); } /** * Get next island phase + * * @param user island owner or team member * @return next island phase */ public String getNextPhase(User user) { - if (user == null || user.getUniqueId() == null) return ""; - Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); - return i == null ? "" : addon.getOneBlockManager().getNextPhase(addon.getOneBlocksIsland(i)); + if (user == null || user.getUniqueId() == null) + return ""; + Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); + return i == null ? "" : addon.getOneBlockManager().getNextPhase(addon.getOneBlocksIsland(i)); } /** * Get how many blocks until next phase based on user's location + * * @param user user * @return string number of blocks */ public String getNextPhaseBlocksByLocation(User user) { - if (user == null || user.getUniqueId() == null) return ""; - return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) - .map(addon::getOneBlocksIsland) - .map(addon.getOneBlockManager()::getNextPhaseBlocks) - .map(num -> num < 0 ? user.getTranslation("aoneblock.placeholders.infinite") : String.valueOf(num)) - .orElse(""); + if (user == null || user.getUniqueId() == null) + return ""; + return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) + .map(addon::getOneBlocksIsland).map(addon.getOneBlockManager()::getNextPhaseBlocks) + .map(num -> num < 0 ? user.getTranslation(INFINITE) : String.valueOf(num)).orElse(""); } /** * Get how many blocks until the next island phase + * * @param user owner or team member * @return string number of blocks */ public String getNextPhaseBlocks(User user) { - if (user == null || user.getUniqueId() == null) return ""; - Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); - if (i == null) { - return ""; - } - int num = addon.getOneBlockManager().getNextPhaseBlocks(addon.getOneBlocksIsland(i)); - return num < 0 ? user.getTranslation("aoneblock.placeholders.infinite") : String.valueOf(num); + if (user == null || user.getUniqueId() == null) + return ""; + Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); + if (i == null) { + return ""; + } + int num = addon.getOneBlockManager().getNextPhaseBlocks(addon.getOneBlocksIsland(i)); + return num < 0 ? user.getTranslation(INFINITE) : String.valueOf(num); + } + + /** + * Get how many blocks for this phase + * + * @param user owner or team member + * @return string number of blocks + */ + public String getPhaseBlocks(User user) { + if (user == null || user.getUniqueId() == null) + return ""; + Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); + if (i == null) { + return ""; + } + int num = addon.getOneBlockManager().getPhaseBlocks(addon.getOneBlocksIsland(i)); + return num < 0 ? user.getTranslation(INFINITE) : String.valueOf(num); } /** * Get percentage done of current phase by user's location + * * @param user - user * @return string percentage */ public String getPercentDoneByLocation(User user) { - if (user == null || user.getUniqueId() == null) return ""; - return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) - .map(addon::getOneBlocksIsland) - .map(addon.getOneBlockManager()::getPercentageDone) - .map(num -> Math.round(num) + "%") - .orElse(""); + if (user == null || user.getUniqueId() == null) + return ""; + return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) + .map(addon::getOneBlocksIsland).map(addon.getOneBlockManager()::getPercentageDone) + .map(num -> Math.round(num) + "%").orElse(""); } /** * Get percentage done of user's island phase + * * @param user owner or team member * @return string percentage */ public String getPercentDone(User user) { - if (user == null || user.getUniqueId() == null) return ""; - Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); - if (i == null) { - return ""; - } - double num = addon.getOneBlockManager().getPercentageDone(addon.getOneBlocksIsland(i)); - return Math.round(num) + "%"; + if (user == null || user.getUniqueId() == null) + return ""; + Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); + if (i == null) { + return ""; + } + double num = addon.getOneBlockManager().getPercentageDone(addon.getOneBlocksIsland(i)); + return Math.round(num) + "%"; } /** * Get percentage done of phase as colored scale based on user's location + * * @param user user * @return colored scale */ public String getDoneScaleByLocation(User user) { - if (user == null || user.getUniqueId() == null) return ""; - return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) - .map(addon::getOneBlocksIsland) - .map(addon.getOneBlockManager()::getPercentageDone) - .map(num -> SCALE.floorEntry(num).getValue()) - .map(s -> s.replace("╍", addon.getSettings().getPercentCompleteSymbol())) - .orElse(""); + if (user == null || user.getUniqueId() == null) + return ""; + return addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) + .map(addon::getOneBlocksIsland).map(addon.getOneBlockManager()::getPercentageDone) + .map(num -> SCALE.floorEntry(num).getValue()) + .map(s -> s.replace("╍", addon.getSettings().getPercentCompleteSymbol())).orElse(""); } /** * Get percentage done of phase as colored scale + * * @param user owner or team member * @return colored scale */ public String getDoneScale(User user) { - if (user == null || user.getUniqueId() == null) return ""; - Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); - if (i == null) { - return ""; - } - double num = addon.getOneBlockManager().getPercentageDone(addon.getOneBlocksIsland(i)); - return SCALE.floorEntry(num).getValue().replace("╍", addon.getSettings().getPercentCompleteSymbol()); + if (user == null || user.getUniqueId() == null) + return ""; + Island i = addon.getIslands().getIsland(addon.getOverWorld(), user); + if (i == null) { + return ""; + } + double num = addon.getOneBlockManager().getPercentageDone(addon.getOneBlocksIsland(i)); + return SCALE.floorEntry(num).getValue().replace("╍", addon.getSettings().getPercentCompleteSymbol()); } /** * Get island Lifetime count + * * @param user island owner or team member * @return string of Lifetime count */ - public String getLifetime(User user) - { - if (user == null || user.getUniqueId() == null) return ""; + public String getLifetime(User user) { + if (user == null || user.getUniqueId() == null) + return ""; - Island island = this.addon.getIslands().getIsland(this.addon.getOverWorld(), user); + Island island = this.addon.getIslands().getIsland(this.addon.getOverWorld(), user); - return island == null ? "" : String.valueOf(this.addon.getOneBlocksIsland(island).getLifetime()); + return island == null ? "" : String.valueOf(this.addon.getOneBlocksIsland(island).getLifetime()); } - /** * Get Lifetime count by user location + * * @param user - user * @return String of Lifetime */ - public String getLifetimeByLocation(User user) - { - if (user == null || user.getUniqueId() == null) return ""; - - return this.addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())). - map(this.addon::getOneBlocksIsland). - map(OneBlockIslands::getLifetime). - map(String::valueOf). - orElse(""); + public String getLifetimeByLocation(User user) { + if (user == null || user.getUniqueId() == null) + return ""; + + return this.addon.getIslands().getProtectedIslandAt(Objects.requireNonNull(user.getLocation())) + .map(this.addon::getOneBlocksIsland).map(OneBlockIslands::getLifetime).map(String::valueOf).orElse(""); } } diff --git a/src/main/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommand.java b/src/main/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommand.java index 7d44040b..d455510d 100644 --- a/src/main/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommand.java +++ b/src/main/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommand.java @@ -11,6 +11,7 @@ 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.managers.RanksManager; import world.bentobox.bentobox.util.Util; /** @@ -60,7 +61,8 @@ public boolean execute(User user, String label, List args) { Island island = Objects.requireNonNull(getIslands().getIsland(getWorld(), user)); int rank = island.getRank(user); if (rank < island.getRankCommand(getUsage())) { - user.sendMessage("general.errors.insufficient-rank", TextVariables.RANK, user.getTranslation(getPlugin().getRanksManager().getRank(rank))); + user.sendMessage("general.errors.insufficient-rank", TextVariables.RANK, + user.getTranslation(RanksManager.getInstance().getRank(rank))); return false; } // Get value diff --git a/src/main/java/world/bentobox/aoneblock/dataobjects/OneBlockIslands.java b/src/main/java/world/bentobox/aoneblock/dataobjects/OneBlockIslands.java index 9831ad54..c4d0c0e4 100644 --- a/src/main/java/world/bentobox/aoneblock/dataobjects/OneBlockIslands.java +++ b/src/main/java/world/bentobox/aoneblock/dataobjects/OneBlockIslands.java @@ -10,6 +10,7 @@ import com.google.gson.annotations.Expose; import world.bentobox.aoneblock.oneblocks.OneBlockObject; +import world.bentobox.aoneblock.oneblocks.customblock.MobCustomBlock; import world.bentobox.bentobox.database.objects.DataObject; import world.bentobox.bentobox.database.objects.Table; @@ -43,6 +44,12 @@ public class OneBlockIslands implements DataObject { @Expose private String hologram = ""; + /** + * Timestamp of last phase change + */ + @Expose + private long lastPhaseChangeTime = 0; + private Queue queue = new LinkedList<>(); /** @@ -50,44 +57,44 @@ public class OneBlockIslands implements DataObject { */ @NonNull public String getPhaseName() { - return phaseName == null ? "" : phaseName; + return phaseName == null ? "" : phaseName; } /** * @param phaseName the phaseName to set */ public void setPhaseName(String phaseName) { - this.phaseName = phaseName; + this.phaseName = phaseName; } public OneBlockIslands(String uniqueId) { - this.uniqueId = uniqueId; + this.uniqueId = uniqueId; } /** * @return the blockNumber */ public int getBlockNumber() { - return blockNumber; + return blockNumber; } /** * @param blockNumber the blockNumber to set */ public void setBlockNumber(int blockNumber) { - this.blockNumber = blockNumber; + this.blockNumber = blockNumber; } /** * Increments the block number */ public void incrementBlockNumber() { - // Ensure that lifetime is always at least blockNumber - if (this.lifetime < this.blockNumber) { - this.lifetime = this.blockNumber; - } - this.blockNumber++; - this.lifetime++; + // Ensure that lifetime is always at least blockNumber + if (this.lifetime < this.blockNumber) { + this.lifetime = this.blockNumber; + } + this.blockNumber++; + this.lifetime++; } /** @@ -95,96 +102,129 @@ public void incrementBlockNumber() { */ @NonNull public String getHologram() { - return hologram == null ? "" : hologram; + return hologram == null ? "" : hologram; } /** * @param hologramLine Hologram line */ public void setHologram(String hologramLine) { - this.hologram = hologramLine; + this.hologram = hologramLine; } - /* (non-Javadoc) + /* + * (non-Javadoc) + * * @see world.bentobox.bentobox.database.objects.DataObject#getUniqueId() */ @Override public String getUniqueId() { - return uniqueId; + return uniqueId; } - /* (non-Javadoc) - * @see world.bentobox.bentobox.database.objects.DataObject#setUniqueId(java.lang.String) + /* + * (non-Javadoc) + * + * @see + * world.bentobox.bentobox.database.objects.DataObject#setUniqueId(java.lang. + * String) */ @Override public void setUniqueId(String uniqueId) { - this.uniqueId = uniqueId; + this.uniqueId = uniqueId; } /** * @return the queue */ public Queue getQueue() { - if (queue == null) queue = new LinkedList<>(); - return queue; + if (queue == null) + queue = new LinkedList<>(); + return queue; } /** * Get a list of nearby upcoming mobs + * * @param i - look ahead value * @return list of upcoming mobs */ public List getNearestMob(int i) { - return getQueue().stream().limit(i).filter(OneBlockObject::isEntity).map(OneBlockObject::getEntityType).toList(); + return getQueue().stream().limit(i).filter(obo -> + // Include OneBlockObjects that are Entity, or custom block of type + // MobCustomBlock + obo.isEntity() || (obo.isCustomBlock() && obo.getCustomBlock() instanceof MobCustomBlock)).map(obo -> { + if (obo.isCustomBlock() && obo.getCustomBlock() instanceof MobCustomBlock mb) { + return mb.getMob(); + } + + return obo.getEntityType(); + }).toList(); } /** * Adds a OneBlockObject to the queue + * * @param nextBlock the OneBlockObject to be added */ public void add(OneBlockObject nextBlock) { - getQueue().add(nextBlock); + getQueue().add(nextBlock); } /** - * Retrieves and removes the head of the queue, or returns null if this queue is empty. - * Inserts the specified element into the queue if it is possible to do so immediately without - * violating capacity restrictions, and throwing an + * Retrieves and removes the head of the queue, or returns null if this queue is + * empty. Inserts the specified element into the queue if it is possible to do + * so immediately without violating capacity restrictions, and throwing an * {@code IllegalStateException} if no space is currently available. + * * @param toAdd OneBlockObject - * @return OneBlockObject head of the queue, or returns null if this queue is empty. + * @return OneBlockObject head of the queue, or returns null if this queue is + * empty. */ public OneBlockObject pollAndAdd(OneBlockObject toAdd) { - getQueue(); - OneBlockObject b = queue.poll(); - queue.add(toAdd); - return b; + getQueue(); + OneBlockObject b = queue.poll(); + queue.add(toAdd); + return b; } /** * Clear the look ahead queue */ public void clearQueue() { - getQueue().clear(); + getQueue().clear(); } /** - * @return the lifetime number of blocks broken not including the current block count + * @return the lifetime number of blocks broken not including the current block + * count */ public long getLifetime() { - // Ensure that lifetime is always at least blockNumber - if (this.lifetime < this.blockNumber) { - this.lifetime = this.blockNumber; - } - return lifetime; + // Ensure that lifetime is always at least blockNumber + if (this.lifetime < this.blockNumber) { + this.lifetime = this.blockNumber; + } + return lifetime; } /** * @param lifetime lifetime number of blocks broken to set */ public void setLifetime(long lifetime) { - this.lifetime = lifetime; + this.lifetime = lifetime; } + /** + * @return Timestamp of last phase change + */ + public long getLastPhaseChangeTime() { + return this.lastPhaseChangeTime; + } + /** + * @param lastPhaseChangeTime Timestamp of last phase change + */ + public void setLastPhaseChangeTime(long lastPhaseChangeTime) { + this.lastPhaseChangeTime = lastPhaseChangeTime; + } } diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java index 94553c2c..e0135a28 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java @@ -41,7 +41,6 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import dev.lone.itemsadder.api.CustomBlock; import world.bentobox.aoneblock.AOneBlock; import world.bentobox.aoneblock.dataobjects.OneBlockIslands; import world.bentobox.aoneblock.events.MagicBlockEntityEvent; @@ -104,90 +103,90 @@ public class BlockListener implements Listener { private final Random random = new Random(); - /** * @param addon - OneBlock */ public BlockListener(@NonNull AOneBlock addon) { - this.addon = addon; - handler = new Database<>(addon, OneBlockIslands.class); - cache = new HashMap<>(); - oneBlocksManager = addon.getOneBlockManager(); - check = new CheckPhase(addon, this); - warningSounder = new WarningSounder(addon); + this.addon = addon; + handler = new Database<>(addon, OneBlockIslands.class); + cache = new HashMap<>(); + oneBlocksManager = addon.getOneBlockManager(); + check = new CheckPhase(addon, this); + warningSounder = new WarningSounder(addon); } /** * Save the island cache */ public void saveCache() { - cache.values().forEach(handler::saveObjectAsync); + cache.values().forEach(handler::saveObjectAsync); } - // --------------------------------------------------------------------- // Section: Listeners // --------------------------------------------------------------------- - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIsland(IslandCreatedEvent e) { - if (addon.inWorld(e.getIsland().getWorld())) { - setUp(e.getIsland()); - } + if (addon.inWorld(e.getIsland().getWorld())) { + setUp(e.getIsland()); + } } - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIsland(IslandResettedEvent e) { - if (addon.inWorld(e.getIsland().getWorld())) { - setUp(e.getIsland()); - } + if (addon.inWorld(e.getIsland().getWorld())) { + setUp(e.getIsland()); + } } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onDeletedIsland(IslandDeleteEvent e) { - if (addon.inWorld(e.getIsland().getWorld())) { - cache.remove(e.getIsland().getUniqueId()); - handler.deleteID(e.getIsland().getUniqueId()); - } + if (addon.inWorld(e.getIsland().getWorld())) { + cache.remove(e.getIsland().getUniqueId()); + handler.deleteID(e.getIsland().getUniqueId()); + } } /** * Prevents liquids flowing into magic block + * * @param e BlockFromToEvent */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onBlockFromTo(final BlockFromToEvent e) { - if (!addon.inWorld(e.getBlock().getWorld())) { - return; - } - Location l = e.getToBlock().getLocation(); - // Cannot flow to center block - e.setCancelled(addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).isPresent()); + if (!addon.inWorld(e.getBlock().getWorld())) { + return; + } + Location l = e.getToBlock().getLocation(); + // Cannot flow to center block + e.setCancelled(addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).isPresent()); } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onBlockBreak(final BlockBreakEvent e) { - if (!addon.inWorld(e.getBlock().getWorld())) { - return; - } - Location l = e.getBlock().getLocation(); - addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).ifPresent(i -> process(e, i, e.getPlayer(), e.getPlayer().getWorld())); + if (!addon.inWorld(e.getBlock().getWorld())) { + return; + } + Location l = e.getBlock().getLocation(); + addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) + .ifPresent(i -> process(e, i, e.getPlayer(), e.getPlayer().getWorld())); } /** - * Handles JetsMinions. These are special armor stands. Requires Minions 6.9.3 or later + * Handles JetsMinions. These are special armor stands. Requires Minions 6.9.3 + * or later * * @param e - event */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onBlockBreakByMinion(final EntityInteractEvent e) { - if (!addon.inWorld(e.getBlock().getWorld()) || !e.getEntityType().equals(EntityType.ARMOR_STAND)) { - return; - } - Location l = e.getBlock().getLocation(); - addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).ifPresent(i -> process(e, i, null, e.getBlock().getWorld())); + if (!addon.inWorld(e.getBlock().getWorld()) || !e.getEntityType().equals(EntityType.ARMOR_STAND)) { + return; + } + Location l = e.getBlock().getLocation(); + addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) + .ifPresent(i -> process(e, i, null, e.getBlock().getWorld())); } /** @@ -197,70 +196,63 @@ public void onBlockBreakByMinion(final EntityInteractEvent e) { */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onBlockBreak(final PlayerBucketFillEvent e) { - if (addon.inWorld(e.getBlock().getWorld())) { - Location l = e.getBlock().getLocation(); - addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).ifPresent(i -> process(e, i, e.getPlayer(), e.getPlayer().getWorld())); - } + if (addon.inWorld(e.getBlock().getWorld())) { + Location l = e.getBlock().getLocation(); + addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) + .ifPresent(i -> process(e, i, e.getPlayer(), e.getPlayer().getWorld())); + } } - /** * Drop items at the top of the block. + * * @param event EntitySpawnEvent object. */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onItemStackSpawn(EntitySpawnEvent event) - { - if (!this.addon.getSettings().isDropOnTop()) - { - // Do nothing as item spawning is not interested in this case. - return; - } - - if (!EntityType.DROPPED_ITEM.equals(event.getEntityType())) - { - // We are interested only in dropped item entities. - return; - } - - if (!this.addon.inWorld(event.getLocation().getWorld())) - { - // Not correct world - return; - } - - Entity entity = event.getEntity(); - Location location = event.getLocation(); - - Optional optionalIsland = this.addon.getIslands(). - getIslandAt(location). - filter(island -> location.getBlock().getLocation().equals(island.getCenter())); - - if (optionalIsland.isPresent()) - { - // Teleport entity to the top of magic block. - entity.teleport(optionalIsland.get().getCenter().add(0.5, 1, 0.5)); - entity.setVelocity(new Vector(0, 0, 0)); - } + public void onItemStackSpawn(EntitySpawnEvent event) { + if (!this.addon.getSettings().isDropOnTop()) { + // Do nothing as item spawning is not interested in this case. + return; + } + + if (!EntityType.DROPPED_ITEM.equals(event.getEntityType())) { + // We are interested only in dropped item entities. + return; + } + + if (!this.addon.inWorld(event.getLocation().getWorld())) { + // Not correct world + return; + } + + Entity entity = event.getEntity(); + Location location = event.getLocation(); + + Optional optionalIsland = this.addon.getIslands().getIslandAt(location) + .filter(island -> location.getBlock().getLocation().equals(island.getCenter())); + + if (optionalIsland.isPresent()) { + // Teleport entity to the top of magic block. + entity.teleport(optionalIsland.get().getCenter().add(0.5, 1, 0.5)); + entity.setVelocity(new Vector(0, 0, 0)); + } } - // --------------------------------------------------------------------- // Section: Processing methods // --------------------------------------------------------------------- - private void setUp(@NonNull Island island) { - // Set the bedrock to the initial block - Util.getChunkAtAsync(Objects.requireNonNull(island.getCenter())).thenRun(() -> island.getCenter().getBlock().setType(Material.GRASS_BLOCK)); - // Create a database entry - OneBlockIslands is = new OneBlockIslands(island.getUniqueId()); - cache.put(island.getUniqueId(), is); - handler.saveObjectAsync(is); - addon.getHoloListener().setUp(island, is, true); + // Set the bedrock to the initial block + Util.getChunkAtAsync(Objects.requireNonNull(island.getCenter())) + .thenRun(() -> island.getCenter().getBlock().setType(Material.GRASS_BLOCK)); + // Create a database entry + OneBlockIslands is = new OneBlockIslands(island.getUniqueId()); + cache.put(island.getUniqueId(), is); + handler.saveObjectAsync(is); + addon.getHoloListener().setUp(island, is, true); } - /** * Main block processing method * @@ -270,217 +262,250 @@ private void setUp(@NonNull Island island) { * @param world - world where the block is being broken */ private void process(@NonNull Cancellable e, @NonNull Island i, @Nullable Player player, @NonNull World world) { - // Get island from cache or load it - OneBlockIslands is = getIsland(i); - // Get the phase for this island - OneBlockPhase phase = oneBlocksManager.getPhase(is.getBlockNumber()); - // Store the original phase in case it changes. - String originalPhase = is.getPhaseName(); - // Check for a goto - if (Objects.requireNonNull(phase).getGotoBlock() != null) { - handleGoto(is, phase); - } - // Check for new phase and run commands if required - boolean newPhase = check.checkPhase(player, i, is, Objects.requireNonNull(phase)); - if (!newPhase && is.getBlockNumber() % SAVE_EVERY == 0) { - // Save island data every MAX_LOOK_AHEAD blocks. - saveIsland(i); - } - // Check if requirements met - if (check.phaseRequirementsFail(player, i, phase, world)) { - e.setCancelled(true); - return; - } - if (newPhase) { - is.clearQueue(); - } - // Get the block number in this phase - int blockNumber = is.getBlockNumber() - phase.getBlockNumberValue() + (int) is.getQueue().stream().filter(OneBlockObject::isMaterial).count(); - // Get the block that is being broken - Block block = Objects.requireNonNull(i.getCenter()).toVector().toLocation(world).getBlock(); - // Fill a 5 block queue - if (is.getQueue().isEmpty() || newPhase) { - // Add initial 5 blocks - for (int j = 0; j < MAX_LOOK_AHEAD; j++) { - is.add(phase.getNextBlock(addon, blockNumber++)); - } - } - // Manage Holograms - addon.getHoloListener().process(i, is, phase); - // Play warning sound for upcoming mobs - if (addon.getSettings().getMobWarning() > 0) { - warningSounder.play(is, block); - } - // Get the next block - OneBlockObject nextBlock = (newPhase && phase.getFirstBlock() != null) ? phase.getFirstBlock() : is.pollAndAdd(phase.getNextBlock(addon, blockNumber++)); - // Check if this is a new Phase - if (newPhase) { - // Set the biome for the block and one block above it - setBiome(block, phase.getPhaseBiome()); - // Fire new phase event - Bukkit.getPluginManager().callEvent(new MagicBlockPhaseEvent(i, player == null ? null : player.getUniqueId(), block, phase.getPhaseName(), originalPhase, is.getBlockNumber())); - } - // Entity - if (nextBlock.isEntity()) { - if (!(e instanceof EntitySpawnEvent)) e.setCancelled(true); - // Entity spawns do not increment the block number or break the block - spawnEntity(nextBlock, block); - // Fire event - Bukkit.getPluginManager().callEvent(new MagicBlockEntityEvent(i, player == null ? null : player.getUniqueId(), block, nextBlock.getEntityType())); - return; - } - // Break the block - if (e instanceof BlockBreakEvent) { - this.breakBlock(player, block, nextBlock, i); - } else if (e instanceof PlayerBucketFillEvent) { - Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); - // Fire event - ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); - Bukkit.getPluginManager().callEvent(new MagicBlockEvent(i, player.getUniqueId(), tool, block, nextBlock.getMaterial())); - } else if (e instanceof EntitySpawnEvent) { - Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); - } else if (e instanceof EntityInteractEvent) { - // Minion breaking block - Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); - // Fire event - Bukkit.getPluginManager().callEvent(new MagicBlockEvent(i, null, null, block, nextBlock.getMaterial())); - } - // Increment the block number - is.incrementBlockNumber(); + // Get the block that is being broken + Block block = Objects.requireNonNull(i.getCenter()).toVector().toLocation(world).getBlock(); + + // Get oneblock island + OneBlockIslands is = getIsland(i); + + // Get the phase for current block number + OneBlockPhase phase = oneBlocksManager.getPhase(is.getBlockNumber()); + + // Save previous processing phase name + String prevPhaseName = is.getPhaseName(); + + // Check if phase contains `gotoBlock` + if(Objects.requireNonNull(phase).getGotoBlock() != null){ + phase = handleGoto(is, phase.getGotoBlock()); + } + + // Get current phase name + String currPhaseName = phase.getPhaseName() == null ? "" : phase.getPhaseName(); + + // Get the phase for next block number + OneBlockPhase nextPhase = oneBlocksManager.getPhase(is.getBlockNumber() + 1); + + // Check if nextPhase contains `gotoBlock` and override `nextPhase` + if (Objects.requireNonNull(nextPhase).getGotoBlock() != null) { + nextPhase = oneBlocksManager.getPhase(nextPhase.getGotoBlock()); + } + + // Get next phase name + String nextPhaseName = nextPhase == null || nextPhase.getPhaseName() == null ? "" : nextPhase.getPhaseName(); + + // If next phase is new, log break time of the last block of this phase + if (!currPhaseName.equalsIgnoreCase(nextPhaseName)) { + is.setLastPhaseChangeTime(System.currentTimeMillis()); + } + + boolean isCurrPhaseNew = !is.getPhaseName().equalsIgnoreCase(currPhaseName); + + if (isCurrPhaseNew) { + + // Check if requirements for new phase are met + if (check.phaseRequirementsFail(player, i, is, phase, world)) { + e.setCancelled(true); + return; + } + + check.setNewPhase(player, i, is, phase); + is.clearQueue(); + + // Set the biome for the block and one block above it + setBiome(block, phase.getPhaseBiome()); + + // Fire new phase event + Bukkit.getPluginManager() + .callEvent(new MagicBlockPhaseEvent(i, player == null ? null : player.getUniqueId(), block, + phase.getPhaseName(), prevPhaseName, is.getBlockNumber())); + } + + if (!isCurrPhaseNew && is.getBlockNumber() % SAVE_EVERY == 0) { + // Save island data every MAX_LOOK_AHEAD blocks. + saveIsland(i); + } + + // Get the block number in this phase + int materialBlocksInQueue = (int) is.getQueue().stream().filter(obo -> obo.isMaterial() || obo.isCustomBlock()) + .count(); + int blockNumber = is.getBlockNumber() - (phase.getBlockNumberValue() - 1) + materialBlocksInQueue; + + // Fill a 5 block queue + if (is.getQueue().isEmpty() || isCurrPhaseNew) { + // Add initial 5 blocks + for (int j = 0; j < MAX_LOOK_AHEAD; j++) { + is.add(phase.getNextBlock(addon, blockNumber++)); + } + } + + // Manage Holograms + addon.getHoloListener().process(i, is, phase); + + // Play warning sound for upcoming mobs + if (addon.getSettings().getMobWarning() > 0) { + warningSounder.play(is, block); + } + + // Get the next block + OneBlockObject nextBlock = (isCurrPhaseNew && phase.getFirstBlock() != null) ? phase.getFirstBlock() + : is.pollAndAdd(phase.getNextBlock(addon, blockNumber)); + + // Entity + if (nextBlock.isEntity()) { + if (!(e instanceof EntitySpawnEvent)) { + e.setCancelled(true); + } + // Entity spawns do not increment the block number or break the block + spawnEntity(nextBlock, block); + // Fire event + Bukkit.getPluginManager().callEvent(new MagicBlockEntityEvent(i, + player == null ? null : player.getUniqueId(), block, nextBlock.getEntityType())); + return; + } + + // Break the block + if (e instanceof BlockBreakEvent) { + this.breakBlock(player, block, nextBlock, i); + } else if (e instanceof PlayerBucketFillEvent) { + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); + // Fire event + ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); + Bukkit.getPluginManager() + .callEvent(new MagicBlockEvent(i, player.getUniqueId(), tool, block, nextBlock.getMaterial())); + } else if (e instanceof EntitySpawnEvent) { + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); + } else if (e instanceof EntityInteractEvent) { + // Minion breaking block + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); + // Fire event + Bukkit.getPluginManager().callEvent(new MagicBlockEvent(i, null, null, block, nextBlock.getMaterial())); + } + + // Increment the block number + is.incrementBlockNumber(); } - private void handleGoto(OneBlockIslands is, OneBlockPhase phase) { - int gotoBlock = phase.getGotoBlock(); - phase = oneBlocksManager.getPhase(gotoBlock); - // Store lifetime - is.setLifetime(is.getLifetime() + gotoBlock); - // Set current block - is.setBlockNumber(gotoBlock); + private OneBlockPhase handleGoto(OneBlockIslands is, int gotoBlock) { + // Store lifetime + is.setLifetime(is.getLifetime() + gotoBlock); + // Set current block + is.setBlockNumber(gotoBlock); + return oneBlocksManager.getPhase(gotoBlock); } private void setBiome(@NonNull Block block, @Nullable Biome biome) { - if (biome == null) { - return; - } - for (int x = -4; x <= 4; x++) { - for (int z = -4; z <= 4; z++) { - for (int y = -4; y <= 4; y++) { - block.getWorld().setBiome(block.getX() + x, block.getY() + y, block.getZ() + z, biome); - } - } - } + if (biome == null) { + return; + } + for (int x = -4; x <= 4; x++) { + for (int z = -4; z <= 4; z++) { + for (int y = -4; y <= 4; y++) { + block.getWorld().setBiome(block.getX() + x, block.getY() + y, block.getZ() + z, biome); + } + } + } } - /** - * This method is called when block is removed, and next must be spawned. - * It also teleports player above the magic block, to avoid falling in void. - * @param player Player who breaks the block. - * @param block Block that was broken. + * This method is called when block is removed, and next must be spawned. It + * also teleports player above the magic block, to avoid falling in void. + * + * @param player Player who breaks the block. + * @param block Block that was broken. * @param nextBlock Next Block that will be summoned. - * @param island Island where player is located. + * @param island Island where player is located. */ - private void breakBlock(@Nullable Player player, Block block, @NonNull OneBlockObject nextBlock, @NonNull Island island) - { - ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); - - // Break normally and lift the player up so they don't fall - Bukkit.getScheduler().runTask(addon.getPlugin(), () -> this.spawnBlock(nextBlock, block)); - - if (player.getLocation().getBlock().equals(block)) - { - double delta = 1 - (player.getLocation().getY() - block.getY()); - player.teleport(player.getLocation().add(new Vector(0, delta, 0))); - player.setVelocity(new Vector(0, 0, 0)); - } - else if (player.getLocation().getBlock().equals(block.getRelative(BlockFace.UP))) - { - player.teleport(player.getLocation()); - player.setVelocity(new Vector(0, 0, 0)); - } - - // Fire event - Bukkit.getPluginManager().callEvent(new MagicBlockEvent(island, player.getUniqueId(), tool, block, nextBlock.getMaterial())); + private void breakBlock(@Nullable Player player, Block block, @NonNull OneBlockObject nextBlock, + @NonNull Island island) { + ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); + + // Break normally and lift the player up so they don't fall + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> this.spawnBlock(nextBlock, block)); + + if (player.getLocation().getBlock().equals(block)) { + double delta = 1 - (player.getLocation().getY() - block.getY()); + player.teleport(player.getLocation().add(new Vector(0, delta, 0))); + player.setVelocity(new Vector(0, 0, 0)); + } else if (player.getLocation().getBlock().equals(block.getRelative(BlockFace.UP))) { + player.teleport(player.getLocation()); + player.setVelocity(new Vector(0, 0, 0)); + } + + // Fire event + Bukkit.getPluginManager() + .callEvent(new MagicBlockEvent(island, player.getUniqueId(), tool, block, nextBlock.getMaterial())); } - private void spawnBlock(@NonNull OneBlockObject nextBlock, @NonNull Block block) { - if (nextBlock.isCustomBlock()) { - nextBlock.getCustomBlock().setBlock(block); - } else if (nextBlock.isItemsAdderBlock()) { - //Get Custom Block from ItemsAdder and place it - CustomBlock cBlock = CustomBlock.getInstance(nextBlock.getItemsAdderBlock()); - if (cBlock != null) { - block.getLocation().getBlock().setType(Material.AIR); - cBlock.place(block.getLocation()); - } - } else { - @NonNull - Material type = nextBlock.getMaterial(); - // Place new block with no physics - block.setType(type, false); - // Fill the chest - if (type.equals(Material.CHEST) && nextBlock.getChest() != null) { - fillChest(nextBlock, block); - return; - } else if (Tag.LEAVES.isTagged(type)) { - Leaves leaves = (Leaves) block.getState().getBlockData(); - leaves.setPersistent(true); - block.setBlockData(leaves); - } else if (block.getState() instanceof BrushableBlock bb) { - LootTable lt = switch(bb.getBlock().getBiome()) { - default -> { - if (random.nextDouble() < 0.8) { - yield LootTables.TRAIL_RUINS_ARCHAEOLOGY_COMMON.getLootTable(); - } else { - // 20% rare - yield LootTables.TRAIL_RUINS_ARCHAEOLOGY_RARE.getLootTable(); - } - } - case DESERT -> LootTables.DESERT_PYRAMID_ARCHAEOLOGY.getLootTable(); - case FROZEN_OCEAN -> LootTables.OCEAN_RUIN_COLD_ARCHAEOLOGY.getLootTable(); - case OCEAN -> LootTables.OCEAN_RUIN_COLD_ARCHAEOLOGY.getLootTable(); - case WARM_OCEAN -> LootTables.OCEAN_RUIN_WARM_ARCHAEOLOGY.getLootTable(); - }; - bb.setLootTable(lt); - bb.update(); - } - } + if (nextBlock.isCustomBlock()) { + nextBlock.getCustomBlock().execute(addon, block); + } else { + @NonNull + Material type = nextBlock.getMaterial(); + // Place new block with no physics + block.setType(type, false); + // Fill the chest + if (type.equals(Material.CHEST) && nextBlock.getChest() != null) { + fillChest(nextBlock, block); + return; + } else if (Tag.LEAVES.isTagged(type)) { + Leaves leaves = (Leaves) block.getState().getBlockData(); + leaves.setPersistent(true); + block.setBlockData(leaves); + } else if (block.getState() instanceof BrushableBlock bb) { + LootTable lt = switch (bb.getBlock().getBiome()) { + default -> { + if (random.nextDouble() < 0.8) { + yield LootTables.TRAIL_RUINS_ARCHAEOLOGY_COMMON.getLootTable(); + } else { + // 20% rare + yield LootTables.TRAIL_RUINS_ARCHAEOLOGY_RARE.getLootTable(); + } + } + case DESERT -> LootTables.DESERT_PYRAMID_ARCHAEOLOGY.getLootTable(); + case FROZEN_OCEAN -> LootTables.OCEAN_RUIN_COLD_ARCHAEOLOGY.getLootTable(); + case OCEAN -> LootTables.OCEAN_RUIN_COLD_ARCHAEOLOGY.getLootTable(); + case WARM_OCEAN -> LootTables.OCEAN_RUIN_WARM_ARCHAEOLOGY.getLootTable(); + }; + bb.setLootTable(lt); + bb.update(); + } + } } private void spawnEntity(@NonNull OneBlockObject nextBlock, @NonNull Block block) { - if (block.isEmpty()) block.setType(Material.STONE); - Location spawnLoc = block.getLocation().add(new Vector(0.5D, 1D, 0.5D)); - Entity entity = block.getWorld().spawnEntity(spawnLoc, nextBlock.getEntityType()); - // Make space for entity - this will blot out blocks - if (addon.getSettings().isClearBlocks()) { - new MakeSpace(addon).makeSpace(entity, spawnLoc); - } - block.getWorld().playSound(block.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1F, 2F); + if (block.isEmpty()) + block.setType(Material.STONE); + Location spawnLoc = block.getLocation().add(new Vector(0.5D, 1D, 0.5D)); + Entity entity = block.getWorld().spawnEntity(spawnLoc, nextBlock.getEntityType()); + // Make space for entity - this will blot out blocks + if (addon.getSettings().isClearBlocks()) { + new MakeSpace(addon).makeSpace(entity, spawnLoc); + } + block.getWorld().playSound(block.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1F, 2F); } - - private void fillChest(@NonNull OneBlockObject nextBlock, @NonNull Block block) { - Chest chest = (Chest) block.getState(); - nextBlock.getChest().forEach(chest.getBlockInventory()::setItem); - Color color = Color.fromBGR(0, 255, 255); // yellow - switch (nextBlock.getRarity()) { - case EPIC: - color = Color.fromBGR(255, 0, 255); // magenta - break; - case RARE: - color = Color.fromBGR(255, 255, 255); // cyan - break; - case UNCOMMON: - // Yellow - break; - default: - // No sparkles for regular chests - return; - } - block.getWorld().spawnParticle(Particle.REDSTONE, block.getLocation().add(new Vector(0.5, 1.0, 0.5)), 50, 0.5, 0, 0.5, 1, new Particle.DustOptions(color, 1)); + Chest chest = (Chest) block.getState(); + nextBlock.getChest().forEach(chest.getBlockInventory()::setItem); + Color color = Color.fromBGR(0, 255, 255); // yellow + switch (nextBlock.getRarity()) { + case EPIC: + color = Color.fromBGR(255, 0, 255); // magenta + break; + case RARE: + color = Color.fromBGR(255, 255, 255); // cyan + break; + case UNCOMMON: + // Yellow + break; + default: + // No sparkles for regular chests + return; + } + block.getWorld().spawnParticle(Particle.REDSTONE, block.getLocation().add(new Vector(0.5, 1.0, 0.5)), 50, 0.5, + 0, 0.5, 1, new Particle.DustOptions(color, 1)); } /** @@ -491,47 +516,49 @@ private void fillChest(@NonNull OneBlockObject nextBlock, @NonNull Block block) */ @NonNull public OneBlockIslands getIsland(@NonNull Island i) { - return cache.containsKey(i.getUniqueId()) ? cache.get(i.getUniqueId()) : loadIsland(i.getUniqueId()); + return cache.containsKey(i.getUniqueId()) ? cache.get(i.getUniqueId()) : loadIsland(i.getUniqueId()); } /** * Get all the OneBlockIslands from the Database + * * @return list of oneblock islands */ public List getAllIslands() { - return handler.loadObjects(); + return handler.loadObjects(); } @NonNull private OneBlockIslands loadIsland(@NonNull String uniqueId) { - if (handler.objectExists(uniqueId)) { - OneBlockIslands island = handler.loadObject(uniqueId); - if (island != null) { - // Add to cache - cache.put(island.getUniqueId(), island); - return island; - } - } - return cache.computeIfAbsent(uniqueId, OneBlockIslands::new); + if (handler.objectExists(uniqueId)) { + OneBlockIslands island = handler.loadObject(uniqueId); + if (island != null) { + // Add to cache + cache.put(island.getUniqueId(), island); + return island; + } + } + return cache.computeIfAbsent(uniqueId, OneBlockIslands::new); } /** * @return the oneBlocksManager */ public OneBlocksManager getOneBlocksManager() { - return oneBlocksManager; + return oneBlocksManager; } /** * Saves the island progress to the database async * * @param island - island - * @return CompletableFuture - true if saved or not in cache, false if save failed + * @return CompletableFuture - true if saved or not in cache, false if save + * failed */ public CompletableFuture saveIsland(@NonNull Island island) { - if (cache.containsKey(island.getUniqueId())) { - return handler.saveObjectAsync(cache.get(island.getUniqueId())); - } - return CompletableFuture.completedFuture(true); + if (cache.containsKey(island.getUniqueId())) { + return handler.saveObjectAsync(cache.get(island.getUniqueId())); + } + return CompletableFuture.completedFuture(true); } } diff --git a/src/main/java/world/bentobox/aoneblock/listeners/CheckPhase.java b/src/main/java/world/bentobox/aoneblock/listeners/CheckPhase.java index 7cb0a734..6423a345 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/CheckPhase.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/CheckPhase.java @@ -1,6 +1,7 @@ package world.bentobox.aoneblock.listeners; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.bukkit.World; @@ -22,6 +23,7 @@ /** * Performs end of phase checking + * * @author tastybento * */ @@ -31,121 +33,153 @@ public class CheckPhase { private final OneBlocksManager oneBlocksManager; private final BlockListener blockListener; - /** - * @param addon AOneBlock + * @param addon AOneBlock * @param blockListener */ public CheckPhase(AOneBlock addon, BlockListener blockListener) { - this.addon = addon; - this.oneBlocksManager = addon.getOneBlockManager(); - this.blockListener = blockListener; + this.addon = addon; + this.oneBlocksManager = addon.getOneBlockManager(); + this.blockListener = blockListener; } /** - * Checks whether the player can proceed to the next phase + * Runs end phase commands, sets new phase and runs new phase commands * * @param player - player * @param i - island - * @param phase - one block phase - * @param world - world - * @return true if the player cannot proceed to the next phase. + * @param is - OneBlockIslands object + * @param phase - current phase */ - protected boolean phaseRequirementsFail(@Nullable Player player, @NonNull Island i, OneBlockPhase phase, @NonNull World world) { - if (phase.getRequirements().isEmpty()) { - return false; - } - // Check requirements - boolean blocked = false; - for (Requirement r : phase.getRequirements()) { - boolean b = switch (r.getType()) { - case LEVEL -> addon.getAddonByName("Level").map(l -> { - if (((Level) l).getIslandLevel(world, i.getOwner()) < r.getLevel()) { - User.getInstance(player).sendMessage("aoneblock.phase.insufficient-level", TextVariables.NUMBER, String.valueOf(r.getLevel())); - return true; - } - return false; - }).orElse(false); - case BANK -> addon.getAddonByName("Bank").map(l -> { - if (((Bank) l).getBankManager().getBalance(i).getValue() < r.getBank()) { - User.getInstance(player).sendMessage("aoneblock.phase.insufficient-bank-balance", TextVariables.NUMBER, String.valueOf(r.getBank())); - return true; - } - return false; - }).orElse(false); - case ECO -> addon.getPlugin().getVault().map(l -> { - if (l.getBalance(User.getInstance(player), world) < r.getEco()) { - User.getInstance(player).sendMessage("aoneblock.phase.insufficient-funds", TextVariables.NUMBER, String.valueOf(r.getEco())); - return true; - } - return false; - }).orElse(false); - case PERMISSION -> { - if (player != null && !player.hasPermission(r.getPermission())) { - User.getInstance(player).sendMessage("aoneblock.phase.insufficient-permission", TextVariables.NAME, String.valueOf(r.getPermission())); - yield true; - } - yield false; - } - }; - if (b) blocked = true; - } - return blocked; + void setNewPhase(@Nullable Player player, @NonNull Island i, @NonNull OneBlockIslands is, + @NonNull OneBlockPhase phase) { + // Handle NPCs + User user; + if (player == null || player.hasMetadata("NPC")) { + // Default to the owner + user = addon.getPlayers().getUser(i.getOwner()); + } else { + user = User.getInstance(player); + } + + String newPhaseName = Objects.requireNonNullElse(phase.getPhaseName(), ""); + + // Run previous phase end commands + oneBlocksManager.getPhase(is.getPhaseName()).ifPresent(oldPhase -> { + String oldPhaseName = oldPhase.getPhaseName() == null ? "" : oldPhase.getPhaseName(); + Util.runCommands(user, + replacePlaceholders(player, oldPhaseName, phase.getBlockNumber(), i, oldPhase.getEndCommands()), + "Commands run for end of " + oldPhaseName); + // If first time + if (is.getBlockNumber() >= is.getLifetime()) { + Util.runCommands(user, + replacePlaceholders(player, oldPhaseName, phase.getBlockNumber(), i, + oldPhase.getFirstTimeEndCommands()), + "Commands run for first time completing " + oldPhaseName); + } + }); + // Set the phase name + is.setPhaseName(newPhaseName); + if (user.isPlayer() && user.isOnline() && addon.inWorld(user.getWorld())) { + user.getPlayer().sendTitle(newPhaseName, null, -1, -1, -1); + } + // Run phase start commands + Util.runCommands(user, + replacePlaceholders(player, newPhaseName, phase.getBlockNumber(), i, phase.getStartCommands()), + "Commands run for start of " + newPhaseName); + + blockListener.saveIsland(i); } /** - * Check whether this phase is done or not. + * Checks whether the player can proceed to the next phase * * @param player - player * @param i - island - * @param is - OneBlockIslands object - * @param phase - current phase name - * @return true if this is a new phase, false if not + * @param phase - one block phase + * @param world - world + * @return true if the player cannot proceed to the next phase. */ - protected boolean checkPhase(@Nullable Player player, @NonNull Island i, @NonNull OneBlockIslands is, @NonNull OneBlockPhase phase) { - // Handle NPCs - User user; - if (player == null || player.hasMetadata("NPC")) { - // Default to the owner - user = addon.getPlayers().getUser(i.getOwner()); - } else { - user = User.getInstance(player); - } - - String phaseName = phase.getPhaseName() == null ? "" : phase.getPhaseName(); - if (!is.getPhaseName().equalsIgnoreCase(phaseName)) { - // Run previous phase end commands - oneBlocksManager.getPhase(is.getPhaseName()).ifPresent(oldPhase -> { - String oldPhaseName = oldPhase.getPhaseName() == null ? "" : oldPhase.getPhaseName(); - Util.runCommands(user, - replacePlaceholders(player, oldPhaseName, phase.getBlockNumber(), i, oldPhase.getEndCommands()), - "Commands run for end of " + oldPhaseName); - // If first time - if (is.getBlockNumber() >= is.getLifetime()) { - Util.runCommands(user, - replacePlaceholders(player, oldPhaseName, phase.getBlockNumber(), i, oldPhase.getFirstTimeEndCommands()), - "Commands run for first time completing " + oldPhaseName); - } - }); - // Set the phase name - is.setPhaseName(phaseName); - if (user.isPlayer() && user.isOnline() && addon.inWorld(user.getWorld())) { - user.getPlayer().sendTitle(phaseName, null, -1, -1, -1); - } - // Run phase start commands - Util.runCommands(user, - replacePlaceholders(player, phaseName, phase.getBlockNumber(), i, phase.getStartCommands()), - "Commands run for start of " + phaseName); - - blockListener.saveIsland(i); - return true; - } - return false; + protected boolean phaseRequirementsFail(@Nullable Player player, @NonNull Island i, @NonNull OneBlockIslands is, + OneBlockPhase phase, @NonNull World world) { + + if (phase.getRequirements().isEmpty()) { + return false; + } + + return phase.getRequirements().stream() + .anyMatch(r -> checkRequirement(r, User.getInstance(player), i, is, world)); + } + + private boolean checkRequirement(Requirement r, User user, Island i, OneBlockIslands is, World world) { + return switch (r.getType()) { + case LEVEL -> checkLevelRequirement(r, user, i, world); + case BANK -> checkBankRequirement(r, user, i); + case ECO -> checkEcoRequirement(r, user, world); + case PERMISSION -> checkPermissionRequirement(r, user); + case COOLDOWN -> checkCooldownRequirement(r, user, is); + }; + } + + private boolean checkLevelRequirement(Requirement r, User user, Island i, World world) { + // Level checking logic + return addon.getAddonByName("Level").map(l -> { + if (((Level) l).getIslandLevel(world, i.getOwner()) < r.getLevel()) { + user.sendMessage("aoneblock.phase.insufficient-level", TextVariables.NUMBER, + String.valueOf(r.getLevel())); + return true; + } + return false; + }).orElse(false); + } + + private boolean checkBankRequirement(Requirement r, User user, Island i) { + // Bank checking logic + return addon.getAddonByName("Bank").map(l -> { + if (((Bank) l).getBankManager().getBalance(i).getValue() < r.getBank()) { + user.sendMessage("aoneblock.phase.insufficient-bank-balance", TextVariables.NUMBER, + String.valueOf(r.getBank())); + return true; + } + return false; + }).orElse(false); + } + + private boolean checkEcoRequirement(Requirement r, User user, World world) { + // Eco checking logic + return addon.getPlugin().getVault().map(vaultHook -> { + if (vaultHook.getBalance(user, world) < r.getEco()) { + user.sendMessage("aoneblock.phase.insufficient-funds", TextVariables.NUMBER, + vaultHook.format(r.getEco())); + return true; + } + return false; + }).orElse(false); + } + + private boolean checkPermissionRequirement(Requirement r, User user) { + // Permission checking logic + if (user != null && !user.hasPermission(r.getPermission())) { + user.sendMessage("aoneblock.phase.insufficient-permission", TextVariables.NAME, r.getPermission()); + return true; + } + return false; + } + + private boolean checkCooldownRequirement(Requirement r, User player, OneBlockIslands is) { + // Cooldown checking logic + long remainingTime = r.getCooldown() - (System.currentTimeMillis() - is.getLastPhaseChangeTime()) / 1000; + if (remainingTime > 0) { + player.sendMessage("aoneblock.phase.cooldown", TextVariables.NUMBER, String.valueOf(remainingTime)); + return true; + } + return false; } /** * Replaces placeholders in commands. + * *
      * [island] - Island name
      * [owner] - Island owner's name
@@ -165,23 +199,23 @@ protected boolean checkPhase(@Nullable Player player, @NonNull Island i, @NonNul
      * @return list of commands with placeholders replaced
      */
     @NonNull
-    List replacePlaceholders(@Nullable Player player, @NonNull String phaseName, @NonNull String phaseNumber, @NonNull Island i, List commands) {
-        return commands.stream()
-                .map(c -> {
-                    long level = addon.getAddonByName("Level").map(l -> ((Level) l).getIslandLevel(addon.getOverWorld(), i.getOwner())).orElse(0L);
-                    double balance = addon.getAddonByName("Bank").map(b -> ((Bank) b).getBankManager().getBalance(i).getValue()).orElse(0D);
-                    double ecoBalance = addon.getPlugin().getVault().map(v -> v.getBalance(User.getInstance(player), addon.getOverWorld())).orElse(0D);
-
-                    return c.replace("[island]", i.getName() == null ? "" : i.getName())
-                            .replace("[owner]", addon.getPlayers().getName(i.getOwner()))
-                            .replace("[phase]", phaseName)
-                            .replace("[blocks]", phaseNumber)
-                            .replace("[level]", String.valueOf(level))
-                            .replace("[bank-balance]", String.valueOf(balance))
-                            .replace("[eco-balance]", String.valueOf(ecoBalance));
-
-                })
-                .map(c -> addon.getPlugin().getPlaceholdersManager().replacePlaceholders(player, c))
-                .collect(Collectors.toList());
+    List replacePlaceholders(@Nullable Player player, @NonNull String phaseName, @NonNull String phaseNumber,
+	    @NonNull Island i, List commands) {
+	return commands.stream().map(c -> {
+	    long level = addon.getAddonByName("Level")
+		    .map(l -> ((Level) l).getIslandLevel(addon.getOverWorld(), i.getOwner())).orElse(0L);
+	    double balance = addon.getAddonByName("Bank").map(b -> ((Bank) b).getBankManager().getBalance(i).getValue())
+		    .orElse(0D);
+	    double ecoBalance = addon.getPlugin().getVault()
+		    .map(v -> v.getBalance(User.getInstance(player), addon.getOverWorld())).orElse(0D);
+
+	    return c.replace("[island]", i.getName() == null ? "" : i.getName())
+		    .replace("[owner]", addon.getPlayers().getName(i.getOwner())).replace("[phase]", phaseName)
+		    .replace("[blocks]", phaseNumber).replace("[level]", String.valueOf(level))
+		    .replace("[bank-balance]", String.valueOf(balance))
+		    .replace("[eco-balance]", String.valueOf(ecoBalance));
+
+	}).map(c -> addon.getPlugin().getPlaceholdersManager().replacePlaceholders(player, c))
+		.collect(Collectors.toList());
     }
 }
diff --git a/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java b/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java
index 8a3efb22..21f3dc78 100644
--- a/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java
+++ b/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java
@@ -1,5 +1,10 @@
 package world.bentobox.aoneblock.listeners;
 
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
 import org.bukkit.Bukkit;
 import org.bukkit.Location;
 import org.bukkit.World;
@@ -9,6 +14,7 @@
 import org.bukkit.event.EventPriority;
 import org.bukkit.event.Listener;
 import org.eclipse.jdt.annotation.NonNull;
+
 import world.bentobox.aoneblock.AOneBlock;
 import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
 import world.bentobox.aoneblock.oneblocks.OneBlockPhase;
@@ -17,11 +23,6 @@
 import world.bentobox.bentobox.database.objects.Island;
 import world.bentobox.bentobox.util.Util;
 
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-
 /**
  * Handles Holographic elements
  *
diff --git a/src/main/java/world/bentobox/aoneblock/listeners/ItemsAdderListener.java b/src/main/java/world/bentobox/aoneblock/listeners/ItemsAdderListener.java
index a6948c77..bf4c25bb 100644
--- a/src/main/java/world/bentobox/aoneblock/listeners/ItemsAdderListener.java
+++ b/src/main/java/world/bentobox/aoneblock/listeners/ItemsAdderListener.java
@@ -1,8 +1,9 @@
 package world.bentobox.aoneblock.listeners;
 
-import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
+
+import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
 import world.bentobox.aoneblock.AOneBlock;
 
 /**
diff --git a/src/main/java/world/bentobox/aoneblock/listeners/MakeSpace.java b/src/main/java/world/bentobox/aoneblock/listeners/MakeSpace.java
index 75d2dce1..5dfa914a 100644
--- a/src/main/java/world/bentobox/aoneblock/listeners/MakeSpace.java
+++ b/src/main/java/world/bentobox/aoneblock/listeners/MakeSpace.java
@@ -52,7 +52,7 @@ public MakeSpace(AOneBlock addon) {
      * @param entity Entity that is spawned.
      * @param spawnLocation Location where entity is spawned.
      */
-    void makeSpace(@NonNull Entity entity, @NonNull Location spawnLocation)
+    public void makeSpace(@NonNull Entity entity, @NonNull Location spawnLocation)
     {
         World world = entity.getWorld();
         List airBlocks = new ArrayList<>();
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockCustomBlock.java b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockCustomBlock.java
index 40560508..458eb47a 100644
--- a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockCustomBlock.java
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockCustomBlock.java
@@ -2,16 +2,18 @@
 
 import org.bukkit.block.Block;
 
+import world.bentobox.aoneblock.AOneBlock;
+
 /**
- * Represents a custom block
+ * Represents a custom block with custom executable
  *
  * @author HSGamer
  */
 public interface OneBlockCustomBlock {
     /**
-     * Set the block
+     * Executes the custom logic
      *
      * @param block the block
      */
-    void setBlock(Block block);
+    void execute(AOneBlock addon, Block block);
 }
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockCustomBlockCreator.java b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockCustomBlockCreator.java
index 36cb8af2..d46b507c 100644
--- a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockCustomBlockCreator.java
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockCustomBlockCreator.java
@@ -1,10 +1,16 @@
 package world.bentobox.aoneblock.oneblocks;
 
-import world.bentobox.aoneblock.oneblocks.customblock.BlockDataCustomBlock;
-
-import java.util.*;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.function.Function;
 
+import world.bentobox.aoneblock.oneblocks.customblock.BlockDataCustomBlock;
+import world.bentobox.aoneblock.oneblocks.customblock.MobCustomBlock;
+
 /**
  * A creator for {@link OneBlockCustomBlock}
  *
@@ -16,6 +22,7 @@ public final class OneBlockCustomBlockCreator {
 
     static {
         register("block-data", BlockDataCustomBlock::fromMap);
+        register("mob", MobCustomBlock::fromMap);
         register("short", map -> {
             String type = Objects.toString(map.get("data"), null);
             if (type == null) {
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockObject.java b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockObject.java
index 64646f8d..b86eb693 100644
--- a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockObject.java
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockObject.java
@@ -42,7 +42,6 @@ public enum Rarity {
     private Map chest;
     private Rarity rarity;
     private OneBlockCustomBlock customBlock;
-    private String itemsAdderBlock;
     private int prob;
 
     /**
@@ -78,18 +77,6 @@ public OneBlockObject(OneBlockCustomBlock customBlock, int prob) {
         this.prob = prob;
     }
 
-    /**
-     * An ItemsAdder block
-     *
-     * @param namedSpaceID - ItemsAdder block
-     * @param prob        - relative probability
-     */
-
-    public OneBlockObject(String namedSpaceID, int prob) {
-        this.itemsAdderBlock = namedSpaceID;
-        this.prob = prob;
-    }
-
     /**
      * A chest
      *
@@ -113,7 +100,6 @@ public OneBlockObject(OneBlockObject ob) {
         this.rarity = ob.getRarity();
         this.prob = ob.getProb();
         this.customBlock = ob.getCustomBlock();
-        this.itemsAdderBlock = ob.getItemsAdderBlock();
     }
 
     /**
@@ -147,11 +133,6 @@ public OneBlockCustomBlock getCustomBlock() {
         return customBlock;
     }
 
-    /**
-     * @return the itemsAdderBlock
-     */
-    public String getItemsAdderBlock() { return itemsAdderBlock; }
-
 
     /**
      * @return the isMaterial
@@ -176,13 +157,6 @@ public boolean isCustomBlock() {
         return customBlock != null;
     }
 
-    /**
-     * @return the isItemsAdderBlock
-     */
-    public boolean isItemsAdderBlock() {
-        return itemsAdderBlock != null;
-    }
-
     /**
      * @return the rarity
      */
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockPhase.java b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockPhase.java
index 9f9efbe4..af595537 100644
--- a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockPhase.java
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlockPhase.java
@@ -165,17 +165,6 @@ public void addCustomBlock(OneBlockCustomBlock customBlock, int prob) {
         probMap.put(total, new OneBlockObject(customBlock, prob));
     }
 
-    /**
-     * Adds a ItemsAdder's custom block and associated probability
-     * @param namedSpaceID - name space and ID
-     * @param prob        - probability
-     */
-    public void addItemsAdderCustomBlock(String namedSpaceID, int prob) {
-        total += prob;
-        blockTotal += prob;
-        probMap.put(total, new OneBlockObject(namedSpaceID, prob));
-    }
-
     /**
      * Adds an entity type and associated probability
      *
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java
index e369b6bb..e5516509 100644
--- a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java
@@ -31,8 +31,6 @@
 import com.google.common.base.Enums;
 import com.google.common.io.Files;
 
-import dev.lone.itemsadder.api.CustomBlock;
-import dev.lone.itemsadder.api.ItemsAdder;
 import world.bentobox.aoneblock.AOneBlock;
 import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
 import world.bentobox.aoneblock.oneblocks.OneBlockObject.Rarity;
@@ -42,6 +40,7 @@
 
 /**
  * Provides a manager for all phases
+ * 
  * @author tastybento
  *
  */
@@ -65,6 +64,9 @@ public class OneBlocksManager {
     private static final String END_COMMANDS = "end-commands";
     private static final String END_COMMANDS_FIRST_TIME = "end-commands-first-time";
     private static final String REQUIREMENTS = "requirements";
+    private static final String BLOCK = "Block ";
+    private static final String BUT_ALREADY_SET_TO = " but already set to ";
+    private static final String DUPLICATE = " Duplicate phase file?";
     private final AOneBlock addon;
     private TreeMap blockProbs;
 
@@ -72,9 +74,9 @@ public class OneBlocksManager {
      * @param addon - addon
      */
     public OneBlocksManager(AOneBlock addon) {
-        this.addon = addon;
-        // Initialize block probabilities
-        blockProbs = new TreeMap<>();
+	this.addon = addon;
+	// Initialize block probabilities
+	blockProbs = new TreeMap<>();
     }
 
     /**
@@ -83,34 +85,34 @@ public OneBlocksManager(AOneBlock addon) {
      * @throws IOException - if config file has bad syntax or migration fails
      */
     public void loadPhases() throws IOException {
-        // Clear block probabilities
-        blockProbs = new TreeMap<>();
-        // Check for folder
-        File check = new File(addon.getDataFolder(), PHASES);
-        if (check.mkdirs()) {
-            addon.log(check.getAbsolutePath() + " does not exist, made folder.");
-            // Check for oneblock.yml
-            File oneblockFile = new File(addon.getDataFolder(), ONE_BLOCKS_YML);
-            if (oneblockFile.exists()) {
-                // Migrate to new folders
-                File renamedFile = new File(check, ONE_BLOCKS_YML);
-                Files.move(oneblockFile, renamedFile);
-                loadPhase(renamedFile);
-                this.saveOneBlockConfig();
-                java.nio.file.Files.delete(oneblockFile.toPath());
-                java.nio.file.Files.delete(renamedFile.toPath());
-                blockProbs.clear();
-            } else {
-                // Copy files from JAR
-                copyPhasesFromAddonJar(check);
-            }
-        }
-        // Get files in folder
-        // Filter for files ending with .yml
-        FilenameFilter ymlFilter = (dir, name) -> name.toLowerCase(java.util.Locale.ENGLISH).endsWith(".yml");
-        for (File phaseFile : Objects.requireNonNull(check.listFiles(ymlFilter))) {
-            loadPhase(phaseFile);
-        }
+	// Clear block probabilities
+	blockProbs = new TreeMap<>();
+	// Check for folder
+	File check = new File(addon.getDataFolder(), PHASES);
+	if (check.mkdirs()) {
+	    addon.log(check.getAbsolutePath() + " does not exist, made folder.");
+	    // Check for oneblock.yml
+	    File oneblockFile = new File(addon.getDataFolder(), ONE_BLOCKS_YML);
+	    if (oneblockFile.exists()) {
+		// Migrate to new folders
+		File renamedFile = new File(check, ONE_BLOCKS_YML);
+		Files.move(oneblockFile, renamedFile);
+		loadPhase(renamedFile);
+		this.saveOneBlockConfig();
+		java.nio.file.Files.delete(oneblockFile.toPath());
+		java.nio.file.Files.delete(renamedFile.toPath());
+		blockProbs.clear();
+	    } else {
+		// Copy files from JAR
+		copyPhasesFromAddonJar(check);
+	    }
+	}
+	// Get files in folder
+	// Filter for files ending with .yml
+	FilenameFilter ymlFilter = (dir, name) -> name.toLowerCase(java.util.Locale.ENGLISH).endsWith(".yml");
+	for (File phaseFile : Objects.requireNonNull(check.listFiles(ymlFilter))) {
+	    loadPhase(phaseFile);
+	}
     }
 
     /**
@@ -119,391 +121,425 @@ public void loadPhases() throws IOException {
      * @param file - the file to copy
      */
     void copyPhasesFromAddonJar(File file) {
-        try (JarFile jar = new JarFile(addon.getFile())) {
-            // Obtain any locale files, save them and update
-            Util.listJarFiles(jar, PHASES, ".yml").forEach(lf -> addon.saveResource(lf, file, false, true));
-        } catch (Exception e) {
-            addon.logError(e.getMessage());
-        }
+	try (JarFile jar = new JarFile(addon.getFile())) {
+	    // Obtain any locale files, save them and update
+	    Util.listJarFiles(jar, PHASES, ".yml").forEach(lf -> addon.saveResource(lf, file, false, true));
+	} catch (Exception e) {
+	    addon.logError(e.getMessage());
+	}
     }
 
     private void loadPhase(File phaseFile) throws IOException {
-        addon.log("Loading " + phaseFile.getName());
-        // Load the config file
-        YamlConfiguration oneBlocks = new YamlConfiguration();
-        try {
-            oneBlocks.load(phaseFile);
-        } catch (Exception e) {
-            addon.logError(e.getMessage());
-            return;
-        }
-        for (String blockNumber : oneBlocks.getKeys(false)) {
-            Integer blockNum = Integer.valueOf(blockNumber);
-            OneBlockPhase obPhase = blockProbs.computeIfAbsent(blockNum, k -> new OneBlockPhase(blockNumber));
-            // Get config Section
-            ConfigurationSection phase = oneBlocks.getConfigurationSection(blockNumber);
-            // goto
-            if (phase.contains(GOTO_BLOCK)) {
-                obPhase.setGotoBlock(phase.getInt(GOTO_BLOCK, 0));
-                continue;
-            }
-            initBlock(blockNumber, obPhase, phase);
-            // Blocks
-            addBlocks(obPhase, phase);
-            // Mobs
-            addMobs(obPhase, phase);
-            // Chests
-            addChests(obPhase, phase);
-            // Commands
-            addCommands(obPhase, phase);
-            // Requirements
-            addRequirements(obPhase, phase);
-            // Add to the map
-            blockProbs.put(blockNum, obPhase);
-        }
+	addon.log("Loading " + phaseFile.getName());
+	// Load the config file
+	YamlConfiguration oneBlocks = new YamlConfiguration();
+	try {
+	    oneBlocks.load(phaseFile);
+	} catch (Exception e) {
+	    addon.logError(e.getMessage());
+	    return;
+	}
+	for (String phaseStartBlockNumKey : oneBlocks.getKeys(false)) {
+	    Integer phaseStartBlockNum = Integer.valueOf(phaseStartBlockNumKey);
+	    OneBlockPhase obPhase = blockProbs.computeIfAbsent(phaseStartBlockNum,
+		    k -> new OneBlockPhase(phaseStartBlockNumKey));
+	    // Get config Section
+	    ConfigurationSection phaseConfig = oneBlocks.getConfigurationSection(phaseStartBlockNumKey);
+	    // goto
+	    if (phaseConfig.contains(GOTO_BLOCK)) {
+		obPhase.setGotoBlock(phaseConfig.getInt(GOTO_BLOCK, 0));
+		continue;
+	    }
+	    initBlock(phaseStartBlockNumKey, obPhase, phaseConfig);
+	    // Blocks
+	    addBlocks(obPhase, phaseConfig);
+	    // Mobs
+	    addMobs(obPhase, phaseConfig);
+	    // Chests
+	    addChests(obPhase, phaseConfig);
+	    // Commands
+	    addCommands(obPhase, phaseConfig);
+	    // Requirements
+	    addRequirements(obPhase, phaseConfig);
+	    // Add to the map
+	    blockProbs.put(phaseStartBlockNum, obPhase);
+	}
     }
 
     /**
      * Load in the phase's init
+     *
      * @param blockNumber string representation of this phase's block number
-     * @param obPhase OneBlockPhase
-     * @param phase configuration section being read
+     * @param obPhase     OneBlockPhase
+     * @param phaseConfig configuration section being read
      * @throws IOException if there's an error in the config file
      */
-    void initBlock(String blockNumber, OneBlockPhase obPhase, ConfigurationSection phase) throws IOException {
-        if (phase.contains(NAME, true)) {
-            if (obPhase.getPhaseName() != null) {
-                throw new IOException("Block " + blockNumber + ": Phase name trying to be set to " + phase.getString(NAME) + " but already set to " + obPhase.getPhaseName() + ". Duplicate phase file?");
-            }
-            // name
-            obPhase.setPhaseName(phase.getString(NAME, blockNumber));
-        }
-        // biome
-        if (phase.contains(BIOME, true)) {
-            if (obPhase.getPhaseBiome() != null) {
-                throw new IOException("Block " + blockNumber + ": Biome trying to be set to " + phase.getString(BIOME) + " but already set to " + obPhase.getPhaseBiome() + " Duplicate phase file?");
-            }
-            obPhase.setPhaseBiome(getBiome(phase.getString(BIOME)));
-        }
-        // First block
-        if (phase.contains(FIRST_BLOCK)) {
-            if (obPhase.getFirstBlock() != null) {
-                throw new IOException("Block " + blockNumber + ": First block trying to be set to " + phase.getString(FIRST_BLOCK) + " but already set to " + obPhase.getFirstBlock() + " Duplicate phase file?");
-            }
-            addFirstBlock(obPhase, phase.getString(FIRST_BLOCK));
-        }
-        // Icon block
-        if (phase.contains(ICON)) {
-            ItemStack icon = ItemParser.parse(phase.getString(ICON));
-
-            if (icon == null) {
-                throw new IOException("ItemParser failed to parse icon: '" + phase.getString(ICON) + "' for phase " + obPhase.getFirstBlock() + ". Can you check if it is correct?");
-            }
-
-            obPhase.setIconBlock(icon);
-        }
-        // First blocks
-        if (phase.contains(FIXED_BLOCKS)) {
-            if (!obPhase.getFixedBlocks().isEmpty()) {
-                throw new IOException("Block " + blockNumber + ": Fixed blocks trying to be set to " + phase.getString(FIXED_BLOCKS) + " but already set to " + obPhase.getFixedBlocks() + " Duplicate phase file?");
-            }
-            addFixedBlocks(obPhase, phase.getConfigurationSection(FIXED_BLOCKS));
-        }
-
-        if (phase.contains(HOLOGRAMS)) {
-            if (!obPhase.getHologramLines().isEmpty()) {
-                throw new IOException("Block " + blockNumber + ": Hologram Lines trying to be set to " + phase.getString(HOLOGRAMS) + " but already set to " + obPhase.getHologramLines() + " Duplicate phase file?");
-            }
-            addHologramLines(obPhase, phase.getConfigurationSection(HOLOGRAMS));
-        }
-    }
-
-    private void addFixedBlocks(OneBlockPhase obPhase, ConfigurationSection fb) {
-        if (fb == null) {
-            return;
-        }
-        Map result = new HashMap<>();
-        for (String key : fb.getKeys(false)) {
-            if (!NumberUtils.isNumber(key)) {
-                addon.logError("Fixed block key must be an integer. " + key);
-                continue;
-            }
-            int k = Integer.parseInt(key);
-            if (fb.isConfigurationSection(key)) {
-                Map map = fb.getConfigurationSection(key).getValues(false);
-                Optional customBlock = OneBlockCustomBlockCreator.create(map);
-                if (customBlock.isPresent()) {
-                    result.put(k, new OneBlockObject(customBlock.get(), 0));
-                } else {
-                    addon.logError("Fixed block key " + key + " material is not a valid custom block. Ignoring.");
-                }
-            } else {
-                String mat = fb.getString(key);
-                if (mat == null) {
-                    continue;
-                }
-
-                Optional customBlock = OneBlockCustomBlockCreator.create(mat);
-                if (customBlock.isPresent()) {
-                    result.put(k, new OneBlockObject(customBlock.get(), 0));
-                } else {
-                    Material m = Material.matchMaterial(mat);
-                    if (m != null && m.isBlock()) {
-                        result.put(k, new OneBlockObject(m, 0));
-                    } else {
-                        addon.logError("Fixed block key " + key + " material is invalid or not a block. Ignoring.");
-                    }
-                }
-            }
-        }
-        // Set the first block if it exists
-        if (result.containsKey(0)) {
-            obPhase.setFirstBlock(result.get(0));
-        }
-        // Store the remainder
-        obPhase.setFixedBlocks(result);
+    void initBlock(String blockNumber, OneBlockPhase obPhase, ConfigurationSection phaseConfig) throws IOException {
+	// Set name
+	if (phaseConfig.contains(NAME, true)) {
+	    if (obPhase.getPhaseName() != null) {
+		throw new IOException(
+			BLOCK + blockNumber + ": Phase name trying to be set to " + phaseConfig.getString(NAME)
+				+ BUT_ALREADY_SET_TO + obPhase.getPhaseName() + ". Duplicate phase file?");
+	    }
+	    obPhase.setPhaseName(phaseConfig.getString(NAME, blockNumber));
+	}
+
+	// Set biome
+	if (phaseConfig.contains(BIOME, true)) {
+	    if (obPhase.getPhaseBiome() != null) {
+		throw new IOException(BLOCK + blockNumber + ": Biome trying to be set to "
+			+ phaseConfig.getString(BIOME) + BUT_ALREADY_SET_TO + obPhase.getPhaseBiome() + DUPLICATE);
+	    }
+	    obPhase.setPhaseBiome(getBiome(phaseConfig.getString(BIOME)));
+	}
+
+	// Set first block
+	if (phaseConfig.contains(FIRST_BLOCK)) {
+	    if (obPhase.getFirstBlock() != null) {
+		throw new IOException(
+			BLOCK + blockNumber + ": First block trying to be set to " + phaseConfig.getString(FIRST_BLOCK)
+				+ BUT_ALREADY_SET_TO + obPhase.getFirstBlock() + DUPLICATE);
+	    }
+	    addFirstBlock(obPhase, phaseConfig.getString(FIRST_BLOCK));
+	}
+
+	// Set icon
+	if (phaseConfig.contains(ICON)) {
+	    ItemStack icon = ItemParser.parse(phaseConfig.getString(ICON));
+
+	    if (icon == null) {
+		throw new IOException("ItemParser failed to parse icon: '" + phaseConfig.getString(ICON)
+			+ "' for phase " + obPhase.getFirstBlock() + ". Can you check if it is correct?");
+	    }
+
+	    obPhase.setIconBlock(icon);
+	}
+
+	// Add fixed blocks
+	if (phaseConfig.contains(FIXED_BLOCKS)) {
+	    if (!obPhase.getFixedBlocks().isEmpty()) {
+		throw new IOException(BLOCK + blockNumber + ": Fixed blocks trying to be set to "
+			+ phaseConfig.getString(FIXED_BLOCKS) + BUT_ALREADY_SET_TO + obPhase.getFixedBlocks()
+			+ DUPLICATE);
+	    }
+	    addFixedBlocks(obPhase, phaseConfig.getConfigurationSection(FIXED_BLOCKS));
+	}
+
+	// Add holograms
+	if (phaseConfig.contains(HOLOGRAMS)) {
+	    if (!obPhase.getHologramLines().isEmpty()) {
+		throw new IOException(
+			BLOCK + blockNumber + ": Hologram Lines trying to be set to " + phaseConfig.getString(HOLOGRAMS)
+				+ BUT_ALREADY_SET_TO + obPhase.getHologramLines() + DUPLICATE);
+	    }
+	    addHologramLines(obPhase, phaseConfig.getConfigurationSection(HOLOGRAMS));
+	}
+    }
+
+    private void addFixedBlocks(OneBlockPhase obPhase, ConfigurationSection firstBlocksConfig) {
+	if (firstBlocksConfig == null) {
+	    return;
+	}
+
+	Map result = parseFirstBlocksConfig(firstBlocksConfig);
+
+	// Set the first block if it exists
+	if (result.containsKey(0)) {
+	    addon.log("Found firstBlock in fixedBlocks.");
+	    obPhase.setFirstBlock(result.get(0));
+	}
+	// Store the remainder
+	obPhase.setFixedBlocks(result);
+    }
+
+    private Map parseFirstBlocksConfig(ConfigurationSection firstBlocksConfig) {
+	Map result = new HashMap<>();
+
+	for (String key : firstBlocksConfig.getKeys(false)) {
+	    if (!NumberUtils.isNumber(key)) {
+		addon.logError("Fixed block key must be an integer. " + key);
+		continue;
+	    }
+	    int k = Integer.parseInt(key);
+	    parseBlock(result, firstBlocksConfig, key, k);
+	}
+	return result;
+    }
+
+    private void parseBlock(Map result, ConfigurationSection firstBlocksConfig, String key,
+	    int k) {
+	if (firstBlocksConfig.isConfigurationSection(key)) {
+	    parseObjectBlock(result, firstBlocksConfig, key, k);
+	} else {
+	    parseStringBlock(result, firstBlocksConfig, key, k);
+	}
+    }
+
+    /**
+     * This method handles the case where the block's value is a configuration
+     * section, indicating that the block is defined as an object.
+     * 
+     * @param result            the resulting map
+     * @param firstBlocksConfig config
+     * @param key               key
+     * @param k                 integer value of key
+     */
+    private void parseObjectBlock(Map result, ConfigurationSection firstBlocksConfig,
+	    String key, int k) {
+	// Object block parsing logic
+	Map map = firstBlocksConfig.getConfigurationSection(key).getValues(false);
+	Optional customBlock = OneBlockCustomBlockCreator.create(map);
+	if (customBlock.isPresent()) {
+	    result.put(k, new OneBlockObject(customBlock.get(), 0));
+	} else {
+	    addon.logError("Fixed block key " + key + " material is not a valid custom block. Ignoring.");
+	}
+    }
 
+    /**
+     * This method handles the case where the block's value is a string, which could
+     * either be a custom block or a standard Minecraft material.
+     * 
+     * @param result            the resulting map
+     * @param firstBlocksConfig config
+     * @param key               key
+     * @param k                 integer value of key
+     */
+    private void parseStringBlock(Map result, ConfigurationSection firstBlocksConfig,
+	    String key, int k) {
+	// String block parsing logic
+	String mat = firstBlocksConfig.getString(key);
+	if (mat == null) {
+	    return;
+	}
+
+	Optional customBlock = OneBlockCustomBlockCreator.create(mat);
+	if (customBlock.isPresent()) {
+	    result.put(k, new OneBlockObject(customBlock.get(), 0));
+	} else {
+	    Material m = Material.matchMaterial(mat);
+	    if (m != null && m.isBlock()) {
+		result.put(k, new OneBlockObject(m, 0));
+	    } else {
+		addon.logError("Fixed block key " + key + " material is invalid or not a block. Ignoring.");
+	    }
+	}
     }
 
     private void addHologramLines(OneBlockPhase obPhase, ConfigurationSection fb) {
-        if (fb == null) return;
-        Map result = new HashMap<>();
-        for (String key : fb.getKeys(false)) {
-            if (!NumberUtils.isNumber(key)) {
-                addon.logError("Fixed block key must be an integer. " + key);
-                continue;
-            }
-            int k = Integer.parseInt(key);
-            String line = fb.getString(key);
-            if (line != null) {
-                result.put(k, line);
-            }
-        }
-        // Set Hologram Lines
-        obPhase.setHologramLines(result);
+	if (fb == null)
+	    return;
+	Map result = new HashMap<>();
+	for (String key : fb.getKeys(false)) {
+	    if (!NumberUtils.isNumber(key)) {
+		addon.logError("Fixed block key must be an integer. " + key);
+		continue;
+	    }
+	    int k = Integer.parseInt(key);
+	    String line = fb.getString(key);
+	    if (line != null) {
+		result.put(k, line);
+	    }
+	}
+	// Set Hologram Lines
+	obPhase.setHologramLines(result);
 
     }
 
     private Biome getBiome(String string) {
-        if (string == null) {
-            return Biome.PLAINS;
-        }
-        if (Enums.getIfPresent(Biome.class, string).isPresent()) {
-            return Biome.valueOf(string);
-        }
-        // Special case for nether
-        if (string.equals("NETHER") || string.equals("NETHER_WASTES")) {
-            return Enums.getIfPresent(Biome.class, "NETHER")
-                    .or(Enums.getIfPresent(Biome.class, "NETHER_WASTES").or(Biome.PLAINS));
-        }
-        addon.logError("Biome " + string.toUpperCase() + " is invalid! Use one of these...");
-        addon.logError(Arrays.stream(Biome.values()).map(Biome::name).collect(Collectors.joining(",")));
-        return Biome.PLAINS;
+	if (string == null) {
+	    return Biome.PLAINS;
+	}
+	if (Enums.getIfPresent(Biome.class, string).isPresent()) {
+	    return Biome.valueOf(string);
+	}
+	// Special case for nether
+	if (string.equals("NETHER") || string.equals("NETHER_WASTES")) {
+	    return Enums.getIfPresent(Biome.class, "NETHER")
+		    .or(Enums.getIfPresent(Biome.class, "NETHER_WASTES").or(Biome.PLAINS));
+	}
+	addon.logError("Biome " + string.toUpperCase() + " is invalid! Use one of these...");
+	addon.logError(Arrays.stream(Biome.values()).map(Biome::name).collect(Collectors.joining(",")));
+	return Biome.PLAINS;
     }
 
     void addFirstBlock(OneBlockPhase obPhase, @Nullable String material) {
-        if (material == null) {
-            return;
-        }
-        Material m = Material.matchMaterial(material);
-        if (m == null || !m.isBlock()) {
-            addon.logError("Bad firstBlock material: " + material);
-        } else {
-            obPhase.setFirstBlock(new OneBlockObject(m, 0));
-        }
+	if (material == null) {
+	    return;
+	}
+	Material m = Material.matchMaterial(material);
+	if (m == null || !m.isBlock()) {
+	    addon.logError("Bad firstBlock material: " + material);
+	} else {
+	    obPhase.setFirstBlock(new OneBlockObject(m, 0));
+	}
     }
 
     void addCommands(OneBlockPhase obPhase, ConfigurationSection phase) {
-        if (phase.contains(START_COMMANDS)) {
-            obPhase.setStartCommands(phase.getStringList(START_COMMANDS));
-        }
-        if (phase.contains(END_COMMANDS)) {
-            obPhase.setEndCommands(phase.getStringList(END_COMMANDS));
-        }
-        if (phase.contains(END_COMMANDS_FIRST_TIME)) {
-            obPhase.setFirstTimeEndCommands(phase.getStringList(END_COMMANDS_FIRST_TIME));
-        }
+	if (phase.contains(START_COMMANDS)) {
+	    obPhase.setStartCommands(phase.getStringList(START_COMMANDS));
+	}
+	if (phase.contains(END_COMMANDS)) {
+	    obPhase.setEndCommands(phase.getStringList(END_COMMANDS));
+	}
+	if (phase.contains(END_COMMANDS_FIRST_TIME)) {
+	    obPhase.setFirstTimeEndCommands(phase.getStringList(END_COMMANDS_FIRST_TIME));
+	}
     }
 
     void addRequirements(OneBlockPhase obPhase, ConfigurationSection phase) {
-        List reqList = new ArrayList<>();
-        if (!phase.isConfigurationSection(REQUIREMENTS)) {
-            return;
-        }
-        ConfigurationSection reqs = phase.getConfigurationSection(REQUIREMENTS);
-        for (ReqType key : Requirement.ReqType.values()) {
-            if (reqs.contains(key.getKey())) {
-                Requirement r;
-                if (key.getClazz().equals(Double.class)) {
-                    r = new Requirement(key, reqs.getDouble(key.getKey()));
-                } else if (key.getClazz().equals(Long.class)) {
-                    r = new Requirement(key, reqs.getLong(key.getKey()));
-                } else {
-                    r = new Requirement(key, reqs.getString(key.getKey()));
-                }
-                reqList.add(r);
-            }
-        }
-        obPhase.setRequirements(reqList);
+	List reqList = new ArrayList<>();
+	if (!phase.isConfigurationSection(REQUIREMENTS)) {
+	    return;
+	}
+	ConfigurationSection reqs = phase.getConfigurationSection(REQUIREMENTS);
+	for (ReqType key : Requirement.ReqType.values()) {
+	    if (reqs.contains(key.getKey())) {
+		Requirement r;
+		if (key.getClazz().equals(Double.class)) {
+		    r = new Requirement(key, reqs.getDouble(key.getKey()));
+		} else if (key.getClazz().equals(Long.class)) {
+		    r = new Requirement(key, reqs.getLong(key.getKey()));
+		} else {
+		    r = new Requirement(key, reqs.getString(key.getKey()));
+		}
+		reqList.add(r);
+	    }
+	}
+	obPhase.setRequirements(reqList);
     }
 
     void addChests(OneBlockPhase obPhase, ConfigurationSection phase) throws IOException {
-        if (!phase.isConfigurationSection(CHESTS)) {
-            return;
-        }
-        if (!obPhase.getChests().isEmpty()) {
-            throw new IOException(obPhase.getPhaseName() + ": Chests cannot be set more than once. Duplicate file?");
-        }
-        ConfigurationSection chests = phase.getConfigurationSection(CHESTS);
-        for (String chestId : chests.getKeys(false)) {
-            ConfigurationSection chest = chests.getConfigurationSection(chestId);
-            Rarity rarity = Rarity.COMMON;
-            try {
-                rarity = OneBlockObject.Rarity.valueOf(chest.getString(RARITY, "COMMON").toUpperCase());
-            } catch (Exception e) {
-                addon.logError(
-                        "Rarity value of " + chest.getString(RARITY, "UNKNOWN") + " is invalid! Use one of these...");
-                addon.logError(Arrays.stream(Rarity.values()).map(Rarity::name).collect(Collectors.joining(",")));
-                rarity = Rarity.COMMON;
-            }
-            Map items = new HashMap<>();
-            ConfigurationSection contents = chest.getConfigurationSection(CONTENTS);
-            if (contents != null) {
-                for (String index : contents.getKeys(false)) {
-                    int slot = Integer.parseInt(index);
-                    ItemStack item = contents.getItemStack(index);
-                    if (item != null) {
-                        items.put(slot, item);
-                    }
-                }
-            }
-            obPhase.addChest(items, rarity);
-        }
+	if (!phase.isConfigurationSection(CHESTS)) {
+	    return;
+	}
+	if (!obPhase.getChests().isEmpty()) {
+	    throw new IOException(obPhase.getPhaseName() + ": Chests cannot be set more than once. Duplicate file?");
+	}
+	ConfigurationSection chests = phase.getConfigurationSection(CHESTS);
+	for (String chestId : chests.getKeys(false)) {
+	    ConfigurationSection chest = chests.getConfigurationSection(chestId);
+	    Rarity rarity = Rarity.COMMON;
+	    try {
+		rarity = OneBlockObject.Rarity.valueOf(chest.getString(RARITY, "COMMON").toUpperCase());
+	    } catch (Exception e) {
+		addon.logError(
+			"Rarity value of " + chest.getString(RARITY, "UNKNOWN") + " is invalid! Use one of these...");
+		addon.logError(Arrays.stream(Rarity.values()).map(Rarity::name).collect(Collectors.joining(",")));
+		rarity = Rarity.COMMON;
+	    }
+	    Map items = new HashMap<>();
+	    ConfigurationSection contents = chest.getConfigurationSection(CONTENTS);
+	    if (contents != null) {
+		for (String index : contents.getKeys(false)) {
+		    int slot = Integer.parseInt(index);
+		    ItemStack item = contents.getItemStack(index);
+		    if (item != null) {
+			items.put(slot, item);
+		    }
+		}
+	    }
+	    obPhase.addChest(items, rarity);
+	}
     }
 
     void addMobs(OneBlockPhase obPhase, ConfigurationSection phase) throws IOException {
-        if (!phase.isConfigurationSection(MOBS)) {
-            return;
-        }
-        if (!obPhase.getMobs().isEmpty()) {
-            throw new IOException(obPhase.getPhaseName() + ": Mobs cannot be set more than once. Duplicate file?");
-        }
-        ConfigurationSection mobs = phase.getConfigurationSection(MOBS);
-        for (String entity : mobs.getKeys(false)) {
-            String name = entity.toUpperCase(Locale.ENGLISH);
-            EntityType et = null;
-            // Pig zombie handling
-            if (name.equals("PIG_ZOMBIE") || name.equals("ZOMBIFIED_PIGLIN")) {
-                et = Enums.getIfPresent(EntityType.class, "ZOMBIFIED_PIGLIN")
-                        .or(Enums.getIfPresent(EntityType.class, "PIG_ZOMBIE").or(EntityType.PIG));
-            } else {
-                et = Enums.getIfPresent(EntityType.class, name).orNull();
-            }
-            if (et == null) {
-                // Does not exist
-                addon.logError("Bad entity type in " + obPhase.getPhaseName() + ": " + entity);
-                addon.logError("Try one of these...");
-                addon.logError(Arrays.stream(EntityType.values()).filter(EntityType::isSpawnable)
-                        .filter(EntityType::isAlive).map(EntityType::name).collect(Collectors.joining(",")));
-                return;
-            }
-            if (et.isSpawnable() && et.isAlive()) {
-                if (mobs.getInt(entity) > 0) {
-                    obPhase.addMob(et, mobs.getInt(entity));
-                } else {
-                    addon.logWarning("Bad entity weight for " + obPhase.getPhaseName() + ": " + entity + ". Must be positive number above 1.");
-                }
-            } else {
-                addon.logError("Entity type is not spawnable " + obPhase.getPhaseName() + ": " + entity);
-            }
-        }
+	if (!phase.isConfigurationSection(MOBS)) {
+	    return;
+	}
+	if (!obPhase.getMobs().isEmpty()) {
+	    throw new IOException(obPhase.getPhaseName() + ": Mobs cannot be set more than once. Duplicate file?");
+	}
+	ConfigurationSection mobs = phase.getConfigurationSection(MOBS);
+	for (String entity : mobs.getKeys(false)) {
+	    String name = entity.toUpperCase(Locale.ENGLISH);
+	    EntityType et = null;
+	    // Pig zombie handling
+	    if (name.equals("PIG_ZOMBIE") || name.equals("ZOMBIFIED_PIGLIN")) {
+		et = Enums.getIfPresent(EntityType.class, "ZOMBIFIED_PIGLIN")
+			.or(Enums.getIfPresent(EntityType.class, "PIG_ZOMBIE").or(EntityType.PIG));
+	    } else {
+		et = Enums.getIfPresent(EntityType.class, name).orNull();
+	    }
+	    if (et == null) {
+		// Does not exist
+		addon.logError("Bad entity type in " + obPhase.getPhaseName() + ": " + entity);
+		addon.logError("Try one of these...");
+		addon.logError(Arrays.stream(EntityType.values()).filter(EntityType::isSpawnable)
+			.filter(EntityType::isAlive).map(EntityType::name).collect(Collectors.joining(",")));
+		return;
+	    }
+	    if (et.isSpawnable() && et.isAlive()) {
+		if (mobs.getInt(entity) > 0) {
+		    obPhase.addMob(et, mobs.getInt(entity));
+		} else {
+		    addon.logWarning("Bad entity weight for " + obPhase.getPhaseName() + ": " + entity
+			    + ". Must be positive number above 1.");
+		}
+	    } else {
+		addon.logError("Entity type is not spawnable " + obPhase.getPhaseName() + ": " + entity);
+	    }
+	}
     }
 
     void addBlocks(OneBlockPhase obPhase, ConfigurationSection phase) {
-        if (phase.isConfigurationSection(BLOCKS)) {
-            ConfigurationSection blocks = phase.getConfigurationSection(BLOCKS);
-            for (String material : blocks.getKeys(false)) {
-                if (Material.getMaterial(material) != null) {
-                    addMaterial(obPhase, material, Objects.toString(blocks.get(material)));
-                } else {
-                    if (addon.hasItemsAdder()) {
-                        CustomBlock block = CustomBlock.getInstance(material);
-                        if (block != null) {
-                            addItemsAdderBlock(obPhase, material, Objects.toString(blocks.get(material)));
-                        } else if (ItemsAdder.getAllItems() != null){
-                            if (ItemsAdder.getAllItems().size() != 0) {
-                                addon.logError("Bad block material in " + obPhase.getPhaseName() + ": " + material);
-                            }
-                        }
-                    } else {
-                        addon.logError("Bad block material in " + obPhase.getPhaseName() + ": " + material);
-                    }
-                }
-            }
-        } else if (phase.isList(BLOCKS)) {
-            List> blocks = phase.getMapList(BLOCKS);
-            for (Map map : blocks) {
-                if (map.size() == 1) {
-                    Map.Entry entry = map.entrySet().iterator().next();
-                    if (addMaterial(obPhase, Objects.toString(entry.getKey()), Objects.toString(entry.getValue()))) {
-                        continue;
-                    }
-                }
-
-                int probability = Integer.parseInt(Objects.toString(map.get("probability"), "0"));
-                Optional customBlock = OneBlockCustomBlockCreator.create(map);
-                if (customBlock.isPresent()) {
-                    obPhase.addCustomBlock(customBlock.get(), probability);
-                } else {
-                    addon.logError("Bad custom block in " + obPhase.getPhaseName() + ": " + map);
-                }
-            }
-        }
+	if (phase.isConfigurationSection(BLOCKS)) {
+	    ConfigurationSection blocks = phase.getConfigurationSection(BLOCKS);
+	    for (String material : blocks.getKeys(false)) {
+			addMaterial(obPhase, material, Objects.toString(blocks.get(material)));
+	    }
+	} else if (phase.isList(BLOCKS)) {
+	    List> blocks = phase.getMapList(BLOCKS);
+	    for (Map map : blocks) {
+		if (map.size() == 1) {
+		    Map.Entry entry = map.entrySet().iterator().next();
+		    if (addMaterial(obPhase, Objects.toString(entry.getKey()), Objects.toString(entry.getValue()))) {
+			continue;
+		    }
+		}
+
+		int probability = Integer.parseInt(Objects.toString(map.get("probability"), "0"));
+		Optional customBlock = OneBlockCustomBlockCreator.create(map);
+		if (customBlock.isPresent()) {
+		    obPhase.addCustomBlock(customBlock.get(), probability);
+		} else {
+		    addon.logError("Bad custom block in " + obPhase.getPhaseName() + ": " + map);
+		}
+	    }
+	}
     }
 
     private boolean addMaterial(OneBlockPhase obPhase, String material, String probability) {
-        int prob;
-        try {
-            prob = Integer.parseInt(probability);
-        } catch (Exception e) {
-            return false;
-        }
-
-        if (prob < 1) {
-            addon.logWarning("Bad item weight for " + obPhase.getPhaseName() + ": " + material + ". Must be positive number above 1.");
-            return false;
-        }
-
-        // Register if the material is a valid custom block and can be created from the short creator from OneBlockCustomBlockCreator
-        Optional optionalCustomBlock = OneBlockCustomBlockCreator.create(material);
-        if (optionalCustomBlock.isPresent()) {
-            obPhase.addCustomBlock(optionalCustomBlock.get(), prob);
-            return true;
-        }
-
-        // Otherwise, register the material as a block
-        Material m = Material.matchMaterial(material);
-        if (m == null || !m.isBlock()) {
-            addon.logError("Bad block material in " + obPhase.getPhaseName() + ": " + material);
-            return false;
-        }
-        obPhase.addBlock(m, prob);
-        return true;
-    }
-
-    private void addItemsAdderBlock(OneBlockPhase obPhase, String block, String probability) {
-        int prob;
-        try {
-            prob = Integer.parseInt(probability);
-            if (prob < 1) {
-                addon.logWarning("Bad item weight for " + obPhase.getPhaseName() + ": " + block + ". Must be positive number above 1.");
-            } else {
-                obPhase.addItemsAdderCustomBlock(block, prob);
-            }
-        } catch (Exception e) {
-            addon.logError("Bad item weight for " + obPhase.getPhaseName() + ": " + block + ". Must be a number.");
-        }
-
+	int prob;
+	try {
+	    prob = Integer.parseInt(probability);
+	} catch (Exception e) {
+	    return false;
+	}
+
+	if (prob < 1) {
+	    addon.logWarning("Bad item weight for " + obPhase.getPhaseName() + ": " + material
+		    + ". Must be positive number above 1.");
+	    return false;
+	}
+
+	// Register if the material is a valid custom block and can be created from the
+	// short creator from OneBlockCustomBlockCreator
+	Optional optionalCustomBlock = OneBlockCustomBlockCreator.create(material);
+	if (optionalCustomBlock.isPresent()) {
+	    obPhase.addCustomBlock(optionalCustomBlock.get(), prob);
+	    return true;
+	}
+
+	// Otherwise, register the material as a block
+	Material m = Material.matchMaterial(material);
+	if (m == null || !m.isBlock()) {
+	    addon.logError("Bad block material in " + obPhase.getPhaseName() + ": " + material);
+	    return false;
+	}
+	obPhase.addBlock(m, prob);
+	return true;
     }
 
     /**
@@ -514,24 +550,24 @@ private void addItemsAdderBlock(OneBlockPhase obPhase, String block, String prob
      */
     @Nullable
     public OneBlockPhase getPhase(int blockCount) {
-        Entry en = blockProbs.floorEntry(blockCount);
-        return en != null ? en.getValue() : null;
+	Entry en = blockProbs.floorEntry(blockCount);
+	return en != null ? en.getValue() : null;
     }
 
     /**
      * @return list of phase names with spaces replaced by underscore so they are
-     * one word
+     *         one word
      */
     public List getPhaseList() {
-        return blockProbs.values().stream().map(OneBlockPhase::getPhaseName).filter(Objects::nonNull)
-                .map(n -> n.replace(" ", "_")).collect(Collectors.toList());
+	return blockProbs.values().stream().map(OneBlockPhase::getPhaseName).filter(Objects::nonNull)
+		.map(n -> n.replace(" ", "_")).collect(Collectors.toList());
     }
 
     /**
      * @return the blockProbs
      */
     public NavigableMap getBlockProbs() {
-        return blockProbs;
+	return blockProbs;
     }
 
     /**
@@ -542,11 +578,10 @@ public NavigableMap getBlockProbs() {
      * @return optional OneBlockPhase
      */
     public Optional getPhase(String name) {
-        return blockProbs.values().stream()
-                .filter(p -> p.getPhaseName() != null
-                && (p.getPhaseName().equalsIgnoreCase(name)
-                        || p.getPhaseName().replace(" ", "_").equalsIgnoreCase(name)))
-                .findFirst();
+	return blockProbs.values().stream()
+		.filter(p -> p.getPhaseName() != null && (p.getPhaseName().equalsIgnoreCase(name)
+			|| p.getPhaseName().replace(" ", "_").equalsIgnoreCase(name)))
+		.findFirst();
     }
 
     /**
@@ -556,122 +591,124 @@ public Optional getPhase(String name) {
      */
 
     public boolean saveOneBlockConfig() {
-        // Go through each phase
-        boolean success = true;
-        for (OneBlockPhase p : blockProbs.values()) {
-            success = savePhase(p);
-        }
-        return success;
+	// Go through each phase
+	boolean success = true;
+	for (OneBlockPhase p : blockProbs.values()) {
+	    success = savePhase(p);
+	}
+	return success;
     }
 
     /**
      * Save a phase
+     * 
      * @param p OneBlockPhase
      * @return true if successfully saved
      */
     public boolean savePhase(OneBlockPhase p) {
-        if (!saveMainPhase(p)) {
-            // Failure
-            return false;
-        }
-        // No chests in goto phases
-        if (p.isGotoPhase()) {
-            // Done
-            return true;
-        }
-        return saveChestPhase(p);
+	if (!saveMainPhase(p)) {
+	    // Failure
+	    return false;
+	}
+	// No chests in goto phases
+	if (p.isGotoPhase()) {
+	    // Done
+	    return true;
+	}
+	return saveChestPhase(p);
     }
 
     private boolean saveMainPhase(OneBlockPhase p) {
-        YamlConfiguration oneBlocks = new YamlConfiguration();
-        ConfigurationSection phSec = oneBlocks.createSection(p.getBlockNumber());
-        // Check for a goto block
-        if (p.isGotoPhase()) {
-            phSec.set(GOTO_BLOCK, p.getGotoBlock());
-        } else {
-            phSec.set(NAME, p.getPhaseName());
-            if (p.getIconBlock() != null) {
-                phSec.set(ICON, p.getIconBlock().getType().name());
-            }
-            if (p.getFirstBlock() != null) {
-                phSec.set(FIRST_BLOCK, p.getFirstBlock().getMaterial().name());
-            }
-            if (p.getPhaseBiome() != null) {
-                phSec.set(BIOME, p.getPhaseBiome().name());
-            }
-            saveBlocks(phSec, p);
-            saveEntities(phSec, p);
-            saveHolos(phSec, p);
-            saveCommands(phSec, p);
-        }
-        try {
-            // Save
-            File phaseFile = new File(addon.getDataFolder() + File.separator + PHASES,
-                    getPhaseFileName(p) + ".yml");
-            oneBlocks.save(phaseFile);
-        } catch (IOException e) {
-            addon.logError("Could not save phase " + p.getPhaseName() + " " + e.getMessage());
-            return false;
-        }
-        return true;
+	YamlConfiguration oneBlocks = new YamlConfiguration();
+	ConfigurationSection phSec = oneBlocks.createSection(p.getBlockNumber());
+	// Check for a goto block
+	if (p.isGotoPhase()) {
+	    phSec.set(GOTO_BLOCK, p.getGotoBlock());
+	} else {
+	    phSec.set(NAME, p.getPhaseName());
+	    if (p.getIconBlock() != null) {
+		phSec.set(ICON, p.getIconBlock().getType().name());
+	    }
+	    if (p.getFirstBlock() != null) {
+		phSec.set(FIRST_BLOCK, p.getFirstBlock().getMaterial().name());
+	    }
+	    if (p.getPhaseBiome() != null) {
+		phSec.set(BIOME, p.getPhaseBiome().name());
+	    }
+	    saveBlocks(phSec, p);
+	    saveEntities(phSec, p);
+	    saveHolos(phSec, p);
+	    saveCommands(phSec, p);
+	}
+	try {
+	    // Save
+	    File phaseFile = new File(addon.getDataFolder() + File.separator + PHASES, getPhaseFileName(p) + ".yml");
+	    oneBlocks.save(phaseFile);
+	} catch (IOException e) {
+	    addon.logError("Could not save phase " + p.getPhaseName() + " " + e.getMessage());
+	    return false;
+	}
+	return true;
     }
 
     private void saveCommands(ConfigurationSection phSec, OneBlockPhase p) {
-        phSec.set(START_COMMANDS, p.getStartCommands());
-        phSec.set(END_COMMANDS, p.getEndCommands());
+	phSec.set(START_COMMANDS, p.getStartCommands());
+	phSec.set(END_COMMANDS, p.getEndCommands());
 
     }
 
     private void saveHolos(ConfigurationSection phSec, OneBlockPhase p) {
-        if (p.getHologramLines() == null) return;
-        ConfigurationSection holos = phSec.createSection(HOLOGRAMS);
-        p.getHologramLines().forEach((k, v) -> holos.set(String.valueOf(k), v));
+	if (p.getHologramLines() == null)
+	    return;
+	ConfigurationSection holos = phSec.createSection(HOLOGRAMS);
+	p.getHologramLines().forEach((k, v) -> holos.set(String.valueOf(k), v));
     }
 
     private boolean saveChestPhase(OneBlockPhase p) {
-        YamlConfiguration oneBlocks = new YamlConfiguration();
-        ConfigurationSection phSec = oneBlocks.createSection(p.getBlockNumber());
-        saveChests(phSec, p);
-        try {
-            // Save
-            File phaseFile = new File(addon.getDataFolder() + File.separator + PHASES,
-                    getPhaseFileName(p) + "_chests.yml");
-            oneBlocks.save(phaseFile);
-        } catch (IOException e) {
-            addon.logError("Could not save chest phase " + p.getPhaseName() + " " + e.getMessage());
-            return false;
-        }
-        return true;
+	YamlConfiguration oneBlocks = new YamlConfiguration();
+	ConfigurationSection phSec = oneBlocks.createSection(p.getBlockNumber());
+	saveChests(phSec, p);
+	try {
+	    // Save
+	    File phaseFile = new File(addon.getDataFolder() + File.separator + PHASES,
+		    getPhaseFileName(p) + "_chests.yml");
+	    oneBlocks.save(phaseFile);
+	} catch (IOException e) {
+	    addon.logError("Could not save chest phase " + p.getPhaseName() + " " + e.getMessage());
+	    return false;
+	}
+	return true;
     }
 
     private String getPhaseFileName(OneBlockPhase p) {
-        if (p.isGotoPhase()) {
-            return p.getBlockNumber() + "_goto_" + p.getGotoBlock();
-        }
-        return p.getBlockNumber() + "_" + (p.getPhaseName() == null ? "" : p.getPhaseName().toLowerCase().replace(' ', '_'));
+	if (p.isGotoPhase()) {
+	    return p.getBlockNumber() + "_goto_" + p.getGotoBlock();
+	}
+	return p.getBlockNumber() + "_"
+		+ (p.getPhaseName() == null ? "" : p.getPhaseName().toLowerCase().replace(' ', '_'));
     }
 
     private void saveChests(ConfigurationSection phSec, OneBlockPhase phase) {
-        ConfigurationSection chests = phSec.createSection(CHESTS);
-        int index = 1;
-        for (OneBlockObject chest : phase.getChests()) {
-            ConfigurationSection c = chests.createSection(String.valueOf(index++));
-            c.set(CONTENTS, chest.getChest());
-            c.set(RARITY, chest.getRarity().name());
-        }
+	ConfigurationSection chests = phSec.createSection(CHESTS);
+	int index = 1;
+	for (OneBlockObject chest : phase.getChests()) {
+	    ConfigurationSection c = chests.createSection(String.valueOf(index++));
+	    c.set(CONTENTS, chest.getChest());
+	    c.set(RARITY, chest.getRarity().name());
+	}
 
     }
 
     private void saveEntities(ConfigurationSection phSec, OneBlockPhase phase) {
-        ConfigurationSection mobs = phSec.createSection(MOBS);
-        phase.getMobs().forEach((k, v) -> mobs.set(k.name(), v));
+	ConfigurationSection mobs = phSec.createSection(MOBS);
+	phase.getMobs().forEach((k, v) -> mobs.set(k.name(), v));
     }
 
     private void saveBlocks(ConfigurationSection phSec, OneBlockPhase phase) {
-        ConfigurationSection fixedBlocks = phSec.createSection(FIXED_BLOCKS);
-        phase.getFixedBlocks().forEach((k, v) -> fixedBlocks.set(String.valueOf(k), v.getMaterial().name()));
-        ConfigurationSection blocks = phSec.createSection(BLOCKS);
-        phase.getBlocks().forEach((k, v) -> blocks.set(k.name(), v));
+	ConfigurationSection fixedBlocks = phSec.createSection(FIXED_BLOCKS);
+	phase.getFixedBlocks().forEach((k, v) -> fixedBlocks.set(String.valueOf(k), v.getMaterial().name()));
+	ConfigurationSection blocks = phSec.createSection(BLOCKS);
+	phase.getBlocks().forEach((k, v) -> blocks.set(k.name(), v));
 
     }
 
@@ -684,10 +721,10 @@ private void saveBlocks(ConfigurationSection phSec, OneBlockPhase phase) {
     @SuppressWarnings("WrapperTypeMayBePrimitive")
     @Nullable
     public OneBlockPhase getNextPhase(@NonNull OneBlockPhase phase) {
-        // These are Integer objects because GSON can yield nulls if they do not exist
-        Integer blockNum = phase.getBlockNumberValue();
-        Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
-        return nextKey != null ? this.getPhase(nextKey) : null;
+	// These are Integer objects because GSON can yield nulls if they do not exist
+	Integer blockNum = phase.getBlockNumberValue();
+	Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
+	return nextKey != null ? this.getPhase(nextKey) : null;
     }
 
     /**
@@ -695,16 +732,36 @@ public OneBlockPhase getNextPhase(@NonNull OneBlockPhase phase) {
      *
      * @param obi - one block island
      * @return number of blocks to the next phase. If there is no phase after -1 is
-     * returned.
+     *         returned.
      */
     public int getNextPhaseBlocks(@NonNull OneBlockIslands obi) {
-        Integer blockNum = obi.getBlockNumber();
-        Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
-        if (nextKey == null) {
-            return -1;
-        }
-        OneBlockPhase nextPhase = this.getPhase(nextKey);
-        return nextPhase == null ? -1 : (nextPhase.getBlockNumberValue() - obi.getBlockNumber());
+	Integer blockNum = obi.getBlockNumber();
+	Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
+	if (nextKey == null) {
+	    return -1;
+	}
+	OneBlockPhase nextPhase = this.getPhase(nextKey);
+	return nextPhase == null ? -1 : (nextPhase.getBlockNumberValue() - obi.getBlockNumber());
+    }
+
+    /**
+     * Get the number of blocks for this phase
+     *
+     * @param obi - one block island
+     * @return number of blocks for this current phase. If there is no phase after
+     *         -1 is returned.
+     */
+    public int getPhaseBlocks(@NonNull OneBlockIslands obi) {
+	Integer blockNum = obi.getBlockNumber();
+	Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
+	if (nextKey == null) {
+	    return -1;
+	}
+	OneBlockPhase thisPhase = this.getPhase(blockNum);
+	if (thisPhase == null)
+	    return -1;
+	OneBlockPhase nextPhase = this.getPhase(nextKey);
+	return nextPhase == null ? -1 : (nextPhase.getBlockNumberValue() - thisPhase.getBlockNumberValue());
     }
 
     /**
@@ -714,97 +771,97 @@ public int getNextPhaseBlocks(@NonNull OneBlockIslands obi) {
      * @return percentage done. If there is no next phase then return 0
      */
     public double getPercentageDone(@NonNull OneBlockIslands obi) {
-        int blockNum = obi.getBlockNumber();
-        OneBlockPhase thisPhase = this.getPhase(blockNum);
-        if (thisPhase == null) {
-            return 0;
-        }
-        Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
-        if (nextKey == null) {
-            return 0;
-        }
-        OneBlockPhase nextPhase = this.getPhase(nextKey);
-        if (nextPhase == null) {
-            return 0;
-        }
-        int phaseSize = nextPhase.getBlockNumberValue() - thisPhase.getBlockNumberValue();
-        return 100 - (100 * (double) (nextPhase.getBlockNumberValue() - obi.getBlockNumber()) / phaseSize);
+	int blockNum = obi.getBlockNumber();
+	OneBlockPhase thisPhase = this.getPhase(blockNum);
+	if (thisPhase == null) {
+	    return 0;
+	}
+	Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
+	if (nextKey == null) {
+	    return 0;
+	}
+	OneBlockPhase nextPhase = this.getPhase(nextKey);
+	if (nextPhase == null) {
+	    return 0;
+	}
+	int phaseSize = nextPhase.getBlockNumberValue() - thisPhase.getBlockNumberValue();
+	return 100 - (100 * (double) (nextPhase.getBlockNumberValue() - obi.getBlockNumber()) / phaseSize);
     }
 
     public void getProbs(OneBlockPhase phase) {
-        // Find the phase after this one
-        Integer blockNum = Integer.valueOf(phase.getBlockNumber());
-        Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
-        if (nextKey != null) {
-            // This is the size of the phase in blocks
-            int phaseSize = nextKey - blockNum;
-            int blockTotal = phase.getBlockTotal();
-            int likelyChestTotal = 0;
-            double totalBlocks = 0;
-            // Now calculate the relative block probability
-            for (Entry en : phase.getBlocks().entrySet()) {
-                double chance = (double) en.getValue() / blockTotal;
-                double likelyNumberGenerated = chance * phaseSize;
-                totalBlocks += likelyNumberGenerated;
-                String report = en.getKey() + " likely generated = " + Math.round(likelyNumberGenerated) + " = "
-                        + Math.round(likelyNumberGenerated * 100 / phaseSize) + "%";
-                if (likelyNumberGenerated < 1) {
-                    addon.logWarning(report);
-                } else {
-                    addon.log(report);
-                }
-                if (en.getKey().equals(Material.CHEST)) {
-                    likelyChestTotal = (int) Math.round(likelyNumberGenerated);
-                }
-            }
-            addon.log("Total blocks generated = " + totalBlocks);
-            // Get the specific chest probability
-            if (likelyChestTotal == 0) {
-                addon.logWarning("No chests will be generated");
-                return;
-            }
-            addon.log("**** A total of " + likelyChestTotal + " chests will be generated ****");
-            // Now calculate chest chances
-            double lastChance = 0;
-            for (Entry en : OneBlockPhase.CHEST_CHANCES.entrySet()) {
-                // Get the number of chests in this rarity group
-                int num = phase.getChestsMap().getOrDefault(en.getValue(), Collections.emptyList()).size();
-                double likelyNumberGenerated = (en.getKey() - lastChance) * likelyChestTotal;
-                lastChance = en.getKey();
-                String report = num + " " + en.getValue() + " chests in phase. Likely number generated = "
-                        + Math.round(likelyNumberGenerated);
-                if (num > 0 && likelyNumberGenerated < 1) {
-                    addon.logWarning(report);
-                } else {
-                    addon.log(report);
-                }
-
-            }
-            // Mobs
-            addon.log("-=-=-=-= Mobs -=-=-=-=-");
-            double totalMobs = 0;
-            // Now calculate the relative block probability
-            for (Entry en : phase.getMobs().entrySet()) {
-                double chance = (double) en.getValue() / phase.getTotal();
-                double likelyNumberGenerated = chance * phaseSize;
-                totalMobs += likelyNumberGenerated;
-                String report = en.getKey() + " likely generated = " + Math.round(likelyNumberGenerated) + " = "
-                        + Math.round(likelyNumberGenerated * 100 / phaseSize) + "%";
-                if (likelyNumberGenerated < 1) {
-                    addon.logWarning(report);
-                } else {
-                    addon.log(report);
-                }
-            }
-            addon.log("**** A total of " + Math.round(totalMobs) + " mobs will likely be generated ****");
-        }
+	// Find the phase after this one
+	Integer blockNum = Integer.valueOf(phase.getBlockNumber());
+	Integer nextKey = blockProbs.ceilingKey(blockNum + 1);
+	if (nextKey != null) {
+	    // This is the size of the phase in blocks
+	    int phaseSize = nextKey - blockNum;
+	    int blockTotal = phase.getBlockTotal();
+	    int likelyChestTotal = 0;
+	    double totalBlocks = 0;
+	    // Now calculate the relative block probability
+	    for (Entry en : phase.getBlocks().entrySet()) {
+		double chance = (double) en.getValue() / blockTotal;
+		double likelyNumberGenerated = chance * phaseSize;
+		totalBlocks += likelyNumberGenerated;
+		String report = en.getKey() + " likely generated = " + Math.round(likelyNumberGenerated) + " = "
+			+ Math.round(likelyNumberGenerated * 100 / phaseSize) + "%";
+		if (likelyNumberGenerated < 1) {
+		    addon.logWarning(report);
+		} else {
+		    addon.log(report);
+		}
+		if (en.getKey().equals(Material.CHEST)) {
+		    likelyChestTotal = (int) Math.round(likelyNumberGenerated);
+		}
+	    }
+	    addon.log("Total blocks generated = " + totalBlocks);
+	    // Get the specific chest probability
+	    if (likelyChestTotal == 0) {
+		addon.logWarning("No chests will be generated");
+		return;
+	    }
+	    addon.log("**** A total of " + likelyChestTotal + " chests will be generated ****");
+	    // Now calculate chest chances
+	    double lastChance = 0;
+	    for (Entry en : OneBlockPhase.CHEST_CHANCES.entrySet()) {
+		// Get the number of chests in this rarity group
+		int num = phase.getChestsMap().getOrDefault(en.getValue(), Collections.emptyList()).size();
+		double likelyNumberGenerated = (en.getKey() - lastChance) * likelyChestTotal;
+		lastChance = en.getKey();
+		String report = num + " " + en.getValue() + " chests in phase. Likely number generated = "
+			+ Math.round(likelyNumberGenerated);
+		if (num > 0 && likelyNumberGenerated < 1) {
+		    addon.logWarning(report);
+		} else {
+		    addon.log(report);
+		}
+
+	    }
+	    // Mobs
+	    addon.log("-=-=-=-= Mobs -=-=-=-=-");
+	    double totalMobs = 0;
+	    // Now calculate the relative block probability
+	    for (Entry en : phase.getMobs().entrySet()) {
+		double chance = (double) en.getValue() / phase.getTotal();
+		double likelyNumberGenerated = chance * phaseSize;
+		totalMobs += likelyNumberGenerated;
+		String report = en.getKey() + " likely generated = " + Math.round(likelyNumberGenerated) + " = "
+			+ Math.round(likelyNumberGenerated * 100 / phaseSize) + "%";
+		if (likelyNumberGenerated < 1) {
+		    addon.logWarning(report);
+		} else {
+		    addon.log(report);
+		}
+	    }
+	    addon.log("**** A total of " + Math.round(totalMobs) + " mobs will likely be generated ****");
+	}
     }
 
     /**
      * Get all the probs for each phases and log to console
      */
     public void getAllProbs() {
-        blockProbs.values().forEach(this::getProbs);
+	blockProbs.values().forEach(this::getProbs);
     }
 
     /**
@@ -814,7 +871,7 @@ public void getAllProbs() {
      * @return next phase name or an empty string
      */
     public String getNextPhase(@NonNull OneBlockIslands obi) {
-        return getPhase(obi.getPhaseName()).map(this::getNextPhase) // Next phase or null
-                .filter(Objects::nonNull).map(OneBlockPhase::getPhaseName).orElse("");
+	return getPhase(obi.getPhaseName()).map(this::getNextPhase) // Next phase or null
+		.filter(Objects::nonNull).map(OneBlockPhase::getPhaseName).orElse("");
     }
 }
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/Requirement.java b/src/main/java/world/bentobox/aoneblock/oneblocks/Requirement.java
index ffb324de..cc4f62e0 100644
--- a/src/main/java/world/bentobox/aoneblock/oneblocks/Requirement.java
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/Requirement.java
@@ -26,7 +26,11 @@ public enum ReqType {
         /**
          * Permission
          */
-        PERMISSION("permission", String.class);
+        PERMISSION("permission", String.class),
+        /**
+         * Cooldown
+         */
+        COOLDOWN("cooldown", Long.class);
 
         private final String key;
         private final Class clazz;
@@ -79,6 +83,13 @@ public String getPermission() {
         return (String)requirement;
     }
 
+    /**
+     * @return the cooldown
+     */
+    public long getCooldown() {
+        return (long)requirement;
+    }
+
     /**
      * @return the type
      */
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/BlockDataCustomBlock.java b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/BlockDataCustomBlock.java
index fb9004df..1f777022 100644
--- a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/BlockDataCustomBlock.java
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/BlockDataCustomBlock.java
@@ -7,6 +7,7 @@
 import org.bukkit.Bukkit;
 import org.bukkit.block.Block;
 
+import world.bentobox.aoneblock.AOneBlock;
 import world.bentobox.aoneblock.oneblocks.OneBlockCustomBlock;
 import world.bentobox.bentobox.BentoBox;
 
@@ -31,7 +32,7 @@ public static Optional fromMap(Map map) {
     }
 
     @Override
-    public void setBlock(Block block) {
+    public void execute(AOneBlock addon, Block block) {
         try {
             block.setBlockData(Bukkit.createBlockData(blockData));
         } catch (IllegalArgumentException e) {
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/ItemsAdderCustomBlock.java b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/ItemsAdderCustomBlock.java
new file mode 100644
index 00000000..34d79235
--- /dev/null
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/ItemsAdderCustomBlock.java
@@ -0,0 +1,42 @@
+package world.bentobox.aoneblock.oneblocks.customblock;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+
+import dev.lone.itemsadder.api.CustomBlock;
+import world.bentobox.aoneblock.AOneBlock;
+import world.bentobox.aoneblock.oneblocks.OneBlockCustomBlock;
+import world.bentobox.bentobox.BentoBox;
+
+public class ItemsAdderCustomBlock implements OneBlockCustomBlock {
+    private final CustomBlock customBlock;
+
+    public ItemsAdderCustomBlock(CustomBlock customBlock) {
+        this.customBlock = customBlock;
+    }
+
+    public static Optional fromId(String id) {
+        return Optional.ofNullable(CustomBlock.getInstance(id)).map(ItemsAdderCustomBlock::new);
+    }
+
+    public static Optional fromMap(Map map) {
+        return Optional
+                .ofNullable(Objects.toString(map.get("id"), null))
+                .flatMap(ItemsAdderCustomBlock::fromId);
+    }
+
+    @Override
+    public void execute(AOneBlock addon, Block block) {
+        try {
+            block.setType(Material.AIR);
+            customBlock.place(block.getLocation());
+        } catch (Exception e) {
+            BentoBox.getInstance().logError("Could not place custom block " + customBlock.getId() + " for block " + block.getType());
+            block.setType(Material.STONE);
+        }
+    }
+}
diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/MobCustomBlock.java b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/MobCustomBlock.java
new file mode 100644
index 00000000..ce03a8cf
--- /dev/null
+++ b/src/main/java/world/bentobox/aoneblock/oneblocks/customblock/MobCustomBlock.java
@@ -0,0 +1,101 @@
+package world.bentobox.aoneblock.oneblocks.customblock;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.Sound;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.util.Vector;
+import org.eclipse.jdt.annotation.NonNull;
+
+import com.google.common.base.Enums;
+
+import world.bentobox.aoneblock.AOneBlock;
+import world.bentobox.aoneblock.listeners.MakeSpace;
+import world.bentobox.aoneblock.oneblocks.OneBlockCustomBlock;
+import world.bentobox.bentobox.BentoBox;
+
+/**
+ * A custom block that spawns mob on an underlying block
+ *
+ * @author Baterka
+ */
+public class MobCustomBlock implements OneBlockCustomBlock {
+    private final EntityType mob;
+    private final Material underlyingBlock;
+
+    public MobCustomBlock(EntityType mob, Material underlyingBlock) {
+        this.mob = mob;
+        this.underlyingBlock = underlyingBlock;
+    }
+
+    public static Optional fromMap(Map map) {
+        String entityTypeValue = Objects.toString(map.get("mob"), null);
+        String underlyingBlockValue = Objects.toString(map.get("underlying-block"), null);
+
+        EntityType entityType = maybeEntity(entityTypeValue);
+        Material underlyingBlock = Material.getMaterial(underlyingBlockValue);
+
+        if (underlyingBlock == null) {
+            BentoBox.getInstance().logWarning("Underlying block " + underlyingBlockValue + " does not exist and will be replaced with STONE.");
+        }
+
+        return Optional.of(new MobCustomBlock(entityType, underlyingBlock));
+    }
+
+    private static EntityType maybeEntity(String entityTypeValue) {
+        String name = entityTypeValue.toUpperCase(Locale.ENGLISH);
+        EntityType et;
+
+        // Pig zombie handling
+        if (name.equals("PIG_ZOMBIE") || name.equals("ZOMBIFIED_PIGLIN")) {
+            et = Enums.getIfPresent(EntityType.class, "ZOMBIFIED_PIGLIN")
+                    .or(Enums.getIfPresent(EntityType.class, "PIG_ZOMBIE").or(EntityType.PIG));
+        } else {
+            et = Enums.getIfPresent(EntityType.class, name).orNull();
+        }
+
+        if (et == null) {
+            // Does not exist
+            BentoBox.getInstance().logWarning("Entity " + name + " does not exist and will not spawn when block is shown.");
+            return null;
+        }
+        if (et.isSpawnable() && et.isAlive()) {
+            return et;
+        } else {
+            // Not spawnable
+            BentoBox.getInstance().logWarning("Entity " + et.name() + " is not spawnable and will not spawn when block is shown.");
+            return null;
+        }
+    }
+
+    @Override
+    public void execute(AOneBlock addon, Block block) {
+        try {
+            block.setType(Objects.requireNonNullElse(underlyingBlock, Material.STONE));
+            spawnEntity(addon, block, mob);
+        } catch (Exception e) {
+            BentoBox.getInstance().logError("Could not spawn entity " + mob.name() + " on block " + block.getType());
+        }
+    }
+
+    private void spawnEntity(AOneBlock addon, @NonNull Block block, @NonNull EntityType mob) {
+        Location spawnLoc = block.getLocation().add(new Vector(0.5D, 1D, 0.5D));
+        Entity entity = block.getWorld().spawnEntity(spawnLoc, mob);
+        // Make space for entity - this will blot out blocks
+        if (addon.getSettings().isClearBlocks()) {
+            new MakeSpace(addon).makeSpace(entity, spawnLoc);
+        }
+        block.getWorld().playSound(block.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1F, 2F);
+    }
+
+    public EntityType getMob() {
+        return mob;
+    }
+}
diff --git a/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java b/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java
index 2eeb76d9..c2643f95 100644
--- a/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java
+++ b/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java
@@ -402,6 +402,10 @@ private PanelItem createPhaseButton(ItemTemplateRecord template, Map.Entry permissionText.append(this.user.getTranslationOrNothing(reference + "permission",
                     PERMISSION, requirement.getPermission()));
+            case COOLDOWN -> {
+                // do nothing
+            }
+            default -> throw new IllegalArgumentException("Unexpected value: " + requirement.getType());
 
             }
         });
@@ -453,7 +457,7 @@ private PanelItem createPhaseButton(ItemTemplateRecord template, Map.Entry this.addon.getPlugin().getVault().map(a -> a.getBalance(this.user, this.world) < requirement.getEco()).orElse(false);
 
             case PERMISSION -> this.user != null && !this.user.hasPermission(requirement.getPermission());
+
+            case COOLDOWN -> (requirement.getCooldown() - (System.currentTimeMillis() - is.getLastPhaseChangeTime()) / 1000) > 0;
             }) {
                 return true;
             }
diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml
index 89bb0291..8b01750d 100644
--- a/src/main/resources/locales/cs.yml
+++ b/src/main/resources/locales/cs.yml
@@ -3,9 +3,10 @@ aoneblock:
   commands:
     admin:
       setcount:
-        parameters: " "
+        parameters: "  [lifetime]"
         description: nastavit počet bloků hráče
         set: "&a počet [name] je nastaven na [number]"
+        set-lifetime: "&a [name] je nastaveno na [number]"
       setchest:
         parameters: " "
         description: dejte pohled na hrudník do fáze se specifikovanou vzácností
@@ -24,6 +25,9 @@ aoneblock:
     count:
       description: zobrazit počet bloků a fázi
       info: "&a Jste na bloku &b [number] ve fázi &a [name]"
+    info:
+      count: Ostrov &a je na bloku &b [number]&a ve fázi &b [name] &a. Počet doživotí
+        &b [lifetime] &a.
     phases:
       description: zobrazit seznam všech fází
       title: "&2 Fáze OneBlock"
@@ -32,16 +36,45 @@ aoneblock:
     island:
       setcount:
         parameters: ""
+        description: nastavte počet bloků na dříve dokončenou hodnotu
         set: "&a Počet nastaven na [number]."
         too-high: "&c Maximálně můžeš nastavit [number]!"
+    respawn-block:
+      description: respawnuje magický blok v situacích, kdy zmizí
+      block-exist: "&a Blok existuje, nevyžadoval respawning. Označil jsem to za vás."
+      block-respawned: "&a Blok byl znovu vytvořen."
   phase:
     insufficient-level: Tvůj ostrov je na příliš nízké úrovni, musí být alespoň [number].
     insufficient-funds: Nemáš dostatečné prostředky! Musíš mít alespoň [number].
     insufficient-bank-balance: V Bance ostrova není dostatek financí! Je potřeba alespoň
       [number].
-    insufficient-permission: Nemůžeš pokračovat, dokud nedostaneš oprávnění [name].
+    insufficient-permission: "&c Nemůžete pokračovat, dokud nezískáte oprávnění [name]!"
+    cooldown: "&c Další fáze bude dostupná za [number] sekund!"
   placeholders:
     infinite: Nekonečný
+  gui:
+    titles:
+      phases: "&0&l Jednoblokové fáze"
+    buttons:
+      previous:
+        name: "&f&l Předchozí stránka"
+        description: "&7 Přepnout na stránku [number]"
+      next:
+        name: "&f&l Další stránka"
+        description: "&7 Přepnout na stránku [number]"
+      phase:
+        name: "&f&l [phase]"
+        description: "[starting-block]\n[biome]\n[bank]\n[economy] \n[level]\n[permission]"
+        starting-block: "&7 Spustí se po rozbití bloků &e [number]."
+        biome: "&7 Biom: &e [biome]"
+        bank: "&7 Vyžaduje &e $[number] &7 na bankovním účtu."
+        economy: "&7 Vyžaduje &e $[number] &7 v hráčském účtu."
+        level: "&7 Vyžaduje &e [number] &7 úroveň ostrova."
+        permission: "&7 Vyžaduje oprávnění `&e[permission]&7`."
+    tips:
+      click-to-previous: "&e Klepnutím na &7 zobrazíte předchozí stránku."
+      click-to-next: "&e Klepnutím na &7 zobrazíte další stránku."
+      click-to-change: "&e Klikněte na &7 pro změnu."
   island:
     starting-hologram: |-
       &a Vítejte v AOneBlock
diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml
index 35902341..ded39ead 100644
--- a/src/main/resources/locales/de.yml
+++ b/src/main/resources/locales/de.yml
@@ -3,9 +3,10 @@ aoneblock:
   commands:
     admin:
       setcount:
-        parameters: " "
+        parameters: " [lifetime]"
         description: Setze die Blockanzahl des Spielers
         set: "&a [name] zählt auf [number]"
+        set-lifetime: Die Lebenszeitanzahl von &a [name] wurde auf [number] gesetzt
       setchest:
         parameters: " "
         description: Versetzen Sie die betrachtete Truhe in eine Phase mit der angegebenen
@@ -28,11 +29,26 @@ aoneblock:
     count:
       description: Zeige die Blockanzahl und Phase
       info: "&a Sie befinden sich in der Phase &a [name] in Block &b [number]"
+    info:
+      count: "&a Island befindet sich in Block &b [number]&a in der Phase &b [Name]
+        &a. Lebenszeitanzahl &b [lifetime] &a."
     phases:
       description: Zeigen Sie eine Liste aller Phasen an
       title: "&2 OneBlock-Phasen"
       name-syntax: "&a [name]"
       description-syntax: "&b [number] Blöcke"
+    island:
+      setcount:
+        parameters: ""
+        description: Setzen Sie die Blockanzahl auf den zuvor abgeschlossenen Wert
+        set: "&a Zähler auf [number] gesetzt."
+        too-high: "&c Das Maximum, das Sie festlegen können, ist [number]!"
+    respawn-block:
+      description: lässt den magischen Block in Situationen wieder erscheinen, in
+        denen er verschwindet
+      block-exist: "&ein Block existiert, musste nicht neu gestartet werden. Ich habe
+        es für dich markiert."
+      block-respawned: "&a Block wieder aufgetaucht."
   phase:
     insufficient-level: "&c Ihr Insellevel ist zu niedrig, um fortzufahren! Es muss
       [number] sein."
@@ -40,10 +56,40 @@ aoneblock:
       [number] sein."
     insufficient-bank-balance: "&c Der Saldo der Inselbank ist zu niedrig, um fortzufahren!
       Es muss [number] sein."
-    insufficient-permission: "&c Sie können nicht weitermachen, bis Sie die Berechtigung
-      [name] erhalten haben!"
+    insufficient-permission: "&c Sie können nicht weitermachen, bis Sie die [name]-Berechtigung
+      erhalten!"
+    cooldown: "&c Die nächste Stufe ist in [number] Sekunden verfügbar!"
   placeholders:
     infinite: Unendlich
+  gui:
+    titles:
+      phases: "&0&l OneBlock-Phasen"
+    buttons:
+      previous:
+        name: "&f&l Vorherige Seite"
+        description: "&7 Zur Seite [number] wechseln"
+      next:
+        name: "&f&l Nächste Seite"
+        description: "&7 Zur Seite [number] wechseln"
+      phase:
+        name: "&f&l [phase]"
+        description: |-
+          [starting-block]
+          [biome]
+          [bank]
+          [economy]
+          [level]
+          [permission]
+        starting-block: "&7 Startet nach dem Aufbrechen von &e [number] Blöcken."
+        biome: "&7 Biom: &e [biome]"
+        bank: "&7 Erfordert &e $[number] &7 auf dem Bankkonto."
+        economy: "&7 Erfordert &e $[number] &7 im Spielerkonto."
+        level: "&7 Erfordert &e [number] &7 Inselebene."
+        permission: "&7 Erfordert die Berechtigung „&e[permission]&7“."
+    tips:
+      click-to-previous: "&e Klicken Sie auf &7, um die vorherige Seite anzuzeigen."
+      click-to-next: "&e Klicken Sie auf &7, um die nächste Seite anzuzeigen."
+      click-to-change: "&e Zum Ändern &7klicken."
   island:
     starting-hologram: |-
       &aWillkommen bei AOneBlock
diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml
index cb9e8570..1d59b20e 100755
--- a/src/main/resources/locales/en-US.yml
+++ b/src/main/resources/locales/en-US.yml
@@ -50,6 +50,7 @@ aoneblock:
     insufficient-funds: "&c Your funds are too low to proceed! They must be [number]."
     insufficient-bank-balance: "&c The island bank balance is too low to proceed! It must be [number]."
     insufficient-permission: "&c You can proceed no further until you obtain the [name] permission!"
+    cooldown: "&c Next phase will be available in [number] seconds!"
   placeholders:
     infinite: Infinite
   gui:
@@ -94,4 +95,4 @@ aoneblock:
       click-to-next: "&e Click &7 to view next page."
       click-to-change: "&e Click &7 to change."
   island:
-    starting-hologram: "&aWelcome to AOneBlock\n&eBreak This Block to Begin"
\ No newline at end of file
+    starting-hologram: "&aWelcome to AOneBlock\n&eBreak This Block to Begin"
diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml
index d2ddcf7e..70705b4c 100644
--- a/src/main/resources/locales/es.yml
+++ b/src/main/resources/locales/es.yml
@@ -3,27 +3,33 @@ aoneblock:
   commands:
     admin:
       setcount:
-        parameters: "  [lifetime]"
-        description: "Establece un número de bloques minados al jugador"
+        parameters: "  [lifetime]"
+        description: Establece el número de bloques minados al jugador
         set: "&aEl número de bloques minados de [name] se ha establecido en [number]"
-        set-lifetime: "&aEl numero de bloques en forma permanente de [name] se ha establecido en [number]"
+        set-lifetime: "&aEl numero de bloques totales de [name] se ha establecido
+          en [number]"
       setchest:
-        parameters: " "
-        description: "Coloca un cofre al que apuntes en una fase con la rareza especificada"
+        parameters: " "
+        description: Coloca el cofre que estas mirando en una fase con la rareza especificada
         chest-is-empty: "&cEse cofre está vacío, así que no se puede agregar"
         unknown-phase: "&cFase desconocida. Presione TAB para verlas"
         unknown-rarity: "&cRareza desconocida. Use COMMON, UNCOMMON, RARE o EPIC"
         look-at-chest: "&cApunta hacia un cofre lleno para configurarlo"
         only-single-chest: "&cSolo se pueden configurar cofres individuales"
         success: "&aEl cofre ha sido agregado con éxito a la fase"
-        failure: "&c¡No se pudo agregar el cofre a la fase! Revisa la consola para más detalles"
+        failure: "&c¡No se pudo agregar el cofre a la fase! Revisa la consola para
+          más detalles"
       sanity:
-        parameters: ""
-        description: "Muestra la proporción detallada de las probabilidades durante las fases en la consola"
+        parameters: ""
+        description: Muestra una comprobación de las probabilidades de la fase en
+          la consola
         see-console: "&aRevisa la consola para ver el informe"
     count:
       description: Muestra el número de bloques minados y la fase correspondiente
       info: "&aTienes &b[number] bloques minados en la fase &a[name]"
+    info:
+      count: "&a Island está en el bloque &b [number]&a en la fase &b [name] &a. Recuento
+        de vida &b [lifetime] &a."
     phases:
       description: Muestra una lista de todas las fases
       title: "&2Fases de OneBlock"
@@ -31,16 +37,55 @@ aoneblock:
       description-syntax: "&b[number] bloques"
     island:
       setcount:
-        parameters: ""
-        description: "Establece la cantidad de bloques a un valor previamente completado"
+        parameters: ""
+        description: Establece la cantidad de bloques a un valor previamente completado
         set: "&aCantidad establecida en [number]."
         too-high: "&c¡Lo máximo que puedes establecer es [number]!"
+    respawn-block:
+      description: reaparece el bloque mágico en situaciones en las que desaparece
+      block-exist: "&a Block existe, no requirió reaparición. Te lo marqué."
+      block-respawned: "& un bloque reapareció."
   phase:
-    insufficient-level: "&c¡Tu nivel de isla es demasiado bajo para seguir! Este debe ser de [number]."
+    insufficient-level: "&c¡Tu nivel de isla es demasiado bajo para seguir! Este debe
+      ser de [number]."
     insufficient-funds: "&c¡Tus fondos son insuficientes! Debes tener [number]."
-    insufficient-bank-balance: "&c¡El dinero del banco en la isla es demasiado bajo para seguir! Debes tener [number]."
-    insufficient-permission: "&cNo puedes seguir hasta conseguir el permiso [name]"
+    insufficient-bank-balance: "&c¡El dinero del banco en la isla es demasiado bajo
+      para seguir! Debes tener [number]."
+    insufficient-permission: "&c ¡No puede continuar hasta que obtenga el permiso
+      de [name]!"
+    cooldown: "&c ¡La siguiente etapa estará disponible en [number] segundos!"
   placeholders:
     infinite: Infinito
+  gui:
+    titles:
+      phases: "&0&l Fases de OneBlock"
+    buttons:
+      previous:
+        name: "&f&l Pagina Anterior"
+        description: "&7 Ir a la pagina [number]"
+      next:
+        name: "&f&l Siguiente pagina"
+        description: "&7 Ir a la pagina [number]"
+      phase:
+        name: "&f&l [phase]"
+        description: |-
+          [starting-block]
+          [biome]
+          [bank]
+          [economy]
+          [level]
+          [permission]
+        starting-block: "&7 Comienza tras romper &e [number] bloques."
+        biome: "&7 Bioma: &e [biome]"
+        bank: "&7 Requiere &e $[number] &7 en la cuenta del banco."
+        economy: "&7 Requiere &e $[number] &7 en la cuenta del jugador."
+        level: "&7 Requiere &e [number] &7 nivel de isla."
+        permission: "&7 Requiere permiso `&e[permission]&7`."
+    tips:
+      click-to-previous: "&e Click &7 para ver pagina anterior."
+      click-to-next: "&e Click &7 para ver pagina siguiente."
+      click-to-change: "&e Click &7 para cambiar."
   island:
-    starting-hologram: "&aBienvenido a AOneBlock\n&eRompe este bloque para empezar"
+    starting-hologram: |-
+      &aBienvenido a AOneBlock
+      &eRompe este bloque para empezar
diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml
index 792580db..f7e6cf2d 100644
--- a/src/main/resources/locales/fr.yml
+++ b/src/main/resources/locales/fr.yml
@@ -5,7 +5,7 @@ aoneblock:
       setcount:
         parameters: "  [DuréeDeVie]"
         description: Définir le nombre de blocks du joueur
-        set: "&a Le compte de [nom] est défini sur [nombre]."
+        set: "&a Le compte de [name] est défini sur [number]."
         set-lifetime: "&a La durée de vie de [name] est de [number]"
       setchest:
         parameters: " "
@@ -26,6 +26,9 @@ aoneblock:
     count:
       description: afficher le nombre de blocs et la phase
       info: "&a Vous êtes sur le bloc &b [number] dans la phase &a [name]"
+    info:
+      count: "&a L'île est sur le bloc &b [number]&a dans la phase &b [name] &a. Nombre
+        de durée de vie &b [lifetime] &a."
     phases:
       description: afficher une liste de toutes les phases
       title: "&2 Phases OneBlock"
@@ -38,10 +41,10 @@ aoneblock:
         set: "&a Nombre défini sur [number]."
         too-high: "&c Le maximum que vous pouvez définir est [number] !"
     respawn-block:
-      description: Force la réapparition du bloc magique dans le cas ou il disparait
-      block-exist: "&a Le bloc magique existe déja, inutile de le faire réapparaitre.
-        Je l'ai marqué pour toi."
-      block-respawned: "&a Bloc magique réapparu, s'il te plait, ne recommence pas."
+      description: réapparaît le bloc magique dans les situations où il disparaît
+      block-exist: "&un bloc existe, n'a pas nécessité de réapparition. Je l'ai noté
+        pour toi."
+      block-respawned: "&un bloc réapparu."
   phase:
     insufficient-level: Ton niveau d'île est trop bas ! Il doit être de [number] au
       minimum.
@@ -49,8 +52,9 @@ aoneblock:
       [number].
     insufficient-bank-balance: Ta banque d'île n'a pas les fonds nécessaire ! Vous
       devez au moins avoir [number].
-    insufficient-permission: 'Tu ne peux plus avancer, il te manque la permission
-      : [name] !'
+    insufficient-permission: "&c Vous ne pouvez pas continuer jusqu'à ce que vous
+      obteniez l'autorisation de [name] !"
+    cooldown: "&c La prochaine étape sera disponible dans [number] secondes!"
   placeholders:
     infinite: Infini
   gui:
@@ -66,12 +70,12 @@ aoneblock:
       phase:
         name: "&f&l [phase]"
         description: |-
-          [Bloc de départ]
+          [starting-block]
           [biome]
-          [banque]
-          [économie]
-          [niveau]
-          [autorisation]
+          [bank]
+          [economy]
+          [level]
+          [permission]
         starting-block: "&7 Commence après avoir détruit &e [number] blocs."
         biome: "&7 Biome : &e [biome]"
         bank: "&7 Requiert &e $[number] &7 dans ta banque."
diff --git a/src/main/resources/locales/hr.yml b/src/main/resources/locales/hr.yml
index 0704926e..49b02a87 100644
--- a/src/main/resources/locales/hr.yml
+++ b/src/main/resources/locales/hr.yml
@@ -6,6 +6,7 @@ aoneblock:
         parameters: " "
         description: postavljanje broja blokova igrača
         set: "&a broj [name] postavljen je na [number]"
+        set-lifetime: "&broj životnog vijeka [name] postavljen na [number]"
       setchest:
         parameters: " "
         description: stavite pregledani sanduk u fazu s specificiranom rijetkošću
@@ -24,8 +25,64 @@ aoneblock:
     count:
       description: prikazuju broj i fazu bloka
       info: "&a Nalazite se na bloku &b [number] u fazi &a [name]"
+    info:
+      count: "&a Otok je u bloku &b [number]&a u &b [name] &a fazi. Životni vijek
+        &b [lifetime] &a."
     phases:
       description: prikažite popis svih faza
       title: "&2 OneBlock Faze"
       name-syntax: "&a [name]"
       description-syntax: "&b [number] blokova"
+    island:
+      setcount:
+        parameters: ""
+        description: postaviti broj blokova na prethodno dovršenu vrijednost
+        set: "&a Brojanje postavljeno na [number]."
+        too-high: "&c Maksimalno što možete postaviti je [number]!"
+    respawn-block:
+      description: ponovno rađa magični blok u situacijama kada nestane
+      block-exist: "&a blok postoji, nije zahtijevao ponovno stvaranje. Označila sam
+        za tebe."
+      block-respawned: "&a blok se ponovno pojavio."
+  phase:
+    insufficient-level: "&c Vaša razina otoka je preniska za nastavak! Mora biti [number]."
+    insufficient-funds: "&c Vaša su sredstva premala za nastavak! Moraju biti [number]."
+    insufficient-bank-balance: "&c Stanje otočne banke je premalo za nastavak! Mora
+      biti [number]."
+    insufficient-permission: "&c Ne možete nastaviti dok ne dobijete dopuštenje [name]!"
+    cooldown: "&c Sljedeća faza bit će dostupna za [number] sekundi!"
+  placeholders:
+    infinite: Beskonačno
+  gui:
+    titles:
+      phases: "&0&l OneBlock faze"
+    buttons:
+      previous:
+        name: "&f&l Prethodna stranica"
+        description: "&7 Prijeđi na stranicu [number]."
+      next:
+        name: "&f&l Sljedeća stranica"
+        description: "&7 Prijeđi na stranicu [number]."
+      phase:
+        name: "&f&l [phase]"
+        description: |-
+          [starting-block]
+          [biome]
+          [bank]
+          [economy]
+          [level]
+          [permission]
+        starting-block: "&7 Počinje nakon razbijanja &e [number] blokova."
+        biome: "&7 Biome: &e [biome]"
+        bank: "&7 Zahtijeva &e $[number] &7 na bankovnom računu."
+        economy: "&7 Zahtijeva &e $[number] &7 na računu igrača."
+        level: "&7 Zahtijeva &e [number] &7 razinu otoka."
+        permission: "&7 Zahtijeva dozvolu `&e[permission]&7`."
+    tips:
+      click-to-previous: "&e Kliknite &7 za pregled prethodne stranice."
+      click-to-next: "&e Kliknite &7 za pregled sljedeće stranice."
+      click-to-change: "&e Kliknite &7 za promjenu."
+  island:
+    starting-hologram: |-
+      &aDobro došli u AOneBlock
+      &eRazbijte ovaj blok za početak
diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml
index 5ea83162..6a692b8b 100644
--- a/src/main/resources/locales/hu.yml
+++ b/src/main/resources/locales/hu.yml
@@ -5,11 +5,11 @@ aoneblock:
       setcount:
         parameters: " "
         description: állítsa be a játékos blokkszámát
-        set: "&a [name] számának beállítása [number]"
+        set: "&a [name] számának beállítása erre: [number]"
+        set-lifetime: "&a [name] élettartama a következőre van állítva: [number]"
       setchest:
         parameters: " "
         description: helyezze a nézett mellkasát egy szakaszba a megadott ritkasággal
-        success: és egy mellkas sikeresen hozzáadva a fázishoz
         chest-is-empty: "&c A mellkas üres, ezért nem adható hozzá"
         unknown-phase: "&c Ismeretlen fázis. A Tab-Complete használatával megtekintheti
           őket"
@@ -17,6 +17,7 @@ aoneblock:
           vagy EPIC"
         look-at-chest: "&c Nézzen meg egy töltött mellkasat, hogy beállítsa"
         only-single-chest: "&c Csak egyetlen ládát lehet beállítani"
+        success: és egy mellkas sikeresen hozzáadva a fázishoz
         failure: "&c A mellkas nem adható hozzá a fázishoz! A hibákat lásd a konzolon"
       sanity:
         parameters: ""
@@ -26,8 +27,67 @@ aoneblock:
     count:
       description: mutassa meg a blokkok számát és a fázist
       info: "&a Ön a &b [number] blokkban van a &a [name] fázisban"
+    info:
+      count: Az &a sziget a &b [number]&a blokkon található, a &b [name] &a fázisban.
+        Élettartam száma &b [lifetime] &a.
     phases:
       description: az összes fázis felsorolása
       title: "&2 OneBlock Fázis"
       name-syntax: "&a [name]"
       description-syntax: "&b [number] blokkolja"
+    island:
+      setcount:
+        parameters: ""
+        description: állítsa be a blokkszámot a korábban kitöltött értékre
+        set: "&a A számláló értéke [szám]."
+        too-high: "&c A beállítható maximum [number]!"
+    respawn-block:
+      description: varázsblokkot hoz újra olyan helyzetekben, amikor eltűnik
+      block-exist: "&a Blokk létezik, nem igényelt újbóli megjelenést. megjelöltem
+        neked."
+      block-respawned: "&a blokk újjáéledt."
+  phase:
+    insufficient-level: "&c A sziget szintje túl alacsony a folytatáshoz! Ennek a
+      következőnek kell lennie: [number]."
+    insufficient-funds: "&c A kerete túl kevés a folytatáshoz! Ezeknek [number]-nak
+      kell lenniük."
+    insufficient-bank-balance: "&c A sziget banki egyenlege túl alacsony a folytatáshoz!
+      Ennek a következőnek kell lennie: [number]."
+    insufficient-permission: "&c Nem folytathatja tovább, amíg meg nem szerzi a [name]
+      engedélyt!"
+    cooldown: "&c A következő szakasz [number] másodpercen belül elérhető lesz!"
+  placeholders:
+    infinite: Végtelen
+  gui:
+    titles:
+      phases: "&0&l OneBlock fázisok"
+    buttons:
+      previous:
+        name: "&f&l Előző oldal"
+        description: "&7 Váltás a [number] oldalra"
+      next:
+        name: "&f&l Következő oldal"
+        description: "&7 Váltás a [number] oldalra"
+      phase:
+        name: "&f&l [phase]"
+        description: |-
+          [starting-block]
+          [biome]
+          [bank]
+          [economy]
+          [level]
+          [permission]
+        starting-block: "&7 Az &e [number] blokk feltörése után indul."
+        biome: "&7 életrajz: &e [biome]"
+        bank: "&7 Szükséges &e $[number] &7 bankszámlára."
+        economy: "&7 &e $[number] &7 játékos fiókot igényel."
+        level: "&7 &e [number] &7 szigetszint szükséges."
+        permission: "&7 `&e[permission]&7` engedély szükséges."
+    tips:
+      click-to-previous: "&e Kattintson a &7 gombra az előző oldal megtekintéséhez."
+      click-to-next: "&e Kattintson a &7 gombra a következő oldal megtekintéséhez."
+      click-to-change: "&e Kattintson a &7 gombra a módosításhoz."
+  island:
+    starting-hologram: |-
+      &aÜdvözlünk az AOneBlockban
+      &eSzüntesse meg ezt a blokkot a kezdéshez
diff --git a/src/main/resources/locales/ja.yml b/src/main/resources/locales/ja.yml
index 7e6dc052..f8135c54 100644
--- a/src/main/resources/locales/ja.yml
+++ b/src/main/resources/locales/ja.yml
@@ -2,29 +2,84 @@
 aoneblock:
   commands:
     admin:
-      sanity:
-        description: コンソールに位相確率の健全性チェックを表示する
-        parameters: "<フェーズ>"
-        see-console: "&aコンソールでレポートを表示"
+      setcount:
+        parameters: "<名前> <数>"
+        description: プレイヤーのブロック数を設定する
+        set: "[name]の数が[number]に設定されました"
+        set-lifetime: "&a [name] の有効期間カウントが [number] に設定されました"
       setchest:
-        chest-is-empty: "&cそのチェストは空なので追加できません"
+        parameters: "<フェーズ> <レア度>"
         description: 見つめられた胸部を、指定された希少性を持つフェーズに置く
-        failure: "&cチェストをフェーズに追加できませんでした!エラーについてはコンソールを参照してください"
+        chest-is-empty: "&cそのチェストは空なので追加できません"
+        unknown-phase: "&c不明なフェーズ。タブコンプリートを使用して表示します"
+        unknown-rarity: "&c希少性は不明です。 COMMON、UNCOMMON、RARE、またはEPICを使用します"
         look-at-chest: "&c満たされた箱を見てそれを設定します"
         only-single-chest: "&c単一のチェストのみを設定できます"
-        parameters: "<フェーズ> <レア度>"
         success: "&a胸部がフェーズに追加されました"
-        unknown-phase: "&c不明なフェーズ。タブコンプリートを使用して表示します"
-        unknown-rarity: "&c希少性は不明です。 COMMON、UNCOMMON、RARE、またはEPICを使用します"
-      setcount:
-        description: プレイヤーのブロック数を設定する
-        parameters: "<名前> <数>"
-        set: "[name]の数が[number]に設定されました"
+        failure: "&cチェストをフェーズに追加できませんでした!エラーについてはコンソールを参照してください"
+      sanity:
+        parameters: "<フェーズ>"
+        description: コンソールに位相確率の健全性チェックを表示する
+        see-console: "&aコンソールでレポートを表示"
     count:
       description: ブロック数とフェーズを表示する
       info: "[name]フェーズのブロック[number]にいます"
+    info:
+      count: "&a 島は &b [name] &a フェーズのブロック &b [number]&a 上にあります。生涯カウント &b [lifetime]
+        &a。"
     phases:
       description: すべてのフェーズのリストを表示する
       title: "&2 OneBlockフェーズ"
       name-syntax: "&a[name]"
       description-syntax: "&b [number]ブロック"
+    island:
+      setcount:
+        parameters: "<カウント>"
+        description: ブロック数を以前に完了した値に設定する
+        set: "&a カウントを [数値] に設定します。"
+        too-high: "&c 設定できる最大値は [number] です!"
+    respawn-block:
+      description: マジックブロックが消えた場合に再出現します
+      block-exist: "&a ブロックが存在します。再生成は必要ありませんでした。私はあなたのためにそれをマークしました。"
+      block-respawned: "&a ブロックが復活しました。"
+  phase:
+    insufficient-level: "&c 島のレベルが低すぎるので先に進めません! [number] である必要があります。"
+    insufficient-funds: "&c 資金が少なすぎるため続行できません。 [number] である必要があります。"
+    insufficient-bank-balance: "&c 島の銀行残高が少なすぎるため続行できません。 [number] である必要があります。"
+    insufficient-permission: "&c [name] の許可を取得するまで、これ以上先に進むことはできません。"
+    cooldown: "&c [number] 秒で次のステージへ!"
+  placeholders:
+    infinite: 無限
+  gui:
+    titles:
+      phases: "&0&l ワンブロックフェーズ"
+    buttons:
+      previous:
+        name: "&f&l 前のページ"
+        description: "&7 [number]ページに切り替えます"
+      next:
+        name: "&f&l 次のページ"
+        description: "&7 [番号]ページに切り替えます"
+      phase:
+        name: "&f&l [phase]"
+        description: |-
+          [starting-block]
+          [biome]
+          [bank]
+          [economy]
+          [level]
+          [permission]
+        starting-block: "&7 &e [number] ブロックを分割した後に開始します。"
+        biome: "&7 バイオーム: &e [biome]"
+        bank: "&7 銀行口座に &e $[number] &7 が必要です。"
+        economy: "&7 プレイヤーアカウントに &e $[number] &7 が必要です。"
+        level: "&7 &e [number] &7 の島レベルが必要です。"
+        permission: "&7 `&e[permission]&7` 権限が必要です。"
+    tips:
+      click-to-previous: "&e &7 をクリックして前のページを表示します。"
+      click-to-next: "&e &7 をクリックして次のページを表示します。"
+      click-to-change: "&e &7 をクリックして変更します。"
+  island:
+    starting-hologram: |-
+      &aAOneBlock へようこそ
+      &eこのブロックを壊して開始してください
diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml
index bbd44532..5f1e563e 100644
--- a/src/main/resources/locales/pl.yml
+++ b/src/main/resources/locales/pl.yml
@@ -48,6 +48,7 @@ aoneblock:
       aby kontynuować! Musi to być [number].
     insufficient-permission: Nie możesz kontynuować, dopóki nie uzyskasz pozwolenia
       [name]!
+    cooldown: "&c Następny etap będzie dostępny za [number] sekund!"
   placeholders:
     infinite: Nieskończony
   gui:
diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml
index b6ce370f..071b4a88 100644
--- a/src/main/resources/locales/ru.yml
+++ b/src/main/resources/locales/ru.yml
@@ -51,6 +51,7 @@ aoneblock:
       Должно быть [number]."
     insufficient-permission: "&c Вы не можете продолжать, пока не получите разрешение
       [name]!"
+    cooldown: "&c Следующий этап будет доступен через [number] секунд!"
   placeholders:
     infinite: Бесконечный
   gui:
diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml
index 0adccb2b..10ef4b4c 100644
--- a/src/main/resources/locales/tr.yml
+++ b/src/main/resources/locales/tr.yml
@@ -43,6 +43,7 @@ aoneblock:
     insufficient-bank-balance: "&c Ada bankası bakiyesi devam etmek için çok düşük!
       [number] olmalıdır."
     insufficient-permission: "&c [name] iznini alana kadar devam edemezsiniz!"
+    cooldown: "&c Bir sonraki aşama [number] saniye içinde hazır olacak!"
   placeholders:
     infinite: Sonsuz
   island:
diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml
new file mode 100644
index 00000000..25a1aa56
--- /dev/null
+++ b/src/main/resources/locales/uk.yml
@@ -0,0 +1,91 @@
+---
+aoneblock:
+  commands:
+    admin:
+      setcount:
+        parameters: "  [lifetime]"
+        description: встановити кількість блоків гравця
+        set: "&a [name] встановлено значення [number]"
+        set-lifetime: "&a [name] тривалість життя встановлено на [number]"
+      setchest:
+        parameters: " "
+        description: поставити скриню, на яку дивляться, у фазу з указаною рідкістю
+        chest-is-empty: "&c Ця скриня порожня, тому її неможливо додати"
+        unknown-phase: "&c Невідома фаза. Використовуйте Tab-complete, щоб побачити
+          їх"
+        unknown-rarity: "&c Невідома рідкість. Використовуйте COMMON, UNCOMMON, RARE
+          або EPIC"
+        look-at-chest: "&c Подивіться на заповнену скриню, щоб встановити її"
+        only-single-chest: "&c Можна встановити лише окремі скрині"
+        success: "& Скриню успішно додано до фази"
+        failure: "&c Скриня не може бути додана до фази! Перегляньте консоль для помилок"
+      sanity:
+        parameters: ""
+        description: відобразити перевірку працездатності ймовірностей фази на консолі
+        see-console: "&a Дивіться консоль для звіту"
+    count:
+      description: показати кількість блоків і фазу
+      info: "&a Ви знаходитесь у блоці &b [number] у фазі &a [name]."
+    info:
+      count: "&a Острів знаходиться на блоці &b [number]&a у фазі &b [name] &a. Підрахунок
+        тривалості життя &b [lifetime] &a."
+    phases:
+      description: показати список усіх фаз
+      title: "&2 OneBlock фази"
+      name-syntax: "&a [name]"
+      description-syntax: "&b [number] блоків"
+    island:
+      setcount:
+        parameters: ""
+        description: встановити кількість блоків до попередньо завершеного значення
+        set: "&a Лічильник встановлено на [number]."
+        too-high: "&c Максимум, який ви можете встановити, це [number]!"
+    respawn-block:
+      description: відроджує магічний блок у ситуаціях, коли він зникає
+      block-exist: "&a Блок існує, не потребує відновлення. Я позначив це для вас."
+      block-respawned: "& Блок відродився."
+  phase:
+    insufficient-level: "&c Рівень вашого острова занадто низький, щоб продовжити!
+      Це має бути [number]."
+    insufficient-funds: "&c Ваших коштів занадто мало, щоб продовжити! Вони мають
+      бути [number]."
+    insufficient-bank-balance: "&c Баланс острівного банку занадто низький, щоб продовжити!
+      Це має бути [number]."
+    insufficient-permission: "&c Ви не можете продовжувати далі, доки не отримаєте
+      дозвіл [name]!"
+    cooldown: "&c Наступна фаза буде доступна через [number] секунд!"
+  placeholders:
+    infinite: Нескінченний
+  gui:
+    titles:
+      phases: "&0&l Фази одного блоку"
+    buttons:
+      previous:
+        name: "&f&l Попередня сторінка"
+        description: "&7 Перейти на сторінку [number]."
+      next:
+        name: "&f&l Наступна сторінка"
+        description: "&7 Перейти на сторінку [number]."
+      phase:
+        name: "&f&l [phase]"
+        description: |-
+          [starting-block]
+          [biome]
+          [bank]
+          [economy]
+          [level]
+          [permission]
+        starting-block: "&7 Запускається після розбиття &e [number] блоків."
+        biome: "&7 Біом: &e [biome]"
+        bank: "&7 Потрібен &e $[number] &7 на банківському рахунку."
+        economy: "&7 Потрібен &e $[number] &7 в обліковому записі гравця."
+        level: "&7 Потрібен рівень острова &e [number] &7."
+        permission: "&7 Потрібен дозвіл `&e[permission]&7`."
+    tips:
+      click-to-previous: "&e Натисніть &7, щоб переглянути попередню сторінку."
+      click-to-next: "&e Натисніть &7, щоб переглянути наступну сторінку."
+      click-to-change: "&e Натисніть &7, щоб змінити."
+  island:
+    starting-hologram: |-
+      &aЛаскаво просимо до AOneBlock
+      &eРозбийте цей блок, щоб почати
diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml
index 62035374..09f4638b 100644
--- a/src/main/resources/locales/vi.yml
+++ b/src/main/resources/locales/vi.yml
@@ -44,6 +44,7 @@ aoneblock:
     insufficient-bank-balance: Ngân hàng đảo của bạn quá thấp để thực thi! Nó phải
       là [number].
     insufficient-permission: Bạn không thể thực thi tiếp cho đến khi có quyền [name]!
+    cooldown: Giai đoạn tiếp theo sẽ có sau [number] giây!
   placeholders:
     infinite: Vô hạn
   island:
diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml
index f140358e..c1767d79 100644
--- a/src/main/resources/locales/zh-CN.yml
+++ b/src/main/resources/locales/zh-CN.yml
@@ -24,6 +24,8 @@ aoneblock:
     count:
       description: 显示方块数量和阶段
       info: "&a 您当前挖掘的方块数量是 &b [number], 为 &a [name] 阶段"
+    info:
+      count: "&a 岛位于 &b [name] &a 阶段的 &b [number]&a 区块。生命周期计数 &b [lifetime] &a。"
     phases:
       description: 显示所有阶段的列表
       title: "&2 OneBlock 阶段"
@@ -35,13 +37,47 @@ aoneblock:
         description: 将块计数设置为先前完成的值
         set: "&a 数量设置为 [number]."
         too-high: "&c 你最大只能设置 [number]!"
+    respawn-block:
+      description: 在魔法块消失的情况下重生
+      block-exist: "&a 块存在,不需要重生。我给你标记了。"
+      block-respawned: "&a 块重生。"
   phase:
     insufficient-level: "&c 岛屿等级过低, 无法执行此操作! 等级必须达到 [number]."
     insufficient-funds: "&c 余额不足, 无法执行此操作! 余额应多于 [number]."
     insufficient-bank-balance: "&c 岛屿银行余额不足, 无法执行此操作! 余额应多于 [number]."
-    insufficient-permission: "&c 您不能继续执行操作了, 因为没有 [name] 权限!"
+    insufficient-permission: "&c 在获得 [name] 许可之前,您不能继续操作!"
+    cooldown: "&c [number] 秒后即可进入下一阶段!"
   placeholders:
     infinite: 无限
+  gui:
+    titles:
+      phases: "&0&l OneBlock 阶段"
+    buttons:
+      previous:
+        name: "&f&l 上一页"
+        description: "&7 切换到[number]页"
+      next:
+        name: "&f&l 下一页"
+        description: "&7 切换到[number]页"
+      phase:
+        name: "&f&l [phase]"
+        description: |-
+          [starting-block]
+          [biome]
+          [bank]
+          [economy]
+          [level]
+          [permission]
+        starting-block: "&7 在破坏 &e [number] 块后开始。"
+        biome: "&7 生物群落:&e [biome]"
+        bank: "&7 需要银行帐户中有 &e $[number] &7。"
+        economy: "&7 需要玩家帐户中有 &e $[number] &7。"
+        level: "&7 需要 &e [number] &7 岛屿等级。"
+        permission: "&7 需要 `&e[permission]&7` 权限。"
+    tips:
+      click-to-previous: "&e 单击&7 查看上一页。"
+      click-to-next: "&e 单击 &7 查看下一页。"
+      click-to-change: "&e 单击 &7 进行更改。"
   island:
     starting-hologram: |-
       &a欢迎来到 AOneBlock
diff --git a/src/main/resources/locales/zh-TW.yml b/src/main/resources/locales/zh-TW.yml
index e7147e6d..4565d1ab 100644
--- a/src/main/resources/locales/zh-TW.yml
+++ b/src/main/resources/locales/zh-TW.yml
@@ -6,6 +6,7 @@ aoneblock:
         parameters: "<名稱> <計數>"
         description: 設置玩家的蓋帽數
         set: a [name]的計數設置為[number]
+        set-lifetime: "&a [name] 的生命週期計數設定為 [number]"
       setchest:
         parameters: "<階段> <稀有>"
         description: 將所看的箱子放在指定稀有度的階段
@@ -23,8 +24,61 @@ aoneblock:
     count:
       description: 顯示塊數和相位
       info: "&a您正在&a[name]]階段中阻止&b[number]"
+    info:
+      count: "&a 島位於 &b [names] &a 階段的 &b [number]&a 區塊。生命週期計數 &b [lifetime] &a。"
     phases:
       description: 顯示所有階段的列表
       title: "&2 OneBlock階段"
       name-syntax: "&a [name]"
       description-syntax: "&b [number]塊"
+    island:
+      setcount:
+        parameters: "<計數>"
+        description: 將區塊計數設定為之前完成的值
+        set: "&a 計數設定為 [number]。"
+        too-high: "&c 您可以設定的最大值是[number]!"
+    respawn-block:
+      description: 在魔法塊消失的情況下重生
+      block-exist: "&a 塊存在,不需要重生。我給你標記了。"
+      block-respawned: "&a 塊重生。"
+  phase:
+    insufficient-level: "&c 你的島嶼等級太低,無法繼續!必須是[number]。"
+    insufficient-funds: "&c 您的資金太低,無法繼續!他們必須是[數字]。"
+    insufficient-bank-balance: "&c 島上銀行餘額太低,無法繼續!必須是[number]。"
+    insufficient-permission: "&c 在獲得 [name] 許可之前,您不能繼續操作!"
+    cooldown: "&c [number] 秒後即可進入下一階段!"
+  placeholders:
+    infinite: 無窮
+  gui:
+    titles:
+      phases: "&0&l OneBlock 階段"
+    buttons:
+      previous:
+        name: "&f&l 上一頁"
+        description: "&7 切換到[number]頁"
+      next:
+        name: "&f&l 下一頁"
+        description: "&7 切換到[number]頁"
+      phase:
+        name: "&f&l [階段]"
+        description: |-
+          [starting-block]
+          [biome]
+          [bank]
+          [economy]
+          [level]
+          [permission]
+        starting-block: "&7 在破壞 &e [number] 區塊後開始。"
+        biome: "&7 生物群落:&e [biome]"
+        bank: "&7 需要銀行帳戶中有 &e $[number] &7。"
+        economy: "&7 需要玩家帳號中有 &e $[number] &7。"
+        level: "&7 需要 &e [number] &7 島嶼等級。"
+        permission: "&7 需要 `&e[permission]&7` 權限。"
+    tips:
+      click-to-previous: "&e 點選&7 查看上一頁。"
+      click-to-next: "&e 點選&7 查看下一頁。"
+      click-to-change: "&e 點選 &7 進行更改。"
+  island:
+    starting-hologram: |-
+      &a歡迎來到 AOneBlock
+      打破此區塊以開始(&E)
diff --git a/src/main/resources/phases/0_plains.yml b/src/main/resources/phases/0_plains.yml
index 588a8011..1d184a41 100644
--- a/src/main/resources/phases/0_plains.yml
+++ b/src/main/resources/phases/0_plains.yml
@@ -56,6 +56,7 @@
   #   bank-balance: 10000
   #   level: 10
   #   permission: ready.for.battle
+  #   cooldown: 60 # seconds
   
   blocks:
     PODZOL: 40
diff --git a/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java b/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java
index 3f77cab5..9d588260 100644
--- a/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java
+++ b/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java
@@ -12,17 +12,20 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.beans.IntrospectionException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.jar.JarEntry;
 import java.util.jar.JarOutputStream;
 import java.util.logging.Logger;
@@ -34,6 +37,7 @@
 import org.eclipse.jdt.annotation.NonNull;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -51,6 +55,8 @@
 import world.bentobox.bentobox.api.addons.AddonDescription;
 import world.bentobox.bentobox.api.configuration.Config;
 import world.bentobox.bentobox.api.user.User;
+import world.bentobox.bentobox.database.AbstractDatabaseHandler;
+import world.bentobox.bentobox.database.DatabaseSetup;
 import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType;
 import world.bentobox.bentobox.database.objects.Island;
 import world.bentobox.bentobox.managers.AddonsManager;
@@ -65,268 +71,283 @@
  *
  */
 @RunWith(PowerMockRunner.class)
-@PrepareForTest({Bukkit.class, BentoBox.class, User.class, Config.class })
+@PrepareForTest({ Bukkit.class, BentoBox.class, User.class, Config.class, DatabaseSetup.class })
 public class AOneBlockTest {
 
-    @Mock
-    private User user;
-    @Mock
-    private IslandsManager im;
-    @Mock
-    private Island island;
-
-    private AOneBlock addon;
-    @Mock
-    private BentoBox plugin;
-    @Mock
-    private FlagsManager fm;
-    @Mock
-    private Settings settings;
-    @Mock
-    private PlaceholdersManager phm;
-
-    /**
-     * @throws java.lang.Exception
-     */
-    @Before
-    public void setUp() throws Exception {
-        // Set up plugin
-        Whitebox.setInternalState(BentoBox.class, "instance", plugin);
-        when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
-        
-        // The database type has to be created one line before the thenReturn() to work!
-        DatabaseType value = DatabaseType.JSON;
-        when(plugin.getSettings()).thenReturn(settings);
-        when(settings.getDatabaseType()).thenReturn(value);
-        when(plugin.getPlaceholdersManager()).thenReturn(phm);
-        // Placeholders
-        when(phm.replacePlaceholders(any(), anyString())).thenAnswer(a -> (String)a.getArgument(1, String.class));
-
-        
-        // Command manager
-        CommandsManager cm = mock(CommandsManager.class);
-        when(plugin.getCommandsManager()).thenReturn(cm);
-
-        // Player
-        Player p = mock(Player.class);
-        // Sometimes use Mockito.withSettings().verboseLogging()
-        when(user.isOp()).thenReturn(false);
-        UUID uuid = UUID.randomUUID();
-        when(user.getUniqueId()).thenReturn(uuid);
-        when(user.getPlayer()).thenReturn(p);
-        when(user.getName()).thenReturn("tastybento");
-        User.setPlugin(plugin);
-
-        // Island World Manager
-        IslandWorldManager iwm = mock(IslandWorldManager.class);
-        when(plugin.getIWM()).thenReturn(iwm);
-
-
-        // Player has island to begin with
-        island = mock(Island.class);
-        when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island);
-        when(plugin.getIslands()).thenReturn(im);
-
-        // Locales
-        // Return the reference (USE THIS IN THE FUTURE)
-        when(user.getTranslation(Mockito.anyString())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class));
-
-        // Server
-        PowerMockito.mockStatic(Bukkit.class);
-        Server server = mock(Server.class);
-        when(Bukkit.getServer()).thenReturn(server);
-        when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger());
-        when(Bukkit.getPluginManager()).thenReturn(mock(PluginManager.class));
-
-        // Create an Addon
-        addon = new AOneBlock();
-        File jFile = new File("addon.jar");
-        try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) {
-
-            // Copy over config file from src folder
-            Path fromPath = Paths.get("src/main/resources/config.yml");
-            Path path = Paths.get("config.yml");
-            Files.copy(fromPath, path);
-
-            //Add the new files to the jar.
-            add(path, tempJarOutputStream);
-
-            // Copy over panels file from src folder
-            fromPath = Paths.get("src/main/resources/panels/phases_panel.yml");
-            path = Paths.get("panels");
-            Files.createDirectory(path);
-            path = Paths.get("panels/phases_panel.yml");
-            Files.copy(fromPath, path);
-
-            //Add the new files to the jar.
-            add(path, tempJarOutputStream);
-        }
-
-        File dataFolder = new File("addons/AOneBlock");
-        addon.setDataFolder(dataFolder);
-        addon.setFile(jFile);
-        AddonDescription desc = new AddonDescription.Builder("bentobox", "aoneblock", "1.3").description("test").authors("tasty").build();
-        addon.setDescription(desc);
-        // Addons manager
-        AddonsManager am = mock(AddonsManager.class);
-        when(plugin.getAddonsManager()).thenReturn(am);
-
-        // Flags manager
-        when(plugin.getFlagsManager()).thenReturn(fm);
-        when(fm.getFlags()).thenReturn(Collections.emptyList());
-
-    }
-
-    private void add(Path path, JarOutputStream tempJarOutputStream) throws FileNotFoundException, IOException {
-        try (FileInputStream fis = new FileInputStream(path.toFile())) {
-            byte[] buffer = new byte[1024];
-            int bytesRead = 0;
-            JarEntry entry = new JarEntry(path.toString());
-            tempJarOutputStream.putNextEntry(entry);
-            while((bytesRead = fis.read(buffer)) != -1) {
-                tempJarOutputStream.write(buffer, 0, bytesRead);
-            }
-        }
-
-    }
-
-    /**
-     * @throws java.lang.Exception
-     */
-    @After
-    public void tearDown() throws Exception {
-        //new File("addon.jar").delete();
-        new File("config.yml").delete();
-        deleteAll(new File("addons"));
-        deleteAll(new File("panels"));
-    }
-
-    private void deleteAll(File file) throws IOException {
-        if (file.exists()) {
-            Files.walk(file.toPath())
-            .sorted(Comparator.reverseOrder())
-            .map(Path::toFile)
-            .forEach(File::delete);
-        }
-
-    }
-
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#onEnable()}.
-     */
-    @Test
-    public void testOnEnable() {
-        testOnLoad();
-        addon.setState(State.ENABLED);
-        addon.onEnable();
-        verify(plugin, never()).logError(anyString());
-        assertNotEquals(State.DISABLED, addon.getState());
-        assertNotNull(addon.getBlockListener());
-        assertNotNull(addon.getOneBlockManager());
-
-        
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#onLoad()}.
-     */
-    @Test
-    public void testOnLoad() {
-        addon.onLoad();
-        // Check that config.yml file has been saved
-        File check = new File("addons/AOneBlock","config.yml");
-        assertTrue(check.exists());
-        assertTrue(addon.getPlayerCommand().isPresent());
-        assertTrue(addon.getAdminCommand().isPresent());
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#onReload()}.
-     */
-    @Test
-    public void testOnReload() {
-        addon.onEnable();
-        addon.onReload();
-        // Check that config.yml file has been saved
-        File check = new File("addons/AOneBlock","config.yml");
-        assertTrue(check.exists());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#createWorlds()}.
-     */
-    @Test
-    public void testCreateWorlds() {
-        addon.onLoad();
-        addon.createWorlds();
-        verify(plugin).log("[aoneblock] Creating AOneBlock world ...");
-        verify(plugin).log("[aoneblock] Creating AOneBlock's Nether...");
-        verify(plugin).log("[aoneblock] Creating AOneBlock's End World...");
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#getSettings()}.
-     */
-    @Test
-    public void testGetSettings() {
-        addon.onLoad();
-        assertNotNull(addon.getSettings());
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#getWorldSettings()}.
-     */
-    @Test
-    public void testGetWorldSettings() {
-        addon.onLoad();
-        assertEquals(addon.getSettings(), addon.getWorldSettings());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#getOneBlocksIsland(world.bentobox.bentobox.database.objects.Island)}.
-     */
-    @Test
-    public void testGetOneBlocksIsland() {
-        addon.onEnable();
-        @NonNull
-        OneBlockIslands i = addon.getOneBlocksIsland(island);
-        assertEquals(island.getUniqueId(), i.getUniqueId());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#getOneBlockManager()}.
-     */
-    @Test
-    public void testGetOneBlockManager() {
-        assertNull(addon.getOneBlockManager());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#getBlockListener()}.
-     */
-    @Test
-    public void testGetBlockListener() {
-        assertNull(addon.getBlockListener());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#getPlaceholdersManager()}.
-     */
-    @Test
-    public void testGetPlaceholdersManager() {
-        assertNull(addon.getPlaceholdersManager());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.AOneBlock#getHoloListener()}.
-     */
-    @Test
-    public void testGetHoloListener() {
-        assertNull(addon.getHoloListener());
-    }
+	@Mock
+	private User user;
+	@Mock
+	private IslandsManager im;
+	@Mock
+	private Island island;
+
+	private AOneBlock addon;
+	@Mock
+	private BentoBox plugin;
+	@Mock
+	private FlagsManager fm;
+	@Mock
+	private Settings settings;
+	@Mock
+	private PlaceholdersManager phm;
+
+	private static AbstractDatabaseHandler h;
+
+	@SuppressWarnings("unchecked")
+	@BeforeClass
+	public static void beforeClass() throws IllegalAccessException, InvocationTargetException, IntrospectionException {
+		// This has to be done beforeClass otherwise the tests will interfere with each
+		// other
+		h = mock(AbstractDatabaseHandler.class);
+		// Database
+		PowerMockito.mockStatic(DatabaseSetup.class);
+		DatabaseSetup dbSetup = mock(DatabaseSetup.class);
+		when(DatabaseSetup.getDatabase()).thenReturn(dbSetup);
+		when(dbSetup.getHandler(any())).thenReturn(h);
+		when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true));
+	}
+
+	@After
+	public void tearDown() throws IOException {
+		User.clearUsers();
+		Mockito.framework().clearInlineMocks();
+		deleteAll(new File("database"));
+		deleteAll(new File("database_backup"));
+		new File("config.yml").delete();
+		deleteAll(new File("addons"));
+		deleteAll(new File("panels"));
+	}
+
+	private void deleteAll(File file) throws IOException {
+		if (file.exists()) {
+			Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+		}
+
+	}
+
+	/**
+	 * @throws java.lang.Exception
+	 */
+	@Before
+	public void setUp() throws Exception {
+		// Set up plugin
+		Whitebox.setInternalState(BentoBox.class, "instance", plugin);
+		when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
+
+		// The database type has to be created one line before the thenReturn() to work!
+		DatabaseType value = DatabaseType.JSON;
+		when(plugin.getSettings()).thenReturn(settings);
+		when(settings.getDatabaseType()).thenReturn(value);
+		when(plugin.getPlaceholdersManager()).thenReturn(phm);
+		// Placeholders
+		when(phm.replacePlaceholders(any(), anyString())).thenAnswer(a -> (String) a.getArgument(1, String.class));
+
+		// Command manager
+		CommandsManager cm = mock(CommandsManager.class);
+		when(plugin.getCommandsManager()).thenReturn(cm);
+
+		// Player
+		Player p = mock(Player.class);
+		// Sometimes use Mockito.withSettings().verboseLogging()
+		when(user.isOp()).thenReturn(false);
+		UUID uuid = UUID.randomUUID();
+		when(user.getUniqueId()).thenReturn(uuid);
+		when(user.getPlayer()).thenReturn(p);
+		when(user.getName()).thenReturn("tastybento");
+		User.setPlugin(plugin);
+
+		// Island World Manager
+		IslandWorldManager iwm = mock(IslandWorldManager.class);
+		when(plugin.getIWM()).thenReturn(iwm);
+
+		// Player has island to begin with
+		island = mock(Island.class);
+		when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island);
+		when(plugin.getIslands()).thenReturn(im);
+
+		// Locales
+		// Return the reference (USE THIS IN THE FUTURE)
+		when(user.getTranslation(Mockito.anyString()))
+				.thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class));
+
+		// Server
+		PowerMockito.mockStatic(Bukkit.class);
+		Server server = mock(Server.class);
+		when(Bukkit.getServer()).thenReturn(server);
+		when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger());
+		when(Bukkit.getPluginManager()).thenReturn(mock(PluginManager.class));
+
+		// Create an Addon
+		addon = new AOneBlock();
+		File jFile = new File("addon.jar");
+		try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) {
+
+			// Copy over config file from src folder
+			Path fromPath = Paths.get("src/main/resources/config.yml");
+			Path path = Paths.get("config.yml");
+			Files.copy(fromPath, path);
+
+			// Add the new files to the jar.
+			add(path, tempJarOutputStream);
+
+			// Copy over panels file from src folder
+			fromPath = Paths.get("src/main/resources/panels/phases_panel.yml");
+			path = Paths.get("panels");
+			Files.createDirectory(path);
+			path = Paths.get("panels/phases_panel.yml");
+			Files.copy(fromPath, path);
+
+			// Add the new files to the jar.
+			add(path, tempJarOutputStream);
+		}
+
+		File dataFolder = new File("addons/AOneBlock");
+		addon.setDataFolder(dataFolder);
+		addon.setFile(jFile);
+		AddonDescription desc = new AddonDescription.Builder("bentobox", "aoneblock", "1.3").description("test")
+				.authors("tasty").build();
+		addon.setDescription(desc);
+		// Addons manager
+		AddonsManager am = mock(AddonsManager.class);
+		when(plugin.getAddonsManager()).thenReturn(am);
+
+		// Flags manager
+		when(plugin.getFlagsManager()).thenReturn(fm);
+		when(fm.getFlags()).thenReturn(Collections.emptyList());
+
+	}
+
+	private void add(Path path, JarOutputStream tempJarOutputStream) throws FileNotFoundException, IOException {
+		try (FileInputStream fis = new FileInputStream(path.toFile())) {
+			byte[] buffer = new byte[1024];
+			int bytesRead = 0;
+			JarEntry entry = new JarEntry(path.toString());
+			tempJarOutputStream.putNextEntry(entry);
+			while ((bytesRead = fis.read(buffer)) != -1) {
+				tempJarOutputStream.write(buffer, 0, bytesRead);
+			}
+		}
+
+	}
+
+	/**
+	 * Test method for {@link world.bentobox.aoneblock.AOneBlock#onEnable()}.
+	 */
+	@Test
+	public void testOnEnable() {
+		testOnLoad();
+		addon.setState(State.ENABLED);
+		addon.onEnable();
+		verify(plugin, never()).logError(anyString());
+		assertNotEquals(State.DISABLED, addon.getState());
+		assertNotNull(addon.getBlockListener());
+		assertNotNull(addon.getOneBlockManager());
+
+	}
+
+	/**
+	 * Test method for {@link world.bentobox.aoneblock.AOneBlock#onLoad()}.
+	 */
+	@Test
+	public void testOnLoad() {
+		addon.onLoad();
+		// Check that config.yml file has been saved
+		File check = new File("addons/AOneBlock", "config.yml");
+		assertTrue(check.exists());
+		assertTrue(addon.getPlayerCommand().isPresent());
+		assertTrue(addon.getAdminCommand().isPresent());
+
+	}
+
+	/**
+	 * Test method for {@link world.bentobox.aoneblock.AOneBlock#onReload()}.
+	 */
+	@Test
+	public void testOnReload() {
+		addon.onEnable();
+		addon.onReload();
+		// Check that config.yml file has been saved
+		File check = new File("addons/AOneBlock", "config.yml");
+		assertTrue(check.exists());
+	}
+
+	/**
+	 * Test method for {@link world.bentobox.aoneblock.AOneBlock#createWorlds()}.
+	 */
+	@Test
+	public void testCreateWorlds() {
+		addon.onLoad();
+		addon.createWorlds();
+		verify(plugin).log("[aoneblock] Creating AOneBlock world ...");
+		verify(plugin).log("[aoneblock] Creating AOneBlock's Nether...");
+		verify(plugin).log("[aoneblock] Creating AOneBlock's End World...");
+
+	}
+
+	/**
+	 * Test method for {@link world.bentobox.aoneblock.AOneBlock#getSettings()}.
+	 */
+	@Test
+	public void testGetSettings() {
+		addon.onLoad();
+		assertNotNull(addon.getSettings());
+
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.AOneBlock#getWorldSettings()}.
+	 */
+	@Test
+	public void testGetWorldSettings() {
+		addon.onLoad();
+		assertEquals(addon.getSettings(), addon.getWorldSettings());
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.AOneBlock#getOneBlocksIsland(world.bentobox.bentobox.database.objects.Island)}.
+	 */
+	@Test
+	public void testGetOneBlocksIsland() {
+		addon.onEnable();
+		@NonNull
+		OneBlockIslands i = addon.getOneBlocksIsland(island);
+		assertEquals(island.getUniqueId(), i.getUniqueId());
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.AOneBlock#getOneBlockManager()}.
+	 */
+	@Test
+	public void testGetOneBlockManager() {
+		assertNull(addon.getOneBlockManager());
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.AOneBlock#getBlockListener()}.
+	 */
+	@Test
+	public void testGetBlockListener() {
+		assertNull(addon.getBlockListener());
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.AOneBlock#getPlaceholdersManager()}.
+	 */
+	@Test
+	public void testGetPlaceholdersManager() {
+		assertNull(addon.getPlaceholdersManager());
+	}
+
+	/**
+	 * Test method for {@link world.bentobox.aoneblock.AOneBlock#getHoloListener()}.
+	 */
+	@Test
+	public void testGetHoloListener() {
+		assertNull(addon.getHoloListener());
+	}
 }
diff --git a/src/test/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommandTest.java b/src/test/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommandTest.java
index 4dd9bb7d..58e50193 100644
--- a/src/test/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommandTest.java
+++ b/src/test/java/world/bentobox/aoneblock/commands/island/IslandSetCountCommandTest.java
@@ -11,10 +11,18 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.beans.IntrospectionException;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 
 import org.bukkit.Bukkit;
 import org.bukkit.World;
@@ -22,10 +30,14 @@
 import org.bukkit.entity.Player;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
+import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 import org.powermock.reflect.Whitebox;
@@ -39,6 +51,8 @@
 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.AbstractDatabaseHandler;
+import world.bentobox.bentobox.database.DatabaseSetup;
 import world.bentobox.bentobox.database.objects.Island;
 import world.bentobox.bentobox.managers.CommandsManager;
 import world.bentobox.bentobox.managers.IslandWorldManager;
@@ -51,126 +65,158 @@
  *
  */
 @RunWith(PowerMockRunner.class)
-@PrepareForTest({Bukkit.class, BentoBox.class, User.class})
+@PrepareForTest({ Bukkit.class, BentoBox.class, User.class, DatabaseSetup.class, RanksManager.class })
 public class IslandSetCountCommandTest {
+	@Mock
+	private BentoBox plugin;
+	@Mock
+	private CompositeCommand ac;
+	@Mock
+	private User user;
+	@Mock
+	private LocalesManager lm;
+	@Mock
+	private AOneBlock addon;
+	private UUID uuid;
+	@Mock
+	private World world;
+	@Mock
+	private IslandsManager im;
+	@Mock
+	private @Nullable Island island;
+	@Mock
+	private IslandWorldManager iwm;
+	private IslandSetCountCommand iscc;
+	@Mock
+	private BlockListener bl;
     @Mock
-    private BentoBox plugin;
-    @Mock
-    private CompositeCommand ac;
-    @Mock
-    private User user;
-    @Mock
-    private LocalesManager lm;
-    @Mock
-    private AOneBlock addon;
-    private UUID uuid;
-    @Mock
-    private World world;
-    @Mock
-    private IslandsManager im;
-    @Mock
-    private @Nullable Island island;
-    @Mock
-    private IslandWorldManager iwm;
-    private IslandSetCountCommand iscc;
-    @Mock
-    private BlockListener bl;
-    private @NonNull OneBlockIslands oneBlockIsland = new OneBlockIslands(UUID.randomUUID().toString());
+    private RanksManager rm;
 
+	private @NonNull OneBlockIslands oneBlockIsland = new OneBlockIslands(UUID.randomUUID().toString());
 
-    /**
-     * @throws java.lang.Exception
-     */
-    @Before
-    public void setUp() throws Exception {
-        // Set up plugin
-        BentoBox plugin = mock(BentoBox.class);
-        Whitebox.setInternalState(BentoBox.class, "instance", plugin);
-
-        // Command manager
-        CommandsManager cm = mock(CommandsManager.class);
-        when(plugin.getCommandsManager()).thenReturn(cm);
-
-        // Player
-        Player p = mock(Player.class);
-        // Sometimes use Mockito.withSettings().verboseLogging()
-        when(user.isOp()).thenReturn(false);
-        when(user.getPermissionValue(anyString(), anyInt())).thenReturn(4);
-        when(user.getWorld()).thenReturn(world);
-        uuid = UUID.randomUUID();
-        when(user.getUniqueId()).thenReturn(uuid);
-        when(user.getPlayer()).thenReturn(p);
-        when(user.getName()).thenReturn("tastybento");
-        when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class));
-        User.setPlugin(plugin);
-
-        // Parent command has no aliases
-        when(ac.getSubCommandAliases()).thenReturn(new HashMap<>());
-        when(ac.getWorld()).thenReturn(world);
-        when(ac.getAddon()).thenReturn(addon);
-
-        // Islands
-        when(plugin.getIslands()).thenReturn(im);
-        when(im.getIsland(world, user)).thenReturn(island);
-        when(im.hasIsland(world, user)).thenReturn(true);
-        when(im.inTeam(world, uuid)).thenReturn(true);
-        when(island.getRankCommand(anyString())).thenReturn(RanksManager.OWNER_RANK);
-        when(island.getRank(user)).thenReturn(RanksManager.MEMBER_RANK);
-
-
-        // IWM
-        when(plugin.getIWM()).thenReturn(iwm);
-        when(iwm.getPermissionPrefix(any())).thenReturn("bskyblock.");        
-        
-        // Settings
-        Settings settings = new Settings();
-        when(addon.getSettings()).thenReturn(settings);
-        
-        // RanksManager
-        RanksManager rm = new RanksManager();
-        when(plugin.getRanksManager()).thenReturn(rm);
-        
-        //  BlockListener
-        when(addon.getBlockListener()).thenReturn(bl);
-        when(bl.getIsland(island)).thenReturn(oneBlockIsland);
-        
-        
-        // DUT
-        iscc = new IslandSetCountCommand(this.ac,
-                settings.getSetCountCommand().split(" ")[0],
-                settings.getSetCountCommand().split(" "));
-    }
+	private static AbstractDatabaseHandler h;
 
-    /**
-     * Test method for {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#IslandSetCountCommand(world.bentobox.bentobox.api.commands.CompositeCommand, java.lang.String, java.lang.String[])}.
-     */
-    @Test
-    public void testIslandSetCountCommand() {
-        assertNotNull(iscc);
-    }
+	@SuppressWarnings("unchecked")
+	@BeforeClass
+	public static void beforeClass() throws IllegalAccessException, InvocationTargetException, IntrospectionException {
+		// This has to be done beforeClass otherwise the tests will interfere with each
+		// other
+		h = mock(AbstractDatabaseHandler.class);
+		// Database
+		PowerMockito.mockStatic(DatabaseSetup.class);
+		DatabaseSetup dbSetup = mock(DatabaseSetup.class);
+		when(DatabaseSetup.getDatabase()).thenReturn(dbSetup);
+		when(dbSetup.getHandler(any())).thenReturn(h);
+		when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true));
+	}
 
-    /**
-     * Test method for {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#setup()}.
-     */
-    @Test
-    public void testSetup() {
-        assertEquals("island.setcount", iscc.getPermission());
-        assertEquals("aoneblock.commands.island.setcount.parameters", iscc.getParameters());
-        assertEquals("aoneblock.commands.island.setcount.description", iscc.getDescription());
-        assertTrue(iscc.isConfigurableRankCommand());
-        assertTrue(iscc.isOnlyPlayer());
+	@After
+	public void tearDown() throws IOException {
+		User.clearUsers();
+		Mockito.framework().clearInlineMocks();
+		deleteAll(new File("database"));
+		deleteAll(new File("database_backup"));
+	}
 
-    }
-    
-    /**
-     * Test method for {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}.
-     */
-    public void testExecuteUserStringListOfStringShowHelp() {
-        assertFalse(iscc.execute(user, "", Collections.emptyList()));
-        verify(user).sendMessage("commands.help.header","[label]",null);
-    }
-    
-    /**
+	private void deleteAll(File file) throws IOException {
+		if (file.exists()) {
+			Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+		}
+
+	}
+
+	/**
+	 * @throws java.lang.Exception
+	 */
+	@Before
+	public void setUp() throws Exception {
+		// Set up plugin
+		BentoBox plugin = mock(BentoBox.class);
+		Whitebox.setInternalState(BentoBox.class, "instance", plugin);
+
+        // Set up RanksManager
+        Whitebox.setInternalState(RanksManager.class, "instance", rm);
+
+		// Command manager
+		CommandsManager cm = mock(CommandsManager.class);
+		when(plugin.getCommandsManager()).thenReturn(cm);
+
+		// Player
+		Player p = mock(Player.class);
+		// Sometimes use Mockito.withSettings().verboseLogging()
+		when(user.isOp()).thenReturn(false);
+		when(user.getPermissionValue(anyString(), anyInt())).thenReturn(4);
+		when(user.getWorld()).thenReturn(world);
+		uuid = UUID.randomUUID();
+		when(user.getUniqueId()).thenReturn(uuid);
+		when(user.getPlayer()).thenReturn(p);
+		when(user.getName()).thenReturn("tastybento");
+		when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class));
+		User.setPlugin(plugin);
+
+		// Parent command has no aliases
+		when(ac.getSubCommandAliases()).thenReturn(new HashMap<>());
+		when(ac.getWorld()).thenReturn(world);
+		when(ac.getAddon()).thenReturn(addon);
+
+		// Islands
+		when(plugin.getIslands()).thenReturn(im);
+		when(im.getIsland(world, user)).thenReturn(island);
+		when(im.hasIsland(world, user)).thenReturn(true);
+		when(im.inTeam(world, uuid)).thenReturn(true);
+		when(island.getRankCommand(anyString())).thenReturn(RanksManager.OWNER_RANK);
+		when(island.getRank(user)).thenReturn(RanksManager.MEMBER_RANK);
+
+		// IWM
+		when(plugin.getIWM()).thenReturn(iwm);
+		when(iwm.getPermissionPrefix(any())).thenReturn("bskyblock.");
+
+		// Settings
+		Settings settings = new Settings();
+		when(addon.getSettings()).thenReturn(settings);
+
+		// BlockListener
+		when(addon.getBlockListener()).thenReturn(bl);
+		when(bl.getIsland(island)).thenReturn(oneBlockIsland);
+
+		// DUT
+		iscc = new IslandSetCountCommand(this.ac, settings.getSetCountCommand().split(" ")[0],
+				settings.getSetCountCommand().split(" "));
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#IslandSetCountCommand(world.bentobox.bentobox.api.commands.CompositeCommand, java.lang.String, java.lang.String[])}.
+	 */
+	@Test
+	public void testIslandSetCountCommand() {
+		assertNotNull(iscc);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#setup()}.
+	 */
+	@Test
+	public void testSetup() {
+		assertEquals("island.setcount", iscc.getPermission());
+		assertEquals("aoneblock.commands.island.setcount.parameters", iscc.getParameters());
+		assertEquals("aoneblock.commands.island.setcount.description", iscc.getDescription());
+		assertTrue(iscc.isConfigurableRankCommand());
+		assertTrue(iscc.isOnlyPlayer());
+
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}.
+	 */
+	public void testExecuteUserStringListOfStringShowHelp() {
+		assertFalse(iscc.execute(user, "", Collections.emptyList()));
+		verify(user).sendMessage("commands.help.header", "[label]", null);
+	}
+
+	/**
      * Test method for {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}.
      */
     @Test
@@ -180,17 +226,19 @@ public void testExecuteUserStringListOfStringNoIsland() {
         assertFalse(iscc.execute(user, "", List.of("2000")));
         verify(user).sendMessage("general.errors.no-island");
     }
-    
-    /**
-     * Test method for {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}.
-     */
-    @Test
-    public void testExecuteUserStringListOfStringLowRank() {
-        assertFalse(iscc.execute(user, "", List.of("2000")));
-        verify(user).sendMessage("general.errors.insufficient-rank", TextVariables.RANK, RanksManager.MEMBER_RANK_REF);
-    }
-    
-    /**
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}.
+	 */
+	@Test
+	public void testExecuteUserStringListOfStringLowRank() {
+        when(rm.getRank(anyInt())).thenReturn(RanksManager.MEMBER_RANK_REF);
+		assertFalse(iscc.execute(user, "", List.of("2000")));
+		verify(user).sendMessage("general.errors.insufficient-rank", TextVariables.RANK, RanksManager.MEMBER_RANK_REF);
+	}
+
+	/**
      * Test method for {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}.
      */
     @Test
@@ -199,8 +247,8 @@ public void testExecuteUserStringListOfStringRankOKNegativeCount() {
         assertFalse(iscc.execute(user, "", List.of("-2000")));
         verify(user).sendMessage("general.errors.must-be-positive-number", TextVariables.NUMBER, "-2000");
     }
-    
-    /**
+
+	/**
      * Test method for {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}.
      */
     @Test
@@ -211,7 +259,7 @@ public void testExecuteUserStringListOfStringRankOKTooHighCount() {
         verify(user).sendMessage("aoneblock.commands.island.setcount.too-high", TextVariables.NUMBER, "0");
     }
 
-    /**
+	/**
      * Test method for {@link world.bentobox.aoneblock.commands.island.IslandSetCountCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}.
      */
     @Test
@@ -226,5 +274,4 @@ public void testExecuteUserStringListOfString() {
 
     }
 
-
 }
diff --git a/src/test/java/world/bentobox/aoneblock/dataobjects/OneBlockIslandsTest.java b/src/test/java/world/bentobox/aoneblock/dataobjects/OneBlockIslandsTest.java
index 772d8ef9..714e915f 100644
--- a/src/test/java/world/bentobox/aoneblock/dataobjects/OneBlockIslandsTest.java
+++ b/src/test/java/world/bentobox/aoneblock/dataobjects/OneBlockIslandsTest.java
@@ -1,6 +1,9 @@
 package world.bentobox.aoneblock.dataobjects;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import java.util.List;
 import java.util.Queue;
diff --git a/src/test/java/world/bentobox/aoneblock/listeners/CheckPhaseTest.java b/src/test/java/world/bentobox/aoneblock/listeners/CheckPhaseTest.java
index 6ef0b605..c5a91791 100644
--- a/src/test/java/world/bentobox/aoneblock/listeners/CheckPhaseTest.java
+++ b/src/test/java/world/bentobox/aoneblock/listeners/CheckPhaseTest.java
@@ -1,8 +1,6 @@
 package world.bentobox.aoneblock.listeners;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -55,7 +53,7 @@
  *
  */
 @RunWith(PowerMockRunner.class)
-@PrepareForTest({Bukkit.class, BentoBox.class, DatabaseSetup.class, Util.class})
+@PrepareForTest({ Bukkit.class, BentoBox.class, DatabaseSetup.class, Util.class })
 public class CheckPhaseTest {
 
     @Mock
@@ -88,60 +86,56 @@ public class CheckPhaseTest {
     @Mock
     private Level level;
 
-
-
     /**
      * @throws java.lang.Exception
      */
     @Before
     public void setUp() throws Exception {
-        // Bukkit
-        PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS);
-        // Set up plugin
-        Whitebox.setInternalState(BentoBox.class, "instance", plugin);
-
-        // Addon
-        when(addon.getPlugin()).thenReturn(plugin);
-        when(addon.getOneBlockManager()).thenReturn(obm);
-        when(addon.getPlayers()).thenReturn(pm);
-        when(addon.getOverWorld()).thenReturn(world);
-        when(addon.getIslands()).thenReturn(im);
-        when(addon.inWorld(world)).thenReturn(true);
-        when(addon.getBlockListener()).thenReturn(blis);
-
-        // Player
-        when(player.getUniqueId()).thenReturn(UUID.randomUUID());
-        when(player.getName()).thenReturn("tastybento");
-        when(player.isOnline()).thenReturn(true);
-        when(player.getWorld()).thenReturn(world);
-        User.setPlugin(plugin);
-        user = User.getInstance(player);
-
-        // Island
-        island = new Island();
-        island.setOwner(UUID.randomUUID());
-        island.setName("island_name");
-        // Players Manager
-        when(pm.getName(any())).thenReturn("tastybento2");
-        when(pm.getUser(any(UUID.class))).thenReturn(user);
-
-        // Bank
-        BankManager bm = mock(BankManager.class);
-        when(bank.getBankManager()).thenReturn(bm);
-        // Phat balance to start
-        when(bm.getBalance(island)).thenReturn(new Money(100000D));
-        when(addon.getAddonByName("Bank")).thenReturn(Optional.of(bank));
-        // Level
-        when(level.getIslandLevel(eq(world), any())).thenReturn(1000L);
-        when(addon.getAddonByName("Level")).thenReturn(Optional.of(level));
-
-
-        // Placeholders
-        when(plugin.getPlaceholdersManager()).thenReturn(phm);
-        when(phm.replacePlaceholders(any(), anyString())).thenAnswer(a -> (String)a.getArgument(1, String.class));
-
-
-        bl = new CheckPhase(addon, blis);
+	// Bukkit
+	PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS);
+	// Set up plugin
+	Whitebox.setInternalState(BentoBox.class, "instance", plugin);
+
+	// Addon
+	when(addon.getPlugin()).thenReturn(plugin);
+	when(addon.getOneBlockManager()).thenReturn(obm);
+	when(addon.getPlayers()).thenReturn(pm);
+	when(addon.getOverWorld()).thenReturn(world);
+	when(addon.getIslands()).thenReturn(im);
+	when(addon.inWorld(world)).thenReturn(true);
+	when(addon.getBlockListener()).thenReturn(blis);
+
+	// Player
+	when(player.getUniqueId()).thenReturn(UUID.randomUUID());
+	when(player.getName()).thenReturn("tastybento");
+	when(player.isOnline()).thenReturn(true);
+	when(player.getWorld()).thenReturn(world);
+	User.setPlugin(plugin);
+	user = User.getInstance(player);
+
+	// Island
+	island = new Island();
+	island.setOwner(UUID.randomUUID());
+	island.setName("island_name");
+	// Players Manager
+	when(pm.getName(any())).thenReturn("tastybento2");
+	when(pm.getUser(any(UUID.class))).thenReturn(user);
+
+	// Bank
+	BankManager bm = mock(BankManager.class);
+	when(bank.getBankManager()).thenReturn(bm);
+	// Phat balance to start
+	when(bm.getBalance(island)).thenReturn(new Money(100000D));
+	when(addon.getAddonByName("Bank")).thenReturn(Optional.of(bank));
+	// Level
+	when(level.getIslandLevel(eq(world), any())).thenReturn(1000L);
+	when(addon.getAddonByName("Level")).thenReturn(Optional.of(level));
+
+	// Placeholders
+	when(plugin.getPlaceholdersManager()).thenReturn(phm);
+	when(phm.replacePlaceholders(any(), anyString())).thenAnswer(a -> (String) a.getArgument(1, String.class));
+
+	bl = new CheckPhase(addon, blis);
     }
 
     /**
@@ -151,99 +145,107 @@ public void setUp() throws Exception {
     public void tearDown() throws Exception {
     }
 
-
     /**
-     * Test method for {@link world.bentobox.aoneblock.listeners.CheckPhase#checkPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
+     * Test method for
+     * {@link world.bentobox.aoneblock.listeners.CheckPhase#setNewPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
      */
     @Test
-    public void testCheckPhase() {
-        // Set up that a phase has been completed
-        is = new OneBlockIslands(UUID.randomUUID().toString());
-        is.setPhaseName("Previous");
-        is.setBlockNumber(500);
-        is.setLifetime(500L);
-        // The phase the user has just moved to
-        phase = new OneBlockPhase("500");
-        phase.setPhaseName("Next Phase");
-        phase.setStartCommands(List.of("start1", "start2"));
-
-        // The previous phase
-        OneBlockPhase previous = mock(OneBlockPhase.class);
-        when(previous.getPhaseName()).thenReturn("Previous");
-
-        when(obm.getPhase("Previous")).thenReturn(Optional.of(previous));
-
-        assertTrue(bl.checkPhase(player, island, is, phase));
-        // Verify commands run
-        verify(previous).getEndCommands();
-        verify(previous).getFirstTimeEndCommands();
-        // Verify title shown
-        verify(player).sendTitle("Next Phase", null, -1, -1, -1);
+    public void testSetNewPhase() {
+	// Set up that a phase has been completed
+	is = new OneBlockIslands(UUID.randomUUID().toString());
+	is.setPhaseName("Previous");
+	is.setBlockNumber(500);
+	is.setLifetime(500L);
+	// The phase the user has just moved to
+	phase = new OneBlockPhase("500");
+	phase.setPhaseName("Next Phase");
+	phase.setStartCommands(List.of("start1", "start2"));
+
+	// The previous phase
+	OneBlockPhase previous = mock(OneBlockPhase.class);
+	when(previous.getPhaseName()).thenReturn("Previous");
+
+	when(obm.getPhase("Previous")).thenReturn(Optional.of(previous));
+
+	bl.setNewPhase(player, island, is, phase);
+	// Verify commands run
+	verify(previous).getEndCommands();
+	verify(previous).getFirstTimeEndCommands();
+	// Verify phase name change
+	assertEquals("Next Phase", is.getPhaseName());
+	// Verify title shown
+	verify(player).sendTitle("Next Phase", null, -1, -1, -1);
 
     }
 
     /**
-     * Test method for {@link world.bentobox.aoneblock.listeners.CheckPhase#checkPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
+     * Test method for
+     * {@link world.bentobox.aoneblock.listeners.CheckPhase#setNewPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
      */
     @Test
-    public void testCheckPhaseSecondTime() {
-        // Set up that a phase has been completed
-        is = new OneBlockIslands(UUID.randomUUID().toString());
-        is.setPhaseName("Previous");
-        is.setBlockNumber(500);
-        is.setLifetime(10500L);
-        // The phase the user has just moved to
-        phase = new OneBlockPhase("500");
-        phase.setPhaseName("Next Phase");
-        phase.setStartCommands(List.of("start1", "start2"));
-
-        // The previous phase
-        OneBlockPhase previous = mock(OneBlockPhase.class);
-        when(previous.getPhaseName()).thenReturn("Previous");
-
-        when(obm.getPhase("Previous")).thenReturn(Optional.of(previous));
-
-        assertTrue(bl.checkPhase(player, island, is, phase));
-        // Verify commands run
-        verify(previous).getEndCommands();
-        verify(previous, never()).getFirstTimeEndCommands();
-        // Verify title shown
-        verify(player).sendTitle("Next Phase", null, -1, -1, -1);
+    public void testSetNewPhaseSecondTime() {
+	// Set up that a phase has been completed
+	is = new OneBlockIslands(UUID.randomUUID().toString());
+	is.setPhaseName("Previous");
+	is.setBlockNumber(500);
+	is.setLifetime(10500L);
+	// The phase the user has just moved to
+	phase = new OneBlockPhase("500");
+	phase.setPhaseName("Next Phase");
+	phase.setStartCommands(List.of("start1", "start2"));
+
+	// The previous phase
+	OneBlockPhase previous = mock(OneBlockPhase.class);
+	when(previous.getPhaseName()).thenReturn("Previous");
+
+	when(obm.getPhase("Previous")).thenReturn(Optional.of(previous));
+
+	bl.setNewPhase(player, island, is, phase);
+	// Verify commands run
+	verify(previous).getEndCommands();
+	verify(previous, never()).getFirstTimeEndCommands();
+	// Verify phase name change
+	assertEquals("Next Phase", is.getPhaseName());
+	// Verify title shown
+	verify(player).sendTitle("Next Phase", null, -1, -1, -1);
 
     }
 
     /**
-     * Test method for {@link world.bentobox.aoneblock.listeners.CheckPhase#checkPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
+     * Test method for
+     * {@link world.bentobox.aoneblock.listeners.CheckPhase#setNewPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
      */
     @Test
-    public void testCheckPhaseNullPlayer() {
-        // Set up that a phase has been completed
-        is = new OneBlockIslands(UUID.randomUUID().toString());
-        is.setPhaseName("Previous");
-        is.setBlockNumber(500);
-        is.setLifetime(500L);
-        // The phase the user has just moved to
-        phase = new OneBlockPhase("500");
-        phase.setPhaseName("Next Phase");
-        phase.setStartCommands(List.of("start1", "start2"));
-
-        // The previous phase
-        OneBlockPhase previous = mock(OneBlockPhase.class);
-        when(previous.getPhaseName()).thenReturn("Previous");
-
-        when(obm.getPhase("Previous")).thenReturn(Optional.of(previous));
-
-        assertTrue(bl.checkPhase(null, island, is, phase));
-        // Verify commands run
-        verify(previous).getEndCommands();
-        verify(previous).getFirstTimeEndCommands();
-        // Verify title shown
-        verify(player).sendTitle("Next Phase", null, -1, -1, -1);
+    public void testSetNewPhaseNullPlayer() {
+	// Set up that a phase has been completed
+	is = new OneBlockIslands(UUID.randomUUID().toString());
+	is.setPhaseName("Previous");
+	is.setBlockNumber(500);
+	is.setLifetime(500L);
+	// The phase the user has just moved to
+	phase = new OneBlockPhase("500");
+	phase.setPhaseName("Next Phase");
+	phase.setStartCommands(List.of("start1", "start2"));
+
+	// The previous phase
+	OneBlockPhase previous = mock(OneBlockPhase.class);
+	when(previous.getPhaseName()).thenReturn("Previous");
+
+	when(obm.getPhase("Previous")).thenReturn(Optional.of(previous));
+
+	bl.setNewPhase(null, island, is, phase);
+	// Verify commands run
+	verify(previous).getEndCommands();
+	verify(previous).getFirstTimeEndCommands();
+	// Verify phase name change
+	assertEquals("Next Phase", is.getPhaseName());
+	// Verify title shown
+	verify(player).sendTitle("Next Phase", null, -1, -1, -1);
 
     }
 
     /**
-     * Test method for {@link world.bentobox.aoneblock.listeners.CheckPhase#checkPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
+     * Test method for {@link world.bentobox.aoneblock.listeners.CheckPhase#setNewPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
      */
     @Test
     public void testCheckPhaseNPCPlayer() {
@@ -264,57 +266,41 @@ public void testCheckPhaseNPCPlayer() {
 
         when(obm.getPhase("Previous")).thenReturn(Optional.of(previous));
 
-        assertTrue(bl.checkPhase(player, island, is, phase));
+        bl.setNewPhase(player, island, is, phase);
         // Verify commands run
         verify(previous).getEndCommands();
         verify(previous).getFirstTimeEndCommands();
+        // Verify phase name change
+        assertEquals("Next Phase", is.getPhaseName());
         // Verify title shown
         verify(player).sendTitle("Next Phase", null, -1, -1, -1);
 
     }
 
     /**
-     * Test method for {@link world.bentobox.aoneblock.listeners.CheckPhase#checkPhase(Player, Island, world.bentobox.aoneblock.dataobjects.OneBlockIslands, world.bentobox.aoneblock.oneblocks.OneBlockPhase)}
-     */
-    @Test
-    public void testCheckSamePhase() {
-        is = new OneBlockIslands(UUID.randomUUID().toString());
-        is.setPhaseName("Previous");
-        is.setBlockNumber(500);
-        is.setLifetime(500L);
-        // The phase the user has just moved to
-        phase = new OneBlockPhase("500");
-        phase.setPhaseName("Previous");
-
-        assertFalse(bl.checkPhase(player, island, is, phase));
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.listeners.BlockListener#replacePlaceholders(org.bukkit.entity.Player, java.lang.String, java.lang.String, world.bentobox.bentobox.database.objects.Island, java.util.List)}.
+     * Test method for
+     * {@link world.bentobox.aoneblock.listeners.BlockListener#replacePlaceholders(org.bukkit.entity.Player, java.lang.String, java.lang.String, world.bentobox.bentobox.database.objects.Island, java.util.List)}.
      */
     @Test
     public void testReplacePlaceholders() {
-        // Commands
-        /*
-         * [island] - Island name
-         * [owner] - Island owner's name
-         * [player] - The name of the player who broke the block triggering the commands
-         * [phase] - the name of this phase
-         * [blocks] - the number of blocks broken
-         * [level] - island level (Requires Levels Addon)
-         * [bank-balance] - island bank balance (Requires Bank Addon)
-         * [eco-balance] - player's economy balance (Requires Vault and an economy plugin)
-
-         */
-        List commandList = new ArrayList<>();
-
-        commandList.add("no replacement");
-        commandList.add("[island] [owner] [phase] [blocks] [level] [bank-balance] [eco-balance]");
-        List r = bl.replacePlaceholders(player, "phaseName", "1000", island, commandList);
-        assertEquals(2, r.size());
-        assertEquals("no replacement", r.get(0));
-        assertEquals("island_name tastybento2 phaseName 1000 1000 100000.0 0.0", r.get(1));
-        verify(phm, times(2)).replacePlaceholders(eq(player), any());
+	// Commands
+	/*
+	 * [island] - Island name [owner] - Island owner's name [player] - The name of
+	 * the player who broke the block triggering the commands [phase] - the name of
+	 * this phase [blocks] - the number of blocks broken [level] - island level
+	 * (Requires Levels Addon) [bank-balance] - island bank balance (Requires Bank
+	 * Addon) [eco-balance] - player's economy balance (Requires Vault and an
+	 * economy plugin)
+	 * 
+	 */
+	List commandList = new ArrayList<>();
+
+	commandList.add("no replacement");
+	commandList.add("[island] [owner] [phase] [blocks] [level] [bank-balance] [eco-balance]");
+	List r = bl.replacePlaceholders(player, "phaseName", "1000", island, commandList);
+	assertEquals(2, r.size());
+	assertEquals("no replacement", r.get(0));
+	assertEquals("island_name tastybento2 phaseName 1000 1000 100000.0 0.0", r.get(1));
+	verify(phm, times(2)).replacePlaceholders(eq(player), any());
     }
 }
diff --git a/src/test/java/world/bentobox/aoneblock/listeners/NoBlockHandlerTest.java b/src/test/java/world/bentobox/aoneblock/listeners/NoBlockHandlerTest.java
index 10c4363c..5ad0baa6 100644
--- a/src/test/java/world/bentobox/aoneblock/listeners/NoBlockHandlerTest.java
+++ b/src/test/java/world/bentobox/aoneblock/listeners/NoBlockHandlerTest.java
@@ -14,6 +14,7 @@
 import org.bukkit.block.Block;
 import org.bukkit.entity.Player;
 import org.bukkit.event.player.PlayerRespawnEvent;
+import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -31,16 +32,16 @@
  */
 @RunWith(PowerMockRunner.class)
 public class NoBlockHandlerTest {
-    
+
     private static final UUID ID = UUID.randomUUID();
-    
+
     @Mock
     private AOneBlock aob;
     @Mock
     private Player p;
-    
+
     private NoBlockHandler nbh;
-    
+
     @Mock
     private Block block;
     @Mock
@@ -54,7 +55,6 @@ public class NoBlockHandlerTest {
     @Mock
     private World world;
 
-
     /**
      * @throws java.lang.Exception
      */
@@ -92,36 +92,38 @@ public void tearDown() throws Exception {
     }
 
     /**
-     * Test method for {@link world.bentobox.aoneblock.listeners.NoBlockHandler#NoBlockHandler(world.bentobox.aoneblock.AOneBlock)}.
+     * Test method for
+     * {@link world.bentobox.aoneblock.listeners.NoBlockHandler#NoBlockHandler(world.bentobox.aoneblock.AOneBlock)}.
      */
     @Test
     public void testNoBlockHandler() {
-        assertNotNull(nbh);
+	assertNotNull(nbh);
     }
 
     /**
-     * Test method for {@link world.bentobox.aoneblock.listeners.NoBlockHandler#onRespawn(org.bukkit.event.player.PlayerRespawnEvent)}.
+     * Test method for
+     * {@link world.bentobox.aoneblock.listeners.NoBlockHandler#onRespawn(org.bukkit.event.player.PlayerRespawnEvent)}.
      */
     @Test
     public void testOnRespawnSolidBlock() {
-        PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false);
-        nbh.onRespawn(event);
-        verify(block, never()).setType(any(Material.class));
-        
+	PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, RespawnReason.DEATH);
+	nbh.onRespawn(event);
+	verify(block, never()).setType(any(Material.class));
+
     }
-    
+
     /**
      * Test method for {@link world.bentobox.aoneblock.listeners.NoBlockHandler#onRespawn(org.bukkit.event.player.PlayerRespawnEvent)}.
      */
     @Test
     public void testOnRespawnAirBlock() {
         when(block.isEmpty()).thenReturn(true);
-        PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false);
+        PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, RespawnReason.DEATH);
         nbh.onRespawn(event);
         verify(block).setType(any(Material.class));
         
     }
-    
+
     /**
      * Test method for {@link world.bentobox.aoneblock.listeners.NoBlockHandler#onRespawn(org.bukkit.event.player.PlayerRespawnEvent)}.
      */
@@ -129,12 +131,12 @@ public void testOnRespawnAirBlock() {
     public void testOnRespawnAirBlockWrongWorld() {
         when(aob.inWorld(world)).thenReturn(false);
         when(block.isEmpty()).thenReturn(true);
-        PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false);
+        PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, RespawnReason.DEATH);
         nbh.onRespawn(event);
         verify(block, never()).setType(any(Material.class));
         
     }
-    
+
     /**
      * Test method for {@link world.bentobox.aoneblock.listeners.NoBlockHandler#onRespawn(org.bukkit.event.player.PlayerRespawnEvent)}.
      */
@@ -142,11 +144,10 @@ public void testOnRespawnAirBlockWrongWorld() {
     public void testOnRespawnAirBlockNoIsland() {
         when(im.getIsland(world, ID)).thenReturn(null);
         when(block.isEmpty()).thenReturn(true);
-        PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false);
+        PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, RespawnReason.DEATH);
         nbh.onRespawn(event);
         verify(block, never()).setType(any(Material.class));
         
     }
 
-
 }
diff --git a/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest.java b/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest.java
deleted file mode 100644
index 9be1c3e1..00000000
--- a/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest.java
+++ /dev/null
@@ -1,296 +0,0 @@
-package world.bentobox.aoneblock.oneblocks;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Comparator;
-import java.util.List;
-import java.util.jar.JarEntry;
-import java.util.jar.JarOutputStream;
-
-import org.bukkit.Bukkit;
-import org.bukkit.Material;
-import org.bukkit.UnsafeValues;
-import org.bukkit.block.Biome;
-import org.bukkit.configuration.InvalidConfigurationException;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.bukkit.inventory.ItemFactory;
-import org.bukkit.inventory.meta.ItemMeta;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
-import org.powermock.reflect.Whitebox;
-
-import world.bentobox.aoneblock.AOneBlock;
-import world.bentobox.bentobox.BentoBox;
-import world.bentobox.bentobox.api.addons.AddonDescription;
-import world.bentobox.bentobox.api.user.User;
-import world.bentobox.bentobox.managers.AddonsManager;
-
-@SuppressWarnings("deprecation")
-@RunWith(PowerMockRunner.class)
-@PrepareForTest({Bukkit.class, BentoBox.class, User.class})
-public class OneBlocksManagerTest {
-    private static File jFile;
-    private static YamlConfiguration oneBlocks;
-    @Mock
-    private BentoBox plugin;
-    private AOneBlock addon;
-    @Mock
-    private AddonsManager am;
-    private OneBlocksManager obm;
-
-    @BeforeClass
-    public static void beforeClass() throws IOException, InvalidConfigurationException {
-        // Make the addon jar
-        jFile = new File("addon.jar");
-        // Copy over config file from src folder
-        /*
-        Path fromPath = Paths.get("src/main/resources/config.yml");
-        Path path = Paths.get("config.yml");
-        Files.copy(fromPath, path);*/
-        // Dummy oneblocks.yml
-        String oneblocks =
-                "'0':\n" +
-                        "  name: Plains\n" +
-                        "  firstBlock: GRASS_BLOCK\n" +
-                        "  biome: PLAINS\n" +
-                        "  blocks:\n" +
-                        "    GRASS_BLOCK: 2000\n" +
-                        "    BIRCH_LOG: 500\n" +
-                        "  mobs:\n" +
-                        "    SHEEP: 150\n" +
-                        "    VILLAGER: 30\n" +
-                        "'700':\n" +
-                        "  name: Underground\n" +
-                        "  firstBlock: STONE\n" +
-                        "  biome: TAIGA\n" +
-                        "  blocks:\n" +
-                        "    EMERALD_ORE: 5\n" +
-                        "    COBWEB: 250\n" +
-                        "    DIRT: 500\n" +
-                        "'11000':\n" +
-                        "  gotoBlock: 0";
-        oneBlocks = new YamlConfiguration();
-        oneBlocks.loadFromString(oneblocks);
-        // Save
-        File obFileDir = new File("phases");
-
-        File obFile = new File(obFileDir, "0_plains.yml");
-        obFileDir.mkdirs();
-        oneBlocks.save(obFile);
-        /*
-        // Copy over block config file from src folder
-        fromPath = Paths.get("src/main/resources/oneblocks.yml");
-        path = Paths.get("oneblocks.yml");
-        Files.copy(fromPath, path);
-         */
-        try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) {
-            //Added the new files to the jar.
-            try (FileInputStream fis = new FileInputStream(obFile)) {
-                byte[] buffer = new byte[1024];
-                int bytesRead = 0;
-                JarEntry entry = new JarEntry(obFile.toPath().toString());
-                tempJarOutputStream.putNextEntry(entry);
-                while((bytesRead = fis.read(buffer)) != -1) {
-                    tempJarOutputStream.write(buffer, 0, bytesRead);
-                }
-            }
-        }
-    }
-
-    /**
-     * @throws java.lang.Exception
-     */
-    @Before
-    public void setUp() throws Exception {
-        // Set up plugin
-        Whitebox.setInternalState(BentoBox.class, "instance", plugin);
-        // Addon
-        addon = new AOneBlock();
-        File dataFolder = new File("addons/AOneBlock");
-        addon.setDataFolder(dataFolder);
-        addon.setFile(jFile);
-        AddonDescription desc = new AddonDescription.Builder("bentobox", "AOneBlock", "1.3").description("test").authors("tastybento").build();
-        addon.setDescription(desc);
-        //addon.setSettings(new Settings());
-        // Addons manager
-        when(plugin.getAddonsManager()).thenReturn(am);
-
-        // Bukkit
-        PowerMockito.mockStatic(Bukkit.class);
-        ItemMeta meta = mock(ItemMeta.class);
-        ItemFactory itemFactory = mock(ItemFactory.class);
-        when(itemFactory.getItemMeta(any())).thenReturn(meta);
-        when(Bukkit.getItemFactory()).thenReturn(itemFactory);
-        UnsafeValues unsafe = mock(UnsafeValues.class);
-        when(unsafe.getDataVersion()).thenReturn(777);
-        when(Bukkit.getUnsafe()).thenReturn(unsafe);
-
-        // Class under test
-        obm = new OneBlocksManager(addon);
-    }
-
-    /**
-     * @throws java.lang.Exception
-     */
-    @After
-    public void tearDown() throws Exception {
-        deleteAll(new File("database"));
-    }
-
-    @AfterClass
-    public static void cleanUp() throws Exception {
-
-        new File("addon.jar").delete();
-        new File("config.yml").delete();
-
-        deleteAll(new File("addons"));
-        deleteAll(new File("phases"));
-    }
-
-    private static void deleteAll(File file) throws IOException {
-        if (file.exists()) {
-            Files.walk(file.toPath())
-            .sorted(Comparator.reverseOrder())
-            .map(Path::toFile)
-            .forEach(File::delete);
-        }
-    }
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#OneBlocksManager(world.bentobox.aoneblock.AOneBlock)}.
-     * @throws IOException
-     */
-    @Test
-    public void testOneBlocksManager() throws IOException {
-        File f = new File("phases", "0_plains.yml");
-        assertTrue(f.exists());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#loadPhases()}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    //@Ignore("Cannot deserialize objects right now")
-    @Test
-    public void testLoadPhases() throws NumberFormatException, IOException, InvalidConfigurationException {
-        obm.loadPhases();
-        verify(plugin, never()).logError(anyString());
-        assertEquals(Material.GRASS_BLOCK, obm.getPhase(0).getFirstBlock().getMaterial());
-        assertEquals(Biome.PLAINS, obm.getPhase(0).getPhaseBiome());
-        assertEquals("Plains", obm.getPhase(0).getPhaseName());
-        assertNull(obm.getPhase(0).getGotoBlock());
-
-        assertEquals(Material.STONE, obm.getPhase(700).getFirstBlock().getMaterial());
-        assertEquals(Biome.TAIGA, obm.getPhase(700).getPhaseBiome());
-        assertEquals("Underground", obm.getPhase(700).getPhaseName());
-        assertNull(obm.getPhase(700).getGotoBlock());
-
-        assertEquals(0, (int)obm.getPhase(11000).getGotoBlock());
-
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhaseList()}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    @Test
-    public void testGetPhaseList() throws NumberFormatException, IOException, InvalidConfigurationException {
-        testLoadPhases();
-        List l = obm.getPhaseList();
-        assertEquals(2, l.size());
-        assertEquals("Plains", l.get(0));
-        assertEquals("Underground", l.get(1));
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhase(java.lang.String)}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    @Test
-    public void testGetPhaseString() throws NumberFormatException, IOException, InvalidConfigurationException {
-        testLoadPhases();
-        assertFalse(obm.getPhase("sdf").isPresent());
-        assertTrue(obm.getPhase("Plains").isPresent());
-        assertTrue(obm.getPhase("Underground").isPresent());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#saveOneBlockConfig()}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    @Test
-    public void testSaveOneBlockConfig() throws NumberFormatException, IOException, InvalidConfigurationException {
-        //testLoadPhases();
-        //assertTrue(obm.saveOneBlockConfig());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhase(OneBlockPhase)}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    @Test
-    public void testGetNextPhase() throws NumberFormatException, IOException, InvalidConfigurationException {
-        testLoadPhases();
-        OneBlockPhase plains = obm.getPhase("Plains").get();
-        OneBlockPhase underground = obm.getPhase("Underground").get();
-        OneBlockPhase gotoPhase = obm.getPhase(11000);
-        assertEquals(underground, obm.getNextPhase(plains));
-        assertEquals(gotoPhase, obm.getNextPhase(underground));
-        assertNull(obm.getNextPhase(gotoPhase));
-    }
-
-    @Ignore
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getProbs(world.bentobox.aoneblock.oneblocks.OneBlockPhase)}.
-     */
-    @Test
-    public void testGetProbs() {
-
-    }
-
-    @Ignore
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getAllProbs()}.
-     */
-    @Test
-    public void testGetAllProbs() {
-        fail("Not yet implemented"); // TODO
-    }
-
-}
diff --git a/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest2.java b/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest2.java
deleted file mode 100644
index b894ef1a..00000000
--- a/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest2.java
+++ /dev/null
@@ -1,446 +0,0 @@
-package world.bentobox.aoneblock.oneblocks;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Comparator;
-import java.util.List;
-import java.util.NavigableMap;
-import java.util.jar.JarEntry;
-import java.util.jar.JarOutputStream;
-
-import org.bukkit.Bukkit;
-import org.bukkit.Material;
-import org.bukkit.UnsafeValues;
-import org.bukkit.block.Biome;
-import org.bukkit.configuration.InvalidConfigurationException;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.bukkit.inventory.ItemFactory;
-import org.bukkit.inventory.meta.ItemMeta;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
-import org.powermock.reflect.Whitebox;
-
-import world.bentobox.aoneblock.AOneBlock;
-import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
-import world.bentobox.bentobox.BentoBox;
-import world.bentobox.bentobox.api.addons.AddonDescription;
-import world.bentobox.bentobox.api.user.User;
-import world.bentobox.bentobox.managers.AddonsManager;
-
-/**
- * @author tastybento
- *
- */
-@RunWith(PowerMockRunner.class)
-@PrepareForTest({Bukkit.class, BentoBox.class, User.class})
-public class OneBlocksManagerTest2 {
-
-    private static File jFile;
-    private static YamlConfiguration oneBlocks;
-    @Mock
-    private BentoBox plugin;
-    private AOneBlock addon;
-    @Mock
-    private AddonsManager am;
-    private OneBlocksManager obm;
-    private OneBlockPhase obPhase;
-    @Mock
-    private @NonNull OneBlockIslands obi;
-
-    @BeforeClass
-    public static void beforeClass() throws IOException, InvalidConfigurationException {
-        // Make the addon jar
-        jFile = new File("addon.jar");
-        // Copy over config file from src folder
-        /*
-        Path fromPath = Paths.get("src/main/resources/config.yml");
-        Path path = Paths.get("config.yml");
-        Files.copy(fromPath, path);*/
-        // Dummy oneblocks.yml
-        String oneblocks =
-                "'0':\n" +
-                        "  name: Plains\n" +
-                        "  icon: GRASS_BLOCK\n" +
-                        "  firstBlock: GRASS_BLOCK\n" +
-                        "  biome: PLAINS\n" +
-                        "  fixedBlocks:\n" +
-                        "    0: GRASS_BLOCK\n" +
-                        "    1: GRASS_BLOCK\n" +
-                        "  holograms:\n" +
-                        "    0: &aGood Luck!\n" +
-                        "  blocks:\n" +
-                        "    GRASS_BLOCK: 2000\n" +
-                        "    BIRCH_LOG: 500\n" +
-                        "  mobs:\n" +
-                        "    SHEEP: 150\n" +
-                        "    VILLAGER: 30\n" +
-                        "'700':\n" +
-                        "  name: Underground\n" +
-                        "  firstBlock: STONE\n" +
-                        "  biome: TAIGA\n" +
-                        "  blocks:\n" +
-                        "    EMERALD_ORE: 5\n" +
-                        "    COBWEB: 250\n" +
-                        "    DIRT: 500\n" +
-                        "'11000':\n" +
-                        "  gotoBlock: 0";
-        oneBlocks = new YamlConfiguration();
-        oneBlocks.loadFromString(oneblocks);
-        // Save
-        File obFileDir = new File("phases");
-
-        File obFile = new File(obFileDir, "0_plains.yml");
-        obFileDir.mkdirs();
-        oneBlocks.save(obFile);
-        /*
-        // Copy over block config file from src folder
-        fromPath = Paths.get("src/main/resources/oneblocks.yml");
-        path = Paths.get("oneblocks.yml");
-        Files.copy(fromPath, path);
-         */
-        try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) {
-            //Added the new files to the jar.
-            try (FileInputStream fis = new FileInputStream(obFile)) {
-                byte[] buffer = new byte[1024];
-                int bytesRead = 0;
-                JarEntry entry = new JarEntry(obFile.toPath().toString());
-                tempJarOutputStream.putNextEntry(entry);
-                while((bytesRead = fis.read(buffer)) != -1) {
-                    tempJarOutputStream.write(buffer, 0, bytesRead);
-                }
-            }
-        }
-    }
-
-    /**
-     * @throws java.lang.Exception
-     */
-    @Before
-    public void setUp() throws Exception {
-        // Set up plugin
-        Whitebox.setInternalState(BentoBox.class, "instance", plugin);
-        // Addon
-        addon = new AOneBlock();
-        File dataFolder = new File("addons/AOneBlock");
-        addon.setDataFolder(dataFolder);
-        addon.setFile(jFile);
-        AddonDescription desc = new AddonDescription.Builder("bentobox", "AOneBlock", "1.3").description("test").authors("tastybento").build();
-        addon.setDescription(desc);
-        //addon.setSettings(new Settings());
-        // Addons manager
-        when(plugin.getAddonsManager()).thenReturn(am);
-
-        // Bukkit
-        PowerMockito.mockStatic(Bukkit.class);
-        ItemMeta meta = mock(ItemMeta.class);
-        ItemFactory itemFactory = mock(ItemFactory.class);
-        when(itemFactory.getItemMeta(any())).thenReturn(meta);
-        when(Bukkit.getItemFactory()).thenReturn(itemFactory);
-        UnsafeValues unsafe = mock(UnsafeValues.class);
-        when(unsafe.getDataVersion()).thenReturn(777);
-        when(Bukkit.getUnsafe()).thenReturn(unsafe);
-
-        // Phase
-        obPhase = new OneBlockPhase("0");
-
-        // Class under test
-        obm = new OneBlocksManager(addon);
-    }
-
-    /**
-     * @throws java.lang.Exception
-     */
-    @After
-    public void tearDown() throws Exception {
-        deleteAll(new File("database"));
-    }
-
-    @AfterClass
-    public static void cleanUp() throws Exception {
-
-        new File("addon.jar").delete();
-        new File("config.yml").delete();
-
-        deleteAll(new File("addons"));
-        deleteAll(new File("phases"));
-    }
-
-    private static void deleteAll(File file) throws IOException {
-        if (file.exists()) {
-            Files.walk(file.toPath())
-            .sorted(Comparator.reverseOrder())
-            .map(Path::toFile)
-            .forEach(File::delete);
-        }
-    }
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#OneBlocksManager(world.bentobox.aoneblock.AOneBlock)}.
-     * @throws IOException
-     */
-    @Test
-    public void testOneBlocksManager() throws IOException {
-        File f = new File("phases", "0_plains.yml");
-        assertTrue(f.exists());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#loadPhases()}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    //@Ignore("Cannot deserialize objects right now")
-    @Test
-    public void testLoadPhases() throws NumberFormatException, IOException, InvalidConfigurationException {
-        obm.loadPhases();
-        verify(plugin, never()).logError(anyString());
-        assertEquals(Material.GRASS_BLOCK, obm.getPhase(0).getFirstBlock().getMaterial());
-        assertEquals(Biome.PLAINS, obm.getPhase(0).getPhaseBiome());
-        assertEquals("Plains", obm.getPhase(0).getPhaseName());
-        assertNull(obm.getPhase(0).getGotoBlock());
-
-        assertEquals(Material.STONE, obm.getPhase(700).getFirstBlock().getMaterial());
-        assertEquals(Biome.TAIGA, obm.getPhase(700).getPhaseBiome());
-        assertEquals("Underground", obm.getPhase(700).getPhaseName());
-        assertNull(obm.getPhase(700).getGotoBlock());
-
-        assertEquals(0, (int)obm.getPhase(11000).getGotoBlock());
-
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhaseList()}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    @Test
-    public void testGetPhaseList() throws NumberFormatException, IOException, InvalidConfigurationException {
-        testLoadPhases();
-        List l = obm.getPhaseList();
-        assertEquals(2, l.size());
-        assertEquals("Plains", l.get(0));
-        assertEquals("Underground", l.get(1));
-
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhase(java.lang.String)}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    @Test
-    public void testGetPhaseString() throws NumberFormatException, IOException, InvalidConfigurationException {
-        testLoadPhases();
-        assertFalse(obm.getPhase("sdf").isPresent());
-        assertTrue(obm.getPhase("Plains").isPresent());
-        assertTrue(obm.getPhase("Underground").isPresent());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#saveOneBlockConfig()}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    @Ignore("Not saving")
-    @Test
-    public void testSaveOneBlockConfig() throws NumberFormatException, IOException, InvalidConfigurationException {
-        //testLoadPhases();
-        //assertTrue(obm.saveOneBlockConfig());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhase(OneBlockPhase)}.
-     * @throws InvalidConfigurationException
-     * @throws IOException
-     * @throws NumberFormatException
-     */
-    @Test
-    public void testGetNextPhase() throws NumberFormatException, IOException, InvalidConfigurationException {
-        testLoadPhases();
-        OneBlockPhase plains = obm.getPhase("Plains").get();
-        OneBlockPhase underground = obm.getPhase("Underground").get();
-        OneBlockPhase gotoPhase = obm.getPhase(11000);
-        assertEquals(underground, obm.getNextPhase(plains));
-        assertEquals(gotoPhase, obm.getNextPhase(underground));
-        assertNull(obm.getNextPhase(gotoPhase));
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#copyPhasesFromAddonJar(java.io.File)}.
-     * @throws IOException
-     */
-    @Test
-    public void testCopyPhasesFromAddonJar() throws IOException {
-        File dest = new File("dest");
-        dest.mkdir();
-        obm.copyPhasesFromAddonJar(dest);
-        File check = new File(dest, "0_plains.yml");
-        assertTrue(check.exists());
-        // Clean up
-        deleteAll(dest);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#initBlock(java.lang.String, world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
-     * @throws IOException
-     */
-    @Test
-    public void testInitBlock() throws IOException {
-        System.out.println(oneBlocks);
-        obm.initBlock("0", obPhase, oneBlocks);
-        assertEquals("", obPhase.getPhaseName());
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addFirstBlock(world.bentobox.aoneblock.oneblocks.OneBlockPhase, java.lang.String)}.
-     */
-    @Test
-    public void testAddFirstBlock() {
-        obm.addFirstBlock(obPhase, "SPONGE");
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addCommands(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
-     */
-    @Test
-    public void testAddCommands() {
-        obm.addCommands(obPhase, oneBlocks);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addRequirements(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
-     */
-    @Test
-    public void testAddRequirements() {
-        obm.addRequirements(obPhase, oneBlocks);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addChests(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
-     */
-    @Test
-    public void testAddChests() throws IOException {
-        obm.addChests(obPhase, oneBlocks);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addMobs(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
-     */
-    @Test
-    public void testAddMobs() throws IOException {
-        obm.addMobs(obPhase, oneBlocks);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addBlocks(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
-     */
-    @Test
-    public void testAddBlocks() throws IOException {
-        obm.addBlocks(obPhase, oneBlocks);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhase(int)}.
-     */
-    @Test
-    public void testGetPhaseInt() {
-        @Nullable
-        OneBlockPhase phase = obm.getPhase(1);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getBlockProbs()}.
-     */
-    @Test
-    public void testGetBlockProbs() {
-        NavigableMap probs = obm.getBlockProbs();
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#savePhase(world.bentobox.aoneblock.oneblocks.OneBlockPhase)}.
-     */
-    @Test
-    public void testSavePhase() {
-        boolean result = obm.savePhase(obPhase);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhase(world.bentobox.aoneblock.oneblocks.OneBlockPhase)}.
-     */
-    @Test
-    public void testGetNextPhaseOneBlockPhase() {
-        @Nullable
-        OneBlockPhase phase = obm.getNextPhase(obPhase);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhaseBlocks(world.bentobox.aoneblock.dataobjects.OneBlockIslands)}.
-     */
-    @Test
-    public void testGetNextPhaseBlocks() {
-        int phase = obm.getNextPhaseBlocks(obi);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPercentageDone(world.bentobox.aoneblock.dataobjects.OneBlockIslands)}.
-     */
-    @Test
-    public void testGetPercentageDone() {
-        double percent = obm.getPercentageDone(obi);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getProbs(world.bentobox.aoneblock.oneblocks.OneBlockPhase)}.
-     */
-    @Test
-    public void testGetProbs() {
-        obm.getProbs(obPhase);
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getAllProbs()}.
-     */
-    @Test
-    public void testGetAllProbs() {
-        obm.getAllProbs();
-    }
-
-    /**
-     * Test method for {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhase(world.bentobox.aoneblock.dataobjects.OneBlockIslands)}.
-     */
-    @Test
-    public void testGetNextPhaseOneBlockIslands() {
-        String phase = obm.getNextPhase(obi);
-    }
-
-}
diff --git a/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest3.java b/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest3.java
new file mode 100644
index 00000000..57323b39
--- /dev/null
+++ b/src/test/java/world/bentobox/aoneblock/oneblocks/OneBlocksManagerTest3.java
@@ -0,0 +1,482 @@
+package world.bentobox.aoneblock.oneblocks;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.UnsafeValues;
+import org.bukkit.block.Biome;
+import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.inventory.ItemFactory;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.powermock.reflect.Whitebox;
+
+import world.bentobox.aoneblock.AOneBlock;
+import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
+import world.bentobox.bentobox.BentoBox;
+import world.bentobox.bentobox.api.addons.AddonDescription;
+import world.bentobox.bentobox.api.user.User;
+import world.bentobox.bentobox.managers.AddonsManager;
+
+/**
+ * @author tastybento
+ *
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ Bukkit.class, BentoBox.class, User.class })
+public class OneBlocksManagerTest3 {
+
+	private static File jFile;
+	private static YamlConfiguration oneBlocks;
+	@Mock
+	private BentoBox plugin;
+	private AOneBlock addon;
+	@Mock
+	private AddonsManager am;
+	private OneBlocksManager obm;
+	private OneBlockPhase obPhase;
+	@Mock
+	private @NonNull OneBlockIslands obi;
+
+	@BeforeClass
+	public static void beforeClass() throws IOException, InvalidConfigurationException {
+		// Make the addon jar
+		jFile = new File("addon.jar");
+		// Copy over config file from src folder
+		/*
+		 * Path fromPath = Paths.get("src/main/resources/config.yml"); Path path =
+		 * Paths.get("config.yml"); Files.copy(fromPath, path);
+		 */
+		// Dummy oneblocks.yml
+		String oneblocks = "'0':\n" + "  name: Plains\n" + "  icon: GRASS_BLOCK\n" + "  firstBlock: GRASS_BLOCK\n"
+				+ "  biome: PLAINS\n" + "  fixedBlocks:\n" + "    0: GRASS_BLOCK\n" + "    1: GRASS_BLOCK\n"
+				+ "  holograms:\n" + "    0: &aGood Luck!\n" + "  blocks:\n" + "    GRASS_BLOCK: 2000\n"
+				+ "    BIRCH_LOG: 500\n" + "  mobs:\n" + "    SHEEP: 150\n" + "    VILLAGER: 30\n" + "'700':\n"
+				+ "  name: Underground\n" + "  firstBlock: STONE\n" + "  biome: TAIGA\n" + "  blocks:\n"
+				+ "    EMERALD_ORE: 5\n" + "    COBWEB: 250\n" + "    DIRT: 500\n" + "'11000':\n" + "  gotoBlock: 0";
+		oneBlocks = new YamlConfiguration();
+		oneBlocks.loadFromString(oneblocks);
+		// Save
+		File obFileDir = new File("phases");
+
+		File obFile = new File(obFileDir, "0_plains.yml");
+		obFileDir.mkdirs();
+		oneBlocks.save(obFile);
+		/*
+		 * // Copy over block config file from src folder fromPath =
+		 * Paths.get("src/main/resources/oneblocks.yml"); path =
+		 * Paths.get("oneblocks.yml"); Files.copy(fromPath, path);
+		 */
+		try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) {
+			// Added the new files to the jar.
+			try (FileInputStream fis = new FileInputStream(obFile)) {
+				byte[] buffer = new byte[1024];
+				int bytesRead = 0;
+				JarEntry entry = new JarEntry(obFile.toPath().toString());
+				tempJarOutputStream.putNextEntry(entry);
+				while ((bytesRead = fis.read(buffer)) != -1) {
+					tempJarOutputStream.write(buffer, 0, bytesRead);
+				}
+			}
+		}
+	}
+
+	/**
+	 * @throws java.lang.Exception
+	 */
+	@Before
+	public void setUp() throws Exception {
+		// Set up plugin
+		Whitebox.setInternalState(BentoBox.class, "instance", plugin);
+		// Addon
+		addon = new AOneBlock();
+		File dataFolder = new File("addons/AOneBlock");
+		addon.setDataFolder(dataFolder);
+		addon.setFile(jFile);
+		AddonDescription desc = new AddonDescription.Builder("bentobox", "AOneBlock", "1.3").description("test")
+				.authors("tastybento").build();
+		addon.setDescription(desc);
+		// addon.setSettings(new Settings());
+		// Addons manager
+		when(plugin.getAddonsManager()).thenReturn(am);
+
+		// Bukkit
+		PowerMockito.mockStatic(Bukkit.class);
+		ItemMeta meta = mock(ItemMeta.class);
+		ItemFactory itemFactory = mock(ItemFactory.class);
+		when(itemFactory.getItemMeta(any())).thenReturn(meta);
+		when(Bukkit.getItemFactory()).thenReturn(itemFactory);
+		UnsafeValues unsafe = mock(UnsafeValues.class);
+		when(unsafe.getDataVersion()).thenReturn(777);
+		when(Bukkit.getUnsafe()).thenReturn(unsafe);
+
+		// Phase
+		obPhase = new OneBlockPhase("0");
+
+		// Class under test
+		obm = new OneBlocksManager(addon);
+	}
+
+	/**
+	 * @throws java.lang.Exception
+	 */
+	@After
+	public void tearDown() throws Exception {
+		deleteAll(new File("database"));
+	}
+
+	@AfterClass
+	public static void cleanUp() throws Exception {
+
+		new File("addon.jar").delete();
+		new File("config.yml").delete();
+
+		deleteAll(new File("addons"));
+		deleteAll(new File("phases"));
+	}
+
+	private static void deleteAll(File file) throws IOException {
+		if (file.exists()) {
+			Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+		}
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#OneBlocksManager(world.bentobox.aoneblock.AOneBlock)}.
+	 * 
+	 * @throws IOException
+	 */
+	@Test
+	public void testOneBlocksManager() throws IOException {
+		File f = new File("phases", "0_plains.yml");
+		assertTrue(f.exists());
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#loadPhases()}.
+	 * 
+	 * @throws InvalidConfigurationException
+	 * @throws IOException
+	 * @throws NumberFormatException
+	 */
+	// @Ignore("Cannot deserialize objects right now")
+	@Test
+	public void testLoadPhases() throws NumberFormatException, IOException, InvalidConfigurationException {
+		obm.loadPhases();
+		verify(plugin, never()).logError(anyString());
+		assertEquals(Material.GRASS_BLOCK, obm.getPhase(0).getFirstBlock().getMaterial());
+		assertEquals(Biome.PLAINS, obm.getPhase(0).getPhaseBiome());
+		assertEquals("Plains", obm.getPhase(0).getPhaseName());
+		assertNull(obm.getPhase(0).getGotoBlock());
+
+		assertEquals(Material.STONE, obm.getPhase(700).getFirstBlock().getMaterial());
+		assertEquals(Biome.TAIGA, obm.getPhase(700).getPhaseBiome());
+		assertEquals("Underground", obm.getPhase(700).getPhaseName());
+		assertNull(obm.getPhase(700).getGotoBlock());
+
+		assertEquals(0, (int) obm.getPhase(11000).getGotoBlock());
+
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhaseList()}.
+	 * 
+	 * @throws InvalidConfigurationException
+	 * @throws IOException
+	 * @throws NumberFormatException
+	 */
+	@Test
+	public void testGetPhaseList() throws NumberFormatException, IOException, InvalidConfigurationException {
+		testLoadPhases();
+		List l = obm.getPhaseList();
+		assertEquals(2, l.size());
+		assertEquals("Plains", l.get(0));
+		assertEquals("Underground", l.get(1));
+
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhase(java.lang.String)}.
+	 * 
+	 * @throws InvalidConfigurationException
+	 * @throws IOException
+	 * @throws NumberFormatException
+	 */
+	@Test
+	public void testGetPhaseString() throws NumberFormatException, IOException, InvalidConfigurationException {
+		testLoadPhases();
+		assertFalse(obm.getPhase("sdf").isPresent());
+		assertTrue(obm.getPhase("Plains").isPresent());
+		assertTrue(obm.getPhase("Underground").isPresent());
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#saveOneBlockConfig()}.
+	 * 
+	 * @throws InvalidConfigurationException
+	 * @throws IOException
+	 * @throws NumberFormatException
+	 */
+	@Ignore("Not saving")
+	@Test
+	public void testSaveOneBlockConfig() throws NumberFormatException, IOException, InvalidConfigurationException {
+		// testLoadPhases();
+		// assertTrue(obm.saveOneBlockConfig());
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhase(OneBlockPhase)}.
+	 * 
+	 * @throws InvalidConfigurationException
+	 * @throws IOException
+	 * @throws NumberFormatException
+	 */
+	@Test
+	public void testGetNextPhase() throws NumberFormatException, IOException, InvalidConfigurationException {
+		testLoadPhases();
+		OneBlockPhase plains = obm.getPhase("Plains").get();
+		OneBlockPhase underground = obm.getPhase("Underground").get();
+		OneBlockPhase gotoPhase = obm.getPhase(11000);
+		assertEquals(underground, obm.getNextPhase(plains));
+		assertEquals(gotoPhase, obm.getNextPhase(underground));
+		assertNull(obm.getNextPhase(gotoPhase));
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#copyPhasesFromAddonJar(java.io.File)}.
+	 * 
+	 * @throws IOException
+	 */
+	@Test
+	public void testCopyPhasesFromAddonJar() throws IOException {
+		File dest = new File("dest");
+		dest.mkdir();
+		obm.copyPhasesFromAddonJar(dest);
+		File check = new File(dest, "0_plains.yml");
+		assertTrue(check.exists());
+		// Clean up
+		deleteAll(dest);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#initBlock(java.lang.String, world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
+	 * 
+	 * @throws IOException
+	 */
+	@Test
+	@Ignore
+	public void testInitBlock() throws IOException {
+		System.out.println(oneBlocks);
+		obm.initBlock("0", obPhase, oneBlocks);
+		assertEquals("", obPhase.getPhaseName());
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addFirstBlock(world.bentobox.aoneblock.oneblocks.OneBlockPhase, java.lang.String)}.
+	 */
+	@Test
+	public void testAddFirstBlockBadMateril() {
+		obm.addFirstBlock(obPhase, "shshhs");
+		verify(plugin).logError("[AOneBlock] Bad firstBlock material: shshhs");
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addFirstBlock(world.bentobox.aoneblock.oneblocks.OneBlockPhase, java.lang.String)}.
+	 */
+	@Test
+	public void testAddFirstBlockNullMaterial() {
+		obm.addFirstBlock(obPhase, null);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addFirstBlock(world.bentobox.aoneblock.oneblocks.OneBlockPhase, java.lang.String)}.
+	 */
+	@Test
+	public void testAddFirstBlock() {
+		obm.addFirstBlock(obPhase, "SPONGE");
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addCommands(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
+	 */
+	@Test
+	public void testAddCommands() {
+		obm.addCommands(obPhase, oneBlocks);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addRequirements(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
+	 */
+	@Test
+	public void testAddRequirements() {
+		obm.addRequirements(obPhase, oneBlocks);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addChests(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
+	 */
+	@Test
+	public void testAddChests() throws IOException {
+		obm.addChests(obPhase, oneBlocks);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addMobs(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
+	 */
+	@Test
+	public void testAddMobs() throws IOException {
+		obm.addMobs(obPhase, oneBlocks);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#addBlocks(world.bentobox.aoneblock.oneblocks.OneBlockPhase, org.bukkit.configuration.ConfigurationSection)}.
+	 */
+	@Test
+	public void testAddBlocks() throws IOException {
+		obm.addBlocks(obPhase, oneBlocks);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhase(int)}.
+	 */
+	@Test
+	public void testGetPhaseInt() {
+		@Nullable
+		OneBlockPhase phase = obm.getPhase(1);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getBlockProbs()}.
+	 */
+	@Test
+	public void testGetBlockProbs() {
+		NavigableMap probs = obm.getBlockProbs();
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#savePhase(world.bentobox.aoneblock.oneblocks.OneBlockPhase)}.
+	 */
+	@Test
+	public void testSavePhase() {
+		boolean result = obm.savePhase(obPhase);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhase(world.bentobox.aoneblock.oneblocks.OneBlockPhase)}.
+	 */
+	@Test
+	public void testGetNextPhaseOneBlockPhase() {
+		@Nullable
+		OneBlockPhase phase = obm.getNextPhase(obPhase);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhaseBlocks(world.bentobox.aoneblock.dataobjects.OneBlockIslands)}.
+	 */
+	@Test
+	public void testGetNextPhaseBlocks() {
+		int phase = obm.getNextPhaseBlocks(obi);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPercentageDone(world.bentobox.aoneblock.dataobjects.OneBlockIslands)}.
+	 */
+	@Test
+	public void testGetPercentageDone() {
+		double percent = obm.getPercentageDone(obi);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getProbs(world.bentobox.aoneblock.oneblocks.OneBlockPhase)}.
+	 */
+	@Test
+	public void testGetProbs() {
+		obm.getProbs(obPhase);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getAllProbs()}.
+	 */
+	@Test
+	public void testGetAllProbs() {
+		obm.getAllProbs();
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getNextPhase(world.bentobox.aoneblock.dataobjects.OneBlockIslands)}.
+	 */
+	@Test
+	public void testGetNextPhaseOneBlockIslands() {
+		String phase = obm.getNextPhase(obi);
+	}
+
+	/**
+	 * Test method for
+	 * {@link world.bentobox.aoneblock.oneblocks.OneBlocksManager#getPhaseBlocks(world.bentobox.aoneblock.dataobjects.OneBlockIslands)}.
+	 */
+	@Test
+	public void testGetPhaseBlocks() {
+		assertEquals(-1, obm.getPhaseBlocks(obi));
+	}
+
+}