From 928f1eb1eb7b2c063572516eaf0d1aed93d83d96 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 17 Apr 2022 09:04:37 -0700 Subject: [PATCH 1/2] Version 1.20.1 (#1966) * Version 1.20.1 * Added name of the addon causing the issue. https://github.com/BentoBoxWorld/BentoBox/issues/1944 * Use world min-height for island bounding box. * Fixes a bug when fallback could not use reusable There was an issue in PanelItemTemplate that prevented fallback buttons to be "reusable" things. The issue was that reusable items were not passed to the panel item reader. * Adjusted test to try to avoid errors * Fix for random test failures. * Added 1.18.2 support * Address unnecessary PVP reports on each teleport (#1948) If a player is teleporting on the same island in the same dimension, it keeps spamming that PVP is enabled in dimension. It should be enough with sending messages when the player teleports to the island. Fixes #1885 * Fixes bug with Safe Spot Teleport (#1951) There was a bug that prevented finding a safe spot if all valid blocks were in height with the `startY` location. Reported via discord. * Fix Exception error reported by IDE I am not sure why Eclipse is saying this is an error. * Fix for kicking offline players https://github.com/BentoBoxWorld/BentoBox/issues/1950 * Add an option in SafeSpotTeleport to cancel if fail (#1952) There was no option to cancel teleportation if SafeSpotTeleport could not find a valid spot. This option could be used to avoid creating "backup" blocks in situations when teleportation is avoidable, f.e. visiting an island. * Replace peplaceAll with replace It does the same thing if the first argument is not a regex. * Use constants for common strings * Use constants for common strings * Go back to replaceAll This is required. * Clearer paster (#1953) * WIP - make easier to understand. * Small refactor of paster to make it easier to understand * Fix tabs to spaces. Sorry - new editor! * Fix tabs to spaces * Fix tab to spaces * Improve team kick command (#1957) The kick command has an unnecessary owner check. As command should be configurable by island owners, then limiting it to an owner is wrong. Add a code that allows kicking only lower-ranked players. Add message that shows who kicked from the island. Add message that shows that rank does not allow to kick. * Solve crashes with Addon#allLoaded call (#1959) If some addon has code in Addon#allLoaded that crashes the call, then it did not disable addon as well as did not call allLoaded for every other addon that was left in the list. This should be solved by adding an extra try-catch. * using java 16 syntax (#1958) * Fixes kick command (#1960) PR #1957 broke kick command and noone could kick players from teams. This should fix it. * Fixes a bug with blueprint height (#1961) Blueprint clipboard was preventing setting Y below 0 or above 255. The code was not adjusted to 1.18 changes. Reported via discord. * Fixes Lava Duplication Glitch (#1964) Due to the fact, that Obsidian Scooping uses one tick delay to remove obsidian, a player with a bucket in hand and offhand duplicated lava. To avoid that, added an extra check that ignores the interact event if a player holds a bucket in both hands, and interacted hand is offhand. Fixes #1963 * Fixes failures in obsidian cooping listener. (#1965) Failures happened after implementing #1964 Co-authored-by: BONNe Co-authored-by: Invvk <70810073+Invvk@users.noreply.github.com> --- pom.xml | 2 +- .../team/IslandTeamInviteAcceptCommand.java | 7 +- .../island/team/IslandTeamKickCommand.java | 16 +- .../api/panels/reader/TemplateReader.java | 56 +++--- .../blueprints/BlueprintClipboard.java | 41 +++-- .../bentobox/blueprints/BlueprintPaster.java | 163 ++++++++++-------- .../dataobjects/BlueprintEntity.java | 31 ++-- .../bentobox/database/objects/Island.java | 2 +- .../database/yaml/YamlDatabaseHandler.java | 4 +- .../bentobox/bentobox/hooks/VaultHook.java | 7 +- .../listeners/flags/settings/PVPListener.java | 11 ++ .../ObsidianScoopingListener.java | 11 ++ .../bentobox/managers/AddonsManager.java | 23 ++- .../bentobox/managers/LocalesManager.java | 2 +- .../bentobox/managers/PlayersManager.java | 6 +- .../bentobox/managers/WebManager.java | 2 +- .../bentobox/bentobox/util/IslandInfo.java | 26 +-- .../util/teleport/SafeSpotTeleport.java | 29 +++- .../versions/ServerCompatibility.java | 4 + src/main/resources/locales/en-US.yml | 3 +- .../team/IslandTeamKickCommandTest.java | 74 +++++++- .../listeners/flags/AbstractCommonSetup.java | 5 +- .../EntityInteractListenerTest.java | 33 ++-- .../ObsidianScoopingListenerTest.java | 5 + .../bentobox/managers/PlayersManagerTest.java | 1 + 25 files changed, 380 insertions(+), 184 deletions(-) diff --git a/pom.xml b/pom.xml index 981054aa8..329505f39 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ -LOCAL - 1.20.0 + 1.20.1 bentobox-world https://sonarcloud.io diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteAcceptCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteAcceptCommand.java index ed543bce2..bc5ceb271 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteAcceptCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteAcceptCommand.java @@ -20,6 +20,7 @@ import world.bentobox.bentobox.util.Util; */ public class IslandTeamInviteAcceptCommand extends ConfirmableCommand { + private static final String INVALID_INVITE = "commands.island.team.invite.errors.invalid-invite"; private final IslandTeamCommand itc; private UUID playerUUID; private UUID prospectiveOwnerUUID; @@ -47,7 +48,7 @@ public class IslandTeamInviteAcceptCommand extends ConfirmableCommand { // Get the island owner prospectiveOwnerUUID = itc.getInviter(playerUUID); if (prospectiveOwnerUUID == null) { - user.sendMessage("commands.island.team.invite.errors.invalid-invite"); + user.sendMessage(INVALID_INVITE); return false; } Invite invite = itc.getInvite(playerUUID); @@ -56,7 +57,7 @@ public class IslandTeamInviteAcceptCommand extends ConfirmableCommand { Island island = getIslands().getIsland(getWorld(), prospectiveOwnerUUID); String inviteUsage = getParent().getSubCommand("invite").map(CompositeCommand::getUsage).orElse(""); if (island == null || island.getRank(prospectiveOwnerUUID) < island.getRankCommand(inviteUsage)) { - user.sendMessage("commands.island.team.invite.errors.invalid-invite"); + user.sendMessage(INVALID_INVITE); itc.removeInvite(playerUUID); return false; } @@ -149,7 +150,7 @@ public class IslandTeamInviteAcceptCommand extends ConfirmableCommand { // Get the team's island Island teamIsland = getIslands().getIsland(getWorld(), prospectiveOwnerUUID); if (teamIsland == null) { - user.sendMessage("commands.island.team.invite.errors.invalid-invite"); + user.sendMessage(INVALID_INVITE); return; } if (teamIsland.getMemberSet(RanksManager.MEMBER_RANK, true).size() > getIslands().getMaxMembers(teamIsland, RanksManager.MEMBER_RANK)) { diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamKickCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamKickCommand.java index 2aab070fa..44ca46783 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamKickCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamKickCommand.java @@ -42,10 +42,6 @@ public class IslandTeamKickCommand extends ConfirmableCommand { user.sendMessage("general.errors.no-team"); return false; } - if (!user.getUniqueId().equals(getOwner(getWorld(), user))) { - user.sendMessage("general.errors.not-owner"); - return false; - } // Check rank to use command Island island = getIslands().getIsland(getWorld(), user); int rank = Objects.requireNonNull(island).getRank(user); @@ -72,6 +68,14 @@ public class IslandTeamKickCommand extends ConfirmableCommand { user.sendMessage("general.errors.not-in-team"); return false; } + + int targetRank = Objects.requireNonNull(island).getRank(targetUUID); + if (rank <= targetRank) { + user.sendMessage("commands.island.team.kick.cannot-kick-rank", + TextVariables.NAME, getPlayers().getName(targetUUID)); + return false; + } + if (!getSettings().isKickConfirmation()) { kick(user, targetUUID); return true; @@ -93,7 +97,9 @@ public class IslandTeamKickCommand extends ConfirmableCommand { if (event.isCancelled()) { return; } - target.sendMessage("commands.island.team.kick.owner-kicked", TextVariables.GAMEMODE, getAddon().getDescription().getName()); + target.sendMessage("commands.island.team.kick.player-kicked", + TextVariables.GAMEMODE, getAddon().getDescription().getName(), + TextVariables.NAME, user.getName()); getIslands().removePlayer(getWorld(), targetUUID); // Clean the target player diff --git a/src/main/java/world/bentobox/bentobox/api/panels/reader/TemplateReader.java b/src/main/java/world/bentobox/bentobox/api/panels/reader/TemplateReader.java index d98d501ad..4407ae8d2 100644 --- a/src/main/java/world/bentobox/bentobox/api/panels/reader/TemplateReader.java +++ b/src/main/java/world/bentobox/bentobox/api/panels/reader/TemplateReader.java @@ -41,6 +41,12 @@ public class TemplateReader private static final String BORDER = "border"; private static final String FORCE_SHOWN = "force-shown"; private static final String FALLBACK = "fallback"; + private static final String YML = ".yml"; + private static final String ACTIONS = "actions"; + private static final String TOOLTIP = "tooltip"; + private static final String CLICK_TYPE = "click-type"; + private static final String CONTENT = "content"; + private static final String TYPE = "type"; /** @@ -73,7 +79,7 @@ public class TemplateReader return null; } - File file = new File(panelLocation, templateName.endsWith(".yml") ? templateName : templateName + ".yml"); + File file = new File(panelLocation, templateName.endsWith(YML) ? templateName : templateName + YML); if (!file.exists()) { @@ -82,7 +88,7 @@ public class TemplateReader } final String panelKey = file.getAbsolutePath() + ":" + panelName; - + // Check if panel is already crafted. if (TemplateReader.loadedPanels.containsKey(panelKey)) { @@ -125,7 +131,7 @@ public class TemplateReader String title = configurationSection.getString(TITLE); Panel.Type type = - Enums.getIfPresent(Panel.Type.class, configurationSection.getString("type", "INVENTORY")). + Enums.getIfPresent(Panel.Type.class, configurationSection.getString(TYPE, "INVENTORY")). or(Panel.Type.INVENTORY); PanelTemplateRecord.TemplateItem borderItem = null; @@ -194,7 +200,7 @@ public class TemplateReader PanelTemplateRecord template = new PanelTemplateRecord(type, title, borderItem, backgroundItem, forcedRows); // Read content - ConfigurationSection content = configurationSection.getConfigurationSection("content"); + ConfigurationSection content = configurationSection.getConfigurationSection(CONTENT); if (content == null) { @@ -219,7 +225,7 @@ public class TemplateReader // If it contains a section, then build a new button template from it. template.addButtonTemplate(rowIndex, columnIndex, - readPanelItemTemplate(line.getConfigurationSection(String.valueOf(columnIndex + 1)))); + readPanelItemTemplate(line.getConfigurationSection(String.valueOf(columnIndex + 1)), null, panelItemDataMap)); } else if (line.isString(String.valueOf(columnIndex + 1))) { @@ -337,9 +343,9 @@ public class TemplateReader } // Read Click data - if (section.isConfigurationSection("actions")) + if (section.isConfigurationSection(ACTIONS)) { - ConfigurationSection actionSection = section.getConfigurationSection("actions"); + ConfigurationSection actionSection = section.getConfigurationSection(ACTIONS); if (actionSection != null) { @@ -354,9 +360,9 @@ public class TemplateReader { ItemTemplateRecord.ActionRecords actionData = new ItemTemplateRecord.ActionRecords(clickType, - actionDataSection.getString("type"), - actionDataSection.getString("content"), - actionDataSection.getString("tooltip")); + actionDataSection.getString(TYPE), + actionDataSection.getString(CONTENT), + actionDataSection.getString(TOOLTIP)); itemRecord.addAction(actionData); } } @@ -364,42 +370,42 @@ public class TemplateReader { ConfigurationSection actionDataSection = actionSection.getConfigurationSection(actionKey); - if (actionDataSection != null && actionDataSection.contains("click-type")) + if (actionDataSection != null && actionDataSection.contains(CLICK_TYPE)) { clickType = Enums.getIfPresent(ClickType.class, - actionDataSection.getString("click-type", "UNKNOWN").toUpperCase()). - or(ClickType.UNKNOWN); - + actionDataSection.getString(CLICK_TYPE, "UNKNOWN").toUpperCase()). + or(ClickType.UNKNOWN); + ItemTemplateRecord.ActionRecords actionData = - new ItemTemplateRecord.ActionRecords(clickType, - actionKey, - actionDataSection.getString("content"), - actionDataSection.getString("tooltip")); + new ItemTemplateRecord.ActionRecords(clickType, + actionKey, + actionDataSection.getString(CONTENT), + actionDataSection.getString(TOOLTIP)); itemRecord.addAction(actionData); } } }); } } - else if (section.isList("actions")) + else if (section.isList(ACTIONS)) { // Read Click data as list which allows to have duplicate click types. - List> actionList = section.getMapList("actions"); + List> actionList = section.getMapList(ACTIONS); if (!actionList.isEmpty()) { actionList.forEach(valueMap -> { ClickType clickType = Enums.getIfPresent(ClickType.class, - String.valueOf(valueMap.get("click-type")).toUpperCase()).orNull(); + String.valueOf(valueMap.get(CLICK_TYPE)).toUpperCase()).orNull(); if (clickType != null) { ItemTemplateRecord.ActionRecords actionData = - new ItemTemplateRecord.ActionRecords(clickType, - valueMap.containsKey("type") ? String.valueOf(valueMap.get("type")) : null, - valueMap.containsKey("content") ? String.valueOf(valueMap.get("content")) : null, - valueMap.containsKey("tooltip") ? String.valueOf(valueMap.get("tooltip")) : null); + new ItemTemplateRecord.ActionRecords(clickType, + valueMap.containsKey(TYPE) ? String.valueOf(valueMap.get(TYPE)) : null, + valueMap.containsKey(CONTENT) ? String.valueOf(valueMap.get(CONTENT)) : null, + valueMap.containsKey(TOOLTIP) ? String.valueOf(valueMap.get(TOOLTIP)) : null); itemRecord.addAction(actionData); } }); diff --git a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java index 7d0c01b82..a3daa230c 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java @@ -257,8 +257,8 @@ public class BlueprintClipboard { } // Banners - if (blockState instanceof Banner) { - b.setBannerPatterns(((Banner) blockState).getPatterns()); + if (blockState instanceof Banner banner) { + b.setBannerPatterns(banner.getPatterns()); } return b; @@ -282,8 +282,8 @@ public class BlueprintClipboard { BlueprintEntity bpe = new BlueprintEntity(); bpe.setType(entity.getType()); bpe.setCustomName(entity.getCustomName()); - if (entity instanceof Villager) { - setVillager(entity, bpe); + if (entity instanceof Villager villager) { + setVillager(villager, bpe); } if (entity instanceof Colorable c) { if (c.getColor() != null) { @@ -321,11 +321,10 @@ public class BlueprintClipboard { /** * Set the villager stats - * @param entity - villager + * @param v - villager * @param bpe - Blueprint Entity */ - private void setVillager(LivingEntity entity, BlueprintEntity bpe) { - Villager v = (Villager)entity; + private void setVillager(Villager v, BlueprintEntity bpe) { bpe.setExperience(v.getVillagerExperience()); bpe.setLevel(v.getVillagerLevel()); bpe.setProfession(v.getProfession()); @@ -371,11 +370,17 @@ public class BlueprintClipboard { public void setPos1(@Nullable Location pos1) { origin = null; if (pos1 != null) { - if (pos1.getBlockY() < 0) { - pos1.setY(0); + final int minHeight = pos1.getWorld() == null ? 0 : pos1.getWorld().getMinHeight(); + final int maxHeight = pos1.getWorld() == null ? 255 : pos1.getWorld().getMaxHeight(); + + if (pos1.getBlockY() < minHeight) + { + pos1.setY(minHeight); } - if (pos1.getBlockY() > 255) { - pos1.setY(255); + + if (pos1.getBlockY() > maxHeight) + { + pos1.setY(maxHeight); } } this.pos1 = pos1; @@ -387,11 +392,17 @@ public class BlueprintClipboard { public void setPos2(@Nullable Location pos2) { origin = null; if (pos2 != null) { - if (pos2.getBlockY() < 0) { - pos2.setY(0); + final int minHeight = pos2.getWorld() == null ? 0 : pos2.getWorld().getMinHeight(); + final int maxHeight = pos2.getWorld() == null ? 255 : pos2.getWorld().getMaxHeight(); + + if (pos2.getBlockY() < minHeight) + { + pos2.setY(minHeight); } - if (pos2.getBlockY() > 255) { - pos2.setY(255); + + if (pos2.getBlockY() > maxHeight) + { + pos2.setY(maxHeight); } } this.pos2 = pos2; diff --git a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java index 6528f58bd..a3be6906c 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java @@ -134,99 +134,110 @@ public class BlueprintPaster { this.location = island.getProtectionCenter().toVector().subtract(off).toLocation(world); } + private record Bits(Map blocks, + Map attached, + Map> entities, + Iterator> it, + Iterator> it2, + Iterator>> it3, + int pasteSpeed) {} /** * The main pasting method */ public CompletableFuture paste() { CompletableFuture result = new CompletableFuture<>(); // Iterators for the various maps to paste - Map blocks = blueprint.getBlocks() == null ? Collections.emptyMap() : blueprint.getBlocks(); - Map attached = blueprint.getAttached() == null ? Collections.emptyMap() : blueprint.getAttached(); - Map> entities = blueprint.getEntities() == null ? Collections.emptyMap() : blueprint.getEntities(); - Iterator> it = blocks.entrySet().iterator(); - Iterator> it2 = attached.entrySet().iterator(); - Iterator>> it3 = entities.entrySet().iterator(); + final Map blocks = blueprint.getBlocks() == null ? Collections.emptyMap() : blueprint.getBlocks(); + final Map attached = blueprint.getAttached() == null ? Collections.emptyMap() : blueprint.getAttached(); + final Map> entities = blueprint.getEntities() == null ? Collections.emptyMap() : blueprint.getEntities(); // Initial state & speed pasteState = PasteState.CHUNK_LOAD; - final int pasteSpeed = plugin.getSettings().getPasteSpeed(); // If this is an island OVERWORLD paste, get the island owner. final Optional owner = Optional.ofNullable(island) .filter(i -> location.getWorld().getEnvironment().equals(World.Environment.NORMAL)) .map(i -> User.getInstance(i.getOwner())); // Tell the owner we're pasting blocks and how much time it might take - owner.ifPresent(user -> { - // Estimated time: - double total = (double) blocks.size() + attached.size() + entities.size(); - BigDecimal time = BigDecimal.valueOf(total / (pasteSpeed * 20.0D) + (chunkLoadTime / 1000.0D)).setScale(1, RoundingMode.UP); - user.sendMessage("commands.island.create.pasting.estimated-time", TextVariables.NUMBER, String.valueOf(time.doubleValue())); - // We're pasting blocks! - user.sendMessage("commands.island.create.pasting.blocks", TextVariables.NUMBER, String.valueOf(blocks.size() + attached.size())); - }); - - pastingTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { - long timer = System.currentTimeMillis(); - int count = 0; - if (pasteState.equals(PasteState.CHUNK_LOAD)) { - pasteState = PasteState.CHUNK_LOADING; - // Load chunk - Util.getChunkAtAsync(location).thenRun(() -> { - pasteState = PasteState.BLOCKS; - long duration = System.currentTimeMillis() - timer; - if (duration > chunkLoadTime) { - chunkLoadTime = duration; - } - }); - } - while (pasteState.equals(PasteState.BLOCKS) && count < pasteSpeed && it.hasNext()) { - pasteBlock(location, it.next()); - count++; - } - while (pasteState.equals(PasteState.ATTACHMENTS) && count < pasteSpeed && it2.hasNext()) { - pasteBlock(location, it2.next()); - count++; - } - while (pasteState.equals(PasteState.ENTITIES) && count < pasteSpeed && it3.hasNext()) { - pasteEntity(location, it3.next()); - count++; - } - // STATE SHIFT - if (pasteState.equals(PasteState.BLOCKS) && !it.hasNext()) { - // Blocks done - // Next paste attachments - pasteState = PasteState.ATTACHMENTS; - } - else if (pasteState.equals(PasteState.ATTACHMENTS) && !it2.hasNext()) { - // Attachments done. Next paste entities - pasteState = PasteState.ENTITIES; - if (entities.size() != 0) { - owner.ifPresent(user -> user.sendMessage("commands.island.create.pasting.entities", TextVariables.NUMBER, String.valueOf(entities.size()))); - } - } - else if (pasteState.equals(PasteState.ENTITIES) && !it3.hasNext()) { - pasteState = PasteState.DONE; - owner.ifPresent(user -> user.sendMessage("commands.island.create.pasting.done")); - } - else if (pasteState.equals(PasteState.DONE)) { - // All done. Cancel task - // Set pos1 and 2 if this was a clipboard paste - if (island == null && clipboard != null) { - clipboard.setPos1(pos1); - clipboard.setPos2(pos2); - } - result.complete(true); - pasteState = PasteState.CANCEL; - } else if (pasteState.equals(PasteState.CANCEL)) { - // This state makes sure the follow-on task only ever runs once - pastingTask.cancel(); - result.complete(true); - } - }, 0L, 1L); + owner.ifPresent(user -> tellOwner(user, blocks.size(), attached.size(), entities.size(), plugin.getSettings().getPasteSpeed())); + Bits bits = new Bits(blocks, attached, entities, + blocks.entrySet().iterator(), attached.entrySet().iterator(), entities.entrySet().iterator(), + plugin.getSettings().getPasteSpeed()); + pastingTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> pasterTask(result, owner, bits), 0L, 1L); return result; } + private void pasterTask(CompletableFuture result, Optional owner, Bits bits) { + final int pasteSpeed = plugin.getSettings().getPasteSpeed(); + + long timer = System.currentTimeMillis(); + int count = 0; + if (pasteState.equals(PasteState.CHUNK_LOAD)) { + pasteState = PasteState.CHUNK_LOADING; + // Load chunk + Util.getChunkAtAsync(location).thenRun(() -> { + pasteState = PasteState.BLOCKS; + long duration = System.currentTimeMillis() - timer; + if (duration > chunkLoadTime) { + chunkLoadTime = duration; + } + }); + } + while (pasteState.equals(PasteState.BLOCKS) && count < pasteSpeed && bits.it.hasNext()) { + pasteBlock(location, bits.it.next()); + count++; + } + while (pasteState.equals(PasteState.ATTACHMENTS) && count < pasteSpeed && bits.it2.hasNext()) { + pasteBlock(location, bits.it2.next()); + count++; + } + while (pasteState.equals(PasteState.ENTITIES) && count < pasteSpeed && bits.it3.hasNext()) { + pasteEntity(location, bits.it3.next()); + count++; + } + // STATE SHIFT + if (pasteState.equals(PasteState.BLOCKS) && !bits.it.hasNext()) { + // Blocks done + // Next paste attachments + pasteState = PasteState.ATTACHMENTS; + } + else if (pasteState.equals(PasteState.ATTACHMENTS) && !bits.it2.hasNext()) { + // Attachments done. Next paste entities + pasteState = PasteState.ENTITIES; + if (bits.entities.size() != 0) { + owner.ifPresent(user -> user.sendMessage("commands.island.create.pasting.entities", TextVariables.NUMBER, String.valueOf(bits.entities.size()))); + } + } + else if (pasteState.equals(PasteState.ENTITIES) && !bits.it3.hasNext()) { + pasteState = PasteState.DONE; + owner.ifPresent(user -> user.sendMessage("commands.island.create.pasting.done")); + } + else if (pasteState.equals(PasteState.DONE)) { + // All done. Cancel task + // Set pos1 and 2 if this was a clipboard paste + if (island == null && clipboard != null) { + clipboard.setPos1(pos1); + clipboard.setPos2(pos2); + } + pasteState = PasteState.CANCEL; + result.complete(true); + } else if (pasteState.equals(PasteState.CANCEL)) { + // This state makes sure the follow-on task only ever runs once + pastingTask.cancel(); + result.complete(true); + } + } + + private void tellOwner(User user, int blocksSize, int attachedSize, int entitiesSize, int pasteSpeed) { + // Estimated time: + double total = (double) blocksSize + attachedSize + entitiesSize; + BigDecimal time = BigDecimal.valueOf(total / (pasteSpeed * 20.0D) + (chunkLoadTime / 1000.0D)).setScale(1, RoundingMode.UP); + user.sendMessage("commands.island.create.pasting.estimated-time", TextVariables.NUMBER, String.valueOf(time.doubleValue())); + // We're pasting blocks! + user.sendMessage("commands.island.create.pasting.blocks", TextVariables.NUMBER, String.valueOf(blocksSize + attachedSize)); + } + private void pasteBlock(Location location, Entry entry) { World world = location.getWorld(); Location pasteTo = location.clone().add(entry.getKey()); @@ -294,8 +305,8 @@ public class BlueprintPaster { writeSign(block, bpBlock.getSignLines(), bpBlock.isGlowingText()); } // Chests, in general - if (bs instanceof InventoryHolder) { - Inventory ih = ((InventoryHolder)bs).getInventory(); + if (bs instanceof InventoryHolder holder) { + Inventory ih = holder.getInventory(); // Double chests are pasted as two blocks so inventory is filled twice. // This code stops over-filling for the first block. bpBlock.getInventory().forEach(ih::setItem); diff --git a/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java b/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java index e173f6b86..6a5927ddb 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java @@ -56,23 +56,23 @@ public class BlueprintEntity { * @since 1.8.0 */ public void configureEntity(Entity e) { - if (e instanceof Villager) { - setVillager(e); + if (e instanceof Villager villager) { + setVillager(villager); } - if (e instanceof Colorable) { - ((Colorable) e).setColor(color); + if (e instanceof Colorable c) { + c.setColor(color); } - if (tamed != null && e instanceof Tameable) { - ((Tameable)e).setTamed(tamed); + if (tamed != null && e instanceof Tameable tameable) { + tameable.setTamed(tamed); } - if (chest != null && e instanceof ChestedHorse) { - ((ChestedHorse)e).setCarryingChest(chest); + if (chest != null && e instanceof ChestedHorse chestedHorse) { + chestedHorse.setCarryingChest(chest); } - if (adult != null && e instanceof Ageable) { + if (adult != null && e instanceof Ageable ageable) { if (adult) { - ((Ageable)e).setAdult(); + ageable.setAdult(); } else { - ((Ageable)e).setBaby(); + ageable.setBaby(); } } if (e instanceof AbstractHorse horse) { @@ -81,18 +81,17 @@ public class BlueprintEntity { inventory.forEach(horse.getInventory()::setItem); } } - if (style != null && e instanceof Horse) { - ((Horse)e).setStyle(style); + if (style != null && e instanceof Horse horse) { + horse.setStyle(style); } } /** - * @param e - villager + * @param v - villager * @since 1.16.0 */ - private void setVillager(Entity e) { - Villager v = (Villager)e; + private void setVillager(Villager v) { v.setProfession(profession == null ? Profession.NONE : profession); v.setVillagerExperience(experience == null ? 0 : experience); v.setVillagerLevel(level == null ? 0 : level); diff --git a/src/main/java/world/bentobox/bentobox/database/objects/Island.java b/src/main/java/world/bentobox/bentobox/database/objects/Island.java index 76e97a97e..4b463fe33 100644 --- a/src/main/java/world/bentobox/bentobox/database/objects/Island.java +++ b/src/main/java/world/bentobox/bentobox/database/objects/Island.java @@ -698,7 +698,7 @@ public class Island implements DataObject, MetaDataAble { * @since 1.5.2 */ public BoundingBox getBoundingBox() { - return new BoundingBox(getMinX(), 0.0D, getMinZ(), getMaxX(), world.getMaxHeight(), getMaxZ()); + return new BoundingBox(getMinX(), world.getMinHeight(), getMinZ(), getMaxX(), world.getMaxHeight(), getMaxZ()); } /** diff --git a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java index 05b42d537..008ff23a9 100644 --- a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java @@ -289,7 +289,7 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // Convert any serialized dots back to dots // In YAML dots . cause a lot of problems, so I serialize them as :dot: // There may be a better way to do this. - key = key.replaceAll(":dot:", "."); + key = key.replace(":dot:", "."); Object mapKey = deserialize(key,Class.forName(keyType.getTypeName())); if (mapKey == null) { continue; @@ -452,7 +452,7 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { for (Entry object : value.entrySet()) { // Serialize all key and values String key = (String)serialize(object.getKey()); - key = key.replaceAll("\\.", ":dot:"); + key = key.replace("\\.", ":dot:"); result.put(key, serialize(object.getValue())); } // Save the list in the config file diff --git a/src/main/java/world/bentobox/bentobox/hooks/VaultHook.java b/src/main/java/world/bentobox/bentobox/hooks/VaultHook.java index b4e6002c0..acece9e21 100644 --- a/src/main/java/world/bentobox/bentobox/hooks/VaultHook.java +++ b/src/main/java/world/bentobox/bentobox/hooks/VaultHook.java @@ -17,6 +17,7 @@ import world.bentobox.bentobox.api.user.User; public class VaultHook extends Hook { private static final String AMOUNT_MUST_BE_POSITIVE = "Amount must be positive."; + private static final String PLAYER_OR_OFFLINEPLAYER_REQUIRED = "User must be a Player or an OfflinePlayer"; private Economy economy; public VaultHook() { @@ -109,7 +110,7 @@ public class VaultHook extends Hook { */ public EconomyResponse withdraw(User user, double amount, World world) { if (!user.isOfflinePlayer()) { - throw new IllegalArgumentException("User must be a Player or an OfflinePlayer"); + throw new IllegalArgumentException(PLAYER_OR_OFFLINEPLAYER_REQUIRED); } if (amount < 0.0D) { throw new IllegalArgumentException(AMOUNT_MUST_BE_POSITIVE); @@ -149,7 +150,7 @@ public class VaultHook extends Hook { */ public EconomyResponse deposit(User user, double amount, World world) { if (!user.isOfflinePlayer()) { - throw new IllegalArgumentException("User must be a Player or an OfflinePlayer"); + throw new IllegalArgumentException(PLAYER_OR_OFFLINEPLAYER_REQUIRED); } if (amount < 0.0D) { throw new IllegalArgumentException(AMOUNT_MUST_BE_POSITIVE); @@ -198,7 +199,7 @@ public class VaultHook extends Hook { } if (!user.isOfflinePlayer()) { - throw new IllegalArgumentException("User must be a Player or an OfflinePlayer"); + throw new IllegalArgumentException(PLAYER_OR_OFFLINEPLAYER_REQUIRED); } if (world == null) diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/settings/PVPListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/settings/PVPListener.java index 95cf5ee01..c0c46b964 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/settings/PVPListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/settings/PVPListener.java @@ -30,6 +30,7 @@ import world.bentobox.bentobox.api.events.flags.FlagSettingChangeEvent; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.flags.FlagListener; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.lists.Flags; import world.bentobox.bentobox.managers.RanksManager; @@ -239,10 +240,20 @@ public class PVPListener extends FlagListener { if (e.getTo() == null) { return; } + + // Get previous island to skip reporting if island is not changed. + Island previousIsland = this.getIslands().getIslandAt(e.getFrom()).orElse(null); + getIslands().getIslandAt(e.getTo()).ifPresent(island -> { if (island.getMemberSet(RanksManager.COOP_RANK).contains(e.getPlayer().getUniqueId())) { return; } + + if (e.getFrom().getWorld() == e.getTo().getWorld() && island == previousIsland) { + // do not report as it is the same world and same island. + return; + } + if (island.isAllowed(Flags.PVP_OVERWORLD)) { alertUser(e.getPlayer(), Flags.PVP_OVERWORLD); } diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/worldsettings/ObsidianScoopingListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/worldsettings/ObsidianScoopingListener.java index df67b0a5c..0900d396a 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/worldsettings/ObsidianScoopingListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/worldsettings/ObsidianScoopingListener.java @@ -15,6 +15,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import world.bentobox.bentobox.BentoBox; @@ -54,6 +55,16 @@ public class ObsidianScoopingListener extends FlagListener { || e.getClickedBlock().getRelative(e.getBlockFace()).getType().equals(Material.WATER)) { return false; } + + if (Material.BUCKET.equals(e.getPlayer().getInventory().getItemInOffHand().getType()) && + Material.BUCKET.equals(e.getPlayer().getInventory().getItemInMainHand().getType()) && + EquipmentSlot.OFF_HAND.equals(e.getHand())) + { + // If player is holding bucket in both hands, then allow to interact only with main hand. + // Prevents lava duplication glitch. + return false; + } + return lookForLava(e); } diff --git a/src/main/java/world/bentobox/bentobox/managers/AddonsManager.java b/src/main/java/world/bentobox/bentobox/managers/AddonsManager.java index 0026d94c3..0d47ed93e 100644 --- a/src/main/java/world/bentobox/bentobox/managers/AddonsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/AddonsManager.java @@ -666,7 +666,28 @@ public class AddonsManager { * @since 1.8.0 */ public void allLoaded() { - addons.forEach(Addon::allLoaded); + this.getEnabledAddons().forEach(this::allLoaded); } + + /** + * This method calls Addon#allLoaded in safe manner. If for some reason addon crashes on Addon#allLoaded, then + * it will disable itself without harming other addons. + * @param addon Addon that should trigger Addon#allLoaded method. + */ + private void allLoaded(@NonNull Addon addon) { + try { + addon.allLoaded(); + } catch (NoClassDefFoundError | NoSuchMethodError | NoSuchFieldError e) { + // Looks like the addon is incompatible, because it tries to refer to missing classes... + this.handleAddonIncompatibility(addon, e); + // Disable addon. + this.disable(addon); + } catch (Exception e) { + // Unhandled exception. We'll give a bit of debug here. + this.handleAddonError(addon, e); + // Disable addon. + this.disable(addon); + } + } } diff --git a/src/main/java/world/bentobox/bentobox/managers/LocalesManager.java b/src/main/java/world/bentobox/bentobox/managers/LocalesManager.java index 934148c78..46f67d3bd 100644 --- a/src/main/java/world/bentobox/bentobox/managers/LocalesManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/LocalesManager.java @@ -179,7 +179,7 @@ public class LocalesManager { } catch (InvalidConfigurationException e) { plugin.logError("Could not update locale file '" + lf + "' due to it being malformed: " + e.getMessage()); } catch (Exception e) { - plugin.logError("Error updating locale file '" + lf + "': " + e.getMessage()); + plugin.logError("Error updating locale file for " + addon.getDescription().getName() + "'s '" + lf + "': " + e.getMessage()); plugin.logStacktrace(e); } } diff --git a/src/main/java/world/bentobox/bentobox/managers/PlayersManager.java b/src/main/java/world/bentobox/bentobox/managers/PlayersManager.java index fc1c7e44c..1ab0b7d86 100644 --- a/src/main/java/world/bentobox/bentobox/managers/PlayersManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/PlayersManager.java @@ -577,17 +577,17 @@ public class PlayersManager { plugin.getVault().ifPresent(vault -> vault.withdraw(target, vault.getBalance(target), world)); } // Reset the health - if (plugin.getIWM().isOnLeaveResetHealth(world)) { + if (plugin.getIWM().isOnLeaveResetHealth(world) && target.isPlayer()) { Util.resetHealth(target.getPlayer()); } // Reset the hunger - if (plugin.getIWM().isOnLeaveResetHunger(world)) { + if (plugin.getIWM().isOnLeaveResetHunger(world) && target.isPlayer()) { target.getPlayer().setFoodLevel(20); } // Reset the XP - if (plugin.getIWM().isOnLeaveResetXP(world)) { + if (plugin.getIWM().isOnLeaveResetXP(world) && target.isPlayer()) { target.getPlayer().setTotalExperience(0); } // Save player diff --git a/src/main/java/world/bentobox/bentobox/managers/WebManager.java b/src/main/java/world/bentobox/bentobox/managers/WebManager.java index dbd5ef1d5..c04d1e9c5 100644 --- a/src/main/java/world/bentobox/bentobox/managers/WebManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/WebManager.java @@ -196,7 +196,7 @@ public class WebManager { @NonNull private String getContent(@NonNull GitHubRepository repo, String fileName) { try { - String content = repo.getContent(fileName).getContent().replaceAll("\\n", ""); + String content = repo.getContent(fileName).getContent().replaceAll("\\n", ""); // replaceAll is required here return new String(Base64.getDecoder().decode(content), StandardCharsets.UTF_8); } catch (IllegalAccessException e) { // Fail silently diff --git a/src/main/java/world/bentobox/bentobox/util/IslandInfo.java b/src/main/java/world/bentobox/bentobox/util/IslandInfo.java index 5e3e76414..e772b6ba5 100644 --- a/src/main/java/world/bentobox/bentobox/util/IslandInfo.java +++ b/src/main/java/world/bentobox/bentobox/util/IslandInfo.java @@ -21,7 +21,9 @@ import world.bentobox.bentobox.managers.RanksManager; */ public class IslandInfo { - private final BentoBox plugin; + private static final String XZ1 = "[xz1]"; + private static final String RANGE = "[range]"; + private final BentoBox plugin; private final Island island; private final @Nullable UUID owner; private final World world; @@ -57,23 +59,23 @@ public class IslandInfo { try { String dateTimeFormat = plugin.getLocalesManager().get("commands.admin.info.last-login-date-time-format"); formattedDate = new SimpleDateFormat(dateTimeFormat).format(new Date(lastPlayed)); - } catch (NullPointerException | IllegalArgumentException ignored) { + } catch (Exception ignored) { formattedDate = new Date(lastPlayed).toString(); } user.sendMessage("commands.admin.info.last-login","[date]", formattedDate); - user.sendMessage("commands.admin.info.deaths", "[number]", String.valueOf(plugin.getPlayers().getDeaths(world, owner))); + user.sendMessage("commands.admin.info.deaths", TextVariables.NUMBER, String.valueOf(plugin.getPlayers().getDeaths(world, owner))); String resets = String.valueOf(plugin.getPlayers().getResets(world, owner)); String total = plugin.getIWM().getResetLimit(world) < 0 ? "Unlimited" : String.valueOf(plugin.getIWM().getResetLimit(world)); - user.sendMessage("commands.admin.info.resets-left", "[number]", resets, "[total]", total); + user.sendMessage("commands.admin.info.resets-left", TextVariables.NUMBER, resets, "[total]", total); // Show team members showMembers(user); } Vector location = island.getProtectionCenter().toVector(); user.sendMessage("commands.admin.info.island-protection-center", TextVariables.XYZ, Util.xyz(location)); user.sendMessage("commands.admin.info.island-center", TextVariables.XYZ, Util.xyz(island.getCenter().toVector())); - user.sendMessage("commands.admin.info.island-coords", "[xz1]", Util.xyz(new Vector(island.getMinX(), 0, island.getMinZ())), "[xz2]", Util.xyz(new Vector(island.getMaxX(), 0, island.getMaxZ()))); - user.sendMessage("commands.admin.info.protection-range", "[range]", String.valueOf(island.getProtectionRange())); + user.sendMessage("commands.admin.info.island-coords", XZ1, Util.xyz(new Vector(island.getMinX(), 0, island.getMinZ())), "[xz2]", Util.xyz(new Vector(island.getMaxX(), 0, island.getMaxZ()))); + user.sendMessage("commands.admin.info.protection-range", RANGE, String.valueOf(island.getProtectionRange())); if (!island.getBonusRanges().isEmpty()) { user.sendMessage("commands.admin.info.protection-range-bonus-title"); } @@ -84,8 +86,8 @@ public class IslandInfo { user.sendMessage(brb.getMessage(), TextVariables.NUMBER, String.valueOf(brb.getRange())); } }); - user.sendMessage("commands.admin.info.max-protection-range", "[range]", String.valueOf(island.getMaxEverProtectionRange())); - user.sendMessage("commands.admin.info.protection-coords", "[xz1]", Util.xyz(new Vector(island.getMinProtectedX(), 0, island.getMinProtectedZ())), "[xz2]", Util.xyz(new Vector(island.getMaxProtectedX() - 1, 0, island.getMaxProtectedZ() - 1))); + user.sendMessage("commands.admin.info.max-protection-range", RANGE, String.valueOf(island.getMaxEverProtectionRange())); + user.sendMessage("commands.admin.info.protection-coords", XZ1, Util.xyz(new Vector(island.getMinProtectedX(), 0, island.getMinProtectedZ())), "[xz2]", Util.xyz(new Vector(island.getMaxProtectedX() - 1, 0, island.getMaxProtectedZ() - 1))); if (island.isSpawn()) { user.sendMessage("commands.admin.info.is-spawn"); } @@ -110,17 +112,17 @@ public class IslandInfo { user.sendMessage("commands.admin.info.unowned"); } else { user.sendMessage("commands.admin.info.owner", "[owner]", plugin.getPlayers().getName(owner), TextVariables.UUID, owner.toString()); - user.sendMessage("commands.admin.info.deaths", "[number]", String.valueOf(plugin.getPlayers().getDeaths(world, owner))); + user.sendMessage("commands.admin.info.deaths", TextVariables.NUMBER, String.valueOf(plugin.getPlayers().getDeaths(world, owner))); String resets = String.valueOf(plugin.getPlayers().getResets(world, owner)); String total = plugin.getIWM().getResetLimit(world) < 0 ? "Unlimited" : String.valueOf(plugin.getIWM().getResetLimit(world)); - user.sendMessage("commands.admin.info.resets-left", "[number]", resets, "[total]", total); + user.sendMessage("commands.admin.info.resets-left", TextVariables.NUMBER, resets, "[total]", total); // Show team members showMembers(user); } Vector location = island.getProtectionCenter().toVector(); user.sendMessage("commands.admin.info.island-center", TextVariables.XYZ, Util.xyz(location)); - user.sendMessage("commands.admin.info.protection-range", "[range]", String.valueOf(island.getProtectionRange())); - user.sendMessage("commands.admin.info.protection-coords", "[xz1]", Util.xyz(new Vector(island.getMinProtectedX(), 0, island.getMinProtectedZ())), "[xz2]", Util.xyz(new Vector(island.getMaxProtectedX() - 1, 0, island.getMaxProtectedZ() - 1))); + user.sendMessage("commands.admin.info.protection-range", RANGE, String.valueOf(island.getProtectionRange())); + user.sendMessage("commands.admin.info.protection-coords", XZ1, Util.xyz(new Vector(island.getMinProtectedX(), 0, island.getMinProtectedZ())), "[xz2]", Util.xyz(new Vector(island.getMaxProtectedX() - 1, 0, island.getMaxProtectedZ() - 1))); if (island.isSpawn()) { user.sendMessage("commands.admin.info.is-spawn"); } diff --git a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java index a413010e5..3e427a0a2 100644 --- a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java +++ b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java @@ -51,6 +51,7 @@ public class SafeSpotTeleport { private final AtomicBoolean checking = new AtomicBoolean(); private BukkitTask task; private boolean portal; + private boolean cancelIfFail; // Locations private Location bestSpot; private Iterator> chunksToScanIterator; @@ -73,6 +74,7 @@ public class SafeSpotTeleport { this.result = builder.getResult(); this.world = Objects.requireNonNull(location.getWorld()); this.maxHeight = world.getMaxHeight() - 20; + this.cancelIfFail = builder.isCancelIfFail(); // Try to go Util.getChunkAtAsync(location).thenRun(() -> tryToGo(builder.getFailureMessage())); } @@ -150,7 +152,7 @@ public class SafeSpotTeleport { if (!plugin.getIWM().inWorld(entity.getLocation())) { // Last resort player.performCommand("spawn"); - } else { + } else if (!this.cancelIfFail) { // Create a spot for the player to be if (world.getEnvironment().equals(Environment.NETHER)) { makeAndTeleport(Material.NETHERRACK); @@ -226,7 +228,7 @@ public class SafeSpotTeleport { // Check the safe spot at the current height for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { - if (minY >= startY && checkBlock(chunk, x, startY, z)) { + if (minY <= startY && checkBlock(chunk, x, startY, z)) { return true; } maxY = Math.max(chunk.getHighestBlockYAt(x, z), maxY); @@ -336,6 +338,7 @@ public class SafeSpotTeleport { private Location location; private Runnable runnable; private Runnable failRunnable; + private boolean cancelIfFail; public Builder(BentoBox plugin) { this.plugin = plugin; @@ -461,6 +464,20 @@ public class SafeSpotTeleport { return new SafeSpotTeleport(this); } + + /** + * This method allows stopping "safe" block generation if teleportation fails. + * @param cancelIfFail - value for canceling + * @return Builder + * @since 1.20.1 + */ + public Builder cancelIfFail(boolean cancelIfFail) + { + this.cancelIfFail = cancelIfFail; + return this; + } + + /** * The task to run after the player is safely teleported. * @@ -557,5 +574,13 @@ public class SafeSpotTeleport { } + /** + * @return the cancelIfFail + * @since 1.20.1 + */ + public boolean isCancelIfFail() + { + return this.cancelIfFail; + } } } diff --git a/src/main/java/world/bentobox/bentobox/versions/ServerCompatibility.java b/src/main/java/world/bentobox/bentobox/versions/ServerCompatibility.java index cc2ff5bc8..6fd5d1011 100644 --- a/src/main/java/world/bentobox/bentobox/versions/ServerCompatibility.java +++ b/src/main/java/world/bentobox/bentobox/versions/ServerCompatibility.java @@ -194,6 +194,10 @@ public class ServerCompatibility { * @since 1.19.0 */ V1_18_1(Compatibility.COMPATIBLE), + /** + * @since 1.20.1 + */ + V1_18_2(Compatibility.COMPATIBLE), ; private final Compatibility compatibility; diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 2da92e94e..51c28f0c9 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -652,8 +652,9 @@ commands: kick: description: "remove a member from your island" parameters: "" - owner-kicked: "&c The owner kicked you from the island in [gamemode]!" + player-kicked: "&c The [name] kicked you from the island in [gamemode]!" cannot-kick: "&c You cannot kick yourself!" + cannot-kick-rank: "&c Your rank does not allow to kick [name]!" success: "&b [name] &a has been kicked from your island." demote: description: "demote a player on your island down a rank" diff --git a/src/test/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamKickCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamKickCommandTest.java index 3e1c3ac28..b51cec80c 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamKickCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamKickCommandTest.java @@ -184,6 +184,10 @@ public class IslandTeamKickCommandTest { RanksManager rm = new RanksManager(); when(plugin.getRanksManager()).thenReturn(rm); + // Ranks + when(island.getRank(uuid)).thenReturn(RanksManager.OWNER_RANK); + when(island.getRank(user)).thenReturn(RanksManager.OWNER_RANK); + when(island.getRank(notUUID)).thenReturn(RanksManager.MEMBER_RANK); } @After @@ -206,11 +210,75 @@ public class IslandTeamKickCommandTest { * Test method for {@link IslandTeamKickCommand#execute(User, String, java.util.List)} */ @Test - public void testExecuteNotTeamOwner() { - when(im.getOwner(any(), any())).thenReturn(notUUID); + public void testExecuteLowerTeamRank() { + when(island.getRank(user)).thenReturn(RanksManager.MEMBER_RANK); + when(island.getRank(notUUID)).thenReturn(RanksManager.SUB_OWNER_RANK); + + when(pm.getUUID(any())).thenReturn(notUUID); + when(pm.getName(notUUID)).thenReturn("poslovitch"); + + Set members = new HashSet<>(); + members.add(notUUID); + when(im.getMembers(any(), any())).thenReturn(members); + + IslandTeamKickCommand itl = new IslandTeamKickCommand(ic); + assertFalse(itl.execute(user, itl.getLabel(), Collections.singletonList("poslovitch"))); + verify(user).sendMessage(eq("commands.island.team.kick.cannot-kick-rank"), eq(TextVariables.NAME), eq("poslovitch")); + } + + + /** + * Test method for {@link IslandTeamKickCommand#execute(User, String, java.util.List)} + */ + @Test + public void testExecuteEqualTeamRank() { + when(island.getRank(user)).thenReturn(RanksManager.SUB_OWNER_RANK); + when(island.getRank(notUUID)).thenReturn(RanksManager.SUB_OWNER_RANK); + + when(pm.getUUID(any())).thenReturn(notUUID); + when(pm.getName(notUUID)).thenReturn("poslovitch"); + + Set members = new HashSet<>(); + members.add(notUUID); + when(im.getMembers(any(), any())).thenReturn(members); + + IslandTeamKickCommand itl = new IslandTeamKickCommand(ic); + assertFalse(itl.execute(user, itl.getLabel(), Collections.singletonList("poslovitch"))); + verify(user).sendMessage(eq("commands.island.team.kick.cannot-kick-rank"), eq(TextVariables.NAME), eq("poslovitch")); + } + + /** + * Test method for {@link IslandTeamKickCommand#execute(User, String, java.util.List)} + */ + @Test + public void testExecuteLargerTeamRank() { + when(island.getRank(user)).thenReturn(RanksManager.SUB_OWNER_RANK); + when(island.getRank(notUUID)).thenReturn(RanksManager.MEMBER_RANK); + + when(pm.getUUID(any())).thenReturn(notUUID); + when(pm.getName(notUUID)).thenReturn("poslovitch"); + + Set members = new HashSet<>(); + members.add(notUUID); + when(im.getMembers(any(), any())).thenReturn(members); + + IslandTeamKickCommand itl = new IslandTeamKickCommand(ic); + assertTrue(itl.execute(user, itl.getLabel(), Collections.singletonList("poslovitch"))); + verify(im).removePlayer(any(), eq(notUUID)); + verify(user).sendMessage("commands.island.team.kick.success", TextVariables.NAME, "poslovitch"); + } + + /** + * Test method for {@link IslandTeamKickCommand#execute(User, String, java.util.List)} + */ + @Test + public void testExecuteNoCommandRank() { + when(island.getRankCommand(anyString())).thenReturn(RanksManager.SUB_OWNER_RANK); + when(island.getRank(user)).thenReturn(RanksManager.MEMBER_RANK); + IslandTeamKickCommand itl = new IslandTeamKickCommand(ic); assertFalse(itl.execute(user, itl.getLabel(), Collections.emptyList())); - verify(user).sendMessage(eq("general.errors.not-owner")); + verify(user).sendMessage(eq("general.errors.insufficient-rank"), eq(TextVariables.RANK), eq("ranks.member")); } /** diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/AbstractCommonSetup.java b/src/test/java/world/bentobox/bentobox/listeners/flags/AbstractCommonSetup.java index ba9131e91..3140a989a 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/AbstractCommonSetup.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/AbstractCommonSetup.java @@ -21,10 +21,12 @@ import org.bukkit.metadata.MetadataValue; import org.bukkit.plugin.PluginManager; import org.eclipse.jdt.annotation.Nullable; import org.junit.After; +import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; import com.google.common.collect.ImmutableSet; @@ -57,6 +59,7 @@ import world.bentobox.bentobox.util.Util; * @author tastybento * */ +@RunWith(PowerMockRunner.class) public abstract class AbstractCommonSetup { protected UUID uuid = UUID.randomUUID(); @@ -136,7 +139,7 @@ public abstract class AbstractCommonSetup { // Island - nothing is allowed by default when(island.isAllowed(any())).thenReturn(false); - when(island.isAllowed(any(), any())).thenReturn(false); + when(island.isAllowed(any(User.class), any())).thenReturn(false); when(island.getOwner()).thenReturn(uuid); when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid)); diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListenerTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListenerTest.java index 7cde56a00..0f258b06e 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListenerTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/protection/EntityInteractListenerTest.java @@ -69,6 +69,10 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { position = new Vector(10,10,10); when(inv.getItemInMainHand()).thenReturn(new ItemStack(Material.NAME_TAG)); + // Initialize the Flags class. This is a workaround to prevent weird errors when mocking + // I think it's because the flag class needs to be initialized before use in argument matchers + Flags.TRADING.setDefaultSetting(false); + // Class under test eil = new EntityInteractListener(); } @@ -91,7 +95,7 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { */ @Test public void testOnPlayerInteractAtEntityArmorStandAllowed() { - when(island.isAllowed(any(), any())).thenReturn(true); + when(island.isAllowed(any(User.class), any())).thenReturn(true); clickedEntity = mock(ArmorStand.class); when(clickedEntity.getLocation()).thenReturn(location); PlayerInteractAtEntityEvent e = new PlayerInteractAtEntityEvent(player, clickedEntity, position, hand); @@ -118,7 +122,7 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { */ @Test public void testOnPlayerInteractEntityHorseAllowed() { - when(island.isAllowed(any(), any())).thenReturn(true); + when(island.isAllowed(any(User.class), any())).thenReturn(true); clickedEntity = mock(Horse.class); when(clickedEntity.getLocation()).thenReturn(location); PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(player, clickedEntity, hand); @@ -145,7 +149,7 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { */ @Test public void testOnPlayerInteractEntityMinecartAllowed() { - when(island.isAllowed(any(), any())).thenReturn(true); + when(island.isAllowed(any(User.class), any())).thenReturn(true); clickedEntity = mock(RideableMinecart.class); when(clickedEntity.getLocation()).thenReturn(location); PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(player, clickedEntity, hand); @@ -172,7 +176,7 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { */ @Test public void testOnPlayerInteractEntityBoatAllowed() { - when(island.isAllowed(any(), any())).thenReturn(true); + when(island.isAllowed(any(User.class), any())).thenReturn(true); clickedEntity = mock(Boat.class); when(clickedEntity.getLocation()).thenReturn(location); PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(player, clickedEntity, hand); @@ -199,7 +203,7 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { */ @Test public void testOnPlayerInteractAtEntityVillagerAllowed() { - when(island.isAllowed(any(), any())).thenReturn(true); + when(island.isAllowed(any(User.class), any())).thenReturn(true); clickedEntity = mock(Villager.class); when(clickedEntity.getLocation()).thenReturn(location); PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(player, clickedEntity, hand); @@ -212,9 +216,10 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { * Test method for {@link world.bentobox.bentobox.listeners.flags.protection.EntityInteractListener#onPlayerInteractEntity(org.bukkit.event.player.PlayerInteractAtEntityEvent)}. */ @Test + public void testOnPlayerInteractEntityNamingVillagerAllowedNoTrading() { - when(island.isAllowed(any(), eq(Flags.TRADING))).thenReturn(false); - when(island.isAllowed(any(), eq(Flags.NAME_TAG))).thenReturn(true); + when(island.isAllowed(any(User.class), eq(Flags.TRADING))).thenReturn(false); + when(island.isAllowed(any(User.class), eq(Flags.NAME_TAG))).thenReturn(true); clickedEntity = mock(Villager.class); when(clickedEntity.getLocation()).thenReturn(location); PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(player, clickedEntity, hand); @@ -228,8 +233,8 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { */ @Test public void testOnPlayerInteractEntityNamingVillagerAllowedTradingNoNaming() { - when(island.isAllowed(any(), eq(Flags.TRADING))).thenReturn(true); - when(island.isAllowed(any(), eq(Flags.NAME_TAG))).thenReturn(false); + when(island.isAllowed(any(User.class), eq(Flags.TRADING))).thenReturn(true); + when(island.isAllowed(any(User.class), eq(Flags.NAME_TAG))).thenReturn(false); clickedEntity = mock(Villager.class); when(clickedEntity.getLocation()).thenReturn(location); PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(player, clickedEntity, hand); @@ -257,7 +262,7 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { */ @Test public void testOnPlayerInteractAtEntityWanderingTraderAllowed() { - when(island.isAllowed(any(), any())).thenReturn(true); + when(island.isAllowed(any(User.class), any())).thenReturn(true); clickedEntity = mock(WanderingTrader.class); when(clickedEntity.getType()).thenReturn(EntityType.WANDERING_TRADER); when(clickedEntity.getLocation()).thenReturn(location); @@ -271,9 +276,12 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { * Test method for {@link world.bentobox.bentobox.listeners.flags.protection.EntityInteractListener#onPlayerInteractEntity(org.bukkit.event.player.PlayerInteractAtEntityEvent)}. */ @Test + public void testOnPlayerInteractEntityNamingWanderingTraderAllowedNoTrading() { - when(island.isAllowed(any(User.class), eq(Flags.TRADING))).thenReturn(false); - when(island.isAllowed(any(User.class), eq(Flags.NAME_TAG))).thenReturn(true); + when(island.isAllowed(any(), + eq(Flags.TRADING))).thenReturn(false); + when(island.isAllowed(any(User.class), + eq(Flags.NAME_TAG))).thenReturn(true); clickedEntity = mock(WanderingTrader.class); when(clickedEntity.getType()).thenReturn(EntityType.WANDERING_TRADER); when(clickedEntity.getLocation()).thenReturn(location); @@ -287,6 +295,7 @@ public class EntityInteractListenerTest extends AbstractCommonSetup { * Test method for {@link world.bentobox.bentobox.listeners.flags.protection.EntityInteractListener#onPlayerInteractEntity(org.bukkit.event.player.PlayerInteractAtEntityEvent)}. */ @Test + public void testOnPlayerInteractEntityNamingWanderingTraderAllowedTradingNoNaming() { when(island.isAllowed(any(User.class), eq(Flags.TRADING))).thenReturn(true); when(island.isAllowed(any(User.class), eq(Flags.NAME_TAG))).thenReturn(false); diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/worldsettings/ObsidianScoopingListenerTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/worldsettings/ObsidianScoopingListenerTest.java index 73f134462..8eb31b5b0 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/worldsettings/ObsidianScoopingListenerTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/worldsettings/ObsidianScoopingListenerTest.java @@ -153,6 +153,11 @@ public class ObsidianScoopingListenerTest { map.put("OBSIDIAN_SCOOPING", true); when(ws.getWorldFlags()).thenReturn(map); + PlayerInventory playerInventory = mock(PlayerInventory.class); + when(playerInventory.getItemInMainHand()).thenReturn(item); + when(playerInventory.getItemInOffHand()).thenReturn(new ItemStack(Material.AIR)); + when(p.getInventory()).thenReturn(playerInventory); + // Addon when(iwm.getAddon(Mockito.any())).thenReturn(Optional.empty()); } diff --git a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java index c99d1b0da..033e154ac 100644 --- a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java @@ -169,6 +169,7 @@ public class PlayersManagerTest { when(user.getPlayer()).thenReturn(p); when(user.getName()).thenReturn("tastybento"); when(user.isOnline()).thenReturn(true); + when(user.isPlayer()).thenReturn(true); User.setPlugin(plugin); From 065f4ba4bd4f1414b8829721a1dfbb018325ba0b Mon Sep 17 00:00:00 2001 From: BONNe Date: Tue, 11 Oct 2022 09:37:59 +0300 Subject: [PATCH 2/2] Spigot does not support parallel stream processing for placing blocks and entities in the world. This is a hot-fix release. --- pom.xml | 2 +- .../bentobox/bentobox/nms/fallback/PasteHandlerImpl.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index cf62879d3..5006baaeb 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ -LOCAL - 1.21.0 + 1.21.1 bentobox-world https://sonarcloud.io diff --git a/src/main/java/world/bentobox/bentobox/nms/fallback/PasteHandlerImpl.java b/src/main/java/world/bentobox/bentobox/nms/fallback/PasteHandlerImpl.java index 4ba2d8eef..fa2eae3e8 100644 --- a/src/main/java/world/bentobox/bentobox/nms/fallback/PasteHandlerImpl.java +++ b/src/main/java/world/bentobox/bentobox/nms/fallback/PasteHandlerImpl.java @@ -16,7 +16,7 @@ import java.util.stream.Collectors; public class PasteHandlerImpl implements PasteHandler { @Override public CompletableFuture pasteBlocks(Island island, World world, Map blockMap) { - return blockMap.entrySet().parallelStream() + return blockMap.entrySet().stream() .map(entry -> DefaultPasteUtil.setBlock(island, entry.getKey(), entry.getValue())) .collect( Collectors.collectingAndThen( @@ -28,7 +28,7 @@ public class PasteHandlerImpl implements PasteHandler { @Override public CompletableFuture pasteEntities(Island island, World world, Map> entityMap) { - return entityMap.entrySet().parallelStream() + return entityMap.entrySet().stream() .map(entry -> DefaultPasteUtil.setEntity(island, entry.getKey(), entry.getValue())) .collect( Collectors.collectingAndThen(