diff --git a/README.md b/README.md index f95d526..e8e3dee 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,12 @@ Add-on for BentoBox to provide challenges for any BentoBox GameMode. ## Where to find Currently Challenges Addon is in **Beta stage**, so it may or may not contain bugs... a lot of bugs. Also it means, that some features are not working or implemented. -Latest official **Beta Release is 0.8.1**, and you can download it from [Release tab](https://github.com/BentoBoxWorld/Challenges/releases) -But it will work with BentoBox 1.6.x and BentoBox 1.7.x. +Latest official **Beta Release is 0.8.3**, and you can download it from [Release tab](https://github.com/BentoBoxWorld/Challenges/releases) +But it will work with BentoBox 1.14. -Latest development builds will be based on **Minecraft 1.14.4** and **BentoBox 1.8.0**. +Latest development builds will be based on **Minecraft 1.16.1** and **BentoBox 1.14.0**. **Nightly builds** are available in [Jenkins Server](https://ci.codemc.org/job/BentoBoxWorld/job/Challenges/lastStableBuild/). -Be aware that 0.8.0 stores data differently than it is in 0.7.5 and below. It will be necessary to migrate data via command `/[gamemode_admin] challenges migrate`. - If you like this addon but something is missing or is not working as you want, you can always submit an [Issue request](https://github.com/BentoBoxWorld/Challenges/issues) or get a support in Discord [BentoBox ![icon](https://avatars2.githubusercontent.com/u/41555324?s=15&v=4)](https://discord.bentobox.world) ## Translations @@ -38,7 +36,7 @@ There exist also Web Library, where users can download public challenges. It is ## Compatibility -- [x] BentoBox - 1.6.x and 1.7.x versions +- [x] BentoBox - 1.14 versions - [x] BSkyBlock - [x] AcidIsland - [x] SkyGrid @@ -46,4 +44,4 @@ There exist also Web Library, where users can download public challenges. It is ## Information -More information can be found in [Wiki Pages](https://github.com/BentoBoxWorld/Challenges/wiki). +More information can be found in [Wiki Pages](https://docs.bentobox.world/addons/Challenges/). diff --git a/pom.xml b/pom.xml index bc35c4d..ca3beb8 100644 --- a/pom.xml +++ b/pom.xml @@ -35,14 +35,14 @@ 1.8 2.0.2 - 1.14.4-R0.1-SNAPSHOT - 1.7.0 + 1.15.2-R0.1-SNAPSHOT + 1.14.0 1.6.0 1.7 ${build.version}-SNAPSHOT - 0.8.2 + 0.8.3 -LOCAL diff --git a/src/main/java/world/bentobox/challenges/ChallengesAddon.java b/src/main/java/world/bentobox/challenges/ChallengesAddon.java index e2fd286..8436dc5 100644 --- a/src/main/java/world/bentobox/challenges/ChallengesAddon.java +++ b/src/main/java/world/bentobox/challenges/ChallengesAddon.java @@ -179,6 +179,9 @@ public class ChallengesAddon extends Addon { CHALLENGES_ISLAND_PROTECTION.addGameModeAddon(gameModeAddon); this.registerPlaceholders(gameModeAddon); + + // TODO: this is old placeholders. Remove when backward compatibility ends. + this.registerPlaceholdersOld(gameModeAddon); } }); @@ -283,7 +286,14 @@ public class ChallengesAddon extends Addon { if (this.hooked) { this.challengesManager.save(); } + } + + /** + * This method saves addon settings into file. + */ + public void saveSettings() + { if (this.settings != null) { new Config<>(this, Settings.class).saveConfigObject(this.settings); @@ -320,6 +330,93 @@ public class ChallengesAddon extends Addon { * @param gameModeAddon GameMode addon where placeholders must be hooked in. */ private void registerPlaceholders(GameModeAddon gameModeAddon) + { + final String addonName = this.getDescription().getName().toLowerCase(); + final World world = gameModeAddon.getOverWorld(); + + // Number of completions for all challenges placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_total_completion_count", + user -> String.valueOf(this.challengesManager.getTotalChallengeCompletionCount(user, world))); + + // Completed challenge count placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_completed_count", + user -> String.valueOf(this.challengesManager.getCompletedChallengeCount(user, world))); + + // Uncompleted challenge count placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_uncompleted_count", + user -> String.valueOf(this.challengesManager.getChallengeCount(world) - + this.challengesManager.getCompletedChallengeCount(user, world))); + + // Completed challenge level count placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_completed_level_count", + user -> String.valueOf(this.challengesManager.getCompletedLevelCount(user, world))); + + // Uncompleted challenge level count placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_uncompleted_level_count", + user -> String.valueOf(this.challengesManager.getLevelCount(world) - + this.challengesManager.getCompletedLevelCount(user, world))); + + // Unlocked challenge level count placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_unlocked_level_count", + user -> String.valueOf(this.challengesManager.getLevelCount(world) - + this.challengesManager.getUnlockedLevelCount(user, world))); + + // Locked challenge level count placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_locked_level_count", + user -> String.valueOf(this.challengesManager.getLevelCount(world) - + this.challengesManager.getUnlockedLevelCount(user, world))); + + // Latest challenge level name placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_latest_level_name", + user -> { + ChallengeLevel level = this.challengesManager.getLatestUnlockedLevel(user, world); + return level != null ? level.getFriendlyName() : ""; + }); + + // Latest challenge level id placeholder + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_latest_level_id", + user -> { + ChallengeLevel level = this.challengesManager.getLatestUnlockedLevel(user, world); + return level != null ? level.getUniqueId() : ""; + }); + + // Completed challenge count in latest level + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_latest_level_completed_count", + user -> { + ChallengeLevel level = this.challengesManager.getLatestUnlockedLevel(user, world); + return String.valueOf(level != null ? + this.challengesManager.getLevelCompletedChallengeCount(user, world, level) : 0); + }); + + // Uncompleted challenge count in latest level + this.getPlugin().getPlaceholdersManager().registerPlaceholder(gameModeAddon, + addonName + "_latest_level_uncompleted_count", + user -> { + ChallengeLevel level = this.challengesManager.getLatestUnlockedLevel(user, world); + return String.valueOf(level != null ? + level.getChallenges().size() - this.challengesManager.getLevelCompletedChallengeCount(user, world, level) : 0); + }); + } + + + /** + * This method registers placeholders into GameMode addon. + * @param gameModeAddon GameMode addon where placeholders must be hooked in. + * @since 0.8.1 + * @deprecated remove after 0.9.0 + */ + @Deprecated + private void registerPlaceholdersOld(GameModeAddon gameModeAddon) { final String gameMode = gameModeAddon.getDescription().getName().toLowerCase(); final World world = gameModeAddon.getOverWorld(); diff --git a/src/main/java/world/bentobox/challenges/ChallengesImportManager.java b/src/main/java/world/bentobox/challenges/ChallengesImportManager.java index e0feb65..36b5655 100644 --- a/src/main/java/world/bentobox/challenges/ChallengesImportManager.java +++ b/src/main/java/world/bentobox/challenges/ChallengesImportManager.java @@ -126,7 +126,8 @@ public class ChallengesImportManager return false; } - this.addon.getChallengesManager().save(); + this.addon.getChallengesManager().saveChallenges(); + this.addon.getChallengesManager().saveLevels(); if (removeAtEnd) { @@ -203,7 +204,8 @@ public class ChallengesImportManager return false; } - this.addon.getChallengesManager().save(); + this.addon.getChallengesManager().saveChallenges(); + this.addon.getChallengesManager().saveLevels(); return true; } diff --git a/src/main/java/world/bentobox/challenges/ChallengesManager.java b/src/main/java/world/bentobox/challenges/ChallengesManager.java index 26e7653..6be8e14 100644 --- a/src/main/java/world/bentobox/challenges/ChallengesManager.java +++ b/src/main/java/world/bentobox/challenges/ChallengesManager.java @@ -206,11 +206,15 @@ public class ChallengesManager } this.playerCacheData.clear(); - loadAndValidate(); + this.loadAndValidate(); } - private void loadAndValidate() { + /** + * This method loads and validates all challenges and levels. + */ + private void loadAndValidate() + { this.challengeDatabase.loadObjects().forEach(this::loadChallenge); this.levelDatabase.loadObjects().forEach(this::loadLevel); // this validate challenge levels @@ -233,7 +237,7 @@ public class ChallengesManager //this.levelDatabase = new Database<>(addon, ChallengeLevel.class); //this.playersDatabase = new Database<>(addon, ChallengesPlayerData.class); - loadAndValidate(); + this.loadAndValidate(); } @@ -263,6 +267,18 @@ public class ChallengesManager User user, boolean silent) { + // This may happen if database somehow failed to load challenge and return + // null as input. + if (challenge == null) + { + if (!silent) + { + user.sendMessage("load-error", "[value]", "NULL"); + } + + return false; + } + if (this.challengeCacheData.containsKey(challenge.getUniqueId())) { if (!overwrite) @@ -324,6 +340,18 @@ public class ChallengesManager User user, boolean silent) { + // This may happen if database somehow failed to load challenge and return + // null as input. + if (level == null) + { + if (!silent) + { + user.sendMessage("load-error", "[value]", "NULL"); + } + + return false; + } + if (!this.isValidLevel(level)) { if (user != null) @@ -402,12 +430,15 @@ public class ChallengesManager */ public void removeFromCache(UUID playerID) { - if (!this.settings.isStoreAsIslandData() && this.playerCacheData.containsKey(playerID.toString())) - { - // save before remove - this.savePlayerData(playerID.toString()); - this.playerCacheData.remove(playerID.toString()); - } +// Remove due possible issues with saving... (#246) +// if (!this.settings.isStoreAsIslandData() && this.playerCacheData.containsKey(playerID.toString())) +// { +// // save before remove +// this.savePlayerData(playerID.toString()); +// this.playerCacheData.remove(playerID.toString()); +// } + + this.savePlayerData(playerID.toString()); // TODO: It would be necessary to remove also data, if they stores islands. // Unfortunately, I do not know all worlds. Checking everything would be bad. Probably, I could @@ -474,11 +505,8 @@ public class ChallengesManager { if (!this.challengeCacheData.containsKey(uniqueID)) { - if (this.challengeDatabase.objectExists(uniqueID)) - { - this.loadChallenge(this.challengeDatabase.loadObject(uniqueID)); - } - else + if (!this.challengeDatabase.objectExists(uniqueID) || + !this.loadChallenge(this.challengeDatabase.loadObject(uniqueID), false, null, true)) { this.addon.logError("Cannot find " + uniqueID + " challenge for " + level.getUniqueId()); return false; @@ -524,7 +552,7 @@ public class ChallengesManager { // Create the player data ChallengesPlayerData pd = new ChallengesPlayerData(uniqueID); - this.playersDatabase.saveObject(pd); + this.playersDatabase.saveObjectAsync(pd); // Add to cache this.playerCacheData.put(uniqueID, pd); } @@ -671,7 +699,7 @@ public class ChallengesManager challengesID.forEach(challenge -> level.getChallenges().add(addonName + challenge.substring(world.getName().length()))); - this.levelDatabase.saveObject(level); + this.levelDatabase.saveObjectAsync(level); this.levelCacheData.put(level.getUniqueId(), level); updated = true; @@ -715,7 +743,7 @@ public class ChallengesManager updated = true; - this.challengeDatabase.saveObject(challenge); + this.challengeDatabase.saveObjectAsync(challenge); this.challengeCacheData.put(challenge.getUniqueId(), challenge); } @@ -758,7 +786,7 @@ public class ChallengesManager // This save should not involve any upgrades in other parts. - this.challengeDatabase.saveObject(challenge); + this.challengeDatabase.saveObjectAsync(challenge); this.challengeCacheData.put(challenge.getUniqueId(), challenge); } } @@ -809,7 +837,7 @@ public class ChallengesManager } }); - this.playersDatabase.saveObject(playerData); + this.playersDatabase.saveObjectAsync(playerData); }); } @@ -824,8 +852,11 @@ public class ChallengesManager */ public void save() { - this.saveChallenges(); - this.saveLevels(); + // Challenges and Levels are saved on modifications only to avoid issues with + // NULL's in data after interrupting server while in saving stage. + // this.saveChallenges(); + // this.saveLevels(); + this.savePlayersData(); } @@ -833,9 +864,9 @@ public class ChallengesManager /** * This method saves all challenges to database. */ - private void saveChallenges() + public void saveChallenges() { - this.challengeCacheData.values().forEach(this.challengeDatabase::saveObject); + this.challengeCacheData.values().forEach(this::saveChallenge); } @@ -845,16 +876,16 @@ public class ChallengesManager */ public void saveChallenge(Challenge challenge) { - this.challengeDatabase.saveObject(challenge); + this.challengeDatabase.saveObjectAsync(challenge); } /** * This method saves all levels to database. */ - private void saveLevels() + public void saveLevels() { - this.levelCacheData.values().forEach(this.levelDatabase::saveObject); + this.levelCacheData.values().forEach(this::saveLevel); } @@ -864,7 +895,7 @@ public class ChallengesManager */ public void saveLevel(ChallengeLevel level) { - this.levelDatabase.saveObject(level); + this.levelDatabase.saveObjectAsync(level); } @@ -873,7 +904,7 @@ public class ChallengesManager */ private void savePlayersData() { - this.playerCacheData.values().forEach(this.playersDatabase::saveObject); + this.playerCacheData.values().forEach(this.playersDatabase::saveObjectAsync); } @@ -902,7 +933,7 @@ public class ChallengesManager } } - this.playersDatabase.saveObject(cachedData); + this.playersDatabase.saveObjectAsync(cachedData); } } @@ -1053,7 +1084,20 @@ public class ChallengesManager private void resetAllChallenges(@NonNull String storageDataID, @NonNull String gameMode) { this.addPlayerData(storageDataID); - this.playerCacheData.get(storageDataID).reset(gameMode); + + if (this.playerCacheData.containsKey(storageDataID)) + { + // There may be a rare situation when player data cannot be loaded. Just avoid + // error. + this.playerCacheData.get(storageDataID).reset(gameMode); + } + else + { + // If object cannot be loaded remove it completely. + this.playersDatabase.deleteID(storageDataID); + this.addon.logError("Database object was not loaded. It is removed completely. Object Id: " + storageDataID); + } + // Save this.savePlayerData(storageDataID); } @@ -1079,6 +1123,7 @@ public class ChallengesManager // The first level is always unlocked and previous for it is null. ChallengeLevel previousLevel = null; int doneChallengeCount = 0; + boolean previousUnlocked = true; // For each challenge level, check how many the storageDataID has done for (ChallengeLevel level : challengeLevelList) @@ -1088,20 +1133,25 @@ public class ChallengesManager // remove waiver amount to get count of challenges that still necessary to do. int challengesToDo = previousLevel == null ? 0 : - (previousLevel.getChallenges().size() - doneChallengeCount - level.getWaiverAmount()); + (previousLevel.getChallenges().size() - doneChallengeCount - previousLevel.getWaiverAmount()); // As level already contains unique ids of challenges, just iterate through them. doneChallengeCount = (int) level.getChallenges().stream().filter(playerData::isChallengeDone).count(); + // Mark if level is unlocked + boolean unlocked = previousUnlocked && challengesToDo <= 0; + result.add(new LevelStatus( level, previousLevel, challengesToDo, level.getChallenges().size() == doneChallengeCount, - challengesToDo <= 0)); + unlocked)); previousLevel = level; + previousUnlocked = unlocked; } + return result; } @@ -1132,7 +1182,7 @@ public class ChallengesManager ChallengeLevel previousLevel = levelIndex < 1 ? null : challengeLevelList.get(levelIndex - 1); int challengesToDo = previousLevel == null ? 0 : - (previousLevel.getChallenges().size() - level.getWaiverAmount()) - + (previousLevel.getChallenges().size() - previousLevel.getWaiverAmount()) - (int) previousLevel.getChallenges().stream().filter(playerData::isChallengeDone).count(); // As level already contains unique ids of challenges, just iterate through them. @@ -1362,7 +1412,7 @@ public class ChallengesManager * @param world - World where challenges must be reset. * @param adminID - admin iD */ - public void resetAllChallenges(UUID userID, World world, UUID adminID) + public void resetAllChallenges(@NonNull UUID userID, World world, @Nullable UUID adminID) { String storageID = this.getDataUniqueID(userID, Util.getWorld(world)); @@ -1526,12 +1576,12 @@ public class ChallengesManager LevelStatus lastStatus = null; for (Iterator statusIterator = this.getAllChallengeLevelStatus(user, world).iterator(); - statusIterator.hasNext() && (lastStatus == null || !lastStatus.isUnlocked());) + statusIterator.hasNext() && (lastStatus == null || lastStatus.isUnlocked());) { lastStatus = statusIterator.next(); } - return lastStatus != null ? lastStatus.getLevel() : null; + return lastStatus != null ? lastStatus.getPreviousLevel() : null; } @@ -1752,6 +1802,21 @@ public class ChallengesManager } + /** + * This method returns completed challenge count in given level. + * @param user User which should be checked + * @param world World where challenges are operating + * @param level Level which challenges must be checked. + * @return Number of completed challenges in given level. + */ + public long getLevelCompletedChallengeCount(User user, World world, ChallengeLevel level) + { + return this.getLevelChallenges(level).stream(). + filter(challenge -> this.getChallengeTimes(user, world, challenge) > 0). + count(); + } + + // --------------------------------------------------------------------- // Section: Level related methods // --------------------------------------------------------------------- diff --git a/src/main/java/world/bentobox/challenges/config/Settings.java b/src/main/java/world/bentobox/challenges/config/Settings.java index 49df416..2332b46 100644 --- a/src/main/java/world/bentobox/challenges/config/Settings.java +++ b/src/main/java/world/bentobox/challenges/config/Settings.java @@ -1,7 +1,7 @@ package world.bentobox.challenges.config; -import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -34,14 +34,14 @@ public class Settings implements ConfigObject @ConfigComment("Allows to define common challenges command that will open User GUI") @ConfigComment("with all GameMode selection or Challenges from user world.") @ConfigComment("This will not affect /{gamemode_user} challenges command.") - @ConfigEntry(path = "commands.user", needsReset = true) + @ConfigEntry(path = "commands.user", needsRestart = true) private String userCommand = "challenges c"; @ConfigComment("") @ConfigComment("Allows to define common challenges command that will open Admin GUI") @ConfigComment("with all GameMode selection.") @ConfigComment("This will not affect /{gamemode_admin} challenges command.") - @ConfigEntry(path = "commands.admin", needsReset = true) + @ConfigEntry(path = "commands.admin", needsRestart = true) private String adminCommand = "challengesadmin chadmin"; @ConfigComment("") @@ -49,7 +49,7 @@ public class Settings implements ConfigObject @ConfigComment("all GameModes. For admins it will open selection with all GameModes") @ConfigComment("(unless there is one), but for users it will open GUI that corresponds") @ConfigComment("to their world (unless it is specified other way in Admin GUI).") - @ConfigEntry(path = "commands.single-gui", needsReset = true) + @ConfigEntry(path = "commands.single-gui", needsRestart = true) private boolean useCommonGUI = false; @ConfigComment("") @@ -130,7 +130,7 @@ public class Settings implements ConfigObject @ConfigComment("Requirement and reward items, blocks and entities that are defined in challenge and can be customized under 'challenges.gui.item-description.*'") @ConfigEntry(path = "gui-settings.challenge-lore") @Adapter(ChallengeLoreAdapter.class) - private List challengeLoreMessage = new ArrayList<>(); + private List challengeLoreMessage = Arrays.asList(ChallengeLore.values()); @ConfigComment("") @ConfigComment("This string allows to change element order in Level description. Each letter represents") @@ -149,7 +149,7 @@ public class Settings implements ConfigObject @ConfigComment("Reward items that are defined in challenge level and can be customized under 'challenges.gui.item-description.*'") @ConfigEntry(path = "gui-settings.level-lore") @Adapter(LevelLoreAdapter.class) - private List levelLoreMessage = new ArrayList<>(); + private List levelLoreMessage = Arrays.asList(LevelLore.values()); @ConfigComment("") @ConfigComment("This indicate if challenges data will be stored per island (true) or per player (false).") diff --git a/src/main/java/world/bentobox/challenges/database/object/Challenge.java b/src/main/java/world/bentobox/challenges/database/object/Challenge.java index cff291f..a8990cc 100644 --- a/src/main/java/world/bentobox/challenges/database/object/Challenge.java +++ b/src/main/java/world/bentobox/challenges/database/object/Challenge.java @@ -19,6 +19,8 @@ import com.google.gson.annotations.Expose; import com.google.gson.annotations.JsonAdapter; import world.bentobox.bentobox.database.objects.DataObject; +import world.bentobox.bentobox.database.objects.Table; +import world.bentobox.challenges.database.object.adapters.EntityCompatibilityAdapter; import world.bentobox.challenges.database.object.adapters.RequirementsAdapter; import world.bentobox.challenges.database.object.requirements.Requirements; @@ -28,6 +30,7 @@ import world.bentobox.challenges.database.object.requirements.Requirements; * @author tastybento * */ +@Table(name = "Challenge") public class Challenge implements DataObject { /** @@ -156,6 +159,7 @@ public class Challenge implements DataObject @Deprecated @Expose + @JsonAdapter(EntityCompatibilityAdapter.class) private Map requiredEntities = new EnumMap<>(EntityType.class); @Deprecated diff --git a/src/main/java/world/bentobox/challenges/database/object/ChallengeLevel.java b/src/main/java/world/bentobox/challenges/database/object/ChallengeLevel.java index 7fb26bf..63c108d 100644 --- a/src/main/java/world/bentobox/challenges/database/object/ChallengeLevel.java +++ b/src/main/java/world/bentobox/challenges/database/object/ChallengeLevel.java @@ -14,6 +14,7 @@ import com.google.gson.annotations.Expose; import world.bentobox.bentobox.api.configuration.ConfigComment; import world.bentobox.bentobox.database.objects.DataObject; +import world.bentobox.bentobox.database.objects.Table; import world.bentobox.challenges.ChallengesManager; /** @@ -21,6 +22,7 @@ import world.bentobox.challenges.ChallengesManager; * @author tastybento * */ +@Table(name = "ChallengeLevel") public class ChallengeLevel implements DataObject, Comparable { /** diff --git a/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java b/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java index db306a9..c1de447 100644 --- a/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java +++ b/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java @@ -14,6 +14,7 @@ import com.google.gson.annotations.Expose; import world.bentobox.bentobox.api.logs.LogEntry; import world.bentobox.bentobox.database.objects.DataObject; +import world.bentobox.bentobox.database.objects.Table; import world.bentobox.bentobox.database.objects.adapters.Adapter; import world.bentobox.bentobox.database.objects.adapters.LogEntryListAdapter; @@ -23,6 +24,7 @@ import world.bentobox.bentobox.database.objects.adapters.LogEntryListAdapter; * @author tastybento * */ +@Table(name = "ChallengesPlayerData") public class ChallengesPlayerData implements DataObject { /** diff --git a/src/main/java/world/bentobox/challenges/database/object/adapters/EntityCompatibilityAdapter.java b/src/main/java/world/bentobox/challenges/database/object/adapters/EntityCompatibilityAdapter.java new file mode 100644 index 0000000..b64a32f --- /dev/null +++ b/src/main/java/world/bentobox/challenges/database/object/adapters/EntityCompatibilityAdapter.java @@ -0,0 +1,92 @@ +// +// Created by BONNe +// Copyright - 2020 +// + + +package world.bentobox.challenges.database.object.adapters; + + +import com.google.gson.*; +import org.bukkit.entity.EntityType; +import java.lang.reflect.Type; +import java.util.EnumMap; +import java.util.Map; + +import world.bentobox.bentobox.BentoBox; + + +/** + * This is compatibility class for dealing with Mojang renamed entities. + * Created for update 1.16. + */ +public class EntityCompatibilityAdapter implements + JsonSerializer>, JsonDeserializer> +{ + /** + * This method serializes input map as String Key and Integer value. + * @param src EnumMap that contains EntityType as key and Integer as value. + * @return serialized JsonElement. + */ + @Override + public JsonElement serialize(Map src, Type typeOfSrc, JsonSerializationContext context) + { + JsonObject jsonArray = new JsonObject(); + + src.forEach((entity, number) -> + { + jsonArray.addProperty(entity.name(), number); + }); + + return jsonArray; + } + + + /** + * This method deserializes json object that stores Entity Name and amount as integer. + * @param json Json element that must be parsed. + * @return EnumMap that contains EntityType as key and Integer as value. + * @throws JsonParseException + */ + @Override + public Map deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) + throws JsonParseException + { + Map map = new EnumMap<>(EntityType.class); + + for (Map.Entry entrySet : json.getAsJsonObject().entrySet()) + { + try + { + EntityType entityType = EntityType.valueOf(entrySet.getKey()); + map.put(entityType, entrySet.getValue().getAsInt()); + } + catch (IllegalArgumentException e) + { + if (entrySet.getKey().equals("PIG_ZOMBIE")) + { + // Hacky way how to get new entity name. + map.put(EntityType.valueOf("ZOMBIFIED_PIGLIN"), + entrySet.getValue().getAsInt()); + } + else if (entrySet.getKey().equals("ZOMBIFIED_PIGLIN")) + { + // Hacky way how to get new entity name. + map.put(EntityType.valueOf("PIG_ZOMBIE"), + entrySet.getValue().getAsInt()); + } + else + { + // No replacement for new entities in older server. + BentoBox.getInstance().logWarning("[ChallengesAddon] Entity with name `" + + entrySet.getKey() + "` does not exist in your Minecraft server version." + + " It will be skipped!"); + } + } + } + + return map; + } +} \ No newline at end of file diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java b/src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java index dac9ab4..196c8a6 100644 --- a/src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java +++ b/src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java @@ -16,6 +16,9 @@ import org.bukkit.Material; import org.bukkit.entity.EntityType; import com.google.gson.annotations.Expose; +import com.google.gson.annotations.JsonAdapter; + +import world.bentobox.challenges.database.object.adapters.EntityCompatibilityAdapter; /** @@ -193,6 +196,7 @@ public class IslandRequirements extends Requirements * Map that contains which entities and how many is necessary around player to complete challenge. */ @Expose + @JsonAdapter(EntityCompatibilityAdapter.class) private Map requiredEntities = new EnumMap<>(EntityType.class); /** diff --git a/src/main/java/world/bentobox/challenges/panel/admin/EditSettingsGUI.java b/src/main/java/world/bentobox/challenges/panel/admin/EditSettingsGUI.java index 40abf68..c808dac 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/EditSettingsGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/EditSettingsGUI.java @@ -124,6 +124,10 @@ public class EditSettingsGUI extends CommonGUI // Return Button panelBuilder.item(44, this.returnButton); + // Save Settings every time this GUI is created. It will avoid issues with + // Overwritten setting after server stop. + this.addon.saveSettings(); + panelBuilder.build(); } diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ListLibraryGUI.java b/src/main/java/world/bentobox/challenges/panel/admin/ListLibraryGUI.java index 179cbb1..d153da3 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/ListLibraryGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/ListLibraryGUI.java @@ -7,6 +7,7 @@ import java.util.List; import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.scheduler.BukkitTask; import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; @@ -118,7 +119,7 @@ public class ListLibraryGUI extends CommonGUI } panelBuilder.item(4, this.createDownloadNow()); - panelBuilder.item(44, this.returnButton); + panelBuilder.item(44, this.createReturnButton()); panelBuilder.build(); } @@ -153,10 +154,16 @@ public class ListLibraryGUI extends CommonGUI } else { - this.addon.getWebManager().requestCatalogGitHubData(false); + this.addon.getWebManager().requestCatalogGitHubData(this.clearCache); + + // Fix multiclick issue. + if (this.updateTask != null) + { + this.updateTask.cancel(); + } // add some delay to rebuilding gui. - this.addon.getPlugin().getServer().getScheduler().runTaskLaterAsynchronously( + this.updateTask = this.addon.getPlugin().getServer().getScheduler().runTaskLater( this.addon.getPlugin(), this::build, 100L); @@ -169,6 +176,33 @@ public class ListLibraryGUI extends CommonGUI } + /** + * This creates return button, that allows to exist or return to parent gui, + * @return PanelItem for return button. + */ + private PanelItem createReturnButton() + { + return new PanelItemBuilder(). + name(this.user.getTranslation("challenges.gui.buttons.return")). + icon(Material.OAK_DOOR). + clickHandler((panel, user1, clickType, i) -> { + if (this.updateTask != null) + { + this.updateTask.cancel(); + } + + if (this.parentGUI == null) + { + this.user.closeInventory(); + return true; + } + + this.parentGUI.build(); + return true; + }).build(); + } + + /** * This method creates button for given library entry. * @param libraryEntry LibraryEntry which button must be created. @@ -199,10 +233,20 @@ public class ListLibraryGUI extends CommonGUI if (this.parentGUI != null) { + if (this.updateTask != null) + { + this.updateTask.cancel(); + } + this.parentGUI.build(); } else { + if (this.updateTask != null) + { + this.updateTask.cancel(); + } + this.user.closeInventory(); } } @@ -252,6 +296,11 @@ public class ListLibraryGUI extends CommonGUI */ private boolean clearCache; + /** + * Stores update task that is triggered. + */ + private BukkitTask updateTask = null; + /** * This variable will protect against spam-click. */ diff --git a/src/main/java/world/bentobox/challenges/utils/GuiUtils.java b/src/main/java/world/bentobox/challenges/utils/GuiUtils.java index 40b069c..55edbb9 100644 --- a/src/main/java/world/bentobox/challenges/utils/GuiUtils.java +++ b/src/main/java/world/bentobox/challenges/utils/GuiUtils.java @@ -114,9 +114,6 @@ public class GuiUtils switch (entity) { - case PIG_ZOMBIE: - itemStack = new ItemStack(Material.ZOMBIE_PIGMAN_SPAWN_EGG); - break; case ENDER_DRAGON: itemStack = new ItemStack(Material.DRAGON_EGG); break; @@ -153,6 +150,12 @@ public class GuiUtils break; } + if (entity.name().equals("PIG_ZOMBIE")) + { + // If pig zombie exist, then pigman spawn egg exists too. + itemStack = new ItemStack(Material.getMaterial("ZOMBIE_PIGMAN_SPAWN_EGG")); + } + itemStack.setAmount(amount); return itemStack; diff --git a/src/main/java/world/bentobox/challenges/utils/HeadLib.java b/src/main/java/world/bentobox/challenges/utils/HeadLib.java index 1bbae6a..d08b06e 100644 --- a/src/main/java/world/bentobox/challenges/utils/HeadLib.java +++ b/src/main/java/world/bentobox/challenges/utils/HeadLib.java @@ -111,7 +111,15 @@ public enum HeadLib PILLAGER("1ac9d5aa-46ef-4d71-b077-4564382c0a43", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNGFlZTZiYjM3Y2JmYzkyYjBkODZkYjVhZGE0NzkwYzY0ZmY0NDY4ZDY4Yjg0OTQyZmRlMDQ0MDVlOGVmNTMzMyJ9fX0="), RAVAGER("def81bd7-85e5-4644-b1b2-e7521e53bba8", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWNiOWYxMzlmOTQ4OWQ4NmU0MTBhMDZkOGNiYzY3MGM4MDI4MTM3NTA4ZTNlNGJlZjYxMmZlMzJlZGQ2MDE5MyJ9fX0="), TRADER_LLAMA("47dbdab5-105f-42bc-9580-c61cee9231f3", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzA4N2E1NTZkNGZmYTk1ZWNkMjg0NGYzNTBkYzQzZTI1NGU1ZDUzNWZhNTk2ZjU0MGQ3ZTc3ZmE2N2RmNDY5NiJ9fX0="), - WANDERING_TRADER("943947ea-3e1a-4fdc-85e5-f538379f05e9", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWYxMzc5YTgyMjkwZDdhYmUxZWZhYWJiYzcwNzEwZmYyZWMwMmRkMzRhZGUzODZiYzAwYzkzMGM0NjFjZjkzMiJ9fX0="); + WANDERING_TRADER("943947ea-3e1a-4fdc-85e5-f538379f05e9", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWYxMzc5YTgyMjkwZDdhYmUxZWZhYWJiYzcwNzEwZmYyZWMwMmRkMzRhZGUzODZiYzAwYzkzMGM0NjFjZjkzMiJ9fX0="), + // Since 1.15 + BEE("77342662-8870-445a-869f-f0aef1406b3d", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTlhYzE2ZjI5NmI0NjFkMDVlYTA3ODVkNDc3MDMzZTUyNzM1OGI0ZjMwYzI2NmFhMDJmMDIwMTU3ZmZjYTczNiJ9fX0="), + // Since 1.16 + PIGLIN("7b3f9b15-325b-4d6e-a184-0455e233a1cc", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvY2NlZDlkODAxYWE2ZjgzZjhlNDlmOTBkOWE4Yjg1YjdmOGZkYTU4M2Q4NWY3MmNmZmI2OTg2NzI1Nzg5ZjYzNiJ9fX0="), + ZOMBIFIED_PIGLIN("4f013cfb-84f8-4d80-8529-25127f6c70ee", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2VhYmFlY2M1ZmFlNWE4YTQ5Yzg4NjNmZjQ4MzFhYWEyODQxOThmMWEyMzk4ODkwYzc2NWUwYThkZTE4ZGE4YyJ9fX0="), + STRIDER("d1c2fba9-6633-4625-9cda-8528fae6fe09", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMThhOWFkZjc4MGVjN2RkNDYyNWM5YzA3NzkwNTJlNmExNWE0NTE4NjY2MjM1MTFlNGM4MmU5NjU1NzE0YjNjMSJ9fX0="), + HOGLIN("8196c240-e96a-4434-b630-6b191ceeb480", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWJiOWJjMGYwMWRiZDc2MmEwOGQ5ZTc3YzA4MDY5ZWQ3Yzk1MzY0YWEzMGNhMTA3MjIwODU2MWI3MzBlOGQ3NSJ9fX0="), + ZOGLIN("d6f4e7ce-dc71-4c81-97dc-df0d15d39a68", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWZhMGFkYTM0MTFmYmE4Yjg4NTgzZDg2NGIyNTI2MDZlOTNkZmRmNjQ3NjkwZDNjZjRjMDE3YjYzYmFiMTJiMCJ9fX0="); // --------------------------------------------------------------------- diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml index cdbe733..c43b240 100755 --- a/src/main/resources/addon.yml +++ b/src/main/resources/addon.yml @@ -1,6 +1,7 @@ name: Challenges main: world.bentobox.challenges.ChallengesAddon version: ${version}${build.number} +api-version: 1.14 repository: 'BentoBoxWorld/Challenges' metrics: true @@ -18,52 +19,12 @@ permissions: description: Let the player use the '/challenges' command default: true - bskyblock.challenges: + '[gamemode].challenges': description: Let the player use the '/island challenges' command default: true - bskyblock.challenges.multiple: + '[gamemode].challenges.multiple': description: Let the player complete challenge multiple times default: true - bskyblock.admin.challenges: + '[gamemode].admin.challenges': description: Access challenge admin commands - default: op - - acidisland.challenges: - description: Let the player use the '/ai challenges' command - default: true - acidisland.challenges.multiple: - description: Let the player complete challenge multiple times - default: true - acidisland.admin.challenges: - description: Access challenge admin commands - default: op - - caveblock.challenges: - description: Let the player use the '/cave challenges' command - default: true - caveblock.challenges.multiple: - description: Let the player complete challenge multiple times - default: true - caveblock.admin.challenges: - description: Access challenge admin commands - default: op - - skygrid.challenges: - description: Let the player use the '/skygrid challenges' command - default: true - skygrid.challenges.multiple: - description: Let the player complete challenge multiple times - default: true - skygrid.admin.challenges: - description: Access challenge admin commands - default: op - - aoneblock.challenges: - description: Let the player use the '/aoneblock challenges' command - default: true - aoneblock.challenges.multiple: - description: Let the player complete challenge multiple times - default: true - aoneblock.admin.challenges: - description: Access challenge admin commands - default: op + default: op \ No newline at end of file diff --git a/src/main/resources/default.json b/src/main/resources/default.json index 0163aa4..e98a54f 100644 --- a/src/main/resources/default.json +++ b/src/main/resources/default.json @@ -1196,7 +1196,8 @@ }, "rewardText": "Some handy mending books and redstone ore", "rewardItems": [ - "is:\n ==: org.bukkit.inventory.ItemStack\n v: 1976\n type: ENCHANTED_BOOK\n amount: 2\n meta:\n ==: ItemMeta\n meta-type: ENCHANTED\n stored-enchants:\n MENDING: 1\n", + "is:\n ==: org.bukkit.inventory.ItemStack\n v: 1976\n type: ENCHANTED_BOOK\n amount: 1\n meta:\n ==: ItemMeta\n meta-type: ENCHANTED\n stored-enchants:\n MENDING: 1\n", + "is:\n ==: org.bukkit.inventory.ItemStack\n v: 1976\n type: ENCHANTED_BOOK\n amount: 1\n meta:\n ==: ItemMeta\n meta-type: ENCHANTED\n stored-enchants:\n MENDING: 1\n", "is:\n ==: org.bukkit.inventory.ItemStack\n v: 1976\n type: REDSTONE_ORE\n amount: 16\n" ], "rewardExperience": 50, @@ -1282,7 +1283,8 @@ }, "rewardText": "Some mending books and lapis ore", "rewardItems": [ - "is:\n ==: org.bukkit.inventory.ItemStack\n v: 1976\n type: ENCHANTED_BOOK\n amount: 2\n meta:\n ==: ItemMeta\n meta-type: ENCHANTED\n stored-enchants:\n MENDING: 1\n", + "is:\n ==: org.bukkit.inventory.ItemStack\n v: 1976\n type: ENCHANTED_BOOK\n amount: 1\n meta:\n ==: ItemMeta\n meta-type: ENCHANTED\n stored-enchants:\n MENDING: 1\n", + "is:\n ==: org.bukkit.inventory.ItemStack\n v: 1976\n type: ENCHANTED_BOOK\n amount: 1\n meta:\n ==: ItemMeta\n meta-type: ENCHANTED\n stored-enchants:\n MENDING: 1\n", "is:\n ==: org.bukkit.inventory.ItemStack\n v: 1976\n type: LAPIS_ORE\n amount: 16\n" ], "rewardExperience": 50, diff --git a/src/test/java/world/bentobox/challenges/ChallengesManagerTest.java b/src/test/java/world/bentobox/challenges/ChallengesManagerTest.java index bbe3b8e..d9aa352 100644 --- a/src/test/java/world/bentobox/challenges/ChallengesManagerTest.java +++ b/src/test/java/world/bentobox/challenges/ChallengesManagerTest.java @@ -118,6 +118,7 @@ public class ChallengesManagerTest { BentoBox plugin = mock(BentoBox.class); Whitebox.setInternalState(BentoBox.class, "instance", plugin); when(addon.getPlugin()).thenReturn(plugin); + User.setPlugin(plugin); // IWM when(plugin.getIWM()).thenReturn(iwm); diff --git a/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java b/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java index 9f72b31..2bbbe26 100644 --- a/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java +++ b/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java @@ -167,6 +167,7 @@ public class TryToCompleteTest { PowerMockito.mockStatic(Util.class); when(Util.getWorld(any())).thenReturn(world); when(Util.prettifyText(anyString())).thenCallRealMethod(); + when(Util.stripSpaceAfterColorCodes(anyString())).thenCallRealMethod(); // Island World Manager IslandWorldManager iwm = mock(IslandWorldManager.class); @@ -252,7 +253,6 @@ public class TryToCompleteTest { // ItemFactory ItemFactory itemFactory = mock(ItemFactory.class); when(Bukkit.getItemFactory()).thenReturn(itemFactory); - } /**