diff --git a/src/main/java/world/bentobox/bentobox/BentoBox.java b/src/main/java/world/bentobox/bentobox/BentoBox.java index 650d5e4a5..b0ad73046 100644 --- a/src/main/java/world/bentobox/bentobox/BentoBox.java +++ b/src/main/java/world/bentobox/bentobox/BentoBox.java @@ -1,7 +1,5 @@ package world.bentobox.bentobox; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; import java.util.Optional; @@ -466,16 +464,6 @@ public class BentoBox extends JavaPlugin implements Listener { return false; } - log("Saving default panels..."); - if (!Files.exists(Path.of(this.getDataFolder().getPath(), "panels", "island_creation_panel.yml"))) { - log("Saving default island_creation_panel..."); - this.saveResource("panels/island_creation_panel.yml", false); - } - - if (!Files.exists(Path.of(this.getDataFolder().getPath(), "panels", "language_panel.yml"))) { - log("Saving default language_panel..."); - this.saveResource("panels/language_panel.yml", false); - } return true; } diff --git a/src/main/java/world/bentobox/bentobox/api/commands/CompositeCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/CompositeCommand.java index 4d0a8146e..c75d67c80 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/CompositeCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/CompositeCommand.java @@ -346,7 +346,7 @@ public abstract class CompositeCommand extends Command implements PluginIdentifi * * @return IslandsManager */ - protected IslandsManager getIslands() { + public IslandsManager getIslands() { return plugin.getIslands(); } diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java index cfe3f6c8e..0eeccbcbe 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java @@ -61,9 +61,9 @@ public class IslandGoCommand extends DelayedTeleportCommand { @Override public boolean execute(User user, String label, List args) { + Map names = getNameIslandMap(user); // Check if the home is known if (!args.isEmpty()) { - Map names = getNameIslandMap(user); final String name = String.join(" ", args); if (!names.containsKey(name)) { // Failed home name check @@ -113,7 +113,11 @@ public class IslandGoCommand extends DelayedTeleportCommand { } - private record IslandInfo(Island island, boolean islandName) {} + /** + * Record of islands and the name to type + */ + private record IslandInfo(Island island, boolean islandName) { + } private Map getNameIslandMap(User user) { Map islandMap = new HashMap<>(); @@ -129,7 +133,8 @@ public class IslandGoCommand extends DelayedTeleportCommand { islandMap.put(text, new IslandInfo(island, true)); } // Add homes. Homes do not need an island specified - island.getHomes().keySet().forEach(n -> islandMap.put(n, new IslandInfo(island, false))); + island.getHomes().keySet().stream().filter(n -> !n.isBlank()) + .forEach(n -> islandMap.put(n, new IslandInfo(island, false))); } return islandMap; diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandHomesCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandHomesCommand.java index 33b077c5f..b9298664a 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandHomesCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandHomesCommand.java @@ -1,19 +1,12 @@ package world.bentobox.bentobox.api.commands.island; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; import world.bentobox.bentobox.api.commands.CompositeCommand; -import world.bentobox.bentobox.api.commands.ConfirmableCommand; -import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.util.Util; +import world.bentobox.bentobox.panels.customizable.IslandHomesPanel; -public class IslandHomesCommand extends ConfirmableCommand { - - private List islands; +public class IslandHomesCommand extends CompositeCommand { public IslandHomesCommand(CompositeCommand islandCommand) { super(islandCommand, "homes"); @@ -28,9 +21,8 @@ public class IslandHomesCommand extends ConfirmableCommand { @Override public boolean canExecute(User user, String label, List args) { - islands = getIslands().getIslands(getWorld(), user); // Check island - if (islands.isEmpty()) { + if (getIslands().getIslands(getWorld(), user).isEmpty()) { user.sendMessage("general.errors.no-island"); return false; } @@ -39,22 +31,8 @@ public class IslandHomesCommand extends ConfirmableCommand { @Override public boolean execute(User user, String label, List args) { - user.sendMessage("commands.island.sethome.homes-are"); - islands.forEach(island -> - island.getHomes().keySet().stream().filter(s -> !s.isEmpty()) - .forEach(s -> user.sendMessage("commands.island.sethome.home-list-syntax", TextVariables.NAME, s))); + IslandHomesPanel.openPanel(this, user); return true; } - @Override - public Optional> tabComplete(User user, String alias, List args) { - String lastArg = !args.isEmpty() ? args.get(args.size()-1) : ""; - List result = new ArrayList<>(); - for (Island island : getIslands().getIslands(getWorld(), user.getUniqueId())) { - result.addAll(island.getHomes().keySet()); - } - return Optional.of(Util.tabLimit(result, lastArg)); - - } - } diff --git a/src/main/java/world/bentobox/bentobox/api/panels/reader/TemplateReader.java b/src/main/java/world/bentobox/bentobox/api/panels/reader/TemplateReader.java index 524f0f260..ce837602d 100644 --- a/src/main/java/world/bentobox/bentobox/api/panels/reader/TemplateReader.java +++ b/src/main/java/world/bentobox/bentobox/api/panels/reader/TemplateReader.java @@ -90,15 +90,29 @@ public class TemplateReader } File file = new File(panelLocation, templateName.endsWith(YML) ? templateName : templateName + YML); - + String absolutePath = file.getAbsolutePath(); if (!file.exists()) { - BentoBox.getInstance().logError(file.getAbsolutePath() + " does not exist for panel template"); - // Return as file does not exist. - return null; + // Try to get it from the JAR + + String keyword = "panels/"; + + // Find the index of the keyword "panels/" + int index = absolutePath.indexOf(keyword); + + // If the keyword is found, extract the substring starting from that index + if (index != -1) { + BentoBox.getInstance().saveResource(absolutePath.substring(index), false); + file = new File(panelLocation, templateName.endsWith(YML) ? templateName : templateName + YML); + } else { + BentoBox.getInstance().logError(file.getAbsolutePath() + " does not exist for panel template"); + // Return as file does not exist. + return null; + } + } - final String panelKey = file.getAbsolutePath() + ":" + panelName; + final String panelKey = absolutePath + ":" + panelName; // Check if panel is already crafted. if (TemplateReader.loadedPanels.containsKey(panelKey)) diff --git a/src/main/java/world/bentobox/bentobox/panels/customizable/IslandHomesPanel.java b/src/main/java/world/bentobox/bentobox/panels/customizable/IslandHomesPanel.java new file mode 100644 index 000000000..c760a21d8 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/panels/customizable/IslandHomesPanel.java @@ -0,0 +1,419 @@ +package world.bentobox.bentobox.panels.customizable; + + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.bukkit.Material; +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 world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.commands.island.IslandGoCommand; +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.database.objects.Island; + + +/** + * Panel for island homes command + */ +public class IslandHomesPanel extends AbstractPanel +{ + + private static final String ISLAND = "ISLAND"; + + + /** + * This variable stores filtered elements. + */ + private final Map islandMap; + private final Map order = new HashMap<>(); + + + /** + * The world that this command applies to + */ + private final World world; + + private final IslandGoCommand goCommand; + + // --------------------------------------------------------------------- + // Section: Constructor + // --------------------------------------------------------------------- + + + /** + * This is internal constructor. It is used internally in current class to avoid creating objects everywhere. + * + * @param command CompositeCommand + * @param user User who opens panel + * @param islandMap map of island names and IslandInfo + */ + private IslandHomesPanel(@NonNull CompositeCommand command, @NonNull User user) + { + super(command, user); + this.world = command.getWorld(); + this.islandMap = this.getNameIslandMap(user); + int index = 0; + for (String name : islandMap.keySet()) { + order.put(index++, name); + } + goCommand = (IslandGoCommand) command.getParent().getSubCommand("go").orElse(null); + } + + + // --------------------------------------------------------------------- + // Section: Methods + // --------------------------------------------------------------------- + + + /** + * Build method manages current panel opening. It uses BentoBox PanelAPI that is easy to use and users can get nice + * panels. + */ + @Override + protected void build() + { + // Do not open gui if there are no islands + if (this.islandMap.isEmpty()) + { + user.sendMessage("general.errors.no-island"); + return; + } + + // Start building panel. + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + + // Set main template. + if (this.doesCustomPanelExists(this.command.getAddon(), "island_homes_panel")) + { + // Addon has its own island homes panel. Use it. + panelBuilder.template("island_homes_panel", new File(this.command.getAddon().getDataFolder(), "panels")); + } + else + { + // Use default island creation panel. + panelBuilder.template("island_homes_panel", new File(this.plugin.getDataFolder(), "panels")); + } + + panelBuilder.user(this.user); + panelBuilder.world(world); + + // Register button builders + panelBuilder.registerTypeBuilder(ISLAND, this::createIslandButton); + + // Register next and previous builders + panelBuilder.registerTypeBuilder(NEXT, this::createNextButton); + panelBuilder.registerTypeBuilder(PREVIOUS, this::createPreviousButton); + + // Register unknown type builder. + panelBuilder.build(); + } + + // --------------------------------------------------------------------- + // Section: Buttons + // --------------------------------------------------------------------- + + + /** + * Create next button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + @Override + @Nullable + protected PanelItem createNextButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + int size = this.islandMap.size(); + + if (size <= slot.amountMap().getOrDefault(ISLAND, 1) + || 1.0 * size / slot.amountMap().getOrDefault(ISLAND, 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.command.getWorld(), template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.command.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.command.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 + @Override + protected 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.command.getWorld(), template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.command.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.command.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 island button. + * + * @return PanelItem that represents island button. + */ + @Nullable + private PanelItem createIslandButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.islandMap.isEmpty()) + { + // Does not contain any islands. + return null; + } + + int index = this.pageIndex * slot.amountMap().getOrDefault(ISLAND, 1) + slot.slot(); + if (index >= this.islandMap.size()) + { + // Out of index. + return null; + } + return this.createIslandButtonDetail(template, slot); + } + + + /** + * This method creates bundle button. + * + * @return PanelItem that allows to select bundle button + */ + private PanelItem createIslandButtonDetail(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + // Get settings for island. + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + builder.icon(template.icon().clone()); + } + else + { + builder.icon(Material.GRASS_BLOCK); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.command.getWorld(), template.title(), + TextVariables.NAME, order.get(slot.slot()))); + } + else + { + builder.name(this.user.getTranslation("panels.island_homes.buttons.name", TextVariables.NAME, + order.get(slot.slot()))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> { + template.actions().forEach(action -> { + if (goCommand != null) { + String name = order.get(slot.slot()); + user.closeInventory(); + if (goCommand.canExecute(user, "", List.of(name))) { + goCommand.execute(user, "", List.of(name)); + } + } + }); + + // Always return true. + return true; + }); + + return builder.build(); + } + + /** + * Record of islands and the name to type + */ + private record IslandInfo(Island island, boolean islandName) { + } + + /** + * This is duplicate code from the Go command. + * @param user user + * @return name and island info + */ + private Map getNameIslandMap(User user) { + Map islandMap = new HashMap<>(); + int index = 0; + for (Island island : command.getIslands().getIslands(command.getWorld(), user.getUniqueId())) { + index++; + if (island.getName() != null && !island.getName().isBlank()) { + // Name has been set + islandMap.put(island.getName(), new IslandInfo(island, true)); + } else { + // Name has not been set + String text = user.getTranslation("protection.flags.ENTER_EXIT_MESSAGES.island", TextVariables.NAME, + user.getName(), TextVariables.DISPLAY_NAME, user.getDisplayName()) + " " + index; + islandMap.put(text, new IslandInfo(island, true)); + } + // Add homes. Homes do not need an island specified + island.getHomes().keySet().stream().filter(n -> !n.isBlank()) + .forEach(n -> islandMap.put(n, new IslandInfo(island, false))); + } + + return islandMap; + + } + + // --------------------------------------------------------------------- + // 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 user User who opens panel + */ + public static void openPanel(@NonNull CompositeCommand command, @NonNull User user) { + new IslandHomesPanel(command, user).build(); + } + + +} diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 97134b403..e89e21a49 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -1906,6 +1906,12 @@ panel: # This section contains values for BentoBox panels. panels: + # The section of translations used in Island Homes Panel + island_homes: + title: "&2&l Your island homes" + buttons: + # This button is used for displaying islands to teleport to + name: "&l [name]" # The section of translations used in Island Creation Panel island_creation: title: "&2&l Pick an island" diff --git a/src/main/resources/panels/island_homes_panel.yml b/src/main/resources/panels/island_homes_panel.yml new file mode 100644 index 000000000..1b0d6327b --- /dev/null +++ b/src/main/resources/panels/island_homes_panel.yml @@ -0,0 +1,69 @@ +# This is default island homes panel. It is used in all situations when gamemode addon does not have specified their +# of panel. +island_homes_panel: + title: panels.island_go.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: island_button # String values are expected to be `reusables` that are defined at the end of this file. + 3: island_button + 4: island_button + 5: island_button + 6: island_button + 7: island_button + 8: island_button + 3: + 1: + icon: tipped_arrow{CustomPotionColor:11546150} # 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: island_button + 3: island_button + 4: island_button + 5: island_button + 6: island_button + 7: island_button + 8: island_button + 9: + icon: tipped_arrow{CustomPotionColor:8439583} + 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: island_button + 3: island_button + 4: island_button + 5: island_button + 6: island_button + 7: island_button + 8: island_button + reusable: # List of reoccurring buttons in the panels. + island_button: # The ID of the button + # icon: GRASS_BLOCK + title: panels.island_homes.buttons.name + data: + type: ISLAND + actions: + select: + click-type: UNKNOWN + tooltip: panels.tips.click-to-choose \ No newline at end of file