package world.bentobox.bentobox.managers; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.Boat; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.PufferFish; import org.bukkit.inventory.ItemStack; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import io.papermc.lib.PaperLib; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.events.IslandBaseEvent; import world.bentobox.bentobox.api.events.island.IslandEvent; import world.bentobox.bentobox.api.events.island.IslandEvent.Reason; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.logs.LogEntry; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.Database; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.database.objects.IslandDeletion; import world.bentobox.bentobox.lists.Flags; import world.bentobox.bentobox.managers.island.IslandCache; import world.bentobox.bentobox.util.Util; import world.bentobox.bentobox.util.teleport.SafeSpotTeleport; /** * The job of this class is manage all island related data. It also handles * island ownership, including team, trustees, coops, etc. The data object that * it uses is Island * * @author tastybento */ public class IslandsManager { private final BentoBox plugin; /** * One island can be spawn, this is the one - otherwise, this value is null */ @NonNull private final Map<@NonNull World, @Nullable Island> spawn; @NonNull private Database handler; /** * The last locations where an island were put. This is not stored persistently * and resets when the server starts */ private final Map last; /** * Island Cache */ @NonNull private IslandCache islandCache; // Quarantined islands @NonNull private final Map> quarantineCache; // Deleted islands @NonNull private final List deletedIslands; private boolean isSaveTaskRunning; private final Set goingHome; /** * Islands Manager * * @param plugin - plugin */ public IslandsManager(@NonNull BentoBox plugin) { this.plugin = plugin; // Set up the database handler to store and retrieve Island classes handler = new Database<>(plugin, Island.class); islandCache = new IslandCache(); quarantineCache = new HashMap<>(); spawn = new HashMap<>(); last = new HashMap<>(); // This list should always be empty unless database deletion failed // In that case a purge utility may be required in the future deletedIslands = new ArrayList<>(); // Mid-teleport players going home goingHome = new HashSet<>(); } /** * Used only for testing. Sets the database to a mock database. * * @param handler - handler */ public void setHandler(@NonNull Database handler) { this.handler = handler; } /** * Checks if this location is safe for a player to teleport to. Used by warps * and boat exits Unsafe is any liquid or air and also if there's no space * * @param l Location to be checked, not null. * @return true if safe, otherwise false */ public boolean isSafeLocation(@NonNull Location l) { Block ground = l.getBlock().getRelative(BlockFace.DOWN); Block space1 = l.getBlock(); Block space2 = l.getBlock().getRelative(BlockFace.UP); return checkIfSafe(l.getWorld(), ground.getType(), space1.getType(), space2.getType()); } /** * Checks if this location is safe for a player to teleport to and loads chunks * async to check. * * @param l Location to be checked, not null. * @return a completable future that will be true if safe, otherwise false * @since 1.14.0 */ public CompletableFuture isSafeLocationAsync(@NonNull Location l) { CompletableFuture result = new CompletableFuture<>(); Util.getChunkAtAsync(l).thenRun(() -> { Block ground = l.getBlock().getRelative(BlockFace.DOWN); Block space1 = l.getBlock(); Block space2 = l.getBlock().getRelative(BlockFace.UP); result.complete(checkIfSafe(l.getWorld(), ground.getType(), space1.getType(), space2.getType())); }); return result; } /** * Check if a location is safe for teleporting * * @param world - world * @param ground Material of the block that is going to be the ground * @param space1 Material of the block above the ground * @param space2 Material of the block that is two blocks above the ground * @return {@code true} if the location is considered safe, {@code false} * otherwise. */ public boolean checkIfSafe(@Nullable World world, @NonNull Material ground, @NonNull Material space1, @NonNull Material space2) { // Ground must be solid, space 1 and 2 must not be solid if (world == null || !ground.isSolid() || (space1.isSolid() && !Tag.SIGNS.isTagged(space1)) || (space2.isSolid() && !Tag.SIGNS.isTagged(space2))) { return false; } // Cannot be submerged or water cannot be dangerous if (space1.equals(Material.WATER) && (space2.equals(Material.WATER) || plugin.getIWM().isWaterNotSafe(world))) { return false; } // Unsafe if (ground.equals(Material.LAVA) || space1.equals(Material.LAVA) || space2.equals(Material.LAVA) || Tag.SIGNS.isTagged(ground) || Tag.TRAPDOORS.isTagged(ground) || Tag.BANNERS.isTagged(ground) || Tag.PRESSURE_PLATES.isTagged(ground) || Tag.FENCE_GATES.isTagged(ground) || Tag.DOORS.isTagged(ground) || Tag.FENCES.isTagged(ground) || Tag.BUTTONS.isTagged(ground) || Tag.ITEMS_BOATS.isTagged(ground) || Tag.ITEMS_CHEST_BOATS.isTagged(ground) || Tag.CAMPFIRES.isTagged(ground) || Tag.FIRE.isTagged(ground) || Tag.FIRE.isTagged(space1) || space1.equals(Material.END_PORTAL) || space2.equals(Material.END_PORTAL) || space1.equals(Material.END_GATEWAY) || space2.equals(Material.END_GATEWAY)) { return false; } // Known unsafe blocks return switch (ground) { // Unsafe case ANVIL, BARRIER, CACTUS, END_PORTAL, END_ROD, FIRE, FLOWER_POT, LADDER, LEVER, TALL_GRASS, PISTON_HEAD, MOVING_PISTON, TORCH, WALL_TORCH, TRIPWIRE, WATER, COBWEB, NETHER_PORTAL, MAGMA_BLOCK -> false; default -> true; }; } /** * Create an island with no owner at location * * @param location the location, not null * @return Island or null if the island could not be created for some reason */ @Nullable public Island createIsland(@NonNull Location location) { return createIsland(location, null); } /** * Create an island with owner. Note this does not paste blocks. It just creates * the island data object. * * @param location the location, not null * @param owner the island owner UUID, may be null * @return Island or null if the island could not be created for some reason */ @Nullable public Island createIsland(@NonNull Location location, @Nullable UUID owner) { Island island = new Island(location, owner, plugin.getIWM().getIslandProtectionRange(location.getWorld())); // Game the gamemode name and prefix the uniqueId String gmName = plugin.getIWM().getAddon(location.getWorld()).map(gm -> gm.getDescription().getName()) .orElse(""); island.setGameMode(gmName); island.setUniqueId(gmName + island.getUniqueId()); while (handler.objectExists(island.getUniqueId())) { // This should never happen, so although this is a potential infinite loop I'm // going to leave it here because // it will be bad if this does occur and the server should crash. plugin.logWarning("Duplicate island UUID occurred"); island.setUniqueId(gmName + UUID.randomUUID()); } if (islandCache.addIsland(island)) { return island; } return null; } /** * Deletes island. * * @param island island to delete, not null * @param removeBlocks whether the island blocks should be removed or not * @param involvedPlayer - player related to the island deletion, if any */ public void deleteIsland(@NonNull Island island, boolean removeBlocks, @Nullable UUID involvedPlayer) { // Fire event IslandBaseEvent event = IslandEvent.builder().island(island).involvedPlayer(involvedPlayer) .reason(Reason.DELETE).build(); if (event.getNewEvent().map(IslandBaseEvent::isCancelled).orElse(event.isCancelled())) { return; } // Set the owner of the island to no one. island.setOwner(null); island.setFlag(Flags.LOCK, RanksManager.VISITOR_RANK); if (removeBlocks) { // Remove island from the cache islandCache.deleteIslandFromCache(island); // Log the deletion (it shouldn't matter but may be useful) island.log(new LogEntry.Builder("DELETED").build()); // Set the delete flag which will prevent it from being loaded even if database // deletion fails island.setDeleted(true); // Save the island handler.saveObjectAsync(island); // Delete the island handler.deleteObject(island); // Remove players from island removePlayersFromIsland(island); if (!plugin.getSettings().isKeepPreviousIslandOnReset()) { // Remove blocks from world plugin.getIslandDeletionManager().getIslandChunkDeletionManager().add(new IslandDeletion(island)); } } } /** * Get the number of islands made on this server. Used by stats. * * @return total number of islands known to this server */ public int getIslandCount() { return islandCache.size(); } /** * Get the number of islands made on this server in a particular world. Used to * limit the number of islands if required by settings. * * @param world game world * @return number of islands */ public long getIslandCount(@NonNull World world) { return islandCache.size(world); } /** * Gets the current active island for this player. If they are in a team, the * team island is returned. If they have more than one island, then the island * they are on now, or the last island they were on is returned. * * @param world world to check * @param user user * @return Island or null if not found or null user */ @Nullable public Island getIsland(@NonNull World world, @Nullable User user) { return user == null || user.getUniqueId() == null ? null : getIsland(world, user.getUniqueId()); } /** * Gets the islands for this player. If they are in a team, the team island is * returned. * * @param world world to check * @param user user * @return List of islands or empty list if none found for user */ @NonNull public Set getIslands(@NonNull World world, @NonNull User user) { return getIslands(world, user.getUniqueId()); } /** * Gets all the islands for this player in this world including team islands. * * @param world world to check * @param uniqueId user's UUID * @return List of islands or empty list if none found for user */ @NonNull public Set getIslands(@NonNull World world, UUID uniqueId) { return islandCache.getIslands(world, uniqueId); } /** * Gets all the islands for this player in this world that this player owns. * * @param world world to check * @param user user * @return List of islands or empty list if none found for user * @since 2.1.0 */ @NonNull public Set getOwnedIslands(@NonNull World world, @NonNull User user) { if (user.getUniqueId() == null) { return Collections.emptySet(); } return getOwnedIslands(world, user.getUniqueId()); } /** * Gets all the islands for this player in this world that this player owns. * * @param world world to check * @param uniqueId user's UUID * @return List of islands or empty list if none found for user * @since 2.1.0 */ @NonNull public Set getOwnedIslands(@NonNull World world, @NonNull UUID uniqueId) { return islandCache.getIslands(world, uniqueId).stream().filter(island -> uniqueId.equals(island.getOwner())) .collect(Collectors.toSet()); } /** * Gets the active island for this player. If they are in a team, the team * island is returned. User may have more than one island. Returns the island * the player is on now, or their last known island. * * @param world world to check. Includes nether and end worlds. * @param uuid user's uuid * @return Island or null */ @Nullable public Island getIsland(@NonNull World world, @NonNull UUID uuid) { return islandCache.get(world, uuid); } /** * Returns the island at the location or Optional empty if there is none. This * includes the full island space, not just the protected area. Use * {@link #getProtectedIslandAt(Location)} for only the protected island space. * * @param location - the location * @return Optional Island object */ public Optional getIslandAt(@NonNull Location location) { return plugin.getIWM().inWorld(location) ? Optional.ofNullable(islandCache.getIslandAt(location)) : Optional.empty(); } /** * Returns an unmodifiable collection of all existing islands * (even those who may be unowned). * * @return unmodifiable collection containing every island. * @since 1.1 */ @NonNull public Collection getIslands() { return islandCache.getIslands(); } /** * Returns an unmodifiable collection of all the islands (even * those who may be unowned) in the specified world. * * @param world World of the gamemode. * @return unmodifiable collection containing all the islands in the specified * world. * @since 1.7.0 */ @NonNull public Collection getIslands(@NonNull World world) { return islandCache.getIslands(world); } /** * Returns the IslandCache instance. * * @return the islandCache * @since 1.5.0 */ @NonNull public IslandCache getIslandCache() { return islandCache; } /** * Used for testing only to inject the islandCache mock object * * @param islandCache - island cache */ public void setIslandCache(@NonNull IslandCache islandCache) { this.islandCache = islandCache; } /** * Returns the player's current island location in World based on the island * protection center. If you need the actual island center location for some * reason use {@link Island#getCenter()} *

* * @param world - world to check * @param uuid - the player's UUID * @return Location of the center of the player's protection area or null if an * island does not exist. Returns an island location OR a team island * location */ @Nullable public Location getIslandLocation(@NonNull World world, @NonNull UUID uuid) { Island island = getIsland(world, uuid); return island != null ? island.getProtectionCenter() : null; } /** * Get the last location where an island was created * * @param world - world * @return location */ public Location getLast(@NonNull World world) { return last.get(world); } /** * Gets the maximum number of island members allowed on this island. Will update * the value based on world settings or island owner permissions (if online). If * the island is unowned, then this value will be 0. The number given for * MEMBER_RANK is meant to include this rank and higher, e.g. * {@link RanksManager#SUB_OWNER_RANK} and {@link RanksManager#OWNER_RANK} * * @param island - island * @param rank {@link RanksManager#MEMBER_RANK}, * {@link RanksManager#COOP_RANK}, or * {@link RanksManager#TRUSTED_RANK} * @return max number of members. If negative, then this means unlimited. * @since 1.16.0 */ public int getMaxMembers(@NonNull Island island, int rank) { if (island.getOwner() == null) { // No owner, no rank settings island.setMaxMembers(null); this.save(island); return 0; } // Island max is either the world default or specified amount for this island int worldDefault = plugin.getIWM().getMaxTeamSize(island.getWorld()); String perm = "team.maxsize"; if (rank == RanksManager.COOP_RANK) { worldDefault = plugin.getIWM().getMaxCoopSize(island.getWorld()); perm = "coop.maxsize"; } else if (rank == RanksManager.TRUSTED_RANK) { worldDefault = plugin.getIWM().getMaxTrustSize(island.getWorld()); perm = "trust.maxsize"; } int islandMax = island.getMaxMembers(rank) == null ? worldDefault : island.getMaxMembers(rank); // Update based on owner permissions if online if (island.getOwner() != null && Bukkit.getPlayer(island.getOwner()) != null) { User owner = User.getInstance(island.getOwner()); islandMax = owner.getPermissionValue(plugin.getIWM().getPermissionPrefix(island.getWorld()) + perm, islandMax); } island.setMaxMembers(rank, islandMax == worldDefault ? null : islandMax); this.save(island); return islandMax; } /** * Sets the island max member size. * * @param island - island * @param rank {@link RanksManager#MEMBER_RANK}, * {@link RanksManager#COOP_RANK}, or * {@link RanksManager#TRUSTED_RANK} * @param maxMembers - max number of members. If negative, then this means * unlimited. Null means the world default will be used. * @since 1.16.0 */ public void setMaxMembers(@NonNull Island island, int rank, Integer maxMembers) { island.setMaxMembers(rank, maxMembers); } /** * Get the maximum number of homes allowed on this island. Will be updated with * the owner's permission settings if they exist and the owner is online * * @param island - island * @return maximum number of homes * @since 1.16.0 */ public int getMaxHomes(@NonNull Island island) { int islandMax = island.getMaxHomes() == null ? plugin.getIWM().getMaxHomes(island.getWorld()) : island.getMaxHomes(); // Update based on owner permissions if online if (island.getOwner() != null && Bukkit.getPlayer(island.getOwner()) != null) { User owner = User.getInstance(island.getOwner()); islandMax = owner.getPermissionValue( plugin.getIWM().getPermissionPrefix(island.getWorld()) + "island.maxhomes", islandMax); } // If the island maxHomes is just the same as the world default, then set to // null island.setMaxHomes(islandMax == plugin.getIWM().getMaxHomes(island.getWorld()) ? null : islandMax); this.save(island); return islandMax; } /** * Set the maximum numbber of homes allowed on this island * * @param island - island * @param maxHomes - max number of homes allowed, or null if the world default * should be used * @since 1.16.0 */ public void setMaxHomes(@NonNull Island island, @Nullable Integer maxHomes) { island.setMaxHomes(maxHomes); } /** * Returns the island at the location or Optional empty if there is none. This * includes only the protected area. Use {@link #getIslandAt(Location)} for the * full island space. * * @param location - the location * @return Optional Island object */ public Optional getProtectedIslandAt(@NonNull Location location) { return getIslandAt(location).filter(i -> i.onIsland(location)); } /** * Get a safe home location using async chunk loading and set the home location * * @param world - world * @param user - user * @param homeName - home name * @return CompletableFuture with the location found, or null * @since 1.14.0 */ private CompletableFuture getAsyncSafeHomeLocation(@NonNull World world, @NonNull User user, String homeName) { CompletableFuture result = new CompletableFuture<>(); // Check if the world is a gamemode world and the player has an island Location islandLoc = getIslandLocation(world, user.getUniqueId()); if (!plugin.getIWM().inWorld(world) || islandLoc == null) { result.complete(null); return result; } // Check if the user is switching island and if so, switch name String name = this.getIslands(world, user).stream().filter(i -> !homeName.isBlank() && i.getName() != null && !i.getName().isBlank() && i.getName().equalsIgnoreCase(homeName)).findFirst().map(island -> { // This is an island, so switch to that island and then go to the default home this.setPrimaryIsland(user.getUniqueId(), island); return ""; }).orElse(homeName); // Try the home location first Location defaultHome = getHomeLocation(world, user); Location namedHome = homeName.isBlank() ? null : getHomeLocation(world, user, name); Location l = namedHome != null ? namedHome : defaultHome; if (l != null) { Util.getChunkAtAsync(l).thenRun(() -> { // Check if it is safe if (isSafeLocation(l)) { result.complete(l); return; } // To cover slabs, stairs and other half blocks, try one block above Location lPlusOne = l.clone().add(new Vector(0, 1, 0)); if (isSafeLocation(lPlusOne)) { // Adjust the home location accordingly setHomeLocation(user, lPlusOne, name); result.complete(lPlusOne); return; } // Try island tryIsland(result, islandLoc, user, name); }); return result; } // Try island tryIsland(result, islandLoc, user, name); return result; } private void tryIsland(CompletableFuture result, Location islandLoc, @NonNull User user, String name) { Util.getChunkAtAsync(islandLoc).thenRun(() -> { World w = islandLoc.getWorld(); if (isSafeLocation(islandLoc)) { setHomeLocation(user, islandLoc, name); result.complete(islandLoc.clone().add(new Vector(0.5D, 0, 0.5D))); return; } else { // If these island locations are not safe, then we need to get creative // Try the default location Location dl = islandLoc.clone().add(new Vector(0.5D, 5D, 2.5D)); if (isSafeLocation(dl)) { setHomeLocation(user, dl, name); result.complete(dl); return; } // Try just above the bedrock dl = islandLoc.clone().add(new Vector(0.5D, 5D, 0.5D)); if (isSafeLocation(dl)) { setHomeLocation(user, dl, name); result.complete(dl); return; } // Try all the way up to the sky for (int y = islandLoc.getBlockY(); y < w.getMaxHeight(); y++) { dl = new Location(w, islandLoc.getX() + 0.5D, y, islandLoc.getZ() + 0.5D); if (isSafeLocation(dl)) { setHomeLocation(user, dl, name); result.complete(dl); return; } } } result.complete(null); }); } /** * Sets a default home location on user's island. Replaces previous default * location. * * @param user - user * @param location - location on island * @return true if home location was set. False if this location is not on the * island. * @since 1.18.0 */ public boolean setHomeLocation(@NonNull User user, Location location) { return setHomeLocation(user.getUniqueId(), location, ""); } /** * Sets a home location on user's island. Replaces previous location if the same * name is used * * @param user - user * @param location - location on island * @param name - name of home, or blank for default home * @return true if home location was set. False if this location is not on the * island. * @since 1.16.0 */ public boolean setHomeLocation(@NonNull User user, Location location, String name) { return setHomeLocation(user.getUniqueId(), location, name); } /** * Sets a home location on user's island. Replaces previous location if the same * name is used * * @param uuid - user uuid * @param location - location on island * @param name - name of home, or blank for default home * @return true if home location was set. False if this location is not on the * island. * @since 1.16.0 */ public boolean setHomeLocation(@NonNull UUID uuid, Location location, String name) { return setHomeLocation(this.getIsland(location.getWorld(), uuid), location, name); } /** * Set a default home location for user on their island * * @param uuid - user uuid * @param location - location on island * @return true if home location was set. False if this location is not on the * island. * @since 1.16.0 */ public boolean setHomeLocation(@NonNull UUID uuid, Location location) { return setHomeLocation(uuid, location, ""); } /** * Set a home location for island * * @param island - island * @param location - location * @param name - name of home, or blank for default home * @return true if home location was set. False if this location is not on the * island. * @since 1.16.0 */ public boolean setHomeLocation(@Nullable Island island, Location location, String name) { if (island != null) { island.addHome(name, location); this.save(island); return true; } return false; } /** * Get the home location for user in world for their primary island * * @param world - world * @param user - user * @return home location or the protection center location if no home defined * @since 2.0.0 */ @Nullable public Location getHomeLocation(@NonNull World world, @NonNull User user) { return this.getPrimaryIsland(world, user.getUniqueId()).getHome(""); } /** * Get the home location for player's UUID in world for their primary island * * @param world - world * @param uuid - uuid of player * @return home location or the protection center location if no home defined * @since 1.16.0 */ @Nullable public Location getHomeLocation(@NonNull World world, @NonNull UUID uuid) { return this.getPrimaryIsland(world, uuid).getHome(""); } /** * Get the named home location for user in world * * @param world - world * @param user - user * @param name - name of home, or blank for default * @return home location or null if there is no home * @since 1.16.0 */ @Nullable public Location getHomeLocation(@NonNull World world, @NonNull User user, String name) { return getHomeLocation(world, user.getUniqueId(), name); } /** * Get the named home location for user in world * * @param world - world * @param uuid - uuid of player * @param name - name of home, or blank for default * @return home location or null if there is no home * @since 1.16.0 */ @Nullable public Location getHomeLocation(@NonNull World world, @NonNull UUID uuid, String name) { return getIslands(world, uuid).stream().filter(is -> is.getHomes().containsKey(name)) .map(is -> is.getHome(name)).findFirst() .orElse(null); } /** * Get the default home location for this island * * @param island - island * @return home location * @since 1.16.0 */ @NonNull public Location getHomeLocation(@NonNull Island island) { return getHomeLocation(island, ""); } /** * Get the named home location for this island * * @param island - island * @param name - name of home, or blank for default * @return home location or if there is none, then the island's center * @since 1.16.0 */ @NonNull public Location getHomeLocation(@NonNull Island island, @NonNull String name) { return Objects.requireNonNullElse(island.getHome(name), island.getProtectionCenter()); } /** * Remove the named home location from this island * * @param island - island * @param name - name of home, or blank for default * @return true if successful, false if not * @since 1.16.0 */ public boolean removeHomeLocation(@NonNull Island island, @NonNull String name) { return island.removeHome(name); } /** * Rename a home * * @param island - island * @param oldName - old name * @param newName - new name * @return true if successful, false if not */ public boolean renameHomeLocation(@NonNull Island island, @NonNull String oldName, @NonNull String newName) { return island.renameHome(oldName, newName); } /** * Get the all the home locations for this island * * @param island - island * @return map of home locations with the name as the key * @since 1.16.0 */ @NonNull public Map getHomeLocations(@NonNull Island island) { return island.getHomes(); } /** * Check if a home name exists or not * * @param island - island * @param name - name being checked * @return true if it exists or not */ public boolean isHomeLocation(@NonNull Island island, @NonNull String name) { return island.getHomes().containsKey(name.toLowerCase()); } /** * Get the number of homes on this island if this home were added * * @param island - island * @param name - name * @return number of homes after adding this one */ public int getNumberOfHomesIfAdded(@NonNull Island island, @NonNull String name) { return isHomeLocation(island, name) ? getHomeLocations(island).size() : getHomeLocations(island).size() + 1; } /** * Gets the island that is defined as spawn in this world * * @param world world * @return optional island, may be empty */ @NonNull public Optional getSpawn(@NonNull World world) { return Optional.ofNullable(spawn.get(world)); } /** * Get the spawn point on the spawn island if it exists * * @param world - world * @return the spawnPoint or null if spawn does not exist */ @Nullable public Location getSpawnPoint(@NonNull World world) { return spawn.containsKey(world) ? spawn.get(world).getSpawnPoint(world.getEnvironment()) : null; } /** * Checks if a player has an island in the world and owns it. Note that players * may have more than one island * * @param world - world to check * @param user - the user * @return true if player has island and owns it */ public boolean hasIsland(@NonNull World world, @NonNull User user) { return islandCache.hasIsland(world, user.getUniqueId()); } /** * Checks if a player has an island in the world and owns it * * @param world - world to check * @param uuid - the user's uuid * @return true if player has island and owns it */ public boolean hasIsland(@NonNull World world, @NonNull UUID uuid) { return islandCache.hasIsland(world, uuid); } /** * This teleports player to their island. If not safe place can be found then * the player is sent to spawn via /spawn command * * @param world - world to check * @param player - the player * @return CompletableFuture true if successful, false if not * @since 1.14.0 */ public CompletableFuture homeTeleportAsync(@NonNull World world, @NonNull Player player) { return homeTeleportAsync(world, player, "", false); } /** * Teleport player to a home location. If one cannot be found a search is done * to find a safe place. * * @param world - world to check * @param player - the player * @param name - a named home location or island name. Blank means default * home for current island. * @return CompletableFuture true if successful, false if not * @since 1.16.0 */ public CompletableFuture homeTeleportAsync(@NonNull World world, @NonNull Player player, String name) { return homeTeleportAsync(world, player, name, false); } /** * This teleports player to their island. If no safe place can be found then the * player is sent to spawn via /spawn command * * @param world - world to check * @param player - the player * @param newIsland - true if this is a new island teleport * @return CompletableFuture true if successful, false if not * @since 1.14.0 */ public CompletableFuture homeTeleportAsync(@NonNull World world, @NonNull Player player, boolean newIsland) { return homeTeleportAsync(world, player, "", newIsland); } /** * Teleports player async * * @param world world * @param player player * @param name - a named home location or island name. Blank means default * home for current island. * @param newIsland true if this is a new island * @return completable future that is true when the teleport has been completed */ private CompletableFuture homeTeleportAsync(@NonNull World world, @NonNull Player player, String name, boolean newIsland) { CompletableFuture result = new CompletableFuture<>(); User user = User.getInstance(player); user.sendMessage("commands.island.go.teleport"); goingHome.add(user.getUniqueId()); readyPlayer(player); this.getAsyncSafeHomeLocation(world, user, name).thenAccept(home -> { Island island = getIsland(world, user); if (home == null) { // Try to fix this teleport location and teleport the player if possible new SafeSpotTeleport.Builder(plugin).entity(player).island(island).homeName(name) .thenRun(() -> teleported(world, user, name, newIsland, island)) .ifFail(() -> goingHome.remove(user.getUniqueId())).buildFuture().thenAccept(result::complete); return; } PaperLib.teleportAsync(Objects.requireNonNull(player), home).thenAccept(b -> { // Only run the commands if the player is successfully teleported if (Boolean.TRUE.equals(b)) { teleported(world, user, name, newIsland, island); result.complete(true); } else { // Remove from mid-teleport set goingHome.remove(user.getUniqueId()); result.complete(false); } }); }); return result; } /** * Called when a player is teleported to their island * * @param world - world * @param user - user * @param name - name of home * @param newIsland - true if this is a new island * @param island - island */ private void teleported(World world, User user, String name, boolean newIsland, Island island) { if (!name.isEmpty()) { user.sendMessage("commands.island.go.teleported", TextVariables.NUMBER, name); } // Remove from mid-teleport set goingHome.remove(user.getUniqueId()); // If this is a new island, then run commands and do resets if (newIsland) { // Fire event if (IslandEvent.builder().involvedPlayer(user.getUniqueId()).reason(Reason.NEW_ISLAND).island(island) .location(island.getCenter()).build().isCancelled()) { // Do nothing return; } // Remove money inventory etc. if (plugin.getIWM().isOnJoinResetEnderChest(world)) { user.getPlayer().getEnderChest().clear(); } if (plugin.getIWM().isOnJoinResetInventory(world)) { user.getPlayer().getInventory().clear(); } if (plugin.getSettings().isUseEconomy() && plugin.getIWM().isOnJoinResetMoney(world)) { plugin.getVault().ifPresent(vault -> vault.withdraw(user, vault.getBalance(user))); } // Reset the health if (plugin.getIWM().isOnJoinResetHealth(world)) { Util.resetHealth(user.getPlayer()); } // Reset the hunger if (plugin.getIWM().isOnJoinResetHunger(world)) { user.getPlayer().setFoodLevel(20); } // Reset the XP if (plugin.getIWM().isOnJoinResetXP(world)) { // Player collected XP (displayed) user.getPlayer().setLevel(0); user.getPlayer().setExp(0); // Player total XP (not displayed) user.getPlayer().setTotalExperience(0); } // Set the game mode user.setGameMode(plugin.getIWM().getDefaultGameMode(world)); // Execute commands Util.runCommands(user, user.getName(), plugin.getIWM().getOnJoinCommands(world), "join"); } // Remove from mid-teleport set goingHome.remove(user.getUniqueId()); } /** * Teleports the player to the spawn location for this world * * @param world world * @param player player to teleport * @since 1.1 */ public void spawnTeleport(@NonNull World world, @NonNull Player player) { User user = User.getInstance(player); // If there's no spawn island or the spawn location is null for some reason, // then error Optional spawnTo = getSpawn(world).map(island -> { Location spawnPoint = island.getSpawnPoint(World.Environment.NORMAL); return spawnPoint != null ? spawnPoint : island.getCenter(); }); if (spawnTo.isEmpty()) { // There is no spawn here. user.sendMessage("commands.island.spawn.no-spawn"); } else { // Teleport the player to the spawn readyPlayer(player); user.sendMessage("commands.island.spawn.teleporting"); // Safe teleport new SafeSpotTeleport.Builder(plugin).entity(player).location(spawnTo.get()).build(); } } /** * Prepares the player for teleporting by: stopping gliding, exiting any boats * and giving the player the boat * * @param player player */ private void readyPlayer(@NonNull Player player) { // Stop any gliding player.setGliding(false); // Check if the player is a passenger in a boat if (player.isInsideVehicle()) { Entity boat = player.getVehicle(); if (boat instanceof Boat boaty) { player.leaveVehicle(); // Remove the boat so they don't lie around everywhere boat.remove(); player.getInventory().addItem(new ItemStack(boaty.getBoatType().getMaterial())); player.updateInventory(); } } } /** * Indicates whether a player is at an island spawn or not * * @param playerLoc - player's location * @return true if they are, false if they are not, or spawn does not exist */ public boolean isAtSpawn(Location playerLoc) { return spawn.containsKey(playerLoc.getWorld()) && spawn.get(playerLoc.getWorld()).onIsland(playerLoc); } /** * Sets an Island to be the spawn of its World. It will become an unowned * Island.
* If there was already a spawn set for this World, it will no longer be the * spawn but it will remain unowned. * * @param spawn the Island to set as spawn. Must not be null. */ public void setSpawn(@NonNull Island spawn) { // Checking if there is already a spawn set for this world if (this.spawn.containsKey(spawn.getWorld()) && this.spawn.get(spawn.getWorld()) != null) { Island oldSpawn = this.spawn.get(spawn.getWorld()); if (oldSpawn.equals(spawn)) { return; // The spawn is already the current spawn - no need to update anything. } else { oldSpawn.setSpawn(false); } } this.spawn.put(spawn.getWorld(), spawn); spawn.setSpawn(true); } /** * Clears the spawn island for this world * * @param world - world * @since 1.8.0 */ public void clearSpawn(World world) { Island spawnIsland = spawn.get(Util.getWorld(world)); if (spawnIsland != null) { spawnIsland.setSpawn(false); } this.spawn.remove(world); } /** * Check is a player has an island and owns it in world * * @param uniqueId - unique ID * @return true if the player is the owner of any island in the world. * @deprecated Duplicate of {@link #hasIsland(World, UUID)}. Players can have * multiple islands. */ @Deprecated(since = "2.0", forRemoval = true) public boolean isOwner(@NonNull World world, @NonNull UUID uniqueId) { return hasIsland(world, uniqueId); } /** * Clear and reload all islands from database * * @throws IOException - if a loaded island distance does not match the expected * distance in config.yml */ public void load() throws IOException { islandCache.clear(); quarantineCache.clear(); List toQuarantine = new ArrayList<>(); int owned = 0; int unowned = 0; // Attempt to load islands for (Island island : handler.loadObjects()) { if (island == null) { plugin.logWarning("Null island when loading..."); continue; } if (island.isDeleted()) { // These will be deleted later deletedIslands.add(island.getUniqueId()); } else if (island.isDoNotLoad() && island.getWorld() != null && island.getCenter() != null) { // Add to quarantine cache quarantineCache.computeIfAbsent(island.getOwner(), k -> new ArrayList<>()).add(island); } // Check island distance and if incorrect stop BentoBox else if (island.getWorld() != null && plugin.getIWM().inWorld(island.getWorld()) && island.getRange() != plugin.getIWM().getIslandDistance(island.getWorld())) { throw new IOException("Island distance mismatch!\n" + "World '" + island.getWorld().getName() + "' distance " + plugin.getIWM().getIslandDistance(island.getWorld()) + " != island range " + island.getRange() + "!\n" + "Island ID in database is " + island.getUniqueId() + ".\n" + "Island distance in config.yml cannot be changed mid-game! Fix config.yml or clean database."); } else { // Fix island center if it is off fixIslandCenter(island); if (!islandCache.addIsland(island)) { // Quarantine the offending island toQuarantine.add(island); // Add to quarantine cache island.setDoNotLoad(true); quarantineCache.computeIfAbsent(island.getOwner(), k -> new ArrayList<>()).add(island); if (island.isUnowned()) { unowned++; } else { owned++; } } else if (island.isSpawn()) { // Success, set spawn if this is the spawn island. this.setSpawn(island); } else { // Successful load // Clean any null flags out of the island - these can occur for various reasons island.getFlags().keySet().removeIf(f -> f.startsWith("NULL_FLAG")); } } // Update some of their fields if (island.getGameMode() == null) { island.setGameMode(plugin.getIWM().getAddon(island.getWorld()).map(gm -> gm.getDescription().getName()) .orElse("")); } } if (!toQuarantine.isEmpty()) { plugin.logError(toQuarantine.size() + " islands could not be loaded successfully; moving to trash bin."); plugin.logError(unowned + " are unowned, " + owned + " are owned."); toQuarantine.forEach(handler::saveObjectAsync); // Check if there are any islands with duplicate islands Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Set duplicatedUUIDRemovedSet = new HashSet<>(); Set duplicated = islandCache.getIslands().stream().map(Island::getOwner).filter(Objects::nonNull) .filter(n -> !duplicatedUUIDRemovedSet.add(n)).collect(Collectors.toSet()); if (!duplicated.isEmpty()) { plugin.logError("**** Owners that have more than one island = " + duplicated.size()); for (UUID uuid : duplicated) { Set set = islandCache.getIslands().stream().filter(i -> uuid.equals(i.getOwner())) .collect(Collectors.toSet()); plugin.logError(plugin.getPlayers().getName(uuid) + "(" + uuid.toString() + ") has " + set.size() + " islands:"); set.forEach(i -> { plugin.logError("Island at " + i.getCenter()); plugin.logError("Island unique ID = " + i.getUniqueId()); }); plugin.logError( "You should find out which island is real and delete the uniqueID from the database for the bogus one."); plugin.logError(""); } } }); } } /** * Island coordinates should always be a multiple of the island distance x 2. If * they are not, this method realigns the grid coordinates. * * @param island - island * @return true if coordinate is altered * @since 1.3.0 */ boolean fixIslandCenter(Island island) { World world = island.getWorld(); if (world == null || island.getCenter() == null || !plugin.getIWM().inWorld(world)) { return false; } int distance = plugin.getIWM().getIslandDistance(island.getWorld()) * 2; long x = ((long) island.getCenter().getBlockX()) - plugin.getIWM().getIslandXOffset(world) - plugin.getIWM().getIslandStartX(world); long z = ((long) island.getCenter().getBlockZ()) - plugin.getIWM().getIslandZOffset(world) - plugin.getIWM().getIslandStartZ(world); if (x % distance != 0 || z % distance != 0) { // Island is off grid x = Math.round((double) x / distance) * distance + plugin.getIWM().getIslandXOffset(world) + plugin.getIWM().getIslandStartX(world); z = Math.round((double) z / distance) * distance + plugin.getIWM().getIslandZOffset(world) + plugin.getIWM().getIslandStartZ(world); island.setCenter(new Location(world, x, island.getCenter().getBlockY(), z)); return true; } return false; } /** * Checks if a specific location is within the protected range of an island that * the player is a member of (owner or member) * * @param player - the player * @param loc - location * @return true if location is on island of player */ public boolean locationIsOnIsland(Player player, Location loc) { if (player == null) { return false; } // Get the player's island return getIslandAt(loc).filter(i -> i.onIsland(loc)).map(i -> i.getMemberSet().contains(player.getUniqueId())) .orElse(false); } /** * Checks if an online player is in the protected area of an island he owns or * he is part of. i.e. rank is greater than VISITOR_RANK * * @param world the World to check. Typically this is the user's world. Does not * check nether or end worlds. If null the method will always * return {@code false}. * @param user the User to check, if null or if this is not a Player the method * will always return {@code false}. * * @return {@code true} if this User is located within the protected area of an * island he owns or he is part of, {@code false} otherwise or if this * User is not located in this World. */ public boolean userIsOnIsland(World world, User user) { if (user == null || !user.isPlayer() || world == null) { return false; } return (user.getLocation().getWorld() == world) && getProtectedIslandAt(user.getLocation()) .map(i -> i.getMembers().entrySet().stream().anyMatch( en -> en.getKey().equals(user.getUniqueId()) && en.getValue() > RanksManager.VISITOR_RANK)) .orElse(false); } /** * Removes this player from any and all islands in world * * @param world - world * @param user - user */ public void removePlayer(World world, User user) { removePlayer(world, user.getUniqueId()); } /** * Removes this player from any and all islands in world * * @param world - world * @param uuid - user's uuid */ public void removePlayer(World world, UUID uuid) { islandCache.removePlayer(world, uuid).forEach(handler::saveObjectAsync); } /** * Remove this player from this island * * @param island island * @param uuid uuid of member */ public void removePlayer(Island island, UUID uuid) { islandCache.removePlayer(island, uuid); } /** * This teleports players away from an island - used when reseting or deleting * an island * * @param island to remove players from */ public void removePlayersFromIsland(Island island) { World w = island.getWorld(); Bukkit.getOnlinePlayers().stream() .filter(p -> p.getGameMode().equals(plugin.getIWM().getDefaultGameMode(island.getWorld()))) .filter(p -> island.onIsland(p.getLocation())).forEach(p -> { // Teleport island players to their island home if (!island.getMemberSet().contains(p.getUniqueId()) && (hasIsland(w, p.getUniqueId()) || inTeam(w, p.getUniqueId()))) { homeTeleportAsync(w, p); } else { // Move player to spawn if (spawn.containsKey(w)) { // go to island spawn Location sp = spawn.get(w).getSpawnPoint(w.getEnvironment()); if (sp != null) { PaperLib.teleportAsync(p, sp); } else { plugin.logWarning("Spawn exists but its location is null!"); } } } }); } public boolean isSaveTaskRunning() { return isSaveTaskRunning; } /** * Save the all the islands to the database */ public void saveAll() { saveAll(false); } /** * Save the all the islands to the database * * @param schedule true if we should let the task run over multiple ticks to * reduce lag spikes */ public void saveAll(boolean schedule) { if (!schedule) { for (Island island : islandCache.getIslands()) { if (island.isChanged()) { try { handler.saveObjectAsync(island); } catch (Exception e) { plugin.logError("Could not save island to database when running sync! " + e.getMessage()); } } } return; } isSaveTaskRunning = true; Queue queue = new LinkedList<>(islandCache.getIslands()); new BukkitRunnable() { @Override public void run() { for (int i = 0; i < plugin.getSettings().getMaxSavedIslandsPerTick(); i++) { Island island = queue.poll(); if (island == null) { isSaveTaskRunning = false; cancel(); return; } if (island.isChanged()) { try { handler.saveObjectAsync(island); } catch (Exception e) { plugin.logError("Could not save island to database when running sync! " + e.getMessage()); } } } } }.runTaskTimer(plugin, 0, 1); } /** * Puts a player in a team. Removes them from their old island if required. * * @param teamIsland - team island * @param playerUUID - the player's UUID */ public void setJoinTeam(Island teamIsland, UUID playerUUID) { // Add player to new island teamIsland.addMember(playerUUID); islandCache.addPlayer(playerUUID, teamIsland); // Save the island handler.saveObjectAsync(teamIsland); } public void setLast(Location last) { this.last.put(last.getWorld(), last); } public void shutdown() { plugin.log("Removing coops from islands..."); // Remove all coop associations islandCache.getIslands().forEach(i -> i.getMembers().values().removeIf(p -> p == RanksManager.COOP_RANK)); plugin.log("Saving islands - this has to be done sync so it may take a while with a lot of islands..."); saveAll(); plugin.log("Islands saved."); islandCache.clear(); plugin.log("Closing database."); handler.close(); } /** * Checks if a player is in a team in this world. Note that the player may have * multiple islands in the world, any one of which may have a team. * * @param world - world * @param playerUUID - player's UUID * @return true if in team, false if not */ public boolean inTeam(World world, @NonNull UUID playerUUID) { return this.islandCache.getIslands(world, playerUUID).stream() .anyMatch(island -> island.getMemberSet().size() > 1 && island.getMemberSet().contains(playerUUID)); } /** * Sets this target as the owner for this island * * @param world world * @param user the user who is issuing the command * @param targetUUID the current island member who is going to become the new * owner */ public void setOwner(World world, User user, UUID targetUUID) { setOwner(user, targetUUID, getIsland(world, user.getUniqueId()), RanksManager.SUB_OWNER_RANK); } /** * Sets this target as the owner for this island * * @param user user making the change * @param targetUUID new owner * @param island island to register * @param rank rank to which to set old owner. */ public void setOwner(User user, UUID targetUUID, Island island, int rank) { // Demote the old owner if (rank >= RanksManager.OWNER_RANK) { plugin.logWarning("Setowner: previous owner's rank cannot be higher than SubOwner"); rank = RanksManager.SUB_OWNER_RANK; } if (rank > RanksManager.VISITOR_RANK && island.getOwner() != null) { island.setRank(island.getOwner(), rank); } // Make the new owner islandCache.setOwner(island, targetUUID); user.sendMessage("commands.island.team.setowner.name-is-the-owner", "[name]", plugin.getPlayers().getName(targetUUID)); plugin.getIWM().getAddon(island.getWorld()).ifPresent(addon -> { User target = User.getInstance(targetUUID); // Tell target. If they are offline, then they may receive a message when they // login target.sendMessage("commands.island.team.setowner.you-are-the-owner"); // Permission checks for range changes only work when the target is online if (target.isOnline() && target.getEffectivePermissions().parallelStream().map(PermissionAttachmentInfo::getPermission) .anyMatch(p -> p.startsWith(addon.getPermissionPrefix() + "island.range"))) { // Check if new owner has a different range permission than the island size int range = target.getPermissionValue(addon.getPermissionPrefix() + "island.range", plugin.getIWM().getIslandProtectionRange(Util.getWorld(island.getWorld()))); // Range can go up or down if (range != island.getProtectionRange()) { user.sendMessage("commands.admin.setrange.range-updated", TextVariables.NUMBER, String.valueOf(range)); target.sendMessage("commands.admin.setrange.range-updated", TextVariables.NUMBER, String.valueOf(range)); plugin.log("Setowner: Island protection range changed from " + island.getProtectionRange() + " to " + range + " for " + user.getName() + " due to permission."); // Get old range for event int oldRange = island.getProtectionRange(); island.setProtectionRange(range); // Call Protection Range Change event. Does not support canceling. IslandEvent.builder().island(island).location(island.getCenter()) .reason(IslandEvent.Reason.RANGE_CHANGE).involvedPlayer(targetUUID).admin(true) .protectionRange(range, oldRange).build(); } } }); } /** * Clear an area of mobs as per world rules. Radius is default 5 blocks in every * direction. Value is set in BentoBox config.yml Will not remove any named * monsters. * * @param loc - location to clear */ public void clearArea(Location loc) { if (!plugin.getIWM().inWorld(loc)) return; loc.getWorld() .getNearbyEntities(loc, plugin.getSettings().getClearRadius(), plugin.getSettings().getClearRadius(), plugin.getSettings().getClearRadius()) .stream().filter(LivingEntity.class::isInstance) .filter(en -> Util.isHostileEntity(en) && !plugin.getIWM().getRemoveMobsWhitelist(loc.getWorld()).contains(en.getType()) && !(en instanceof PufferFish) && ((LivingEntity) en).getRemoveWhenFarAway()) .filter(en -> en.getCustomName() == null).forEach(Entity::remove); } /** * Removes a player from any island where they hold the indicated rank. * Typically this is to remove temporary ranks such as coop. Removal is done in * all worlds. * * @param rank - rank to clear * @param uniqueId - UUID of player */ public void clearRank(int rank, UUID uniqueId) { islandCache.getIslands().forEach( i -> i.getMembers().entrySet().removeIf(e -> e.getKey().equals(uniqueId) && e.getValue() == rank)); } /** * Save the island to the database * * @param island - island */ public void save(Island island) { handler.saveObjectAsync(island); } /** * Try to get an island by its unique id * * @param uniqueId - unique id string * @return optional island * @since 1.3.0 */ @NonNull public Optional getIslandById(String uniqueId) { return Optional.ofNullable(islandCache.getIslandById(uniqueId)); } /** * Try to get an unmodifiable list of quarantined islands owned by uuid in this * world * * @param world - world * @param uuid - target player's UUID, or null = unowned islands * @return list of islands; may be empty * @since 1.3.0 */ @NonNull public List getQuarantinedIslandByUser(@NonNull World world, @Nullable UUID uuid) { return quarantineCache.getOrDefault(uuid, Collections.emptyList()).stream() .filter(i -> i.getWorld().equals(world)).toList(); } /** * Delete quarantined islands owned by uuid in this world * * @param world - world * @param uuid - target player's UUID, or null = unowned islands * @since 1.3.0 */ public void deleteQuarantinedIslandByUser(World world, @Nullable UUID uuid) { if (quarantineCache.containsKey(uuid)) { quarantineCache.get(uuid).stream().filter(i -> i.getWorld().equals(world)) .forEach(i -> handler.deleteObject(i)); quarantineCache.get(uuid).removeIf(i -> i.getWorld().equals(world)); } } /** * @return the quarantineCache * @since 1.3.0 */ @NonNull public Map> getQuarantineCache() { return quarantineCache; } /** * Remove a quarantined island and delete it from the database completely. This * is NOT recoverable unless you have database backups. * * @param island island * @return {@code true} if island is quarantined and removed * @since 1.3.0 */ public boolean purgeQuarantinedIsland(Island island) { if (quarantineCache.containsKey(island.getOwner()) && quarantineCache.get(island.getOwner()).remove(island)) { handler.deleteObject(island); return true; } return false; } /** * Switches active island and island in trash * * @param world - game world * @param target - target player's UUID * @param island - island in trash * @return true if successful, otherwise false * @since 1.3.0 */ public boolean switchIsland(World world, UUID target, Island island) { // Remove trashed island from trash if (!quarantineCache.containsKey(island.getOwner()) || !quarantineCache.get(island.getOwner()).remove(island)) { plugin.logError("Could not remove island from trash"); return false; } // Remove old island from cache if it exists if (this.hasIsland(world, target)) { Island oldIsland = islandCache.get(world, target); islandCache.removeIsland(oldIsland); // Set old island to trash oldIsland.setDoNotLoad(true); // Put old island into trash quarantineCache.computeIfAbsent(target, k -> new ArrayList<>()).add(oldIsland); // Save old island handler.saveObjectAsync(oldIsland).thenAccept(result -> { if (Boolean.FALSE.equals(result)) plugin.logError("Could not save trashed island in database"); }); } // Restore island from trash island.setDoNotLoad(false); // Add new island to cache if (!islandCache.addIsland(island)) { plugin.logError("Could not add recovered island to cache"); return false; } // Save new island handler.saveObjectAsync(island).thenAccept(result -> { if (Boolean.FALSE.equals(result)) { plugin.logError("Could not save recovered island to database"); } }); return true; } /** * Resets all flags to gamemode config.yml default * * @param world - world * @since 1.3.0 */ public void resetAllFlags(World world) { islandCache.resetAllFlags(world); this.saveAll(); } /** * Resets a flag to gamemode config.yml default * * @param world - world * @param flag - flag to reset * @since 1.8.0 */ public void resetFlag(World world, Flag flag) { islandCache.resetFlag(world, flag); this.saveAll(); } /** * Returns whether the specified island custom name exists in this world. * * @param world World of the gamemode * @param name Name of an island * @return {@code true} if there is an island with the specified name in this * world, {@code false} otherwise. * @since 1.7.0 */ public boolean nameExists(@NonNull World world, @NonNull String name) { return getIslands(world).stream().map(Island::getName).filter(Objects::nonNull) .anyMatch(n -> ChatColor.stripColor(n).equals(ChatColor.stripColor(name))); } /** * Called by the admin team fix command. Attempts to fix the database for teams. * It will identify and correct situations where a player is listed in multiple * teams, or is the owner of multiple teams. It will also try to fix the current * cache. It is recommended to restart the server after this command is run. * * @param user - admin calling * @param world - game world to check * @return CompletableFuture boolean - true when done * @deprecated Not compatible with multi-islands. Will be removed. */ @Deprecated public CompletableFuture checkTeams(User user, World world) { CompletableFuture r = new CompletableFuture<>(); user.sendMessage("commands.admin.team.fix.scanning"); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Map owners = new HashMap<>(); Map freq = new HashMap<>(); Map> memberships = new HashMap<>(); handler.loadObjects().stream().filter(i -> i.getOwner() != null).filter(i -> i.getWorld() != null) .filter(i -> i.getWorld().equals(world)).filter(i -> !i.isDoNotLoad()).forEach(i -> { int count = freq.getOrDefault(i.getOwner(), 0); freq.put(i.getOwner(), count + 1); if (owners.containsKey(i.getOwner())) { // Player already has an island in the database user.sendMessage("commands.admin.team.fix.duplicate-owner", TextVariables.NAME, plugin.getPlayers().getName(i.getOwner())); Island prev = owners.get(i.getOwner()); // Find out if this island is in the cache Island cachedIsland = this.getIsland(i.getWorld(), i.getOwner()); if (cachedIsland != null && !cachedIsland.getUniqueId().equals(i.getUniqueId())) { islandCache.deleteIslandFromCache(i.getUniqueId()); handler.deleteID(i.getUniqueId()); } if (cachedIsland != null && !cachedIsland.getUniqueId().equals(prev.getUniqueId())) { islandCache.deleteIslandFromCache(prev.getUniqueId()); handler.deleteID(prev.getUniqueId()); } } else { owners.put(i.getOwner(), i); i.getMemberSet().forEach(u -> // Place into membership memberships.computeIfAbsent(u, k -> new ArrayList<>()).add(i)); } }); freq.entrySet().stream().filter(en -> en.getValue() > 1) .forEach(en -> user.sendMessage("commands.admin.team.fix.player-has", TextVariables.NAME, plugin.getPlayers().getName(en.getKey()), TextVariables.NUMBER, String.valueOf(en.getValue()))); // Check for players in multiple teams memberships.entrySet().stream().filter(en -> en.getValue().size() > 1).forEach(en -> { // Get the islands String ownerName = plugin.getPlayers().getName(en.getKey()); user.sendMessage("commands.admin.team.fix.duplicate-member", TextVariables.NAME, ownerName); int highestRank = 0; Island highestIsland = null; for (Island i : en.getValue()) { int rankValue = i.getRank(en.getKey()); String rank = RanksManager.getInstance().getRank(rankValue); if (rankValue > highestRank || highestIsland == null) { highestRank = rankValue; highestIsland = i; } String xyz = Util.xyz(i.getCenter().toVector()); user.sendMessage("commands.admin.team.fix.rank-on-island", TextVariables.RANK, user.getTranslation(rank), TextVariables.XYZ, xyz); user.sendRawMessage(i.getUniqueId()); } // Fix island ownership in cache // Correct island cache if (highestRank == RanksManager.OWNER_RANK && highestIsland != null && islandCache.getIslandById(highestIsland.getUniqueId()) != null) { islandCache.setOwner(islandCache.getIslandById(highestIsland.getUniqueId()), en.getKey()); } // Fix all the entries that are not the highest for (Island island : en.getValue()) { if (!island.equals(highestIsland)) { // Get the actual island being used in the cache Island i = islandCache.getIslandById(island.getUniqueId()); if (i != null) { // Remove membership of this island i.removeMember(en.getKey()); } // Remove from database island island.removeMember(en.getKey()); // Save to database handler.saveObjectAsync(island) .thenRun(() -> user.sendMessage("commands.admin.team.fix.fixed")); } else { // Special check for when a player is an owner and member } } }); user.sendMessage("commands.admin.team.fix.done"); r.complete(true); }); return r; } /** * Is user mid home teleport? * * @return true or false */ public boolean isGoingHome(User user) { return goingHome.contains(user.getUniqueId()); } /** * Get the number of concurrent islands for this player * * @param uuid UUID of player * @param world world to check * @return number of islands this player owns in this game world */ public int getNumberOfConcurrentIslands(UUID uuid, World world) { return islandCache.getIslands(world, uuid).size(); } /** * Sets the user's primary island * * @param uuid user's uuid * @param i island */ public void setPrimaryIsland(UUID uuid, Island i) { this.getIslandCache().setPrimaryIsland(uuid, i); } /** * Convenience method. See {@link IslandCache#get(World, UUID)} * * @param world world * @param uuid player's UUID * @return Island of player or null if there isn't one */ public Island getPrimaryIsland(World world, UUID uuid) { return this.getIslandCache().get(world, uuid); } }