From d60f20e394f6e9e9238db8fed47763f2800c25ca Mon Sep 17 00:00:00 2001 From: BONNe Date: Sat, 4 May 2019 11:16:41 +0300 Subject: [PATCH 1/6] Fix issue when Color codes did not work in opened Panels and Titles (#110) These text element colors were not translated correctly. Now it should work. --- .../bentobox/challenges/panel/admin/EditLevelGUI.java | 3 ++- .../challenges/panel/admin/ListChallengesGUI.java | 3 ++- .../bentobox/challenges/panel/admin/ListLevelsGUI.java | 3 ++- .../bentobox/challenges/panel/user/ChallengesGUI.java | 7 +++++-- .../challenges/panel/util/SelectChallengeGUI.java | 3 ++- .../world/bentobox/challenges/tasks/TryToComplete.java | 9 +++------ 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/world/bentobox/challenges/panel/admin/EditLevelGUI.java b/src/main/java/world/bentobox/challenges/panel/admin/EditLevelGUI.java index 175c2a3..6f5a3f7 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/EditLevelGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/EditLevelGUI.java @@ -1,6 +1,7 @@ package world.bentobox.challenges.panel.admin; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.enchantments.Enchantment; @@ -275,7 +276,7 @@ public class EditLevelGUI extends CommonGUI private PanelItem createChallengeIcon(Challenge challenge) { return new PanelItemBuilder(). - name(challenge.getFriendlyName()). + name(ChatColor.translateAlternateColorCodes('&', challenge.getFriendlyName())). description(GuiUtils.stringSplit( challenge.getDescription(), this.addon.getChallengesSettings().getLoreLineLength())). diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ListChallengesGUI.java b/src/main/java/world/bentobox/challenges/panel/admin/ListChallengesGUI.java index 6771147..1c21908 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/ListChallengesGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/ListChallengesGUI.java @@ -1,6 +1,7 @@ package world.bentobox.challenges.panel.admin; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.World; import java.util.List; @@ -133,7 +134,7 @@ public class ListChallengesGUI extends CommonGUI private PanelItem createChallengeIcon(Challenge challenge) { PanelItemBuilder itemBuilder = new PanelItemBuilder(). - name(challenge.getFriendlyName()). + name(ChatColor.translateAlternateColorCodes('&', challenge.getFriendlyName())). description(GuiUtils.stringSplit(this.generateChallengeDescription(challenge, this.user.getPlayer()), this.addon.getChallengesSettings().getLoreLineLength())). icon(challenge.getIcon()). diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ListLevelsGUI.java b/src/main/java/world/bentobox/challenges/panel/admin/ListLevelsGUI.java index 83a0cf5..b7fbc94 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/ListLevelsGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/ListLevelsGUI.java @@ -1,6 +1,7 @@ package world.bentobox.challenges.panel.admin; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.World; import java.util.List; @@ -133,7 +134,7 @@ public class ListLevelsGUI extends CommonGUI private PanelItem createLevelIcon(ChallengeLevel challengeLevel) { PanelItemBuilder itemBuilder = new PanelItemBuilder(). - name(challengeLevel.getFriendlyName()). + name(ChatColor.translateAlternateColorCodes('&', challengeLevel.getFriendlyName())). description(GuiUtils.stringSplit( this.generateLevelDescription(challengeLevel, this.user.getPlayer()), this.addon.getChallengesSettings().getLoreLineLength())). diff --git a/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java b/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java index 4f1f4e7..69220ab 100644 --- a/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java @@ -1,6 +1,7 @@ package world.bentobox.challenges.panel.user; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.inventory.ItemStack; @@ -349,7 +350,9 @@ public class ChallengesGUI extends CommonGUI { return new PanelItemBuilder(). icon(challenge.getIcon()). - name(challenge.getFriendlyName().isEmpty() ? challenge.getUniqueId() : challenge.getFriendlyName()). + name(challenge.getFriendlyName().isEmpty() ? + challenge.getUniqueId() : + ChatColor.translateAlternateColorCodes('&', challenge.getFriendlyName())). description(GuiUtils.stringSplit(this.generateChallengeDescription(challenge, this.user.getPlayer()), this.addon.getChallengesSettings().getLoreLineLength())). clickHandler((panel, user1, clickType, slot) -> { @@ -440,7 +443,7 @@ public class ChallengesGUI extends CommonGUI return new PanelItemBuilder(). icon(icon). - name(name). + name(ChatColor.translateAlternateColorCodes('&', name)). description(description). glow(glow). clickHandler(clickHandler). diff --git a/src/main/java/world/bentobox/challenges/panel/util/SelectChallengeGUI.java b/src/main/java/world/bentobox/challenges/panel/util/SelectChallengeGUI.java index d47082c..b348663 100644 --- a/src/main/java/world/bentobox/challenges/panel/util/SelectChallengeGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/util/SelectChallengeGUI.java @@ -1,6 +1,7 @@ package world.bentobox.challenges.panel.util; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.event.inventory.ClickType; import java.util.*; @@ -144,7 +145,7 @@ public class SelectChallengeGUI return new PanelItemBuilder(). - name(challenge.getFriendlyName()). + name(ChatColor.translateAlternateColorCodes('&', challenge.getFriendlyName())). description(GuiUtils.stringSplit(description, this.lineLength)). icon(challenge.getIcon()). clickHandler((panel, user1, clickType, slot) -> { diff --git a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java index b680240..596d991 100644 --- a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java +++ b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java @@ -4,10 +4,7 @@ package world.bentobox.challenges.tasks; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; +import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.EntityType; @@ -1027,7 +1024,7 @@ public class TryToComplete outputMessage = outputMessage.replace("[rewardText]", challenge.getRewardText()); } - return outputMessage; + return ChatColor.translateAlternateColorCodes('&', outputMessage); } @@ -1047,7 +1044,7 @@ public class TryToComplete outputMessage = outputMessage.replace("[rewardText]", level.getRewardText()); } - return outputMessage; + return ChatColor.translateAlternateColorCodes('&', outputMessage); } From 88e84cef2aceb3c7e098b37b6636e686b30acaab Mon Sep 17 00:00:00 2001 From: wellnesscookie <46493763+wellnesscookie@users.noreply.github.com> Date: Sat, 4 May 2019 18:01:44 +0200 Subject: [PATCH 2/6] Color extends on further locale text (#112) * Update ReadMe. * Remove # symbol from development build name * Color extends on further locale text If a Friendly Name of a challenge is colored, it's color will pass on to all words after it. Resetting it's color and setting it back after [level] will fix this. Not really a relevant PR, but I've asked @BONNe on Discord if it's okay with him if I change this and he confirmed. --- pom.xml | 2 +- src/main/resources/locales/en-US.yml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 7798513..436efc6 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - -#${env.BUILD_NUMBER} + -#${env.BUILD_NUMBER} diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 2f2af6f..0d09812 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -320,11 +320,11 @@ challenges: hit-things: 'Hit things to add them to the list of things required. Right click when done.' you-added: 'You added one [thing] to the challenge' challenge-created: '[challenge] created!' - you-completed-challenge: '&2You completed the [value] challenge!' - you-repeated-challenge: '&2You repeated the [value] challenge!' - you-completed-level: '&2You completed the [value] level!' - name-has-completed-challenge: '&5[name] has completed the [value] challenge!' - name-has-completed-level: '&5[name] has completed the [value] level!' + you-completed-challenge: '&2You completed the [value] &r&2challenge!' + you-repeated-challenge: '&2You repeated the [value] &r&2challenge!' + you-completed-level: '&2You completed the [value] &r&2level!' + name-has-completed-challenge: '&5[name] has completed the [value] &r&5challenge!' + name-has-completed-level: '&5[name] has completed the [value] &r&5level!' import-levels: 'Start importing Levels' import-challenges: 'Start importing Challenges' no-levels: 'Warning: No levels defined in challenges.yml' From f62816f9c36ad662b58a2149d1256b0e9d092557 Mon Sep 17 00:00:00 2001 From: BONNe Date: Sat, 4 May 2019 19:31:01 +0300 Subject: [PATCH 3/6] Update ReadMe with newest information. --- README.md | 73 ++++++++++++++++++++++++------------------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 99cd3ff..0d38f40 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 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.6.5-SNAPSHOT**, and you can download it from [Release tab](https://github.com/BentoBoxWorld/Challenges/releases) +Latest official **Beta Release is 0.7.0**, and you can download it from [Release tab](https://github.com/BentoBoxWorld/Challenges/releases) Or you can try **nightly builds** where you can check and test new features that will be implemented in next release from [Jenkins Server](https://ci.codemc.org/job/BentoBoxWorld/job/Challenges/lastStableBuild/). @@ -18,16 +18,21 @@ If you like this addon but something is missing or is not working as you want, y 1. Place the addon jar in the addons folder of the BentoBox plugin 2. Restart the server 3. The addon will create a data folder and inside the folder will be a config.yml and an example challenges.yml -4. Edit the config.yml and challenges.yml files how you want. Note that unlike ASkyBlock, the challenges.yml is for *importing only* and faster start. +4. Edit the config.yml how you want. 5. Restart the server -6. To import challenges into GameMode, you must run admin command and attach `challenges import` at the end. Or you can use challenges admin GUI to do the same. + +#### Challenges + +By default, challenges addon comes without any challenge or level. On first runtime only Admin GUI will be accessible. +Admins can create their own challenges or use challenges from ASkyBlock, by importing them via Admin GUI. This requires challenges.yml file in `./plugins/BentoBox/addons/Challenges/` folder. +There exist also some default challenges, which importing also are available via Admin GUI. ## Compatibility - [x] BentoBox - 1.4.0 version -- [x] BSkyBlock - 1.4.0-SNAPSHO version -- [x] AcidIsland - 1.4.0-SNAPSHO version -- [x] SkyGrid - 1.4.0-SNAPSHOT version +- [x] BSkyBlock - 1.4.0 version +- [x] AcidIsland - 1.4.0 version +- [x] SkyGrid - 1.4.0 version - [x] CaveBlock - 1.4.0 version ## Config.yml @@ -35,40 +40,26 @@ If you like this addon but something is missing or is not working as you want, y As most of BenotBox addons, config can be edited only when server is stopped. Otherwise all changes will be overwritten by server. The config.yml has the following sections: -* Reset Challenges - if this is true, player's challenges will reset when they reset an island or if they are kicked or leave a team. Prevents exploiting the challenges by doing them repeatedly. Default is true -* Broadcast 1st time challenge completion messages to all players. Change to false if the spam becomes too much. Default is true. -* Remove non-repeatable challenges from the challenge GUI when complete. Default is false. -* Add enchanted glow to completed challenges. Default is true -* Free challenges location - You can decide, either free challenges will be at the top, or at the bottom. -* Description line length - allows to specify maximal line length in GUI icon descriptions. -* Challenge Description structure - allows to modify structure of challenge description. -* Level Description structure - allows to modify structure of Level description. -* Disabled GameModes - specify Game Modes where challenges will not work. +* **Commands** - ability to enable */challenges* command. This option change is possible only via configuration and requires server restart. + To enable, you should change `single-gui` to `true`. +* **History** - ability to enable completion history storing in player data object. + To enable, you should change `store-history-data` to `true`. + It is possible to change life-span of history data in days. (0 means that data will not be removed) +* **GUI Settings** - ability to change some options that are visible only in challenges GUI. + * Remove non-repeatable challenges from the challenge GUI when complete. Default is false. + * Add enchanted glow to completed challenges. Default is true. + * Locked level icon is displayed for locked levels. + * Free challenges location - You can decide, either free challenges will be at the top, or at the bottom. + * Description line length - allows to specify maximal line length in GUI icon descriptions. + * Challenge Description structure - allows to modify structure of challenge description. + * Level Description structure - allows to modify structure of Level description. +* **Store mode** - ability to store challenges completion per island or per player. + To enable storing challenges data per island change `store-island-data` to `true`. ATTENTION: progress will be lost on this option change. +* **Reset Challenges** - if this is true, player's challenges will reset when they reset an island or if they are kicked or leave a team. Prevents exploiting the challenges by doing them repeatedly. Default is true +* **Broadcast** - ability to broadcast 1st time challenge completion messages to all players. Change to false if the spam becomes too much. Default is true. +* **Title** - ability to enable showing Title screen on first challenge completion or level completion. +* **Disabled GameModes** - specify Game Modes where challenges will not work. -## Challenges.yml +## Information -This file is just to facilitate importing of old ASkyBlock or AcidIsland challenges and is not used during the normal operation of the game. it is meant to just enable you to jump start your challenge collection. - -This file format is very similar to the ASkyBlock file but not exactly the same because it is designed for 1.13.x servers and higher. If you try to import ASkyBlock challenges, they may or may not completely import, so check for errors in the console. - -Once you have imported challenges, the *real* challenge files are actually in two folders in the BentoBox database folder. One folder is for the challenges and the other is for the challenge levels. They are all defined in .yml files in these locations: - -``` -plugins/BentoBox/database/Challenges -plugins/BentoBox/database/ChallengeLevels -``` - -If you edit a file, then you should reload the challenge database by using the admin reload command, e.g. **/bsb challenges reload** or **/acid challenges reload**. - -If you want to force an overwrite of challenges via an import, add the **overwrite** option to the end of the import command. - -Note that you must import challenges into both BSkyBlock and AcidIsland separately. - - -## Admin commands - -There are a few admin commands and more being written. The main challenge admin command is **/bsb challenges** or **/acid challenges**. Use - -* /bsbadmin challenges help : Show help for all the commands -* /bsbadmin challenges import [overwrite]: import challenges from challenges.yml -* /bsbadmin challenges reload : reload challenges from the database +More information can be found in [Wiki Pages](https://github.com/BentoBoxWorld/Challenges/wiki). From f18c8d0dffd915849b960aa9e7630fb344ffd73f Mon Sep 17 00:00:00 2001 From: BONNe Date: Sun, 5 May 2019 17:10:10 +0300 Subject: [PATCH 4/6] Remove NBT editing via NMS. Use Dummy Mojang profiles with custom skulls, instead of editing NBT with NMS. This will work on all versions instead of just with correct NMS hacks. --- .../bentobox/challenges/utils/HeadLib.java | 85 ++++++------------- 1 file changed, 24 insertions(+), 61 deletions(-) diff --git a/src/main/java/world/bentobox/challenges/utils/HeadLib.java b/src/main/java/world/bentobox/challenges/utils/HeadLib.java index 3492d0a..0e27d48 100644 --- a/src/main/java/world/bentobox/challenges/utils/HeadLib.java +++ b/src/main/java/world/bentobox/challenges/utils/HeadLib.java @@ -15,16 +15,16 @@ package world.bentobox.challenges.utils; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; import org.bukkit.ChatColor; import org.bukkit.Material; -import org.bukkit.craftbukkit.v1_13_R2.inventory.CraftItemStack; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import java.lang.reflect.Field; import java.util.*; -import net.minecraft.server.v1_13_R2.NBTBase; -import net.minecraft.server.v1_13_R2.NBTTagCompound; -import net.minecraft.server.v1_13_R2.NBTTagList; +import world.bentobox.bentobox.BentoBox; /** @@ -191,10 +191,11 @@ public enum HeadLib public ItemStack toItemStack(int amount, String displayName, String... loreLines) { ItemStack item = new ItemStack(Material.PLAYER_HEAD, amount); + ItemMeta meta = item.getItemMeta(); - if (displayName != null) + // Set Lora and DisplayName + if (meta != null && displayName != null) { - ItemMeta meta = item.getItemMeta(); meta.setDisplayName(ChatColor.translateAlternateColorCodes('&', displayName)); if (loreLines.length != 0) @@ -207,64 +208,26 @@ public enum HeadLib item.setItemMeta(meta); } - return this.setSkullOwner(item, this.getSkullOwner()); - } - - -// --------------------------------------------------------------------- -// Section: Private inner methods -// --------------------------------------------------------------------- - - - /** - * This method returns SkullOwner or create new one if it is not created yet. - * SkullOwner is NBTTagCompound object that contains information about player_head skin. - * @return skullOwner object. - */ - private Object getSkullOwner() - { - if (this.skullOwner == null) + // Set correct Skull texture + if (meta != null && this.textureValue != null && !this.textureValue.isEmpty()) { - this.skullOwner = this.createOwnerCompound(this.uuid, this.textureValue); + GameProfile profile = new GameProfile(UUID.fromString(this.uuid), null); + profile.getProperties().put("textures", new Property("textures", this.textureValue)); + + try + { + Field profileField = meta.getClass().getDeclaredField("profile"); + profileField.setAccessible(true); + profileField.set(meta, profile); + item.setItemMeta(meta); + } + catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) + { + BentoBox.getInstance().log("Error while creating Skull Icon"); + } } - return this.skullOwner; - } - - - /** - * This method creates new NBTTagCompound object that contains UUID and texture link. - * @param id - UUID of user. - * @param textureValue - Encoded texture string. - * @return NBTTagCompound object that contains all necessary information about player_head skin. - */ - private NBTTagCompound createOwnerCompound(String id, String textureValue) - { - NBTTagCompound skullOwner = new NBTTagCompound(); - skullOwner.setString("Id", id); - NBTTagCompound properties = new NBTTagCompound(); - NBTTagList textures = new NBTTagList(); - NBTTagCompound value = new NBTTagCompound(); - value.setString("Value", textureValue); - textures.add(value); - properties.set("textures", textures); - skullOwner.set("Properties", properties); - - return skullOwner; - } - - - /** - * This method adds SkullOwner tag to given item. - * @param item Item whom SkullOwner tag must be added. - * @param compound NBTTagCompound object that contains UUID and texture link. - * @return new copy of given item with SkullOwner tag. - */ - private ItemStack setSkullOwner(ItemStack item, Object compound) - { - net.minecraft.server.v1_13_R2.ItemStack nmsStack = CraftItemStack.asNMSCopy(item); - nmsStack.getOrCreateTag().set("SkullOwner", (NBTBase) compound); - return CraftItemStack.asBukkitCopy(nmsStack); + return item; } From d9486bbbdbd46153a6e1f2cb278701d042287b62 Mon Sep 17 00:00:00 2001 From: BONNe Date: Sun, 5 May 2019 23:54:35 +0300 Subject: [PATCH 5/6] Fix issue when Import Default Challenges Button did not work --- .../world/bentobox/challenges/panel/CommonGUI.java | 2 +- .../bentobox/challenges/panel/admin/AdminGUI.java | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/main/java/world/bentobox/challenges/panel/CommonGUI.java b/src/main/java/world/bentobox/challenges/panel/CommonGUI.java index 2f2a2ae..a061962 100644 --- a/src/main/java/world/bentobox/challenges/panel/CommonGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/CommonGUI.java @@ -102,7 +102,7 @@ public abstract class CommonGUI protected static final String IMPORT = "import"; - protected static final String DEFAULT = "default"; + protected static final String DEFAULT = "defaults"; protected static final String GENERATE = "generate"; diff --git a/src/main/java/world/bentobox/challenges/panel/admin/AdminGUI.java b/src/main/java/world/bentobox/challenges/panel/admin/AdminGUI.java index 08d43d8..dc9e410 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/AdminGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/AdminGUI.java @@ -384,16 +384,9 @@ public class AdminGUI extends CommonGUI description = this.user.getTranslation("challenges.gui.descriptions.admin.default-import"); icon = new ItemStack(Material.HOPPER); clickHandler = (panel, user, clickType, slot) -> { - if (clickType.isRightClick()) - { - this.overwriteMode = !this.overwriteMode; - this.build(); - } - else - { - // Run import command. - this.user.performCommand(this.topLabel + " " + CHALLENGES + " " + DEFAULT + " " + IMPORT); - } + // Run import command. + this.user.performCommand(this.topLabel + " " + CHALLENGES + " " + DEFAULT + " " + IMPORT); + return true; }; glow = false; From c8088f275d030be3970c46e9f44121ca2872d45c Mon Sep 17 00:00:00 2001 From: BONNe Date: Mon, 6 May 2019 12:10:02 +0300 Subject: [PATCH 6/6] Rework TryToComplete. (#109) * Rework TryToComplete. Implement ability to complete challenge multiple times at once. To do it, I split everything in checking/removing/rewarding steps. In checking step, it calculates if it is possible to complete with minimal requirements and then calculates maximal repeating factor. In removing step, it removes everything that is necessary. In rewarding step, it give rewards by necessary factor (multilayer). I rework item/block/entity removing as factors may be influenced at the last element, so I improve everything by memory usage. Create necessary sets/lists/queues for faster access to already collected items. * Add method that returns removed items, if somehow algorithm did not manage to remove all items. Fix issue when removeItems method did not merge ItemStacks as they had different item amount. Return and fix TryToCompleteTest. * Implement Multiple Challenge Completion command and GUI. (#73) /[gamemode] challenges complete [number] allows to complete challenges [number] amount (or less if not enough items) Via GUI users can right click on challenge and if it is repeatable, then it will open AnvilGUI that accepts only numbers as input. --- .../challenges/ChallengesManager.java | 25 +- .../commands/CompleteChallengeCommand.java | 17 +- .../database/object/ChallengesPlayerData.java | 17 +- .../challenges/panel/user/ChallengesGUI.java | 48 +- .../challenges/tasks/TryToComplete.java | 683 +++++++++++++----- src/main/resources/locales/en-US.yml | 3 +- .../challenges/tasks/TryToCompleteTest.java | 328 +++++---- 7 files changed, 798 insertions(+), 323 deletions(-) diff --git a/src/main/java/world/bentobox/challenges/ChallengesManager.java b/src/main/java/world/bentobox/challenges/ChallengesManager.java index 3677c76..f27b6db 100644 --- a/src/main/java/world/bentobox/challenges/ChallengesManager.java +++ b/src/main/java/world/bentobox/challenges/ChallengesManager.java @@ -604,9 +604,23 @@ public class ChallengesManager * @param challengeID - challengeID */ private void setChallengeComplete(@NonNull String storageDataID, @NonNull String challengeID) + { + this.setChallengeComplete(storageDataID, challengeID, 1); + } + + + /** + * Sets the challenge with given ID as complete and increments the number of times it has been + * completed + * + * @param storageDataID - playerData ID + * @param challengeID - challengeID + * @param count - how many times challenge is completed + */ + private void setChallengeComplete(@NonNull String storageDataID, @NonNull String challengeID, int count) { this.addPlayerData(storageDataID); - this.playerCacheData.get(storageDataID).setChallengeDone(challengeID); + this.playerCacheData.get(storageDataID).addChallengeDone(challengeID, count); // Save this.savePlayerData(storageDataID); } @@ -842,9 +856,9 @@ public class ChallengesManager * @param world - World where completion must be called. * @param challenge - That must be completed. */ - public void setChallengeComplete(User user, World world, Challenge challenge) + public void setChallengeComplete(User user, World world, Challenge challenge, int completionCount) { - this.setChallengeComplete(user.getUniqueId(), world, challenge); + this.setChallengeComplete(user.getUniqueId(), world, challenge, completionCount); } @@ -854,13 +868,14 @@ public class ChallengesManager * @param world - World where completion must be called. * @param challenge - That must be completed. */ - public void setChallengeComplete(UUID userID, World world, Challenge challenge) + public void setChallengeComplete(UUID userID, World world, Challenge challenge, int completionCount) { String storageID = this.getDataUniqueID(userID, Util.getWorld(world)); this.setChallengeComplete(storageID, challenge.getUniqueId()); this.addLogEntry(storageID, new LogEntry.Builder("COMPLETE"). data("user-id", userID.toString()). data("challenge-id", challenge.getUniqueId()). + data("completion-count", Integer.toString(completionCount)). build()); // Fire event that user completes challenge @@ -868,7 +883,7 @@ public class ChallengesManager new ChallengeCompletedEvent(challenge.getUniqueId(), userID, false, - 1)); + completionCount)); } diff --git a/src/main/java/world/bentobox/challenges/commands/CompleteChallengeCommand.java b/src/main/java/world/bentobox/challenges/commands/CompleteChallengeCommand.java index b9f7d5a..4837dc9 100644 --- a/src/main/java/world/bentobox/challenges/commands/CompleteChallengeCommand.java +++ b/src/main/java/world/bentobox/challenges/commands/CompleteChallengeCommand.java @@ -112,15 +112,14 @@ public class CompleteChallengeCommand extends CompositeCommand }); break; -// TODO: not implemented YET -// case 4: -// // Suggest a number of completions. -// if (lastString.isEmpty() || lastString.matches("[0-9]*")) -// { -// returnList.addAll(Util.tabLimit(Collections.singletonList(""), lastString)); -// } -// -// break; + case 4: + // Suggest a number of completions. + if (lastString.isEmpty() || lastString.matches("[0-9]*")) + { + returnList.addAll(Util.tabLimit(Collections.singletonList(""), lastString)); + } + + break; default: { returnList.addAll(Util.tabLimit(Collections.singletonList("help"), lastString)); 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 4f81405..e8984c7 100644 --- a/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java +++ b/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java @@ -217,8 +217,21 @@ public class ChallengesPlayerData implements DataObject */ public void setChallengeDone(@NonNull String challengeName) { - int times = challengeStatus.getOrDefault(challengeName, 0) + 1; - challengeStatus.put(challengeName, times); + this.addChallengeDone(challengeName, 1); + } + + + /** + * Mark a challenge as having been completed. Will increment the number of times and + * timestamp + * + * @param challengeName - unique challenge name + * @param times - how many new times should be added + */ + public void addChallengeDone(@NonNull String challengeName, int times) + { + int newTimes = challengeStatus.getOrDefault(challengeName, 0) + times; + challengeStatus.put(challengeName, newTimes); challengesTimestamp.put(challengeName, System.currentTimeMillis()); } diff --git a/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java b/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java index 69220ab..29d52ff 100644 --- a/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java @@ -7,6 +7,7 @@ import org.bukkit.World; import org.bukkit.inventory.ItemStack; import java.util.List; +import net.wesjd.anvilgui.AnvilGUI; import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; @@ -356,14 +357,47 @@ public class ChallengesGUI extends CommonGUI description(GuiUtils.stringSplit(this.generateChallengeDescription(challenge, this.user.getPlayer()), this.addon.getChallengesSettings().getLoreLineLength())). clickHandler((panel, user1, clickType, slot) -> { - if (TryToComplete.complete(this.addon, - this.user, - challenge, - this.world, - this.topLabel, - this.permissionPrefix)) + + // Add ability to input how many repeats player should do. + // Do not open if challenge is not repeatable. + if (clickType.isRightClick() && challenge.isRepeatable()) { - panel.getInventory().setItem(slot, this.getChallengeButton(challenge).getItem()); + new AnvilGUI(this.addon.getPlugin(), + this.user.getPlayer(), + "1", + (player, reply) -> { + try + { + if (TryToComplete.complete(this.addon, + this.user, + challenge, + this.world, + this.topLabel, + this.permissionPrefix, + Integer.parseInt(reply))) + { + panel.getInventory().setItem(slot, this.getChallengeButton(challenge).getItem()); + } + } + catch (Exception e) + { + this.user.sendMessage("challenges.errors.not-a-integer", "[value]", reply); + } + + return reply; + }); + } + else + { + if (TryToComplete.complete(this.addon, + this.user, + challenge, + this.world, + this.topLabel, + this.permissionPrefix)) + { + panel.getInventory().setItem(slot, this.getChallengeButton(challenge).getItem()); + } } return true; diff --git a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java index 596d991..cf7e060 100644 --- a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java +++ b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java @@ -4,9 +4,11 @@ package world.bentobox.challenges.tasks; + import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -23,7 +25,6 @@ import world.bentobox.challenges.ChallengesManager; import world.bentobox.challenges.database.object.Challenge; import world.bentobox.challenges.database.object.Challenge.ChallengeType; import world.bentobox.challenges.database.object.ChallengeLevel; -import world.bentobox.challenges.utils.GuiUtils; /** @@ -182,9 +183,32 @@ public class TryToComplete World world, String topLabel, String permissionPrefix) + { + return TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix, 1); + } + + + /** + * This static method allows complete challenge and get result about completion. + * @param addon - Challenges Addon. + * @param user - User who performs challenge. + * @param challenge - Challenge that should be completed. + * @param world - World where completion may occur. + * @param topLabel - Label of the top command. + * @param permissionPrefix - Permission prefix for GameMode addon. + * @param maxTimes - Integer that represents how many times user wants to complete challenges. + * @return true, if challenge is completed, otherwise false. + */ + public static boolean complete(ChallengesAddon addon, + User user, + Challenge challenge, + World world, + String topLabel, + String permissionPrefix, + int maxTimes) { return new TryToComplete(addon, user, challenge, world, topLabel, permissionPrefix). - build().meetsRequirements; + build(maxTimes).meetsRequirements; } @@ -197,17 +221,41 @@ public class TryToComplete * This method checks if challenge can be done, and complete it, if it is possible. * @return ChallengeResult object, that contains completion status. */ - public ChallengeResult build() + public ChallengeResult build(int maxTimes) { // Check if can complete challenge - ChallengeResult result = this.checkIfCanCompleteChallenge(); + ChallengeResult result = this.checkIfCanCompleteChallenge(maxTimes); - if (!result.meetsRequirements) + if (!result.isMeetsRequirements()) { return result; } - if (!result.repeat) + this.fullFillRequirements(result); + + // Validation to avoid rewarding if something goes wrong in removing requirements. + + if (!result.isMeetsRequirements()) + { + if (result.removedItems != null) + { + result.removedItems.forEach((item, amount) -> + { + ItemStack returnItem = item.clone(); + returnItem.setAmount(amount); + + this.user.getInventory().addItem(returnItem).forEach((k, v) -> + this.user.getWorld().dropItem(this.user.getLocation(), v)); + }); + } + + // Entities and blocks will not be restored. + + return result; + } + + // If challenge was not completed then reward items for completing it first time. + if (!result.wasCompleted()) { // Item rewards for (ItemStack reward : this.challenge.getRewardItems()) @@ -230,7 +278,11 @@ public class TryToComplete // Run commands this.runCommands(this.challenge.getRewardCommands()); - this.user.sendMessage("challenges.messages.you-completed-challenge", "[value]", this.challenge.getFriendlyName()); + // Send message about first completion only if it is completed only once. + if (result.getFactor() == 1) + { + this.user.sendMessage("challenges.messages.you-completed-challenge", "[value]", this.challenge.getFriendlyName()); + } if (this.addon.getChallengesSettings().isBroadcastMessages()) { @@ -257,36 +309,58 @@ public class TryToComplete 20); } } - else + + if (result.wasCompleted() || result.getFactor() > 1) { + int rewardFactor = result.getFactor() - (result.wasCompleted() ? 0 : 1); + // Item Repeat Rewards for (ItemStack reward : this.challenge.getRepeatItemReward()) { // Clone is necessary because otherwise it will chane reward itemstack // amount. - this.user.getInventory().addItem(reward.clone()).forEach((k, v) -> - this.user.getWorld().dropItem(this.user.getLocation(), v)); + + for (int i = 0; i < rewardFactor; i++) + { + this.user.getInventory().addItem(reward.clone()).forEach((k, v) -> + this.user.getWorld().dropItem(this.user.getLocation(), v)); + } } // Money Repeat Reward if (this.addon.isEconomyProvided()) { - this.addon.getEconomyProvider().deposit(this.user, this.challenge.getRepeatMoneyReward()); + this.addon.getEconomyProvider().deposit(this.user, + this.challenge.getRepeatMoneyReward() * rewardFactor); } // Experience Repeat Reward - this.user.getPlayer().giveExp(this.challenge.getRepeatExperienceReward()); + this.user.getPlayer().giveExp( + this.challenge.getRepeatExperienceReward() * rewardFactor); // Run commands - this.runCommands(this.challenge.getRepeatRewardCommands()); + for (int i = 0; i < rewardFactor; i++) + { + this.runCommands(this.challenge.getRepeatRewardCommands()); + } - this.user.sendMessage("challenges.messages.you-repeated-challenge", "[value]", this.challenge.getFriendlyName()); + if (result.getFactor() > 1) + { + this.user.sendMessage("challenges.messages.you-repeated-challenge-multiple", + "[value]", this.challenge.getFriendlyName(), + "[count]", Integer.toString(result.getFactor())); + } + else + { + this.user.sendMessage("challenges.messages.you-repeated-challenge", "[value]", this.challenge.getFriendlyName()); + } } // Mark as complete - this.manager.setChallengeComplete(this.user, this.world, this.challenge); + this.manager.setChallengeComplete(this.user, this.world, this.challenge, result.getFactor()); - if (!result.repeat) + // Check level completion. + if (!result.wasCompleted()) { ChallengeLevel level = this.manager.getLevel(this.challenge); @@ -350,11 +424,77 @@ public class TryToComplete } + /** + * This method full fills all challenge type requirements, that is not full filled yet. + * @param result Challenge Results + */ + private void fullFillRequirements(ChallengeResult result) + { + if (this.challenge.getChallengeType().equals(ChallengeType.ISLAND)) + { + if (result.meetsRequirements && + this.challenge.isRemoveEntities() && + !this.challenge.getRequiredEntities().isEmpty()) + { + this.removeEntities(result.entities, result.getFactor()); + } + + if (result.meetsRequirements && + this.challenge.isRemoveBlocks() && + !this.challenge.getRequiredBlocks().isEmpty()) + { + this.removeBlocks(result.blocks, result.getFactor()); + } + } + else if (this.challenge.getChallengeType().equals(ChallengeType.INVENTORY)) + { + + // If remove items, then remove them + if (this.challenge.isTakeItems()) + { + int sumEverything = result.requiredItems.stream(). + mapToInt(itemStack -> itemStack.getAmount() * result.getFactor()). + sum(); + + Map removedItems = + this.removeItems(result.requiredItems, result.getFactor()); + + int removedAmount = removedItems.values().stream().mapToInt(num -> num).sum(); + + // Something is not removed. + if (sumEverything != removedAmount) + { + this.user.sendMessage("challenges.errors.cannot-remove-items"); + + result.removedItems = removedItems; + result.meetsRequirements = false; + } + } + } + else if (this.challenge.getChallengeType().equals(ChallengeType.OTHER)) + { + if (this.addon.isEconomyProvided() && this.challenge.isTakeMoney()) + { + this.addon.getEconomyProvider().withdraw(this.user, this.challenge.getRequiredMoney()); + } + + if (this.challenge.isTakeExperience() && + this.user.getPlayer().getGameMode() != GameMode.CREATIVE) + { + // Cannot take anything from creative game mode. + this.user.getPlayer().setTotalExperience( + this.user.getPlayer().getTotalExperience() - this.challenge.getRequiredExperience()); + } + } + } + + /** * Checks if a challenge can be completed or not * It returns ChallengeResult. + * @param maxTimes - times that user wanted to complete */ - private ChallengeResult checkIfCanCompleteChallenge() + private ChallengeResult checkIfCanCompleteChallenge(int maxTimes) { ChallengeResult result; @@ -422,21 +562,27 @@ public class TryToComplete } else if (type.equals(ChallengeType.INVENTORY)) { - result = this.checkInventory(); + result = this.checkInventory(this.getAvailableCompletionTimes(maxTimes)); } else if (type.equals(ChallengeType.ISLAND)) { - result = this.checkSurrounding(); + result = this.checkSurrounding(this.getAvailableCompletionTimes(maxTimes)); } else if (type.equals(ChallengeType.OTHER)) { - result = this.checkOthers(); + result = this.checkOthers(this.getAvailableCompletionTimes(maxTimes)); } else { result = EMPTY_RESULT; } + // Mark if challenge is completed. + if (result.isMeetsRequirements()) + { + result.setCompleted(this.manager.isChallengeComplete(this.user, this.world, this.challenge)); + } + // Everything fails till this point. return result; } @@ -452,6 +598,35 @@ public class TryToComplete this.challenge.getRequiredPermissions().stream().allMatch(s -> this.user.hasPermission(s)); } + + /** + * This method checks if it is possible to complete maxTimes current challenge by + * challenge constraints and already completed times. + * @param vantedTimes How many times user wants to complete challenge + * @return how many times user is able complete challenge by its constraints. + */ + private int getAvailableCompletionTimes(int vantedTimes) + { + if (!this.challenge.isRepeatable()) + { + // Challenge is not repeatable + vantedTimes = 1; + } + else if (this.challenge.getMaxTimes() != 0) + { + // Challenge has limitations + long availableTimes = this.challenge.getMaxTimes() - this.manager.getChallengeTimes(this.user, this.world, this.challenge); + + if (availableTimes < vantedTimes) + { + vantedTimes = (int) availableTimes; + } + } + + return vantedTimes; + } + + /** * This method runs all commands from command list. * @param commands List of commands that must be performed. @@ -520,8 +695,9 @@ public class TryToComplete /** * Checks if a inventory challenge can be completed or not * It returns ChallengeResult. + * @param maxTimes - times that user wanted to complete */ - private ChallengeResult checkInventory() + private ChallengeResult checkInventory(int maxTimes) { // Run through inventory List requiredItems = new ArrayList<>(this.challenge.getRequiredItems().size()); @@ -560,77 +736,62 @@ public class TryToComplete } } - int sumEverything = 0; - // Check if all required items are in players inventory. for (ItemStack required : requiredItems) { + int numInInventory; + if (this.canIgnoreMeta(required.getType())) { - int numInInventory = + numInInventory = Arrays.stream(this.user.getInventory().getContents()). filter(Objects::nonNull). filter(i -> i.getType().equals(required.getType())). mapToInt(ItemStack::getAmount). sum(); - - if (numInInventory < required.getAmount()) - { - this.user.sendMessage("challenges.errors.not-enough-items", - "[items]", - Util.prettifyText(required.getType().toString())); - return EMPTY_RESULT; - } } else { - if (!this.user.getInventory().containsAtLeast(required, required.getAmount())) - { - this.user.sendMessage("challenges.errors.not-enough-items", - "[items]", - Util.prettifyText(required.getType().toString())); - return EMPTY_RESULT; - } + numInInventory = + Arrays.stream(this.user.getInventory().getContents()). + filter(Objects::nonNull). + filter(i -> i.isSimilar(required)). + mapToInt(ItemStack::getAmount). + sum(); } - sumEverything += required.getAmount(); - } - - // If remove items, then remove them - if (this.challenge.isTakeItems()) - { - Map removedItems = this.removeItems(requiredItems); - - int removedAmount = removedItems.values().stream().mapToInt(num -> num).sum(); - - // Something is not removed. - if (sumEverything != removedAmount) + if (numInInventory < required.getAmount()) { - this.user.sendMessage("challenges.errors.cannot-remove-items"); - // TODO: Necessary to implement returning removed items. - + this.user.sendMessage("challenges.errors.not-enough-items", + "[items]", + Util.prettifyText(required.getType().toString())); return EMPTY_RESULT; } + + maxTimes = Math.min(maxTimes, numInInventory / required.getAmount()); } } // Return the result - return new ChallengeResult().setMeetsRequirements().setRepeat( - this.manager.isChallengeComplete(this.user, this.world, this.challenge)); + return new ChallengeResult(). + setMeetsRequirements(). + setCompleteFactor(maxTimes). + setRequiredItems(requiredItems); } /** * Removes items from a user's inventory * @param requiredItemList - a list of item stacks to be removed + * @param factor - factor for required items. */ - Map removeItems(List requiredItemList) + Map removeItems(List requiredItemList, int factor) { - Map removed = new HashMap<>(); + Map removed = new HashMap<>(); for (ItemStack required : requiredItemList) { - int amountToBeRemoved = required.getAmount(); + int amountToBeRemoved = required.getAmount() * factor; List itemsInInventory; @@ -651,22 +812,25 @@ public class TryToComplete collect(Collectors.toList()); } - for (ItemStack i : itemsInInventory) + for (ItemStack itemStack : itemsInInventory) { if (amountToBeRemoved > 0) { + ItemStack dummy = itemStack.clone(); + dummy.setAmount(1); + // Remove either the full amount or the remaining amount - if (i.getAmount() >= amountToBeRemoved) + if (itemStack.getAmount() >= amountToBeRemoved) { - i.setAmount(i.getAmount() - amountToBeRemoved); - removed.merge(i.getType(), amountToBeRemoved, Integer::sum); + itemStack.setAmount(itemStack.getAmount() - amountToBeRemoved); + removed.merge(dummy, amountToBeRemoved, Integer::sum); amountToBeRemoved = 0; } else { - removed.merge(i.getType(), i.getAmount(), Integer::sum); - amountToBeRemoved -= i.getAmount(); - i.setAmount(0); + removed.merge(dummy, itemStack.getAmount(), Integer::sum); + amountToBeRemoved -= itemStack.getAmount(); + itemStack.setAmount(0); } } } @@ -713,8 +877,9 @@ public class TryToComplete /** * Checks if a island challenge can be completed or not * It returns ChallengeResult. + * @param factor - times that user wanted to complete */ - private ChallengeResult checkSurrounding() + private ChallengeResult checkSurrounding(int factor) { // Init location in player position. BoundingBox boundingBox = this.user.getPlayer().getBoundingBox().clone(); @@ -754,66 +919,73 @@ public class TryToComplete } } - ChallengeResult result = this.searchForEntities(this.challenge.getRequiredEntities(), boundingBox); + ChallengeResult result = this.searchForEntities(this.challenge.getRequiredEntities(), factor, boundingBox); - if (result.meetsRequirements && !this.challenge.getRequiredBlocks().isEmpty()) + if (result.isMeetsRequirements() && !this.challenge.getRequiredBlocks().isEmpty()) { // Search for items only if entities found - result = this.searchForBlocks(this.challenge.getRequiredBlocks(), boundingBox); + result = this.searchForBlocks(this.challenge.getRequiredBlocks(), result.getFactor(), boundingBox); } - if (result.meetsRequirements && - this.challenge.isRemoveEntities() && - !this.challenge.getRequiredEntities().isEmpty()) - { - this.removeEntities(boundingBox); - } - - if (result.meetsRequirements && - this.challenge.isRemoveBlocks() && - !this.challenge.getRequiredBlocks().isEmpty()) - { - this.removeBlocks(boundingBox); - } - - // Check if challenge is repeated. - result.setRepeat(this.manager.isChallengeComplete(this.user, this.world, this.challenge)); - return result; } /** * This method search required blocks in given challenge boundingBox. - * @param map RequiredBlock Map. + * @param requiredMap RequiredBlock Map. + * @param factor - requirement multilayer. * @param boundingBox Bounding box of island challenge * @return ChallengeResult */ - private ChallengeResult searchForBlocks(Map map, BoundingBox boundingBox) + private ChallengeResult searchForBlocks(Map requiredMap, int factor, BoundingBox boundingBox) { - Map blocks = new EnumMap<>(map); - - if (blocks.isEmpty()) + if (requiredMap.isEmpty()) { - return new ChallengeResult().setMeetsRequirements(); + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); } + Map blocks = new EnumMap<>(requiredMap); + Map blocksFound = new HashMap<>(requiredMap.size()); + + // This queue will contain only blocks whit required type ordered by distance till player. + Queue blockFromWorld = new PriorityQueue<>((o1, o2) -> { + if (o1.getType().equals(o2.getType())) + { + return Double.compare(o1.getLocation().distance(this.user.getLocation()), + o2.getLocation().distance(this.user.getLocation())); + } + else + { + return o1.getType().compareTo(o2.getType()); + } + }); + for (int x = (int) boundingBox.getMinX(); x <= boundingBox.getMaxX(); x++) { for (int y = (int) boundingBox.getMinY(); y <= boundingBox.getMaxY(); y++) { for (int z = (int) boundingBox.getMinZ(); z <= boundingBox.getMaxZ(); z++) { - Material mat = this.user.getWorld().getBlockAt(x, y, z).getType(); - // Remove one - blocks.computeIfPresent(mat, (b, amount) -> amount - 1); - // Remove any that have an amount of 0 - blocks.entrySet().removeIf(en -> en.getValue() <= 0); + Block block = this.user.getWorld().getBlockAt(x, y, z); - if (blocks.isEmpty()) + if (requiredMap.containsKey(block.getType())) { - // Return as soon as it s empty as no point to search more. - return new ChallengeResult().setMeetsRequirements(); + blockFromWorld.add(block); + + blocksFound.putIfAbsent(block.getType(), 1); + blocksFound.computeIfPresent(block.getType(), (reqEntity, amount) -> amount + 1); + + // Remove one + blocks.computeIfPresent(block.getType(), (b, amount) -> amount - 1); + // Remove any that have an amount of 0 + blocks.entrySet().removeIf(en -> en.getValue() <= 0); + + if (blocks.isEmpty() && factor == 1) + { + // Return as soon as it s empty as no point to search more. + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setBlockQueue(blockFromWorld); + } } } } @@ -821,7 +993,21 @@ public class TryToComplete if (blocks.isEmpty()) { - return new ChallengeResult().setMeetsRequirements(); + if (factor > 1) + { + // Calculate minimal completion count. + + for (Map.Entry entry : blocksFound.entrySet()) + { + factor = Math.min(factor, + entry.getValue() / requiredMap.get(entry.getKey())); + } + } + + // kick garbage collector + blocksFound.clear(); + + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setBlockQueue(blockFromWorld); } this.user.sendMessage("challenges.errors.not-close-enough", "[number]", String.valueOf(this.challenge.getSearchRadius())); @@ -830,94 +1016,134 @@ public class TryToComplete "[amount]", String.valueOf(v), "[item]", Util.prettifyText(k.toString()))); + + // kick garbage collector + blocks.clear(); + blocksFound.clear(); + requiredMap.clear(); + return EMPTY_RESULT; } /** * This method search required entities in given radius from user position and entity is inside boundingBox. - * @param map RequiredEntities Map. + * @param requiredMap RequiredEntities Map. + * @param factor - requirements multiplier. * @param boundingBox Bounding box of island challenge * @return ChallengeResult */ - private ChallengeResult searchForEntities(Map map, BoundingBox boundingBox) + private ChallengeResult searchForEntities(Map requiredMap, + int factor, + BoundingBox boundingBox) { - Map entities = map.isEmpty() ? new EnumMap<>(EntityType.class) : new EnumMap<>(map); - - if (entities.isEmpty()) + if (requiredMap.isEmpty()) { - return new ChallengeResult().setMeetsRequirements(); + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); } - int searchRadius = this.challenge.getSearchRadius(); + // Collect all entities that could be removed. + Map entitiesFound = new HashMap<>(); + Map minimalRequirements = new EnumMap<>(requiredMap); - this.user.getPlayer().getNearbyEntities(searchRadius, searchRadius, searchRadius).forEach(entity -> { - // Check if entity is inside challenge bounding box - if (boundingBox.contains(entity.getBoundingBox())) + // Create queue that contains all required entities ordered by distance till player. + Queue entityQueue = new PriorityQueue<>((o1, o2) -> { + if (o1.getType().equals(o2.getType())) { - // Look through all the nearby Entities, filtering by type - entities.computeIfPresent(entity.getType(), (reqEntity, amount) -> amount - 1); - entities.entrySet().removeIf(e -> e.getValue() == 0); + return Double.compare(o1.getLocation().distance(this.user.getLocation()), + o2.getLocation().distance(this.user.getLocation())); + } + else + { + return o1.getType().compareTo(o2.getType()); } }); - if (entities.isEmpty()) + this.world.getNearbyEntities(boundingBox).forEach(entity -> { + // Check if entity is inside challenge bounding box + if (requiredMap.containsKey(entity.getType())) + { + entitiesFound.putIfAbsent(entity.getType(), 1); + entitiesFound.computeIfPresent(entity.getType(), (reqEntity, amount) -> amount + 1); + + // Look through all the nearby Entities, filtering by type + minimalRequirements.computeIfPresent(entity.getType(), (reqEntity, amount) -> amount - 1); + minimalRequirements.entrySet().removeIf(e -> e.getValue() == 0); + } + }); + + if (minimalRequirements.isEmpty()) { - return new ChallengeResult().setMeetsRequirements(); + if (factor > 1) + { + // Calculate minimal completion count. + + for (Map.Entry entry : entitiesFound.entrySet()) + { + factor = Math.min(factor, + entry.getValue() / requiredMap.get(entry.getKey())); + } + } + + // Kick garbage collector + entitiesFound.clear(); + + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setEntityQueue(entityQueue); } - entities.forEach((reqEnt, amount) -> this.user.sendMessage("challenges.errors.you-still-need", + minimalRequirements.forEach((reqEnt, amount) -> this.user.sendMessage("challenges.errors.you-still-need", "[amount]", String.valueOf(amount), "[item]", Util.prettifyText(reqEnt.toString()))); + // Kick garbage collector + entitiesFound.clear(); + minimalRequirements.clear(); + entityQueue.clear(); + return EMPTY_RESULT; } /** * This method removes required block and set air instead of it. - * @param boundingBox Bounding box of island challenge + * @param blockQueue Queue with blocks that could be removed + * @param factor requirement factor for each block type. */ - private void removeBlocks(BoundingBox boundingBox) + private void removeBlocks(Queue blockQueue, int factor) { Map blocks = new EnumMap<>(this.challenge.getRequiredBlocks()); - for (int x = (int) boundingBox.getMinX(); x <= boundingBox.getMaxX(); x++) - { - for (int y = (int) boundingBox.getMinY(); y <= boundingBox.getMaxY(); y++) + // Increase required blocks by factor. + blocks.entrySet().forEach(entry -> entry.setValue(entry.getValue() * factor)); + + blockQueue.forEach(block -> { + if (blocks.containsKey(block.getType())) { - for (int z = (int) boundingBox.getMinZ(); z <= boundingBox.getMaxZ(); z++) - { - Block block = this.user.getWorld().getBlockAt(new Location(this.user.getWorld(), x, y, z)); + blocks.computeIfPresent(block.getType(), (b, amount) -> amount - 1); + blocks.entrySet().removeIf(en -> en.getValue() <= 0); - if (blocks.containsKey(block.getType())) - { - blocks.computeIfPresent(block.getType(), (b, amount) -> amount - 1); - blocks.entrySet().removeIf(en -> en.getValue() <= 0); - - block.setType(Material.AIR); - } - } + block.setType(Material.AIR); } - } + }); } /** * This method removes required entities. - * @param boundingBox Bounding box of island challenge + * @param entityQueue Queue with entities that could be removed + * @param factor requirement factor for each entity type. */ - private void removeEntities(BoundingBox boundingBox) + private void removeEntities(Queue entityQueue, int factor) { Map entities = this.challenge.getRequiredEntities().isEmpty() ? new EnumMap<>(EntityType.class) : new EnumMap<>(this.challenge.getRequiredEntities()); - int searchRadius = this.challenge.getSearchRadius(); + // Increase required entities by factor. + entities.entrySet().forEach(entry -> entry.setValue(entry.getValue() * factor)); - this.user.getPlayer().getNearbyEntities(searchRadius, searchRadius, searchRadius).forEach(entity -> { - // Look through all the nearby Entities, filtering by type - - if (entities.containsKey(entity.getType()) && boundingBox.contains(entity.getBoundingBox())) + // Go through entity queue and remove entities that are requried. + entityQueue.forEach(entity -> { + if (entities.containsKey(entity.getType())) { entities.computeIfPresent(entity.getType(), (reqEntity, amount) -> amount - 1); entities.entrySet().removeIf(e -> e.getValue() == 0); @@ -935,8 +1161,9 @@ public class TryToComplete /** * Checks if a other challenge can be completed or not * It returns ChallengeResult. + * @param factor - times that user wanted to complete */ - private ChallengeResult checkOthers() + private ChallengeResult checkOthers(int factor) { if (!this.addon.isLevelProvided() && this.challenge.getRequiredIslandLevel() != 0) @@ -981,21 +1208,19 @@ public class TryToComplete } else { - if (this.addon.isEconomyProvided() && this.challenge.isTakeMoney()) - { - this.addon.getEconomyProvider().withdraw(this.user, this.challenge.getRequiredMoney()); - } + // calculate factor - if (this.challenge.isTakeExperience() && - this.user.getPlayer().getGameMode() != GameMode.CREATIVE) - { - // Cannot take anything from creative game mode. - this.user.getPlayer().setTotalExperience( - this.user.getPlayer().getTotalExperience() - this.challenge.getRequiredExperience()); - } + if (this.addon.isEconomyProvided() && this.challenge.isTakeMoney()) + { + factor = Math.min(factor, (int) this.addon.getEconomyProvider().getBalance(this.user) / this.challenge.getRequiredMoney()); + } - return new ChallengeResult().setMeetsRequirements(). - setRepeat(this.manager.isChallengeComplete(this.user, this.world, this.challenge)); + if (this.challenge.getRequiredExperience() > 0 && this.challenge.isTakeExperience()) + { + factor = Math.min(factor, this.user.getPlayer().getTotalExperience() / this.challenge.getRequiredExperience()); + } + + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); } return EMPTY_RESULT; @@ -1060,12 +1285,9 @@ public class TryToComplete */ private class ChallengeResult { - private boolean meetsRequirements; - - private boolean repeat; - - /** + * This method sets that challenge meets all requirements at least once. + * @return Current object. */ ChallengeResult setMeetsRequirements() { @@ -1075,12 +1297,145 @@ public class TryToComplete /** - * @param repeat the repeat to set + * Method sets that challenge is completed once already + * @param completed boolean that indicate that challenge has been already completed. + * @return Current object. */ - ChallengeResult setRepeat(boolean repeat) + ChallengeResult setCompleted(boolean completed) { - this.repeat = repeat; + this.completed = completed; return this; } + + + /** + * Method sets how many times challenge can be completed. + * @param factor Integer that represents completion count. + * @return Current object. + */ + ChallengeResult setCompleteFactor(int factor) + { + this.factor = factor; + return this; + } + + + // --------------------------------------------------------------------- + // Section: Requirement memory + // --------------------------------------------------------------------- + + + /** + * Method sets requiredItems for inventory challenge. + * @param requiredItems items that are required by inventory challenge. + * @return Current object. + */ + ChallengeResult setRequiredItems(List requiredItems) + { + this.requiredItems = requiredItems; + return this; + } + + + /** + * Method sets queue that contains all blocks with required material type. + * @param blocks queue that contains required materials from world. + * @return Current object. + */ + ChallengeResult setBlockQueue(Queue blocks) + { + this.blocks = blocks; + return this; + } + + /** + * Method sets queue that contains all entities with required entity type. + * @param entities queue that contains required entities from world. + * @return Current object. + */ + ChallengeResult setEntityQueue(Queue entities) + { + this.entities = entities; + return this; + } + + + // --------------------------------------------------------------------- + // Section: Getters + // --------------------------------------------------------------------- + + + /** + * Returns value of was completed variable. + * @return value of completed variable + */ + boolean wasCompleted() + { + return this.completed; + } + + + /** + * This method returns how many times challenge can be completed. + * @return completion count. + */ + int getFactor() + { + return this.factor; + } + + + /** + * This method returns if challenge requirements has been met at least once. + * @return value of meets requirements variable. + */ + boolean isMeetsRequirements() + { + return this.meetsRequirements; + } + + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + + /** + * Boolean that indicate that challenge has already bean completed once before. + */ + private boolean completed; + + /** + * Indicates that challenge can be completed. + */ + private boolean meetsRequirements; + + /** + * Integer that represents how many times challenge is completed + */ + private int factor; + + /** + * List that contains required items for Inventory Challenge + * Necessary as it contains grouped items by type or similarity, not by limit 64. + */ + private List requiredItems; + + /** + * Map that contains removed items and their removed count. + */ + private Map removedItems = null; + + /** + * Queue of blocks that contains all blocks with the same type as requiredBlock from + * challenge requirements. + */ + private Queue blocks; + + /** + * Queue of entities that contains all entities with the same type as requiredEntities from + * challenge requirements. + */ + private Queue entities; } } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 0d09812..22706b0 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -319,9 +319,10 @@ challenges: admin: hit-things: 'Hit things to add them to the list of things required. Right click when done.' you-added: 'You added one [thing] to the challenge' - challenge-created: '[challenge] created!' + challenge-created: '[challenge]&r created!' you-completed-challenge: '&2You completed the [value] &r&2challenge!' you-repeated-challenge: '&2You repeated the [value] &r&2challenge!' + you-repeated-challenge-multiple: '&2You repeated the [value] &r&2challenge [count] times!' you-completed-level: '&2You completed the [value] &r&2level!' name-has-completed-challenge: '&5[name] has completed the [value] &r&5challenge!' name-has-completed-level: '&5[name] has completed the [value] &r&5level!' diff --git a/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java b/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java index 00ab34c..b60aff7 100644 --- a/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java +++ b/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java @@ -1,24 +1,27 @@ -/** - * - */ package world.bentobox.challenges.tasks; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.logging.Logger; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import world.bentobox.challenges.ChallengesAddon; @@ -26,147 +29,202 @@ import world.bentobox.bentobox.api.user.User; /** * @author tastybento - * TODO: This test should be fixed. */ @RunWith(PowerMockRunner.class) +@PrepareForTest({ Bukkit.class}) public class TryToCompleteTest { - private User user; - ItemStack[] stacks = { new ItemStack(Material.PAPER, 32), - new ItemStack(Material.ACACIA_BOAT), - null, - null, - new ItemStack(Material.CACTUS, 32), - new ItemStack(Material.CACTUS, 32), - new ItemStack(Material.CACTUS, 32), - new ItemStack(Material.GOLD_BLOCK, 32) - }; - List required; - private ChallengesAddon addon; - private PlayerInventory inv; + private User user; + ItemStack[] stacks = { new ItemStack(Material.PAPER, 32), + new ItemStack(Material.ACACIA_BOAT), + null, + null, + new ItemStack(Material.CACTUS, 32), + new ItemStack(Material.CACTUS, 32), + new ItemStack(Material.CACTUS, 32), + new ItemStack(Material.BRICK_STAIRS, 64), + new ItemStack(Material.BRICK_STAIRS, 64), + new ItemStack(Material.BRICK_STAIRS, 5), + new ItemStack(Material.GOLD_BLOCK, 32) + }; + List required; + private ChallengesAddon addon; + private PlayerInventory inv; - /** - * @throws java.lang.Exception - */ - @Before - public void setUp() throws Exception { - user = mock(User.class); - inv = mock(PlayerInventory.class); - when(inv.getContents()).thenReturn(stacks); - when(user.getInventory()).thenReturn(inv); - addon = mock(ChallengesAddon.class); - required = new ArrayList<>(); - } + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + user = mock(User.class); + inv = mock(PlayerInventory.class); + when(inv.getContents()).thenReturn(stacks); + when(user.getInventory()).thenReturn(inv); + addon = mock(ChallengesAddon.class); + required = new ArrayList<>(); - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsSuccess() { - Material reqMat = Material.PAPER; - int reqQty = 21; - required.add(new ItemStack(reqMat, reqQty)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(reqMat) == reqQty); - } + Server server = mock(Server.class); + ItemFactory itemFactory = mock(ItemFactory.class); + when(server.getItemFactory()).thenReturn(itemFactory); - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsMax() { - Material reqMat = Material.PAPER; - int reqQty = 50; - required.add(new ItemStack(reqMat, reqQty)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(reqMat) == 32); - } + // Test will not work with items that has meta data. + when(itemFactory.getItemMeta(any())).thenReturn(null); + when(itemFactory.equals(null, null)).thenReturn(true); - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsZero() { - Material reqMat = Material.PAPER; - int reqQty = 0; - required.add(new ItemStack(reqMat, reqQty)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(reqMat) == null); - } + PowerMockito.mockStatic(Bukkit.class); + when(Bukkit.getServer()).thenReturn(server); - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsSuccessMultiple() { - required.add(new ItemStack(Material.PAPER, 11)); - required.add(new ItemStack(Material.PAPER, 5)); - required.add(new ItemStack(Material.PAPER, 5)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(Material.PAPER) == 21); - } + when(Bukkit.getItemFactory()).thenReturn(itemFactory); + when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger()); + } - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsSuccessMultipleOther() { - required.add(new ItemStack(Material.CACTUS, 5)); - required.add(new ItemStack(Material.PAPER, 11)); - required.add(new ItemStack(Material.PAPER, 5)); - required.add(new ItemStack(Material.PAPER, 5)); - required.add(new ItemStack(Material.CACTUS, 5)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(Material.PAPER) == 21); - assertTrue(removed.get(Material.CACTUS) == 10); - } + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsSuccess() { + Material requiredMaterial = Material.PAPER; + int requiredQuantity = 21; - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsMultipleOtherFail() { - required.add(new ItemStack(Material.ACACIA_FENCE, 5)); - required.add(new ItemStack(Material.ARROW, 11)); - required.add(new ItemStack(Material.STONE, 5)); - required.add(new ItemStack(Material.BAKED_POTATO, 5)); - required.add(new ItemStack(Material.GHAST_SPAWN_EGG, 5)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.isEmpty()); + this.required.add(new ItemStack(requiredMaterial, requiredQuantity)); + TryToComplete x = new TryToComplete(this.addon); + x.user(this.user); + Map removed = x.removeItems(this.required, 1); - } + assertEquals((int) removed.getOrDefault(new ItemStack(requiredMaterial, 1), 0), requiredQuantity); + } - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsFail() { - required.add(new ItemStack(Material.GOLD_BLOCK, 55)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - // It will remove 32, but not any more - assertTrue(removed.get(Material.GOLD_BLOCK) == 32); - // An error will be thrown - Mockito.verify(addon, Mockito.times(1)).logError(Mockito.anyString()); - } + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsMax() { + Material requiredMaterial = Material.PAPER; + int requiredQuantity = 50; + + this.required.add(new ItemStack(requiredMaterial, requiredQuantity)); + TryToComplete x = new TryToComplete(this.addon); + x.user(this.user); + Map removed = x.removeItems(this.required, 1); + + assertNotEquals((int) removed.getOrDefault(new ItemStack(requiredMaterial, 1), 0), requiredQuantity); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsZero() { + Material requiredMaterial = Material.PAPER; + int requiredQuantity = 0; + + this.required.add(new ItemStack(requiredMaterial, requiredQuantity)); + TryToComplete x = new TryToComplete(this.addon); + x.user(this.user); + Map removed = x.removeItems(this.required, 1); + + assertTrue(removed.isEmpty()); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsSuccessMultiple() { + required.add(new ItemStack(Material.PAPER, 11)); + required.add(new ItemStack(Material.PAPER, 5)); + required.add(new ItemStack(Material.PAPER, 5)); + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + + assertEquals((int) removed.getOrDefault(new ItemStack(Material.PAPER, 1), 0), 21); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsSuccessMultipleOther() { + required.add(new ItemStack(Material.CACTUS, 5)); + required.add(new ItemStack(Material.PAPER, 11)); + required.add(new ItemStack(Material.PAPER, 5)); + required.add(new ItemStack(Material.PAPER, 5)); + required.add(new ItemStack(Material.CACTUS, 5)); + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + + assertEquals((int) removed.getOrDefault(new ItemStack(Material.PAPER, 1), 0), 21); + assertEquals((int) removed.getOrDefault(new ItemStack(Material.CACTUS, 1), 0), 10); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsMultipleOtherFail() { + required.add(new ItemStack(Material.ACACIA_FENCE, 5)); + required.add(new ItemStack(Material.ARROW, 11)); + required.add(new ItemStack(Material.STONE, 5)); + required.add(new ItemStack(Material.BAKED_POTATO, 5)); + required.add(new ItemStack(Material.GHAST_SPAWN_EGG, 5)); + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + assertTrue(removed.isEmpty()); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsFail() { + ItemStack input = new ItemStack(Material.GOLD_BLOCK, 55); + required.add(input); + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + + // It will remove 32, but not any more + assertEquals((int) removed.getOrDefault(new ItemStack(Material.GOLD_BLOCK, 1), 0), 32); + + // An error will be thrown + Mockito.verify(addon, Mockito.times(1)).logError(Mockito.anyString()); + } + + + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRequireTwoStacks() { + required.add(new ItemStack(Material.BRICK_STAIRS, 64)); + required.add(new ItemStack(Material.BRICK_STAIRS, 64)); + + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + + // It should remove both stacks + assertEquals((int) removed.getOrDefault(new ItemStack(Material.BRICK_STAIRS, 1), 0), 128); + } + + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testFactorStacks() { + required.add(new ItemStack(Material.BRICK_STAIRS, 32)); + + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 4); + + // It should remove both stacks + assertEquals((int) removed.getOrDefault(new ItemStack(Material.BRICK_STAIRS, 1), 0), 128); + } } +