From 3061e80097e796fb87ae2d4fe807909ce2e6e88b Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 13 Jan 2024 19:50:15 -0800 Subject: [PATCH] Release 2.12.0 (#301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Version 2.7.1 * Version 2.7.2 * Use Java 9's takeWhile * Added placeholder %Level_[gamemode]_rank_value Fixes https://github.com/BentoBoxWorld/Level/issues/228 * No save on disable (#231) * Release 2.6.4 * Remove saving to database on disable. https://github.com/BentoBoxWorld/Level/issues/229 First, the top ten tables are never actually used or loaded. They are created in memory by loading the island levels. So there is no reason to keep saving them. Second, the island level data is saved every time it is changed, so there is no need to save all of the cache on exit. * Fixes tests * Rosestacker (#232) * Add support for RoseStacker 1.3.0 * Made plugin a Pladdon. * Version 2.8.0 * Added new placeholders %Level_%gamemode%_top_island_name_%rank% - lists the island name %Level_%gamemode%_top_island_members_%rank% - a comma separated list of team members https://github.com/BentoBoxWorld/Level/issues/224 https://github.com/BentoBoxWorld/Level/issues/211 https://github.com/BentoBoxWorld/Level/issues/132 https://github.com/BentoBoxWorld/Level/issues/107 https://github.com/BentoBoxWorld/Level/issues/105 * Update to BentoBox API 1.18 * Open up modules for testing access. * Back support for BentoBox 1.16.5. * Version 2.8.1 * Speeds up level calculation by doing more chunk scans async. If chests are scanned, then it will take longer because these have to be done sync. https://github.com/BentoBoxWorld/Level/issues/243 * add Vietnamese (#240) * Raw island level placeholder (#241) * Changed IslandLevelCalculator minHeight to world minHeight for negative blocks height support since 1.18. (#246) * Version 2.9.0 * Chinese Translation (#249) * Translate zh-CN.yml via GitLocalize * Translate zh-CN.yml via GitLocalize Co-authored-by: mt-gitlocalize Co-authored-by: 织梦 <493733933@qq.com> * Translate id.yml via GitLocalize (#250) Co-authored-by: Nathan Adhitya * Translate fr.yml via GitLocalize (#251) Co-authored-by: organizatsiya * Korean translation (#252) * Translate ko.yml via GitLocalize * Translate ko.yml via GitLocalize Co-authored-by: chickiyeah Co-authored-by: mt-gitlocalize * German Translation (#253) * Translate de.yml via GitLocalize * Update de.yml Co-authored-by: Rikamo045 Co-authored-by: tastybento * Translate hu.yml via GitLocalize (#254) Co-authored-by: András Marczinkó * Version 2.9.1 * Attempt to handle WildStacker spawners * Fix error lon loading id locale * Avoid async chunk snapshotting. Fixes https://github.com/BentoBoxWorld/Level/issues/256 * Update to BentoBox API 1.20. Replace plugin.yml with spigot-annotations. Implement customizable TopLevelPanel. * Fixes some small issues with TopLevelPanel Add Utils class that contains some useful things. * Implement customizable DetailsPanel. Remove old DetailsGUITab due to new implementation. * Fix failing test. * Remove blank file * Added repo for maven plugin snapshots * Implement feature that allows to sort items in detail panel. (#259) Apparently, because it is 2 years old request, it got in a state -> implement or drop. Fixes #192 * Implement calculated value for blocks. (#260) It is ~ value, as calculation formula cannot be applied per block. At least I think so. Part of #192 * Update es.yml (#261) * Implement customizable Values GUI. (#262) This GUI shows value to all items in game. It also shows max limit of blocks, if it is set. Fixes of #192 * Support for AdvancedChests was updated. (#266) * Implements visit/warp actions in top gui Add 2 new actions for island buttons in TOP GUI: - Visit -> allows to visit island, but it requires Visit Addon - Warp -> allows to warp to island, but it requires Warp Addon Requested via Discord. * Fixes a Level addon crash on startup. Level addon crashed at the startup if Visit or Warps addon were not installed. It happened because Level addon main class were implementing Listener interface. To avoid it and fix the crash, I moved migration listener to a separate class. Fixes #2012 * Translate pl.yml via GitLocalize (#269) Co-authored-by: wiktorm12 * Translate fr.yml via GitLocalize (#272) Co-authored-by: organizatsiya * Update to Java 17 * Update Github workflow to Java 17 * Adds %Level_[gamemode]_island_level_max% placeholder This records the lifetime maximum level the island has ever had. Addresses #271 * Only shows Members or higher in the top members placeholder Fixes #267 * Add natural log to level-calc formula parsing Relates to #274 * feat: add island total points + placeholder (#264) * feat: add island total points + placeholder * Update IslandLevels.java * Fix JavaDoc * Translate zh-CN.yml via GitLocalize (#276) Co-authored-by: dawnTak * Translate nl.yml via GitLocalize (#277) Co-authored-by: DevSolaris * Add ${argLine} to get jacoco coverage * Updated Jacoco POM * Add shulker to in chest count (#275) * Sonar Cloud code smell clean up (#278) * Refactor placeholders (#279) * Update ReadMe * Fix Jacoco * Remove unused imports * Remove placeholders from main class Created a separate class for cleaner code and added a test class. * Remove dependency * Add UltimateStacker hook for stacked blocks (#281) * Create plugin.yml (#282) * Create plugin.yml The annotations do not provide the option to define the version dynamically from maven. This should fix that. * Remove Spigot Plugin Annotations * Remove plugin-annotation repo * Updated dependencies * Version 2.10.1 * Add blocks that should be zero by default as they are available on the ocean floor. https://github.com/BentoBoxWorld/Level/issues/284 * Chinese (#288) * Translate zh-CN.yml via GitLocalize * Translate zh-CN.yml via GitLocalize --------- Co-authored-by: Jeansou Co-authored-by: dawnTak * Translate id.yml via GitLocalize (#287) Co-authored-by: Dusty * French (#286) * Translate fr.yml via GitLocalize * Translate fr.yml via GitLocalize * Translate fr.yml via GitLocalize --------- Co-authored-by: gitlocalize-app[bot] <55277160+gitlocalize-app[bot]@users.noreply.github.com> Co-authored-by: organizatsiya Co-authored-by: Florian CUNY * Spanish (#285) * Translate es.yml via GitLocalize * Translate es.yml via GitLocalize --------- Co-authored-by: ChrissTM03 Co-authored-by: Espan * Version 2.11.0 * Update Github Build script * Added distribution required for Github Action * Update Jacoco * Update pom.xml * Add config option to disable plugin hooks (#291) * Update UltimateStacker dependency * Add config option to disable plugin hooks * Use 2.0.0 BentoBox API * Version 2.12.0 * Adds an admin stats command. See #293 * Update tests * Update to BentoBox 2.0.0 API * Added test for Stats command * Try lower version of jacoco * Changed top ten internally to use islands instead of players as keys (#295) Added %[gamemode]_top_weighted_value_x% placeholder https://github.com/BentoBoxWorld/Level/issues/294 * Added more placeholders. #296 Refactored how the top ten maps are structured. In the future, it may be best to have the key be the island. * Translate uk.yml via GitLocalize (#297) Co-authored-by: GIGABAIT * Move to 1.20.4 Refactored the calculator code for clarity. Added Jacoco line to prvent issues with the bigger Material class. * Added comments on the panel templates. * Add protection around unknown blockconfig.yml entries. GRASS>SHORT_GRASS * Uses latest Visit API to avoid chat spam. Fixes #299 (#300) * Added test class for EquationEvaluator * Fix merge error --------- Co-authored-by: Huynh Tien Co-authored-by: Rubén <44579213+Rubenicos@users.noreply.github.com> Co-authored-by: Pierre Dedrie Co-authored-by: gitlocalize-app[bot] <55277160+gitlocalize-app[bot]@users.noreply.github.com> Co-authored-by: mt-gitlocalize Co-authored-by: 织梦 <493733933@qq.com> Co-authored-by: Nathan Adhitya Co-authored-by: organizatsiya Co-authored-by: chickiyeah Co-authored-by: Rikamo045 Co-authored-by: András Marczinkó Co-authored-by: BONNe Co-authored-by: KrazyxWolf <68208993+KrazyxWolf@users.noreply.github.com> Co-authored-by: DeadSilenceIV Co-authored-by: wiktorm12 Co-authored-by: evlad Co-authored-by: dawnTak Co-authored-by: DevSolaris Co-authored-by: DevSolaris <105156235+DevSolaris@users.noreply.github.com> Co-authored-by: ceze88 Co-authored-by: Jeansou Co-authored-by: Dusty Co-authored-by: Florian CUNY Co-authored-by: ChrissTM03 Co-authored-by: Espan Co-authored-by: PapiCapi <49530141+PapiCapi@users.noreply.github.com> Co-authored-by: GIGABAIT --- .github/workflows/build.yml | 9 +- pom.xml | 14 +- src/main/java/world/bentobox/level/Level.java | 794 ++++----- .../world/bentobox/level/LevelsManager.java | 546 ++++--- .../bentobox/level/PlaceholderManager.java | 265 +-- .../level/calculators/EquationEvaluator.java | 121 ++ .../calculators/IslandLevelCalculator.java | 1023 ++++++------ .../level/commands/AdminStatsCommand.java | 98 ++ .../level/commands/AdminTopCommand.java | 44 +- .../level/commands/AdminTopRemoveCommand.java | 55 +- .../bentobox/level/config/BlockConfig.java | 16 +- .../bentobox/level/config/ConfigSettings.java | 15 + .../listeners/IslandActivitiesListeners.java | 80 +- .../bentobox/level/objects/TopTenData.java | 33 +- .../bentobox/level/panels/DetailsPanel.java | 1421 ++++++++--------- .../bentobox/level/panels/TopLevelPanel.java | 694 ++++---- src/main/resources/blockconfig.yml | 2 +- src/main/resources/locales/en-US.yml | 14 +- src/main/resources/locales/uk.yml | 184 +++ src/main/resources/panels/detail_panel.yml | 46 +- src/main/resources/panels/top_panel.yml | 13 + .../java/world/bentobox/level/LevelTest.java | 447 +++--- .../bentobox/level/LevelsManagerTest.java | 244 +-- .../level/PlaceholderManagerTest.java | 249 +-- .../calculators/EquationEvaluatorTest.java | 37 + .../level/commands/AdminStatsCommandTest.java | 183 +++ .../commands/AdminTopRemoveCommandTest.java | 208 +++ .../admin/AdminTopRemoveCommandTest.java | 199 --- 28 files changed, 3822 insertions(+), 3232 deletions(-) create mode 100644 src/main/java/world/bentobox/level/calculators/EquationEvaluator.java create mode 100644 src/main/java/world/bentobox/level/commands/AdminStatsCommand.java create mode 100644 src/main/resources/locales/uk.yml create mode 100644 src/test/java/world/bentobox/level/calculators/EquationEvaluatorTest.java create mode 100644 src/test/java/world/bentobox/level/commands/AdminStatsCommandTest.java create mode 100644 src/test/java/world/bentobox/level/commands/AdminTopRemoveCommandTest.java delete mode 100644 src/test/java/world/bentobox/level/commands/admin/AdminTopRemoveCommandTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c771fd5..825b18d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,21 +11,22 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 17 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: + distribution: 'adopt' java-version: 17 - name: Cache SonarCloud packages - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Maven packages - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} diff --git a/pom.xml b/pom.xml index d204ec7..881d55e 100644 --- a/pom.xml +++ b/pom.xml @@ -58,12 +58,12 @@ 2.0.9 - 1.19.4-R0.1-SNAPSHOT - 1.23.0 + 1.20.4-R0.1-SNAPSHOT + 2.0.0-SNAPSHOT 1.12.0 - 1.4.0 + 1.6.0 1.1.0 @@ -71,7 +71,7 @@ -LOCAL - 2.11.0 + 2.12.0 BentoBoxWorld_Level bentobox-world https://sonarcloud.io @@ -237,7 +237,7 @@ com.songoda UltimateStacker - 2.3.3 + 2.4.0 provided @@ -403,13 +403,15 @@ org.jacoco jacoco-maven-plugin - 0.8.7 + 0.8.10 true **/*Names* + + org/bukkit/Material* diff --git a/src/main/java/world/bentobox/level/Level.java b/src/main/java/world/bentobox/level/Level.java index 0d02802..e13a70d 100644 --- a/src/main/java/world/bentobox/level/Level.java +++ b/src/main/java/world/bentobox/level/Level.java @@ -24,6 +24,7 @@ import world.bentobox.level.commands.AdminLevelCommand; import world.bentobox.level.commands.AdminLevelStatusCommand; import world.bentobox.level.commands.AdminSetInitialLevelCommand; +import world.bentobox.level.commands.AdminStatsCommand; import world.bentobox.level.commands.AdminTopCommand; import world.bentobox.level.commands.IslandLevelCommand; import world.bentobox.level.commands.IslandTopCommand; @@ -39,401 +40,412 @@ import world.bentobox.visit.VisitAddon; import world.bentobox.warps.Warp; - /** * @author tastybento * */ public class Level extends Addon { - // The 10 in top ten - public static final int TEN = 10; - - // Settings - private ConfigSettings settings; - private Config configObject = new Config<>(this, ConfigSettings.class); - private BlockConfig blockConfig; - private Pipeliner pipeliner; - private LevelsManager manager; - private boolean stackersEnabled; - private boolean advChestEnabled; - private boolean roseStackersEnabled; - private boolean ultimateStackerEnabled; - private final List registeredGameModes = new ArrayList<>(); - - /** - * Local variable that stores if warpHook is present. - */ - private Warp warpHook; - - /** - * Local variable that stores if visitHook is present. - */ - private VisitAddon visitHook; - - - @Override - public void onLoad() { - // Save the default config from config.yml - saveDefaultConfig(); - if (loadSettings()) { - // Disable - logError("Level settings could not load! Addon disabled."); - setState(State.DISABLED); - } else { - configObject.saveConfigObject(settings); - } - - // Save existing panels. - this.saveResource("panels/top_panel.yml", false); - this.saveResource("panels/detail_panel.yml", false); - this.saveResource("panels/value_panel.yml", false); - } - - private boolean loadSettings() { - // Load settings again to get worlds - settings = configObject.loadConfigObject(); - - return settings == null; - } - - @Override - public void onEnable() { - loadBlockSettings(); - // Start pipeline - pipeliner = new Pipeliner(this); - // Start Manager - manager = new LevelsManager(this); - // Register listeners - this.registerListener(new IslandActivitiesListeners(this)); - this.registerListener(new JoinLeaveListener(this)); - this.registerListener(new MigrationListener(this)); - - // Register commands for GameModes - registeredGameModes.clear(); - getPlugin().getAddonsManager().getGameModeAddons().stream() - .filter(gm -> !settings.getGameModes().contains(gm.getDescription().getName())) - .forEach(gm -> { - log("Level hooking into " + gm.getDescription().getName()); - registerCommands(gm); - new PlaceholderManager(this).registerPlaceholders(gm); - registeredGameModes.add(gm); - }); - // Register request handlers - registerRequestHandler(new LevelRequestHandler(this)); - registerRequestHandler(new TopTenRequestHandler(this)); - - // Check if WildStackers is enabled on the server - // I only added support for counting blocks into the island level - // Someone else can PR if they want spawners added to the Leveling system :) - stackersEnabled = Bukkit.getPluginManager().isPluginEnabled("WildStacker"); - if (stackersEnabled) { - log("Hooked into WildStackers."); - } - // Check if AdvancedChests is enabled on the server - Plugin advChest = Bukkit.getPluginManager().getPlugin("AdvancedChests"); - advChestEnabled = advChest != null; - if (advChestEnabled) { - // Check version - if (compareVersions(advChest.getDescription().getVersion(), "23.0") > 0) { - log("Hooked into AdvancedChests."); - } else { - logError("Could not hook into AdvancedChests " + advChest.getDescription().getVersion() + " - requires version 23.0 or later"); - advChestEnabled = false; - } - } - // Check if RoseStackers is enabled - roseStackersEnabled = Bukkit.getPluginManager().isPluginEnabled("RoseStacker"); - if (roseStackersEnabled) { - log("Hooked into RoseStackers."); - } - - // Check if UltimateStacker is enabled - ultimateStackerEnabled = Bukkit.getPluginManager().isPluginEnabled("UltimateStacker"); - if (ultimateStackerEnabled) { - log("Hooked into UltimateStacker."); - } - } - - @Override - public void allLoaded() - { - super.allLoaded(); - - if (this.isEnabled()) - { - this.hookExtensions(); - } - } - - - /** - * This method tries to hook into addons and plugins - */ - private void hookExtensions() - { - // Try to find Visit addon and if it does not exist, display a warning - this.getAddonByName("Visit").ifPresentOrElse(addon -> - { - this.visitHook = (VisitAddon) addon; - this.log("Level Addon hooked into Visit addon."); - }, () -> this.visitHook = null); - - // Try to find Warps addon and if it does not exist, display a warning - this.getAddonByName("Warps").ifPresentOrElse(addon -> - { - this.warpHook = (Warp) addon; - this.log("Level Addon hooked into Warps addon."); - }, () -> this.warpHook = null); - } - - - /** - * Compares versions - * @param version1 version 1 - * @param version2 version 2 - * @return {@code <0 if version 1 is older than version 2, =0 if the same, >0 if version 1 is newer than version 2} - */ - public static int compareVersions(String version1, String version2) { - int comparisonResult = 0; - - String[] version1Splits = version1.split("\\."); - String[] version2Splits = version2.split("\\."); - int maxLengthOfVersionSplits = Math.max(version1Splits.length, version2Splits.length); - - for (int i = 0; i < maxLengthOfVersionSplits; i++){ - Integer v1 = i < version1Splits.length ? Integer.parseInt(version1Splits[i]) : 0; - Integer v2 = i < version2Splits.length ? Integer.parseInt(version2Splits[i]) : 0; - int compare = v1.compareTo(v2); - if (compare != 0) { - comparisonResult = compare; - break; - } - } - return comparisonResult; - } - - private void registerCommands(GameModeAddon gm) { - gm.getAdminCommand().ifPresent(adminCommand -> { - new AdminLevelCommand(this, adminCommand); - new AdminTopCommand(this, adminCommand); - new AdminLevelStatusCommand(this, adminCommand); - if (getSettings().isZeroNewIslandLevels()) { - new AdminSetInitialLevelCommand(this, adminCommand); - } - }); - gm.getPlayerCommand().ifPresent(playerCmd -> { - new IslandLevelCommand(this, playerCmd); - new IslandTopCommand(this, playerCmd); - new IslandValueCommand(this, playerCmd); - }); - } - - @Override - public void onDisable() { - // Stop the pipeline - this.getPipeliner().stop(); - } - - private void loadBlockSettings() { - // Save the default blockconfig.yml - this.saveResource("blockconfig.yml", false); - - YamlConfiguration blockValues = new YamlConfiguration(); - try { - File file = new File(this.getDataFolder(), "blockconfig.yml"); - blockValues.load(file); - // Load the block config class - blockConfig = new BlockConfig(this, blockValues, file); - } catch (IOException | InvalidConfigurationException e) { - // Disable - logError("Level blockconfig.yml settings could not load! Addon disabled."); - setState(State.DISABLED); - } - - } - - - /** - * @return the blockConfig - */ - public BlockConfig getBlockConfig() { - return blockConfig; - } - - /** - * @return the settings - */ - public ConfigSettings getSettings() { - return settings; - } - - /** - * @return the pipeliner - */ - public Pipeliner getPipeliner() { - return pipeliner; - } - - /** - * @return the manager - */ - public LevelsManager getManager() { - return manager; - } - - /** - * Set the config settings - used for tests only - * @param configSettings - config settings - */ - void setSettings(ConfigSettings configSettings) { - this.settings = configSettings; - - } - - /** - * @return the stackersEnabled - */ - public boolean isStackersEnabled() { - return stackersEnabled; - } - - /** - * @return the advChestEnabled - */ - public boolean isAdvChestEnabled() { - return advChestEnabled; - } - - /** - * Get level from cache for a player. - * @param targetPlayer - target player UUID - * @return Level of player or zero if player is unknown or UUID is null - */ - public long getIslandLevel(World world, @Nullable UUID targetPlayer) { - return getManager().getIslandLevel(world, targetPlayer); - } - - /** - * Sets the player's level to a value - * @param world - world - * @param targetPlayer - target player - * @param level - level - */ - public void setIslandLevel(World world, UUID targetPlayer, long level) { - getManager().setIslandLevel(world, targetPlayer, level); - } - - /** - * Zeros the initial island level - * @param island - island - * @param level - initial calculated island level - */ - public void setInitialIslandLevel(@NonNull Island island, long level) { - getManager().setInitialIslandLevel(island, level); - } - - /** - * Get the initial island level - * @param island - island - * @return level or 0 by default - */ - public long getInitialIslandLevel(@NonNull Island island) { - return getManager().getInitialLevel(island); - } - - /** - * Calculates a user's island - * @param world - the world where this island is - * @param user - not used! See depecration message - * @param playerUUID - the target island member's UUID - * @deprecated Do not use this anymore. Use getManager().calculateLevel(playerUUID, island) - */ - @Deprecated(since="2.3.0", forRemoval=true) - public void calculateIslandLevel(World world, @Nullable User user, @NonNull UUID playerUUID) { - Island island = getIslands().getIsland(world, playerUUID); - if (island != null) getManager().calculateLevel(playerUUID, island); - } - - /** - * Provide the levels data for the target player - * @param targetPlayer - UUID of target player - * @return LevelsData object or null if not found. Only island levels are set! - * @deprecated Do not use this anymore. Use {@link #getIslandLevel(World, UUID)} - */ - @Deprecated(since="2.3.0", forRemoval=true) - public LevelsData getLevelsData(UUID targetPlayer) { - LevelsData ld = new LevelsData(targetPlayer); - getPlugin().getAddonsManager().getGameModeAddons().stream() - .filter(gm -> !settings.getGameModes().contains(gm.getDescription().getName())) - .forEach(gm -> { - if (getSettings().isZeroNewIslandLevels()) { - Island island = getIslands().getIsland(gm.getOverWorld(), targetPlayer); - if (island != null) { - ld.setInitialLevel(gm.getOverWorld(), this.getInitialIslandLevel(island)); - } - } - ld.setLevel(gm.getOverWorld(), this.getIslandLevel(gm.getOverWorld(), targetPlayer)); - }); - return ld; - } - - /** - * @return the registeredGameModes - */ - public List getRegisteredGameModes() { - return registeredGameModes; - } - - /** - * Check if Level addon is active in game mode - * @param gm Game Mode Addon - * @return true if active, false if not - */ - public boolean isRegisteredGameMode(GameModeAddon gm) { - return registeredGameModes.contains(gm); - } - - /** - * Checks if Level addon is active in world - * @param world world - * @return true if active, false if not - */ - public boolean isRegisteredGameModeWorld(World world) { - return registeredGameModes.stream().map(GameModeAddon::getOverWorld).anyMatch(w -> Util.sameWorld(world, w)); - } - - /** - * @return the roseStackersEnabled - */ - public boolean isRoseStackersEnabled() { - return roseStackersEnabled; - } - - /** - * @return the ultimateStackerEnabled - */ - public boolean isUltimateStackerEnabled() { - return ultimateStackerEnabled; - } - - /** - * Method Level#getVisitHook returns the visitHook of this object. - * - * @return {@code Visit} of this object, {@code null} otherwise. - */ - public VisitAddon getVisitHook() - { - return this.visitHook; - } - - /** - * Method Level#getWarpHook returns the warpHook of this object. - * - * @return {@code Warp} of this object, {@code null} otherwise. - */ - public Warp getWarpHook() - { - return this.warpHook; - } + // The 10 in top ten + public static final int TEN = 10; + + // Settings + private ConfigSettings settings; + private Config configObject = new Config<>(this, ConfigSettings.class); + private BlockConfig blockConfig; + private Pipeliner pipeliner; + private LevelsManager manager; + private boolean stackersEnabled; + private boolean advChestEnabled; + private boolean roseStackersEnabled; + private boolean ultimateStackerEnabled; + private final List registeredGameModes = new ArrayList<>(); + + /** + * Local variable that stores if warpHook is present. + */ + private Warp warpHook; + + /** + * Local variable that stores if visitHook is present. + */ + private VisitAddon visitHook; + + @Override + public void onLoad() { + // Save the default config from config.yml + saveDefaultConfig(); + if (loadSettings()) { + // Disable + logError("Level settings could not load! Addon disabled."); + setState(State.DISABLED); + } else { + configObject.saveConfigObject(settings); + } + + // Save existing panels. + this.saveResource("panels/top_panel.yml", false); + this.saveResource("panels/detail_panel.yml", false); + this.saveResource("panels/value_panel.yml", false); + } + + private boolean loadSettings() { + // Load settings again to get worlds + settings = configObject.loadConfigObject(); + + return settings == null; + } + + @Override + public void onEnable() { + loadBlockSettings(); + // Start pipeline + pipeliner = new Pipeliner(this); + // Start Manager + manager = new LevelsManager(this); + // Register listeners + this.registerListener(new IslandActivitiesListeners(this)); + this.registerListener(new JoinLeaveListener(this)); + this.registerListener(new MigrationListener(this)); + + // Register commands for GameModes + registeredGameModes.clear(); + getPlugin().getAddonsManager().getGameModeAddons().stream() + .filter(gm -> !settings.getGameModes().contains(gm.getDescription().getName())).forEach(gm -> { + log("Level hooking into " + gm.getDescription().getName()); + registerCommands(gm); + new PlaceholderManager(this).registerPlaceholders(gm); + registeredGameModes.add(gm); + }); + // Register request handlers + registerRequestHandler(new LevelRequestHandler(this)); + registerRequestHandler(new TopTenRequestHandler(this)); + + // Check if WildStackers is enabled on the server + // I only added support for counting blocks into the island level + // Someone else can PR if they want spawners added to the Leveling system :) + if (!settings.getDisabledPluginHooks().contains("WildStacker")) { + stackersEnabled = Bukkit.getPluginManager().isPluginEnabled("WildStacker"); + if (stackersEnabled) { + log("Hooked into WildStackers."); + } + } + + // Check if AdvancedChests is enabled on the server + if (!settings.getDisabledPluginHooks().contains("AdvancedChests")) { + Plugin advChest = Bukkit.getPluginManager().getPlugin("AdvancedChests"); + advChestEnabled = advChest != null; + if (advChestEnabled) { + // Check version + if (compareVersions(advChest.getDescription().getVersion(), "23.0") > 0) { + log("Hooked into AdvancedChests."); + } else { + logError("Could not hook into AdvancedChests " + advChest.getDescription().getVersion() + + " - requires version 23.0 or later"); + advChestEnabled = false; + } + } + } + + // Check if RoseStackers is enabled + if (!settings.getDisabledPluginHooks().contains("RoseStacker")) { + roseStackersEnabled = Bukkit.getPluginManager().isPluginEnabled("RoseStacker"); + if (roseStackersEnabled) { + log("Hooked into RoseStackers."); + } + } + + // Check if UltimateStacker is enabled + if (!settings.getDisabledPluginHooks().contains("UltimateStacker")) { + ultimateStackerEnabled = Bukkit.getPluginManager().isPluginEnabled("UltimateStacker"); + if (ultimateStackerEnabled) { + log("Hooked into UltimateStacker."); + } + } + } + + @Override + public void allLoaded() { + super.allLoaded(); + + if (this.isEnabled()) { + this.hookExtensions(); + } + } + + /** + * This method tries to hook into addons and plugins + */ + private void hookExtensions() { + // Try to find Visit addon and if it does not exist, display a warning + this.getAddonByName("Visit").ifPresentOrElse(addon -> { + this.visitHook = (VisitAddon) addon; + this.log("Level Addon hooked into Visit addon."); + }, () -> this.visitHook = null); + + // Try to find Warps addon and if it does not exist, display a warning + this.getAddonByName("Warps").ifPresentOrElse(addon -> { + this.warpHook = (Warp) addon; + this.log("Level Addon hooked into Warps addon."); + }, () -> this.warpHook = null); + } + + /** + * Compares versions + * + * @param version1 version 1 + * @param version2 version 2 + * @return {@code <0 if version 1 is older than version 2, =0 if the same, >0 if version 1 is newer than version 2} + */ + public static int compareVersions(String version1, String version2) { + int comparisonResult = 0; + + String[] version1Splits = version1.split("\\."); + String[] version2Splits = version2.split("\\."); + int maxLengthOfVersionSplits = Math.max(version1Splits.length, version2Splits.length); + + for (int i = 0; i < maxLengthOfVersionSplits; i++) { + Integer v1 = i < version1Splits.length ? Integer.parseInt(version1Splits[i]) : 0; + Integer v2 = i < version2Splits.length ? Integer.parseInt(version2Splits[i]) : 0; + int compare = v1.compareTo(v2); + if (compare != 0) { + comparisonResult = compare; + break; + } + } + return comparisonResult; + } + + private void registerCommands(GameModeAddon gm) { + gm.getAdminCommand().ifPresent(adminCommand -> { + new AdminLevelCommand(this, adminCommand); + new AdminTopCommand(this, adminCommand); + new AdminLevelStatusCommand(this, adminCommand); + if (getSettings().isZeroNewIslandLevels()) { + new AdminSetInitialLevelCommand(this, adminCommand); + } + new AdminStatsCommand(this, adminCommand); + }); + gm.getPlayerCommand().ifPresent(playerCmd -> { + new IslandLevelCommand(this, playerCmd); + new IslandTopCommand(this, playerCmd); + new IslandValueCommand(this, playerCmd); + }); + } + + @Override + public void onDisable() { + // Stop the pipeline + this.getPipeliner().stop(); + } + + private void loadBlockSettings() { + // Save the default blockconfig.yml + this.saveResource("blockconfig.yml", false); + + YamlConfiguration blockValues = new YamlConfiguration(); + try { + File file = new File(this.getDataFolder(), "blockconfig.yml"); + blockValues.load(file); + // Load the block config class + blockConfig = new BlockConfig(this, blockValues, file); + } catch (IOException | InvalidConfigurationException e) { + // Disable + logError("Level blockconfig.yml settings could not load! Addon disabled."); + setState(State.DISABLED); + } + + } + + /** + * @return the blockConfig + */ + public BlockConfig getBlockConfig() { + return blockConfig; + } + + /** + * @return the settings + */ + public ConfigSettings getSettings() { + return settings; + } + + /** + * @return the pipeliner + */ + public Pipeliner getPipeliner() { + return pipeliner; + } + + /** + * @return the manager + */ + public LevelsManager getManager() { + return manager; + } + + /** + * Set the config settings - used for tests only + * + * @param configSettings - config settings + */ + void setSettings(ConfigSettings configSettings) { + this.settings = configSettings; + + } + + /** + * @return the stackersEnabled + */ + public boolean isStackersEnabled() { + return stackersEnabled; + } + + /** + * @return the advChestEnabled + */ + public boolean isAdvChestEnabled() { + return advChestEnabled; + } + + /** + * Get level from cache for a player. + * + * @param targetPlayer - target player UUID + * @return Level of player or zero if player is unknown or UUID is null + */ + public long getIslandLevel(World world, @Nullable UUID targetPlayer) { + return getManager().getIslandLevel(world, targetPlayer); + } + + /** + * Sets the player's level to a value + * + * @param world - world + * @param targetPlayer - target player + * @param level - level + */ + public void setIslandLevel(World world, UUID targetPlayer, long level) { + getManager().setIslandLevel(world, targetPlayer, level); + } + + /** + * Zeros the initial island level + * + * @param island - island + * @param level - initial calculated island level + */ + public void setInitialIslandLevel(@NonNull Island island, long level) { + getManager().setInitialIslandLevel(island, level); + } + + /** + * Get the initial island level + * + * @param island - island + * @return level or 0 by default + */ + public long getInitialIslandLevel(@NonNull Island island) { + return getManager().getInitialLevel(island); + } + + /** + * Calculates a user's island + * + * @param world - the world where this island is + * @param user - not used! See depecration message + * @param playerUUID - the target island member's UUID + * @deprecated Do not use this anymore. Use + * getManager().calculateLevel(playerUUID, island) + */ + @Deprecated(since = "2.3.0", forRemoval = true) + public void calculateIslandLevel(World world, @Nullable User user, @NonNull UUID playerUUID) { + Island island = getIslands().getIsland(world, playerUUID); + if (island != null) + getManager().calculateLevel(playerUUID, island); + } + + /** + * Provide the levels data for the target player + * + * @param targetPlayer - UUID of target player + * @return LevelsData object or null if not found. Only island levels are set! + * @deprecated Do not use this anymore. Use {@link #getIslandLevel(World, UUID)} + */ + @Deprecated(since = "2.3.0", forRemoval = true) + public LevelsData getLevelsData(UUID targetPlayer) { + LevelsData ld = new LevelsData(targetPlayer); + getPlugin().getAddonsManager().getGameModeAddons().stream() + .filter(gm -> !settings.getGameModes().contains(gm.getDescription().getName())).forEach(gm -> { + if (getSettings().isZeroNewIslandLevels()) { + Island island = getIslands().getIsland(gm.getOverWorld(), targetPlayer); + if (island != null) { + ld.setInitialLevel(gm.getOverWorld(), this.getInitialIslandLevel(island)); + } + } + ld.setLevel(gm.getOverWorld(), this.getIslandLevel(gm.getOverWorld(), targetPlayer)); + }); + return ld; + } + + /** + * @return the registeredGameModes + */ + public List getRegisteredGameModes() { + return registeredGameModes; + } + + /** + * Check if Level addon is active in game mode + * + * @param gm Game Mode Addon + * @return true if active, false if not + */ + public boolean isRegisteredGameMode(GameModeAddon gm) { + return registeredGameModes.contains(gm); + } + + /** + * Checks if Level addon is active in world + * + * @param world world + * @return true if active, false if not + */ + public boolean isRegisteredGameModeWorld(World world) { + return registeredGameModes.stream().map(GameModeAddon::getOverWorld).anyMatch(w -> Util.sameWorld(world, w)); + } + + /** + * @return the roseStackersEnabled + */ + public boolean isRoseStackersEnabled() { + return roseStackersEnabled; + } + + /** + * @return the ultimateStackerEnabled + */ + public boolean isUltimateStackerEnabled() { + return ultimateStackerEnabled; + } + + /** + * Method Level#getVisitHook returns the visitHook of this object. + * + * @return {@code Visit} of this object, {@code null} otherwise. + */ + public VisitAddon getVisitHook() { + return this.visitHook; + } + + /** + * Method Level#getWarpHook returns the warpHook of this object. + * + * @return {@code Warp} of this object, {@code null} otherwise. + */ + public Warp getWarpHook() { + return this.warpHook; + } + } diff --git a/src/main/java/world/bentobox/level/LevelsManager.java b/src/main/java/world/bentobox/level/LevelsManager.java index e40ba49..f3532aa 100644 --- a/src/main/java/world/bentobox/level/LevelsManager.java +++ b/src/main/java/world/bentobox/level/LevelsManager.java @@ -2,6 +2,7 @@ import java.math.BigInteger; import java.text.DecimalFormat; +import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -31,18 +32,17 @@ import world.bentobox.level.objects.LevelsData; import world.bentobox.level.objects.TopTenData; - public class LevelsManager { private static final String INTOPTEN = "intopten"; private static final TreeMap LEVELS; private static final BigInteger THOUSAND = BigInteger.valueOf(1000); static { - LEVELS = new TreeMap<>(); + LEVELS = new TreeMap<>(); - LEVELS.put(THOUSAND, "k"); - LEVELS.put(THOUSAND.pow(2), "M"); - LEVELS.put(THOUSAND.pow(3), "G"); - LEVELS.put(THOUSAND.pow(4), "T"); + LEVELS.put(THOUSAND, "k"); + LEVELS.put(THOUSAND.pow(2), "M"); + LEVELS.put(THOUSAND.pow(3), "G"); + LEVELS.put(THOUSAND.pow(4), "T"); } private final Level addon; @@ -51,397 +51,445 @@ public class LevelsManager { // A cache of island levels. private final Map levelsCache; // Top ten lists - private final Map topTenLists; - + private final Map topTenLists; public LevelsManager(Level addon) { - this.addon = addon; - // Get the BentoBox database - // Set up the database handler to store and retrieve data - // Note that these are saved by the BentoBox database - handler = new Database<>(addon, IslandLevels.class); - // Initialize the cache - levelsCache = new HashMap<>(); - // Initialize top ten lists - topTenLists = new ConcurrentHashMap<>(); + this.addon = addon; + // Get the BentoBox database + // Set up the database handler to store and retrieve data + // Note that these are saved by the BentoBox database + handler = new Database<>(addon, IslandLevels.class); + // Initialize the cache + levelsCache = new HashMap<>(); + // Initialize top ten lists + topTenLists = new ConcurrentHashMap<>(); } public void migrate() { - Database oldDb = new Database<>(addon, LevelsData.class); - oldDb.loadObjects().forEach(ld -> { - try { - UUID owner = UUID.fromString(ld.getUniqueId()); - // Step through each world - ld.getLevels().keySet().stream() - // World - .map(Bukkit::getWorld).filter(Objects::nonNull) - // Island - .map(w -> addon.getIslands().getIsland(w, owner)).filter(Objects::nonNull) - .forEach(i -> { - // Make new database entry - World w = i.getWorld(); - IslandLevels il = new IslandLevels(i.getUniqueId()); - il.setInitialLevel(ld.getInitialLevel(w)); - il.setLevel(ld.getLevel(w)); - il.setMdCount(ld.getMdCount(w)); - il.setPointsToNextLevel(ld.getPointsToNextLevel(w)); - il.setUwCount(ld.getUwCount(w)); - // Save it - handler.saveObjectAsync(il); - }); - // Now delete the old database entry - oldDb.deleteID(ld.getUniqueId()); - } catch (Exception e) { - addon.logError("Could not migrate level data database! " + e.getMessage()); - e.printStackTrace(); - return; - } - }); - } - - /** - * Add a score to the top players list - * @param world - world - * @param targetPlayer - target player - * @param lv - island level - */ - private void addToTopTen(@NonNull World world, @NonNull UUID targetPlayer, long lv) { - // Get top ten - Map topTen = topTenLists.computeIfAbsent(world, k -> new TopTenData(world)).getTopTen(); - // Remove this player from the top list no matter what (we'll put them back later if required) - topTen.remove(targetPlayer); - - // Get the island - Island island = addon.getIslands().getIsland(world, targetPlayer); - if (island != null && island.getOwner() != null && hasTopTenPerm(world, island.getOwner())) { - // Insert the owner into the top ten - topTen.put(island.getOwner(), lv); - } + Database oldDb = new Database<>(addon, LevelsData.class); + oldDb.loadObjects().forEach(ld -> { + try { + UUID owner = UUID.fromString(ld.getUniqueId()); + // Step through each world + ld.getLevels().keySet().stream() + // World + .map(Bukkit::getWorld).filter(Objects::nonNull) + // Island + .map(w -> addon.getIslands().getIsland(w, owner)).filter(Objects::nonNull).forEach(i -> { + // Make new database entry + World w = i.getWorld(); + IslandLevels il = new IslandLevels(i.getUniqueId()); + il.setInitialLevel(ld.getInitialLevel(w)); + il.setLevel(ld.getLevel(w)); + il.setMdCount(ld.getMdCount(w)); + il.setPointsToNextLevel(ld.getPointsToNextLevel(w)); + il.setUwCount(ld.getUwCount(w)); + // Save it + handler.saveObjectAsync(il); + }); + // Now delete the old database entry + oldDb.deleteID(ld.getUniqueId()); + } catch (Exception e) { + addon.logError("Could not migrate level data database! " + e.getMessage()); + e.printStackTrace(); + return; + } + }); } /** * Add an island to a top ten + * * @param island - island to add - * @param lv - level + * @param lv - level * @return true if successful, false if not added */ private boolean addToTopTen(Island island, long lv) { - if (island != null && island.getOwner() != null && hasTopTenPerm(island.getWorld(), island.getOwner())) { - topTenLists.computeIfAbsent(island.getWorld(), k -> new TopTenData(island.getWorld())) - .getTopTen().put(island.getOwner(), lv); - return true; - } - return false; + if (island != null && island.getOwner() != null && hasTopTenPerm(island.getWorld(), island.getOwner())) { + topTenLists.computeIfAbsent(island.getWorld(), k -> new TopTenData(island.getWorld())).getTopTen() + .put(island.getUniqueId(), lv); + return true; + } + return false; } /** - * Calculate the island level, set all island member's levels to the result and try to add the owner to the top ten + * Calculate the island level, set all island member's levels to the result and + * try to add the owner to the top ten + * * @param targetPlayer - uuid of targeted player - owner or team member - * @param island - island to calculate + * @param island - island to calculate * @return completable future with the results of the calculation */ public CompletableFuture calculateLevel(UUID targetPlayer, Island island) { - CompletableFuture result = new CompletableFuture<>(); - // Fire pre-level calc event - IslandPreLevelEvent e = new IslandPreLevelEvent(targetPlayer, island); - Bukkit.getPluginManager().callEvent(e); - if (e.isCancelled()) { - return CompletableFuture.completedFuture(null); - } - // Add island to the pipeline - addon.getPipeliner().addIsland(island).thenAccept(r -> { - // Results are irrelevant because the island is unowned or deleted, or IslandLevelCalcEvent is cancelled - if (r == null || fireIslandLevelCalcEvent(targetPlayer, island, r)) { - result.complete(null); - } - // Save result - setIslandResults(island.getWorld(), island.getOwner(), r); - // Save the island scan details - result.complete(r); - }); - return result; + CompletableFuture result = new CompletableFuture<>(); + // Fire pre-level calc event + IslandPreLevelEvent e = new IslandPreLevelEvent(targetPlayer, island); + Bukkit.getPluginManager().callEvent(e); + if (e.isCancelled()) { + return CompletableFuture.completedFuture(null); + } + // Add island to the pipeline + addon.getPipeliner().addIsland(island).thenAccept(r -> { + // Results are irrelevant because the island is unowned or deleted, or + // IslandLevelCalcEvent is cancelled + if (r == null || fireIslandLevelCalcEvent(targetPlayer, island, r)) { + result.complete(null); + } + // Save result + setIslandResults(island, r); + // Save the island scan details + result.complete(r); + }); + return result; } /** * Fires the IslandLevelCalculatedEvent and returns true if it is canceled + * * @param targetPlayer - target player - * @param island - island - * @param results - results set + * @param island - island + * @param results - results set * @return true if canceled */ private boolean fireIslandLevelCalcEvent(UUID targetPlayer, Island island, Results results) { - // Fire post calculation event - IslandLevelCalculatedEvent ilce = new IslandLevelCalculatedEvent(targetPlayer, island, results); - Bukkit.getPluginManager().callEvent(ilce); - if (ilce.isCancelled()) return true; - // Set the values if they were altered - results.setLevel((Long)ilce.getKeyValues().getOrDefault("level", results.getLevel())); - results.setInitialLevel((Long)ilce.getKeyValues().getOrDefault("initialLevel", results.getInitialLevel())); - results.setDeathHandicap((int)ilce.getKeyValues().getOrDefault("deathHandicap", results.getDeathHandicap())); - results.setPointsToNextLevel((Long)ilce.getKeyValues().getOrDefault("pointsToNextLevel", results.getPointsToNextLevel())); - results.setTotalPoints((Long)ilce.getKeyValues().getOrDefault("totalPoints", results.getTotalPoints())); - return ((Boolean)ilce.getKeyValues().getOrDefault("isCancelled", false)); + // Fire post calculation event + IslandLevelCalculatedEvent ilce = new IslandLevelCalculatedEvent(targetPlayer, island, results); + Bukkit.getPluginManager().callEvent(ilce); + if (ilce.isCancelled()) + return true; + // Set the values if they were altered + results.setLevel((Long) ilce.getKeyValues().getOrDefault("level", results.getLevel())); + results.setInitialLevel((Long) ilce.getKeyValues().getOrDefault("initialLevel", results.getInitialLevel())); + results.setDeathHandicap((int) ilce.getKeyValues().getOrDefault("deathHandicap", results.getDeathHandicap())); + results.setPointsToNextLevel( + (Long) ilce.getKeyValues().getOrDefault("pointsToNextLevel", results.getPointsToNextLevel())); + results.setTotalPoints((Long) ilce.getKeyValues().getOrDefault("totalPoints", results.getTotalPoints())); + return ((Boolean) ilce.getKeyValues().getOrDefault("isCancelled", false)); } /** - * Get the string representation of the level. May be converted to shorthand notation, e.g., 104556 = 10.5k + * Get the string representation of the level. May be converted to shorthand + * notation, e.g., 104556 = 10.5k + * * @param lvl - long value to represent * @return string of the level. */ public String formatLevel(@Nullable Long lvl) { - if (lvl == null) return ""; - String level = String.valueOf(lvl); - // Asking for the level of another player - if(addon.getSettings().isShorthand()) { - BigInteger levelValue = BigInteger.valueOf(lvl); - - Map.Entry stage = LEVELS.floorEntry(levelValue); - - if (stage != null) { // level > 1000 - // 1 052 -> 1.0k - // 1 527 314 -> 1.5M - // 3 874 130 021 -> 3.8G - // 4 002 317 889 -> 4.0T - level = new DecimalFormat("#.#").format(levelValue.divide(stage.getKey().divide(THOUSAND)).doubleValue()/1000.0) + stage.getValue(); - } - } - return level; + if (lvl == null) + return ""; + String level = String.valueOf(lvl); + // Asking for the level of another player + if (addon.getSettings().isShorthand()) { + BigInteger levelValue = BigInteger.valueOf(lvl); + + Map.Entry stage = LEVELS.floorEntry(levelValue); + + if (stage != null) { // level > 1000 + // 1 052 -> 1.0k + // 1 527 314 -> 1.5M + // 3 874 130 021 -> 3.8G + // 4 002 317 889 -> 4.0T + level = new DecimalFormat("#.#").format( + levelValue.divide(stage.getKey().divide(THOUSAND)).doubleValue() / 1000.0) + stage.getValue(); + } + } + return level; } /** * Get the initial level of the island. Used to zero island levels + * * @param island - island * @return initial level of island */ public long getInitialLevel(Island island) { - return getLevelsData(island).getInitialLevel(); + return getLevelsData(island).getInitialLevel(); } /** * Get level of island from cache for a player. - * @param world - world where the island is + * + * @param world - world where the island is * @param targetPlayer - target player UUID - * @return Level of the player's island or zero if player is unknown or UUID is null + * @return Level of the player's island or zero if player is unknown or UUID is + * null */ public long getIslandLevel(@NonNull World world, @Nullable UUID targetPlayer) { - if (targetPlayer == null) return 0L; - // Get the island - Island island = addon.getIslands().getIsland(world, targetPlayer); - return island == null ? 0L : getLevelsData(island).getLevel(); + if (targetPlayer == null) + return 0L; + // Get the island + Island island = addon.getIslands().getIsland(world, targetPlayer); + return island == null ? 0L : getLevelsData(island).getLevel(); } /** * Get the maximum level ever given to this island - * @param world - world where the island is + * + * @param world - world where the island is * @param targetPlayer - target player UUID - * @return Max level of the player's island or zero if player is unknown or UUID is null + * @return Max level of the player's island or zero if player is unknown or UUID + * is null */ public long getIslandMaxLevel(@NonNull World world, @Nullable UUID targetPlayer) { - if (targetPlayer == null) return 0L; - // Get the island - Island island = addon.getIslands().getIsland(world, targetPlayer); - return island == null ? 0L : getLevelsData(island).getMaxLevel(); + if (targetPlayer == null) + return 0L; + // Get the island + Island island = addon.getIslands().getIsland(world, targetPlayer); + return island == null ? 0L : getLevelsData(island).getMaxLevel(); } /** * Returns a formatted string of the target player's island level - * @param world - world where the island is + * + * @param world - world where the island is * @param targetPlayer - target player's UUID - * @return Formatted level of player or zero if player is unknown or UUID is null + * @return Formatted level of player or zero if player is unknown or UUID is + * null */ public String getIslandLevelString(@NonNull World world, @Nullable UUID targetPlayer) { - return formatLevel(getIslandLevel(world, targetPlayer)); + return formatLevel(getIslandLevel(world, targetPlayer)); } /** * Load a level data for the island from the cache or database. + * * @param island - UUID of island * @return IslandLevels object */ @NonNull public IslandLevels getLevelsData(@NonNull Island island) { - String id = island.getUniqueId(); - if (levelsCache.containsKey(id)) { - return levelsCache.get(id); - } - // Get from database if not in cache - if (handler.objectExists(id)) { - IslandLevels ld = handler.loadObject(id); - if (ld != null) { - levelsCache.put(id, ld); - } else { - handler.deleteID(id); - levelsCache.put(id, new IslandLevels(id)); - } - } else { - levelsCache.put(id, new IslandLevels(id)); - } - // Return cached value - return levelsCache.get(id); + String id = island.getUniqueId(); + if (levelsCache.containsKey(id)) { + return levelsCache.get(id); + } + // Get from database if not in cache + if (handler.objectExists(id)) { + IslandLevels ld = handler.loadObject(id); + if (ld != null) { + levelsCache.put(id, ld); + } else { + handler.deleteID(id); + levelsCache.put(id, new IslandLevels(id)); + } + } else { + levelsCache.put(id, new IslandLevels(id)); + } + // Return cached value + return levelsCache.get(id); } /** - * Get the number of points required until the next level since the last level calc - * @param world - world where the island is + * Get the number of points required until the next level since the last level + * calc + * + * @param world - world where the island is * @param targetPlayer - target player UUID * @return string with the number required or blank if the player is unknown */ public String getPointsToNextString(@NonNull World world, @Nullable UUID targetPlayer) { - if (targetPlayer == null) return ""; - Island island = addon.getIslands().getIsland(world, targetPlayer); - return island == null ? "" : String.valueOf(getLevelsData(island).getPointsToNextLevel()); + if (targetPlayer == null) + return ""; + Island island = addon.getIslands().getIsland(world, targetPlayer); + return island == null ? "" : String.valueOf(getLevelsData(island).getPointsToNextLevel()); } /** - * Get the top ten for this world. Returns offline players or players with the intopten permission. + * Get the weighted top ten for this world. Weighting is based on number of + * players per team. + * * @param world - world requested - * @param size - size of the top ten - * @return sorted top ten map + * @param size - size of the top ten + * @return sorted top ten map. The key is the island unique ID */ @NonNull - public Map getTopTen(@NonNull World world, int size) { - createAndCleanRankings(world); - // Return the sorted map - return Collections.unmodifiableMap(topTenLists.get(world).getTopTen().entrySet().stream() - .filter(e -> addon.getIslands().isOwner(world, e.getKey())) - .filter(l -> l.getValue() > 0) - .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())).limit(size) - .collect(Collectors.toMap( - Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new))); + public Map getWeightedTopTen(@NonNull World world, int size) { + createAndCleanRankings(world); + Map weightedTopTen = topTenLists.get(world).getTopTen().entrySet().stream() + .map(en -> addon.getIslands().getIslandById(en.getKey()).map(island -> { + + long value = (long) (en.getValue() / (double) Math.max(1, island.getMemberSet().size())); // Calculate + // weighted + // value + return new AbstractMap.SimpleEntry<>(island, value); + }).orElse(null)) // Handle islands that do not exist according to this ID - old deleted ones + .filter(Objects::nonNull) // Filter out null entries + .filter(en -> en.getValue() > 0) // Filter out entries with non-positive values + .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) // Sort in descending order of values + .limit(size) // Limit to the top 'size' entries + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, // In case of key + // collision, choose + // the first one + LinkedHashMap::new // Preserves the order of entries + )); + + // Return the unmodifiable map + return Collections.unmodifiableMap(weightedTopTen); + + } + + /** + * Get the top ten for this world. Returns offline players or players with the + * intopten permission. + * + * @param world - world requested + * @param size - size of the top ten + * @return sorted top ten map. The key is the island unique ID + */ + @NonNull + public Map getTopTen(@NonNull World world, int size) { + createAndCleanRankings(world); + // Return the sorted map + return Collections.unmodifiableMap(topTenLists.get(world).getTopTen().entrySet().stream() + .filter(l -> l.getValue() > 0).sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) + .limit(size) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new))); } void createAndCleanRankings(@NonNull World world) { - topTenLists.computeIfAbsent(world, TopTenData::new); - // Remove player from top ten if they are online and do not have the perm - topTenLists.get(world).getTopTen().keySet().removeIf(u -> !hasTopTenPerm(world, u)); + topTenLists.computeIfAbsent(world, TopTenData::new); + // Remove player from top ten if they are online and do not have the perm + topTenLists.get(world).getTopTen().keySet().removeIf(u -> addon.getIslands().getIslandById(u) + .filter(i -> i.getOwner() == null || !hasTopTenPerm(world, i.getOwner())).isPresent()); } /** * @return the topTenLists */ - protected Map getTopTenLists() { - return topTenLists; + public Map getTopTenLists() { + return topTenLists; } /** * Get the rank of the player in the rankings + * * @param world - world - * @param uuid - player UUID + * @param uuid - player UUID * @return rank placing - note - placing of 1 means top ranked */ public int getRank(@NonNull World world, UUID uuid) { - createAndCleanRankings(world); - Stream> stream = topTenLists.get(world).getTopTen().entrySet().stream() - .filter(e -> addon.getIslands().isOwner(world, e.getKey())) - .filter(l -> l.getValue() > 0) - .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())); - return (int) (stream.takeWhile(x -> !x.getKey().equals(uuid)).map(Map.Entry::getKey).count() + 1); + createAndCleanRankings(world); + Stream> stream = topTenLists.get(world).getTopTen().entrySet().stream() + .filter(l -> l.getValue() > 0).sorted(Collections.reverseOrder(Map.Entry.comparingByValue())); + // Get player's current island + Island island = addon.getIslands().getIsland(world, uuid); + String id = island == null ? null : island.getUniqueId(); + return (int) (stream.takeWhile(x -> !x.getKey().equals(id)).map(Map.Entry::getKey).count() + 1); } /** * Checks if player has the correct top ten perm to have their level saved + * * @param world * @param targetPlayer * @return true if player has the perm or the player is offline */ boolean hasTopTenPerm(@NonNull World world, @NonNull UUID targetPlayer) { - String permPrefix = addon.getPlugin().getIWM().getPermissionPrefix(world); - return Bukkit.getPlayer(targetPlayer) == null || Bukkit.getPlayer(targetPlayer).hasPermission(permPrefix + INTOPTEN); + String permPrefix = addon.getPlugin().getIWM().getPermissionPrefix(world); + return Bukkit.getPlayer(targetPlayer) == null + || Bukkit.getPlayer(targetPlayer).hasPermission(permPrefix + INTOPTEN); } /** * Loads all the top tens from the database */ public void loadTopTens() { - topTenLists.clear(); - Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> { - addon.log("Generating rankings"); - handler.loadObjects().forEach(il -> { - if (il.getLevel() > 0) { - addon.getIslands().getIslandById(il.getUniqueId()).ifPresent(i -> this.addToTopTen(i, il.getLevel())); - } - }); - topTenLists.keySet().forEach(w -> addon.log("Generated rankings for " + w.getName())); - }); + topTenLists.clear(); + Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> { + addon.log("Generating rankings"); + handler.loadObjects().forEach(il -> { + if (il.getLevel() > 0) { + addon.getIslands().getIslandById(il.getUniqueId()) + .ifPresent(i -> this.addToTopTen(i, il.getLevel())); + } + }); + topTenLists.keySet().forEach(w -> addon.log("Generated rankings for " + w.getName())); + }); } /** - * Removes a player from a world's top ten and removes world from player's level data + * Removes an island from a world's top ten + * * @param world - world - * @param uuid - the player's uuid + * @param uuid - the island's uuid */ - public void removeEntry(World world, UUID uuid) { - if (topTenLists.containsKey(world)) { - topTenLists.get(world).getTopTen().remove(uuid); - } + public void removeEntry(World world, String uuid) { + if (topTenLists.containsKey(world)) { + topTenLists.get(world).getTopTen().remove(uuid); + } } /** * Set an initial island level + * * @param island - the island to set. Must have a non-null world - * @param lv - initial island level + * @param lv - initial island level */ public void setInitialIslandLevel(@NonNull Island island, long lv) { - if (island.getWorld() == null) return; - levelsCache.computeIfAbsent(island.getUniqueId(), IslandLevels::new).setInitialLevel(lv); - handler.saveObjectAsync(levelsCache.get(island.getUniqueId())); + if (island.getWorld() == null) + return; + levelsCache.computeIfAbsent(island.getUniqueId(), IslandLevels::new).setInitialLevel(lv); + handler.saveObjectAsync(levelsCache.get(island.getUniqueId())); } /** - * Set the island level for the owner of the island that targetPlayer is a member - * @param world - world - * @param targetPlayer - player, may be a team member - * @param lv - level + * Set the island level for the owner of the island that targetPlayer is a + * member + * + * @param world - world + * @param island - island + * @param lv - level */ public void setIslandLevel(@NonNull World world, @NonNull UUID targetPlayer, long lv) { - // Get the island - Island island = addon.getIslands().getIsland(world, targetPlayer); - if (island != null) { - String id = island.getUniqueId(); - IslandLevels il = levelsCache.computeIfAbsent(id, IslandLevels::new); - // Remove the initial level - if (addon.getSettings().isZeroNewIslandLevels()) { - il.setLevel(lv - il.getInitialLevel()); - } else { - il.setLevel(lv); - } - handler.saveObjectAsync(levelsCache.get(id)); - // Update TopTen - addToTopTen(world, targetPlayer, levelsCache.get(id).getLevel()); - } - + // Get the island + Island island = addon.getIslands().getIsland(world, targetPlayer); + if (island != null) { + String id = island.getUniqueId(); + IslandLevels il = levelsCache.computeIfAbsent(id, IslandLevels::new); + // Remove the initial level + if (addon.getSettings().isZeroNewIslandLevels()) { + il.setLevel(lv - il.getInitialLevel()); + } else { + il.setLevel(lv); + } + handler.saveObjectAsync(levelsCache.get(id)); + // Update TopTen + addToTopTen(island, levelsCache.get(id).getLevel()); + } } /** - * Set the island level for the owner of the island that targetPlayer is a member + * Set the island level for the owner of the island that targetPlayer is a + * member + * * @param world - world * @param owner - owner of the island - * @param r - results of the calculation + * @param r - results of the calculation */ - private void setIslandResults(World world, @NonNull UUID owner, Results r) { - // Get the island - Island island = addon.getIslands().getIsland(world, owner); - if (island == null) return; - IslandLevels ld = levelsCache.computeIfAbsent(island.getUniqueId(), IslandLevels::new); - ld.setLevel(r.getLevel()); - ld.setUwCount(Maps.asMap(r.getUwCount().elementSet(), elem -> r.getUwCount().count(elem))); - ld.setMdCount(Maps.asMap(r.getMdCount().elementSet(), elem -> r.getMdCount().count(elem))); - ld.setPointsToNextLevel(r.getPointsToNextLevel()); - ld.setTotalPoints(r.getTotalPoints()); - levelsCache.put(island.getUniqueId(), ld); - handler.saveObjectAsync(ld); - // Update TopTen - addToTopTen(world, owner, ld.getLevel()); + private void setIslandResults(Island island, Results r) { + if (island == null) + return; + IslandLevels ld = levelsCache.computeIfAbsent(island.getUniqueId(), IslandLevels::new); + ld.setLevel(r.getLevel()); + ld.setUwCount(Maps.asMap(r.getUwCount().elementSet(), elem -> r.getUwCount().count(elem))); + ld.setMdCount(Maps.asMap(r.getMdCount().elementSet(), elem -> r.getMdCount().count(elem))); + ld.setPointsToNextLevel(r.getPointsToNextLevel()); + ld.setTotalPoints(r.getTotalPoints()); + levelsCache.put(island.getUniqueId(), ld); + handler.saveObjectAsync(ld); + // Update TopTen + addToTopTen(island, ld.getLevel()); } /** * Removes island from cache when it is deleted + * * @param uniqueId - id of island */ public void deleteIsland(String uniqueId) { - levelsCache.remove(uniqueId); - handler.deleteID(uniqueId); + levelsCache.remove(uniqueId); + handler.deleteID(uniqueId); } } diff --git a/src/main/java/world/bentobox/level/PlaceholderManager.java b/src/main/java/world/bentobox/level/PlaceholderManager.java index 5e02a2c..5f0bd17 100644 --- a/src/main/java/world/bentobox/level/PlaceholderManager.java +++ b/src/main/java/world/bentobox/level/PlaceholderManager.java @@ -2,10 +2,12 @@ import java.util.Collections; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; import org.bukkit.World; +import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; @@ -18,6 +20,7 @@ /** * Handles Level placeholders + * * @author tastybento * */ @@ -27,150 +30,186 @@ public class PlaceholderManager { private final BentoBox plugin; public PlaceholderManager(Level addon) { - this.addon = addon; - this.plugin = addon.getPlugin(); + this.addon = addon; + this.plugin = addon.getPlugin(); } - + protected void registerPlaceholders(GameModeAddon gm) { - if (plugin.getPlaceholdersManager() == null) return; - PlaceholdersManager bpm = plugin.getPlaceholdersManager(); - // Island Level - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_island_level", - user -> addon.getManager().getIslandLevelString(gm.getOverWorld(), user.getUniqueId())); - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_island_level_raw", - user -> String.valueOf(addon.getManager().getIslandLevel(gm.getOverWorld(), user.getUniqueId()))); - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_island_total_points", - user -> { - IslandLevels data = addon.getManager().getLevelsData(addon.getIslands().getIsland(gm.getOverWorld(), user)); - return data.getTotalPoints()+""; - }); - - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_points_to_next_level", - user -> addon.getManager().getPointsToNextString(gm.getOverWorld(), user.getUniqueId())); - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_island_level_max", - user -> String.valueOf(addon.getManager().getIslandMaxLevel(gm.getOverWorld(), user.getUniqueId()))); - - // Visited Island Level - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_visited_island_level", user -> getVisitedIslandLevel(gm, user)); - - // Register Top Ten Placeholders - for (int i = 1; i < 11; i++) { - final int rank = i; - // Name - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_top_name_" + i, u -> getRankName(gm.getOverWorld(), rank)); - // Island Name - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_top_island_name_" + i, u -> getRankIslandName(gm.getOverWorld(), rank)); - // Members - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_top_members_" + i, u -> getRankMembers(gm.getOverWorld(), rank)); - // Level - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_top_value_" + i, u -> getRankLevel(gm.getOverWorld(), rank)); - } - - // Personal rank - bpm.registerPlaceholder(addon, - gm.getDescription().getName().toLowerCase() + "_rank_value", u -> getRankValue(gm.getOverWorld(), u)); + if (plugin.getPlaceholdersManager() == null) + return; + PlaceholdersManager bpm = plugin.getPlaceholdersManager(); + // Island Level + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_island_level", + user -> addon.getManager().getIslandLevelString(gm.getOverWorld(), user.getUniqueId())); + // Unformatted island level + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_island_level_raw", + user -> String.valueOf(addon.getManager().getIslandLevel(gm.getOverWorld(), user.getUniqueId()))); + // Total number of points counted before applying level formula + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_island_total_points", user -> { + IslandLevels data = addon.getManager().getLevelsData(addon.getIslands().getIsland(gm.getOverWorld(), user)); + return data.getTotalPoints() + ""; + }); + // Points to the next level for player + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_points_to_next_level", + user -> addon.getManager().getPointsToNextString(gm.getOverWorld(), user.getUniqueId())); + // Maximum level this island has ever been. Current level maybe lower. + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_island_level_max", + user -> String.valueOf(addon.getManager().getIslandMaxLevel(gm.getOverWorld(), user.getUniqueId()))); + + // Visited Island Level + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_visited_island_level", + user -> getVisitedIslandLevel(gm, user)); + + // Register Top Ten Placeholders + for (int i = 1; i < 11; i++) { + final int rank = i; + // Name + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_top_name_" + i, + u -> getRankName(gm.getOverWorld(), rank, false)); + // Island Name + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_top_island_name_" + i, + u -> getRankIslandName(gm.getOverWorld(), rank, false)); + // Members + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_top_members_" + i, + u -> getRankMembers(gm.getOverWorld(), rank, false)); + // Level + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_top_value_" + i, + u -> getRankLevel(gm.getOverWorld(), rank, false)); + // Weighted Level Name (Level / number of members) + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_top_weighted_name_" + i, + u -> getRankName(gm.getOverWorld(), rank, true)); + // Weighted Island Name + bpm.registerPlaceholder(addon, + gm.getDescription().getName().toLowerCase() + "_top_weighted_island_name_" + i, + u -> getRankIslandName(gm.getOverWorld(), rank, true)); + // Weighted Members + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_top_weighted_members_" + i, + u -> getRankMembers(gm.getOverWorld(), rank, true)); + // Weighted Level (Level / number of members) + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_top_weighted_value_" + i, + u -> getRankLevel(gm.getOverWorld(), rank, true)); + } + + // Personal rank + bpm.registerPlaceholder(addon, gm.getDescription().getName().toLowerCase() + "_rank_value", + u -> getRankValue(gm.getOverWorld(), u)); } /** - * Get the name of the player who holds the rank in this world - * @param world world - * @param rank rank 1 to 10 + * Get the name of the owner of the island who holds the rank in this world. + * + * @param world world + * @param rank rank 1 to 10 + * @param weighted if true, then the weighted rank name is returned * @return rank name */ - String getRankName(World world, int rank) { - if (rank < 1) rank = 1; - if (rank > Level.TEN) rank = Level.TEN; - return addon.getPlayers().getName(addon.getManager().getTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L).limit(1L).findFirst().orElse(null)); + String getRankName(World world, int rank, boolean weighted) { + // Ensure rank is within bounds + rank = Math.max(1, Math.min(rank, Level.TEN)); + if (weighted) { + return addon.getManager().getWeightedTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L).limit(1L) + .findFirst().map(Island::getOwner).map(addon.getPlayers()::getName).orElse(""); + } + @Nullable + UUID owner = addon.getManager().getTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L).limit(1L) + .findFirst().flatMap(addon.getIslands()::getIslandById).map(Island::getOwner).orElse(null); + + return addon.getPlayers().getName(owner); } /** * Get the island name for this rank - * @param world world - * @param rank rank 1 to 10 + * + * @param world world + * @param rank rank 1 to 10 + * @param weighted if true, then the weighted rank name is returned * @return name of island or nothing if there isn't one */ - String getRankIslandName(World world, int rank) { - if (rank < 1) rank = 1; - if (rank > Level.TEN) rank = Level.TEN; - UUID owner = addon.getManager().getTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L).limit(1L).findFirst().orElse(null); - if (owner != null) { - Island island = addon.getIslands().getIsland(world, owner); - if (island != null) { - return island.getName() == null ? "" : island.getName(); - } - } - return ""; + String getRankIslandName(World world, int rank, boolean weighted) { + // Ensure rank is within bounds + rank = Math.max(1, Math.min(rank, Level.TEN)); + if (weighted) { + return addon.getManager().getWeightedTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L).limit(1L) + .findFirst().map(Island::getName).orElse(""); + } + return addon.getManager().getTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L).limit(1L).findFirst() + .flatMap(addon.getIslands()::getIslandById).map(Island::getName).orElse(""); } /** * Gets a comma separated string of island member names - * @param world world - * @param rank rank to request + * + * @param world world + * @param rank rank to request + * @param weighted if true, then the weighted rank name is returned * @return comma separated string of island member names */ - String getRankMembers(World world, int rank) { - if (rank < 1) rank = 1; - if (rank > Level.TEN) rank = Level.TEN; - UUID owner = addon.getManager().getTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L).limit(1L).findFirst().orElse(null); - if (owner != null) { - Island island = addon.getIslands().getIsland(world, owner); - if (island != null) { - // Sort members by rank - return island.getMembers().entrySet().stream() - .filter(e -> e.getValue() >= RanksManager.MEMBER_RANK) - .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) - .map(Map.Entry::getKey) - .map(addon.getPlayers()::getName) - .collect(Collectors.joining(",")); - } - } - return ""; + String getRankMembers(World world, int rank, boolean weighted) { + // Ensure rank is within bounds + rank = Math.max(1, Math.min(rank, Level.TEN)); + if (weighted) { + return addon.getManager().getWeightedTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L).limit(1L) + .findFirst() + .map(is -> is.getMembers().entrySet().stream().filter(e -> e.getValue() >= RanksManager.MEMBER_RANK) + .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())).map(Map.Entry::getKey) + .map(addon.getPlayers()::getName).collect(Collectors.joining(","))) + .orElse(""); + } + + Optional island = addon.getManager().getTopTen(world, Level.TEN).keySet().stream().skip(rank - 1L) + .limit(1L).findFirst().flatMap(addon.getIslands()::getIslandById); + + if (island.isPresent()) { + // Sort members by rank + return island.get().getMembers().entrySet().stream().filter(e -> e.getValue() >= RanksManager.MEMBER_RANK) + .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())).map(Map.Entry::getKey) + .map(addon.getPlayers()::getName).collect(Collectors.joining(",")); + } + return ""; } - String getRankLevel(World world, int rank) { - if (rank < 1) rank = 1; - if (rank > Level.TEN) rank = Level.TEN; - return addon.getManager() - .formatLevel(addon.getManager() - .getTopTen(world, Level.TEN) - .values() - .stream() - .skip(rank - 1L) - .limit(1L) - .findFirst() - .orElse(null)); + /** + * Get the level for the rank requested + * + * @param world world + * @param rank rank wanted + * @param weighted true if weighted (level/number of team members) + * @return level for the rank requested + */ + String getRankLevel(World world, int rank, boolean weighted) { + // Ensure rank is within bounds + rank = Math.max(1, Math.min(rank, Level.TEN)); + if (weighted) { + return addon.getManager().formatLevel(addon.getManager().getWeightedTopTen(world, Level.TEN).values() + .stream().skip(rank - 1L).limit(1L).findFirst().orElse(null)); + } + return addon.getManager().formatLevel(addon.getManager().getTopTen(world, Level.TEN).values().stream() + .skip(rank - 1L).limit(1L).findFirst().orElse(null)); } - + /** * Return the rank of the player in a world + * * @param world world - * @param user player + * @param user player * @return rank where 1 is the top rank. */ private String getRankValue(World world, User user) { - if (user == null) { - return ""; - } - // Get the island level for this user - long level = addon.getManager().getIslandLevel(world, user.getUniqueId()); - return String.valueOf(addon.getManager().getTopTenLists().getOrDefault(world, new TopTenData(world)).getTopTen().values().stream().filter(l -> l > level).count() + 1); + if (user == null) { + return ""; + } + // Get the island level for this user + long level = addon.getManager().getIslandLevel(world, user.getUniqueId()); + return String.valueOf(addon.getManager().getTopTenLists().getOrDefault(world, new TopTenData(world)).getTopTen() + .values().stream().filter(l -> l > level).count() + 1); } String getVisitedIslandLevel(GameModeAddon gm, User user) { - if (user == null || !gm.inWorld(user.getWorld())) return ""; - return addon.getIslands().getIslandAt(user.getLocation()) - .map(island -> addon.getManager().getIslandLevelString(gm.getOverWorld(), island.getOwner())) - .orElse("0"); + if (user == null || !gm.inWorld(user.getWorld())) + return ""; + return addon.getIslands().getIslandAt(user.getLocation()) + .map(island -> addon.getManager().getIslandLevelString(gm.getOverWorld(), island.getOwner())) + .orElse("0"); } } diff --git a/src/main/java/world/bentobox/level/calculators/EquationEvaluator.java b/src/main/java/world/bentobox/level/calculators/EquationEvaluator.java new file mode 100644 index 0000000..3014542 --- /dev/null +++ b/src/main/java/world/bentobox/level/calculators/EquationEvaluator.java @@ -0,0 +1,121 @@ +package world.bentobox.level.calculators; + +import java.text.ParseException; + +/** + * Utility class to evaluate equations + */ +public class EquationEvaluator { + + private static class Parser { + private final String input; + private int pos = -1; + private int currentChar; + + @SuppressWarnings("unused") + private Parser() { + throw new IllegalStateException("Utility class"); + } + + public Parser(String input) { + this.input = input; + moveToNextChar(); + } + + private void moveToNextChar() { + currentChar = (++pos < input.length()) ? input.charAt(pos) : -1; + } + + private boolean tryToEat(int charToEat) { + while (currentChar == ' ') { + moveToNextChar(); + } + if (currentChar == charToEat) { + moveToNextChar(); + return true; + } + return false; + } + + public double evaluate() throws ParseException { + double result = parseExpression(); + if (pos < input.length()) { + throw new ParseException("Unexpected character: " + (char) currentChar, pos); + } + return result; + } + + private double parseExpression() throws ParseException { + double result = parseTerm(); + while (true) { + if (tryToEat('+')) { + result += parseTerm(); + } else if (tryToEat('-')) { + result -= parseTerm(); + } else { + return result; + } + } + } + + private double parseFactor() throws ParseException { + if (tryToEat('+')) { + return parseFactor(); // unary plus + } + if (tryToEat('-')) { + return -parseFactor(); // unary minus + } + double x; + int startPos = this.pos; + if (tryToEat('(')) { // parentheses + x = parseExpression(); + tryToEat(')'); + } else if ((currentChar >= '0' && currentChar <= '9') || currentChar == '.') { // numbers + while ((currentChar >= '0' && currentChar <= '9') || currentChar == '.') { + moveToNextChar(); + } + x = Double.parseDouble(input.substring(startPos, this.pos)); + } else if (currentChar >= 'a' && currentChar <= 'z') { // functions + while (currentChar >= 'a' && currentChar <= 'z') { + moveToNextChar(); + } + String func = input.substring(startPos, this.pos); + x = parseFactor(); + x = switch (func) { + case "sqrt" -> Math.sqrt(x); + case "sin" -> Math.sin(Math.toRadians(x)); + case "cos" -> Math.cos(Math.toRadians(x)); + case "tan" -> Math.tan(Math.toRadians(x)); + case "log" -> Math.log(x); + default -> throw new ParseException("Unknown function: " + func, startPos); + }; + } else { + throw new ParseException("Unexpected: " + (char) currentChar, startPos); + } + + if (tryToEat('^')) { + x = Math.pow(x, parseFactor()); // exponentiation + } + + return x; + } + + private double parseTerm() throws ParseException { + double x = parseFactor(); + for (;;) { + if (tryToEat('*')) + x *= parseFactor(); // multiplication + else if (tryToEat('/')) + x /= parseFactor(); // division + else + return x; + } + } + + } + + public static double eval(final String equation) throws ParseException { + return new Parser(equation).evaluate(); + } + +} diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index 9c2a6cf..7fc1cca 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -1,6 +1,6 @@ package world.bentobox.level.calculators; -import java.io.IOException; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -16,9 +16,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; -import com.songoda.ultimatestacker.UltimateStacker; -import com.songoda.ultimatestacker.core.compatibility.CompatibleMaterial; -import com.songoda.ultimatestacker.stackable.block.BlockStack; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.ChunkSnapshot; @@ -27,7 +24,11 @@ import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.World.Environment; -import org.bukkit.block.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.Container; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.block.ShulkerBox; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Slab; import org.bukkit.inventory.ItemStack; @@ -39,6 +40,9 @@ import com.google.common.collect.Multiset; import com.google.common.collect.Multiset.Entry; import com.google.common.collect.Multisets; +import com.songoda.ultimatestacker.UltimateStacker; +import com.songoda.ultimatestacker.core.compatibility.CompatibleMaterial; +import com.songoda.ultimatestacker.stackable.block.BlockStack; import dev.rosewood.rosestacker.api.RoseStackerAPI; import us.lynuxcraft.deadsilenceiv.advancedchests.AdvancedChestsAPI; @@ -54,121 +58,21 @@ public class IslandLevelCalculator { private static final String LINE_BREAK = "=================================="; public static final long MAX_AMOUNT = 10000000; - private static final List CHESTS = Arrays.asList(Material.CHEST, Material.CHEST_MINECART, Material.TRAPPED_CHEST, - Material.SHULKER_BOX, Material.BLACK_SHULKER_BOX, Material.BLUE_SHULKER_BOX, Material.BROWN_SHULKER_BOX, - Material.CYAN_SHULKER_BOX, Material.GRAY_SHULKER_BOX, Material.GREEN_SHULKER_BOX, Material.LIGHT_BLUE_SHULKER_BOX, - Material.LIGHT_GRAY_SHULKER_BOX, Material.LIME_SHULKER_BOX, Material.MAGENTA_SHULKER_BOX, Material.ORANGE_SHULKER_BOX, - Material.PINK_SHULKER_BOX, Material.PURPLE_SHULKER_BOX, Material.RED_SHULKER_BOX, Material.RED_SHULKER_BOX, - Material.WHITE_SHULKER_BOX, Material.YELLOW_SHULKER_BOX, Material.COMPOSTER, Material.BARREL, Material.DISPENSER, - Material.DROPPER, Material.SMOKER, Material.BLAST_FURNACE); + private static final List CHESTS = Arrays.asList(Material.CHEST, Material.CHEST_MINECART, + Material.TRAPPED_CHEST, Material.SHULKER_BOX, Material.BLACK_SHULKER_BOX, Material.BLUE_SHULKER_BOX, + Material.BROWN_SHULKER_BOX, Material.CYAN_SHULKER_BOX, Material.GRAY_SHULKER_BOX, + Material.GREEN_SHULKER_BOX, Material.LIGHT_BLUE_SHULKER_BOX, Material.LIGHT_GRAY_SHULKER_BOX, + Material.LIME_SHULKER_BOX, Material.MAGENTA_SHULKER_BOX, Material.ORANGE_SHULKER_BOX, + Material.PINK_SHULKER_BOX, Material.PURPLE_SHULKER_BOX, Material.RED_SHULKER_BOX, Material.RED_SHULKER_BOX, + Material.WHITE_SHULKER_BOX, Material.YELLOW_SHULKER_BOX, Material.COMPOSTER, Material.BARREL, + Material.DISPENSER, Material.DROPPER, Material.SMOKER, Material.BLAST_FURNACE); private static final int CHUNKS_TO_SCAN = 100; - - /** - * Method to evaluate a mathematical equation - * @param str - equation to evaluate - * @return value of equation - */ - private static double eval(final String str) throws IOException { - return new Object() { - int pos = -1; - int ch; - - boolean eat(int charToEat) { - while (ch == ' ') nextChar(); - if (ch == charToEat) { - nextChar(); - return true; - } - return false; - } - - void nextChar() { - ch = (++pos < str.length()) ? str.charAt(pos) : -1; - } - - double parse() throws IOException { - nextChar(); - double x = parseExpression(); - if (pos < str.length()) throw new IOException("Unexpected: " + (char)ch); - return x; - } - - // Grammar: - // expression = term | expression `+` term | expression `-` term - // term = factor | term `*` factor | term `/` factor - // factor = `+` factor | `-` factor | `(` expression `)` - // | number | functionName factor | factor `^` factor - - double parseExpression() throws IOException { - double x = parseTerm(); - for (;;) { - if (eat('+')) x += parseTerm(); // addition - else if (eat('-')) x -= parseTerm(); // subtraction - else return x; - } - } - - double parseFactor() throws IOException { - if (eat('+')) return parseFactor(); // unary plus - if (eat('-')) return -parseFactor(); // unary minus - - double x; - int startPos = this.pos; - if (eat('(')) { // parentheses - x = parseExpression(); - eat(')'); - } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers - while ((ch >= '0' && ch <= '9') || ch == '.') nextChar(); - x = Double.parseDouble(str.substring(startPos, this.pos)); - } else if (ch >= 'a' && ch <= 'z') { // functions - while (ch >= 'a' && ch <= 'z') nextChar(); - String func = str.substring(startPos, this.pos); - x = parseFactor(); - switch (func) { - case "sqrt": - x = Math.sqrt(x); - break; - case "sin": - x = Math.sin(Math.toRadians(x)); - break; - case "cos": - x = Math.cos(Math.toRadians(x)); - break; - case "tan": - x = Math.tan(Math.toRadians(x)); - break; - case "log": - x = Math.log(x); - break; - default: - throw new IOException("Unknown function: " + func); - } - } else { - throw new IOException("Unexpected: " + (char)ch); - } - - if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation - - return x; - } - - double parseTerm() throws IOException { - double x = parseFactor(); - for (;;) { - if (eat('*')) x *= parseFactor(); // multiplication - else if (eat('/')) x /= parseFactor(); // division - else return x; - } - } - }.parse(); - } private final Level addon; private final Queue> chunksToCheck; private final Island island; private final Map limitCount; private final CompletableFuture r; - private final Results results; private long duration; private final boolean zeroIsland; @@ -178,569 +82,600 @@ void nextChar() { private final Set chestBlocks = new HashSet<>(); private BukkitTask finishTask; - /** * Constructor to get the level for an island - * @param addon - Level addon - * @param island - the island to scan - * @param r - completable result that will be completed when the calculation is complete + * + * @param addon - Level addon + * @param island - the island to scan + * @param r - completable result that will be completed when the + * calculation is complete * @param zeroIsland - true if the calculation is due to an island zeroing */ public IslandLevelCalculator(Level addon, Island island, CompletableFuture r, boolean zeroIsland) { - this.addon = addon; - this.island = island; - this.r = r; - this.zeroIsland = zeroIsland; - results = new Results(); - duration = System.currentTimeMillis(); - chunksToCheck = getChunksToScan(island); - this.limitCount = new EnumMap<>(addon.getBlockConfig().getBlockLimits()); - // Get the initial island level - results.initialLevel.set(addon.getInitialIslandLevel(island)); - // Set up the worlds - worlds.put(Environment.NORMAL, Util.getWorld(island.getWorld())); - // Nether - if (addon.getSettings().isNether()) { - World nether = addon.getPlugin().getIWM().getNetherWorld(island.getWorld()); - if (nether != null) { - worlds.put(Environment.NETHER, nether); - } - } - // End - if (addon.getSettings().isEnd()) { - World end = addon.getPlugin().getIWM().getEndWorld(island.getWorld()); - if (end != null) { - worlds.put(Environment.THE_END, end); - } - } - // Sea Height - seaHeight = addon.getPlugin().getIWM().getSeaHeight(island.getWorld()); + this.addon = addon; + this.island = island; + this.r = r; + this.zeroIsland = zeroIsland; + results = new Results(); + duration = System.currentTimeMillis(); + chunksToCheck = getChunksToScan(island); + this.limitCount = new EnumMap<>(addon.getBlockConfig().getBlockLimits()); + // Get the initial island level + results.initialLevel.set(addon.getInitialIslandLevel(island)); + // Set up the worlds + worlds.put(Environment.NORMAL, Util.getWorld(island.getWorld())); + // Nether + if (addon.getSettings().isNether()) { + World nether = addon.getPlugin().getIWM().getNetherWorld(island.getWorld()); + if (nether != null) { + worlds.put(Environment.NETHER, nether); + } + } + // End + if (addon.getSettings().isEnd()) { + World end = addon.getPlugin().getIWM().getEndWorld(island.getWorld()); + if (end != null) { + worlds.put(Environment.THE_END, end); + } + } + // Sea Height + seaHeight = addon.getPlugin().getIWM().getSeaHeight(island.getWorld()); } /** * Calculate the level based on the raw points + * * @param blockAndDeathPoints - raw points counted on island * @return level of island */ private long calculateLevel(long blockAndDeathPoints) { - String calcString = addon.getSettings().getLevelCalc(); - String withValues = calcString.replace("blocks", String.valueOf(blockAndDeathPoints)).replace("level_cost", String.valueOf(this.addon.getSettings().getLevelCost())); - long evalWithValues; - try { - evalWithValues = (long)eval(withValues); - return evalWithValues - (addon.getSettings().isZeroNewIslandLevels() ? results.initialLevel.get() : 0); - - } catch (IOException e) { - addon.getPlugin().logStacktrace(e); - return 0L; - } + String calcString = addon.getSettings().getLevelCalc(); + String withValues = calcString.replace("blocks", String.valueOf(blockAndDeathPoints)).replace("level_cost", + String.valueOf(this.addon.getSettings().getLevelCost())); + long evalWithValues; + try { + evalWithValues = (long) EquationEvaluator.eval(withValues); + return evalWithValues - (addon.getSettings().isZeroNewIslandLevels() ? results.initialLevel.get() : 0); + + } catch (ParseException e) { + addon.getPlugin().logStacktrace(e); + return 0L; + } } /** - * Adds value to the results based on the material and whether the block is below sea level or not - * @param mat - material of the block + * Adds value to the results based on the material and whether the block is + * below sea level or not + * + * @param mat - material of the block * @param belowSeaLevel - true if below sea level */ private void checkBlock(Material mat, boolean belowSeaLevel) { - int count = limitCount(mat); - if (belowSeaLevel) { - results.underWaterBlockCount.addAndGet(count); - results.uwCount.add(mat); - } else { - results.rawBlockCount.addAndGet(count); - results.mdCount.add(mat); - } + int count = limitCount(mat); + if (belowSeaLevel) { + results.underWaterBlockCount.addAndGet(count); + results.uwCount.add(mat); + } else { + results.rawBlockCount.addAndGet(count); + results.mdCount.add(mat); + } } /** * Get a set of all the chunks in island + * * @param island - island * @return - set of pairs of x,z coordinates to check */ private Queue> getChunksToScan(Island island) { - Queue> chunkQueue = new ConcurrentLinkedQueue<>(); - for (int x = island.getMinProtectedX(); x < (island.getMinProtectedX() + island.getProtectionRange() * 2 + 16); x += 16) { - for (int z = island.getMinProtectedZ(); z < (island.getMinProtectedZ() + island.getProtectionRange() * 2 + 16); z += 16) { - chunkQueue.add(new Pair<>(x >> 4, z >> 4)); - } - } - return chunkQueue; + Queue> chunkQueue = new ConcurrentLinkedQueue<>(); + for (int x = island.getMinProtectedX(); x < (island.getMinProtectedX() + island.getProtectionRange() * 2 + + 16); x += 16) { + for (int z = island.getMinProtectedZ(); z < (island.getMinProtectedZ() + island.getProtectionRange() * 2 + + 16); z += 16) { + chunkQueue.add(new Pair<>(x >> 4, z >> 4)); + } + } + return chunkQueue; } - /** * @return the island */ public Island getIsland() { - return island; + return island; } /** * Get the completable result for this calculation + * * @return the r */ public CompletableFuture getR() { - return r; + return r; } /** * Get the full analysis report + * * @return a list of lines */ private List getReport() { - List reportLines = new ArrayList<>(); - // provide counts - reportLines.add("Level Log for island in " + addon.getPlugin().getIWM().getFriendlyName(island.getWorld()) + " at " + Util.xyz(island.getCenter().toVector())); - reportLines.add("Island owner UUID = " + island.getOwner()); - reportLines.add("Total block value count = " + String.format("%,d",results.rawBlockCount.get())); - reportLines.add("Formula to calculate island level: " + addon.getSettings().getLevelCalc()); - reportLines.add("Level cost = " + addon.getSettings().getLevelCost()); - reportLines.add("Deaths handicap = " + results.deathHandicap.get()); - if (addon.getSettings().isZeroNewIslandLevels()) { - reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island))); - } - reportLines.add("Previous level = " + addon.getManager().getIslandLevel(island.getWorld(), island.getOwner())); - reportLines.add("New level = " + results.getLevel()); - reportLines.add(LINE_BREAK); - int total = 0; - if (!results.uwCount.isEmpty()) { - reportLines.add("Underwater block count (Multiplier = x" + addon.getSettings().getUnderWaterMultiplier() + ") value"); - reportLines.add("Total number of underwater blocks = " + String.format("%,d",results.uwCount.size())); - reportLines.addAll(sortedReport(total, results.uwCount)); - } - reportLines.add("Regular block count"); - reportLines.add("Total number of blocks = " + String.format("%,d",results.mdCount.size())); - reportLines.addAll(sortedReport(total, results.mdCount)); - - reportLines.add("Blocks not counted because they exceeded limits: " + String.format("%,d",results.ofCount.size())); - Iterable> entriesSortedByCount = results.ofCount.entrySet(); - Iterator> it = entriesSortedByCount.iterator(); - while (it.hasNext()) { - Entry type = it.next(); - Integer limit = addon.getBlockConfig().getBlockLimits().get(type.getElement()); - String explain = ")"; - if (limit == null) { - Material generic = type.getElement(); - limit = addon.getBlockConfig().getBlockLimits().get(generic); - explain = " - All types)"; - } - reportLines.add(type.getElement().toString() + ": " + String.format("%,d",type.getCount()) + " blocks (max " + limit + explain); - } - reportLines.add(LINE_BREAK); - reportLines.add("Blocks on island that are not in config.yml"); - reportLines.add("Total number = " + String.format("%,d",results.ncCount.size())); - entriesSortedByCount = results.ncCount.entrySet(); - it = entriesSortedByCount.iterator(); - while (it.hasNext()) { - Entry type = it.next(); - reportLines.add(type.getElement().toString() + ": " + String.format("%,d",type.getCount()) + " blocks"); - } - reportLines.add(LINE_BREAK); - - return reportLines; + List reportLines = new ArrayList<>(); + // provide counts + reportLines.add("Level Log for island in " + addon.getPlugin().getIWM().getFriendlyName(island.getWorld()) + + " at " + Util.xyz(island.getCenter().toVector())); + reportLines.add("Island owner UUID = " + island.getOwner()); + reportLines.add("Total block value count = " + String.format("%,d", results.rawBlockCount.get())); + reportLines.add("Formula to calculate island level: " + addon.getSettings().getLevelCalc()); + reportLines.add("Level cost = " + addon.getSettings().getLevelCost()); + reportLines.add("Deaths handicap = " + results.deathHandicap.get()); + if (addon.getSettings().isZeroNewIslandLevels()) { + reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island))); + } + reportLines.add("Previous level = " + addon.getManager().getIslandLevel(island.getWorld(), island.getOwner())); + reportLines.add("New level = " + results.getLevel()); + reportLines.add(LINE_BREAK); + int total = 0; + if (!results.uwCount.isEmpty()) { + reportLines.add("Underwater block count (Multiplier = x" + addon.getSettings().getUnderWaterMultiplier() + + ") value"); + reportLines.add("Total number of underwater blocks = " + String.format("%,d", results.uwCount.size())); + reportLines.addAll(sortedReport(total, results.uwCount)); + } + reportLines.add("Regular block count"); + reportLines.add("Total number of blocks = " + String.format("%,d", results.mdCount.size())); + reportLines.addAll(sortedReport(total, results.mdCount)); + + reportLines.add( + "Blocks not counted because they exceeded limits: " + String.format("%,d", results.ofCount.size())); + Iterable> entriesSortedByCount = results.ofCount.entrySet(); + Iterator> it = entriesSortedByCount.iterator(); + while (it.hasNext()) { + Entry type = it.next(); + Integer limit = addon.getBlockConfig().getBlockLimits().get(type.getElement()); + String explain = ")"; + if (limit == null) { + Material generic = type.getElement(); + limit = addon.getBlockConfig().getBlockLimits().get(generic); + explain = " - All types)"; + } + reportLines.add(type.getElement().toString() + ": " + String.format("%,d", type.getCount()) + + " blocks (max " + limit + explain); + } + reportLines.add(LINE_BREAK); + reportLines.add("Blocks on island that are not in config.yml"); + reportLines.add("Total number = " + String.format("%,d", results.ncCount.size())); + entriesSortedByCount = results.ncCount.entrySet(); + it = entriesSortedByCount.iterator(); + while (it.hasNext()) { + Entry type = it.next(); + reportLines.add(type.getElement().toString() + ": " + String.format("%,d", type.getCount()) + " blocks"); + } + reportLines.add(LINE_BREAK); + + return reportLines; } /** * @return the results */ public Results getResults() { - return results; + return results; } + /** - * Get value of a material - * World blocks trump regular block values + * Get value of a material World blocks trump regular block values + * * @param md - Material to check * @return value of a material */ private int getValue(Material md) { - Integer value = addon.getBlockConfig().getValue(island.getWorld(), md); - if (value == null) { - // Not in config - results.ncCount.add(md); - return 0; - } - return value; + Integer value = addon.getBlockConfig().getValue(island.getWorld(), md); + if (value == null) { + // Not in config + results.ncCount.add(md); + return 0; + } + return value; } /** * Get a chunk async - * @param env - the environment + * + * @param env - the environment * @param pairList - chunk coordinate - * @return a future chunk or future null if there is no chunk to load, e.g., there is no island nether + * @return a future chunk or future null if there is no chunk to load, e.g., + * there is no island nether */ private CompletableFuture> getWorldChunk(Environment env, Queue> pairList) { - if (worlds.containsKey(env)) { - CompletableFuture> r2 = new CompletableFuture<>(); - List chunkList = new ArrayList<>(); - World world = worlds.get(env); - // Get the chunk, and then coincidentally check the RoseStacker - loadChunks(r2, world, pairList, chunkList); - return r2; - } - return CompletableFuture.completedFuture(Collections.emptyList()); + if (worlds.containsKey(env)) { + CompletableFuture> r2 = new CompletableFuture<>(); + List chunkList = new ArrayList<>(); + World world = worlds.get(env); + // Get the chunk, and then coincidentally check the RoseStacker + loadChunks(r2, world, pairList, chunkList); + return r2; + } + return CompletableFuture.completedFuture(Collections.emptyList()); } private void loadChunks(CompletableFuture> r2, World world, Queue> pairList, - List chunkList) { - if (pairList.isEmpty()) { - r2.complete(chunkList); - return; - } - Pair p = pairList.poll(); - Util.getChunkAtAsync(world, p.x, p.z, world.getEnvironment().equals(Environment.NETHER)).thenAccept(chunk -> { - if (chunk != null) { - chunkList.add(chunk); - roseStackerCheck(chunk); - } - loadChunks(r2, world, pairList, chunkList); // Iteration - }); + List chunkList) { + if (pairList.isEmpty()) { + r2.complete(chunkList); + return; + } + Pair p = pairList.poll(); + Util.getChunkAtAsync(world, p.x, p.z, world.getEnvironment().equals(Environment.NETHER)).thenAccept(chunk -> { + if (chunk != null) { + chunkList.add(chunk); + roseStackerCheck(chunk); + } + loadChunks(r2, world, pairList, chunkList); // Iteration + }); } private void roseStackerCheck(Chunk chunk) { - if (addon.isRoseStackersEnabled()) { - RoseStackerAPI.getInstance().getStackedBlocks(Collections.singletonList(chunk)).forEach(e -> { - // Blocks below sea level can be scored differently - boolean belowSeaLevel = seaHeight > 0 && e.getLocation().getY() <= seaHeight; - // Check block once because the base block will be counted in the chunk snapshot - for (int _x = 0; _x < e.getStackSize() - 1; _x++) { - checkBlock(e.getBlock().getType(), belowSeaLevel); - } - }); - } + if (addon.isRoseStackersEnabled()) { + RoseStackerAPI.getInstance().getStackedBlocks(Collections.singletonList(chunk)).forEach(e -> { + // Blocks below sea level can be scored differently + boolean belowSeaLevel = seaHeight > 0 && e.getLocation().getY() <= seaHeight; + // Check block once because the base block will be counted in the chunk snapshot + for (int _x = 0; _x < e.getStackSize() - 1; _x++) { + checkBlock(e.getBlock().getType(), belowSeaLevel); + } + }); + } } /** - * Checks if a block has been limited or not and whether a block has any value or not + * Checks if a block has been limited or not and whether a block has any value + * or not + * * @param md Material * @return value of the block if can be counted */ private int limitCount(Material md) { - if (limitCount.containsKey(md)) { - int count = limitCount.get(md); - if (count > 0) { - limitCount.put(md, --count); - return getValue(md); - } else { - results.ofCount.add(md); - return 0; - } - } - return getValue(md); + if (limitCount.containsKey(md)) { + int count = limitCount.get(md); + if (count > 0) { + limitCount.put(md, --count); + return getValue(md); + } else { + results.ofCount.add(md); + return 0; + } + } + return getValue(md); } - /** * Scan all containers in a chunk and count their blocks + * * @param chunk - the chunk to scan */ private void scanChests(Chunk chunk) { - // Count blocks in chests - for (BlockState bs : chunk.getTileEntities()) { - if (bs instanceof Container container) { - if (addon.isAdvChestEnabled()) { - AdvancedChest aChest = AdvancedChestsAPI.getChestManager().getAdvancedChest(bs.getLocation()); - if (aChest != null && aChest.getChestType().getName().equals("NORMAL")) { - aChest.getPages().stream().map(ChestPage::getItems).forEach(c -> { - for (Object i : c) { - countItemStack((ItemStack)i); - } - }); - continue; - } - } - // Regular chest - container.getSnapshotInventory().forEach(this::countItemStack); - } - } + // Count blocks in chests + for (BlockState bs : chunk.getTileEntities()) { + if (bs instanceof Container container) { + if (addon.isAdvChestEnabled()) { + AdvancedChest aChest = AdvancedChestsAPI.getChestManager().getAdvancedChest(bs.getLocation()); + if (aChest != null && aChest.getChestType().getName().equals("NORMAL")) { + aChest.getPages().stream().map(ChestPage::getItems).forEach(c -> { + for (Object i : c) { + countItemStack((ItemStack) i); + } + }); + continue; + } + } + // Regular chest + container.getSnapshotInventory().forEach(this::countItemStack); + } + } } private void countItemStack(ItemStack i) { - if (i == null || !i.getType().isBlock()) return; - - for (int c = 0; c < i.getAmount(); c++) { - if (addon.getSettings().isIncludeShulkersInChest() - && i.getItemMeta() instanceof BlockStateMeta blockStateMeta - && blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox) { - shulkerBox.getSnapshotInventory().forEach(this::countItemStack); - } - - checkBlock(i.getType(), false); - } + if (i == null || !i.getType().isBlock()) + return; + + for (int c = 0; c < i.getAmount(); c++) { + if (addon.getSettings().isIncludeShulkersInChest() + && i.getItemMeta() instanceof BlockStateMeta blockStateMeta + && blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox) { + shulkerBox.getSnapshotInventory().forEach(this::countItemStack); + } + + checkBlock(i.getType(), false); + } } /** - * Scan the chunk chests and count the blocks. Note that the chunks are a list of all the island chunks - * in a particular world, so the memory usage is high, but I think most servers can handle it. + * Scan the chunk chests and count the blocks. Note that the chunks are a list + * of all the island chunks in a particular world, so the memory usage is high, + * but I think most servers can handle it. + * * @param chunks - a list of chunks to scan - * @return future that completes when the scan is done and supplies a boolean that will be true if the scan was successful, false if not + * @return future that completes when the scan is done and supplies a boolean + * that will be true if the scan was successful, false if not */ private CompletableFuture scanChunk(List chunks) { - // If the chunk hasn't been generated, return - if (chunks == null || chunks.isEmpty()) { - return CompletableFuture.completedFuture(false); - } - // Count blocks in chunk - CompletableFuture result = new CompletableFuture<>(); - /* - * At this point, we need to grab a snapshot of each chunk and then scan it async. - * At the end, we make the CompletableFuture true to show it is done. - * I'm not sure how much lag this will cause, but as all the chunks are loaded, maybe not that much. - */ - List preLoad = chunks.stream().map(c -> new ChunkPair(c.getWorld(), c, c.getChunkSnapshot())).toList(); - Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> { - preLoad.forEach(this::scanAsync); - // Once they are all done, return to the main thread. - Bukkit.getScheduler().runTask(addon.getPlugin(),() -> result.complete(true)); - }); - return result; + // If the chunk hasn't been generated, return + if (chunks == null || chunks.isEmpty()) { + return CompletableFuture.completedFuture(false); + } + // Count blocks in chunk + CompletableFuture result = new CompletableFuture<>(); + /* + * At this point, we need to grab a snapshot of each chunk and then scan it + * async. At the end, we make the CompletableFuture true to show it is done. I'm + * not sure how much lag this will cause, but as all the chunks are loaded, + * maybe not that much. + */ + List preLoad = chunks.stream().map(c -> new ChunkPair(c.getWorld(), c, c.getChunkSnapshot())) + .toList(); + Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> { + preLoad.forEach(this::scanAsync); + // Once they are all done, return to the main thread. + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> result.complete(true)); + }); + return result; } - record ChunkPair(World world, Chunk chunk, ChunkSnapshot chunkSnapshot) {} + record ChunkPair(World world, Chunk chunk, ChunkSnapshot chunkSnapshot) { + } /** * Count the blocks on the island + * * @param cp chunk to scan */ private void scanAsync(ChunkPair cp) { - for (int x = 0; x< 16; x++) { - // Check if the block coordinate is inside the protection zone and if not, don't count it - if (cp.chunkSnapshot.getX() * 16 + x < island.getMinProtectedX() || cp.chunkSnapshot.getX() * 16 + x >= island.getMinProtectedX() + island.getProtectionRange() * 2) { - continue; - } - for (int z = 0; z < 16; z++) { - // Check if the block coordinate is inside the protection zone and if not, don't count it - if (cp.chunkSnapshot.getZ() * 16 + z < island.getMinProtectedZ() || cp.chunkSnapshot.getZ() * 16 + z >= island.getMinProtectedZ() + island.getProtectionRange() * 2) { - continue; - } - // Only count to the highest block in the world for some optimization - for (int y = cp.world.getMinHeight(); y < cp.world.getMaxHeight(); y++) { - BlockData blockData = cp.chunkSnapshot.getBlockData(x, y, z); - boolean belowSeaLevel = seaHeight > 0 && y <= seaHeight; - // Slabs can be doubled, so check them twice - if (Tag.SLABS.isTagged(blockData.getMaterial())) { - Slab slab = (Slab)blockData; - if (slab.getType().equals(Slab.Type.DOUBLE)) { - checkBlock(blockData.getMaterial(), belowSeaLevel); - } - } - // Hook for Wild Stackers (Blocks and Spawners Only) - this has to use the real chunk - if (addon.isStackersEnabled() && (blockData.getMaterial().equals(Material.CAULDRON) || blockData.getMaterial().equals(Material.SPAWNER))) { - stackedBlocks.add(new Location(cp.world, (double)x + cp.chunkSnapshot.getX() * 16, y, (double)z + cp.chunkSnapshot.getZ() * 16)); - } - - Block block = cp.chunk.getBlock(x, y, z); - - if (addon.isUltimateStackerEnabled()) { - if (!blockData.getMaterial().equals(Material.AIR)) { - BlockStack stack = UltimateStacker.getInstance().getBlockStackManager().getBlock(block, CompatibleMaterial.getMaterial(block)); - if (stack != null) { - int value = limitCount(blockData.getMaterial()); - if (belowSeaLevel) { - results.underWaterBlockCount.addAndGet((long) stack.getAmount() * value); - results.uwCount.add(blockData.getMaterial()); - } else { - results.rawBlockCount.addAndGet((long) stack.getAmount() * value); - results.mdCount.add(blockData.getMaterial()); - } - } - } - } - - // Scan chests - if (addon.getSettings().isIncludeChests() && CHESTS.contains(blockData.getMaterial())) { - chestBlocks.add(cp.chunk); - } - // Add the value of the block's material - checkBlock(blockData.getMaterial(), belowSeaLevel); - } - } - } + for (int x = 0; x < 16; x++) { + // Check if the block coordinate is inside the protection zone and if not, don't + // count it + if (cp.chunkSnapshot.getX() * 16 + x < island.getMinProtectedX() || cp.chunkSnapshot.getX() * 16 + + x >= island.getMinProtectedX() + island.getProtectionRange() * 2) { + continue; + } + for (int z = 0; z < 16; z++) { + // Check if the block coordinate is inside the protection zone and if not, don't + // count it + if (cp.chunkSnapshot.getZ() * 16 + z < island.getMinProtectedZ() || cp.chunkSnapshot.getZ() * 16 + + z >= island.getMinProtectedZ() + island.getProtectionRange() * 2) { + continue; + } + // Only count to the highest block in the world for some optimization + for (int y = cp.world.getMinHeight(); y < cp.world.getMaxHeight(); y++) { + BlockData blockData = cp.chunkSnapshot.getBlockData(x, y, z); + boolean belowSeaLevel = seaHeight > 0 && y <= seaHeight; + // Slabs can be doubled, so check them twice + if (Tag.SLABS.isTagged(blockData.getMaterial())) { + Slab slab = (Slab) blockData; + if (slab.getType().equals(Slab.Type.DOUBLE)) { + checkBlock(blockData.getMaterial(), belowSeaLevel); + } + } + // Hook for Wild Stackers (Blocks and Spawners Only) - this has to use the real + // chunk + if (addon.isStackersEnabled() && (blockData.getMaterial().equals(Material.CAULDRON) + || blockData.getMaterial().equals(Material.SPAWNER))) { + stackedBlocks.add(new Location(cp.world, (double) x + cp.chunkSnapshot.getX() * 16, y, + (double) z + cp.chunkSnapshot.getZ() * 16)); + } + + Block block = cp.chunk.getBlock(x, y, z); + + if (addon.isUltimateStackerEnabled()) { + if (!blockData.getMaterial().equals(Material.AIR)) { + BlockStack stack = UltimateStacker.getInstance().getBlockStackManager().getBlock(block, + CompatibleMaterial.getMaterial(block)); + if (stack != null) { + int value = limitCount(blockData.getMaterial()); + if (belowSeaLevel) { + results.underWaterBlockCount.addAndGet((long) stack.getAmount() * value); + results.uwCount.add(blockData.getMaterial()); + } else { + results.rawBlockCount.addAndGet((long) stack.getAmount() * value); + results.mdCount.add(blockData.getMaterial()); + } + } + } + } + + // Scan chests + if (addon.getSettings().isIncludeChests() && CHESTS.contains(blockData.getMaterial())) { + chestBlocks.add(cp.chunk); + } + // Add the value of the block's material + checkBlock(blockData.getMaterial(), belowSeaLevel); + } + } + } } /** * Scan the next chunk on the island - * @return completable boolean future that will be true if more chunks are left to be scanned, and false if not + * + * @return completable boolean future that will be true if more chunks are left + * to be scanned, and false if not */ public CompletableFuture scanNextChunk() { - if (chunksToCheck.isEmpty()) { - addon.logError("Unexpected: no chunks to scan!"); - // This should not be needed, but just in case - return CompletableFuture.completedFuture(false); - } - // Retrieve and remove from the queue - Queue> pairList = new ConcurrentLinkedQueue<>(); - int i = 0; - while (!chunksToCheck.isEmpty() && i++ < CHUNKS_TO_SCAN) { - pairList.add(chunksToCheck.poll()); - } - Queue> endPairList = new ConcurrentLinkedQueue<>(pairList); - Queue> netherPairList = new ConcurrentLinkedQueue<>(pairList); - // Set up the result - CompletableFuture result = new CompletableFuture<>(); - // Get chunks and scan - // Get chunks and scan - getWorldChunk(Environment.THE_END, endPairList).thenAccept(endChunks -> - scanChunk(endChunks).thenAccept(b -> - getWorldChunk(Environment.NETHER, netherPairList).thenAccept(netherChunks -> - scanChunk(netherChunks).thenAccept(b2 -> - getWorldChunk(Environment.NORMAL, pairList).thenAccept(normalChunks -> - scanChunk(normalChunks).thenAccept(b3 -> - // Complete the result now that all chunks have been scanned - result.complete(!chunksToCheck.isEmpty())))) - ) - ) - ); - - return result; + if (chunksToCheck.isEmpty()) { + addon.logError("Unexpected: no chunks to scan!"); + // This should not be needed, but just in case + return CompletableFuture.completedFuture(false); + } + // Retrieve and remove from the queue + Queue> pairList = new ConcurrentLinkedQueue<>(); + int i = 0; + while (!chunksToCheck.isEmpty() && i++ < CHUNKS_TO_SCAN) { + pairList.add(chunksToCheck.poll()); + } + Queue> endPairList = new ConcurrentLinkedQueue<>(pairList); + Queue> netherPairList = new ConcurrentLinkedQueue<>(pairList); + // Set up the result + CompletableFuture result = new CompletableFuture<>(); + // Get chunks and scan + // Get chunks and scan + getWorldChunk(Environment.THE_END, endPairList).thenAccept( + endChunks -> scanChunk(endChunks).thenAccept(b -> getWorldChunk(Environment.NETHER, netherPairList) + .thenAccept(netherChunks -> scanChunk(netherChunks) + .thenAccept(b2 -> getWorldChunk(Environment.NORMAL, pairList) + .thenAccept(normalChunks -> scanChunk(normalChunks).thenAccept(b3 -> + // Complete the result now that all chunks have been scanned + result.complete(!chunksToCheck.isEmpty()))))))); + + return result; } private Collection sortedReport(int total, Multiset materialCount) { - Collection result = new ArrayList<>(); - Iterable> entriesSortedByCount = Multisets.copyHighestCountFirst(materialCount).entrySet(); - for (Entry en : entriesSortedByCount) { - Material type = en.getElement(); - - int value = getValue(type); - - result.add(type.toString() + ":" - + String.format("%,d", en.getCount()) + " blocks x " + value + " = " + (value * en.getCount())); - total += (value * en.getCount()); - - } - result.add("Subtotal = " + total); - result.add(LINE_BREAK); - return result; + Collection result = new ArrayList<>(); + Iterable> entriesSortedByCount = Multisets.copyHighestCountFirst(materialCount) + .entrySet(); + for (Entry en : entriesSortedByCount) { + Material type = en.getElement(); + + int value = getValue(type); + + result.add(type.toString() + ":" + String.format("%,d", en.getCount()) + " blocks x " + value + " = " + + (value * en.getCount())); + total += (value * en.getCount()); + + } + result.add("Subtotal = " + total); + result.add(LINE_BREAK); + return result; } - /** * Finalizes the calculations and makes the report */ public void tidyUp() { - // Finalize calculations - results.rawBlockCount.addAndGet((long)(results.underWaterBlockCount.get() * addon.getSettings().getUnderWaterMultiplier())); - - // Set the death penalty - if (this.addon.getSettings().isSumTeamDeaths()) - { - for (UUID uuid : this.island.getMemberSet()) - { - this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid)); - } - } - else - { - // At this point, it may be that the island has become unowned. - this.results.deathHandicap.set(this.island.getOwner() == null ? 0 : - this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner())); - } - - long blockAndDeathPoints = this.results.rawBlockCount.get(); - this.results.totalPoints.set(blockAndDeathPoints); - - if (this.addon.getSettings().getDeathPenalty() > 0) - { - // Proper death penalty calculation. - blockAndDeathPoints -= this.results.deathHandicap.get() * this.addon.getSettings().getDeathPenalty(); - } - this.results.level.set(calculateLevel(blockAndDeathPoints)); - - // Calculate how many points are required to get to the next level - long nextLevel = this.results.level.get(); - long blocks = blockAndDeathPoints; - while (nextLevel < this.results.level.get() + 1 && blocks - blockAndDeathPoints < MAX_AMOUNT) { - nextLevel = calculateLevel(++blocks); - } - this.results.pointsToNextLevel.set(blocks - blockAndDeathPoints); - - // Report - results.report = getReport(); - // Set the duration - addon.getPipeliner().setTime(System.currentTimeMillis() - duration); - // All done. + // Finalize calculations + results.rawBlockCount + .addAndGet((long) (results.underWaterBlockCount.get() * addon.getSettings().getUnderWaterMultiplier())); + + // Set the death penalty + if (this.addon.getSettings().isSumTeamDeaths()) { + for (UUID uuid : this.island.getMemberSet()) { + this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid)); + } + } else { + // At this point, it may be that the island has become unowned. + this.results.deathHandicap.set(this.island.getOwner() == null ? 0 + : this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner())); + } + + long blockAndDeathPoints = this.results.rawBlockCount.get(); + this.results.totalPoints.set(blockAndDeathPoints); + + if (this.addon.getSettings().getDeathPenalty() > 0) { + // Proper death penalty calculation. + blockAndDeathPoints -= this.results.deathHandicap.get() * this.addon.getSettings().getDeathPenalty(); + } + this.results.level.set(calculateLevel(blockAndDeathPoints)); + + // Calculate how many points are required to get to the next level + long nextLevel = this.results.level.get(); + long blocks = blockAndDeathPoints; + while (nextLevel < this.results.level.get() + 1 && blocks - blockAndDeathPoints < MAX_AMOUNT) { + nextLevel = calculateLevel(++blocks); + } + this.results.pointsToNextLevel.set(blocks - blockAndDeathPoints); + + // Report + results.report = getReport(); + // Set the duration + addon.getPipeliner().setTime(System.currentTimeMillis() - duration); + // All done. } /** * @return the zeroIsland */ boolean isNotZeroIsland() { - return !zeroIsland; + return !zeroIsland; } public void scanIsland(Pipeliner pipeliner) { - // Scan the next chunk - scanNextChunk().thenAccept(result -> { - if (!Bukkit.isPrimaryThread()) { - addon.getPlugin().logError("scanChunk not on Primary Thread!"); - } - // Timeout check - if (System.currentTimeMillis() - pipeliner.getInProcessQueue().get(this) > addon.getSettings().getCalculationTimeout() * 60000) { - // Done - pipeliner.getInProcessQueue().remove(this); - getR().complete(new Results(Result.TIMEOUT)); - addon.logError("Level calculation timed out after " + addon.getSettings().getCalculationTimeout() + "m for island: " + getIsland()); - if (!isNotZeroIsland()) { - addon.logError("Island level was being zeroed."); - } - return; - } - if (Boolean.TRUE.equals(result) && !pipeliner.getTask().isCancelled()) { - // scanNextChunk returns true if there are more chunks to scan - scanIsland(pipeliner); - } else { - // Done - pipeliner.getInProcessQueue().remove(this); - // Chunk finished - // This was the last chunk - handleStackedBlocks(); - handleChests(); - long checkTime = System.currentTimeMillis(); - finishTask = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { - // Check every half second if all the chests and stacks have been cleared - if ((chestBlocks.isEmpty() && stackedBlocks.isEmpty()) || System.currentTimeMillis() - checkTime > MAX_AMOUNT) { - this.tidyUp(); - this.getR().complete(getResults()); - finishTask.cancel(); - } - }, 0, 10L); - - } - }); + // Scan the next chunk + scanNextChunk().thenAccept(result -> { + if (!Bukkit.isPrimaryThread()) { + addon.getPlugin().logError("scanChunk not on Primary Thread!"); + } + // Timeout check + if (System.currentTimeMillis() + - pipeliner.getInProcessQueue().get(this) > addon.getSettings().getCalculationTimeout() * 60000) { + // Done + pipeliner.getInProcessQueue().remove(this); + getR().complete(new Results(Result.TIMEOUT)); + addon.logError("Level calculation timed out after " + addon.getSettings().getCalculationTimeout() + + "m for island: " + getIsland()); + if (!isNotZeroIsland()) { + addon.logError("Island level was being zeroed."); + } + return; + } + if (Boolean.TRUE.equals(result) && !pipeliner.getTask().isCancelled()) { + // scanNextChunk returns true if there are more chunks to scan + scanIsland(pipeliner); + } else { + // Done + pipeliner.getInProcessQueue().remove(this); + // Chunk finished + // This was the last chunk + handleStackedBlocks(); + handleChests(); + long checkTime = System.currentTimeMillis(); + finishTask = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { + // Check every half second if all the chests and stacks have been cleared + if ((chestBlocks.isEmpty() && stackedBlocks.isEmpty()) + || System.currentTimeMillis() - checkTime > MAX_AMOUNT) { + this.tidyUp(); + this.getR().complete(getResults()); + finishTask.cancel(); + } + }, 0, 10L); + + } + }); } private void handleChests() { - Iterator it = chestBlocks.iterator(); - while(it.hasNext()) { - Chunk v = it.next(); - Util.getChunkAtAsync(v.getWorld(), v.getX(), v.getZ()).thenAccept(c -> { - scanChests(c); - it.remove(); - }); - } + Iterator it = chestBlocks.iterator(); + while (it.hasNext()) { + Chunk v = it.next(); + Util.getChunkAtAsync(v.getWorld(), v.getX(), v.getZ()).thenAccept(c -> { + scanChests(c); + it.remove(); + }); + } } private void handleStackedBlocks() { - // Deal with any stacked blocks - Iterator it = stackedBlocks.iterator(); - while (it.hasNext()) { - Location v = it.next(); - Util.getChunkAtAsync(v).thenAccept(c -> { - Block stackedBlock = v.getBlock(); - boolean belowSeaLevel = seaHeight > 0 && v.getBlockY() <= seaHeight; - if (WildStackerAPI.getWildStacker().getSystemManager().isStackedBarrel(stackedBlock)) { - StackedBarrel barrel = WildStackerAPI.getStackedBarrel(stackedBlock); - int barrelAmt = WildStackerAPI.getBarrelAmount(stackedBlock); - for (int _x = 0; _x < barrelAmt; _x++) { - checkBlock(barrel.getType(), belowSeaLevel); - } - } else if (WildStackerAPI.getWildStacker().getSystemManager().isStackedSpawner(stackedBlock)) { - int spawnerAmt = WildStackerAPI.getSpawnersAmount((CreatureSpawner) stackedBlock.getState()); - for (int _x = 0; _x < spawnerAmt; _x++) { - checkBlock(stackedBlock.getType(), belowSeaLevel); - } - } - it.remove(); - }); - } + // Deal with any stacked blocks + Iterator it = stackedBlocks.iterator(); + while (it.hasNext()) { + Location v = it.next(); + Util.getChunkAtAsync(v).thenAccept(c -> { + Block stackedBlock = v.getBlock(); + boolean belowSeaLevel = seaHeight > 0 && v.getBlockY() <= seaHeight; + if (WildStackerAPI.getWildStacker().getSystemManager().isStackedBarrel(stackedBlock)) { + StackedBarrel barrel = WildStackerAPI.getStackedBarrel(stackedBlock); + int barrelAmt = WildStackerAPI.getBarrelAmount(stackedBlock); + for (int _x = 0; _x < barrelAmt; _x++) { + checkBlock(barrel.getType(), belowSeaLevel); + } + } else if (WildStackerAPI.getWildStacker().getSystemManager().isStackedSpawner(stackedBlock)) { + int spawnerAmt = WildStackerAPI.getSpawnersAmount((CreatureSpawner) stackedBlock.getState()); + for (int _x = 0; _x < spawnerAmt; _x++) { + checkBlock(stackedBlock.getType(), belowSeaLevel); + } + } + it.remove(); + }); + } } } diff --git a/src/main/java/world/bentobox/level/commands/AdminStatsCommand.java b/src/main/java/world/bentobox/level/commands/AdminStatsCommand.java new file mode 100644 index 0000000..180c41e --- /dev/null +++ b/src/main/java/world/bentobox/level/commands/AdminStatsCommand.java @@ -0,0 +1,98 @@ +package world.bentobox.level.commands; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.bukkit.World; + +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.level.Level; +import world.bentobox.level.objects.TopTenData; + +public class AdminStatsCommand extends CompositeCommand { + + private final Level level; + + public AdminStatsCommand(Level addon, CompositeCommand parent) { + super(parent, "stats"); + this.level = addon; + new AdminTopRemoveCommand(addon, this); + } + + @Override + public void setup() { + this.setPermission("admin.stats"); + this.setOnlyPlayer(false); + this.setDescription("admin.stats.description"); + } + + @Override + public boolean execute(User user, String label, List args) { + user.sendMessage("admin.stats.title"); + Map topTenLists = level.getManager().getTopTenLists(); + if (topTenLists.isEmpty()) { + user.sendMessage("admin.stats.no-data"); + return false; + } + for (Entry en : topTenLists.entrySet()) { + user.sendMessage("admin.stats.world", TextVariables.NAME, + level.getPlugin().getIWM().getWorldName(en.getKey())); + Map topTen = en.getValue().getTopTen(); + if (topTen.isEmpty()) { + user.sendMessage("admin.stats.no-data"); + return false; + } + + // Calculating basic statistics + long sum = 0, max = Long.MIN_VALUE, min = Long.MAX_VALUE; + Map levelFrequency = new HashMap<>(); + + for (Long level : topTen.values()) { + sum += level; + max = Math.max(max, level); + min = Math.min(min, level); + levelFrequency.merge(level, 1, Integer::sum); + } + + double average = sum / (double) topTen.size(); + List sortedLevels = topTen.values().stream().sorted().collect(Collectors.toList()); + long median = sortedLevels.get(sortedLevels.size() / 2); + Long mode = Collections.max(levelFrequency.entrySet(), Map.Entry.comparingByValue()).getKey(); + + // Logging basic statistics + user.sendMessage("admin.stats.average-level", TextVariables.NUMBER, String.valueOf(average)); + user.sendMessage("admin.stats.median-level", TextVariables.NUMBER, String.valueOf(median)); + user.sendMessage("admin.stats.mode-level", TextVariables.NUMBER, String.valueOf(mode)); + user.sendMessage("admin.stats.highest-level", TextVariables.NUMBER, String.valueOf(max)); + user.sendMessage("admin.stats.lowest-level", TextVariables.NUMBER, String.valueOf(min)); + + // Grouping data for distribution analysis + Map rangeMap = new TreeMap<>(); + for (Long level : topTen.values()) { + String range = getRange(level); + rangeMap.merge(range, 1, Integer::sum); + } + + // Logging distribution + user.sendMessage("admin.stats.distribution"); + for (Map.Entry entry : rangeMap.entrySet()) { + user.sendMessage( + entry.getKey() + ": " + entry.getValue() + " " + user.getTranslation("admin.stats.islands")); + } + } + return true; + } + + private static String getRange(long level) { + long rangeStart = level / 100 * 100; + long rangeEnd = rangeStart + 99; + return rangeStart + "-" + rangeEnd; + } +} diff --git a/src/main/java/world/bentobox/level/commands/AdminTopCommand.java b/src/main/java/world/bentobox/level/commands/AdminTopCommand.java index 9144107..9287e14 100644 --- a/src/main/java/world/bentobox/level/commands/AdminTopCommand.java +++ b/src/main/java/world/bentobox/level/commands/AdminTopCommand.java @@ -2,7 +2,7 @@ import java.util.List; import java.util.Map; -import java.util.UUID; +import java.util.Optional; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; @@ -14,36 +14,32 @@ public class AdminTopCommand extends CompositeCommand { private final Level levelPlugin; public AdminTopCommand(Level addon, CompositeCommand parent) { - super(parent, "top", "topten"); - this.levelPlugin = addon; - new AdminTopRemoveCommand(addon, this); + super(parent, "top", "topten"); + this.levelPlugin = addon; + new AdminTopRemoveCommand(addon, this); } @Override public void setup() { - this.setPermission("admin.top"); - this.setOnlyPlayer(false); - this.setDescription("admin.top.description"); + this.setPermission("admin.top"); + this.setOnlyPlayer(false); + this.setDescription("admin.top.description"); } @Override public boolean execute(User user, String label, List args) { - user.sendMessage("island.top.gui-title"); - int rank = 0; - for (Map.Entry topTen : levelPlugin.getManager().getTopTen(getWorld(), Level.TEN).entrySet()) { - Island island = getPlugin().getIslands().getIsland(getWorld(), topTen.getKey()); - if (island != null) { - rank++; - user.sendMessage("admin.top.display", - "[rank]", - String.valueOf(rank), - "[name]", - this.getPlugin().getPlayers().getUser(island.getOwner()).getName(), - "[level]", - String.valueOf(topTen.getValue())); - } - } - - return true; + user.sendMessage("island.top.gui-title"); + int rank = 0; + for (Map.Entry topTen : levelPlugin.getManager().getTopTen(getWorld(), Level.TEN).entrySet()) { + Optional is = getPlugin().getIslands().getIslandById(topTen.getKey()); + if (is.isPresent()) { + Island island = is.get(); + rank++; + user.sendMessage("admin.top.display", "[rank]", String.valueOf(rank), "[name]", + this.getPlugin().getPlayers().getUser(island.getOwner()).getName(), "[level]", + String.valueOf(topTen.getValue())); + } + } + return true; } } diff --git a/src/main/java/world/bentobox/level/commands/AdminTopRemoveCommand.java b/src/main/java/world/bentobox/level/commands/AdminTopRemoveCommand.java index b54ca3e..1c51fb9 100644 --- a/src/main/java/world/bentobox/level/commands/AdminTopRemoveCommand.java +++ b/src/main/java/world/bentobox/level/commands/AdminTopRemoveCommand.java @@ -6,10 +6,12 @@ import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; import world.bentobox.level.Level; /** * Removes a player from the top ten + * * @author tastybento * */ @@ -19,46 +21,53 @@ public class AdminTopRemoveCommand extends CompositeCommand { private User target; public AdminTopRemoveCommand(Level addon, CompositeCommand parent) { - super(parent, "remove", "delete"); - this.addon = addon; + super(parent, "remove", "delete"); + this.addon = addon; } @Override public void setup() { - this.setPermission("admin.top.remove"); - this.setOnlyPlayer(false); - this.setParametersHelp("admin.top.remove.parameters"); - this.setDescription("admin.top.remove.description"); + this.setPermission("admin.top.remove"); + this.setOnlyPlayer(false); + this.setParametersHelp("admin.top.remove.parameters"); + this.setDescription("admin.top.remove.description"); } - /* (non-Javadoc) - * @see world.bentobox.bentobox.api.commands.BentoBoxCommand#canExecute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List) + /* + * (non-Javadoc) + * + * @see world.bentobox.bentobox.api.commands.BentoBoxCommand#canExecute(world. + * bentobox.bentobox.api.user.User, java.lang.String, java.util.List) */ @Override public boolean canExecute(User user, String label, List args) { - if (args.size() != 1) { - this.showHelp(this, user); - return false; - } - target = getPlayers().getUser(args.get(0)); - if (target == null) { - user.sendMessage("general.errors.unknown-player", TextVariables.NAME, args.get(0)); - return false; - } + if (args.size() != 1) { + this.showHelp(this, user); + return false; + } + target = getPlayers().getUser(args.get(0)); + if (target == null) { + user.sendMessage("general.errors.unknown-player", TextVariables.NAME, args.get(0)); + return false; + } - return true; + return true; } @Override public boolean execute(User user, String label, List args) { - addon.getManager().removeEntry(getWorld(), target.getUniqueId()); - user.sendMessage("general.success"); - return true; + // Removes islands that this target is an owner of + getIslands().getIslands(getWorld(), target.getUniqueId()).stream() + .filter(is -> target.getUniqueId().equals(is.getOwner())) + .forEach(island -> addon.getManager().removeEntry(getWorld(), island.getUniqueId())); + user.sendMessage("general.success"); + return true; } @Override public Optional> tabComplete(User user, String alias, List args) { - return Optional.of(addon.getManager().getTopTen(getWorld(), Level.TEN).keySet().stream().map(addon.getPlayers()::getName) - .filter(n -> !n.isEmpty()).toList()); + return Optional.of(addon.getManager().getTopTen(getWorld(), Level.TEN).keySet().stream() + .map(getIslands()::getIslandById).flatMap(Optional::stream).map(Island::getOwner) + .map(addon.getPlayers()::getName).filter(n -> !n.isEmpty()).toList()); } } diff --git a/src/main/java/world/bentobox/level/config/BlockConfig.java b/src/main/java/world/bentobox/level/config/BlockConfig.java index 2180048..efcb3ee 100644 --- a/src/main/java/world/bentobox/level/config/BlockConfig.java +++ b/src/main/java/world/bentobox/level/config/BlockConfig.java @@ -60,10 +60,16 @@ private void loadWorlds(YamlConfiguration blockValues2) { if (bWorld != null) { ConfigurationSection worldValues = worlds.getConfigurationSection(world); for (String material : Objects.requireNonNull(worldValues).getKeys(false)) { - Material mat = Material.valueOf(material); - Map values = worldBlockValues.getOrDefault(bWorld, new EnumMap<>(Material.class)); - values.put(mat, worldValues.getInt(material)); - worldBlockValues.put(bWorld, values); + try { + Material mat = Material.valueOf(material); + Map values = worldBlockValues.getOrDefault(bWorld, + new EnumMap<>(Material.class)); + values.put(mat, worldValues.getInt(material)); + worldBlockValues.put(bWorld, values); + } catch (Exception e) { + addon.logError( + "Unknown material (" + material + ") in blockconfig.yml worlds section. Skipping..."); + } } } else { addon.logWarning("Level Addon: No such world in blockconfig.yml : " + world); @@ -97,7 +103,7 @@ private Map loadBlockLimits(YamlConfiguration blockValues2) { Material mat = Material.valueOf(material); bl.put(mat, limits.getInt(material, 0)); } catch (Exception e) { - addon.logWarning("Unknown material (" + material + ") in blockconfig.yml Limits section. Skipping..."); + addon.logError("Unknown material (" + material + ") in blockconfig.yml Limits section. Skipping..."); } } return bl; diff --git a/src/main/java/world/bentobox/level/config/ConfigSettings.java b/src/main/java/world/bentobox/level/config/ConfigSettings.java index 7be3364..c287d2a 100644 --- a/src/main/java/world/bentobox/level/config/ConfigSettings.java +++ b/src/main/java/world/bentobox/level/config/ConfigSettings.java @@ -1,5 +1,6 @@ package world.bentobox.level.config; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -126,6 +127,12 @@ public class ConfigSettings implements ConfigObject { @ConfigEntry(path = "include-shulkers-in-chest") private boolean includeShulkersInChest = false; + @ConfigComment("") + @ConfigComment("Disables hooking with other plugins.") + @ConfigComment("Example: disabled-plugin-hooks: [UltimateStacker, RoseStacker]") + @ConfigEntry(path = "disabled-plugin-hooks") + private List disabledPluginHooks = new ArrayList<>(); + /** * @return the gameModes @@ -404,4 +411,12 @@ public boolean isIncludeShulkersInChest() { public void setIncludeShulkersInChest(boolean includeShulkersInChest) { this.includeShulkersInChest = includeShulkersInChest; } + + public List getDisabledPluginHooks() { + return disabledPluginHooks; + } + + public void setDisabledPluginHooks(List disabledPluginHooks) { + this.disabledPluginHooks = disabledPluginHooks; + } } diff --git a/src/main/java/world/bentobox/level/listeners/IslandActivitiesListeners.java b/src/main/java/world/bentobox/level/listeners/IslandActivitiesListeners.java index 05bdfc2..4a75f5c 100644 --- a/src/main/java/world/bentobox/level/listeners/IslandActivitiesListeners.java +++ b/src/main/java/world/bentobox/level/listeners/IslandActivitiesListeners.java @@ -1,7 +1,5 @@ package world.bentobox.level.listeners; -import java.util.UUID; - import org.bukkit.World; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -21,7 +19,9 @@ import world.bentobox.level.Level; /** - * Listens for new islands or ownership changes and sets the level to zero automatically + * Listens for new islands or ownership changes and sets the level to zero + * automatically + * * @author tastybento * */ @@ -33,93 +33,89 @@ public class IslandActivitiesListeners implements Listener { * @param addon - addon */ public IslandActivitiesListeners(Level addon) { - this.addon = addon; + this.addon = addon; } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIsland(IslandCreatedEvent e) { - if (addon.getSettings().isZeroNewIslandLevels()) { - zeroIsland(e.getIsland()); - } + if (addon.getSettings().isZeroNewIslandLevels()) { + zeroIsland(e.getIsland()); + } } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIsland(IslandResettedEvent e) { - if (addon.getSettings().isZeroNewIslandLevels()) { - zeroIsland(e.getIsland()); - } + if (addon.getSettings().isZeroNewIslandLevels()) { + zeroIsland(e.getIsland()); + } } private void zeroIsland(final Island island) { - // Clear the island setting - if (island.getOwner() != null && island.getWorld() != null) { - addon.getPipeliner().zeroIsland(island).thenAccept(results -> - addon.getManager().setInitialIslandLevel(island, results.getLevel())); - } + // Clear the island setting + if (island.getOwner() != null && island.getWorld() != null) { + addon.getPipeliner().zeroIsland(island) + .thenAccept(results -> addon.getManager().setInitialIslandLevel(island, results.getLevel())); + } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onIslandDelete(IslandPreclearEvent e) { - - // Remove player from the top ten and level - UUID uuid = e.getIsland().getOwner(); - World world = e.getIsland().getWorld(); - remove(world, uuid); + remove(e.getIsland().getWorld(), e.getIsland().getUniqueId()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onIslandDeleted(IslandDeleteEvent e) { - // Remove island - addon.getManager().deleteIsland(e.getIsland().getUniqueId()); + // Remove island + addon.getManager().deleteIsland(e.getIsland().getUniqueId()); } - private void remove(World world, UUID uuid) { - if (uuid != null && world != null) { - addon.getManager().removeEntry(world, uuid); - } + private void remove(World world, String uuid) { + if (uuid != null && world != null) { + addon.getManager().removeEntry(world, uuid); + } } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIslandOwner(TeamSetownerEvent e) { - // Remove player from the top ten and level - remove(e.getIsland().getWorld(), e.getIsland().getOwner()); + // Remove island from the top ten and level + remove(e.getIsland().getWorld(), e.getIsland().getUniqueId()); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onIsland(TeamJoinedEvent e) { - - // Remove player from the top ten and level - remove(e.getIsland().getWorld(), e.getPlayerUUID()); + // TODO: anything to do here? + // Remove player from the top ten and level + // remove(e.getIsland().getWorld(), e.getPlayerUUID()); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onIsland(IslandUnregisteredEvent e) { - // Remove player from the top ten - remove(e.getIsland().getWorld(), e.getPlayerUUID()); + // Remove island from the top ten + remove(e.getIsland().getWorld(), e.getIsland().getUniqueId()); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onIsland(IslandRegisteredEvent e) { - - // Remove player from the top ten - remove(e.getIsland().getWorld(), e.getPlayerUUID()); + // TODO: anything to do here? + // Remove player from the top ten + // remove(e.getIsland().getWorld(), e.getPlayerUUID()); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onIsland(TeamLeaveEvent e) { - - // Remove player from the top ten and level - remove(e.getIsland().getWorld(), e.getPlayerUUID()); + // TODO: anything to do here? + // Remove player from the top ten and level + // remove(e.getIsland().getWorld(), e.getPlayerUUID()); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onIsland(TeamKickEvent e) { - - // Remove player from the top ten and level - remove(e.getIsland().getWorld(), e.getPlayerUUID()); + //// TODO: anything to do here? + // Remove player from the top ten and level + // remove(e.getIsland().getWorld(), e.getPlayerUUID()); } } diff --git a/src/main/java/world/bentobox/level/objects/TopTenData.java b/src/main/java/world/bentobox/level/objects/TopTenData.java index e18e8bd..3d50b1f 100644 --- a/src/main/java/world/bentobox/level/objects/TopTenData.java +++ b/src/main/java/world/bentobox/level/objects/TopTenData.java @@ -3,56 +3,41 @@ import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; -import java.util.UUID; import org.bukkit.World; import com.google.gson.annotations.Expose; -import world.bentobox.bentobox.database.objects.DataObject; -import world.bentobox.bentobox.database.objects.Table; - /** * This class stores the top ten. + * * @author tastybento * */ -@Table(name = "TopTenData") -public class TopTenData implements DataObject { +public class TopTenData { // UniqueId is the world name @Expose private String uniqueId = ""; @Expose - private Map topTen = new LinkedHashMap<>(); + private Map topTen = new LinkedHashMap<>(); public TopTenData(World k) { - uniqueId = k.getName().toLowerCase(Locale.ENGLISH); - } - - @Override - public String getUniqueId() { - // This is the world name - return uniqueId; + uniqueId = k.getName().toLowerCase(Locale.ENGLISH); } - @Override - public void setUniqueId(String uniqueId) { - // This is the world name - make it always lowercase - this.uniqueId = uniqueId.toLowerCase(Locale.ENGLISH); - } /** * @return the topTen */ - public Map getTopTen() { - return topTen; + public Map getTopTen() { + return topTen; } + /** * @param topTen the topTen to set */ - public void setTopTen(Map topTen) { - this.topTen = topTen; + public void setTopTen(Map topTen) { + this.topTen = topTen; } - } diff --git a/src/main/java/world/bentobox/level/panels/DetailsPanel.java b/src/main/java/world/bentobox/level/panels/DetailsPanel.java index 395e5ed..fad4988 100644 --- a/src/main/java/world/bentobox/level/panels/DetailsPanel.java +++ b/src/main/java/world/bentobox/level/panels/DetailsPanel.java @@ -1,6 +1,5 @@ package world.bentobox.level.panels; - import java.io.File; import java.util.ArrayList; import java.util.Comparator; @@ -30,775 +29,659 @@ import world.bentobox.level.objects.IslandLevels; import world.bentobox.level.util.Utils; - /** * This class opens GUI that shows generator view for user. */ -public class DetailsPanel -{ - // --------------------------------------------------------------------- - // Section: Internal Constructor - // --------------------------------------------------------------------- - - - /** - * This is internal constructor. It is used internally in current class to avoid creating objects everywhere. - * - * @param addon Level object - * @param world World where user is operating - * @param user User who opens panel - */ - private DetailsPanel(Level addon, - World world, - User user) - { - this.addon = addon; - this.world = world; - this.user = user; - - this.island = this.addon.getIslands().getIsland(world, user); - - if (this.island != null) - { - this.levelsData = this.addon.getManager().getLevelsData(this.island); - } - else - { - this.levelsData = null; - } - - // By default no-filters are active. - this.activeTab = Tab.ALL_BLOCKS; - this.activeFilter = Filter.NAME; - this.materialCountList = new ArrayList<>(Material.values().length); - - this.updateFilters(); - } - - - /** - * This method builds this GUI. - */ - private void build() - { - if (this.island == null || this.levelsData == null) - { - // Nothing to see. - Utils.sendMessage(this.user, this.user.getTranslation("general.errors.no-island")); - return; - } - - if (this.levelsData.getMdCount().isEmpty() && this.levelsData.getUwCount().isEmpty()) - { - // Nothing to see. - Utils.sendMessage(this.user, this.user.getTranslation("level.conversations.no-data")); - return; - } - - // Start building panel. - TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); - panelBuilder.user(this.user); - panelBuilder.world(this.user.getWorld()); - - panelBuilder.template("detail_panel", new File(this.addon.getDataFolder(), "panels")); - - panelBuilder.parameters("[name]", this.user.getName()); - - panelBuilder.registerTypeBuilder("NEXT", this::createNextButton); - panelBuilder.registerTypeBuilder("PREVIOUS", this::createPreviousButton); - panelBuilder.registerTypeBuilder("BLOCK", this::createMaterialButton); - - panelBuilder.registerTypeBuilder("FILTER", this::createFilterButton); - - // Register tabs - panelBuilder.registerTypeBuilder("TAB", this::createTabButton); - - // Register unknown type builder. - panelBuilder.build(); - } - - - /** - * This method updates filter of elements based on tabs. - */ - private void updateFilters() - { - this.materialCountList.clear(); - - switch (this.activeTab) - { - case ALL_BLOCKS -> { - Map materialCountMap = new EnumMap<>(Material.class); - - materialCountMap.putAll(this.levelsData.getMdCount()); - - // Add underwater blocks. - this.levelsData.getUwCount().forEach((material, count) -> materialCountMap.put(material, - materialCountMap.computeIfAbsent(material, key -> 0) + count)); - - materialCountMap.entrySet().stream().sorted((Map.Entry.comparingByKey())). - forEachOrdered(entry -> - this.materialCountList.add(new Pair<>(entry.getKey(), entry.getValue()))); - } - case ABOVE_SEA_LEVEL -> this.levelsData.getMdCount().entrySet().stream().sorted((Map.Entry.comparingByKey())) - .forEachOrdered(entry -> this.materialCountList.add(new Pair<>(entry.getKey(), entry.getValue()))); - - case UNDERWATER -> this.levelsData.getUwCount().entrySet().stream().sorted((Map.Entry.comparingByKey())) - .forEachOrdered(entry -> this.materialCountList.add(new Pair<>(entry.getKey(), entry.getValue()))); - - case SPAWNER -> { - int aboveWater = this.levelsData.getMdCount().getOrDefault(Material.SPAWNER, 0); - int underWater = this.levelsData.getUwCount().getOrDefault(Material.SPAWNER, 0); - - // TODO: spawners need some touch... - this.materialCountList.add(new Pair<>(Material.SPAWNER, underWater + aboveWater)); - } - } - - Comparator> sorter; - - switch (this.activeFilter) - { - case COUNT -> - { - sorter = (o1, o2) -> - { - if (o1.getValue().equals(o2.getValue())) - { - String o1Name = Utils.prettifyObject(o1.getKey(), this.user); - String o2Name = Utils.prettifyObject(o2.getKey(), this.user); - - return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); - } - else - { - return Integer.compare(o2.getValue(), o1.getValue()); - } - }; - } - case VALUE -> - { - sorter = (o1, o2) -> - { - int blockLimit = this.addon.getBlockConfig().getBlockLimits().getOrDefault(o1.getKey(), 0); - int o1Count = blockLimit > 0 ? Math.min(o1.getValue(), blockLimit) : o1.getValue(); - - blockLimit = this.addon.getBlockConfig().getBlockLimits().getOrDefault(o2.getKey(), 0); - int o2Count = blockLimit > 0 ? Math.min(o2.getValue(), blockLimit) : o2.getValue(); - - long o1Value = (long) o1Count * - this.addon.getBlockConfig().getBlockValues().getOrDefault(o1.getKey(), 0); - long o2Value = (long) o2Count * - this.addon.getBlockConfig().getBlockValues().getOrDefault(o2.getKey(), 0); - - if (o1Value == o2Value) - { - String o1Name = Utils.prettifyObject(o1.getKey(), this.user); - String o2Name = Utils.prettifyObject(o2.getKey(), this.user); - - return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); - } - else - { - return Long.compare(o2Value, o1Value); - } - }; - } - default -> - { - sorter = (o1, o2) -> - { - String o1Name = Utils.prettifyObject(o1.getKey(), this.user); - String o2Name = Utils.prettifyObject(o2.getKey(), this.user); - - return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); - }; - } - } - - this.materialCountList.sort(sorter); - - this.pageIndex = 0; - } - - - // --------------------------------------------------------------------- - // Section: Tab Button Type - // --------------------------------------------------------------------- - - - /** - * Create tab button panel item. - * - * @param template the template - * @param slot the slot - * @return the panel item - */ - private PanelItem createTabButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) - { - PanelItemBuilder builder = new PanelItemBuilder(); - - if (template.icon() != null) - { - // Set icon - builder.icon(template.icon().clone()); - } - - if (template.title() != null) - { - // Set title - builder.name(this.user.getTranslation(this.world, template.title())); - } - - if (template.description() != null) - { - // Set description - builder.description(this.user.getTranslation(this.world, template.description())); - } - - Tab tab = Enums.getIfPresent(Tab.class, String.valueOf(template.dataMap().get("tab"))).or(Tab.ALL_BLOCKS); - - // Get only possible actions, by removing all inactive ones. - List activeActions = new ArrayList<>(template.actions()); - - activeActions.removeIf(action -> - "VIEW".equalsIgnoreCase(action.actionType()) && this.activeTab == tab); - - // Add Click handler - builder.clickHandler((panel, user, clickType, i) -> - { - for (ItemTemplateRecord.ActionRecords action : activeActions) - { - if ((clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) - && "VIEW".equalsIgnoreCase(action.actionType())) - { - this.activeTab = tab; - - // Update filters. - this.updateFilters(); - this.build(); - } - } - - return true; - }); - - // Collect tooltips. - List tooltips = activeActions.stream(). - filter(action -> action.tooltip() != null). - map(action -> this.user.getTranslation(this.world, action.tooltip())). - filter(text -> !text.isBlank()). - collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); - - // Add tooltips. - if (!tooltips.isEmpty()) - { - // Empty line and tooltips. - builder.description(""); - builder.description(tooltips); - } - - builder.glow(this.activeTab == tab); - - return builder.build(); - } - - - /** - * Create next button panel item. - * - * @param template the template - * @param slot the slot - * @return the panel item - */ - private PanelItem createFilterButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) - { - PanelItemBuilder builder = new PanelItemBuilder(); - - if (template.icon() != null) - { - // Set icon - builder.icon(template.icon().clone()); - } - - Filter filter; - - if (slot.amountMap().getOrDefault("FILTER", 0) > 1) - { - filter = Enums.getIfPresent(Filter.class, String.valueOf(template.dataMap().get("filter"))).or(Filter.NAME); - } - else - { - filter = this.activeFilter; - } - - final String reference = "level.gui.buttons.filters."; - - if (template.title() != null) - { - // Set title - builder.name(this.user.getTranslation(this.world, template.title().replace("[filter]", filter.name().toLowerCase()))); - } - else - { - builder.name(this.user.getTranslation(this.world, reference + filter.name().toLowerCase() + ".name")); - } - - if (template.description() != null) - { - // Set description - builder.description(this.user.getTranslation(this.world, template.description().replace("[filter]", filter.name().toLowerCase()))); - } - else - { - builder.name(this.user.getTranslation(this.world, reference + filter.name().toLowerCase() + ".description")); - } - - // Get only possible actions, by removing all inactive ones. - List activeActions = new ArrayList<>(template.actions()); - - // Add Click handler - builder.clickHandler((panel, user, clickType, i) -> - { - for (ItemTemplateRecord.ActionRecords action : activeActions) - { - if (clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) - { - if ("UP".equalsIgnoreCase(action.actionType())) - { - this.activeFilter = Utils.getNextValue(Filter.values(), filter); - - // Update filters. - this.updateFilters(); - this.build(); - } - else if ("DOWN".equalsIgnoreCase(action.actionType())) - { - this.activeFilter = Utils.getPreviousValue(Filter.values(), filter); - - // Update filters. - this.updateFilters(); - this.build(); - } - else if ("SELECT".equalsIgnoreCase(action.actionType())) - { - this.activeFilter = filter; - - // Update filters. - this.updateFilters(); - this.build(); - } - } - } - - return true; - }); - - // Collect tooltips. - List tooltips = activeActions.stream(). - filter(action -> action.tooltip() != null). - map(action -> this.user.getTranslation(this.world, action.tooltip())). - filter(text -> !text.isBlank()). - collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); - - // Add tooltips. - if (!tooltips.isEmpty()) - { - // Empty line and tooltips. - builder.description(""); - builder.description(tooltips); - } - - builder.glow(this.activeFilter == filter); - - return builder.build(); - } - - - // --------------------------------------------------------------------- - // Section: Create common buttons - // --------------------------------------------------------------------- - - - /** - * Create next button panel item. - * - * @param template the template - * @param slot the slot - * @return the panel item - */ - private PanelItem createNextButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) - { - long size = this.materialCountList.size(); - - if (size <= slot.amountMap().getOrDefault("BLOCK", 1) || - 1.0 * size / slot.amountMap().getOrDefault("BLOCK", 1) <= this.pageIndex + 1) - { - // There are no next elements - return null; - } - - int nextPageIndex = this.pageIndex + 2; - - PanelItemBuilder builder = new PanelItemBuilder(); - - if (template.icon() != null) - { - ItemStack clone = template.icon().clone(); - - if (Boolean.TRUE.equals(template.dataMap().getOrDefault("indexing", false))) - { - clone.setAmount(nextPageIndex); - } - - builder.icon(clone); - } - - if (template.title() != null) - { - builder.name(this.user.getTranslation(this.world, template.title())); - } - - if (template.description() != null) - { - builder.description(this.user.getTranslation(this.world, template.description(), - TextVariables.NUMBER, String.valueOf(nextPageIndex))); - } - - // Add ClickHandler - builder.clickHandler((panel, user, clickType, i) -> - { - for (ItemTemplateRecord.ActionRecords action : template.actions()) - { - if ((clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) && - "NEXT".equalsIgnoreCase(action.actionType())) - { - this.pageIndex++; - this.build(); - } - } - - // Always return true. - return true; - }); - - // Collect tooltips. - List tooltips = template.actions().stream(). - filter(action -> action.tooltip() != null). - map(action -> this.user.getTranslation(this.world, action.tooltip())). - filter(text -> !text.isBlank()). - collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); - - // Add tooltips. - if (!tooltips.isEmpty()) - { - // Empty line and tooltips. - builder.description(""); - builder.description(tooltips); - } - - return builder.build(); - } - - - /** - * Create previous button panel item. - * - * @param template the template - * @param slot the slot - * @return the panel item - */ - private PanelItem createPreviousButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) - { - if (this.pageIndex == 0) - { - // There are no next elements - return null; - } - - int previousPageIndex = this.pageIndex; - - PanelItemBuilder builder = new PanelItemBuilder(); - - if (template.icon() != null) - { - ItemStack clone = template.icon().clone(); - - if (Boolean.TRUE.equals(template.dataMap().getOrDefault("indexing", false))) - { - clone.setAmount(previousPageIndex); - } - - builder.icon(clone); - } - - if (template.title() != null) - { - builder.name(this.user.getTranslation(this.world, template.title())); - } - - if (template.description() != null) - { - builder.description(this.user.getTranslation(this.world, template.description(), - TextVariables.NUMBER, String.valueOf(previousPageIndex))); - } - - // Add ClickHandler - builder.clickHandler((panel, user, clickType, i) -> - { - for (ItemTemplateRecord.ActionRecords action : template.actions()) - { - if ((clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) - && "PREVIOUS".equalsIgnoreCase(action.actionType())) - { - this.pageIndex--; - this.build(); - } - } - - // Always return true. - return true; - }); - - // Collect tooltips. - List tooltips = template.actions().stream(). - filter(action -> action.tooltip() != null). - map(action -> this.user.getTranslation(this.world, action.tooltip())). - filter(text -> !text.isBlank()). - collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); - - // Add tooltips. - if (!tooltips.isEmpty()) - { - // Empty line and tooltips. - builder.description(""); - builder.description(tooltips); - } - - return builder.build(); - } - - - // --------------------------------------------------------------------- - // Section: Create Material Button - // --------------------------------------------------------------------- - - - /** - * Create material button panel item. - * - * @param template the template - * @param slot the slot - * @return the panel item - */ - private PanelItem createMaterialButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) - { - if (this.materialCountList.isEmpty()) - { - // Does not contain any generators. - return null; - } - - int index = this.pageIndex * slot.amountMap().getOrDefault("BLOCK", 1) + slot.slot(); - - if (index >= this.materialCountList.size()) - { - // Out of index. - return null; - } - - return this.createMaterialButton(template, this.materialCountList.get(index)); - } - - - /** - * This method creates button for material. - * - * @param template the template of the button - * @param materialCount materialCount which button must be created. - * @return PanelItem for generator tier. - */ - private PanelItem createMaterialButton(ItemTemplateRecord template, - Pair materialCount) - { - PanelItemBuilder builder = new PanelItemBuilder(); - - if (template.icon() != null) - { - builder.icon(template.icon().clone()); - } - else - { - builder.icon(PanelUtils.getMaterialItem(materialCount.getKey())); - } - - if (materialCount.getValue() < 64) - { - builder.amount(materialCount.getValue()); - } - - if (template.title() != null) - { - builder.name(this.user.getTranslation(this.world, template.title(), - TextVariables.NUMBER, String.valueOf(materialCount.getValue()), - "[material]", Utils.prettifyObject(materialCount.getKey(), this.user))); - } - - String description = Utils.prettifyDescription(materialCount.getKey(), this.user); - - final String reference = "level.gui.buttons.material."; - String blockId = this.user.getTranslationOrNothing(reference + "id", - "[id]", materialCount.getKey().name()); - - int blockValue = this.addon.getBlockConfig().getBlockValues().getOrDefault(materialCount.getKey(), 0); - String value = blockValue > 0 ? this.user.getTranslationOrNothing(reference + "value", - TextVariables.NUMBER, String.valueOf(blockValue)) : ""; - - int blockLimit = this.addon.getBlockConfig().getBlockLimits().getOrDefault(materialCount.getKey(), 0); - String limit = blockLimit > 0 ? this.user.getTranslationOrNothing(reference + "limit", - TextVariables.NUMBER, String.valueOf(blockLimit)) : ""; - - String count = this.user.getTranslationOrNothing(reference + "count", - TextVariables.NUMBER, String.valueOf(materialCount.getValue())); - - long calculatedValue = (long) Math.min(blockLimit > 0 ? blockLimit : Integer.MAX_VALUE, materialCount.getValue()) * blockValue; - String valueText = calculatedValue > 0 ? this.user.getTranslationOrNothing(reference + "calculated", - TextVariables.NUMBER, String.valueOf(calculatedValue)) : ""; - - if (template.description() != null) - { - builder.description(this.user.getTranslation(this.world, template.description(), - "[description]", description, - "[id]", blockId, - "[value]", value, - "[calculated]", valueText, - "[limit]", limit, - "[count]", count). - replaceAll("(?m)^[ \\t]*\\r?\\n", ""). - replaceAll("(?> materialCountList; - - /** - * This variable holds current pageIndex for multi-page generator choosing. - */ - private int pageIndex; - - /** - * This variable stores which tab currently is active. - */ - private Tab activeTab; - - /** - * This variable stores active filter for items. - */ - private Filter activeFilter; +public class DetailsPanel { + // --------------------------------------------------------------------- + // Section: Internal Constructor + // --------------------------------------------------------------------- + + /** + * This is internal constructor. It is used internally in current class to avoid + * creating objects everywhere. + * + * @param addon Level object + * @param world World where user is operating + * @param user User who opens panel + */ + private DetailsPanel(Level addon, World world, User user) { + this.addon = addon; + this.world = world; + this.user = user; + + this.island = this.addon.getIslands().getIsland(world, user); + + if (this.island != null) { + this.levelsData = this.addon.getManager().getLevelsData(this.island); + } else { + this.levelsData = null; + } + + // By default no-filters are active. + this.activeTab = Tab.ALL_BLOCKS; + this.activeFilter = Filter.NAME; + this.materialCountList = new ArrayList<>(Material.values().length); + + this.updateFilters(); + } + + /** + * This method builds this GUI. + */ + private void build() { + if (this.island == null || this.levelsData == null) { + // Nothing to see. + Utils.sendMessage(this.user, this.user.getTranslation("general.errors.no-island")); + return; + } + + if (this.levelsData.getMdCount().isEmpty() && this.levelsData.getUwCount().isEmpty()) { + // Nothing to see. + Utils.sendMessage(this.user, this.user.getTranslation("level.conversations.no-data")); + return; + } + + // Start building panel. + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + panelBuilder.user(this.user); + panelBuilder.world(this.user.getWorld()); + + panelBuilder.template("detail_panel", new File(this.addon.getDataFolder(), "panels")); + + panelBuilder.parameters("[name]", this.user.getName()); + + panelBuilder.registerTypeBuilder("NEXT", this::createNextButton); + panelBuilder.registerTypeBuilder("PREVIOUS", this::createPreviousButton); + panelBuilder.registerTypeBuilder("BLOCK", this::createMaterialButton); + + panelBuilder.registerTypeBuilder("FILTER", this::createFilterButton); + + // Register tabs + panelBuilder.registerTypeBuilder("TAB", this::createTabButton); + + // Register unknown type builder. + panelBuilder.build(); + } + + /** + * This method updates filter of elements based on tabs. + */ + private void updateFilters() { + this.materialCountList.clear(); + + switch (this.activeTab) { + case ALL_BLOCKS -> { + Map materialCountMap = new EnumMap<>(Material.class); + + materialCountMap.putAll(this.levelsData.getMdCount()); + + // Add underwater blocks. + this.levelsData.getUwCount().forEach((material, count) -> materialCountMap.put(material, + materialCountMap.computeIfAbsent(material, key -> 0) + count)); + + materialCountMap.entrySet().stream().sorted((Map.Entry.comparingByKey())) + .forEachOrdered(entry -> this.materialCountList.add(new Pair<>(entry.getKey(), entry.getValue()))); + } + case ABOVE_SEA_LEVEL -> this.levelsData.getMdCount().entrySet().stream().sorted((Map.Entry.comparingByKey())) + .forEachOrdered(entry -> this.materialCountList.add(new Pair<>(entry.getKey(), entry.getValue()))); + + case UNDERWATER -> this.levelsData.getUwCount().entrySet().stream().sorted((Map.Entry.comparingByKey())) + .forEachOrdered(entry -> this.materialCountList.add(new Pair<>(entry.getKey(), entry.getValue()))); + + case SPAWNER -> { + int aboveWater = this.levelsData.getMdCount().getOrDefault(Material.SPAWNER, 0); + int underWater = this.levelsData.getUwCount().getOrDefault(Material.SPAWNER, 0); + + // TODO: spawners need some touch... + this.materialCountList.add(new Pair<>(Material.SPAWNER, underWater + aboveWater)); + } + } + + Comparator> sorter; + + switch (this.activeFilter) { + case COUNT -> { + sorter = (o1, o2) -> { + if (o1.getValue().equals(o2.getValue())) { + String o1Name = Utils.prettifyObject(o1.getKey(), this.user); + String o2Name = Utils.prettifyObject(o2.getKey(), this.user); + + return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); + } else { + return Integer.compare(o2.getValue(), o1.getValue()); + } + }; + } + case VALUE -> { + sorter = (o1, o2) -> { + int blockLimit = this.addon.getBlockConfig().getBlockLimits().getOrDefault(o1.getKey(), 0); + int o1Count = blockLimit > 0 ? Math.min(o1.getValue(), blockLimit) : o1.getValue(); + + blockLimit = this.addon.getBlockConfig().getBlockLimits().getOrDefault(o2.getKey(), 0); + int o2Count = blockLimit > 0 ? Math.min(o2.getValue(), blockLimit) : o2.getValue(); + + long o1Value = (long) o1Count + * this.addon.getBlockConfig().getBlockValues().getOrDefault(o1.getKey(), 0); + long o2Value = (long) o2Count + * this.addon.getBlockConfig().getBlockValues().getOrDefault(o2.getKey(), 0); + + if (o1Value == o2Value) { + String o1Name = Utils.prettifyObject(o1.getKey(), this.user); + String o2Name = Utils.prettifyObject(o2.getKey(), this.user); + + return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); + } else { + return Long.compare(o2Value, o1Value); + } + }; + } + default -> { + sorter = (o1, o2) -> { + String o1Name = Utils.prettifyObject(o1.getKey(), this.user); + String o2Name = Utils.prettifyObject(o2.getKey(), this.user); + + return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); + }; + } + } + + this.materialCountList.sort(sorter); + + this.pageIndex = 0; + } + + // --------------------------------------------------------------------- + // Section: Tab Button Type + // --------------------------------------------------------------------- + + /** + * Create tab button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createTabButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) { + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) { + // Set icon + builder.icon(template.icon().clone()); + } + + if (template.title() != null) { + // Set title + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) { + // Set description + builder.description(this.user.getTranslation(this.world, template.description())); + } + + Tab tab = Enums.getIfPresent(Tab.class, String.valueOf(template.dataMap().get("tab"))).or(Tab.ALL_BLOCKS); + + // Get only possible actions, by removing all inactive ones. + List activeActions = new ArrayList<>(template.actions()); + + activeActions.removeIf(action -> "VIEW".equalsIgnoreCase(action.actionType()) && this.activeTab == tab); + + // Add Click handler + builder.clickHandler((panel, user, clickType, i) -> { + for (ItemTemplateRecord.ActionRecords action : activeActions) { + if ((clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) + && "VIEW".equalsIgnoreCase(action.actionType())) { + this.activeTab = tab; + + // Update filters. + this.updateFilters(); + this.build(); + } + } + + return true; + }); + + // Collect tooltips. + List tooltips = activeActions.stream().filter(action -> action.tooltip() != null) + .map(action -> this.user.getTranslation(this.world, action.tooltip())).filter(text -> !text.isBlank()) + .collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + builder.glow(this.activeTab == tab); + + return builder.build(); + } + + /** + * Create next button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createFilterButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) { + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) { + // Set icon + builder.icon(template.icon().clone()); + } + + Filter filter; + + if (slot.amountMap().getOrDefault("FILTER", 0) > 1) { + filter = Enums.getIfPresent(Filter.class, String.valueOf(template.dataMap().get("filter"))).or(Filter.NAME); + } else { + filter = this.activeFilter; + } + + final String reference = "level.gui.buttons.filters."; + + if (template.title() != null) { + // Set title + builder.name(this.user.getTranslation(this.world, + template.title().replace("[filter]", filter.name().toLowerCase()))); + } else { + builder.name(this.user.getTranslation(this.world, reference + filter.name().toLowerCase() + ".name")); + } + + if (template.description() != null) { + // Set description + builder.description(this.user.getTranslation(this.world, + template.description().replace("[filter]", filter.name().toLowerCase()))); + } else { + builder.name( + this.user.getTranslation(this.world, reference + filter.name().toLowerCase() + ".description")); + } + + // Get only possible actions, by removing all inactive ones. + List activeActions = new ArrayList<>(template.actions()); + + // Add Click handler + builder.clickHandler((panel, user, clickType, i) -> { + for (ItemTemplateRecord.ActionRecords action : activeActions) { + if (clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) { + if ("UP".equalsIgnoreCase(action.actionType())) { + this.activeFilter = Utils.getNextValue(Filter.values(), filter); + + // Update filters. + this.updateFilters(); + this.build(); + } else if ("DOWN".equalsIgnoreCase(action.actionType())) { + this.activeFilter = Utils.getPreviousValue(Filter.values(), filter); + + // Update filters. + this.updateFilters(); + this.build(); + } else if ("SELECT".equalsIgnoreCase(action.actionType())) { + this.activeFilter = filter; + + // Update filters. + this.updateFilters(); + this.build(); + } + } + } + + return true; + }); + + // Collect tooltips. + List tooltips = activeActions.stream().filter(action -> action.tooltip() != null) + .map(action -> this.user.getTranslation(this.world, action.tooltip())).filter(text -> !text.isBlank()) + .collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + builder.glow(this.activeFilter == filter); + + return builder.build(); + } + + // --------------------------------------------------------------------- + // Section: Create common buttons + // --------------------------------------------------------------------- + + /** + * Create next button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createNextButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) { + long size = this.materialCountList.size(); + + if (size <= slot.amountMap().getOrDefault("BLOCK", 1) + || 1.0 * size / slot.amountMap().getOrDefault("BLOCK", 1) <= this.pageIndex + 1) { + // There are no next elements + return null; + } + + int nextPageIndex = this.pageIndex + 2; + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) { + ItemStack clone = template.icon().clone(); + + if (Boolean.TRUE.equals(template.dataMap().getOrDefault("indexing", false))) { + clone.setAmount(nextPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) { + builder.description(this.user.getTranslation(this.world, template.description(), TextVariables.NUMBER, + String.valueOf(nextPageIndex))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> { + for (ItemTemplateRecord.ActionRecords action : template.actions()) { + if ((clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) + && "NEXT".equalsIgnoreCase(action.actionType())) { + this.pageIndex++; + this.build(); + } + } + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream().filter(action -> action.tooltip() != null) + .map(action -> this.user.getTranslation(this.world, action.tooltip())).filter(text -> !text.isBlank()) + .collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + /** + * Create previous button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createPreviousButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) { + if (this.pageIndex == 0) { + // There are no next elements + return null; + } + + int previousPageIndex = this.pageIndex; + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) { + ItemStack clone = template.icon().clone(); + + if (Boolean.TRUE.equals(template.dataMap().getOrDefault("indexing", false))) { + clone.setAmount(previousPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) { + builder.description(this.user.getTranslation(this.world, template.description(), TextVariables.NUMBER, + String.valueOf(previousPageIndex))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> { + for (ItemTemplateRecord.ActionRecords action : template.actions()) { + if ((clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) + && "PREVIOUS".equalsIgnoreCase(action.actionType())) { + this.pageIndex--; + this.build(); + } + } + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream().filter(action -> action.tooltip() != null) + .map(action -> this.user.getTranslation(this.world, action.tooltip())).filter(text -> !text.isBlank()) + .collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + // --------------------------------------------------------------------- + // Section: Create Material Button + // --------------------------------------------------------------------- + + /** + * Create material button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createMaterialButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) { + if (this.materialCountList.isEmpty()) { + // Does not contain any generators. + return null; + } + + int index = this.pageIndex * slot.amountMap().getOrDefault("BLOCK", 1) + slot.slot(); + + if (index >= this.materialCountList.size()) { + // Out of index. + return null; + } + + return this.createMaterialButton(template, this.materialCountList.get(index)); + } + + /** + * This method creates button for material. + * + * @param template the template of the button + * @param materialCount materialCount which button must be created. + * @return PanelItem for generator tier. + */ + private PanelItem createMaterialButton(ItemTemplateRecord template, Pair materialCount) { + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) { + builder.icon(template.icon().clone()); + } else { + builder.icon(PanelUtils.getMaterialItem(materialCount.getKey())); + } + + if (materialCount.getValue() < 64) { + builder.amount(materialCount.getValue()); + } + + if (template.title() != null) { + builder.name(this.user.getTranslation(this.world, template.title(), TextVariables.NUMBER, + String.valueOf(materialCount.getValue()), "[material]", + Utils.prettifyObject(materialCount.getKey(), this.user))); + } + + String description = Utils.prettifyDescription(materialCount.getKey(), this.user); + + final String reference = "level.gui.buttons.material."; + String blockId = this.user.getTranslationOrNothing(reference + "id", "[id]", materialCount.getKey().name()); + + int blockValue = this.addon.getBlockConfig().getBlockValues().getOrDefault(materialCount.getKey(), 0); + String value = blockValue > 0 + ? this.user.getTranslationOrNothing(reference + "value", TextVariables.NUMBER, + String.valueOf(blockValue)) + : ""; + + int blockLimit = this.addon.getBlockConfig().getBlockLimits().getOrDefault(materialCount.getKey(), 0); + String limit = blockLimit > 0 + ? this.user.getTranslationOrNothing(reference + "limit", TextVariables.NUMBER, + String.valueOf(blockLimit)) + : ""; + + String count = this.user.getTranslationOrNothing(reference + "count", TextVariables.NUMBER, + String.valueOf(materialCount.getValue())); + + long calculatedValue = (long) Math.min(blockLimit > 0 ? blockLimit : Integer.MAX_VALUE, + materialCount.getValue()) * blockValue; + String valueText = calculatedValue > 0 ? this.user.getTranslationOrNothing(reference + "calculated", + TextVariables.NUMBER, String.valueOf(calculatedValue)) : ""; + + if (template.description() != null) { + builder.description(this.user + .getTranslation(this.world, template.description(), "[description]", description, "[id]", blockId, + "[value]", value, "[calculated]", valueText, "[limit]", limit, "[count]", count) + .replaceAll("(?m)^[ \\t]*\\r?\\n", "").replaceAll("(?> materialCountList; + + /** + * This variable holds current pageIndex for multi-page generator choosing. + */ + private int pageIndex; + + /** + * This variable stores which tab currently is active. + */ + private Tab activeTab; + + /** + * This variable stores active filter for items. + */ + private Filter activeFilter; } diff --git a/src/main/java/world/bentobox/level/panels/TopLevelPanel.java b/src/main/java/world/bentobox/level/panels/TopLevelPanel.java index e60d377..81910b8 100644 --- a/src/main/java/world/bentobox/level/panels/TopLevelPanel.java +++ b/src/main/java/world/bentobox/level/panels/TopLevelPanel.java @@ -5,10 +5,11 @@ package world.bentobox.level.panels; - import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -29,474 +30,390 @@ import world.bentobox.level.Level; import world.bentobox.level.util.Utils; - /** * This panel opens top likes panel */ -public class TopLevelPanel -{ - +public class TopLevelPanel { // --------------------------------------------------------------------- // Section: Internal Constructor // --------------------------------------------------------------------- - /** - * This is internal constructor. It is used internally in current class to avoid creating objects everywhere. + * This is internal constructor. It is used internally in current class to avoid + * creating objects everywhere. * - * @param addon Level object. - * @param user User who opens Panel. - * @param world World where gui is opened + * @param addon Level object. + * @param user User who opens Panel. + * @param world World where gui is opened * @param permissionPrefix Permission Prefix */ - private TopLevelPanel(Level addon, User user, World world, String permissionPrefix) - { - this.addon = addon; - this.user = user; - this.world = world; - - this.iconPermission = permissionPrefix + "level.icon"; - - this.topIslands = this.addon.getManager().getTopTen(this.world, 10).entrySet().stream(). - map(entry -> { - Island island = this.addon.getIslandsManager().getIsland(this.world, entry.getKey()); - return new IslandTopRecord(island, entry.getValue()); - }). - collect(Collectors.toList()); + private TopLevelPanel(Level addon, User user, World world, String permissionPrefix) { + this.addon = addon; + this.user = user; + this.world = world; + + this.iconPermission = permissionPrefix + "level.icon"; + topIslands = new ArrayList<>(); + for (Map.Entry en : addon.getManager().getTopTen(this.world, Level.TEN).entrySet()) { + Optional is = addon.getIslands().getIslandById(en.getKey()); + if (is.isPresent()) { + topIslands.add(new IslandTopRecord(is.get(), en.getValue())); + } + } } - /** - * Build method manages current panel opening. It uses BentoBox PanelAPI that is easy to use and users can get nice - * panels. + * Build method manages current panel opening. It uses BentoBox PanelAPI that is + * easy to use and users can get nice panels. */ - public void build() - { - TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + public void build() { + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); - panelBuilder.user(this.user); - panelBuilder.world(this.world); + panelBuilder.user(this.user); + panelBuilder.world(this.world); - panelBuilder.template("top_panel", new File(this.addon.getDataFolder(), "panels")); + panelBuilder.template("top_panel", new File(this.addon.getDataFolder(), "panels")); - panelBuilder.registerTypeBuilder("VIEW", this::createViewerButton); - panelBuilder.registerTypeBuilder("TOP", this::createPlayerButton); + panelBuilder.registerTypeBuilder("VIEW", this::createViewerButton); + panelBuilder.registerTypeBuilder("TOP", this::createPlayerButton); - // Register unknown type builder. - panelBuilder.build(); + // Register unknown type builder. + panelBuilder.build(); } - // --------------------------------------------------------------------- // Section: Methods // --------------------------------------------------------------------- - /** * Creates fallback based on template. + * * @param template Template record for fallback button. - * @param index Place of the fallback. + * @param index Place of the fallback. * @return Fallback panel item. */ - private PanelItem createFallback(ItemTemplateRecord template, long index) - { - if (template == null) - { - return null; - } - - PanelItemBuilder builder = new PanelItemBuilder(); - - if (template.icon() != null) - { - builder.icon(template.icon().clone()); - } - - if (template.title() != null) - { - builder.name(this.user.getTranslation(this.world, template.title(), - TextVariables.NAME, String.valueOf(index))); - } - else - { - builder.name(this.user.getTranslation(this.world, REFERENCE, - TextVariables.NAME, String.valueOf(index))); - } - - if (template.description() != null) - { - builder.description(this.user.getTranslation(this.world, template.description(), - TextVariables.NUMBER, String.valueOf(index))); - } - - builder.amount(index != 0 ? (int) index : 1); - - return builder.build(); - } + private PanelItem createFallback(ItemTemplateRecord template, long index) { + if (template == null) { + return null; + } + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) { + builder.icon(template.icon().clone()); + } + + if (template.title() != null) { + builder.name( + this.user.getTranslation(this.world, template.title(), TextVariables.NAME, String.valueOf(index))); + } else { + builder.name(this.user.getTranslation(this.world, REFERENCE, TextVariables.NAME, String.valueOf(index))); + } + + if (template.description() != null) { + builder.description(this.user.getTranslation(this.world, template.description(), TextVariables.NUMBER, + String.valueOf(index))); + } + builder.amount(index != 0 ? (int) index : 1); + + return builder.build(); + } /** * This method creates player icon with warp functionality. * * @return PanelItem for PanelBuilder. */ - private PanelItem createPlayerButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot) - { - int index = (int) template.dataMap().getOrDefault("index", 0); + private PanelItem createPlayerButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot) { + int index = (int) template.dataMap().getOrDefault("index", 0); - if (index < 1) - { - return this.createFallback(template.fallback(), index); - } + if (index < 1) { + return this.createFallback(template.fallback(), index); + } - IslandTopRecord islandTopRecord = this.topIslands.size() < index ? null : this.topIslands.get(index - 1); + IslandTopRecord islandTopRecord = this.topIslands.size() < index ? null : this.topIslands.get(index - 1); - if (islandTopRecord == null) - { - return this.createFallback(template.fallback(), index); - } + if (islandTopRecord == null) { + return this.createFallback(template.fallback(), index); + } - return this.createIslandIcon(template, islandTopRecord, index); + return this.createIslandIcon(template, islandTopRecord, index); } - /** * This method creates button from template for given island top record. - * @param template Icon Template. + * + * @param template Icon Template. * @param islandTopRecord Island Top Record. - * @param index Place Index. + * @param index Place Index. * @return PanelItem for PanelBuilder. */ - private PanelItem createIslandIcon(ItemTemplateRecord template, IslandTopRecord islandTopRecord, int index) - { - // Get player island. - Island island = islandTopRecord.island(); - - if (island == null) - { - return this.createFallback(template.fallback(), index); - } - - PanelItemBuilder builder = new PanelItemBuilder(); - - this.populateIslandIcon(builder, template, island); - this.populateIslandTitle(builder, template, island); - this.populateIslandDescription(builder, template, island, islandTopRecord, index); - - builder.amount(index); - - // Get only possible actions, by removing all inactive ones. - List activeActions = new ArrayList<>(template.actions()); - - activeActions.removeIf(action -> - { - switch (action.actionType().toUpperCase()) - { - case "WARP" -> { - return island.getOwner() == null || - this.addon.getWarpHook() == null || - !this.addon.getWarpHook().getWarpSignsManager().hasWarp(this.world, island.getOwner()); - } - case "VISIT" -> { - return island.getOwner() == null || - this.addon.getVisitHook() == null || - !this.addon.getVisitHook().getAddonManager().preprocessTeleportation(this.user, island); - } - case "VIEW" -> { - return island.getOwner() == null || - !island.getMemberSet(RanksManager.MEMBER_RANK).contains(this.user.getUniqueId()); - } - default -> { - return false; - } - } - }); - - // Add Click handler - builder.clickHandler((panel, user, clickType, i) -> - { - for (ItemTemplateRecord.ActionRecords action : activeActions) - { - if (clickType == action.clickType() || action.clickType() == ClickType.UNKNOWN) - { - switch (action.actionType().toUpperCase()) - { - case "WARP" -> { - this.user.closeInventory(); - this.addon.getWarpHook().getWarpSignsManager().warpPlayer(this.world, this.user, island.getOwner()); - } - case "VISIT" -> - // The command call implementation solves necessity to check for all visits options, - // like cool down, confirmation and preprocess in single go. Would it be better to write - // all logic here? - - this.addon.getPlugin().getIWM().getAddon(this.world). - flatMap(GameModeAddon::getPlayerCommand).ifPresent(command -> - { - String mainCommand = - this.addon.getVisitHook().getSettings().getPlayerMainCommand(); - - if (!mainCommand.isBlank()) - { - this.user.closeInventory(); - this.user.performCommand(command.getTopLabel() + " " + mainCommand + " " + island.getOwner()); - } - }); - - case "VIEW" -> { - this.user.closeInventory(); - // Open Detailed GUI. - DetailsPanel.openPanel(this.addon, this.world, this.user); - } - // Catch default - default -> { - this.user.closeInventory(); - addon.logError("Unknown action type " + action.actionType().toUpperCase()); - } - } - } - } - - return true; - }); - - // Collect tooltips. - List tooltips = activeActions.stream(). - filter(action -> action.tooltip() != null). - map(action -> this.user.getTranslation(this.world, action.tooltip())). - filter(text -> !text.isBlank()). - collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); - - // Add tooltips. - if (!tooltips.isEmpty()) - { - // Empty line and tooltips. - builder.description(""); - builder.description(tooltips); - } - - return builder.build(); + private PanelItem createIslandIcon(ItemTemplateRecord template, IslandTopRecord islandTopRecord, int index) { + // Get player island. + Island island = islandTopRecord.island(); + + if (island == null) { + return this.createFallback(template.fallback(), index); + } + + PanelItemBuilder builder = new PanelItemBuilder(); + + this.populateIslandIcon(builder, template, island); + this.populateIslandTitle(builder, template, island); + this.populateIslandDescription(builder, template, island, islandTopRecord, index); + + builder.amount(index); + + // Get only possible actions, by removing all inactive ones. + List activeActions = new ArrayList<>(template.actions()); + + activeActions.removeIf(action -> { + switch (action.actionType().toUpperCase()) { + case "WARP" -> { + return island.getOwner() == null || this.addon.getWarpHook() == null + || !this.addon.getWarpHook().getWarpSignsManager().hasWarp(this.world, island.getOwner()); + } + case "VISIT" -> { + return island.getOwner() == null || this.addon.getVisitHook() == null + || !this.addon.getVisitHook().getAddonManager().preprocessTeleportation(this.user, island, true); + } + case "VIEW" -> { + return island.getOwner() == null + || !island.getMemberSet(RanksManager.MEMBER_RANK).contains(this.user.getUniqueId()); + } + default -> { + return false; + } + } + }); + + // Add Click handler + builder.clickHandler((panel, user, clickType, i) -> { + for (ItemTemplateRecord.ActionRecords action : activeActions) { + if (clickType == action.clickType() || action.clickType() == ClickType.UNKNOWN) { + switch (action.actionType().toUpperCase()) { + case "WARP" -> { + this.user.closeInventory(); + this.addon.getWarpHook().getWarpSignsManager().warpPlayer(this.world, this.user, + island.getOwner()); + } + case "VISIT" -> + // The command call implementation solves necessity to check for all visits + // options, + // like cool down, confirmation and preprocess in single go. Would it be better + // to write + // all logic here? + + this.addon.getPlugin().getIWM().getAddon(this.world).flatMap(GameModeAddon::getPlayerCommand) + .ifPresent(command -> { + String mainCommand = this.addon.getVisitHook().getSettings().getPlayerMainCommand(); + + if (!mainCommand.isBlank()) { + this.user.closeInventory(); + this.user.performCommand( + command.getTopLabel() + " " + mainCommand + " " + island.getOwner()); + } + }); + + case "VIEW" -> { + this.user.closeInventory(); + // Open Detailed GUI. + DetailsPanel.openPanel(this.addon, this.world, this.user); + } + // Catch default + default -> { + this.user.closeInventory(); + addon.logError("Unknown action type " + action.actionType().toUpperCase()); + } + } + } + } + + return true; + }); + + // Collect tooltips. + List tooltips = activeActions.stream().filter(action -> action.tooltip() != null) + .map(action -> this.user.getTranslation(this.world, action.tooltip())).filter(text -> !text.isBlank()) + .collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); } - /** - * Populate given panel item builder name with values from template and island objects. + * Populate given panel item builder name with values from template and island + * objects. * - * @param builder the builder + * @param builder the builder * @param template the template - * @param island the island + * @param island the island */ - private void populateIslandTitle(PanelItemBuilder builder, - ItemTemplateRecord template, - Island island) - { - - // Get Island Name - String nameText; - - if (island.getName() == null || island.getName().isEmpty()) - { - nameText = this.user.getTranslation(REFERENCE + "owners-island", - PLAYER, - island.getOwner() == null ? - this.user.getTranslation(REFERENCE + "unknown") : - this.addon.getPlayers().getName(island.getOwner())); - } - else - { - nameText = island.getName(); - } - - // Template specific title is always more important than custom one. - if (template.title() != null && !template.title().isBlank()) - { - builder.name(this.user.getTranslation(this.world, template.title(), - TextVariables.NAME, nameText)); - } - else - { - builder.name(this.user.getTranslation(REFERENCE + "name", TextVariables.NAME, nameText)); - } + private void populateIslandTitle(PanelItemBuilder builder, ItemTemplateRecord template, Island island) { + + // Get Island Name + String nameText; + + if (island.getName() == null || island.getName().isEmpty()) { + nameText = this.user.getTranslation(REFERENCE + "owners-island", PLAYER, + island.getOwner() == null ? this.user.getTranslation(REFERENCE + "unknown") + : this.addon.getPlayers().getName(island.getOwner())); + } else { + nameText = island.getName(); + } + + // Template specific title is always more important than custom one. + if (template.title() != null && !template.title().isBlank()) { + builder.name(this.user.getTranslation(this.world, template.title(), TextVariables.NAME, nameText)); + } else { + builder.name(this.user.getTranslation(REFERENCE + "name", TextVariables.NAME, nameText)); + } } - /** - * Populate given panel item builder icon with values from template and island objects. + * Populate given panel item builder icon with values from template and island + * objects. * - * @param builder the builder + * @param builder the builder * @param template the template - * @param island the island + * @param island the island */ - private void populateIslandIcon(PanelItemBuilder builder, - ItemTemplateRecord template, - Island island) - { - User owner = island.getOwner() == null ? null : User.getInstance(island.getOwner()); - - // Get permission or island icon - String permissionIcon = owner == null ? null : - Utils.getPermissionValue(owner, this.iconPermission, null); - - Material material; - - if (permissionIcon != null && !permissionIcon.equals("*")) - { - material = Material.matchMaterial(permissionIcon); - } - else - { - material = null; - } - - if (material != null) - { - if (!material.equals(Material.PLAYER_HEAD)) - { - builder.icon(material); - } - else - { - builder.icon(owner.getName()); - } - } - else if (template.icon() != null) - { - builder.icon(template.icon().clone()); - } - else if (owner != null) - { - builder.icon(owner.getName()); - } - else - { - builder.icon(Material.PLAYER_HEAD); - } + private void populateIslandIcon(PanelItemBuilder builder, ItemTemplateRecord template, Island island) { + User owner = island.getOwner() == null ? null : User.getInstance(island.getOwner()); + + // Get permission or island icon + String permissionIcon = owner == null ? null : Utils.getPermissionValue(owner, this.iconPermission, null); + + Material material; + + if (permissionIcon != null && !permissionIcon.equals("*")) { + material = Material.matchMaterial(permissionIcon); + } else { + material = null; + } + + if (material != null) { + if (!material.equals(Material.PLAYER_HEAD)) { + builder.icon(material); + } else { + builder.icon(owner.getName()); + } + } else if (template.icon() != null) { + builder.icon(template.icon().clone()); + } else if (owner != null) { + builder.icon(owner.getName()); + } else { + builder.icon(Material.PLAYER_HEAD); + } } - /** - * Populate given panel item builder description with values from template and island objects. + * Populate given panel item builder description with values from template and + * island objects. * - * @param builder the builder - * @param template the template - * @param island the island + * @param builder the builder + * @param template the template + * @param island the island * @param islandTopRecord the top record object - * @param index place index. + * @param index place index. */ - private void populateIslandDescription(PanelItemBuilder builder, - ItemTemplateRecord template, - Island island, - IslandTopRecord islandTopRecord, - int index) - { - // Get Owner Name - String ownerText = this.user.getTranslation(REFERENCE + "owner", - PLAYER, - island.getOwner() == null ? - this.user.getTranslation(REFERENCE + "unknown") : - this.addon.getPlayers().getName(island.getOwner())); - - // Get Members Text - String memberText; - - if (island.getMemberSet().size() > 1) - { - StringBuilder memberBuilder = new StringBuilder( - this.user.getTranslationOrNothing(REFERENCE + "members-title")); - - for (UUID uuid : island.getMemberSet()) - { - User member = User.getInstance(uuid); - - if (memberBuilder.length() > 0) - { - memberBuilder.append("\n"); - } - - memberBuilder.append( - this.user.getTranslationOrNothing(REFERENCE + "member", - PLAYER, member.getName())); - } - - memberText = memberBuilder.toString(); - } - else - { - memberText = ""; - } - - String placeText = this.user.getTranslation(REFERENCE + "place", - TextVariables.NUMBER, String.valueOf(index)); - - String levelText = this.user.getTranslation(REFERENCE + "level", - TextVariables.NUMBER, this.addon.getManager().formatLevel(islandTopRecord.level())); - - // Template specific description is always more important than custom one. - if (template.description() != null && !template.description().isBlank()) - { - builder.description(this.user.getTranslation(this.world, template.description(), - "[owner]", ownerText, - "[members]", memberText, - "[level]", levelText, - "[place]", placeText). - replaceAll("(?m)^[ \\t]*\\r?\\n", ""). - replaceAll("(? 1) { + StringBuilder memberBuilder = new StringBuilder( + this.user.getTranslationOrNothing(REFERENCE + "members-title")); + + for (UUID uuid : island.getMemberSet()) { + User member = User.getInstance(uuid); + + if (memberBuilder.length() > 0) { + memberBuilder.append("\n"); + } + + memberBuilder.append(this.user.getTranslationOrNothing(REFERENCE + "member", PLAYER, member.getName())); + } + + memberText = memberBuilder.toString(); + } else { + memberText = ""; + } + + String placeText = this.user.getTranslation(REFERENCE + "place", TextVariables.NUMBER, String.valueOf(index)); + + String levelText = this.user.getTranslation(REFERENCE + "level", TextVariables.NUMBER, + this.addon.getManager().formatLevel(islandTopRecord.level())); + + // Template specific description is always more important than custom one. + if (template.description() != null && !template.description().isBlank()) { + builder.description(this.user + .getTranslation(this.world, template.description(), "[owner]", ownerText, "[members]", memberText, + "[level]", levelText, "[place]", placeText) + .replaceAll("(?m)^[ \\t]*\\r?\\n", "").replaceAll("(? level to island -> level. + * + * @param island island + * @param level level */ - private record IslandTopRecord(Island island, Long level) {} - + private record IslandTopRecord(Island island, Long level) { + } // --------------------------------------------------------------------- // Section: Variables diff --git a/src/main/resources/blockconfig.yml b/src/main/resources/blockconfig.yml index d1580ac..3af622c 100644 --- a/src/main/resources/blockconfig.yml +++ b/src/main/resources/blockconfig.yml @@ -287,7 +287,7 @@ blocks: GRANITE_SLAB: 1 GRANITE_STAIRS: 1 GRANITE_WALL: 1 - GRASS: 4 + SHORT_GRASS: 4 GRASS_BLOCK: 4 GRAVEL: 1 GRAY_BANNER: 2 diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 6456818..f71b52d 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -22,8 +22,18 @@ admin: remove: description: "remove player from Top Ten" parameters: "" - - + stats: + description: "show stats on islands on this server" + title: "Server Island Stats" + world: "&a [name]" + no-data: "&c No data to process." + average-level: "Average Island Level: [number]" + median-level: "Median Island Level: [number]" + mode-level: "Mode Island Level: [number]" + highest-level: "Highest Island Level: [number]" + lowest-level: "Lowest Island Level: [number]" + distribution: "Island Level Distribution:" + islands: "islands" island: level: parameters: "[player]" diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml new file mode 100644 index 0000000..14962a0 --- /dev/null +++ b/src/main/resources/locales/uk.yml @@ -0,0 +1,184 @@ +--- +admin: + level: + parameters: "" + description: розрахувати рівень острова для гравця + sethandicap: + parameters: " " + description: встановити гандикап острова, як правило, рівень острова стартера + changed: "&a Початковий гандикап острова змінено з [number] на [new_number]." + invalid-level: "&c Недійсний гандикап. Використовуйте ціле число." + levelstatus: + description: показати, скільки островів у черзі на сканування + islands-in-queue: "&a Острови в черзі: [number]" + top: + description: показати першу десятку списку + unknown-world: "&c Невідомий світ!" + display: "&f[rank]. &a[name] &7- &b[level]" + remove: + description: видалити гравця з першої десятки + parameters: "" + stats: + description: показати статистику островів на цьому сервері + title: Статистика острова сервера + world: "&a [name]" + no-data: "&c Немає даних для обробки." + average-level: 'Середній рівень острова: [number]' + median-level: 'Середній рівень острова: [number]' + mode-level: 'Рівень острова режиму: [number]' + highest-level: 'Найвищий рівень острова: [number]' + lowest-level: 'Найнижчий рівень острова: [number]' + distribution: 'Розподіл на рівні острова:' + islands: острови +island: + level: + parameters: "[player]" + description: обчисліть свій рівень острова або покажіть рівень [player] + calculating: "&a Розрахунок рівня..." + estimated-wait: "&a Приблизне очікування: [number] секунд" + in-queue: "&a Ви номер [number] у черзі" + island-level-is: "&a Рівень острова &b[level]" + required-points-to-next-level: "&a [points] потрібні бали до наступного рівня" + deaths: "&c([number] смерті)" + cooldown: "&c Ви повинні зачекати &b[time] &c секунд, поки ви зможете зробити + це знову" + in-progress: "&6 Розрахунок рівня острова триває..." + time-out: "&c Розрахунок рівня тривав занадто довго. Будь-ласка спробуйте пізніше." + top: + description: показати першу десятку + gui-title: "& Десятка Кращих" + gui-heading: "&6[name]: &B[rank]" + island-level: "&b Рівень [level]" + warp-to: "&A Варп на острів [name]." + level-details: + above-sea-level-blocks: Блоки над рівнем моря + spawners: Спавера + underwater-blocks: Підводні блоки + all-blocks: Всі блоки + no-island: "&c Немає острова!" + names-island: острів [name]. + syntax: "[name] x [number]" + hint: "&c Запустіть рівень, щоб переглянути звіт про блокування" +level: + commands: + value: + parameters: "[hand|]" + description: показує значення блоків. Додайте 'hand' в кінці, щоб відобразити + значення предмета в руках. + gui: + titles: + top: "&0&l Топ островів" + detail-panel: "&0&l острів [name]." + value-panel: "&0&l Значення блоку" + buttons: + island: + empty: "&f&l [name]. місце" + name: "&f&l [name]" + description: |- + [owner] + [members] + [place] + [level] + owners-island: Острів [player]. + owner: "&7&l Власник: &r&b [player]" + members-title: "&7&l Члени:" + member: "&b - [player]" + unknown: невідомий + place: "&7&o [number]. &r&7 місце" + level: "&7 Рівень: &o [number]" + material: + name: "&f&l [number] x [material]" + description: |- + [description] + [count] + [value] + [calculated] + [limit] + [id] + id: "&7 Ідентифікатор блоку: &e [id]" + value: "&7 Значення блоку: &e [number]" + limit: "&7 Обмеження блоку: &e [number]" + count: "&7 Кількість блоків: &e [number]" + calculated: "&7 Розраховане значення: &e [number]" + all_blocks: + name: "&f&l Усі блоки" + description: |- + &7 Показати всі блоки + &7 на острові. + above_sea_level: + name: "&f&l Блоки над рівнем моря" + description: |- + &7 Показувати лише блоки + &7, які знаходяться над морем + &7 рівень. + underwater: + name: "&f&l Блоки під рівнем моря" + description: |- + &7 Показувати лише блоки + &7, які знаходяться нижче моря + &7 рівень. + spawner: + name: "&f&l Спанера" + description: "&7 Відображати лише спавнери." + filters: + name: + name: "&f&l Сортувати за назвою" + description: "&7 Сортувати всі блоки за назвою." + value: + name: "&f&l Сортувати за значенням" + description: "&7 Сортувати всі блоки за їх значенням." + count: + name: "&f&l Сортувати за кількістю" + description: "&7 Відсортуйте всі блоки за їх кількістю." + value: + name: "&f&l [material]" + description: |- + [description] + [value] + [underwater] + [limit] + [id] + id: "&7 Ідентифікатор блоку: &e [id]" + value: "&7 Значення блоку: &e [number]" + underwater: "&7 Нижче рівня моря: &e [number]" + limit: "&7 Обмеження блоку: &e [number]" + previous: + name: "&f&l Попередня сторінка" + description: "&7 Перейти на сторінку [number]." + next: + name: "&f&l Наступна сторінка" + description: "&7 Перейти на сторінку [number]." + search: + name: "&f&l Пошук" + description: |- + &7 Пошук конкретного + &7 значення. + search: "&b Значення: [value]" + tips: + click-to-view: "&e Натисніть &7, щоб переглянути." + click-to-previous: "&e Натисніть &7, щоб переглянути попередню сторінку." + click-to-next: "&e Натисніть &7, щоб переглянути наступну сторінку." + click-to-select: "&e Натисніть &7, щоб вибрати." + left-click-to-cycle-up: "&e Клацніть лівою кнопкою миші &7, щоб перейти вгору." + right-click-to-cycle-down: "&e Клацніть правою кнопкою миші &7, щоб перейти + вниз." + left-click-to-change: "&e Клацніть лівою кнопкою миші &7 для редагування." + right-click-to-clear: "&e Клацніть правою кнопкою миші &7, щоб очистити." + click-to-asc: "&e Клацніть &7, щоб відсортувати в порядку збільшення." + click-to-desc: "&e Клацніть &7, щоб відсортувати в порядку зменшення." + click-to-warp: "&e Натисніть &7, щоб деформувати." + click-to-visit: "&e Натисніть &7, щоб відвідати." + right-click-to-visit: "&e Клацніть правою кнопкою миші &7, щоб відвідати." + conversations: + prefix: "&l&6 [BentoBox]: &r" + no-data: "&c Запустіть рівень, щоб переглянути звіт про блокування." + cancel-string: cancel + exit-string: cancel, exit, quit + write-search: "&e Введіть пошукове значення. (Напишіть 'cancel', щоб вийти)" + search-updated: "&a Значення пошуку оновлено." + cancelled: "&c Розмова скасована!" + no-value: "&c Цей предмет не має цінності." + unknown-item: "&c '[material]' не існує в грі." + value: "&7 Значення '[material]' таке: &e[value]" + value-underwater: "&7 Значення '[material]' нижче рівня моря: &e[value]" + empty-hand: "&c У вашій руці немає блоків" diff --git a/src/main/resources/panels/detail_panel.yml b/src/main/resources/panels/detail_panel.yml index 927a788..da08f6c 100644 --- a/src/main/resources/panels/detail_panel.yml +++ b/src/main/resources/panels/detail_panel.yml @@ -1,25 +1,49 @@ +# Name of panel used for indentification in the code detail_panel: + # Title of the panel shown to the user. This is a reference and the reference will be translatable in the locale file title: level.gui.titles.detail-panel + # The type of panel to show. Options are INVENTORY, HOPPER, DROPPER. INVENTORY is that standard chest inventory and + # the others refer to the inventories shown for those items. type: INVENTORY + # The background of the panel. These items will be shown if other items are not there. STAINED_GLASS_PANEs give a good effect. background: icon: BLACK_STAINED_GLASS_PANE - title: "&b&r" # Empty text + # Each item may have text applied to it, but usually for background items, nothing is shown. + title: "&b&r" # Empty text. This is using the Bukkit chat color coding with &'s. border: + # The border of each panel may be shown as a different item. + # It can be used to provide a contrast to items in the panel. icon: BLACK_STAINED_GLASS_PANE title: "&b&r" # Empty text + # This tag indicates which rows in the panel must be shown. The panel will be sized vertically accordingly. This does not include the borders. + # This can be a list and rows must be between 1 and 6, if used. force-shown: [] + # The content section contains details of each item/button in the panel. The numbers indicate the rows and then then columns of each item. content: + # Row number 1: + # Column number 2: + # Icon is a Bukkit Material. icon: STONE + # Title of the button shown to the user. This is a reference and the reference will be translatable in the locale file title: level.gui.buttons.all_blocks.name + # Description of the button shown to the user in the lore. This is a reference and the reference will be translatable in the locale file description: level.gui.buttons.all_blocks.description + # The data section is a key-value list of data relavent for this button. It is interpreted by the code implemented the panel. + # The convention is to specify the type and the panel tab that will open if pressed. These are Enums in the code. data: + # Type button will go to the ALL_BLOCKS tab when clicked. type: TAB tab: ALL_BLOCKS + # Actions cover what happens if the button is clicked or the mouse is moved over it. There can be multiple actions possible for different + # click-types. actions: + # Each action has an arbitrary descriptive name to define it. view: + # The click-type is the same as the bukkit {@link org.bukkit.event.inventory.ClickType}. UNKNOWN is the default. click-type: unknown + # tooltip is a locale reference that will be translated for the user and shown when they hover over the button. tooltip: level.gui.tips.click-to-view 3: icon: GRASS_BLOCK @@ -57,12 +81,12 @@ detail_panel: 9: # You can create multiple buttons. By default it is one. icon: IRON_TRAPDOOR - # [filter] is placeholder for different filter types. It will be replaced with name, value, count. + # [filter] is a placeholder for different filter types. It will be replaced with name, value, count. title: level.gui.buttons.filters.[filter].name description: level.gui.buttons.filters.[filter].description data: type: FILTER - # the value of filter button. Suggestion is to leave fist value to name if you use single button. + # the value of filter button. Suggestion is to leave first value to name if you use single button. filter: NAME actions: up: @@ -76,6 +100,7 @@ detail_panel: # click-type: unknown # tooltip: level.gui.tips.click-to-select 2: + # If a button is used repeatedly then it can be mentioned by name and then defined in the 'reusable' section 2: material_button 3: material_button 4: material_button @@ -85,6 +110,17 @@ detail_panel: 8: material_button 3: 1: + # In this case, the icon is defined as a TIPPED_ARROW with and enchantment applied. The format for the enchantment is + # define in {@link world.bentobox.bentobox.util.ItemParser} and available for POTIONS or TIPPED_ARROWs. + # Format TIPPED_ARROW:NAME::::QTY + # LEVEL, EXTENDED, SPLASH, LINGER are optional. + # LEVEL is a number, 1 or 2 + # LINGER is for V1.9 servers and later + # Examples: + # TIPPED_ARROW:STRENGTH:1:EXTENDED:SPLASH:1 + # TIPPED_ARROW:INSTANT_DAMAGE:2::LINGER:2 + # TIPPED_ARROW:JUMP:2:NOTEXTENDED:NOSPLASH:1 + # TIPPED_ARROW:WEAKNESS::::1 - any weakness enchantment icon: TIPPED_ARROW:INSTANT_HEAL::::1 title: level.gui.buttons.previous.name description: level.gui.buttons.previous.description @@ -121,8 +157,12 @@ detail_panel: 6: material_button 7: material_button 8: material_button + # This is where reuable buttons are defined. reusable: + # This is the name of the button that is referenced material_button: + # If the icon for a button is not defined, it defaults to AIR and so effectively will not be shown. + # icons are usually not defined if the icon is going to be dynamically set in the panel, e.g. in this case the material will vary #icon: STONE title: level.gui.buttons.material.name description: level.gui.buttons.material.description diff --git a/src/main/resources/panels/top_panel.yml b/src/main/resources/panels/top_panel.yml index 3b80784..05d6151 100644 --- a/src/main/resources/panels/top_panel.yml +++ b/src/main/resources/panels/top_panel.yml @@ -1,15 +1,28 @@ +# Name of panel used for indentification in the code top_panel: + # Title of the panel shown to the user. This is a reference and the reference will be translatable in the locale file title: level.gui.titles.top + # The type of panel to show. Options are INVENTORY, HOPPER, DROPPER. INVENTORY is that standard chest inventory and + # the others refer to the inventories shown for those items. type: INVENTORY + # The background of the panel. These items will be shown if other items are not there. STAINED_GLASS_PANEs give a good effect. background: icon: BLACK_STAINED_GLASS_PANE + # Each item may have text applied to it, but usually for background items, nothing is shown. title: "&b&r" # Empty text border: + # The border of each panel may be shown as a different item. + # It can be used to provide a contrast to items in the panel. icon: BLACK_STAINED_GLASS_PANE title: "&b&r" # Empty text + # This tag indicates which rows in the panel must be shown. The panel will be sized vertically accordingly. This does not include the borders. + # This can be a list and rows must be between 1 and 6, if used. force-shown: [2,3,4,5] + # The content section contains details of each item/button in the panel. The numbers indicate the rows and then then columns of each item. content: + # Row number 2: + # Column number 5: #icon: PLAYER_HEAD title: level.gui.buttons.island.name diff --git a/src/test/java/world/bentobox/level/LevelTest.java b/src/test/java/world/bentobox/level/LevelTest.java index 656c865..5f8074a 100644 --- a/src/test/java/world/bentobox/level/LevelTest.java +++ b/src/test/java/world/bentobox/level/LevelTest.java @@ -72,232 +72,229 @@ */ @SuppressWarnings("deprecation") @RunWith(PowerMockRunner.class) -@PrepareForTest({Bukkit.class, BentoBox.class, User.class}) +@PrepareForTest({ Bukkit.class, BentoBox.class, User.class }) public class LevelTest { - private static File jFile; - @Mock - private User user; - @Mock - private IslandsManager im; - @Mock - private Island island; - @Mock - private BentoBox plugin; - @Mock - private FlagsManager fm; - @Mock - private GameModeAddon gameMode; - @Mock - private AddonsManager am; - @Mock - private BukkitScheduler scheduler; - - @Mock - private Settings pluginSettings; - - private Level addon; - - @Mock - private Logger logger; - @Mock - private PlaceholdersManager phm; - @Mock - private CompositeCommand cmd; - @Mock - private CompositeCommand adminCmd; - @Mock - private World world; - private UUID uuid; - - @Mock - private PluginManager pim; - @Mock - private BlockConfig blockConfig; - - @BeforeClass - public static void beforeClass() throws IOException { - // Make the addon jar - jFile = new File("addon.jar"); - // Copy over config file from src folder - Path fromPath = Paths.get("src/main/resources/config.yml"); - Path path = Paths.get("config.yml"); - Files.copy(fromPath, path); - // Copy over block config file from src folder - fromPath = Paths.get("src/main/resources/blockconfig.yml"); - path = Paths.get("blockconfig.yml"); - Files.copy(fromPath, path); - try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) { - //Added the new files to the jar. - try (FileInputStream fis = new FileInputStream(path.toFile())) { - byte[] buffer = new byte[1024]; - int bytesRead = 0; - JarEntry entry = new JarEntry(path.toString()); - tempJarOutputStream.putNextEntry(entry); - while((bytesRead = fis.read(buffer)) != -1) { - tempJarOutputStream.write(buffer, 0, bytesRead); - } - } - } - } - - /** - * @throws java.lang.Exception - */ - @Before - public void setUp() throws Exception { - // Set up plugin - Whitebox.setInternalState(BentoBox.class, "instance", plugin); - when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger()); - - // The database type has to be created one line before the thenReturn() to work! - DatabaseType value = DatabaseType.JSON; - when(plugin.getSettings()).thenReturn(pluginSettings); - when(pluginSettings.getDatabaseType()).thenReturn(value); - - //when(plugin.isEnabled()).thenReturn(true); - // Command manager - CommandsManager cm = mock(CommandsManager.class); - when(plugin.getCommandsManager()).thenReturn(cm); - - // Player - Player p = mock(Player.class); - // Sometimes use Mockito.withSettings().verboseLogging() - when(user.isOp()).thenReturn(false); - uuid = UUID.randomUUID(); - when(user.getUniqueId()).thenReturn(uuid); - when(user.getPlayer()).thenReturn(p); - when(user.getName()).thenReturn("tastybento"); - User.setPlugin(plugin); - - // Island World Manager - IslandWorldManager iwm = mock(IslandWorldManager.class); - when(plugin.getIWM()).thenReturn(iwm); - - - - // Player has island to begin with - when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island); - when(plugin.getIslands()).thenReturn(im); - - // Locales - // Return the reference (USE THIS IN THE FUTURE) - when(user.getTranslation(Mockito.anyString())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); - - // Server - PowerMockito.mockStatic(Bukkit.class); - Server server = mock(Server.class); - when(Bukkit.getServer()).thenReturn(server); - when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger()); - when(Bukkit.getPluginManager()).thenReturn(mock(PluginManager.class)); - - // Addon - addon = new Level(); - File dataFolder = new File("addons/Level"); - addon.setDataFolder(dataFolder); - addon.setFile(jFile); - AddonDescription desc = new AddonDescription.Builder("bentobox", "Level", "1.3").description("test").authors("tastybento").build(); - addon.setDescription(desc); - addon.setSettings(new ConfigSettings()); - // Addons manager - when(plugin.getAddonsManager()).thenReturn(am); - // One game mode - when(am.getGameModeAddons()).thenReturn(Collections.singletonList(gameMode)); - AddonDescription desc2 = new AddonDescription.Builder("bentobox", "BSkyBlock", "1.3").description("test").authors("tasty").build(); - when(gameMode.getDescription()).thenReturn(desc2); - when(gameMode.getOverWorld()).thenReturn(world); - - // Player command - @NonNull - Optional opCmd = Optional.of(cmd); - when(gameMode.getPlayerCommand()).thenReturn(opCmd); - // Admin command - Optional opAdminCmd = Optional.of(adminCmd); - when(gameMode.getAdminCommand()).thenReturn(opAdminCmd); - - // Flags manager - when(plugin.getFlagsManager()).thenReturn(fm); - when(fm.getFlags()).thenReturn(Collections.emptyList()); - - - // Bukkit - PowerMockito.mockStatic(Bukkit.class); - when(Bukkit.getScheduler()).thenReturn(scheduler); - ItemMeta meta = mock(ItemMeta.class); - ItemFactory itemFactory = mock(ItemFactory.class); - when(itemFactory.getItemMeta(any())).thenReturn(meta); - when(Bukkit.getItemFactory()).thenReturn(itemFactory); - UnsafeValues unsafe = mock(UnsafeValues.class); - when(unsafe.getDataVersion()).thenReturn(777); - when(Bukkit.getUnsafe()).thenReturn(unsafe); - when(Bukkit.getPluginManager()).thenReturn(pim); - - // placeholders - when(plugin.getPlaceholdersManager()).thenReturn(phm); - - // World - when(world.getName()).thenReturn("bskyblock-world"); - // Island - when(island.getWorld()).thenReturn(world); - when(island.getOwner()).thenReturn(uuid); - } - - /** - * @throws java.lang.Exception - */ - @After - public void tearDown() throws Exception { - deleteAll(new File("database")); - } - - @AfterClass - public static void cleanUp() throws Exception { - new File("addon.jar").delete(); - new File("config.yml").delete(); - new File("blockconfig.yml").delete(); - deleteAll(new File("addons")); - } - - private static void deleteAll(File file) throws IOException { - if (file.exists()) { - Files.walk(file.toPath()) - .sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); - } - } - - /** - * Test method for {@link world.bentobox.level.Level#onEnable()}. - */ - @Test - public void testOnEnable() { - addon.onEnable(); - verify(plugin).logWarning("[Level] Level Addon: No such world in blockconfig.yml : acidisland_world"); - verify(plugin).log("[Level] Level hooking into BSkyBlock"); - verify(cmd, times(3)).getAddon(); // 3 commands - verify(adminCmd, times(4)).getAddon(); // Four commands - // Placeholders - verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_island_level"), any()); - verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_visited_island_level"), any()); - verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_points_to_next_level"), any()); - for (int i = 1; i < 11; i++) { - verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_top_name_" + i), any()); - verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_top_value_" + i), any()); - } - // Commands - verify(am).registerListener(eq(addon), any(IslandActivitiesListeners.class)); - verify(am).registerListener(eq(addon), any(JoinLeaveListener.class)); - } - - /** - * Test method for {@link world.bentobox.level.Level#getSettings()}. - */ - @Test - public void testGetSettings() { - addon.onEnable(); - ConfigSettings s = addon.getSettings(); - assertEquals(100, s.getLevelCost()); - } + private static File jFile; + @Mock + private User user; + @Mock + private IslandsManager im; + @Mock + private Island island; + @Mock + private BentoBox plugin; + @Mock + private FlagsManager fm; + @Mock + private GameModeAddon gameMode; + @Mock + private AddonsManager am; + @Mock + private BukkitScheduler scheduler; + + @Mock + private Settings pluginSettings; + + private Level addon; + + @Mock + private Logger logger; + @Mock + private PlaceholdersManager phm; + @Mock + private CompositeCommand cmd; + @Mock + private CompositeCommand adminCmd; + @Mock + private World world; + private UUID uuid; + + @Mock + private PluginManager pim; + @Mock + private BlockConfig blockConfig; + + @BeforeClass + public static void beforeClass() throws IOException { + // Make the addon jar + jFile = new File("addon.jar"); + // Copy over config file from src folder + Path fromPath = Paths.get("src/main/resources/config.yml"); + Path path = Paths.get("config.yml"); + Files.copy(fromPath, path); + // Copy over block config file from src folder + fromPath = Paths.get("src/main/resources/blockconfig.yml"); + path = Paths.get("blockconfig.yml"); + Files.copy(fromPath, path); + try (JarOutputStream tempJarOutputStream = new JarOutputStream(new FileOutputStream(jFile))) { + // Added the new files to the jar. + try (FileInputStream fis = new FileInputStream(path.toFile())) { + byte[] buffer = new byte[1024]; + int bytesRead = 0; + JarEntry entry = new JarEntry(path.toString()); + tempJarOutputStream.putNextEntry(entry); + while ((bytesRead = fis.read(buffer)) != -1) { + tempJarOutputStream.write(buffer, 0, bytesRead); + } + } + } + } + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + // Set up plugin + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger()); + + // The database type has to be created one line before the thenReturn() to work! + DatabaseType value = DatabaseType.JSON; + when(plugin.getSettings()).thenReturn(pluginSettings); + when(pluginSettings.getDatabaseType()).thenReturn(value); + + // when(plugin.isEnabled()).thenReturn(true); + // Command manager + CommandsManager cm = mock(CommandsManager.class); + when(plugin.getCommandsManager()).thenReturn(cm); + + // Player + Player p = mock(Player.class); + // Sometimes use Mockito.withSettings().verboseLogging() + when(user.isOp()).thenReturn(false); + uuid = UUID.randomUUID(); + when(user.getUniqueId()).thenReturn(uuid); + when(user.getPlayer()).thenReturn(p); + when(user.getName()).thenReturn("tastybento"); + User.setPlugin(plugin); + + // Island World Manager + IslandWorldManager iwm = mock(IslandWorldManager.class); + when(plugin.getIWM()).thenReturn(iwm); + + // Player has island to begin with + when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island); + when(plugin.getIslands()).thenReturn(im); + + // Locales + // Return the reference (USE THIS IN THE FUTURE) + when(user.getTranslation(Mockito.anyString())) + .thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + + // Server + PowerMockito.mockStatic(Bukkit.class); + Server server = mock(Server.class); + when(Bukkit.getServer()).thenReturn(server); + when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger()); + when(Bukkit.getPluginManager()).thenReturn(mock(PluginManager.class)); + + // Addon + addon = new Level(); + File dataFolder = new File("addons/Level"); + addon.setDataFolder(dataFolder); + addon.setFile(jFile); + AddonDescription desc = new AddonDescription.Builder("bentobox", "Level", "1.3").description("test") + .authors("tastybento").build(); + addon.setDescription(desc); + addon.setSettings(new ConfigSettings()); + // Addons manager + when(plugin.getAddonsManager()).thenReturn(am); + // One game mode + when(am.getGameModeAddons()).thenReturn(Collections.singletonList(gameMode)); + AddonDescription desc2 = new AddonDescription.Builder("bentobox", "BSkyBlock", "1.3").description("test") + .authors("tasty").build(); + when(gameMode.getDescription()).thenReturn(desc2); + when(gameMode.getOverWorld()).thenReturn(world); + + // Player command + @NonNull + Optional opCmd = Optional.of(cmd); + when(gameMode.getPlayerCommand()).thenReturn(opCmd); + // Admin command + Optional opAdminCmd = Optional.of(adminCmd); + when(gameMode.getAdminCommand()).thenReturn(opAdminCmd); + + // Flags manager + when(plugin.getFlagsManager()).thenReturn(fm); + when(fm.getFlags()).thenReturn(Collections.emptyList()); + + // Bukkit + PowerMockito.mockStatic(Bukkit.class); + when(Bukkit.getScheduler()).thenReturn(scheduler); + ItemMeta meta = mock(ItemMeta.class); + ItemFactory itemFactory = mock(ItemFactory.class); + when(itemFactory.getItemMeta(any())).thenReturn(meta); + when(Bukkit.getItemFactory()).thenReturn(itemFactory); + UnsafeValues unsafe = mock(UnsafeValues.class); + when(unsafe.getDataVersion()).thenReturn(777); + when(Bukkit.getUnsafe()).thenReturn(unsafe); + when(Bukkit.getPluginManager()).thenReturn(pim); + + // placeholders + when(plugin.getPlaceholdersManager()).thenReturn(phm); + + // World + when(world.getName()).thenReturn("bskyblock-world"); + // Island + when(island.getWorld()).thenReturn(world); + when(island.getOwner()).thenReturn(uuid); + } + + /** + * @throws java.lang.Exception + */ + @After + public void tearDown() throws Exception { + deleteAll(new File("database")); + } + + @AfterClass + public static void cleanUp() throws Exception { + new File("addon.jar").delete(); + new File("config.yml").delete(); + new File("blockconfig.yml").delete(); + deleteAll(new File("addons")); + } + + private static void deleteAll(File file) throws IOException { + if (file.exists()) { + Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } + + /** + * Test method for {@link world.bentobox.level.Level#onEnable()}. + */ + @Test + public void testOnEnable() { + addon.onEnable(); + verify(plugin).logWarning("[Level] Level Addon: No such world in blockconfig.yml : acidisland_world"); + verify(plugin).log("[Level] Level hooking into BSkyBlock"); + verify(cmd, times(3)).getAddon(); // 3 commands + verify(adminCmd, times(5)).getAddon(); // Five commands + // Placeholders + verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_island_level"), any()); + verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_visited_island_level"), any()); + verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_points_to_next_level"), any()); + for (int i = 1; i < 11; i++) { + verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_top_name_" + i), any()); + verify(phm).registerPlaceholder(eq(addon), eq("bskyblock_top_value_" + i), any()); + } + // Commands + verify(am).registerListener(eq(addon), any(IslandActivitiesListeners.class)); + verify(am).registerListener(eq(addon), any(JoinLeaveListener.class)); + } + + /** + * Test method for {@link world.bentobox.level.Level#getSettings()}. + */ + @Test + public void testGetSettings() { + addon.onEnable(); + ConfigSettings s = addon.getSettings(); + assertEquals(100, s.getLevelCost()); + } } diff --git a/src/test/java/world/bentobox/level/LevelsManagerTest.java b/src/test/java/world/bentobox/level/LevelsManagerTest.java index 5b18aa5..551c573 100644 --- a/src/test/java/world/bentobox/level/LevelsManagerTest.java +++ b/src/test/java/world/bentobox/level/LevelsManagerTest.java @@ -70,7 +70,7 @@ * */ @RunWith(PowerMockRunner.class) -@PrepareForTest({Bukkit.class, BentoBox.class, DatabaseSetup.class, PanelBuilder.class}) +@PrepareForTest({ Bukkit.class, BentoBox.class, DatabaseSetup.class, PanelBuilder.class }) public class LevelsManagerTest { @Mock @@ -82,7 +82,6 @@ public class LevelsManagerTest { @Mock private Settings pluginSettings; - // Class under test private LevelsManager lm; @Mock @@ -114,18 +113,17 @@ public class LevelsManagerTest { @Mock private BukkitScheduler scheduler; - - @SuppressWarnings("unchecked") @BeforeClass public static void beforeClass() { - // This has to be done beforeClass otherwise the tests will interfere with each other - handler = mock(AbstractDatabaseHandler.class); - // Database - PowerMockito.mockStatic(DatabaseSetup.class); - DatabaseSetup dbSetup = mock(DatabaseSetup.class); - when(DatabaseSetup.getDatabase()).thenReturn(dbSetup); - when(dbSetup.getHandler(any())).thenReturn(handler); + // This has to be done beforeClass otherwise the tests will interfere with each + // other + handler = mock(AbstractDatabaseHandler.class); + // Database + PowerMockito.mockStatic(DatabaseSetup.class); + DatabaseSetup dbSetup = mock(DatabaseSetup.class); + when(DatabaseSetup.getDatabase()).thenReturn(dbSetup); + when(dbSetup.getHandler(any())).thenReturn(handler); } /** @@ -162,10 +160,9 @@ public void setUp() throws Exception { when(island.getMemberSet()).thenReturn(iset); when(island.getOwner()).thenReturn(uuid); when(island.getWorld()).thenReturn(world); - when(island.getUniqueId()).thenReturn(UUID.randomUUID().toString()); + when(island.getUniqueId()).thenReturn(uuid.toString()); // Default to uuid's being island owners - when(im.isOwner(eq(world), any())).thenReturn(true); - when(im.getOwner(any(), any(UUID.class))).thenAnswer(in -> in.getArgument(1, UUID.class)); + when(im.hasIsland(eq(world), any(UUID.class))).thenReturn(true); when(im.getIsland(world, uuid)).thenReturn(island); when(im.getIslandById(anyString())).thenReturn(Optional.of(island)); @@ -240,89 +237,92 @@ public void setUp() throws Exception { */ @After public void tearDown() throws Exception { - deleteAll(new File("database")); - User.clearUsers(); - Mockito.framework().clearInlineMocks(); + deleteAll(new File("database")); + User.clearUsers(); + Mockito.framework().clearInlineMocks(); } private static void deleteAll(File file) throws IOException { - if (file.exists()) { - Files.walk(file.toPath()) - .sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); - } + if (file.exists()) { + Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } } /** - * Test method for {@link world.bentobox.level.LevelsManager#calculateLevel(UUID, world.bentobox.bentobox.database.objects.Island)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#calculateLevel(UUID, world.bentobox.bentobox.database.objects.Island)}. */ @Test public void testCalculateLevel() { - Results results = new Results(); - results.setLevel(10000); - results.setInitialLevel(3); - lm.calculateLevel(uuid, island); - // Complete the pipelined completable future - cf.complete(results); - - assertEquals(10000L, lm.getLevelsData(island).getLevel()); - //Map tt = lm.getTopTen(world, 10); - //assertEquals(1, tt.size()); - //assertTrue(tt.get(uuid) == 10000); - assertEquals(10000L, lm.getIslandMaxLevel(world, uuid)); - - results.setLevel(5000); - lm.calculateLevel(uuid, island); - // Complete the pipelined completable future - cf.complete(results); - assertEquals(5000L, lm.getLevelsData(island).getLevel()); - // Still should be 10000 - assertEquals(10000L, lm.getIslandMaxLevel(world, uuid)); + Results results = new Results(); + results.setLevel(10000); + results.setInitialLevel(3); + lm.calculateLevel(uuid, island); + // Complete the pipelined completable future + cf.complete(results); + + assertEquals(10000L, lm.getLevelsData(island).getLevel()); + // Map tt = lm.getTopTen(world, 10); + // assertEquals(1, tt.size()); + // assertTrue(tt.get(uuid) == 10000); + assertEquals(10000L, lm.getIslandMaxLevel(world, uuid)); + + results.setLevel(5000); + lm.calculateLevel(uuid, island); + // Complete the pipelined completable future + cf.complete(results); + assertEquals(5000L, lm.getLevelsData(island).getLevel()); + // Still should be 10000 + assertEquals(10000L, lm.getIslandMaxLevel(world, uuid)); } /** - * Test method for {@link world.bentobox.level.LevelsManager#getInitialLevel(world.bentobox.bentobox.database.objects.Island)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#getInitialLevel(world.bentobox.bentobox.database.objects.Island)}. */ @Test public void testGetInitialLevel() { - assertEquals(0,lm.getInitialLevel(island)); + assertEquals(0, lm.getInitialLevel(island)); } /** - * Test method for {@link world.bentobox.level.LevelsManager#getIslandLevel(org.bukkit.World, java.util.UUID)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#getIslandLevel(org.bukkit.World, java.util.UUID)}. */ @Test public void testGetIslandLevel() { - assertEquals(-5, lm.getIslandLevel(world, uuid)); + assertEquals(-5, lm.getIslandLevel(world, uuid)); } /** - * Test method for {@link world.bentobox.level.LevelsManager#getPointsToNextString(org.bukkit.World, java.util.UUID)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#getPointsToNextString(org.bukkit.World, java.util.UUID)}. */ @Test public void testGetPointsToNextString() { - // No island player - assertEquals("", lm.getPointsToNextString(world, UUID.randomUUID())); - // Player has island - assertEquals("0", lm.getPointsToNextString(world, uuid)); + // No island player + assertEquals("", lm.getPointsToNextString(world, UUID.randomUUID())); + // Player has island + assertEquals("0", lm.getPointsToNextString(world, uuid)); } /** - * Test method for {@link world.bentobox.level.LevelsManager#getIslandLevelString(org.bukkit.World, java.util.UUID)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#getIslandLevelString(org.bukkit.World, java.util.UUID)}. */ @Test public void testGetIslandLevelString() { - assertEquals("-5", lm.getIslandLevelString(world, uuid)); + assertEquals("-5", lm.getIslandLevelString(world, uuid)); } /** - * Test method for {@link world.bentobox.level.LevelsManager#getLevelsData(java.util.UUID)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#getLevelsData(java.util.UUID)}. */ @Test public void testGetLevelsData() { - assertEquals(levelsData, lm.getLevelsData(island)); + assertEquals(levelsData, lm.getLevelsData(island)); } @@ -331,54 +331,59 @@ public void testGetLevelsData() { */ @Test public void testFormatLevel() { - assertEquals("123456789", lm.formatLevel(123456789L)); - when(settings.isShorthand()).thenReturn(true); - assertEquals("123.5M", lm.formatLevel(123456789L)); - assertEquals("1.2k", lm.formatLevel(1234L)); - assertEquals("123.5G", lm.formatLevel(123456789352L)); - assertEquals("1.2T", lm.formatLevel(1234567893524L)); - assertEquals("12345.7T", lm.formatLevel(12345678345345349L)); + assertEquals("123456789", lm.formatLevel(123456789L)); + when(settings.isShorthand()).thenReturn(true); + assertEquals("123.5M", lm.formatLevel(123456789L)); + assertEquals("1.2k", lm.formatLevel(1234L)); + assertEquals("123.5G", lm.formatLevel(123456789352L)); + assertEquals("1.2T", lm.formatLevel(1234567893524L)); + assertEquals("12345.7T", lm.formatLevel(12345678345345349L)); } /** - * Test method for {@link world.bentobox.level.LevelsManager#getTopTen(org.bukkit.World, int)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#getTopTen(org.bukkit.World, int)}. */ @Test public void testGetTopTenEmpty() { - Map tt = lm.getTopTen(world, Level.TEN); - assertTrue(tt.isEmpty()); + Map tt = lm.getTopTen(world, Level.TEN); + assertTrue(tt.isEmpty()); } /** - * Test method for {@link world.bentobox.level.LevelsManager#getTopTen(org.bukkit.World, int)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#getTopTen(org.bukkit.World, int)}. */ @Test public void testGetTopTen() { - testLoadTopTens(); - Map tt = lm.getTopTen(world, Level.TEN); - assertFalse(tt.isEmpty()); - assertEquals(1, tt.size()); - assertEquals(1, lm.getTopTen(world, 1).size()); + testLoadTopTens(); + Map tt = lm.getTopTen(world, Level.TEN); + assertFalse(tt.isEmpty()); + assertEquals(1, tt.size()); + assertEquals(1, lm.getTopTen(world, 1).size()); } /** - * Test method for {@link world.bentobox.level.LevelsManager#getTopTen(org.bukkit.World, int)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#getWeightedTopTen(org.bukkit.World, int)}. */ @Test - public void testGetTopTenNoOwners() { - when(im.isOwner(eq(world), any())).thenReturn(false); - testLoadTopTens(); - Map tt = lm.getTopTen(world, Level.TEN); - assertTrue(tt.isEmpty()); + public void testGetWeightedTopTen() { + testLoadTopTens(); + Map tt = lm.getWeightedTopTen(world, Level.TEN); + assertFalse(tt.isEmpty()); + assertEquals(1, tt.size()); + assertEquals(1, lm.getTopTen(world, 1).size()); } /** - * Test method for {@link world.bentobox.level.LevelsManager#hasTopTenPerm(org.bukkit.World, java.util.UUID)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#hasTopTenPerm(org.bukkit.World, java.util.UUID)}. */ @Test public void testHasTopTenPerm() { - assertTrue(lm.hasTopTenPerm(world, uuid)); + assertTrue(lm.hasTopTenPerm(world, uuid)); } /** @@ -386,69 +391,72 @@ public void testHasTopTenPerm() { */ @Test public void testLoadTopTens() { - ArgumentCaptor task = ArgumentCaptor.forClass(Runnable.class); - lm.loadTopTens(); - PowerMockito.verifyStatic(Bukkit.class); // 1 - Bukkit.getScheduler(); - verify(scheduler).runTaskAsynchronously(eq(plugin), task.capture()); - task.getValue().run(); - verify(addon).log("Generating rankings"); - verify(addon).log("Generated rankings for bskyblock-world"); + ArgumentCaptor task = ArgumentCaptor.forClass(Runnable.class); + lm.loadTopTens(); + PowerMockito.verifyStatic(Bukkit.class); // 1 + Bukkit.getScheduler(); + verify(scheduler).runTaskAsynchronously(eq(plugin), task.capture()); + task.getValue().run(); + verify(addon).log("Generating rankings"); + verify(addon).log("Generated rankings for bskyblock-world"); } /** - * Test method for {@link world.bentobox.level.LevelsManager#removeEntry(org.bukkit.World, java.util.UUID)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#removeEntry(org.bukkit.World, java.util.UUID)}. */ @Test public void testRemoveEntry() { - testLoadTopTens(); - Map tt = lm.getTopTen(world, Level.TEN); - assertTrue(tt.containsKey(uuid)); - lm.removeEntry(world, uuid); - tt = lm.getTopTen(world, Level.TEN); - assertFalse(tt.containsKey(uuid)); + testLoadTopTens(); + Map tt = lm.getTopTen(world, Level.TEN); + assertTrue(tt.containsKey(uuid.toString())); + lm.removeEntry(world, uuid.toString()); + tt = lm.getTopTen(world, Level.TEN); + assertFalse(tt.containsKey(uuid.toString())); } /** - * Test method for {@link world.bentobox.level.LevelsManager#setInitialIslandLevel(world.bentobox.bentobox.database.objects.Island, long)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#setInitialIslandLevel(world.bentobox.bentobox.database.objects.Island, long)}. */ @Test public void testSetInitialIslandLevel() { - lm.setInitialIslandLevel(island, Level.TEN); - assertEquals(Level.TEN, lm.getInitialLevel(island)); + lm.setInitialIslandLevel(island, Level.TEN); + assertEquals(Level.TEN, lm.getInitialLevel(island)); } /** - * Test method for {@link world.bentobox.level.LevelsManager#setIslandLevel(org.bukkit.World, java.util.UUID, long)}. + * Test method for + * {@link world.bentobox.level.LevelsManager#setIslandLevel(org.bukkit.World, java.util.UUID, long)}. */ @Test public void testSetIslandLevel() { - lm.setIslandLevel(world, uuid, 1234); - assertEquals(1234, lm.getIslandLevel(world, uuid)); + lm.setIslandLevel(world, uuid, 1234); + assertEquals(1234, lm.getIslandLevel(world, uuid)); } - /** - * Test method for {@link world.bentobox.level.LevelsManager#getRank(World, UUID)} + * Test method for + * {@link world.bentobox.level.LevelsManager#getRank(World, UUID)} */ @Test public void testGetRank() { - lm.createAndCleanRankings(world); - Map ttl = lm.getTopTenLists(); - Map tt = ttl.get(world).getTopTen(); - for (long i = 100; i < 150; i++) { - tt.put(UUID.randomUUID(), i); - } - // Put player as lowest rank - tt.put(uuid, 10L); - assertEquals(51, lm.getRank(world, uuid)); - // Put player as highest rank - tt.put(uuid, 1000L); - assertEquals(1, lm.getRank(world, uuid)); - // Unknown UUID - lowest rank + 1 - assertEquals(52, lm.getRank(world, UUID.randomUUID())); + lm.createAndCleanRankings(world); + Map ttl = lm.getTopTenLists(); + Map tt = ttl.get(world).getTopTen(); + for (long i = 100; i < 150; i++) { + tt.put(UUID.randomUUID().toString(), i); + } + // Put island as lowest rank + tt.put(uuid.toString(), 10L); + assertEquals(51, lm.getRank(world, uuid)); + // Put island as highest rank + tt.put(uuid.toString(), 1000L); + assertEquals(1, lm.getRank(world, uuid)); + // Unknown UUID - lowest rank + 1 + assertEquals(52, lm.getRank(world, UUID.randomUUID())); } } diff --git a/src/test/java/world/bentobox/level/PlaceholderManagerTest.java b/src/test/java/world/bentobox/level/PlaceholderManagerTest.java index b780b8e..56f0a1d 100644 --- a/src/test/java/world/bentobox/level/PlaceholderManagerTest.java +++ b/src/test/java/world/bentobox/level/PlaceholderManagerTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -10,9 +11,10 @@ import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -44,7 +46,7 @@ * */ @RunWith(PowerMockRunner.class) -@PrepareForTest({BentoBox.class}) +@PrepareForTest({ BentoBox.class }) public class PlaceholderManagerTest { @Mock @@ -54,7 +56,7 @@ public class PlaceholderManagerTest { @Mock private BentoBox plugin; - private PlaceholderManager pm; + private PlaceholderManager phm; @Mock private PlaceholdersManager bpm; @Mock @@ -67,13 +69,25 @@ public class PlaceholderManagerTest { private Island island; @Mock private User user; - private Map names = new HashMap<>(); - private static final List NAMES = List.of("tasty", "bento", "fred", "bonne", "cyprien", "mael", "joe", "horacio", "steph", "vicky"); - private Map islands = new HashMap<>(); - private Map map = new HashMap<>(); + private static final Map names = new LinkedHashMap<>(); + static { + names.put(UUID.randomUUID(), "tasty"); + names.put(UUID.randomUUID(), "bento"); + names.put(UUID.randomUUID(), "fred"); + names.put(UUID.randomUUID(), "bonne"); + names.put(UUID.randomUUID(), "cyprien"); + names.put(UUID.randomUUID(), "mael"); + names.put(UUID.randomUUID(), "joe"); + names.put(UUID.randomUUID(), "horacio"); + names.put(UUID.randomUUID(), "steph"); + names.put(UUID.randomUUID(), "vicky"); + } + private Map islands = new HashMap<>(); + private Map map = new LinkedHashMap<>(); + private Map map2 = new LinkedHashMap<>(); private @NonNull IslandLevels data; @Mock - private PlayersManager players; + private PlayersManager pm; /** * @throws java.lang.Exception @@ -83,29 +97,32 @@ public void setUp() throws Exception { when(addon.getPlugin()).thenReturn(plugin); // Users - when(addon.getPlayers()).thenReturn(players); + when(addon.getPlayers()).thenReturn(pm); + // Users when(user.getWorld()).thenReturn(world); when(user.getLocation()).thenReturn(mock(Location.class)); - for (int i = 0; i < Level.TEN; i++) { - UUID uuid = UUID.randomUUID(); - names.put(uuid, NAMES.get(i)); - map.put(uuid, (long)(100 - i)); + int i = 0; + for (Entry n : names.entrySet()) { + UUID uuid = UUID.randomUUID(); // Random island ID + Long value = (long)(100 - i++); + map.put(uuid.toString(), value); // level Island is = new Island(); - is.setOwner(uuid); - is.setName(NAMES.get(i) + "'s island"); - islands.put(uuid, is); - + is.setUniqueId(uuid.toString()); + is.setOwner(n.getKey()); + is.setName(n.getValue() + "'s island"); + islands.put(uuid.toString(), is); + map2.put(is, value); } // Sort map = map.entrySet().stream() .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); - when(players.getName(any())).thenAnswer((Answer) invocation -> names.getOrDefault(invocation.getArgument(0, UUID.class), "unknown")); + when(pm.getName(any())).thenAnswer((Answer) invocation -> names.getOrDefault(invocation.getArgument(0, UUID.class), "unknown")); Map members = new HashMap<>(); - map.forEach((uuid, l) -> members.put(uuid, RanksManager.MEMBER_RANK)); - islands.values().forEach(i -> i.setMembers(members)); + names.forEach((uuid, l) -> members.put(uuid, RanksManager.MEMBER_RANK)); + islands.values().forEach(is -> is.setMembers(members)); // Placeholders manager for plugin @@ -120,7 +137,8 @@ public void setUp() throws Exception { // Islands when(im.getIsland(any(World.class), any(User.class))).thenReturn(island); when(im.getIslandAt(any(Location.class))).thenReturn(Optional.of(island)); - when(im.getIsland(any(World.class), any(UUID.class))).thenAnswer((Answer) invocation -> islands.get(invocation.getArgument(1, UUID.class))); + when(im.getIslandById(anyString())).thenAnswer((Answer>) invocation -> Optional.of(islands.get(invocation.getArgument(0, String.class)))); + when(im.getIslands(any(), any(UUID.class))).thenReturn(new HashSet<>(islands.values())); when(addon.getIslands()).thenReturn(im); // Levels Manager @@ -129,6 +147,7 @@ public void setUp() throws Exception { when(lm.getPointsToNextString(any(), any())).thenReturn("1234567"); when(lm.getIslandMaxLevel(any(), any())).thenReturn(987654L); when(lm.getTopTen(world, Level.TEN)).thenReturn(map); + when(lm.getWeightedTopTen(world, Level.TEN)).thenReturn(map2); when(lm.formatLevel(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0, Long.class).toString()); data = new IslandLevels("uniqueId"); @@ -136,127 +155,150 @@ public void setUp() throws Exception { when(lm.getLevelsData(island)).thenReturn(data); when(addon.getManager()).thenReturn(lm); - pm = new PlaceholderManager(addon); + phm = new PlaceholderManager(addon); } /** - * Test method for {@link world.bentobox.level.PlaceholderManager#PlaceholderManager(world.bentobox.level.Level)}. + * Test method for + * {@link world.bentobox.level.PlaceholderManager#PlaceholderManager(world.bentobox.level.Level)}. */ @Test public void testPlaceholderManager() { - verify(addon).getPlugin(); + verify(addon).getPlugin(); } /** - * Test method for {@link world.bentobox.level.PlaceholderManager#registerPlaceholders(world.bentobox.bentobox.api.addons.GameModeAddon)}. + * Test method for + * {@link world.bentobox.level.PlaceholderManager#registerPlaceholders(world.bentobox.bentobox.api.addons.GameModeAddon)}. */ @Test public void testRegisterPlaceholders() { - pm.registerPlaceholders(gm); - // Island Level - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level"), any()); - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level_raw"), any()); - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_total_points"), any()); - - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_points_to_next_level"), any()); - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level_max"), any()); - - // Visited Island Level - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_visited_island_level"), any()); - - // Register Top Ten Placeholders - for (int i = 1; i < 11; i++) { - // Name - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_name_" + i), any()); - // Island Name - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_island_name_" + i), any()); - // Members - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_members_" + i), any()); - // Level - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_value_" + i), any()); - } + phm.registerPlaceholders(gm); + // Island Level + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level"), any()); + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level_raw"), any()); + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_total_points"), any()); + + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_points_to_next_level"), any()); + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_island_level_max"), any()); + + // Visited Island Level + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_visited_island_level"), any()); + + // Register Top Ten Placeholders + for (int i = 1; i < 11; i++) { + // Name + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_name_" + i), any()); + // Island Name + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_island_name_" + i), any()); + // Members + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_members_" + i), any()); + // Level + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_top_value_" + i), any()); + } + + // Personal rank + verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_rank_value"), any()); - // Personal rank - verify(bpm).registerPlaceholder(eq(addon), eq("aoneblock_rank_value"), any()); - } /** - * Test method for {@link world.bentobox.level.PlaceholderManager#getRankName(org.bukkit.World, int)}. + * Test method for + * {@link world.bentobox.level.PlaceholderManager#getRankName(org.bukkit.World, int)}. */ @Test public void testGetRankName() { - // Test extremes - assertEquals("tasty", pm.getRankName(world, 0)); - assertEquals("vicky", pm.getRankName(world, 100)); - // Test the ranks - int rank = 1; - for (String name : NAMES) { - assertEquals(name, pm.getRankName(world, rank++)); - } - + // Test extremes + assertEquals("tasty", phm.getRankName(world, 0, false)); + assertEquals("vicky", phm.getRankName(world, 100, false)); + // Test the ranks + int rank = 1; + for (String name : names.values()) { + assertEquals(name, phm.getRankName(world, rank++, false)); + } + } /** - * Test method for {@link world.bentobox.level.PlaceholderManager#getRankIslandName(org.bukkit.World, int)}. + * Test method for + * {@link world.bentobox.level.PlaceholderManager#getRankIslandName(org.bukkit.World, int)}. */ @Test public void testGetRankIslandName() { - // Test extremes - assertEquals("tasty's island", pm.getRankIslandName(world, 0)); - assertEquals("vicky's island", pm.getRankIslandName(world, 100)); - // Test the ranks - int rank = 1; - for (String name : NAMES) { - assertEquals(name + "'s island", pm.getRankIslandName(world, rank++)); - } - + // Test extremes + assertEquals("tasty's island", phm.getRankIslandName(world, 0, false)); + assertEquals("vicky's island", phm.getRankIslandName(world, 100, false)); + // Test the ranks + int rank = 1; + for (String name : names.values()) { + assertEquals(name + "'s island", phm.getRankIslandName(world, rank++, false)); + } + } /** - * Test method for {@link world.bentobox.level.PlaceholderManager#getRankMembers(org.bukkit.World, int)}. + * Test method for + * {@link world.bentobox.level.PlaceholderManager#getRankMembers(org.bukkit.World, int)}. */ @Test public void testGetRankMembers() { - // Test extremes - check(1, pm.getRankMembers(world, 0)); - check(2, pm.getRankMembers(world, 100)); - // Test the ranks - for (int rank = 1; rank < 11; rank++) { - check(3, pm.getRankMembers(world, rank)); - } + // Test extremes + check(1, phm.getRankMembers(world, 0, false)); + check(2, phm.getRankMembers(world, 100, false)); + // Test the ranks + for (int rank = 1; rank < 11; rank++) { + check(3, phm.getRankMembers(world, rank, false)); + } } - + void check(int indicator, String list) { - for (String n : NAMES) { - assertTrue(n + " is missing for twst " + indicator, list.contains(n)); - } + for (String n : names.values()) { + assertTrue(n + " is missing for test " + indicator, list.contains(n)); + } } /** - * Test method for {@link world.bentobox.level.PlaceholderManager#getRankLevel(org.bukkit.World, int)}. + * Test method for + * {@link world.bentobox.level.PlaceholderManager#getRankLevel(org.bukkit.World, int)}. */ @Test public void testGetRankLevel() { - // Test extremes - assertEquals("100", pm.getRankLevel(world, 0)); - assertEquals("91", pm.getRankLevel(world, 100)); - // Test the ranks - for (int rank = 1; rank < 11; rank++) { - assertEquals(String.valueOf(101 - rank), pm.getRankLevel(world, rank)); - } - + // Test extremes + assertEquals("100", phm.getRankLevel(world, 0, false)); + assertEquals("91", phm.getRankLevel(world, 100, false)); + // Test the ranks + for (int rank = 1; rank < 11; rank++) { + assertEquals(String.valueOf(101 - rank), phm.getRankLevel(world, rank, false)); + } + } /** - * Test method for {@link world.bentobox.level.PlaceholderManager#getVisitedIslandLevel(world.bentobox.bentobox.api.addons.GameModeAddon, world.bentobox.bentobox.api.user.User)}. + * Test method for + * {@link world.bentobox.level.PlaceholderManager#getRankLevel(org.bukkit.World, int)}. + */ + @Test + public void testGetWeightedRankLevel() { + // Test extremes + assertEquals("100", phm.getRankLevel(world, 0, true)); + assertEquals("91", phm.getRankLevel(world, 100, true)); + // Test the ranks + for (int rank = 1; rank < 11; rank++) { + assertEquals(String.valueOf(101 - rank), phm.getRankLevel(world, rank, true)); + } + + } + + /** + * Test method for + * {@link world.bentobox.level.PlaceholderManager#getVisitedIslandLevel(world.bentobox.bentobox.api.addons.GameModeAddon, world.bentobox.bentobox.api.user.User)}. */ @Test public void testGetVisitedIslandLevelNullUser() { - assertEquals("", pm.getVisitedIslandLevel(gm, null)); - + assertEquals("", phm.getVisitedIslandLevel(gm, null)); + } - + /** * Test method for {@link world.bentobox.level.PlaceholderManager#getVisitedIslandLevel(world.bentobox.bentobox.api.addons.GameModeAddon, world.bentobox.bentobox.api.user.User)}. */ @@ -264,26 +306,27 @@ public void testGetVisitedIslandLevelNullUser() { public void testGetVisitedIslandLevelUserNotInWorld() { // Another world when(user.getWorld()).thenReturn(mock(World.class)); - assertEquals("", pm.getVisitedIslandLevel(gm, user)); + assertEquals("", phm.getVisitedIslandLevel(gm, user)); } - + /** - * Test method for {@link world.bentobox.level.PlaceholderManager#getVisitedIslandLevel(world.bentobox.bentobox.api.addons.GameModeAddon, world.bentobox.bentobox.api.user.User)}. + * Test method for + * {@link world.bentobox.level.PlaceholderManager#getVisitedIslandLevel(world.bentobox.bentobox.api.addons.GameModeAddon, world.bentobox.bentobox.api.user.User)}. */ @Test public void testGetVisitedIslandLevel() { - assertEquals("1234567", pm.getVisitedIslandLevel(gm, user)); - + assertEquals("1234567", phm.getVisitedIslandLevel(gm, user)); + } - + /** * Test method for {@link world.bentobox.level.PlaceholderManager#getVisitedIslandLevel(world.bentobox.bentobox.api.addons.GameModeAddon, world.bentobox.bentobox.api.user.User)}. */ @Test public void testGetVisitedIslandLevelNoIsland() { when(im.getIslandAt(any(Location.class))).thenReturn(Optional.empty()); - assertEquals("0", pm.getVisitedIslandLevel(gm, user)); + assertEquals("0", phm.getVisitedIslandLevel(gm, user)); } diff --git a/src/test/java/world/bentobox/level/calculators/EquationEvaluatorTest.java b/src/test/java/world/bentobox/level/calculators/EquationEvaluatorTest.java new file mode 100644 index 0000000..9bcb5e2 --- /dev/null +++ b/src/test/java/world/bentobox/level/calculators/EquationEvaluatorTest.java @@ -0,0 +1,37 @@ +package world.bentobox.level.calculators; + +import static org.junit.Assert.assertEquals; + +import java.text.ParseException; + +import org.junit.Test; + +/** + * Test the equation evaluation + */ +public class EquationEvaluatorTest { + + /** + * Test method for {@link world.bentobox.level.calculators.EquationEvaluator#eval(java.lang.String)}. + * @throws ParseException + */ + @Test + public void testEval() throws ParseException { + assertEquals(4D, EquationEvaluator.eval("2+2"), 0D); + assertEquals(0D, EquationEvaluator.eval("2-2"), 0D); + assertEquals(1D, EquationEvaluator.eval("2/2"), 0D); + assertEquals(4D, EquationEvaluator.eval("2*2"), 0D); + assertEquals(8D, EquationEvaluator.eval("2+2+2+2"), 0D); + assertEquals(5D, EquationEvaluator.eval("2.5+2.5"), 0D); + assertEquals(1.414, EquationEvaluator.eval("sqrt(2)"), 0.001D); + assertEquals(3.414, EquationEvaluator.eval("2 + sqrt(2)"), 0.001D); + assertEquals(0D, EquationEvaluator.eval("sin(0)"), 0.1D); + assertEquals(1D, EquationEvaluator.eval("cos(0)"), 0.1D); + assertEquals(0D, EquationEvaluator.eval("tan(0)"), 0.1D); + assertEquals(0D, EquationEvaluator.eval("log(1)"), 0.1D); + assertEquals(27D, EquationEvaluator.eval("3^3"), 0.D); + assertEquals(84.70332D, EquationEvaluator.eval("3^3 + 2 + 2.65 * (3 / 4) - sin(45) * log(10) + 55.344"), + 0.0001D); + + } +} diff --git a/src/test/java/world/bentobox/level/commands/AdminStatsCommandTest.java b/src/test/java/world/bentobox/level/commands/AdminStatsCommandTest.java new file mode 100644 index 0000000..21d26d0 --- /dev/null +++ b/src/test/java/world/bentobox/level/commands/AdminStatsCommandTest.java @@ -0,0 +1,183 @@ +package world.bentobox.level.commands; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFactory; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.IslandsManager; +import world.bentobox.bentobox.managers.LocalesManager; +import world.bentobox.bentobox.managers.PlayersManager; +import world.bentobox.level.Level; +import world.bentobox.level.LevelsManager; +import world.bentobox.level.objects.TopTenData; + +/** + * @author tastybento + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Bukkit.class, BentoBox.class }) +public class AdminStatsCommandTest { + + @Mock + private CompositeCommand ic; + private UUID uuid; + @Mock + private User user; + @Mock + private IslandsManager im; + @Mock + private Island island; + @Mock + private Level addon; + @Mock + private World world; + @Mock + private IslandWorldManager iwm; + @Mock + private GameModeAddon gameModeAddon; + @Mock + private Player p; + @Mock + private LocalesManager lm; + @Mock + private PlayersManager pm; + + private AdminStatsCommand asc; + private TopTenData ttd; + @Mock + private LevelsManager manager; + @Mock + private Server server; + + @Before + public void setUp() { + // Set up plugin + BentoBox plugin = mock(BentoBox.class); + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + User.setPlugin(plugin); + when(addon.getPlugin()).thenReturn(plugin); + + // Addon + when(ic.getAddon()).thenReturn(addon); + when(ic.getPermissionPrefix()).thenReturn("bskyblock."); + when(ic.getLabel()).thenReturn("island"); + when(ic.getTopLabel()).thenReturn("island"); + when(ic.getWorld()).thenReturn(world); + when(ic.getTopLabel()).thenReturn("bsb"); + + // IWM friendly name + when(plugin.getIWM()).thenReturn(iwm); + when(iwm.getFriendlyName(any())).thenReturn("BSkyBlock"); + + // World + when(world.toString()).thenReturn("world"); + when(world.getName()).thenReturn("BSkyBlock_world"); + + // Player manager + when(plugin.getPlayers()).thenReturn(pm); + when(pm.getUser(anyString())).thenReturn(user); + // topTen + when(addon.getManager()).thenReturn(manager); + // User + uuid = UUID.randomUUID(); + when(user.getUniqueId()).thenReturn(uuid); + when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class)); + + // Bukkit + PowerMockito.mockStatic(Bukkit.class); + when(Bukkit.getServer()).thenReturn(server); + // Mock item factory (for itemstacks) + ItemFactory itemFactory = mock(ItemFactory.class); + ItemMeta itemMeta = mock(ItemMeta.class); + when(itemFactory.getItemMeta(any())).thenReturn(itemMeta); + when(server.getItemFactory()).thenReturn(itemFactory); + when(Bukkit.getItemFactory()).thenReturn(itemFactory); + + // Top ten + ttd = new TopTenData(world); + Map topten = new HashMap<>(); + Random r = new Random(); + for (int i = 0; i < 1000; i++) { + topten.put(UUID.randomUUID().toString(), r.nextLong(20000)); + } + ttd.setTopTen(topten); + asc = new AdminStatsCommand(addon, ic); + } + + @After + public void tearDown() { + User.clearUsers(); + } + + /** + * Test method for + * {@link world.bentobox.level.commands.AdminStatsCommand#setup()}. + */ + @Test + public void testSetup() { + assertEquals("bskyblock.admin.stats", asc.getPermission()); + assertFalse(asc.isOnlyPlayer()); + assertEquals("admin.stats.description", asc.getDescription()); + + } + + /** + * Test method for + * {@link world.bentobox.level.commands.AdminStatsCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. + */ + @Test + public void testExecuteUserStringListOfString() { + assertFalse(asc.execute(user, "", List.of())); + verify(user).sendMessage("admin.stats.title"); + verify(user).sendMessage("admin.stats.no-data"); + } + + /** + * Test method for + * {@link world.bentobox.level.commands.AdminStatsCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. + */ + @Test + public void testExecuteUserStringListOfStringLevels() { + Map map = new HashMap<>(); + map.put(world, ttd); + when(manager.getTopTenLists()).thenReturn(map); + assertTrue(asc.execute(user, "", List.of())); + verify(user).sendMessage("admin.stats.title"); + verify(user, never()).sendMessage("admin.stats.no-data"); + } + +} diff --git a/src/test/java/world/bentobox/level/commands/AdminTopRemoveCommandTest.java b/src/test/java/world/bentobox/level/commands/AdminTopRemoveCommandTest.java new file mode 100644 index 0000000..9e82384 --- /dev/null +++ b/src/test/java/world/bentobox/level/commands/AdminTopRemoveCommandTest.java @@ -0,0 +1,208 @@ +package world.bentobox.level.commands; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Set; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFactory; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.IslandsManager; +import world.bentobox.bentobox.managers.LocalesManager; +import world.bentobox.bentobox.managers.PlayersManager; +import world.bentobox.level.Level; +import world.bentobox.level.LevelsManager; +import world.bentobox.level.objects.TopTenData; + +/** + * @author tastybento + * + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Bukkit.class, BentoBox.class }) +public class AdminTopRemoveCommandTest { + + @Mock + private CompositeCommand ic; + private UUID uuid; + @Mock + private User user; + @Mock + private IslandsManager im; + @Mock + private Island island; + @Mock + private Level addon; + @Mock + private World world; + @Mock + private IslandWorldManager iwm; + @Mock + private GameModeAddon gameModeAddon; + @Mock + private Player p; + @Mock + private LocalesManager lm; + @Mock + private PlayersManager pm; + + private AdminTopRemoveCommand atrc; + @Mock + private TopTenData ttd; + @Mock + private LevelsManager manager; + @Mock + private Server server; + + @Before + public void setUp() { + // Set up plugin + BentoBox plugin = mock(BentoBox.class); + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + User.setPlugin(plugin); + + // Addon + when(ic.getAddon()).thenReturn(addon); + when(ic.getPermissionPrefix()).thenReturn("bskyblock."); + when(ic.getLabel()).thenReturn("island"); + when(ic.getTopLabel()).thenReturn("island"); + when(ic.getWorld()).thenReturn(world); + when(ic.getTopLabel()).thenReturn("bsb"); + + // IWM friendly name + when(plugin.getIWM()).thenReturn(iwm); + when(iwm.getFriendlyName(any())).thenReturn("BSkyBlock"); + + // World + when(world.toString()).thenReturn("world"); + when(world.getName()).thenReturn("BSkyBlock_world"); + + // Player manager + when(plugin.getPlayers()).thenReturn(pm); + when(pm.getUser(anyString())).thenReturn(user); + // topTen + when(addon.getManager()).thenReturn(manager); + // User + uuid = UUID.randomUUID(); + when(user.getUniqueId()).thenReturn(uuid); + when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class)); + // Island + when(island.getUniqueId()).thenReturn(uuid.toString()); + when(island.getOwner()).thenReturn(uuid); + // Island Manager + when(plugin.getIslands()).thenReturn(im); + when(im.getIslands(any(), any(User.class))).thenReturn(Set.of(island)); + when(im.getIslands(any(), any(UUID.class))).thenReturn(Set.of(island)); + + // Bukkit + PowerMockito.mockStatic(Bukkit.class); + when(Bukkit.getServer()).thenReturn(server); + // Mock item factory (for itemstacks) + ItemFactory itemFactory = mock(ItemFactory.class); + ItemMeta itemMeta = mock(ItemMeta.class); + when(itemFactory.getItemMeta(any())).thenReturn(itemMeta); + when(server.getItemFactory()).thenReturn(itemFactory); + when(Bukkit.getItemFactory()).thenReturn(itemFactory); + + atrc = new AdminTopRemoveCommand(addon, ic); + } + + @After + public void tearDown() { + User.clearUsers(); + } + + /** + * Test method for + * {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#AdminTopRemoveCommand(world.bentobox.level.Level, world.bentobox.bentobox.api.commands.CompositeCommand)}. + */ + @Test + public void testAdminTopRemoveCommand() { + assertEquals("remove", atrc.getLabel()); + assertEquals("delete", atrc.getAliases().get(0)); + } + + /** + * Test method for + * {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#setup()}. + */ + @Test + public void testSetup() { + assertEquals("bskyblock.admin.top.remove", atrc.getPermission()); + assertEquals("admin.top.remove.parameters", atrc.getParameters()); + assertEquals("admin.top.remove.description", atrc.getDescription()); + assertFalse(atrc.isOnlyPlayer()); + + } + + /** + * Test method for + * {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#canExecute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. + */ + @Test + public void testCanExecuteWrongArgs() { + assertFalse(atrc.canExecute(user, "delete", Collections.emptyList())); + verify(user).sendMessage("commands.help.header", TextVariables.LABEL, "BSkyBlock"); + } + + /** + * Test method for {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#canExecute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. + */ + @Test + public void testCanExecuteUnknown() { + when(pm.getUser(anyString())).thenReturn(null); + assertFalse(atrc.canExecute(user, "delete", Collections.singletonList("tastybento"))); + verify(user).sendMessage("general.errors.unknown-player", TextVariables.NAME, "tastybento"); + } + + /** + * Test method for + * {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#canExecute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. + */ + @Test + public void testCanExecuteKnown() { + assertTrue(atrc.canExecute(user, "delete", Collections.singletonList("tastybento"))); + } + + /** + * Test method for + * {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. + */ + @Test + public void testExecuteUserStringListOfString() { + testCanExecuteKnown(); + assertTrue(atrc.execute(user, "delete", Collections.singletonList("tastybento"))); + verify(manager).removeEntry(world, uuid.toString()); + verify(user).sendMessage("general.success"); + } + +} diff --git a/src/test/java/world/bentobox/level/commands/admin/AdminTopRemoveCommandTest.java b/src/test/java/world/bentobox/level/commands/admin/AdminTopRemoveCommandTest.java deleted file mode 100644 index 71070a4..0000000 --- a/src/test/java/world/bentobox/level/commands/admin/AdminTopRemoveCommandTest.java +++ /dev/null @@ -1,199 +0,0 @@ -package world.bentobox.level.commands.admin; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.UUID; - -import org.bukkit.Bukkit; -import org.bukkit.Server; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.meta.ItemMeta; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.addons.GameModeAddon; -import world.bentobox.bentobox.api.commands.CompositeCommand; -import world.bentobox.bentobox.api.localization.TextVariables; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.managers.IslandWorldManager; -import world.bentobox.bentobox.managers.IslandsManager; -import world.bentobox.bentobox.managers.LocalesManager; -import world.bentobox.bentobox.managers.PlayersManager; -import world.bentobox.level.Level; -import world.bentobox.level.LevelsManager; -import world.bentobox.level.commands.AdminTopRemoveCommand; -import world.bentobox.level.objects.TopTenData; - -/** - * @author tastybento - * - */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({Bukkit.class, BentoBox.class}) -public class AdminTopRemoveCommandTest { - - @Mock - private CompositeCommand ic; - private UUID uuid; - @Mock - private User user; - @Mock - private IslandsManager im; - @Mock - private Island island; - @Mock - private Level addon; - @Mock - private World world; - @Mock - private IslandWorldManager iwm; - @Mock - private GameModeAddon gameModeAddon; - @Mock - private Player p; - @Mock - private LocalesManager lm; - @Mock - private PlayersManager pm; - - private AdminTopRemoveCommand atrc; - @Mock - private TopTenData ttd; - @Mock - private LevelsManager manager; - @Mock - private Server server; - - @Before - public void setUp() { - // Set up plugin - BentoBox plugin = mock(BentoBox.class); - Whitebox.setInternalState(BentoBox.class, "instance", plugin); - User.setPlugin(plugin); - // Addon - when(ic.getAddon()).thenReturn(addon); - when(ic.getPermissionPrefix()).thenReturn("bskyblock."); - when(ic.getLabel()).thenReturn("island"); - when(ic.getTopLabel()).thenReturn("island"); - when(ic.getWorld()).thenReturn(world); - when(ic.getTopLabel()).thenReturn("bsb"); - - - // IWM friendly name - when(plugin.getIWM()).thenReturn(iwm); - when(iwm.getFriendlyName(any())).thenReturn("BSkyBlock"); - - // World - when(world.toString()).thenReturn("world"); - when(world.getName()).thenReturn("BSkyBlock_world"); - - - // Player manager - when(plugin.getPlayers()).thenReturn(pm); - when(pm.getUser(anyString())).thenReturn(user); - // topTen - when(addon.getManager()).thenReturn(manager); - // User - uuid = UUID.randomUUID(); - when(user.getUniqueId()).thenReturn(uuid); - when(user.getTranslation(any())).thenAnswer(invocation -> invocation.getArgument(0, String.class)); - - // Bukkit - PowerMockito.mockStatic(Bukkit.class); - when(Bukkit.getServer()).thenReturn(server); - // Mock item factory (for itemstacks) - ItemFactory itemFactory = mock(ItemFactory.class); - ItemMeta itemMeta = mock(ItemMeta.class); - when(itemFactory.getItemMeta(any())).thenReturn(itemMeta); - when(server.getItemFactory()).thenReturn(itemFactory); - when(Bukkit.getItemFactory()).thenReturn(itemFactory); - - - atrc = new AdminTopRemoveCommand(addon, ic); - } - - @After - public void tearDown() { - User.clearUsers(); - } - - /** - * Test method for {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#AdminTopRemoveCommand(world.bentobox.level.Level, world.bentobox.bentobox.api.commands.CompositeCommand)}. - */ - @Test - public void testAdminTopRemoveCommand() { - assertEquals("remove", atrc.getLabel()); - assertEquals("delete", atrc.getAliases().get(0)); - } - - /** - * Test method for {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#setup()}. - */ - @Test - public void testSetup() { - assertEquals("bskyblock.admin.top.remove", atrc.getPermission()); - assertEquals("admin.top.remove.parameters", atrc.getParameters()); - assertEquals("admin.top.remove.description", atrc.getDescription()); - assertFalse(atrc.isOnlyPlayer()); - - } - - /** - * Test method for {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#canExecute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. - */ - @Test - public void testCanExecuteWrongArgs() { - assertFalse(atrc.canExecute(user, "delete", Collections.emptyList())); - verify(user).sendMessage("commands.help.header", TextVariables.LABEL, "BSkyBlock"); - } - - /** - * Test method for {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#canExecute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. - */ - @Test - public void testCanExecuteUnknown() { - when(pm.getUser(anyString())).thenReturn(null); - assertFalse(atrc.canExecute(user, "delete", Collections.singletonList("tastybento"))); - verify(user).sendMessage("general.errors.unknown-player", TextVariables.NAME, "tastybento"); - } - - /** - * Test method for {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#canExecute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. - */ - @Test - public void testCanExecuteKnown() { - assertTrue(atrc.canExecute(user, "delete", Collections.singletonList("tastybento"))); - } - - /** - * Test method for {@link world.bentobox.level.commands.admin.AdminTopRemoveCommand#execute(world.bentobox.bentobox.api.user.User, java.lang.String, java.util.List)}. - */ - @Test - public void testExecuteUserStringListOfString() { - testCanExecuteKnown(); - assertTrue(atrc.execute(user, "delete", Collections.singletonList("tastybento"))); - verify(manager).removeEntry(any(World.class), eq(uuid)); - verify(user).sendMessage("general.success"); - } - -}