// // Created by BONNe // Copyright - 2023 // package world.bentobox.bentobox.panels.customizable; import org.bukkit.World; 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: 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"; /** * Button reference */ private static final String BUNDLE_BUTTON_REF = "panels.island_creation.buttons.bundle."; // --------------------------------------------------------------------- // 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; /** * The world that this command applies to */ private final World world; /** * true if this panel has been called by a reset command. Changes how the count of used islands is done. */ private final boolean reset; // --------------------------------------------------------------------- // 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 * @param reset */ private IslandCreationPanel(@NonNull CompositeCommand command, @NonNull User user, @NonNull String label, boolean reset) { this.plugin = BentoBox.getInstance(); this.user = user; this.mainLabel = label; this.world = command.getWorld(); this.reset = reset; 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 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 blueprints. 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; } // 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(BUNDLE_BUTTON_REF + "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(BUNDLE_BUTTON_REF + "description", TextVariables.DESCRIPTION, String.join("\n", bundle.getDescription()))); } boolean usedUp = false; if (plugin.getSettings().getIslandNumber() > 1) { // Show how many times this bundle can be used int maxTimes = bundle.getTimes(); if (maxTimes > 0) { long uses = plugin.getIslands().getIslands(world, user).stream() .filter(is -> is.getMetaData("bundle") .map(mdv -> bundle.getDisplayName().equalsIgnoreCase(mdv.asString()) && !(reset && is.isPrimary())) // If this is a reset, then ignore the use of the island being reset .orElse(false)) .count(); builder.description(this.user.getTranslation(BUNDLE_BUTTON_REF + "uses", TextVariables.NUMBER, String.valueOf(uses), "[max]", String.valueOf(maxTimes))); if (uses >= maxTimes) { usedUp = true; } } else { builder.description(this.user.getTranslation(BUNDLE_BUTTON_REF + "unlimited")); } } if (usedUp) { if (plugin.getSettings().isHideUsedBlueprints()) { // Do not show used up blueprints return null; } } else { 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 * @param reset true if this is an island reset */ public static void openPanel(@NonNull CompositeCommand command, @NonNull User user, @NonNull String label, boolean reset) { new IslandCreationPanel(command, user, label, reset).build(); } }