From 29565538c36867527d3c574cce6375f1fec34e70 Mon Sep 17 00:00:00 2001 From: BONNe Date: Sat, 18 Sep 2021 21:37:30 +0300 Subject: [PATCH] Implements Template reading. Add template loading via Admin Panel. Improve LibraryPanel so it could find json and yml files. --- .../bentobox/challenges/ChallengesAddon.java | 4 + .../commands/admin/DefaultsCommand.java | 9 +- .../managers/ChallengesImportManager.java | 1056 ++++++++++++++--- .../managers/ChallengesManager.java | 67 +- .../challenges/panel/CommonPanel.java | 9 + .../challenges/panel/admin/AdminPanel.java | 72 +- .../challenges/panel/admin/LibraryPanel.java | 439 +++++++ .../panel/admin/ListLibraryPanel.java | 306 ----- .../bentobox/challenges/web/WebManager.java | 12 +- .../challenges/web/object/LibraryEntry.java | 186 +-- src/main/resources/locales/en-US.yml | 26 +- .../challenges/ChallengesManagerTest.java | 10 +- 12 files changed, 1521 insertions(+), 675 deletions(-) create mode 100644 src/main/java/world/bentobox/challenges/panel/admin/LibraryPanel.java delete mode 100644 src/main/java/world/bentobox/challenges/panel/admin/ListLibraryPanel.java diff --git a/src/main/java/world/bentobox/challenges/ChallengesAddon.java b/src/main/java/world/bentobox/challenges/ChallengesAddon.java index 5a2bc1f..b178c34 100644 --- a/src/main/java/world/bentobox/challenges/ChallengesAddon.java +++ b/src/main/java/world/bentobox/challenges/ChallengesAddon.java @@ -314,6 +314,10 @@ public class ChallengesAddon extends Addon { this.saveResource("panels/main_panel.yml", false); this.saveResource("panels/multiple_panel.yml",false); this.saveResource("panels/gamemode_panel.yml",false); + + // Save template + this.saveResource("template.yml",false); + this.saveResource("default.json",false); } diff --git a/src/main/java/world/bentobox/challenges/commands/admin/DefaultsCommand.java b/src/main/java/world/bentobox/challenges/commands/admin/DefaultsCommand.java index df9d7a9..171dae3 100644 --- a/src/main/java/world/bentobox/challenges/commands/admin/DefaultsCommand.java +++ b/src/main/java/world/bentobox/challenges/commands/admin/DefaultsCommand.java @@ -97,7 +97,8 @@ public class DefaultsCommand extends CompositeCommand @Override public boolean execute(User user, String label, List args) { - return DefaultsCommand.this.addon.getImportManager().loadDefaultChallenges(user, this.getWorld()); + DefaultsCommand.this.addon.getImportManager().loadDownloadedChallenges(user, this.getWorld(), "default"); + return true; } } @@ -135,10 +136,12 @@ public class DefaultsCommand extends CompositeCommand @Override public boolean execute(User user, String label, List args) { - return DefaultsCommand.this.addon.getImportManager().generateDefaultChallengeFile( + DefaultsCommand.this.addon.getImportManager().generateDatabaseFile( user, this.getWorld(), - !args.isEmpty() && args.get(0).equalsIgnoreCase("overwrite")); + "defaults"); + + return true; } diff --git a/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java b/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java index 41b4b15..f7035cb 100644 --- a/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java +++ b/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java @@ -9,26 +9,39 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; +import org.bukkit.Material; +import org.bukkit.Statistic; import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.json.BentoboxTypeAdapterFactory; import world.bentobox.bentobox.database.objects.DataObject; +import world.bentobox.bentobox.util.ItemParser; import world.bentobox.bentobox.util.Util; import world.bentobox.challenges.ChallengesAddon; import world.bentobox.challenges.database.object.Challenge; import world.bentobox.challenges.database.object.ChallengeLevel; +import world.bentobox.challenges.database.object.requirements.InventoryRequirements; +import world.bentobox.challenges.database.object.requirements.IslandRequirements; +import world.bentobox.challenges.database.object.requirements.OtherRequirements; +import world.bentobox.challenges.database.object.requirements.StatisticRequirements; +import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; @@ -40,8 +53,8 @@ import world.bentobox.challenges.utils.Utils; public class ChallengesImportManager { /** - * Import challenges from default.json - * @param challengesAddon + * Import challenges from file or link. + * @param challengesAddon Challenges addon. */ public ChallengesImportManager(ChallengesAddon challengesAddon) { @@ -50,93 +63,660 @@ public class ChallengesImportManager // --------------------------------------------------------------------- - // Section: Default Challenge Loader + // Section: YAML Importers // --------------------------------------------------------------------- /** - * This method loads default challenges into memory. - * @param user User who calls default challenge loading - * @param world Target world. - * @return true if everything was successful, otherwise false. + * This method imports generator tiers from template + * + * @param user - user + * @param world - world to import into + * @param file - file that must be imported */ - public boolean loadDefaultChallenges(User user, World world) + public void importFile(@Nullable User user, World world, String file) { - ChallengesManager manager = this.addon.getChallengesManager(); + File generatorFile = new File(this.addon.getDataFolder(), file.endsWith(".yml") ? file : file + ".yml"); - // If exist any challenge or level that is bound to current world, then do not load default challenges. - if (manager.hasAnyChallengeData(world.getName())) + if (!generatorFile.exists()) { - if (user.isPlayer()) + if (user != null) { - user.sendMessage("challenges.errors.exist-challenges-or-levels"); - } - else - { - this.addon.logWarning("challenges.errors.exist-challenges-or-levels"); + Utils.sendMessage(user, user.getTranslation(Constants.ERRORS + "no-file", Constants.FILE, file)); } - return false; + return; } - // default configuration should be removed. - // user made configuration should not!. - boolean removeAtEnd = - !Files.exists(Paths.get(this.addon.getDataFolder().getPath() + "/default.json")); - - // Safe json configuration to Challenges folder. - this.addon.saveResource("default.json", false); + YamlConfiguration config = new YamlConfiguration(); try { - // This prefix will be used to all challenges. That is a unique way how to separate challenged for - // each game mode. - String uniqueIDPrefix = Utils.getGameMode(world) + "_"; - DefaultDataHolder defaultChallenges = new DefaultJSONHandler(this.addon).loadObject(); - if (defaultChallenges != null) { - // All new challenges should get correct ID. So we need to map it to loaded challenges. - defaultChallenges.getChallengeList().forEach(challenge -> { - // Set correct challenge ID - challenge.setUniqueId(uniqueIDPrefix + challenge.getUniqueId()); - // Set up correct level ID if it is necessary - if (!challenge.getLevel().isEmpty()) - { - challenge.setLevel(uniqueIDPrefix + challenge.getLevel()); - } - // Load challenge in memory - manager.loadChallenge(challenge, false, user, user == null); - }); - - defaultChallenges.getLevelList().forEach(challengeLevel -> { - // Set correct level ID - challengeLevel.setUniqueId(uniqueIDPrefix + challengeLevel.getUniqueId()); - // Set correct world name - challengeLevel.setWorld(Util.getWorld(world).getName()); - // Reset names for all challenges. - challengeLevel.setChallenges(challengeLevel.getChallenges().stream(). - map(challenge -> uniqueIDPrefix + challenge). - collect(Collectors.toSet())); - // Load level in memory - manager.loadLevel(challengeLevel, false, user, user == null); - }); + config.load(generatorFile); + } + catch (IOException | InvalidConfigurationException e) + { + if (user != null) + { + Utils.sendMessage(user, user.getTranslation(Constants.ERRORS + "no-load", + Constants.FILE, file, TextVariables.DESCRIPTION, e.getMessage())); } + else + { + this.addon.logError("Exception when loading file. " + e.getMessage()); + } + + return; + } + + Optional optional = this.addon.getPlugin().getIWM().getAddon(world); + + if (optional.isEmpty()) + { + if (user != null) + { + Utils.sendMessage(user, + user.getTranslation(Constants.ERRORS + "not-a-gamemode-world", + Constants.WORLD, world.getName())); + } + else + { + this.addon.logWarning("Given world is not a gamemode world."); + } + + return; + } + + this.addon.getChallengesManager().wipeDatabase(optional.get().getDescription().getName().toLowerCase()); + this.createChallenges(config, user, optional.get(), world); + } + + + /** + * This method creates generator tier object from config file. + * + * @param config YamlConfiguration that contains all generators. + * @param user User who calls reading. + * @param gameMode GameMode in which generator tiers must be imported + */ + private void createChallenges(YamlConfiguration config, @Nullable User user, GameModeAddon gameMode, World world) + { + final String prefix = gameMode.getDescription().getName().toLowerCase() + "_"; + + long challengeCount = 0; + long levelCount = 0; + + if (config.contains("challenges")) + { + ConfigurationSection reader = config.getConfigurationSection("challenges"); + + if (reader != null) + { + challengeCount = reader.getKeys(false).stream(). + mapToInt(challengeId -> this.createChallenge(challengeId, + prefix, + reader.getConfigurationSection(challengeId))). + sum(); + } + } + + if (config.contains("levels")) + { + ConfigurationSection reader = config.getConfigurationSection("levels"); + + if (reader != null) + { + levelCount = reader.getKeys(false).stream(). + mapToInt(levelId -> this.createLevel(levelId, + prefix, + world, + reader.getConfigurationSection(levelId))). + sum(); + } + } + + if (user != null) + { + Utils.sendMessage(user, + user.getTranslation(Constants.MESSAGES + "import-count", + "[levels]", String.valueOf(levelCount), + "[challenges]", String.valueOf(challengeCount))); + } + + this.addon.log("Imported " + challengeCount + " challenges and " + + levelCount + " levels into database."); + } + + + /** + * This method creates challenge from given config section. + * @param challengeId Challenge ID. + * @param prefix GameMode prefix. + * @param section Configuration Section that contains information. + * @return 1 if challenge is created, otherwise 0. + */ + private int createChallenge(String challengeId, + String prefix, + @Nullable ConfigurationSection section) + { + if (section == null) + { + return 0; + } + + try + { + Challenge challenge = new Challenge(); + challenge.setUniqueId(prefix + challengeId); + + challenge.setFriendlyName(section.getString("name", challengeId)); + challenge.setIcon(matchIcon(section.getString("icon"), new ItemStack(Material.PAPER))); + + // Read description + if (section.isList("description")) + { + challenge.setDescription(section.getStringList("description")); + } + else if (section.isString("description")) + { + String description = section.getString("description"); + + if (description != null) + { + // Define as list. + challenge.setDescription(Arrays.asList( + description.replaceAll("\\|", "\n"). + split("\n").clone())); + } + } + + challenge.setDeployed(section.getBoolean("deployed", true)); + challenge.setOrder(section.getInt("order", 0)); + challenge.setChallengeType(matchChallengeType(section.getString("type"), + Challenge.ChallengeType.ISLAND_TYPE)); + + // Read environment + Set environments = new HashSet<>(); + challenge.setEnvironment(environments); + + if (section.isList("environments")) + { + section.getStringList("environments"). + forEach(text -> environments.add(matchEnvironment(text, + World.Environment.NORMAL))); + } + else if (section.isString("environments")) + { + environments.add(matchEnvironment(section.getString("environments"), + World.Environment.NORMAL)); + } + + challenge.setRemoveWhenCompleted(section.getBoolean("remove-completed", false)); + + // Read Requirements + this.populateRequirements(challenge, section.getConfigurationSection("requirements")); + // Read Rewards + this.populateRewards(challenge, section.getConfigurationSection("rewards")); + + // Check Repeating status + challenge.setRepeatable(section.getBoolean("repeatable", false)); + challenge.setMaxTimes(section.getInt("repeat-times", -1)); + + if (challenge.isRepeatable()) + { + // Read Repeat Rewards + this.populateRepeatRewards(challenge, + section.getConfigurationSection("repeat-rewards")); + } + + this.addon.getChallengesManager().saveChallenge(challenge); + this.addon.getChallengesManager().loadChallenge(challenge, true, null, true); } catch (Exception e) { - e.printStackTrace(); - return false; + return 0; } - this.addon.getChallengesManager().saveChallenges(); - this.addon.getChallengesManager().saveLevels(); + return 1; + } - if (removeAtEnd) + + /** + * Populates requirements for the given challenge. + * + * @param challenge the challenge + * @param section the section + */ + private void populateRequirements(Challenge challenge, ConfigurationSection section) + { + switch (challenge.getChallengeType()) { - // Remove default.yml file from resources to avoid interacting with it. - return new File(this.addon.getDataFolder(), "default.json").delete(); + case INVENTORY_TYPE -> { + InventoryRequirements requirements = new InventoryRequirements(); + challenge.setRequirements(requirements); + + requirements.setTakeItems(section.getBoolean("take-items", false)); + List requiredItems = new ArrayList<>(); + requirements.setRequiredItems(requiredItems); + + if (section.isList("items")) + { + section.getStringList("items"). + forEach(text -> { + ItemStack itemStack = ItemParser.parse(text); + + if (itemStack != null) + { + requiredItems.add(itemStack); + } + }); + } + } + case ISLAND_TYPE -> { + IslandRequirements requirements = new IslandRequirements(); + challenge.setRequirements(requirements); + + requirements.setRemoveBlocks(section.getBoolean("remove-blocks", false)); + requirements.setRequiredBlocks(this.createMaterialMap(section.getConfigurationSection("blocks"))); + + requirements.setRemoveEntities(section.getBoolean("remove-entities", false)); + requirements.setRequiredEntities(this.createEntityMap(section.getConfigurationSection("entities"))); + + requirements.setSearchRadius(section.getInt("search-distance", 10)); + } + case OTHER_TYPE -> { + OtherRequirements requirements = new OtherRequirements(); + challenge.setRequirements(requirements); + + requirements.setTakeMoney(section.getBoolean("take-money", false)); + requirements.setRequiredMoney(section.getDouble("money", 0)); + + requirements.setTakeExperience(section.getBoolean("take-experience", false)); + requirements.setRequiredExperience(section.getInt("experience", 0)); + + requirements.setRequiredIslandLevel(section.getInt("level", 0)); + } + case STATISTIC_TYPE -> { + StatisticRequirements requirements = new StatisticRequirements(); + challenge.setRequirements(requirements); + + requirements.setAmount(section.getInt("amount", 0)); + requirements.setReduceStatistic(section.getBoolean("reduce", false)); + + requirements.setStatistic(matchStatistic(section.getString("statistic"))); + requirements.setEntity(matchEntity(section.getString("entity"))); + requirements.setMaterial(matchMaterial(section.getString("material"))); + } } - return true; + // Read permissions + if (challenge.getRequirements() != null) + { + Set permissions = new HashSet<>(); + challenge.getRequirements().setRequiredPermissions(permissions); + + if (section.isList("permissions")) + { + permissions.addAll(section.getStringList("permissions")); + } + else if (section.isString("permissions")) + { + String description = section.getString("permissions"); + + if (description != null) + { + // Define as list. + permissions.addAll(Arrays.asList( + description.replaceAll("\\|", "\n"). + split("\n").clone())); + } + } + } + } + + + /** + * This method populates material map from given section field. + * @param section Section that contains material. + * @return Map that links material and number. + */ + private Map createMaterialMap(ConfigurationSection section) + { + Map materialMaps = new HashMap<>(); + + if (section != null) + { + for (String materialKey : section.getKeys(false)) + { + Material material = matchMaterial(materialKey); + + if (material != null) + { + materialMaps.put(material, section.getInt(materialKey, 0)); + } + } + } + + return materialMaps; + } + + + /** + * This method populates entity map from given section field. + * @param section Section that contains material. + * @return Map that links entity and number. + */ + private Map createEntityMap(ConfigurationSection section) + { + Map entityMap = new HashMap<>(); + + if (section != null) + { + for (String EntityType : section.getKeys(false)) + { + EntityType entity = matchEntity(EntityType); + + if (entity != null) + { + entityMap.put(entity, section.getInt(EntityType, 0)); + } + } + } + + return entityMap; + } + + + /** + * This method populates rewards for a challenge. + * @param challenge Challenge + * @param section Section that contains rewards + */ + private void populateRewards(Challenge challenge, @Nullable ConfigurationSection section) + { + List rewardItems = new ArrayList<>(); + challenge.setRewardItems(rewardItems); + + if (section == null) + { + return; + } + + challenge.setRewardText(section.getString("text", "")); + + if (section.isList("items")) + { + section.getStringList("items"). + forEach(text -> { + ItemStack itemStack = ItemParser.parse(text); + + if (itemStack != null) + { + rewardItems.add(itemStack); + } + }); + } + + challenge.setRewardExperience(section.getInt("experience", 0)); + challenge.setRewardMoney(section.getDouble("money", 0)); + + if (section.isList("commands")) + { + challenge.setRewardCommands(section.getStringList("commands")); + } + else if (section.isString("commands")) + { + String description = section.getString("commands"); + + if (description != null) + { + // Define as list. + challenge.setRewardCommands(Arrays.asList( + description.replaceAll("\\|", "\n"). + split("\n").clone())); + } + } + } + + + /** + * This method populates repeat rewards for a challenge. + * @param challenge Challenge + * @param section Section that contains rewards + */ + private void populateRepeatRewards(Challenge challenge, @Nullable ConfigurationSection section) + { + List rewardItems = new ArrayList<>(); + challenge.setRepeatItemReward(rewardItems); + + if (section == null) + { + return; + } + + challenge.setRepeatRewardText(section.getString("text", "")); + + if (section.isList("items")) + { + section.getStringList("items"). + forEach(text -> { + ItemStack itemStack = ItemParser.parse(text); + + if (itemStack != null) + { + rewardItems.add(itemStack); + } + }); + } + + challenge.setRepeatExperienceReward(section.getInt("experience", 0)); + challenge.setRepeatMoneyReward(section.getDouble("money", 0)); + + if (section.isList("commands")) + { + challenge.setRepeatRewardCommands(section.getStringList("commands")); + } + else if (section.isString("commands")) + { + String description = section.getString("commands"); + + if (description != null) + { + // Define as list. + challenge.setRepeatRewardCommands(Arrays.asList( + description.replaceAll("\\|", "\n"). + split("\n").clone())); + } + } + } + + + /** + * This method populates rewards for a level. + * @param level level + * @param section Section that contains rewards + */ + private void populateRewards(ChallengeLevel level, @Nullable ConfigurationSection section) + { + List rewardItems = new ArrayList<>(); + level.setRewardItems(rewardItems); + + if (section == null) + { + return; + } + + level.setRewardText(section.getString("text", "")); + + if (section.isList("items")) + { + section.getStringList("items"). + forEach(text -> { + ItemStack itemStack = ItemParser.parse(text); + + if (itemStack != null) + { + rewardItems.add(itemStack); + } + }); + } + + level.setRewardExperience(section.getInt("experience", 0)); + level.setRewardMoney(section.getDouble("money", 0)); + + if (section.isList("commands")) + { + level.setRewardCommands(section.getStringList("commands")); + } + else if (section.isString("commands")) + { + String description = section.getString("commands"); + + if (description != null) + { + // Define as list. + level.setRewardCommands(Arrays.asList( + description.replaceAll("\\|", "\n"). + split("\n").clone())); + } + } + } + + + /** + * This method creates Level + * @param levelId Level Id + * @param prefix Gamemode prefix + * @param world World where level operates. + * @param section Section that contains level info. + * @return 1 if level created, 0 otherwise. + */ + private int createLevel(String levelId, + String prefix, + World world, + @Nullable ConfigurationSection section) + { + if (section == null) + { + return 0; + } + + try + { + ChallengeLevel level = new ChallengeLevel(); + level.setUniqueId(prefix + levelId); + + level.setFriendlyName(section.getString("name", levelId)); + level.setIcon(matchIcon(section.getString("icon"), new ItemStack(Material.PAPER))); + level.setLockedIcon(matchIcon(section.getString("icon"))); + + level.setWorld(world.getName()); + + level.setOrder(section.getInt("order", 0)); + level.setWaiverAmount(section.getInt("waiver", 0)); + + level.setUnlockMessage(section.getString("description", "")); + + this.populateRewards(level, section.getConfigurationSection("rewards")); + + Set challenges = new HashSet<>(); + level.setChallenges(challenges); + + if (section.isList("challenges")) + { + section.getStringList("challenges").forEach(text -> { + Challenge challenge = this.addon.getChallengesManager().getChallenge(prefix + text); + + if (challenge != null) + { + challenges.add(challenge.getUniqueId()); + this.addon.getChallengesManager().addChallengeToLevel(challenge, level); + } + }); + } + + this.addon.getChallengesManager().saveLevel(level); + this.addon.getChallengesManager().loadLevel(level, true, null, true); + } + catch (Exception ignored) + { + return 0; + } + + return 1; + } + + + // --------------------------------------------------------------------- + // Section: JSON Importers + // --------------------------------------------------------------------- + + + /** + * Import database file from local storage. + * + * @param user the user + * @param world the world + * @param fileName the file name + */ + public void importDatabaseFile(User user, World world, String fileName) + { + ChallengesManager manager = this.addon.getChallengesManager(); + + // If exist any generator that is bound to current world, then do not load generators. + if (manager.hasAnyChallengeData(world.getName())) + { + this.addon.getPlugin().getIWM().getAddon(world).ifPresent(gameModeAddon -> { + manager.wipeDatabase(gameModeAddon.getDescription().getName().toLowerCase()); + }); + } + + try + { + // This prefix will be used to all generators. That is a unique way how to separate generators for + // each game mode. + String uniqueIDPrefix = Utils.getGameMode(world).toLowerCase() + "_"; + DefaultDataHolder downloadedChallenges = new DefaultJSONHandler(this.addon).loadObject(fileName); + + if (downloadedChallenges == null) + { + return; + } + + // All new challenges should get correct ID. So we need to map it to loaded challenges. + downloadedChallenges.getChallengeList().forEach(challenge -> { + // Set correct challenge ID + challenge.setUniqueId(uniqueIDPrefix + challenge.getUniqueId()); + // Set up correct level ID if it is necessary + if (!challenge.getLevel().isEmpty()) + { + challenge.setLevel(uniqueIDPrefix + challenge.getLevel()); + } + // Load challenge in memory + manager.loadChallenge(challenge, false, user, user == null); + }); + + downloadedChallenges.getLevelList().forEach(challengeLevel -> { + // Set correct level ID + challengeLevel.setUniqueId(uniqueIDPrefix + challengeLevel.getUniqueId()); + // Set correct world name + challengeLevel.setWorld(Util.getWorld(world).getName()); + // Reset names for all challenges. + challengeLevel.setChallenges(challengeLevel.getChallenges().stream(). + map(challenge -> uniqueIDPrefix + challenge). + collect(Collectors.toSet())); + // Load level in memory + manager.loadLevel(challengeLevel, false, user, user == null); + }); + } + catch (Exception e) + { + this.addon.getPlugin().logStacktrace(e); + return; + } + + manager.saveChallenges(); + manager.saveLevels(); } @@ -145,9 +725,8 @@ public class ChallengesImportManager * @param user User who calls downloaded challenge loading * @param world Target world. * @param downloadString String that need to be loaded via DefaultDataHolder. - * @return true if everything was successful, otherwise false. */ - public boolean loadDownloadedChallenges(User user, World world, String downloadString) + public void loadDownloadedChallenges(User user, World world, String downloadString) { ChallengesManager manager = this.addon.getChallengesManager(); @@ -163,14 +742,14 @@ public class ChallengesImportManager this.addon.logWarning("challenges.errors.exist-challenges-or-levels"); } - return false; + return; } try { // This prefix will be used to all challenges. That is a unique way how to separate challenged for // each game mode. - String uniqueIDPrefix = Utils.getGameMode(world) + "_"; + String uniqueIDPrefix = Utils.getGameMode(world).toLowerCase() + "_"; DefaultDataHolder downloadedChallenges = new DefaultJSONHandler(this.addon).loadWebObject(downloadString); // All new challenges should get correct ID. So we need to map it to loaded challenges. @@ -201,14 +780,12 @@ public class ChallengesImportManager } catch (Exception e) { - addon.getPlugin().logStacktrace(e); - return false; + this.addon.getPlugin().logStacktrace(e); + return; } this.addon.getChallengesManager().saveChallenges(); this.addon.getChallengesManager().saveLevels(); - - return true; } @@ -217,88 +794,65 @@ public class ChallengesImportManager // --------------------------------------------------------------------- - /** - * Create method that can generate default challenge file from existing challenges in given world. - * This method will create default.json file in Challenges folder. - * @param user User who calls this method. - * @param world from which challenges must be stored. - * @param overwrite indicates if existing default.json file can be overwritten. - * @return true if everything was successful, otherwise false - */ - public boolean generateDefaultChallengeFile(User user, World world, boolean overwrite) + public void generateDatabaseFile(User user, World world, String fileName) { - File defaultFile = new File(this.addon.getDataFolder(), "default.json"); + File defaultFile = new File(this.addon.getDataFolder(), + fileName.endsWith(".json") ? fileName : fileName + ".json"); if (defaultFile.exists()) { - if (overwrite) + if (user.isPlayer()) { - if (user.isPlayer()) - { - user.sendMessage("challenges.messages.defaults-file-overwrite"); - } - else - { - this.addon.logWarning("challenges.messages.defaults-file-overwrite"); - } - - if (!defaultFile.delete()) { - this.addon.logError("Could not delete file: " + defaultFile.getAbsolutePath()); - } + Utils.sendMessage(user, + user.getTranslation(Constants.ERRORS + "file-exist", + Constants.FILE, fileName)); } else { - if (user.isPlayer()) - { - user.sendMessage("challenges.errors.defaults-file-exist"); - } - else - { - this.addon.logWarning("challenges.errors.defaults-file-exist"); - } - - return false; + this.addon.logWarning(Constants.ERRORS + "file-exist"); } + + return; } try { if (defaultFile.createNewFile()) { - String replacementString = Utils.getGameMode(world) + "_"; + String replacementString = Utils.getGameMode(world).toLowerCase() + "_"; ChallengesManager manager = this.addon.getChallengesManager(); List challengeList = manager.getAllChallenges(world). - stream(). - map(challenge -> { - // Use clone to avoid any changes in existing challenges. - Challenge clone = challenge.clone(); - // Remove world name from challenge id. - clone.setUniqueId(challenge.getUniqueId().replaceFirst(replacementString, "")); - // Remove world name from level id. - clone.setLevel(challenge.getLevel().replaceFirst(replacementString, "")); + stream(). + map(challenge -> { + // Use clone to avoid any changes in existing challenges. + Challenge clone = challenge.clone(); + // Remove world name from challenge id. + clone.setUniqueId(challenge.getUniqueId().replaceFirst(replacementString, "")); + // Remove world name from level id. + clone.setLevel(challenge.getLevel().replaceFirst(replacementString, "")); - return clone; - }). - collect(Collectors.toList()); + return clone; + }). + collect(Collectors.toList()); List levelList = manager.getLevels(world). - stream(). - map(challengeLevel -> { - // Use clone to avoid any changes in existing levels. - ChallengeLevel clone = challengeLevel.clone(); - // Remove world name from level ID. - clone.setUniqueId(challengeLevel.getUniqueId().replaceFirst(replacementString, "")); - // Remove world name. - clone.setWorld(""); - // Challenges must be reassign, as they also contains world name. - clone.setChallenges(challengeLevel.getChallenges().stream(). - map(challenge -> challenge.replaceFirst(replacementString, "")). - collect(Collectors.toSet())); + stream(). + map(challengeLevel -> { + // Use clone to avoid any changes in existing levels. + ChallengeLevel clone = challengeLevel.clone(); + // Remove world name from level ID. + clone.setUniqueId(challengeLevel.getUniqueId().replaceFirst(replacementString, "")); + // Remove world name. + clone.setWorld(""); + // Challenges must be reassign, as they also contains world name. + clone.setChallenges(challengeLevel.getChallenges().stream(). + map(challenge -> challenge.replaceFirst(replacementString, "")). + collect(Collectors.toSet())); - return clone; - }). - collect(Collectors.toList()); + return clone; + }). + collect(Collectors.toList()); DefaultDataHolder defaultChallenges = new DefaultDataHolder(); defaultChallenges.setChallengeList(challengeList); @@ -306,9 +860,9 @@ public class ChallengesImportManager defaultChallenges.setVersion(this.addon.getDescription().getVersion()); try (BufferedWriter writer = new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(defaultFile), StandardCharsets.UTF_8))) { + new OutputStreamWriter(new FileOutputStream(defaultFile), StandardCharsets.UTF_8))) { writer.write(Objects.requireNonNull( - new DefaultJSONHandler(this.addon).toJsonString(defaultChallenges))); + new DefaultJSONHandler(this.addon).toJsonString(defaultChallenges))); } } } @@ -316,25 +870,226 @@ public class ChallengesImportManager { if (user.isPlayer()) { - user.sendMessage("challenges.errors.defaults-file-error"); + Utils.sendMessage(user, + user.getTranslation(Constants.ERRORS + "no-load", + Constants.FILE, fileName, + TextVariables.DESCRIPTION, e.getMessage())); } this.addon.logError("Could not save json file: " + e.getMessage()); - return false; } finally { if (user.isPlayer()) { - user.sendMessage("challenges.messages.defaults-file-completed", "[world]", world.getName()); + Utils.sendMessage(user, + user.getTranslation(Constants.CONVERSATIONS + "database-export-completed", + Constants.WORLD, world.getName(), + Constants.FILE, fileName)); } else { - this.addon.logWarning("challenges.messages.defaults-file-completed"); + this.addon.logWarning("Database Export Completed"); } } + } - return true; + + // --------------------------------------------------------------------- + // Section: Static Methods + // --------------------------------------------------------------------- + + + /** + * Match item stack. + * + * @param text the text + * @return the item stack + */ + @Nullable + private static ItemStack matchIcon(@Nullable String text) + { + if (text == null || text.isBlank()) + { + return new ItemStack(Material.PAPER); + } + else + { + return ItemParser.parse(text, new ItemStack(Material.PAPER)); + } + } + + + /** + * Match item stack. + * + * @param text the text + * @param defaultItem the default item + * @return the item stack + */ + @NonNull + private static ItemStack matchIcon(@Nullable String text, ItemStack defaultItem) + { + ItemStack item = matchIcon(text); + return item == null ? defaultItem : item; + } + + + /** + * Match material. + * + * @param text the text + * @return the material + */ + @Nullable + private static Material matchMaterial(@Nullable String text) + { + if (text == null || text.isBlank()) + { + return null; + } + else + { + return Material.getMaterial(text.toUpperCase()); + } + } + + + /** + * Match material. + * + * @param text the text + * @param defaultItem the default item + * @return the material + */ + @NonNull + private static Material matchMaterial(@Nullable String text, Material defaultItem) + { + Material item = matchMaterial(text); + return item == null ? defaultItem : item; + } + + + /** + * Match entity type. + * + * @param text the text + * @return the entity type + */ + @Nullable + private static EntityType matchEntity(@Nullable String text) + { + if (text == null || text.isBlank()) + { + return null; + } + else + { + try + { + return EntityType.valueOf(text.toUpperCase()); + } + catch (Exception e) + { + return null; + } + } + } + + + /** + * Match entity type. + * + * @param text the text + * @param defaultItem the default item + * @return the entity type + */ + @NonNull + private static EntityType matchEntity(@Nullable String text, EntityType defaultItem) + { + EntityType item = matchEntity(text); + return item == null ? defaultItem : item; + } + + + /** + * Match statistic value. + * + * @param text the text + * @return the statistic + */ + @Nullable + private static Statistic matchStatistic(@Nullable String text) + { + if (text == null || text.isBlank()) + { + return null; + } + else + { + try + { + return Statistic.valueOf(text.toUpperCase()); + } + catch (Exception e) + { + return null; + } + } + } + + + /** + * Match challenge type + * + * @param text the text + * @param defaultType default type + * @return the challenge type + */ + private static Challenge.ChallengeType matchChallengeType(@Nullable String text, Challenge.ChallengeType defaultType) + { + if (text == null || text.isBlank()) + { + return defaultType; + } + else + { + try + { + return Challenge.ChallengeType.valueOf(text.toUpperCase()); + } + catch (Exception e) + { + return defaultType; + } + } + } + + + /** + * Match world environment. + * + * @param text the text + * @param defaultType the default type + * @return the world environment + */ + private static World.Environment matchEnvironment(@Nullable String text, World.Environment defaultType) + { + if (text == null || text.isBlank()) + { + return defaultType; + } + else + { + try + { + return World.Environment.valueOf(text.toUpperCase()); + } + catch (Exception e) + { + return defaultType; + } + } } @@ -389,9 +1144,14 @@ public class ChallengesImportManager * This method creates and adds to list all objects from default.json file. * @return List of all objects from default.json that is with T instance. */ - DefaultDataHolder loadObject() + DefaultDataHolder loadObject(String fileName) { - File defaultFile = new File(this.addon.getDataFolder(), "default.json"); + if (!fileName.endsWith(".json")) + { + fileName = fileName + ".json"; + } + + File defaultFile = new File(this.addon.getDataFolder(), fileName); try (InputStreamReader reader = new InputStreamReader(new FileInputStream(defaultFile), StandardCharsets.UTF_8)) { @@ -432,12 +1192,12 @@ public class ChallengesImportManager /** * Holds JSON builder object. */ - private Gson gson; + private final Gson gson; /** * Holds ChallengesAddon object. */ - private ChallengesAddon addon; + private final ChallengesAddon addon; } @@ -569,5 +1329,5 @@ public class ChallengesImportManager // --------------------------------------------------------------------- - private ChallengesAddon addon; + private final ChallengesAddon addon; } \ No newline at end of file diff --git a/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java b/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java index 3e21d30..7d587fa 100644 --- a/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java +++ b/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java @@ -1,19 +1,7 @@ package world.bentobox.challenges.managers; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import org.bukkit.Bukkit; @@ -24,6 +12,7 @@ import org.bukkit.entity.EntityType; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.logs.LogEntry; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.Database; @@ -591,29 +580,45 @@ public class ChallengesManager /** * This method removes all challenges addon data from Database. * @param complete Remove also user data. + * @param name Name of the GameMode. */ - public void wipeDatabase(boolean complete) + public void wipeDatabase(boolean complete, String name) { - this.wipeLevels(); - this.wipeChallenges(); + this.wipeLevels(name); + this.wipeChallenges(name); if (complete) { - this.wipePlayers(); + this.wipePlayers(name); } } + /** + * This method removes all challenges addon data from Database. + * @param name Name of the GameMode. + */ + public void wipeDatabase(String name) + { + this.wipeDatabase(false, name); + } + + /** * This method collects all data from levels database and removes them. * Also clears levels cache data. */ - private void wipeLevels() + private void wipeLevels(String gamemode) { List levelList = this.levelDatabase.loadObjects(); - levelList.forEach(level -> this.levelDatabase.deleteID(level.getUniqueId())); - this.levelCacheData.clear(); + levelList.stream(). + filter(level -> level.getUniqueId().startsWith(gamemode.toLowerCase()) || + level.getUniqueId().startsWith(gamemode)). + forEach(level -> { + this.levelDatabase.deleteID(level.getUniqueId()); + this.levelCacheData.remove(level.getUniqueId()); + }); } @@ -621,12 +626,17 @@ public class ChallengesManager * This method collects all data from challenges database and removes them. * Also clears challenges cache data. */ - private void wipeChallenges() + private void wipeChallenges(String gamemode) { List challengeList = this.challengeDatabase.loadObjects(); - challengeList.forEach(challenge -> this.challengeDatabase.deleteID(challenge.getUniqueId())); - this.challengeCacheData.clear(); + challengeList.stream(). + filter(challenge -> challenge.getUniqueId().startsWith(gamemode.toLowerCase()) || + challenge.getUniqueId().startsWith(gamemode)). + forEach(challenge -> { + this.challengeDatabase.deleteID(challenge.getUniqueId()); + this.challengeCacheData.remove(challenge.getUniqueId()); + }); } @@ -634,12 +644,15 @@ public class ChallengesManager * This method collects all data from players database and removes them. * Also clears players cache data. */ - public void wipePlayers() + public void wipePlayers(String gamemode) { - List playerDataList = this.playersDatabase.loadObjects(); - - playerDataList.forEach(playerData -> this.playersDatabase.deleteID(playerData.getUniqueId())); this.playerCacheData.clear(); + + List playerDataList = this.playersDatabase.loadObjects(); + playerDataList.forEach(playerData -> { + playerData.reset(gamemode); + this.playersDatabase.saveObjectAsync(playerData); + }); } diff --git a/src/main/java/world/bentobox/challenges/panel/CommonPanel.java b/src/main/java/world/bentobox/challenges/panel/CommonPanel.java index 6e9e760..bb48df0 100644 --- a/src/main/java/world/bentobox/challenges/panel/CommonPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/CommonPanel.java @@ -102,6 +102,15 @@ public abstract class CommonPanel protected abstract void build(); + /** + * This method reopens given panel. + * @param panel Panel that must be reopened. + */ + public static void reopen(CommonPanel panel) + { + panel.build(); + } + // --------------------------------------------------------------------- // Section: Common methods diff --git a/src/main/java/world/bentobox/challenges/panel/admin/AdminPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/AdminPanel.java index b80277b..fa681a9 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/AdminPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/AdminPanel.java @@ -1,6 +1,7 @@ package world.bentobox.challenges.panel.admin; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -104,11 +105,11 @@ public class AdminPanel extends CommonPanel panelBuilder.item(31, this.createButton(Button.DELETE_LEVEL)); // Import Challenges - panelBuilder.item(15, this.createButton(Button.IMPORT_CHALLENGES)); - panelBuilder.item(24, this.createButton(Button.LIBRARY)); - - // Not added as I do not think admins should use it. It still will be able via command. - // panelBuilder.item(33, this.createButton(Button.DEFAULT_EXPORT_CHALLENGES)); + panelBuilder.item(14, this.createButton(Button.IMPORT_TEMPLATE)); + panelBuilder.item(15, this.createButton(Button.IMPORT_DATABASE)); + panelBuilder.item(33, this.createButton(Button.LIBRARY)); + // Export Challenges + panelBuilder.item(24, this.createButton(Button.EXPORT_CHALLENGES)); // Edit Addon Settings panelBuilder.item(16, this.createButton(Button.EDIT_SETTINGS)); @@ -326,10 +327,21 @@ public class AdminPanel extends CommonPanel description.add(""); description.add(this.user.getTranslationOrNothing(Constants.TIPS + "click-to-open")); } - case IMPORT_CHALLENGES -> { - icon = new ItemStack(Material.HOPPER); + case IMPORT_DATABASE -> { + icon = new ItemStack(Material.BOOKSHELF); clickHandler = (panel, user, clickType, slot) -> { - // TODO: Importing GUI. + LibraryPanel.open(this, LibraryPanel.Library.DATABASE); + return true; + }; + glow = true; + + description.add(""); + description.add(this.user.getTranslationOrNothing(Constants.TIPS + "click-to-open")); + } + case IMPORT_TEMPLATE -> { + icon = new ItemStack(Material.BOOKSHELF); + clickHandler = (panel, user, clickType, slot) -> { + LibraryPanel.open(this, LibraryPanel.Library.TEMPLATE); return true; }; glow = false; @@ -340,7 +352,36 @@ public class AdminPanel extends CommonPanel case EXPORT_CHALLENGES -> { icon = new ItemStack(Material.HOPPER); clickHandler = (panel, user, clickType, slot) -> { - // TODO: Exporting GUI. + + // This consumer process file exporting after user input is returned. + Consumer fileNameConsumer = value -> { + if (value != null) + { + this.addon.getImportManager().generateDatabaseFile(this.user, + this.world, + Utils.sanitizeInput(value)); + } + + this.build(); + }; + + // This function checks if file can be created. + Function validationFunction = fileName -> + { + String sanitizedName = Utils.sanitizeInput(fileName); + return !new File(this.addon.getDataFolder(), + sanitizedName.endsWith(".json") ? sanitizedName : sanitizedName + ".json").exists(); + }; + + // Call a conversation API to get input string. + ConversationUtils.createIDStringInput(fileNameConsumer, + validationFunction, + this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "exported-file-name"), + this.user.getTranslation(Constants.CONVERSATIONS + "database-export-completed", + Constants.WORLD, world.getName()), + Constants.CONVERSATIONS + "file-name-exist"); + return true; }; glow = false; @@ -361,7 +402,7 @@ public class AdminPanel extends CommonPanel clickHandler = (panel, user, clickType, slot) -> { if (WebManager.isEnabled()) { - ListLibraryPanel.open(this); + LibraryPanel.open(this, LibraryPanel.Library.WEB); } return true; @@ -385,7 +426,8 @@ public class AdminPanel extends CommonPanel Consumer consumer = value -> { if (value) { - this.addon.getChallengesManager().wipeDatabase(this.wipeAll); + this.addon.getChallengesManager().wipeDatabase(this.wipeAll, + Utils.getGameMode(this.world)); } this.build(); @@ -423,7 +465,8 @@ public class AdminPanel extends CommonPanel Consumer consumer = value -> { if (value) { - this.addon.getChallengesManager().wipeDatabase(this.wipeAll); + this.addon.getChallengesManager().wipeDatabase(this.wipeAll, + Utils.getGameMode(this.world)); } this.build(); @@ -454,7 +497,7 @@ public class AdminPanel extends CommonPanel Consumer consumer = value -> { if (value) { - this.addon.getChallengesManager().wipePlayers(); + this.addon.getChallengesManager().wipePlayers(Utils.getGameMode(this.world)); } this.build(); @@ -512,7 +555,8 @@ public class AdminPanel extends CommonPanel DELETE_CHALLENGE, DELETE_LEVEL, EDIT_SETTINGS, - IMPORT_CHALLENGES, + IMPORT_DATABASE, + IMPORT_TEMPLATE, EXPORT_CHALLENGES, /** * Allows to remove whole database diff --git a/src/main/java/world/bentobox/challenges/panel/admin/LibraryPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/LibraryPanel.java new file mode 100644 index 0000000..27982fe --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/admin/LibraryPanel.java @@ -0,0 +1,439 @@ +package world.bentobox.challenges.panel.admin; + + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.scheduler.BukkitTask; + +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.PanelListener; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.challenges.panel.CommonPagedPanel; +import world.bentobox.challenges.panel.CommonPanel; +import world.bentobox.challenges.panel.ConversationUtils; +import world.bentobox.challenges.utils.Constants; +import world.bentobox.challenges.utils.GuiUtils; +import world.bentobox.challenges.utils.Utils; +import world.bentobox.challenges.web.object.LibraryEntry; + + +/** + * This class contains all necessary elements to create GUI that lists all challenges. + * It allows to edit them or remove, depending on given input mode. + */ +public class LibraryPanel extends CommonPagedPanel +{ + // --------------------------------------------------------------------- + // Section: Constructor + // --------------------------------------------------------------------- + + /** + * @param parentGUI ParentGUI object. + */ + private LibraryPanel(CommonPanel parentGUI, Library mode) + { + super(parentGUI); + + this.mode = mode; + + this.libraryEntries = switch (mode) + { + case WEB -> this.addon.getWebManager().getLibraryEntries(); + case DATABASE -> this.generateDatabaseEntries(); + case TEMPLATE -> this.generateTemplateEntries(); + }; + } + + + /** + * This static method allows to easier open Library GUI. + * @param parentGui ParentGUI object. + * @param mode Library view mode. + */ + public static void open(CommonPanel parentGui, Library mode) + { + new LibraryPanel(parentGui, mode).build(); + } + + +// --------------------------------------------------------------------- +// Section: Data Collectors +// --------------------------------------------------------------------- + + + /** + * This method generates list of database file entries. + * + * @return List of entries for database files. + */ + private List generateDatabaseEntries() + { + File localeDir = this.addon.getDataFolder(); + File[] files = localeDir.listFiles(pathname -> + pathname.getName().endsWith(".json") && pathname.isFile()); + + if (files == null || files.length == 0) + { + // No + return Collections.emptyList(); + } + + return Arrays.stream(files). + map(file -> LibraryEntry.fromTemplate( + file.getName().substring(0, file.getName().length() - 5), + Material.PAPER)). + collect(Collectors.toList()); + } + + + /** + * This method generates list of template file entries. + * + * @return List of entries for template files. + */ + private List generateTemplateEntries() + { + File localeDir = this.addon.getDataFolder(); + File[] files = localeDir.listFiles(pathname -> + pathname.getName().endsWith(".yml") && + pathname.isFile() && + !pathname.getName().equals("config.yml")); + + if (files == null || files.length == 0) + { + // No + return Collections.emptyList(); + } + + return Arrays.stream(files). + map(file -> LibraryEntry.fromTemplate( + file.getName().substring(0, file.getName().length() - 4), + Material.PAPER)). + collect(Collectors.toList()); + } + + +// --------------------------------------------------------------------- +// Section: Methods +// --------------------------------------------------------------------- + + + /** + * {@inheritDoc} + */ + @Override + protected void build() + { + if (this.libraryEntries.isEmpty()) + { + Utils.sendMessage(this.user, this.user.getTranslation( + Constants.ERRORS + "no-library-entries")); + return; + } + + // No point to display. Single element. + if (this.libraryEntries.size() == 1 && !this.mode.equals(Library.WEB)) + { + this.generateConfirmationInput(this.libraryEntries.get(0)); + return; + } + + + PanelBuilder panelBuilder = new PanelBuilder().user(this.user).name( + this.user.getTranslation(Constants.TITLE + "library")); + + GuiUtils.fillBorder(panelBuilder); + + this.populateElements(panelBuilder, + this.libraryEntries, + o -> this.createEntryIcon((LibraryEntry) o)); + + if (this.mode == Library.WEB) + { + panelBuilder.item(4, this.createDownloadNow()); + } + + panelBuilder.item(44, this.returnButton); + + panelBuilder.listener(new DownloadCanceller()); + + panelBuilder.build(); + } + + + /** + * This creates download now button, that can skip waiting for automatic request. + * @return PanelItem button that allows to manually download libraries. + */ + private PanelItem createDownloadNow() + { + final String reference = Constants.BUTTON + "download."; + + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + description.add(this.user.getTranslation(reference + + (this.clearCache ? "enabled" : "disabled"))); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "left-click-to-download")); + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-toggle")); + + PanelItemBuilder itemBuilder = new PanelItemBuilder(). + name(this.user.getTranslation(reference + "name")). + description(description). + icon(Material.HOPPER). + glow(this.clearCache); + + itemBuilder.clickHandler((panel, user1, clickType, slot) -> + { + if (clickType.isRightClick()) + { + this.clearCache = !this.clearCache; + panel.getInventory().setItem(slot, this.createDownloadNow().getItem()); + } + else + { + this.addon.getWebManager().requestCatalogGitHubData(this.clearCache); + + // Fix multiclick issue. + if (this.updateTask != null) + { + this.updateTask.cancel(); + } + + // add some delay to rebuilding gui. + this.updateTask = this.addon.getPlugin().getServer().getScheduler().runTaskLater( + this.addon.getPlugin(), + this::build, + 100L); + } + + return true; + }); + + return itemBuilder.build(); + } + + + /** + * This method creates button for given library entry. + * @param libraryEntry LibraryEntry which button must be created. + * @return Entry button. + */ + private PanelItem createEntryIcon(LibraryEntry libraryEntry) + { + PanelItemBuilder itemBuilder = new PanelItemBuilder(). + name(ChatColor.translateAlternateColorCodes('&', libraryEntry.name())). + description(this.generateEntryDescription(libraryEntry)). + description(""). + description(this.user.getTranslation(Constants.TIPS + "click-to-install")). + icon(libraryEntry.icon()). + glow(false); + + itemBuilder.clickHandler((panel, user1, clickType, i) -> { + this.generateConfirmationInput(libraryEntry); + return true; + }); + + return itemBuilder.build(); + } + + + /** + * This method generates consumer and calls ConversationAPI for confirmation that processes file downloading, + * importing and gui opening or closing. + * + * @param libraryEntry Entry that must be processed. + */ + private void generateConfirmationInput(LibraryEntry libraryEntry) + { + Consumer consumer = value -> + { + if (value) + { + switch (this.mode) + { + case TEMPLATE -> { + this.addon.getImportManager().importFile(this.user, + this.world, + libraryEntry.name()); + + CommonPanel.reopen(this.parentPanel != null ? this.parentPanel : this); + } + case DATABASE -> { + this.addon.getImportManager().importDatabaseFile(this.user, + this.world, + libraryEntry.name()); + + CommonPanel.reopen(this.parentPanel != null ? this.parentPanel : this); + } + case WEB -> { + if (!this.blockedForDownland) + { + this.blockedForDownland = true; + + Utils.sendMessage(this.user, this.user.getTranslation( + Constants.MESSAGES + "start-downloading")); + + // Run download task after 5 ticks. + this.updateTask = this.addon.getPlugin().getServer().getScheduler(). + runTaskLaterAsynchronously( + this.addon.getPlugin(), + () -> this.addon.getWebManager().requestEntryGitHubData(this.user, + this.world, + libraryEntry), + 5L); + } + + CommonPanel.reopen(this.parentPanel != null ? this.parentPanel : this); + } + } + } + + if (this.mode.equals(Library.WEB) || this.libraryEntries.size() > 1) + { + this.build(); + } + }; + + ConversationUtils.createConfirmation( + consumer, + this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "confirm-data-replacement", + Constants.GAMEMODE, Utils.getGameMode(this.world)), + this.user.getTranslation(Constants.CONVERSATIONS + "new-challenges-imported", + Constants.GAMEMODE, Utils.getGameMode(this.world))); + } + + + /** + * This method generated description for LibraryEntry object. + * @param entry LibraryEntry object which description must be generated. + * @return List of strings that will be placed in ItemStack lore message. + */ + private List generateEntryDescription(LibraryEntry entry) + { + final String reference = Constants.DESCRIPTIONS + "library."; + + List description = new ArrayList<>(); + + description.add(this.user.getTranslation(reference + "author", + "[author]", entry.author())); + description.add(entry.description()); + + description.add(this.user.getTranslation(reference + "gamemode", + "[gamemode]", entry.gameMode())); + description.add(this.user.getTranslation(reference + "lang", + "[lang]", entry.language())); + description.add(this.user.getTranslation(reference + "version", + "[version]", entry.version())); + + return description; + } + + + /** + * This class allows changing icon for Generator Tier + */ + private class DownloadCanceller implements PanelListener + { + /** + * On inventory click. + * + * @param user the user + * @param event the event + */ + @Override + public void onInventoryClick(User user, InventoryClickEvent event) + { + // do nothing + } + + + /** + * On inventory close. + * + * @param event the event + */ + @Override + public void onInventoryClose(InventoryCloseEvent event) + { + if (LibraryPanel.this.updateTask != null) + { + LibraryPanel.this.updateTask.cancel(); + } + } + + + /** + * Setup current listener. + */ + @Override + public void setup() + { + // Do nothing + } + } + + + /** + * Enum that holds different view modes for current panel. + */ + public enum Library + { + /** + * Mode for templates available in main folder. + */ + TEMPLATE, + /** + * Mode for database files available in main folder. + */ + DATABASE, + /** + * Mode for web library. + */ + WEB + } + + +// --------------------------------------------------------------------- +// Section: Instance Variables +// --------------------------------------------------------------------- + + /** + * Indicates if download now button should trigger cache clearing. + */ + private boolean clearCache; + + /** + * Stores update task that is triggered. + */ + private BukkitTask updateTask = null; + + /** + * This variable will protect against spam-click. + */ + private boolean blockedForDownland; + + /** + * Stores active library that must be searched. + */ + private final Library mode; + + /** + * List of library elements. + */ + private final List libraryEntries; +} diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ListLibraryPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ListLibraryPanel.java deleted file mode 100644 index 7a6c861..0000000 --- a/src/main/java/world/bentobox/challenges/panel/admin/ListLibraryPanel.java +++ /dev/null @@ -1,306 +0,0 @@ -package world.bentobox.challenges.panel.admin; - - -import java.util.ArrayList; -import java.util.List; - -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.scheduler.BukkitTask; - -import world.bentobox.bentobox.api.panels.PanelItem; -import world.bentobox.bentobox.api.panels.PanelListener; -import world.bentobox.bentobox.api.panels.builders.PanelBuilder; -import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.challenges.ChallengesAddon; -import world.bentobox.challenges.panel.CommonPagedPanel; -import world.bentobox.challenges.panel.CommonPanel; -import world.bentobox.challenges.utils.Constants; -import world.bentobox.challenges.utils.GuiUtils; -import world.bentobox.challenges.utils.Utils; -import world.bentobox.challenges.web.object.LibraryEntry; - - -/** - * This class contains all necessary elements to create GUI that lists all challenges. - * It allows to edit them or remove, depending on given input mode. - */ -public class ListLibraryPanel extends CommonPagedPanel -{ - // --------------------------------------------------------------------- - // Section: Constructor - // --------------------------------------------------------------------- - - /** - * @param parentGUI ParentGUI object. - */ - private ListLibraryPanel(CommonPanel parentGUI) - { - super(parentGUI); - } - - - /** - * @param addon Addon where panel operates. - * @param world World from which panel was created. - * @param user User who created panel. - * @param topLabel Command top label which creates panel (f.e. island or ai) - * @param permissionPrefix Command permission prefix (f.e. bskyblock.) - */ - private ListLibraryPanel(ChallengesAddon addon, - World world, - User user, - String topLabel, - String permissionPrefix) - { - super(addon, user, world, topLabel, permissionPrefix); - } - - - /** - * Open the Challenges Admin GUI. - * - * @param addon the addon - * @param world the world - * @param user the user - * @param topLabel the top label - * @param permissionPrefix the permission prefix - */ - public static void open(ChallengesAddon addon, - World world, - User user, - String topLabel, - String permissionPrefix) - { - new ListLibraryPanel(addon, world, user, topLabel, permissionPrefix).build(); - } - - - - /** - * This static method allows to easier open Library GUI. - * @param parentGui ParentGUI object. - */ - public static void open(CommonPanel parentGui) - { - new ListLibraryPanel(parentGui).build(); - } - - - // --------------------------------------------------------------------- - // Section: Methods - // --------------------------------------------------------------------- - - - /** - * {@inheritDoc} - */ - @Override - protected void build() - { - PanelBuilder panelBuilder = new PanelBuilder().user(this.user).name( - this.user.getTranslation(Constants.TITLE + "library")); - - GuiUtils.fillBorder(panelBuilder); - - this.populateElements(panelBuilder, - this.addon.getWebManager().getLibraryEntries(), - o -> this.createEntryIcon((LibraryEntry) o)); - - panelBuilder.item(4, this.createDownloadNow()); - panelBuilder.item(44, this.returnButton); - - panelBuilder.listener(new DownloadCanceller()); - - panelBuilder.build(); - } - - - /** - * This creates download now button, that can skip waiting for automatic request. - * @return PanelItem button that allows to manually download libraries. - */ - private PanelItem createDownloadNow() - { - final String reference = Constants.BUTTON + "download."; - - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - description.add(this.user.getTranslation(reference + - (this.clearCache ? "enabled" : "disabled"))); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "left-click-to-download")); - description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-toggle")); - - PanelItemBuilder itemBuilder = new PanelItemBuilder(). - name(this.user.getTranslation(reference + "name")). - description(description). - icon(Material.HOPPER). - glow(this.clearCache); - - itemBuilder.clickHandler((panel, user1, clickType, slot) -> - { - if (clickType.isRightClick()) - { - this.clearCache = !this.clearCache; - panel.getInventory().setItem(slot, this.createDownloadNow().getItem()); - } - else - { - this.addon.getWebManager().requestCatalogGitHubData(this.clearCache); - - // Fix multiclick issue. - if (this.updateTask != null) - { - this.updateTask.cancel(); - } - - // add some delay to rebuilding gui. - this.updateTask = this.addon.getPlugin().getServer().getScheduler().runTaskLater( - this.addon.getPlugin(), - this::build, - 100L); - } - - return true; - }); - - return itemBuilder.build(); - } - - - /** - * This method creates button for given library entry. - * @param libraryEntry LibraryEntry which button must be created. - * @return Entry button. - */ - private PanelItem createEntryIcon(LibraryEntry libraryEntry) - { - PanelItemBuilder itemBuilder = new PanelItemBuilder(). - name(ChatColor.translateAlternateColorCodes('&', libraryEntry.getName())). - description(this.generateEntryDescription(libraryEntry)). - description(""). - description(this.user.getTranslation(Constants.TIPS + "click-to-install")). - icon(libraryEntry.getIcon()). - glow(false); - - itemBuilder.clickHandler((panel, user1, clickType, i) -> { - - if (!this.blockedForDownland) - { - this.blockedForDownland = true; - - Utils.sendMessage(this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "start-downloading")); - - // Run download task after 5 ticks. - this.addon.getPlugin().getServer().getScheduler(). - runTaskLaterAsynchronously( - this.addon.getPlugin(), - () -> this.addon.getWebManager().requestEntryGitHubData(this.user, this.world, libraryEntry), - 5L); - - this.build(); - } - - return true; - }); - - return itemBuilder.build(); - } - - - /** - * This method generated description for LibraryEntry object. - * @param entry LibraryEntry object which description must be generated. - * @return List of strings that will be placed in ItemStack lore message. - */ - private List generateEntryDescription(LibraryEntry entry) - { - final String reference = Constants.DESCRIPTIONS + "library."; - - List description = new ArrayList<>(); - - description.add(this.user.getTranslation(reference + "author", - "[author]", entry.getAuthor())); - description.add(entry.getDescription()); - - description.add(this.user.getTranslation(reference + "gamemode", - "[gamemode]", entry.getForGameMode())); - description.add(this.user.getTranslation(reference + "lang", - "[lang]", entry.getLanguage())); - description.add(this.user.getTranslation(reference + "version", - "[version]", entry.getVersion())); - - return description; - } - - - /** - * This class allows changing icon for Generator Tier - */ - private class DownloadCanceller implements PanelListener - { - /** - * On inventory click. - * - * @param user the user - * @param event the event - */ - @Override - public void onInventoryClick(User user, InventoryClickEvent event) - { - // do nothing - } - - - /** - * On inventory close. - * - * @param event the event - */ - @Override - public void onInventoryClose(InventoryCloseEvent event) - { - if (ListLibraryPanel.this.updateTask != null) - { - ListLibraryPanel.this.updateTask.cancel(); - } - } - - - /** - * Setup current listener. - */ - @Override - public void setup() - { - // Do nothing - } - } - - -// --------------------------------------------------------------------- -// Section: Instance Variables -// --------------------------------------------------------------------- - - /** - * Indicates if download now button should trigger cache clearing. - */ - private boolean clearCache; - - /** - * Stores update task that is triggered. - */ - private BukkitTask updateTask = null; - - /** - * This variable will protect against spam-click. - */ - private boolean blockedForDownland; -} diff --git a/src/main/java/world/bentobox/challenges/web/WebManager.java b/src/main/java/world/bentobox/challenges/web/WebManager.java index 702d7ef..23a4a01 100644 --- a/src/main/java/world/bentobox/challenges/web/WebManager.java +++ b/src/main/java/world/bentobox/challenges/web/WebManager.java @@ -115,7 +115,7 @@ public class WebManager JsonObject catalog = new JsonParser().parse(catalogContent).getAsJsonObject(); catalog.getAsJsonArray("challenges").forEach(gamemode -> - this.library.add(new LibraryEntry(gamemode.getAsJsonObject()))); + this.library.add(LibraryEntry.fromJson(gamemode.getAsJsonObject()))); } }); } @@ -142,7 +142,7 @@ public class WebManager try { challengeLibrary = gitHubWebAPI.getRepository("BentoBoxWorld", "weblink"). - getContent("challenges/library/" + entry.getRepository() + ".json"). + getContent("challenges/library/" + entry.repository() + ".json"). getContent(). replaceAll("\\n", ""); } @@ -192,7 +192,7 @@ public class WebManager public List getLibraryEntries() { List entries = new ArrayList<>(this.library); - entries.sort(Comparator.comparingInt(LibraryEntry::getSlot)); + entries.sort(Comparator.comparingInt(LibraryEntry::slot)); return entries; } @@ -216,15 +216,15 @@ public class WebManager /** * Challenges Addon variable. */ - private ChallengesAddon addon; + private final ChallengesAddon addon; /** * BentoBox plugin variable. */ - private BentoBox plugin; + private final BentoBox plugin; /** * This list contains all entries that were downloaded from GitHub. */ - private List library; + private final List library; } diff --git a/src/main/java/world/bentobox/challenges/web/object/LibraryEntry.java b/src/main/java/world/bentobox/challenges/web/object/LibraryEntry.java index 69e6528..c31ac54 100644 --- a/src/main/java/world/bentobox/challenges/web/object/LibraryEntry.java +++ b/src/main/java/world/bentobox/challenges/web/object/LibraryEntry.java @@ -3,185 +3,47 @@ package world.bentobox.challenges.web.object; import org.bukkit.Material; import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; import com.google.gson.JsonObject; /** - * This objects allows to load each Challenges Catalog library entry. + * The type Library entry. + * @param name Name of the library entry + * @param icon Icon of the library entry + * @param description Description of the library entry + * @param repository Link to the repository + * @param language Language of the entry + * @param slot order of the entry + * @param gameMode Made primary for gamemode + * @param author Author of the entry + * @param version version of the challenges. */ -public class LibraryEntry +public record LibraryEntry(String name, Material icon, String description, String repository, + String language, int slot, String gameMode, String author, String version) { /** * Default constructor. * @param object Json Object that must be translated to LibraryEntry. */ - public LibraryEntry(@NonNull JsonObject object) + public static LibraryEntry fromJson(@NonNull JsonObject object) { - this.name = object.get("name").getAsString(); - Material material = Material.matchMaterial(object.get("icon").getAsString()); - this.icon = (material != null) ? material : Material.PAPER; - this.description = object.get("description").getAsString(); - this.repository = object.get("repository").getAsString(); - this.language = object.get("language").getAsString(); - - this.slot = object.get("slot").getAsInt(); - - this.forGameMode = object.get("for").getAsString(); - this.author = object.get("author").getAsString(); - this.version = object.get("version").getAsString(); + return new LibraryEntry(object.get("name").getAsString(), + (material != null) ? material : Material.PAPER, + object.get("description").getAsString(), + object.get("repository").getAsString(), + object.get("language").getAsString(), + object.get("slot").getAsInt(), + object.get("for").getAsString(), + object.get("author").getAsString(), + object.get("version").getAsString()); } - /** - * This method returns the name value. - * @return the value of name. - */ - @NonNull - public String getName() + public static LibraryEntry fromTemplate(String name, Material icon) { - return name; + return new LibraryEntry(name, icon, "", "", "", 0, "", "", ""); } - - - /** - * This method returns the icon value. - * @return the value of icon. - */ - @NonNull - public Material getIcon() - { - return icon; - } - - - /** - * This method returns the description value. - * @return the value of description. - */ - @NonNull - public String getDescription() - { - return description; - } - - - /** - * This method returns the repository value. - * @return the value of repository. - */ - @NonNull - public String getRepository() - { - return repository; - } - - - /** - * This method returns the language value. - * @return the value of language. - */ - @NonNull - public String getLanguage() - { - return language; - } - - - /** - * This method returns the slot value. - * @return the value of slot. - */ - @NonNull - public int getSlot() - { - return slot; - } - - - /** - * This method returns the forGameMode value. - * @return the value of forGameMode. - */ - @NonNull - public String getForGameMode() - { - return forGameMode; - } - - - /** - * This method returns the author value. - * @return the value of author. - */ - @NonNull - public String getAuthor() - { - return author; - } - - - /** - * This method returns the version value. - * @return the value of version. - */ - @NonNull - public String getVersion() - { - return version; - } - - -// --------------------------------------------------------------------- -// Section: Variables -// --------------------------------------------------------------------- - - - /** - * Name of entry object - */ - private @NonNull String name; - - /** - * Defaults to {@link Material#PAPER}. - */ - private @NonNull Material icon; - - /** - * Description of entry object. - */ - private @NonNull String description; - - /** - * File name in challenges library. - */ - private @NonNull String repository; - - /** - * Language of content. - */ - private @Nullable String language; - - /** - * Desired slot number. - */ - private int slot; - - /** - * Main GameMode for which challenges were created. - */ - private @Nullable String forGameMode; - - /** - * Author (-s) who created current configuration. - */ - private @Nullable String author; - - /** - * Version of Challenges Addon, for which challenges were created. - */ - private @Nullable String version; } \ No newline at end of file diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 0930350..cb860f0 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -189,14 +189,21 @@ challenges: description: |- &7 Opens a public &7 challenges library. - import_challenges: - name: "&f&l Import Challenges" + import_database: + name: "&f&l Import Database" description: |- - &7 Allows to import challenges. + &7 Allows to import exported + &7 challenges database. + import_template: + name: "&f&l Import Template" + description: |- + &7 Allows to import template + &7 file with challenges. export_challenges: name: "&f&l Export Challenges" description: |- - &7 Allows to export challenges. + &7 Allows to export database + &7 to a local file. properties: name: "&f&l Properties" description: |- @@ -1046,6 +1053,16 @@ challenges: start-downloading: "&a Starting to download and import Challenges Library." # Message that appears when writing multiline text. written-text: "&a Input Text:" + # Message that appears after importing library data into database. + confirm-data-replacement: "&e Please confirm that you want to replace your current challenges with new one." + # Message that appears after successful data importing + new-challenges-imported: "&a Success, new Challenges for [gamemode] were imported." + # Message that appears after admin clicks on database exporting button. + exported-file-name: "&e Please enter a file name for the exported database file. (write 'cancel' to exit)" + # Message that appears after successful database exporting to file. + database-export-completed: "&a Success, the database export for [world] is completed. File [file] generated." + # Message that appears if input file name is already taken. + file-name-exist: "&c File with name '[id]' exists. Cannot overwrite." titles: # Title and subtitle may contain variables in [] that will be replaced with a proper message from the challenge object. # [friendlyName] will be replaced with challenge friendly name. @@ -1131,6 +1148,7 @@ challenges: no-multiple-permission: "&c You do not have permission to complete this challenge multiple times at once." invalid-level: "&c Level [level] contains invalid data. It will not be loaded from database!" invalid-challenge: "&c Challenge [challenge] contains invalid data. It will not be loaded from database!" + no-library-entries: "&c Cannot find any library entries. Nothing to show." protection: flags: CHALLENGES_ISLAND_PROTECTION: diff --git a/src/test/java/world/bentobox/challenges/ChallengesManagerTest.java b/src/test/java/world/bentobox/challenges/ChallengesManagerTest.java index abd2744..de4c94b 100644 --- a/src/test/java/world/bentobox/challenges/ChallengesManagerTest.java +++ b/src/test/java/world/bentobox/challenges/ChallengesManagerTest.java @@ -352,7 +352,7 @@ public class ChallengesManagerTest { } /** - * Test method for {@link ChallengesManager#wipeDatabase(boolean)}. + * Test method for {@link ChallengesManager#wipeDatabase(boolean, String)}. * @throws InterruptedException */ @Test @@ -376,19 +376,19 @@ public class ChallengesManagerTest { assertTrue(checkPd.exists()); // Wipe it - cm.wipeDatabase(false); + cm.wipeDatabase(false, ""); // Verify assertFalse(check.exists()); assertFalse(checkLv.exists()); assertTrue(checkPd.exists()); - cm.wipeDatabase(true); + cm.wipeDatabase(true, ""); assertFalse(checkPd.exists()); } /** - * Test method for {@link ChallengesManager#wipePlayers()}. + * Test method for {@link ChallengesManager#wipePlayers(String)}. * @throws InterruptedException */ @Test @@ -399,7 +399,7 @@ public class ChallengesManagerTest { File plData = new File(database, "ChallengesPlayerData"); File checkLv = new File(plData, playerID.toString() + ".json"); assertTrue(checkLv.exists()); - cm.wipePlayers(); + cm.wipePlayers(""); assertFalse(checkLv.exists()); }