diff --git a/pom.xml b/pom.xml index 35d6c136f..7ab9b80d2 100644 --- a/pom.xml +++ b/pom.xml @@ -73,7 +73,7 @@ 42.2.18 5.0.1 - 1.20.4-R0.1-SNAPSHOT + 1.20.3-R0.1-SNAPSHOT 1.20.2-R0.1-SNAPSHOT diff --git a/src/main/java/world/bentobox/bentobox/BentoBox.java b/src/main/java/world/bentobox/bentobox/BentoBox.java index 6f2a5759e..ee026484f 100644 --- a/src/main/java/world/bentobox/bentobox/BentoBox.java +++ b/src/main/java/world/bentobox/bentobox/BentoBox.java @@ -463,6 +463,10 @@ public boolean loadSettings() { getPluginLoader().disablePlugin(this); return false; } + + log("Saving default panels..."); + this.saveResource("panels/island_creation_panel.yml", false); + this.saveResource("panels/language_panel.yml", false); return true; } diff --git a/src/main/java/world/bentobox/bentobox/api/addons/Addon.java b/src/main/java/world/bentobox/bentobox/api/addons/Addon.java index 4c823244c..57190b7d9 100644 --- a/src/main/java/world/bentobox/bentobox/api/addons/Addon.java +++ b/src/main/java/world/bentobox/bentobox/api/addons/Addon.java @@ -12,6 +12,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Logger; +import java.util.regex.Matcher; import org.bukkit.Bukkit; import org.bukkit.Server; @@ -263,7 +264,7 @@ public File saveResource(String jarResource, File destinationFolder, boolean rep throw new IllegalArgumentException("ResourcePath cannot be null or empty"); } - jarResource = jarResource.replace("\\", File.separator).replace("/", File.separator); + jarResource = jarResource.replace('\\', '/'); try (JarFile jar = new JarFile(file)) { JarEntry jarConfig = jar.getJarEntry(jarResource); if (jarConfig != null) { @@ -273,7 +274,9 @@ public File saveResource(String jarResource, File destinationFolder, boolean rep "The embedded resource '" + jarResource + "' cannot be found in " + jar.getName()); } // There are two options, use the path of the resource or not - File outFile = new File(destinationFolder, jarResource); + File outFile = new File(destinationFolder, + jarResource.replaceAll("/", Matcher.quoteReplacement(File.separator))); + if (noPath) { outFile = new File(destinationFolder, outFile.getName()); } @@ -308,7 +311,7 @@ public YamlConfiguration getYamlFromJar(String jarResource) throws IOException, throw new IllegalArgumentException("jarResource cannot be null or empty"); } YamlConfiguration result = new YamlConfiguration(); - jarResource = jarResource.replace("\\", File.separator).replace("/", File.separator); + jarResource = jarResource.replace('\\', '/'); try (JarFile jar = new JarFile(file)) { JarEntry jarConfig = jar.getJarEntry(jarResource); if (jarConfig != null) { @@ -330,7 +333,7 @@ public InputStream getResource(String jarResource) { throw new IllegalArgumentException("ResourcePath cannot be null or empty"); } - jarResource = jarResource.replace("\\", File.separator).replace("/", File.separator); + jarResource = jarResource.replace('\\', '/'); try (JarFile jar = new JarFile(file)) { JarEntry jarConfig = jar.getJarEntry(jarResource); if (jarConfig != null) { diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandCreateCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandCreateCommand.java index d42c212d6..54d3c7a12 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandCreateCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandCreateCommand.java @@ -12,7 +12,7 @@ import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.managers.BlueprintsManager; import world.bentobox.bentobox.managers.island.NewIsland; -import world.bentobox.bentobox.panels.IslandCreationPanel; +import world.bentobox.bentobox.panels.customizable.IslandCreationPanel; import world.bentobox.bentobox.util.Util; /** diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandLanguageCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandLanguageCommand.java index 87c111caa..7bd15ec90 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandLanguageCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandLanguageCommand.java @@ -7,7 +7,7 @@ import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.panels.LanguagePanel; +import world.bentobox.bentobox.panels.customizable.LanguagePanel; import world.bentobox.bentobox.util.Util; /** @@ -46,7 +46,7 @@ public boolean execute(User user, String label, List args) { return false; } } else { - LanguagePanel.openPanel(user); + LanguagePanel.openPanel(this, user); } return true; } diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandResetCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandResetCommand.java index 884a6713e..feb7fb78b 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandResetCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandResetCommand.java @@ -16,7 +16,7 @@ import world.bentobox.bentobox.managers.BlueprintsManager; import world.bentobox.bentobox.managers.island.NewIsland; import world.bentobox.bentobox.managers.island.NewIsland.Builder; -import world.bentobox.bentobox.panels.IslandCreationPanel; +import world.bentobox.bentobox.panels.customizable.IslandCreationPanel; import world.bentobox.bentobox.util.Util; /** diff --git a/src/main/java/world/bentobox/bentobox/database/json/BentoboxTypeAdapterFactory.java b/src/main/java/world/bentobox/bentobox/database/json/BentoboxTypeAdapterFactory.java index d1263ec9c..a8faa19e1 100644 --- a/src/main/java/world/bentobox/bentobox/database/json/BentoboxTypeAdapterFactory.java +++ b/src/main/java/world/bentobox/bentobox/database/json/BentoboxTypeAdapterFactory.java @@ -3,6 +3,7 @@ import java.util.Map; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.configuration.serialization.ConfigurationSerializable; @@ -23,6 +24,7 @@ import world.bentobox.bentobox.database.json.adapters.FlagTypeAdapter; import world.bentobox.bentobox.database.json.adapters.ItemStackTypeAdapter; import world.bentobox.bentobox.database.json.adapters.LocationTypeAdapter; +import world.bentobox.bentobox.database.json.adapters.MaterialTypeAdapter; import world.bentobox.bentobox.database.json.adapters.PotionEffectTypeAdapter; import world.bentobox.bentobox.database.json.adapters.VectorTypeAdapter; import world.bentobox.bentobox.database.json.adapters.WorldTypeAdapter; @@ -55,6 +57,8 @@ public TypeAdapter create(Gson gson, TypeToken type) { if (Location.class.isAssignableFrom(rawType)) { // Use our current location adapter for backward compatibility return (TypeAdapter) new LocationTypeAdapter(); + } else if (Material.class.isAssignableFrom(rawType)) { + return (TypeAdapter) new MaterialTypeAdapter(); } else if (Biome.class.isAssignableFrom(rawType)) { return (TypeAdapter) new BiomeTypeAdapter(); } else if (Enum.class.isAssignableFrom(rawType)) { diff --git a/src/main/java/world/bentobox/bentobox/database/json/adapters/MaterialTypeAdapter.java b/src/main/java/world/bentobox/bentobox/database/json/adapters/MaterialTypeAdapter.java new file mode 100644 index 000000000..65150dbe6 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/json/adapters/MaterialTypeAdapter.java @@ -0,0 +1,56 @@ +package world.bentobox.bentobox.database.json.adapters; + + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.Material; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + + +/** + * Minecraft 1.20 changed GRASS to SHORT_GRASS. This class provides and backwards compatibility when loading + * databased files stored with previous versions. It can be extended in the future if further enum changes are made. + * @author tastybento + * @since 2.0.0 + */ +public final class MaterialTypeAdapter extends TypeAdapter +{ + /** + * Map that contains string value to the actual Material enum object. + */ + final Map materialMap; + + public MaterialTypeAdapter() { + this.materialMap = new HashMap<>(); + + // Put in current values. + Arrays.stream(Material.values()).forEach(mat -> this.materialMap.put(mat.name(), mat)); + + // Put in renamed material values. + this.materialMap.put("GRASS", Material.SHORT_GRASS); + } + + @Override + public Material read(JsonReader input) throws IOException + { + if (JsonToken.NULL.equals(input.peek())) { + input.nextNull(); + return null; + } + + return this.materialMap.get(input.nextString().toUpperCase()); + } + + @Override + public void write(JsonWriter output, Material enumValue) throws IOException { + output.value(enumValue != null ? enumValue.name() : null); + } +} + diff --git a/src/main/java/world/bentobox/bentobox/panels/IslandCreationPanel.java b/src/main/java/world/bentobox/bentobox/panels/IslandCreationPanel.java deleted file mode 100644 index 0af9ed7c7..000000000 --- a/src/main/java/world/bentobox/bentobox/panels/IslandCreationPanel.java +++ /dev/null @@ -1,91 +0,0 @@ -package world.bentobox.bentobox.panels; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import org.eclipse.jdt.annotation.NonNull; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.commands.CompositeCommand; -import world.bentobox.bentobox.api.panels.PanelItem; -import world.bentobox.bentobox.api.panels.builders.PanelBuilder; -import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBundle; -import world.bentobox.bentobox.managers.BlueprintsManager; -import world.bentobox.bentobox.util.Util; - - -/** - * Displays the available BlueprintBundles to pick up as the island. - * @author tastybento - * @since 1.5.0 - */ -public class IslandCreationPanel { - - private IslandCreationPanel() {} - - /** - * Shows a player a panel of selectable blueprint bundles. Checks user's permission - * @param command - the command requesting the panel, e.g., create or reset - * @param user - the user - * @param label - label - */ - public static void openPanel(@NonNull CompositeCommand command, @NonNull User user, @NonNull String label) { - BentoBox plugin = BentoBox.getInstance(); - // Create the panel - PanelBuilder pb = new PanelBuilder().name(user.getTranslation("commands.island.create.pick")).user(user); - // Get the bundles - Comparator sortByDisplayName = (p, o) -> p.getDisplayName().compareToIgnoreCase(o.getDisplayName()); - List bbs = plugin.getBlueprintsManager().getBlueprintBundles(command.getAddon()).values() - .stream().sorted(sortByDisplayName).toList(); - // Loop through them and create items in the panel - for (BlueprintBundle bb : bbs) { - String perm = command.getPermissionPrefix() + "island.create." + bb.getUniqueId(); - if (bb.getUniqueId().equals(BlueprintsManager.DEFAULT_BUNDLE_NAME) - || !bb.isRequirePermission() - || user.hasPermission(perm)) { - // Add an item - PanelItem item = new PanelItemBuilder() - .name(bb.getDisplayName()) - .description(bb.getDescription().stream().map(Util::translateColorCodes).toList()) - .icon(bb.getIcon()).clickHandler((panel, user1, clickType, slot1) -> { - user1.closeInventory(); - command.execute(user1, label, Collections.singletonList(bb.getUniqueId())); - return true; - }).build(); - // Determine slot - if (bb.getSlot() < 0 || bb.getSlot() > BlueprintManagementPanel.MAX_BP_SLOT) { - bb.setSlot(0); - } - if (pb.slotOccupied(bb.getSlot())) { - int slot = getFirstAvailableSlot(pb); - if (slot == -1) { - // TODO add paging - plugin.logError("Too many blueprint bundles to show!"); - pb.item(item); - } else { - pb.item(slot, item); - } - } else { - pb.item(bb.getSlot(), item); - } - } - } - pb.build(); - } - - /** - * @param pb - panel builder - * @return first available slot, or -1 if none - */ - private static int getFirstAvailableSlot(PanelBuilder pb) { - for (int i = 0; i < BlueprintManagementPanel.MAX_BP_SLOT; i++) { - if (!pb.slotOccupied(i)) { - return i; - } - } - return -1; - } -} diff --git a/src/main/java/world/bentobox/bentobox/panels/LanguagePanel.java b/src/main/java/world/bentobox/bentobox/panels/LanguagePanel.java deleted file mode 100644 index ca2e96ef0..000000000 --- a/src/main/java/world/bentobox/bentobox/panels/LanguagePanel.java +++ /dev/null @@ -1,68 +0,0 @@ -package world.bentobox.bentobox.panels; - -import java.util.Locale; -import java.util.Objects; - -import org.apache.commons.lang.WordUtils; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.localization.BentoBoxLocale; -import world.bentobox.bentobox.api.localization.TextVariables; -import world.bentobox.bentobox.api.panels.builders.PanelBuilder; -import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.managers.LocalesManager; - -/** - * @author Poslovitch - */ -public class LanguagePanel { - - private LanguagePanel() {} - - /** - * Dynamically creates the panel. - * @param user the User to show the panel to - */ - public static void openPanel(User user) { - PanelBuilder panelBuilder = new PanelBuilder() - .name(user.getTranslation("language.panel-title")); - - LocalesManager localesManager = BentoBox.getInstance().getLocalesManager(); - - for (Locale locale : localesManager.getAvailableLocales(true)) { - PanelItemBuilder localeIcon = new PanelItemBuilder(); - - BentoBoxLocale language = localesManager.getLanguages().get(locale); - - ItemStack localeBanner = language.getBanner(); - // Set to a blank banner. - localeIcon.icon(Objects.requireNonNullElseGet(localeBanner, () -> new ItemStack(Material.WHITE_BANNER, 1))); - localeIcon.name(ChatColor.WHITE + WordUtils.capitalize(locale.getDisplayName(user.getLocale()))) - .clickHandler((panel, u, click, slot) -> { - BentoBox.getInstance().getPlayers().setLocale(u.getUniqueId(), locale.toLanguageTag()); - u.sendMessage("language.edited", "[lang]", WordUtils.capitalize(locale.getDisplayName(user.getLocale()))); - openPanel(u); - return true; - }); - if (user.getLocale().equals(locale)) { - localeIcon.description(user.getTranslation("language.description.selected"), ""); - } else { - localeIcon.description(user.getTranslation("language.description.click-to-select"), ""); - } - - localeIcon.description(user.getTranslation("language.description.authors")); - for (String author : language.getAuthors()) { - localeIcon.description(user.getTranslation("language.description.author", TextVariables.NAME, author)); - } - - panelBuilder.item(localeIcon.build()); - } - - panelBuilder.build().open(user); - } - -} diff --git a/src/main/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanel.java b/src/main/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanel.java new file mode 100644 index 000000000..6824e7eac --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanel.java @@ -0,0 +1,544 @@ +// +// Created by BONNe +// Copyright - 2023 +// + + +package world.bentobox.bentobox.panels.customizable; + + +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import java.io.File; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.TemplatedPanel; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.panels.builders.TemplatedPanelBuilder; +import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBundle; +import world.bentobox.bentobox.util.Util; + + +/** + * This class generates Island Creation Panel based on user specified file with name: "island_creation_panel.yml". + * If file with such name is located at gamemode panels directory, then that file will be used. + * Otherwise, file in BentoBox/panels is used. + */ +public class IslandCreationPanel +{ + // --------------------------------------------------------------------- + // Section: Constructor + // --------------------------------------------------------------------- + + + /** + * This is internal constructor. It is used internally in current class to avoid creating objects everywhere. + * + * @param command CompositeCommand object + * @param label The main command label + * @param user User who opens panel + */ + private IslandCreationPanel(@NonNull CompositeCommand command, + @NonNull User user, + @NonNull String label) + { + this.plugin = BentoBox.getInstance(); + this.user = user; + this.mainLabel = label; + + this.elementList = this.plugin.getBlueprintsManager().getBlueprintBundles(command.getAddon()).values().stream(). + sorted(Comparator.comparingInt(BlueprintBundle::getSlot).thenComparing(BlueprintBundle::getUniqueId)). + filter(bundle -> !bundle.isRequirePermission() || + this.user.hasPermission(command.getPermissionPrefix() + "island.create." + bundle.getUniqueId())). + toList(); + + this.mainCommand = command; + } + + + // --------------------------------------------------------------------- + // Section: Methods + // --------------------------------------------------------------------- + + + /** + * Build method manages current panel opening. It uses BentoBox PanelAPI that is easy to use and users can get nice + * panels. + */ + private void build() + { + // Do not open gui if there is no magic sticks. + if (this.elementList.isEmpty()) + { + this.plugin.logError("There are no available phases for selection!"); + this.user.sendMessage("no-phases", + TextVariables.GAMEMODE, this.plugin.getDescription().getName()); + return; + } + + // Start building panel. + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + + // Set main template. + if (this.doesCustomPanelExists(this.mainCommand.getAddon(), "island_creation_panel")) + { + // Addon has its own island creation panel. Use it. + panelBuilder.template("island_creation_panel", new File(this.mainCommand.getAddon().getDataFolder(), "panels")); + } + else + { + // Use default island creation panel. + panelBuilder.template("island_creation_panel", new File(this.plugin.getDataFolder(), "panels")); + } + + panelBuilder.user(this.user); + panelBuilder.world(this.user.getWorld()); + + // Register button builders + panelBuilder.registerTypeBuilder(BUNDLES, this::createBundleButton); + + // Register next and previous builders + panelBuilder.registerTypeBuilder(NEXT, this::createNextButton); + panelBuilder.registerTypeBuilder(PREVIOUS, this::createPreviousButton); + + // Register unknown type builder. + panelBuilder.build(); + } + + + /** + * This method returns if panel with the requested name is located in GameModeAddon folder. + * @param addon GameModeAddon that need to be checked. + * @param name Name of the panel. + * @return {@code true} if panel exists, {@code false} otherwise. + */ + private boolean doesCustomPanelExists(GameModeAddon addon, String name) + { + return addon.getDataFolder().exists() && + new File(addon.getDataFolder(), "panels").exists() && + new File(addon.getDataFolder(), "panels" + File.separator + name + ".yml").exists(); + } + + + // --------------------------------------------------------------------- + // Section: Buttons + // --------------------------------------------------------------------- + + + /** + * Create next button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + @Nullable + private PanelItem createNextButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + int size = this.elementList.size(); + + if (size <= slot.amountMap().getOrDefault(BUNDLES, 1) || + 1.0 * size / slot.amountMap().getOrDefault(BUNDLES, 1) <= this.pageIndex + 1) + { + // There are no next elements + return null; + } + + int nextPageIndex = this.pageIndex + 2; + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((boolean) template.dataMap().getOrDefault(INDEXING, false)) + { + clone.setAmount(nextPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.mainCommand.getWorld(), template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.mainCommand.getWorld(), template.description(), + TextVariables.NUMBER, String.valueOf(nextPageIndex))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + template.actions().forEach(action -> { + if ((clickType == action.clickType() || + action.clickType() == ClickType.UNKNOWN) && NEXT.equalsIgnoreCase(action.actionType())) + { + // Next button ignores click type currently. + this.pageIndex++; + this.build(); + } + + }); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.mainCommand.getWorld(), action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + /** + * Create previous button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + @Nullable + private PanelItem createPreviousButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.pageIndex == 0) + { + // There are no next elements + return null; + } + + int previousPageIndex = this.pageIndex; + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((boolean) template.dataMap().getOrDefault(INDEXING, false)) + { + clone.setAmount(previousPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.mainCommand.getWorld(), template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.mainCommand.getWorld(), template.description(), + TextVariables.NUMBER, String.valueOf(previousPageIndex))); + } + + // Add ClickHandler + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + template.actions().forEach(action -> { + if ((clickType == action.clickType() || + action.clickType() == ClickType.UNKNOWN) && PREVIOUS.equalsIgnoreCase(action.actionType())) + { + // Next button ignores click type currently. + this.pageIndex--; + this.build(); + } + + }); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.mainCommand.getWorld(), action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + /** + * This method creates and returns bundle button. + * + * @return PanelItem that represents bundle button. + */ + @Nullable + private PanelItem createBundleButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.elementList.isEmpty()) + { + // Does not contain any sticks. + return null; + } + + int index = this.pageIndex * slot.amountMap().getOrDefault(BUNDLES, 1) + slot.slot(); + + BlueprintBundle blueprintBundle; + + if (index >= this.elementList.size()) + { + // Out of index. + blueprintBundle = null; + } + else + { + blueprintBundle = this.elementList.get(index); + } + + if (template.dataMap().containsKey("unique_id")) + { + // Try to find bundle with requested ID. if not found, use already collected bundle. + blueprintBundle = this.elementList.stream(). + filter(bundle -> bundle.getUniqueId().equals(template.dataMap().get("unique_id"))). + findFirst(). + orElse(blueprintBundle); + } + + return this.createBundleButton(template, blueprintBundle); + } + + + // --------------------------------------------------------------------- + // Section: Other methods + // --------------------------------------------------------------------- + + + /** + * This method creates bundle button. + * + * @return PanelItem that allows to select bundle button + */ + private PanelItem createBundleButton(ItemTemplateRecord template, BlueprintBundle bundle) + { + if (bundle == null) + { + // return as bundle is null. Empty button will be created. + return null; + } + + final String reference = "panels.island_creation.buttons.bundle."; + + // Get settings for island. + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + builder.icon(template.icon().clone()); + } + else + { + builder.icon(bundle.getIcon()); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.mainCommand.getWorld(), template.title(), + TextVariables.NAME, bundle.getDisplayName())); + } + else + { + builder.name(this.user.getTranslation(reference + "name", + TextVariables.NAME, bundle.getDisplayName())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.mainCommand.getWorld(), template.description(), + TextVariables.DESCRIPTION, String.join("\n", bundle.getDescription()))); + } + else + { + builder.description(this.user.getTranslation(reference + "description", + TextVariables.DESCRIPTION, String.join("\n", bundle.getDescription()))); + } + + List actions = template.actions().stream(). + filter(action -> SELECT_ACTION.equalsIgnoreCase(action.actionType()) || + COMMANDS_ACTION.equalsIgnoreCase(action.actionType())). + toList(); + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + actions.forEach(action -> { + if (clickType == action.clickType() || action.clickType() == ClickType.UNKNOWN) + { + if (SELECT_ACTION.equalsIgnoreCase(action.actionType())) + { + user.closeInventory(); + this.mainCommand.execute(user, this.mainLabel, Collections.singletonList(bundle.getUniqueId())); + } + else if (COMMANDS_ACTION.equalsIgnoreCase(action.actionType())) + { + Util.runCommands(user, + Arrays.stream(action.content(). + replaceAll(Pattern.quote(TextVariables.LABEL), this.mainCommand.getTopLabel()). + split("\n")). + toList(), + ISLAND_CREATION_COMMANDS); + } + } + }); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = actions.stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.mainCommand.getWorld(), action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(actions.size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + // --------------------------------------------------------------------- + // Section: Static methods + // --------------------------------------------------------------------- + + + /** + * This method is used to open Panel outside this class. It will be much easier to open panel with single method + * call then initializing new object. + * + * @param command CompositeCommand object + * @param label The main command label + * @param user User who opens panel + */ + public static void openPanel(@NonNull CompositeCommand command, + @NonNull User user, + @NonNull String label) + { + new IslandCreationPanel(command, user, label).build(); + } + + +// --------------------------------------------------------------------- +// Section: Constants +// --------------------------------------------------------------------- + + + /** + * This constant is used for button to indicate that it is Blueprint Bundle type. + */ + private static final String BUNDLES = "BUNDLE"; + + /** + * This constant is used for button to indicate that it is previous page type. + */ + private static final String PREVIOUS = "PREVIOUS"; + + /** + * This constant is used for button to indicate that it is next page type. + */ + private static final String NEXT = "NEXT"; + + /** + * This constant is used for indicating that pages should contain numbering. + */ + private static final String INDEXING = "indexing"; + + /** + * This constant stores value for SELECT action that is used in panels. + */ + private static final String SELECT_ACTION = "SELECT"; + + /** + * This constant stores value for COMMAND action that is used in panels. + */ + private static final String COMMANDS_ACTION = "COMMANDS"; + + /** + * This constant stores value for ERROR message that will be displayed upon failing to run creation commands. + */ + private static final String ISLAND_CREATION_COMMANDS = "ISLAND_CREATION_COMMANDS"; + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + + /** + * This variable allows to access plugin object. + */ + private final BentoBox plugin; + + /** + * This variable stores main command that was triggered. + */ + private final CompositeCommand mainCommand; + + /** + * This variable holds user who opens panel. Without it panel cannot be opened. + */ + private final User user; + + /** + * This variable holds world where panel is opened. Without it panel cannot be opened. + */ + private final String mainLabel; + + /** + * This variable stores filtered elements. + */ + private final List elementList; + + /** + * This variable holds current pageIndex for multi-page island choosing. + */ + private int pageIndex; +} diff --git a/src/main/java/world/bentobox/bentobox/panels/customizable/LanguagePanel.java b/src/main/java/world/bentobox/bentobox/panels/customizable/LanguagePanel.java new file mode 100644 index 000000000..d9be6a2a9 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/panels/customizable/LanguagePanel.java @@ -0,0 +1,569 @@ +// +// Created by BONNe +// Copyright - 2022 +// + + +package world.bentobox.bentobox.panels.customizable; + + +import org.apache.commons.lang.WordUtils; +import org.bukkit.Material; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import java.io.File; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.localization.BentoBoxLocale; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.TemplatedPanel; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.panels.builders.TemplatedPanelBuilder; +import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; + + +/** + * This class generates Language Panel based on user specified file with name: "language_panel.yml". + * If file with such name is located at gamemode panels directory, then that file will be used. + * Otherwise, file in BentoBox/panels is used. + */ +public class LanguagePanel +{ + // --------------------------------------------------------------------- + // Section: Constructor + // --------------------------------------------------------------------- + + + /** + * This is internal constructor. It is used internally in current class to avoid creating objects everywhere. + * + * @param command The main addon command. + * @param user User who opens panel + */ + private LanguagePanel(@NonNull CompositeCommand command, @NonNull User user) + { + this.plugin = BentoBox.getInstance(); + this.mainCommand = command; + this.user = user; + + this.elementList = BentoBox.getInstance().getLocalesManager().getAvailableLocales(true); + } + + + // --------------------------------------------------------------------- + // Section: Methods + // --------------------------------------------------------------------- + + + /** + * Build method manages current panel opening. It uses BentoBox PanelAPI that is easy to use and users can get nice + * panels. + */ + private void build() + { + // Do not open gui if there is no magic sticks. + if (this.elementList.isEmpty()) + { + this.plugin.logError("There are no available locales for selection!"); + this.user.sendMessage("no-locales", + TextVariables.GAMEMODE, this.plugin.getDescription().getName()); + return; + } + + // Start building panel. + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + + // Set main template. + if (this.doesCustomPanelExists(this.mainCommand.getAddon(), "language_panel")) + { + // Addon has its own island creation panel. Use it. + panelBuilder.template("language_panel", new File(this.mainCommand.getAddon().getDataFolder(), "panels")); + } + else + { + // Use default island creation panel. + panelBuilder.template("language_panel", new File(this.plugin.getDataFolder(), "panels")); + } + + panelBuilder.user(this.user); + panelBuilder.world(this.user.getWorld()); + + // Register button builders + panelBuilder.registerTypeBuilder(LOCALE, this::createLocaleButton); + + // Register next and previous builders + panelBuilder.registerTypeBuilder(NEXT, this::createNextButton); + panelBuilder.registerTypeBuilder(PREVIOUS, this::createPreviousButton); + + // Register unknown type builder. + panelBuilder.build(); + } + + + /** + * This method returns if panel with the requested name is located in GameModeAddon folder. + * @param addon GameModeAddon that need to be checked. + * @param name Name of the panel. + * @return {@code true} if panel exists, {@code false} otherwise. + */ + private boolean doesCustomPanelExists(GameModeAddon addon, String name) + { + return addon.getDataFolder().exists() && + new File(addon.getDataFolder(), "panels").exists() && + new File(addon.getDataFolder(), "panels" + File.separator + name + ".yml").exists(); + } + + + // --------------------------------------------------------------------- + // Section: Buttons + // --------------------------------------------------------------------- + + + /** + * Create next button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + @Nullable + private PanelItem createNextButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + int size = this.elementList.size(); + + if (size <= slot.amountMap().getOrDefault(LOCALE, 1) || + 1.0 * size / slot.amountMap().getOrDefault(LOCALE, 1) <= this.pageIndex + 1) + { + // There are no next elements + return null; + } + + int nextPageIndex = this.pageIndex + 2; + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((boolean) template.dataMap().getOrDefault(INDEXING, false)) + { + clone.setAmount(nextPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(template.description(), + TextVariables.NUMBER, String.valueOf(nextPageIndex))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + template.actions().forEach(action -> { + if ((clickType == action.clickType() || + action.clickType() == ClickType.UNKNOWN) && NEXT.equalsIgnoreCase(action.actionType())) + { + // Next button ignores click type currently. + this.pageIndex++; + this.build(); + } + + }); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation( action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + /** + * Create previous button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + @Nullable + private PanelItem createPreviousButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.pageIndex == 0) + { + // There are no next elements + return null; + } + + int previousPageIndex = this.pageIndex; + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((boolean) template.dataMap().getOrDefault(INDEXING, false)) + { + clone.setAmount(previousPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.mainCommand.getWorld(), template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.mainCommand.getWorld(), template.description(), + TextVariables.NUMBER, String.valueOf(previousPageIndex))); + } + + // Add ClickHandler + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + template.actions().forEach(action -> { + if ((clickType == action.clickType() || + action.clickType() == ClickType.UNKNOWN) && PREVIOUS.equalsIgnoreCase(action.actionType())) + { + // Next button ignores click type currently. + this.pageIndex--; + this.build(); + } + + }); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.mainCommand.getWorld(), action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + /** + * This method creates and returns locale button. + * + * @return PanelItem that represents locale button. + */ + @Nullable + private PanelItem createLocaleButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.elementList.isEmpty()) + { + // Does not contain any sticks. + return null; + } + + int index = this.pageIndex * slot.amountMap().getOrDefault(LOCALE, 1) + slot.slot(); + + Locale locale; + + if (index >= this.elementList.size()) + { + // Out of index. + locale = null; + } + else + { + locale = this.elementList.get(index); + } + + if (template.dataMap().containsKey("lang_id")) + { + // Try to find locale with requested ID. if not found, use already collected locale. + locale = this.elementList.stream(). + filter(localeID -> localeID.toLanguageTag().equals(template.dataMap().get("lang_id"))). + findFirst(). + orElse(locale); + } + + return this.createLocaleButton(template, locale); + } + + + // --------------------------------------------------------------------- + // Section: Other methods + // --------------------------------------------------------------------- + + + /** + * This method creates locale button. + * + * @return PanelItem that allows to select locale button + */ + private PanelItem createLocaleButton(ItemTemplateRecord template, Locale locale) + { + if (locale == null) + { + // return as locale is null. Empty button will be created. + return null; + } + + final String reference = "panels.language.buttons.language."; + + // Get settings for island. + PanelItemBuilder builder = new PanelItemBuilder(); + + BentoBoxLocale language = this.plugin.getLocalesManager().getLanguages().get(locale); + + if (template.icon() != null) + { + builder.icon(template.icon().clone()); + } + else + { + builder.icon(Objects.requireNonNullElseGet(language.getBanner(), + () -> new ItemStack(Material.WHITE_BANNER, 1))); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.mainCommand.getWorld(), template.title(), + TextVariables.NAME, WordUtils.capitalize(locale.getDisplayName(this.user.getLocale())))); + } + else + { + builder.name(this.user.getTranslation(reference + "name", + TextVariables.NAME, WordUtils.capitalize(locale.getDisplayName(this.user.getLocale())))); + } + + final StringBuilder authors = new StringBuilder(); + authors.append(this.user.getTranslation(reference + "authors")); + + for (String author : language.getAuthors()) + { + authors.append("\n").append(this.user.getTranslation(reference + "author", TextVariables.NAME, author)); + } + + final StringBuilder selected = new StringBuilder(); + + if (this.user.getLocale().equals(locale)) + { + selected.append(this.user.getTranslation(reference + "selected")); + } + + String descriptionText; + + if (template.description() != null) + { + descriptionText = this.user.getTranslationOrNothing(template.description(), + AUTHORS, authors.toString(), + SELECTED, selected.toString()); + } + else + { + descriptionText = this.user.getTranslationOrNothing(reference + "description", + AUTHORS, authors.toString(), + SELECTED, selected.toString()); + } + + descriptionText = descriptionText.replaceAll("(?m)^[ \\t]*\\r?\\n", ""). + replaceAll("(? actions = template.actions().stream(). + filter(action -> !this.user.getLocale().equals(locale) && + (SELECT_ACTION.equalsIgnoreCase(action.actionType()) || + COMMANDS_ACTION.equalsIgnoreCase(action.actionType()))). + toList(); + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + actions.forEach(action -> { + if (clickType == action.clickType() || action.clickType() == ClickType.UNKNOWN) + { + if (SELECT_ACTION.equalsIgnoreCase(action.actionType())) + { + this.plugin.getPlayers().setLocale(this.user.getUniqueId(), locale.toLanguageTag()); + this.user.sendMessage("language.edited", "[lang]", + WordUtils.capitalize(locale.getDisplayName(this.user.getLocale()))); + + // Rebuild panel + this.build(); + } + else if (COMMANDS_ACTION.equalsIgnoreCase(action.actionType())) + { + Util.runCommands(user, + Arrays.stream(action.content(). + replaceAll(Pattern.quote(TextVariables.LABEL), this.mainCommand.getTopLabel()). + split("\n")). + toList(), + "CHANGE_LOCALE_COMMANDS"); + } + } + }); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = actions.stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.mainCommand.getWorld(), action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(actions.size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + // --------------------------------------------------------------------- + // Section: Static methods + // --------------------------------------------------------------------- + + + /** + * This method is used to open Panel outside this class. It will be much easier to open panel with single method + * call then initializing new object. + * + * @param command The main addon command. + * @param user User who opens panel + */ + public static void openPanel(@NonNull CompositeCommand command, @NonNull User user) + { + new LanguagePanel(command, user).build(); + } + + +// --------------------------------------------------------------------- +// Section: Constants +// --------------------------------------------------------------------- + + + /** + * This constant is used for button to indicate that it is Language type. + */ + private static final String LOCALE = "LOCALE"; + + /** + * This constant is used for button to indicate that it is previous page type. + */ + private static final String PREVIOUS = "PREVIOUS"; + + /** + * This constant is used for button to indicate that it is next page type. + */ + private static final String NEXT = "NEXT"; + + /** + * This constant is used for indicating that pages should contain numbering. + */ + private static final String INDEXING = "indexing"; + + /** + * This constant stores value for SELECT action that is used in panels. + */ + private static final String SELECT_ACTION = "SELECT"; + + /** + * This constant stores value for COMMANDS action that is used in panels. + */ + private static final String COMMANDS_ACTION = "COMMANDS"; + + /** + * This constant stores value for AUTHORS label that is used in panels. + */ + public static final String AUTHORS = "[authors]"; + + /** + * This constant stores value for SELECTED label that is used in panels. + */ + public static final String SELECTED = "[selected]"; + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + + /** + * This variable allows to access plugin object. + */ + private final BentoBox plugin; + + /** + * This variable stores the main command object. + */ + private final CompositeCommand mainCommand; + + /** + * This variable holds user who opens panel. Without it panel cannot be opened. + */ + private final User user; + + /** + * This variable stores filtered elements. + */ + private final List elementList; + + /** + * This variable holds current pageIndex for multi-page island choosing. + */ + private int pageIndex; +} diff --git a/src/main/java/world/bentobox/bentobox/util/ItemParser.java b/src/main/java/world/bentobox/bentobox/util/ItemParser.java index f483c3d2e..c315aa897 100644 --- a/src/main/java/world/bentobox/bentobox/util/ItemParser.java +++ b/src/main/java/world/bentobox/bentobox/util/ItemParser.java @@ -1,16 +1,14 @@ package world.bentobox.bentobox.util; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.MissingFormatArgumentException; -import java.util.Optional; -import java.util.UUID; +import java.net.URL; +import java.util.*; import org.bukkit.Bukkit; import org.bukkit.DyeColor; import org.bukkit.Material; import org.bukkit.block.banner.Pattern; import org.bukkit.block.banner.PatternType; +import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BannerMeta; import org.bukkit.inventory.meta.Damageable; @@ -19,10 +17,12 @@ import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.potion.PotionData; import org.bukkit.potion.PotionType; +import org.bukkit.profile.PlayerProfile; import org.eclipse.jdt.annotation.Nullable; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; +import com.google.common.base.Enums; +import com.google.gson.Gson; +import com.google.gson.JsonObject; import world.bentobox.bentobox.BentoBox; @@ -55,6 +55,33 @@ public static ItemStack parse(String text) { */ @Nullable public static ItemStack parse(@Nullable String text, @Nullable ItemStack defaultItemStack) { + if (text == null || text.isBlank()) { + // Text does not exist or is empty. + return defaultItemStack; + } + + ItemStack returnValue; + + try { + // Check if item can be parsed using bukkit item factory. + returnValue = Bukkit.getItemFactory().createItemStack(text); + } + catch (IllegalArgumentException exception) { + returnValue = ItemParser.parseOld(text, defaultItemStack); + } + + return returnValue; + } + + + /** + * Parse given string to ItemStack. + * @param text String value of item stack. + * @param defaultItemStack Material that should be returned if parsing failed. + * @return ItemStack of parsed item or defaultItemStack. + */ + @Nullable + private static ItemStack parseOld(@Nullable String text, @Nullable ItemStack defaultItemStack) { if (text == null || text.isBlank()) { // Text does not exist or is empty. @@ -113,22 +140,25 @@ else if (part.length == 2) { returnValue = parseItemDurabilityAndQuantity(part); } - if (returnValue != null - // If wrapper is just for code-style null-pointer checks. - && customModelData != null) { - return customValue(returnValue, customModelData); + // Update item meta with custom data model. + if (returnValue != null && customModelData != null) { + ItemParser.setCustomModelData(returnValue, customModelData); } - } catch (Exception exception) { BentoBox.getInstance().logError("Could not parse item " + text + " " + exception.getLocalizedMessage()); returnValue = defaultItemStack; } - return returnValue; + return returnValue; } - private static @Nullable ItemStack customValue(ItemStack returnValue, Integer customModelData) { + /** + * This method assigns custom model data to the item stack. + * @param returnValue Item stack that should be updated. + * @param customModelData Integer value of custom model data. + */ + private static void setCustomModelData(ItemStack returnValue, Integer customModelData) { // We have custom data model. Now assign it to the item-stack. ItemMeta itemMeta = returnValue.getItemMeta(); @@ -138,8 +168,9 @@ else if (part.length == 2) { // Update meta to the return item. returnValue.setItemMeta(itemMeta); } - return null; } + + /** * This method parses array of 2 items into an item stack. * First array element is material, while second array element is integer, that represents item count. @@ -197,8 +228,10 @@ private static ItemStack parseItemDurabilityAndQuantity(String[] part) { * } * @param part String array that contains 6 elements. * @return Potion with given properties. + * @deprecated due to the spigot potion changes. */ - private static ItemStack parsePotion(String[] part) { + @Deprecated + private static ItemStack parsePotionOld(String[] part) { if (part.length != 6) { throw new MissingFormatArgumentException("Potion parsing requires 6 parts."); } @@ -235,6 +268,61 @@ private static ItemStack parsePotion(String[] part) { } + /** + * This method parses array of 6 items into an item stack. + * Format: + *
{@code
+     *      POTION::QTY
+     * }
+ * Example: + *
{@code
+     *      POTION:STRENGTH:1
+     * }
+ * @link Potion Type + * @param part String array that contains 3 elements. + * @return Potion with given properties. + */ + private static ItemStack parsePotion(String[] part) { + if (part.length == 6) { + BentoBox.getInstance().logWarning("The old potion parsing detected for " + part[0] + + ". Please update your configs, as SPIGOT changed potion types."); + return parsePotionOld(part); + } + + if (part.length != 3) { + throw new MissingFormatArgumentException("Potion parsing requires 3 parts."); + } + + /* + # Format POTION::QTY + # Potion Type can be found out in: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/potion/PotionType.html + # Examples: + # POTION:STRENGTH:1 + # POTION:INSTANT_DAMAGE:2 + # POTION:JUMP:1 + # POTION:WEAKNESS:1 - any weakness potion + */ + + Material material = Material.matchMaterial(part[0]); + + if (material == null) { + BentoBox.getInstance().logWarning("Could not parse potion item " + part[0] + " so using a regular potion."); + material = Material.POTION; + } + + ItemStack result = new ItemStack(material, Integer.parseInt(part[2])); + + if (result.getItemMeta() instanceof PotionMeta meta) { + PotionType potionType = Enums.getIfPresent(PotionType.class, part[1].toUpperCase(Locale.ENGLISH)). + or(PotionType.WATER); + meta.setBasePotionType(potionType); + result.setItemMeta(meta); + } + + return result; + } + + /** * This method parses array of multiple elements for the Banner. * @param part String array that contains at least 2 elements. @@ -298,39 +386,62 @@ private static ItemStack parsePlayerHead(String[] part) { // Set correct Skull texture try { - SkullMeta meta = (SkullMeta) playerHead.getItemMeta(); - - if (part[1].length() < 17) { - // Minecraft player names are in length between 3 and 16 chars. - meta.setOwner(part[1]); - } else if (part[1].length() == 32) { - // trimmed UUID length are 32 chars. - meta.setOwningPlayer(Bukkit.getOfflinePlayer( - UUID.fromString(part[1].replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")))); - } else if (part[1].length() == 36) { - // full UUID length are 36 chars. - meta.setOwningPlayer(Bukkit.getOfflinePlayer(UUID.fromString(part[1]))); - } else { - // If chars are more than 36, apparently it is base64 encoded texture. - GameProfile profile = new GameProfile(UUID.randomUUID(), ""); - profile.getProperties().put("textures", new Property("textures", part[1])); - - // Null pointer will be caught and ignored. - Field profileField = meta.getClass().getDeclaredField("profile"); - profileField.setAccessible(true); - profileField.set(meta, profile); - } + if (playerHead.getItemMeta() instanceof SkullMeta meta) + { + PlayerProfile profile; + + if (part[1].length() < 17) { + // Minecraft player names are in length between 3 and 16 chars. + profile = Bukkit.createPlayerProfile(part[1]); + } else if (part[1].length() == 32) { + // trimmed UUID length are 32 chars. + profile = Bukkit.createPlayerProfile(UUID.fromString(part[1].replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"))); + } else if (part[1].length() == 36) { + // full UUID length are 36 chars. + profile = Bukkit.createPlayerProfile(UUID.fromString(part[1])); + } else { + // If chars are more than 36, apparently it is base64 encoded texture. + profile = Bukkit.createPlayerProfile(UUID.randomUUID(), ""); + profile.getTextures().setSkin(ItemParser.getSkinURLFromBase64(part[1])); + } - // Apply new meta to the item. - playerHead.setItemMeta(meta); + // Apply item meta. + meta.setOwnerProfile(profile); + playerHead.setItemMeta(meta); + } } catch (Exception ignored) { - // Ignored + // Could not parse player head. + BentoBox.getInstance().logError("Could not parse player head item " + part[1] + " so using a Steve head."); } return playerHead; } + /** + * This method parses base64 encoded string into URL. + * @param base64 Base64 encoded string. + * @return URL of the skin. + */ + private static URL getSkinURLFromBase64(String base64) { + /* + * Base64 encoded string is in format: { "timestamp": 0, "profileId": "UUID", + * "profileName": "USERNAME", "textures": { "SKIN": { "url": + * "https://textures.minecraft.net/texture/TEXTURE_ID" }, "CAPE": { "url": + * "https://textures.minecraft.net/texture/TEXTURE_ID" } } } + */ + try { + String decoded = new String(Base64.getDecoder().decode(base64)); + JsonObject json = new Gson().fromJson(decoded, JsonObject.class); + String url = json.getAsJsonObject("textures").getAsJsonObject("SKIN").get("url").getAsString(); + return new URL(url); + } + catch (Exception e) { + return null; + } + } + + /** * Check if given sting is an integer. * @param string Value that must be checked. diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 51e4ea18b..3cc79aa1d 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -1622,15 +1622,6 @@ protection: setting-active: '&a Active' setting-disabled: '&c Disabled' -language: - panel-title: Select your language - description: - selected: '&a Currently selected.' - click-to-select: '&e Click &a to select.' - authors: '&a Authors:' - author: '&3 - &b [name]' - edited: '&a Changed your language to &e [lang]&a .' - management: panel: title: BentoBox Management @@ -1788,6 +1779,51 @@ panel: &a Allow BentoBox to connect to GitHub in &a the configuration or try again later. + +# This section contains values for BentoBox panels. +panels: + # The section of translations used in Island Creation Panel + island_creation: + title: "&2&l Pick an island" + buttons: + # This button is used for displaying blueprint bundle in the island creation panel. + bundle: + name: "&l [name]" + description: |- + [description] + # The section of translations used in Language Panel + language: + title: "&2&l Select your language" + buttons: + # This button is used for displaying different locales that are available in language selection panel. + language: + name: "&f&l [name]" + description: |- + [authors] + |[selected] + authors: "&7 Authors: " + author: "&7 - &b [name]" + selected: "&a Currently selected." + # The set of common buttons used in multiple panels. + buttons: + # Button that is used in multi-page GUIs which allows to return to previous page. + previous: + name: "&f&l Previous Page" + description: |- + &7 Switch to [number] page + # Button that is used in multi-page GUIs which allows to go to next page. + next: + name: "&f&l Next Page" + description: |- + &7 Switch to [number] page + tips: + click-to-next: "&e Click &7 for next." + click-to-previous: "&e Click &7 for previous." + click-to-choose: "&e Click &7 to select." + click-to-toggle: "&e Click &7 to toggle." + left-click-to-cycle-down: "&e Left Click &7 to cycle downwards." + right-click-to-cycle-up: "&e Right Click &7 to cycle upwards." + successfully-loaded: |2 &6 ____ _ ____ diff --git a/src/main/resources/panels/island_creation_panel.yml b/src/main/resources/panels/island_creation_panel.yml new file mode 100644 index 000000000..f58940e29 --- /dev/null +++ b/src/main/resources/panels/island_creation_panel.yml @@ -0,0 +1,71 @@ +# This is default island creation panel. It is used in all situations when gamemode addon does not have specified their +# of panel. +island_creation_panel: + title: panels.island_creation.title # The title of panel or link to the localization location. + type: INVENTORY # The type of inventory: INVENTORY, DROPPER, HOPPER + background: # The item that will be displayed in empty spots. This section can be removed. + icon: BLACK_STAINED_GLASS_PANE # The icon of background item + title: "&b&r" # Empty text # The text of background item + border: # The item that will be displayed around the inventory. This section can be removed. + icon: BLACK_STAINED_GLASS_PANE # The icon of background item + title: "&b&r" # Empty text # The text of background item + force-shown: [] # Allow to specify (1-6, 1-3, 1) which rows must be showed regardless of empty elements. + content: # Allow to define buttons in your panel. + 2: + 2: blueprint_bundle_button # String values are expected to be `reusables` that are defined at the end of this file. + 3: blueprint_bundle_button + 4: blueprint_bundle_button + 5: blueprint_bundle_button + 6: blueprint_bundle_button + 7: blueprint_bundle_button + 8: blueprint_bundle_button + 3: + 1: + icon: TIPPED_ARROW:INSTANT_HEAL::::1 # The icon for button + title: panels.buttons.previous.name # The name of button, or link to the localization. + description: panels.buttons.previous.description # The description of button, or link to the localization. + data: + type: PREVIOUS # Indicates what button is doing. Available values depends on panel + indexing: true # Parameter for button. + actions: # List of actions that button can do. Available values depends on button + previous: + click-type: UNKNOWN # UNKNOWN means that any click type is respected. + tooltip: panels.tips.click-to-previous # Tooltips are always generated an empty line bellow description/title. Not required. + 2: blueprint_bundle_button + 3: blueprint_bundle_button + 4: blueprint_bundle_button + 5: blueprint_bundle_button + 6: blueprint_bundle_button + 7: blueprint_bundle_button + 8: blueprint_bundle_button + 9: + icon: TIPPED_ARROW:JUMP::::1 + title: panels.buttons.next.name + description: panels.buttons.next.description + data: + type: NEXT + indexing: true + actions: + next: + click-type: UNKNOWN + tooltip: panels.tips.click-to-next + 4: + 2: blueprint_bundle_button + 3: blueprint_bundle_button + 4: blueprint_bundle_button + 5: blueprint_bundle_button + 6: blueprint_bundle_button + 7: blueprint_bundle_button + 8: blueprint_bundle_button + reusable: # List of reoccurring buttons in the panels. + blueprint_bundle_button: # The ID of the button + # icon: GRASS_BLOCK + title: panels.island_creation.buttons.bundle.name + description: panels.island_creation.buttons.bundle.description + data: + type: BUNDLE + # unique_id: default # Specifying unique_id will force to show the requested bundle if it is available. + actions: + select: + click-type: UNKNOWN + tooltip: panels.tips.click-to-choose \ No newline at end of file diff --git a/src/main/resources/panels/language_panel.yml b/src/main/resources/panels/language_panel.yml new file mode 100644 index 000000000..707be9534 --- /dev/null +++ b/src/main/resources/panels/language_panel.yml @@ -0,0 +1,71 @@ +# This is default language selection panel. It is used in all situations when gamemode addon does not have specified their +# of panel. +language_panel: + title: panels.language.title # The title of panel or link to the localization location. + type: INVENTORY # The type of inventory: INVENTORY, DROPPER, HOPPER + background: # The item that will be displayed in empty spots. This section can be removed. + icon: BLACK_STAINED_GLASS_PANE # The icon of background item + title: "&b&r" # Empty text # The text of background item + border: # The item that will be displayed around the inventory. This section can be removed. + icon: BLACK_STAINED_GLASS_PANE # The icon of background item + title: "&b&r" # Empty text # The text of background item + force-shown: [] # Allow to specify (1-6, 1-3, 1) which rows must be showed regardless of empty elements. + content: # Allow to define buttons in your panel. + 2: + 2: language_button # String values are expected to be `reusables` that are defined at the end of this file. + 3: language_button + 4: language_button + 5: language_button + 6: language_button + 7: language_button + 8: language_button + 3: + 1: + icon: TIPPED_ARROW:INSTANT_HEAL::::1 # The icon for button + title: panels.buttons.previous.name # The name of button, or link to the localization. + description: panels.buttons.previous.description # The description of button, or link to the localization. + data: + type: PREVIOUS # Indicates what button is doing. Available values depends on panel + indexing: true # Parameter for button. + actions: # List of actions that button can do. Available values depends on button + previous: + click-type: UNKNOWN # UNKNOWN means that any click type is respected. + tooltip: panels.tips.click-to-previous # Tooltips are always generated an empty line bellow description/title. Not required. + 2: language_button + 3: language_button + 4: language_button + 5: language_button + 6: language_button + 7: language_button + 8: language_button + 9: + icon: TIPPED_ARROW:JUMP::::1 + title: panels.buttons.next.name + description: panels.buttons.next.description + data: + type: NEXT + indexing: true + actions: + next: + click-type: UNKNOWN + tooltip: panels.tips.click-to-next + 4: + 2: language_button + 3: language_button + 4: language_button + 5: language_button + 6: language_button + 7: language_button + 8: language_button + reusable: # List of reoccurring buttons in the panels. + language_button: # The ID of the button + # icon: GRASS_BLOCK + title: panels.language.buttons.language.name + description: panels.language.buttons.language.description + data: + type: LOCALE + # lang_id: default # Specifying lang_id will force to show the requested locale if it is available. + actions: + select: + click-type: UNKNOWN + tooltip: panels.tips.click-to-choose \ No newline at end of file diff --git a/src/test/java/world/bentobox/bentobox/api/commands/island/IslandCreateCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/island/IslandCreateCommandTest.java index 39eba339a..5d35d71a7 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/island/IslandCreateCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/island/IslandCreateCommandTest.java @@ -54,7 +54,7 @@ import world.bentobox.bentobox.managers.PlayersManager; import world.bentobox.bentobox.managers.island.NewIsland; import world.bentobox.bentobox.managers.island.NewIsland.Builder; -import world.bentobox.bentobox.panels.IslandCreationPanel; +import world.bentobox.bentobox.panels.customizable.IslandCreationPanel; /** * @author tastybento diff --git a/src/test/java/world/bentobox/bentobox/api/localization/BentoBoxLocaleTest.java b/src/test/java/world/bentobox/bentobox/api/localization/BentoBoxLocaleTest.java index 1beefa775..a195ebecd 100644 --- a/src/test/java/world/bentobox/bentobox/api/localization/BentoBoxLocaleTest.java +++ b/src/test/java/world/bentobox/bentobox/api/localization/BentoBoxLocaleTest.java @@ -50,6 +50,7 @@ public void setUp() throws Exception { ItemFactory itemFactory = mock(ItemFactory.class); bannerMeta = mock(BannerMeta.class); when(itemFactory.getItemMeta(any())).thenReturn(bannerMeta); + when(itemFactory.createItemStack(any())).thenThrow(IllegalArgumentException.class); when(Bukkit.getItemFactory()).thenReturn(itemFactory); Locale locale = Locale.US; diff --git a/src/test/java/world/bentobox/bentobox/panels/IslandCreationPanelTest.java b/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java similarity index 72% rename from src/test/java/world/bentobox/bentobox/panels/IslandCreationPanelTest.java rename to src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java index 271bb0b37..19f5ca654 100644 --- a/src/test/java/world/bentobox/bentobox/panels/IslandCreationPanelTest.java +++ b/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java @@ -1,4 +1,4 @@ -package world.bentobox.bentobox.panels; +package world.bentobox.bentobox.panels.customizable; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -7,18 +7,18 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.scheduler.BukkitScheduler; import org.junit.After; import org.junit.Before; @@ -43,7 +43,6 @@ import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.PlayersManager; -import world.bentobox.bentobox.managers.island.NewIsland.Builder; /** * @author tastybento @@ -60,8 +59,6 @@ public class IslandCreationPanelTest { @Mock private IslandWorldManager iwm; @Mock - private Builder builder; - @Mock private BentoBox plugin; @Mock private Settings settings; @@ -78,6 +75,11 @@ public class IslandCreationPanelTest { @Mock private BlueprintBundle bb3; + /** + * Location of the resources folder + */ + private final Path resourcePath = Paths.get("src","test","resources"); + /** */ @Before @@ -100,14 +102,25 @@ public void setUp() throws Exception { when(user.getUniqueId()).thenReturn(uuid); when(user.getPlayer()).thenReturn(player); when(user.hasPermission(anyString())).thenReturn(true); - when(user.getTranslation(any())) - .thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); User.setPlugin(plugin); // Set up user already User.getInstance(player); // Addon GameModeAddon addon = mock(GameModeAddon.class); + when(addon.getDataFolder()).thenReturn(resourcePath.toFile()); + + when(user.getTranslation(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + when(user.getTranslation(any(World.class), any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + when(user.getTranslation(any(String.class), any())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + when(user.getTranslationOrNothing(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + + when(user.getTranslation(any(World.class), eq("panels.island_creation.buttons.bundle.name"), any())). + thenAnswer((Answer) invocation -> invocation.getArgument(3, String.class)); + when(user.getTranslation(any(World.class), eq("panels.island_creation.buttons.bundle.description"), any())). + thenAnswer((Answer) invocation -> invocation.getArgument(3, String.class)); + when(plugin.getDescription()).thenAnswer((Answer) invocation -> + new PluginDescriptionFile("BentoBox", "1.0", "world.bentobox.bentobox")); // Parent command has no aliases when(ic.getSubCommandAliases()).thenReturn(new HashMap<>()); @@ -117,6 +130,8 @@ public void setUp() throws Exception { when(ic.getUsage()).thenReturn(""); when(ic.getSubCommand(Mockito.anyString())).thenReturn(Optional.empty()); when(ic.getAddon()).thenReturn(addon); + World world = mock(World.class); + when(ic.getWorld()).thenReturn(world); // No island for player to begin with (set it later in the tests) when(im.hasIsland(any(), eq(uuid))).thenReturn(false); @@ -184,42 +199,41 @@ public void tearDown() { /** * Test method for - * {@link world.bentobox.bentobox.panels.IslandCreationPanel#openPanel(world.bentobox.bentobox.api.commands.CompositeCommand, world.bentobox.bentobox.api.user.User, java.lang.String)}. + * {@link world.bentobox.bentobox.panels.customizable.IslandCreationPanel#openPanel(world.bentobox.bentobox.api.commands.CompositeCommand, world.bentobox.bentobox.api.user.User, java.lang.String)}. */ @Test public void testOpenPanel() { IslandCreationPanel.openPanel(ic, user, ""); - // Check for slot being set to 0 - verify(bb2).setSlot(eq(0)); - verify(bb3).setSlot(eq(0)); + // Set correctly - verify(inv).setItem(eq(5), any()); + verify(inv).setItem(eq(0), any()); + verify(inv).setItem(eq(1), any()); verify(meta).setDisplayName(eq("test")); verify(meta).setLocalizedName(eq("test")); - verify(meta).setLore(eq(Collections.singletonList("A description"))); + verify(meta).setLore(eq(List.of("A description", "", "panels.tips.click-to-choose"))); } /** - * Test method for {@link world.bentobox.bentobox.panels.IslandCreationPanel#openPanel(world.bentobox.bentobox.api.commands.CompositeCommand, world.bentobox.bentobox.api.user.User, java.lang.String)}. + * Test method for {@link world.bentobox.bentobox.panels.customizable.IslandCreationPanel#openPanel(world.bentobox.bentobox.api.commands.CompositeCommand, world.bentobox.bentobox.api.user.User, java.lang.String)}. */ @Test public void testOpenPanelSameSlot() { when(bb2.getSlot()).thenReturn(5); when(bb3.getSlot()).thenReturn(5); IslandCreationPanel.openPanel(ic, user, ""); - verify(inv).setItem(eq(5), any()); + verify(inv).setItem(eq(0), any()); + verify(inv).setItem(eq(1), any()); verify(meta).setDisplayName(eq("test")); verify(meta).setLocalizedName(eq("test")); - verify(meta).setLore(eq(Collections.singletonList("A description"))); + verify(meta).setLore(eq(List.of("A description", "", "panels.tips.click-to-choose"))); verify(inv).setItem(eq(0), any()); verify(meta).setDisplayName(eq("test2")); verify(meta).setLocalizedName(eq("test2")); - verify(meta).setLore(eq(Collections.singletonList("A description 2"))); + verify(meta).setLore(eq(List.of("A description 2", "", "panels.tips.click-to-choose"))); verify(inv).setItem(eq(1), any()); verify(meta).setDisplayName(eq("test3")); verify(meta).setLocalizedName(eq("test3")); - verify(meta).setLore(eq(Collections.singletonList("A description 3"))); - + verify(meta).setLore(eq(List.of("A description 3", "", "panels.tips.click-to-choose"))); } } diff --git a/src/test/java/world/bentobox/bentobox/panels/LanguagePanelTest.java b/src/test/java/world/bentobox/bentobox/panels/customizable/LanguagePanelTest.java similarity index 61% rename from src/test/java/world/bentobox/bentobox/panels/LanguagePanelTest.java rename to src/test/java/world/bentobox/bentobox/panels/customizable/LanguagePanelTest.java index cce75f540..aaed88101 100644 --- a/src/test/java/world/bentobox/bentobox/panels/LanguagePanelTest.java +++ b/src/test/java/world/bentobox/bentobox/panels/customizable/LanguagePanelTest.java @@ -1,28 +1,15 @@ -package world.bentobox.bentobox.panels; +package world.bentobox.bentobox.panels.customizable; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.awt.Panel; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.PluginDescriptionFile; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -36,14 +23,23 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; +import java.awt.Panel; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; -import net.md_5.bungee.api.ChatColor; import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.localization.BentoBoxLocale; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.managers.LocalesManager; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + /** * @author tastybento * @@ -61,20 +57,24 @@ public class LanguagePanelTest { private ArrayList localeList; - @Mock - private PanelBuilder pb; - @Mock - private Panel panel; @Mock private Inventory inv; @Mock private ItemMeta meta; + @Mock + private CompositeCommand command; + @Captor private ArgumentCaptor argument; private Map map; + /** + * Location of the resources folder + */ + private final Path resourcePath = Paths.get("src","test","resources"); + /** */ @Before @@ -90,7 +90,24 @@ public void setUp() throws Exception { when(user.getPlayer()).thenReturn(player); when(user.hasPermission(anyString())).thenReturn(true); when(user.getTranslation(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + when(user.getTranslation(any(World.class), any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + when(user.getTranslation(any(String.class), any())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + when(user.getTranslationOrNothing(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); when(user.getLocale()).thenReturn(Locale.ENGLISH); + + when(user.getTranslation(any(World.class), eq("panels.language.buttons.language.name"), any())). + thenAnswer((Answer) invocation -> invocation.getArgument(3, String.class)); + + GameModeAddon addon = mock(GameModeAddon.class); + when(command.getAddon()).thenReturn(addon); + when(addon.getDataFolder()).thenReturn(resourcePath.toFile()); + + World world = mock(World.class); + when(command.getWorld()).thenReturn(world); + + when(plugin.getDescription()).thenAnswer((Answer) invocation -> + new PluginDescriptionFile("BentoBox", "1.0", "world.bentobox.bentobox")); + User.setPlugin(plugin); // Set up user already User.getInstance(player); @@ -123,17 +140,17 @@ public void tearDown() { } /** - * Test method for {@link world.bentobox.bentobox.panels.LanguagePanel#openPanel(world.bentobox.bentobox.api.user.User)}. + * Test method for {@link world.bentobox.bentobox.panels.customizable.LanguagePanel#openPanel(world.bentobox.bentobox.api.commands.CompositeCommand,world.bentobox.bentobox.api.user.User)}. */ @Test public void testOpenPanelNoLocales() { - LanguagePanel.openPanel(user); + LanguagePanel.openPanel(command, user); verify(plugin).getLocalesManager(); verify(lm).getAvailableLocales(eq(true)); } /** - * Test method for {@link world.bentobox.bentobox.panels.LanguagePanel#openPanel(world.bentobox.bentobox.api.user.User)}. + * Test method for {@link world.bentobox.bentobox.panels.customizable.LanguagePanel#openPanel(world.bentobox.bentobox.api.commands.CompositeCommand,world.bentobox.bentobox.api.user.User)}. */ @Test public void testOpenPanelLocalesNullBanner() { @@ -146,29 +163,30 @@ public void testOpenPanelLocalesNullBanner() { map.put(Locale.CHINA, bbl); map.put(Locale.ENGLISH, bbl); - LanguagePanel.openPanel(user); + LanguagePanel.openPanel(command, user); verify(lm, times(3)).getLanguages(); verify(bbl, times(3)).getBanner(); - verify(user).getTranslation("language.panel-title"); + verify(user).getTranslation("panels.language.title"); // Other langs - verify(user, times(2)).getTranslation(eq("language.description.click-to-select")); - verify(user, times(3)).getTranslation(eq("language.description.authors")); - // Selected language - verify(user, Mockito.atMostOnce()).getTranslation(eq("language.description.selected")); + verify(user, times(3)).getTranslation(eq("panels.language.buttons.language.authors")); + verify(user, times(1)).getTranslation(eq("panels.language.buttons.language.selected")); + verify(user, times(3)).getTranslationOrNothing(eq("panels.language.buttons.language.description"), any()); + verify(user, times(2)).getTranslation(any(World.class), eq("panels.tips.click-to-choose")); verify(inv).setItem(eq(0), argument.capture()); assertEquals(Material.WHITE_BANNER, argument.getValue().getType()); assertEquals(1, argument.getValue().getAmount()); assertEquals(meta, argument.getValue().getItemMeta()); - verify(meta).setDisplayName(eq(ChatColor.WHITE + "Chinese (China)")); - verify(meta).setDisplayName(eq(ChatColor.WHITE + "English (Canada)")); + + verify(meta).setDisplayName(eq("Chinese (China)")); + verify(meta).setDisplayName(eq("English (Canada)")); verify(inv).setItem(eq(1), any()); verify(inv).setItem(eq(2), any()); verify(inv, Mockito.never()).setItem(eq(3), any()); } /** - * Test method for {@link world.bentobox.bentobox.panels.LanguagePanel#openPanel(world.bentobox.bentobox.api.user.User)}. + * Test method for {@link world.bentobox.bentobox.panels.customizable.LanguagePanel#openPanel(world.bentobox.bentobox.api.commands.CompositeCommand,world.bentobox.bentobox.api.user.User)}. */ @Test public void testOpenPanelLocalesNotNullBanner() { @@ -178,7 +196,7 @@ public void testOpenPanelLocalesNotNullBanner() { map.put(Locale.CANADA, bbl); when(bbl.getBanner()).thenReturn(new ItemStack(Material.CYAN_BANNER)); - LanguagePanel.openPanel(user); + LanguagePanel.openPanel(command, user); verify(inv).setItem(eq(0), argument.capture()); assertEquals(Material.CYAN_BANNER, argument.getValue().getType()); } diff --git a/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java b/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java index 739587827..b14cc32ee 100644 --- a/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java +++ b/src/test/java/world/bentobox/bentobox/util/ItemParserTest.java @@ -21,8 +21,10 @@ import org.bukkit.inventory.meta.BannerMeta; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.PotionMeta; +import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.potion.PotionData; import org.bukkit.potion.PotionType; +import org.bukkit.profile.PlayerProfile; import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -50,6 +52,9 @@ public class ItemParserTest { private ItemMeta itemMeta; @Mock private ItemFactory itemFactory; + @Mock + private SkullMeta skullMeta; + private ItemStack defaultItem; @SuppressWarnings("deprecation") @@ -61,6 +66,8 @@ public void setUp() throws Exception { PowerMockito.mockStatic(Bukkit.class); when(Bukkit.getItemFactory()).thenReturn(itemFactory); + // Do not test Bukkit createItemStack method output as I assume Bukkit has their tests covered. + when(itemFactory.createItemStack(any())).thenThrow(IllegalArgumentException.class); /* when(itemFactory.getItemMeta(Mockito.eq(Material.POTION))).thenReturn(potionMeta); when(itemFactory.getItemMeta(Mockito.eq(Material.SPLASH_POTION))).thenReturn(potionMeta); @@ -106,148 +113,66 @@ public void testParseNoColons() { assertEquals(defaultItem, ItemParser.parse("NOCOLONS", defaultItem)); } - /* - * # Format POTION:NAME::::QTY - # LEVEL, EXTENDED, SPLASH, LINGER are optional. - # LEVEL is a number, 1 or 2 - # LINGER is for V1.9 servers and later - # Examples: - # POTION:STRENGTH:1:EXTENDED:SPLASH:1 - # POTION:INSTANT_DAMAGE:2::LINGER:2 - # POTION:JUMP:2:NOTEXTENDED:NOSPLASH:1 - # POTION:WEAKNESS::::1 - any weakness potion - */ - - @Ignore("Extended potions now have their own names and are not extended like this") - @Test - public void testParsePotionStrengthExtended() { - when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("POTION:STRENGTH:1:EXTENDED::5"); - assertNotNull(result); - assertEquals(Material.POTION, result.getType()); - PotionType type = PotionType.STRENGTH; - boolean isExtended = true; - boolean isUpgraded = false; - PotionData data = new PotionData(type, isExtended, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - assertEquals(5, result.getAmount()); - } - - @SuppressWarnings("deprecation") @Test - public void testParsePotionStrengthNotExtended() { + public void testParsePotion() { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("POTION:STRENGTH:1:::4"); - assertNotNull(result); - assertEquals(Material.POTION, result.getType()); - PotionType type = PotionType.STRENGTH; - boolean isExtended = false; - boolean isUpgraded = false; - PotionData data = new PotionData(type, isExtended, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - assertEquals(4, result.getAmount()); + for (PotionType type : PotionType.values()) { + ItemStack itemStack = ItemParser.parse("POTION:" + type.name() + ":1"); + assertEquals(itemStack.getType(), Material.POTION); + // Not sure how this can be tested. + // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + assertEquals(1, itemStack.getAmount()); + } } - @SuppressWarnings("deprecation") @Test - public void testParsePotionStrengthNotExtendedSplash() { + public void testParseSplashPotion() { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("POTION:STRENGTH:1::SPLASH:3"); - assertNotNull(result); - assertEquals(Material.SPLASH_POTION, result.getType()); - PotionType type = PotionType.STRENGTH; - boolean isExtended = false; - boolean isUpgraded = false; - PotionData data = new PotionData(type, isExtended, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - assertEquals(3, result.getAmount()); + for (PotionType type : PotionType.values()) { + ItemStack itemStack = ItemParser.parse("SPLASH_POTION:" + type.name() + ":1"); + assertEquals(itemStack.getType(), Material.SPLASH_POTION); + // Not sure how this can be tested. + // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + assertEquals(1, itemStack.getAmount()); + } } - @SuppressWarnings("deprecation") - @Ignore("Potions are no longer upgraded like this") @Test - public void testParsePotionStrengthNotExtendedUpgradedSplash() { + public void testParseLingeringPotion() { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("POTION:STRENGTH:2::SPLASH:3"); - assertNotNull(result); - assertEquals(Material.SPLASH_POTION, result.getType()); - PotionType type = PotionType.STRENGTH; - boolean isExtended = false; - boolean isUpgraded = true; - PotionData data = new PotionData(type, isExtended, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - assertEquals(3, result.getAmount()); - } - - enum extend { - NOT_EXTENDED, - EXTENDED - } - - enum type { - NO_SPLASH, - SPLASH, - LINGER + for (PotionType type : PotionType.values()) { + ItemStack itemStack = ItemParser.parse("LINGERING_POTION:" + type.name() + ":1"); + assertEquals(itemStack.getType(), Material.LINGERING_POTION); + // Not sure how this can be tested. + // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + assertEquals(1, itemStack.getAmount()); + } } - List notExtendable = Arrays.asList( - PotionType.UNCRAFTABLE, - PotionType.WATER, - PotionType.MUNDANE, - PotionType.THICK, - PotionType.AWKWARD, - PotionType.INSTANT_HEAL, - PotionType.INSTANT_DAMAGE, - PotionType.LUCK, - PotionType.NIGHT_VISION - ); - - @SuppressWarnings("deprecation") @Test - public void testParsePotion() { + public void testParseTippedArrow() { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); for (PotionType type : PotionType.values()) { - if (type.name().contains("LONG") || type.name().contains("STRONG")) { - continue; - } - for (ItemParserTest.type t: ItemParserTest.type.values()) { - for (int up = 1; up < 2; up++) { - boolean isUpgraded = up > 1; - String req = "POTION:" + type.name() + ":" + up + "::"+ t.name() + ":3"; - ItemStack result = ItemParser.parse(req); - assertNotNull(result); - switch (t) { - case LINGER: - assertEquals(Material.LINGERING_POTION, result.getType()); - PotionData data = new PotionData(type, false, isUpgraded); - verify(potionMeta, times(3)).setBasePotionData(Mockito.eq(data)); - break; - case NO_SPLASH: - assertEquals(Material.POTION, result.getType()); - data = new PotionData(type, false, isUpgraded); - verify(potionMeta).setBasePotionData(Mockito.eq(data)); - break; - case SPLASH: - assertEquals(Material.SPLASH_POTION, result.getType()); - data = new PotionData(type, false, isUpgraded); - verify(potionMeta, times(2)).setBasePotionData(Mockito.eq(data)); - break; - default: - break; - } - - assertEquals(3, result.getAmount()); - } - } + ItemStack itemStack = ItemParser.parse("TIPPED_ARROW:" + type.name() + ":1"); + assertEquals(itemStack.getType(), Material.TIPPED_ARROW); + // Not sure how this can be tested. + // assertEquals(type, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + assertEquals(1, itemStack.getAmount()); } } @Test - public void testParseTippedArrow() { + public void testParseBadPotion() + { when(itemFactory.getItemMeta(any())).thenReturn(potionMeta); - ItemStack result = ItemParser.parse("TIPPED_ARROW:WEAKNESS::::1"); - assertNotNull(result); - assertEquals(Material.TIPPED_ARROW, result.getType()); + ItemStack itemStack = ItemParser.parse("POTION::5"); + assertEquals(5, itemStack.getAmount()); + // Not sure how this can be tested + // assertEquals(PotionType.WATER, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); + itemStack = ItemParser.parse("POTION:NO_POTION:1"); + assertEquals(1, itemStack.getAmount()); + // Not sure how this can be tested + // assertEquals(PotionType.WATER, ((PotionMeta) itemStack.getItemMeta()).getBasePotionType()); } @@ -318,7 +243,6 @@ public void testParseBadThreeItem() { assertEquals(defaultItem, ItemParser.parse("WOODEN_SWORD:4:AA", defaultItem)); } - @Ignore("This doesn't work for some reason") @Test public void parseCustomModelData() { ItemStack result = ItemParser.parse("WOODEN_SWORD:CMD-23151212:2"); @@ -327,4 +251,21 @@ public void parseCustomModelData() { assertEquals(2, result.getAmount()); assertNull(ItemParser.parse("WOODEN_SWORD:CMD-23151212:2:CMD-23151212")); } + + @Test + public void parsePlayerHead() { + when(itemFactory.getItemMeta(any())).thenReturn(skullMeta); + ItemStack result = ItemParser.parse("PLAYER_HEAD:2"); + assertNotNull(result); + assertEquals(Material.PLAYER_HEAD, result.getType()); + assertEquals(2, result.getAmount()); + + result = ItemParser.parse("PLAYER_HEAD:BONNe1704"); + assertNotNull(result); + assertEquals(Material.PLAYER_HEAD, result.getType()); + assertEquals(1, result.getAmount()); + + // I do not know if it is possible to test metadata, as skull meta is not applied to player heads in testing. + //assertEquals("BONNe1704", ((SkullMeta) result.getItemMeta()).getOwnerProfile().getName()); + } } diff --git a/src/test/resources/panels/island_creation_panel.yml b/src/test/resources/panels/island_creation_panel.yml new file mode 100644 index 000000000..a0e55f5bd --- /dev/null +++ b/src/test/resources/panels/island_creation_panel.yml @@ -0,0 +1,29 @@ +# This is default island creation panel. It is used in all situations when gamemode addon does not have specified their +# of panel. +island_creation_panel: + title: panels.island_creation.title # The title of panel or link to the localization location. + type: INVENTORY # The type of inventory: INVENTORY, DROPPER, HOPPER + force-shown: [] # Allow to specify (1-6, 1-3, 1) which rows must be showed regardless of empty elements. + content: # Allow to define buttons in your panel. + 1: + 1: blueprint_bundle_button # String values are expected to be `reusables` that are defined at the end of this file. + 2: blueprint_bundle_button + 3: blueprint_bundle_button + 4: blueprint_bundle_button + 5: blueprint_bundle_button + 6: blueprint_bundle_button + 7: blueprint_bundle_button + 8: blueprint_bundle_button + 9: blueprint_bundle_button + reusable: # List of reoccurring buttons in the panels. + blueprint_bundle_button: # The ID of the button + # icon: GRASS_BLOCK + title: panels.island_creation.buttons.bundle.name + description: panels.island_creation.buttons.bundle.description + data: + type: BUNDLE + # unique_id: default # Specifying unique_id will force to show the requested bundle if it is available. + actions: + select: + click-type: UNKNOWN + tooltip: panels.tips.click-to-choose \ No newline at end of file diff --git a/src/test/resources/panels/language_panel.yml b/src/test/resources/panels/language_panel.yml new file mode 100644 index 000000000..1f0df0f35 --- /dev/null +++ b/src/test/resources/panels/language_panel.yml @@ -0,0 +1,29 @@ +# This is default language selection panel. It is used in all situations when gamemode addon does not have specified their +# of panel. +language_panel: + title: panels.language.title # The title of panel or link to the localization location. + type: INVENTORY # The type of inventory: INVENTORY, DROPPER, HOPPER + force-shown: [] # Allow to specify (1-6, 1-3, 1) which rows must be showed regardless of empty elements. + content: # Allow to define buttons in your panel. + 1: + 1: language_button # String values are expected to be `reusables` that are defined at the end of this file. + 2: language_button + 3: language_button + 4: language_button + 5: language_button + 6: language_button + 7: language_button + 8: language_button + 9: language_button + reusable: # List of reoccurring buttons in the panels. + language_button: # The ID of the button + # icon: GRASS_BLOCK + title: panels.language.buttons.language.name + description: panels.language.buttons.language.description + data: + type: LOCALE + # lang_id: default # Specifying lang_id will force to show the requested locale if it is available. + actions: + select: + click-type: UNKNOWN + tooltip: panels.tips.click-to-choose \ No newline at end of file