diff --git a/src/main/java/world/bentobox/bentobox/api/panels/TemplatedPanel.java b/src/main/java/world/bentobox/bentobox/api/panels/TemplatedPanel.java new file mode 100644 index 000000000..279d51af3 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/panels/TemplatedPanel.java @@ -0,0 +1,527 @@ +// +// Created by BONNe +// Copyright - 2021 +// + + +package world.bentobox.bentobox.api.panels; + + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import java.util.*; +import java.util.function.BiFunction; + +import world.bentobox.bentobox.BentoBox; +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.panels.reader.PanelTemplateRecord; +import world.bentobox.bentobox.api.user.User; + + +/** + * This class creates a new Panel from the template record. + * @author BONNe + * @since 1.17.3 + */ +public class TemplatedPanel extends Panel +{ + /** + * TemplatedPanel constructor class which generates functional panel. + * @param builder Builder that contains all information about the panel that must be generated. + */ + public TemplatedPanel(@NonNull TemplatedPanelBuilder builder) + { + this.user = builder.getUser(); + this.setWorld(builder.getWorld()); + this.setListener(builder.getListener()); + + this.panelTemplate = builder.getPanelTemplate(); + // Init type creators + this.typeCreators = new HashMap<>(builder.getObjectCreatorMap()); + this.typeIndex = new HashMap<>(builder.getObjectCreatorMap().size()); + this.typeSlotMap = new HashMap<>(builder.getObjectCreatorMap().size()); + + if (this.panelTemplate == null) + { + BentoBox.getInstance().logError("Cannot generate panel because template is not loaded."); + } + else + { + this.generatePanel(); + } + } + + + /** + * This method generates the panel from the template. + */ + private void generatePanel() + { + Map items = switch (this.panelTemplate.type()) + { + case INVENTORY -> this.populateInventoryPanel(); + case HOPPER -> this.populateHopperPanel(); + case DROPPER -> this.populateDropperPanel(); + }; + + super.makePanel(this.user.getTranslation(this.panelTemplate.title()), + items, + items.keySet().stream().max(Comparator.naturalOrder()).orElse(9), + this.user, + this.getListener().orElse(null), + this.panelTemplate.type()); + } + + + /** + * This method creates map with item indexes and their icons that will be added into + * Inventory Panel. + * @return Map that contains indexes linked to the correct panel item. + */ + @NonNull + private Map populateInventoryPanel() + { + // Init item array with the max available size. + PanelItem[][] itemArray = new PanelItem[6][9]; + + // Analyze the GUI button layout a bit. + for (int i = 0; i < this.panelTemplate.content().length; i++) + { + for (int k = 0; k < this.panelTemplate.content()[i].length; k++) + { + ItemTemplateRecord record = this.panelTemplate.content()[i][k]; + + if (record != null && record.dataMap().containsKey("type")) + { + String type = String.valueOf(record.dataMap().get("type")); + + int counter = this.typeSlotMap.computeIfAbsent(type, key -> 1); + this.typeSlotMap.put(type, counter + 1); + } + } + } + + // Make buttons for the GUI + for (int i = 0; i < this.panelTemplate.content().length; i++) + { + for (int k = 0; k < this.panelTemplate.content()[i].length; k++) + { + itemArray[i][k] = this.makeButton(this.panelTemplate.content()[i][k]); + } + } + + // After items are created, remove empty lines. + boolean[] showLine = this.panelTemplate.forcedRows(); + + for (int i = 0; i < this.panelTemplate.content().length; i++) + { + boolean emptyLine = true; + + for (int k = 0; emptyLine && k < this.panelTemplate.content()[i].length; k++) + { + emptyLine = itemArray[i][k] == null; + } + + // Do not generate fallback for "empty" lines. + showLine[i] = showLine[i] || !emptyLine; + } + + // Now fill the border. + if (this.panelTemplate.border() != null) + { + PanelItem template = this.makeTemplate(this.panelTemplate.border()); + + // Hard codded 6 + for (int i = 0; i < 6; i++) + { + if (i == 0 || i == 5) + { + // Fill first and last row completely with border. + for (int k = 0; k < 9; k++) + { + if (itemArray[i][k] == null) + { + itemArray[i][k] = template; + } + } + } + else + { + // Fill first and last element in row with border. + if (itemArray[i][0] == null) + { + itemArray[i][0] = template; + } + + if (itemArray[i][8] == null) + { + itemArray[i][8] = template; + } + } + } + + showLine[0] = true; + showLine[5] = true; + } + + // Now fill the background. + if (this.panelTemplate.background() != null) + { + PanelItem template = this.makeTemplate(this.panelTemplate.background()); + + for (int i = 0; i < 6; i++) + { + for (int k = 0; k < 9; k++) + { + if (itemArray[i][k] == null) + { + itemArray[i][k] = template; + } + } + } + } + + // Now place all panel items with their indexes into item map. + Map itemMap = new HashMap<>(6 * 9); + + int correctIndex = 0; + + for (int i = 0; i < itemArray.length; i++) + { + final boolean iterate = showLine[i]; + + for (int k = 0; iterate && k < itemArray[i].length; k++) + { + if (itemArray[i][k] != null) + { + itemMap.put(correctIndex, itemArray[i][k]); + } + + correctIndex++; + } + } + + return itemMap; + } + + + /** + * This method creates map with item indexes and their icons that will be added into + * hopper Panel. + * @return Map that contains indexes linked to the correct panel item. + */ + @NonNull + private Map populateHopperPanel() + { + // Init item array with the max available size. + PanelItem[] itemArray = new PanelItem[5]; + + // Analyze the template + for (int i = 0; i < 5; i++) + { + ItemTemplateRecord record = this.panelTemplate.content()[0][i]; + + if (record != null && record.dataMap().containsKey("type")) + { + String type = String.valueOf(record.dataMap().get("type")); + + int counter = this.typeSlotMap.computeIfAbsent(type, key -> 1); + this.typeSlotMap.put(type, counter + 1); + } + } + + // Make buttons + for (int i = 0; i < 5; i++) + { + itemArray[i] = this.makeButton(this.panelTemplate.content()[0][i]); + } + + // Now fill the background. + if (this.panelTemplate.background() != null) + { + PanelItem template = this.makeTemplate(this.panelTemplate.background()); + + for (int i = 0; i < 5; i++) + { + if (itemArray[i] == null) + { + itemArray[i] = template; + } + } + } + + // Now place all panel items with their indexes into item map. + Map itemMap = new HashMap<>(5); + + int correctIndex = 0; + + for (PanelItem panelItem : itemArray) + { + if (panelItem != null) + { + itemMap.put(correctIndex, panelItem); + } + + correctIndex++; + } + + return itemMap; + } + + + /** + * This method creates map with item indexes and their icons that will be added into + * dropper Panel. + * @return Map that contains indexes linked to the correct panel item. + */ + @NonNull + private Map populateDropperPanel() + { + // Analyze the template + for (int i = 0; i < 3; i++) + { + for (int k = 0; k < 3; k++) + { + ItemTemplateRecord record = this.panelTemplate.content()[i][k]; + + if (record != null && record.dataMap().containsKey("type")) + { + String type = String.valueOf(record.dataMap().get("type")); + + int counter = this.typeSlotMap.computeIfAbsent(type, key -> 1); + this.typeSlotMap.put(type, counter + 1); + } + } + } + + // Init item array with the max available size. + PanelItem[][] itemArray = new PanelItem[3][3]; + + // Make buttons + for (int i = 0; i < 3; i++) + { + for (int k = 0; k < 3; k++) + { + itemArray[i][k] = this.makeButton(this.panelTemplate.content()[i][k]); + } + } + + // Now fill the background. + if (this.panelTemplate.background() != null) + { + PanelItem template = this.makeTemplate(this.panelTemplate.background()); + + for (int i = 0; i < 3; i++) + { + for (int k = 0; k < 3; k++) + { + if (itemArray[i][k] == null) + { + itemArray[i][k] = template; + } + } + } + } + + // Init item map with the max available size. + Map itemMap = new HashMap<>(9); + + int correctIndex = 0; + + for (int i = 0; i < 3; i++) + { + for (int k = 0; k < 3; k++) + { + if (itemArray[i][k] != null) + { + itemMap.put(correctIndex, itemArray[i][k]); + } + + correctIndex++; + } + } + + return itemMap; + } + + + /** + * This method passes button creation from given record template. + * @param record Template of the button that must be created. + * @return PanelItem of the template, otherwise null. + */ + @Nullable + private PanelItem makeButton(@Nullable ItemTemplateRecord record) + { + if (record == null) + { + // Immediate exit if record is null. + return null; + } + + if (record.dataMap().containsKey("type")) + { + // If dataMap is not null, and it is not empty, then pass button to the object creator function. + + return this.makeAddonButton(record); + } + else + { + PanelItemBuilder itemBuilder = new PanelItemBuilder(); + + if (record.icon() != null) + { + itemBuilder.icon(record.icon().clone()); + } + + if (record.title() != null) + { + itemBuilder.name(this.user.getTranslation(record.title())); + } + + if (record.description() != null) + { + itemBuilder.description(this.user.getTranslation(record.description())); + } + + // If there are generic click handlers that could be added, then this is a place + // where to process them. + + // Click Handlers are managed by custom addon buttons. + return itemBuilder.build(); + } + } + + + /** + * This method passes button to the type creator, if that exists. + * @param record Template of the button that must be created. + * @return PanelItem of the button created by typeCreator, otherwise null. + */ + @Nullable + private PanelItem makeAddonButton(@NonNull ItemTemplateRecord record) + { + // Get object type. + String type = String.valueOf(record.dataMap().getOrDefault("type", "")); + + if (!this.typeCreators.containsKey(type)) + { + // There are no object with a given type. + return this.makeFallBack(record.fallback()); + } + + BiFunction buttonBuilder = this.typeCreators.get(type); + + // Get next slot index. + ItemSlot itemSlot = this.typeIndex.containsKey(type) ? + this.typeIndex.get(type) : + new ItemSlot(0, this.typeSlotMap); + this.typeIndex.put(type, itemSlot.nextItemSlot()); + + // Try to get next object. + PanelItem item = buttonBuilder.apply(record, itemSlot); + return item == null ? this.makeFallBack(record.fallback()) : item; + } + + + /** + * This method creates a fall back button for given record. + * @param record Record which fallback must be created. + * @return PanelItem if fallback was creates successfully, otherwise null. + */ + @Nullable + private PanelItem makeFallBack(@Nullable ItemTemplateRecord record) + { + return record == null ? null : this.makeButton(record.fallback()); + } + + + /** + * This method translates template record into a panel item. + * @param record Record that must be translated. + * @return PanelItem that contains all information from the record. + */ + private PanelItem makeTemplate(PanelTemplateRecord.TemplateItem record) + { + PanelItemBuilder itemBuilder = new PanelItemBuilder(); + + // Read icon only if it is not null. + if (record.icon() != null) + { + itemBuilder.icon(record.icon().clone()); + } + + // Read title only if it is not null. + if (record.title() != null) + { + itemBuilder.name(this.user.getTranslation(record.title())); + } + + // Read description only if it is not null. + if (record.description() != null) + { + itemBuilder.description(this.user.getTranslation(record.description())); + } + + // Click Handlers are managed by custom addon buttons. + return itemBuilder.build(); + } + + +// --------------------------------------------------------------------- +// Section: Classes +// --------------------------------------------------------------------- + + + /** + * This record contains current slot object and map that links types with a number of slots in + * panel with it. + * Some buttons need information about all types, like previous/next. + * @param slot Index of object in current panel. + * @param amountMap Map that links types with number of objects in panel. + */ + public record ItemSlot(int slot, Map amountMap) + { + /** + * This method returns new record object with iterative slot index. + * @return New ItemSlot object that has increased slot index by 1. + */ + ItemSlot nextItemSlot() + { + return new ItemSlot(this.slot() + 1, this.amountMap()); + } + } + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + + /** + * The GUI template record. + */ + private final PanelTemplateRecord panelTemplate; + + /** + * The user who opens the GUI. + */ + private final User user; + + /** + * This map links custom types with their info object. + */ + private final Map> typeCreators; + + /** + * Stores the item slot information for each type. + */ + private final Map typeIndex; + + /** + * Stores the number of items with given type in whole panel. + */ + private final Map typeSlotMap; +} diff --git a/src/main/java/world/bentobox/bentobox/api/panels/builders/TemplatedPanelBuilder.java b/src/main/java/world/bentobox/bentobox/api/panels/builders/TemplatedPanelBuilder.java new file mode 100644 index 000000000..fa8ea51e3 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/panels/builders/TemplatedPanelBuilder.java @@ -0,0 +1,203 @@ +// +// Created by BONNe +// Copyright - 2021 +// + + +package world.bentobox.bentobox.api.panels.builders; + + +import org.bukkit.World; +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; + +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.PanelListener; +import world.bentobox.bentobox.api.panels.TemplatedPanel; +import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; +import world.bentobox.bentobox.api.panels.reader.PanelTemplateRecord; +import world.bentobox.bentobox.api.panels.reader.TemplateReader; +import world.bentobox.bentobox.api.user.User; + + +/** + * Builds {@link TemplatedPanel}'s + * @author BONNe + * @since 1.17.3 + */ +public class TemplatedPanelBuilder +{ +// --------------------------------------------------------------------- +// Section: Builder +// --------------------------------------------------------------------- + + /** + * Adds the template that must be loaded for Template panel builder. + * + * @param guiName the gui name + * @param dataFolder the data folder + * @return the template panel builder + */ + public TemplatedPanelBuilder template(String guiName, File dataFolder) + { + this.panelTemplate = TemplateReader.readTemplatePanel(guiName, dataFolder); + return this; + } + + + /** + * Adds the user for template panel builder. + * + * @param user the user + * @return the template panel builder + */ + public TemplatedPanelBuilder user(User user) + { + this.user = user; + return this; + } + + + /** + * Adds the world for template panel builder. + * + * @param world the world + * @return the template panel builder + */ + public TemplatedPanelBuilder world(World world) + { + this.world = world; + return this; + } + + + /** + * Adds the panel listener for template panel builder. + * + * @param listener the listener + * @return the template panel builder + */ + public TemplatedPanelBuilder listener(PanelListener listener) + { + this.listener = listener; + return this; + } + + + /** + * Registers new button type builder for template panel builder. + * + * @param type the type + * @param buttonCreator the button creator + * @return the template panel builder + */ + public TemplatedPanelBuilder registerTypeBuilder(String type, BiFunction buttonCreator) + { + this.objectCreatorMap.put(type, buttonCreator); + return this; + } + + + /** + * Build templated panel. + * + * @return the templated panel + */ + public TemplatedPanel build() + { + return new TemplatedPanel(this); + } + + +// --------------------------------------------------------------------- +// Section: Getters +// --------------------------------------------------------------------- + + + /** + * Gets panel template. + * + * @return the panel template + */ + public PanelTemplateRecord getPanelTemplate() + { + return this.panelTemplate; + } + + + /** + * Gets user. + * + * @return the user + */ + public User getUser() + { + return this.user; + } + + + /** + * Gets world. + * + * @return the world + */ + public World getWorld() + { + return this.world; + } + + + /** + * Gets listener. + * + * @return the listener + */ + public PanelListener getListener() + { + return this.listener; + } + + + /** + * Gets object creator map. + * + * @return the object creator map + */ + public Map> getObjectCreatorMap() + { + return this.objectCreatorMap; + } + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + + /** + * The GUI template record. + */ + private PanelTemplateRecord panelTemplate; + + /** + * The user who opens the GUI. + */ + private User user; + + /** + * The world where GUI operates. + */ + private World world; + + /** + * Panel Listener + */ + private PanelListener listener; + + /** + * Map that links objects with their panel item creators. + */ + private final Map> objectCreatorMap = new HashMap<>(); +} diff --git a/src/main/java/world/bentobox/bentobox/api/panels/reader/ItemTemplateRecord.java b/src/main/java/world/bentobox/bentobox/api/panels/reader/ItemTemplateRecord.java index 2338171e5..fe8e39974 100644 --- a/src/main/java/world/bentobox/bentobox/api/panels/reader/ItemTemplateRecord.java +++ b/src/main/java/world/bentobox/bentobox/api/panels/reader/ItemTemplateRecord.java @@ -9,6 +9,8 @@ package world.bentobox.bentobox.api.panels.reader; 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.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -27,12 +29,12 @@ import java.util.Map; * * @since 1.17.3 */ -public record ItemTemplateRecord(ItemStack icon, - String title, - String description, - List actions, - Map dataMap, - ItemTemplateRecord fallback) +public record ItemTemplateRecord(@Nullable ItemStack icon, + @Nullable String title, + @Nullable String description, + @NonNull List actions, + @NonNull Map dataMap, + @Nullable ItemTemplateRecord fallback) { /** * Instantiates a new Item template record without actions and data map. diff --git a/src/main/java/world/bentobox/bentobox/api/panels/reader/PanelTemplateRecord.java b/src/main/java/world/bentobox/bentobox/api/panels/reader/PanelTemplateRecord.java index 22f8f6767..f2c48dbf5 100644 --- a/src/main/java/world/bentobox/bentobox/api/panels/reader/PanelTemplateRecord.java +++ b/src/main/java/world/bentobox/bentobox/api/panels/reader/PanelTemplateRecord.java @@ -8,6 +8,8 @@ package world.bentobox.bentobox.api.panels.reader; import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.api.panels.Panel; @@ -20,15 +22,17 @@ import world.bentobox.bentobox.api.panels.Panel; * @param title the title of GUI * @param border the border block for GUI * @param background the background block for GUI. + * @param forcedRows the array of boolean that indicate which rows must be force loaded. * @param content The 2D array of ItemTemplateRecords * * @since 1.17.3 */ public record PanelTemplateRecord(Panel.Type type, - String title, - TemplateItem border, - TemplateItem background, - ItemTemplateRecord[][] content) + @Nullable String title, + @Nullable TemplateItem border, + @Nullable TemplateItem background, + boolean[] forcedRows, + @NonNull ItemTemplateRecord[][] content) { /** * Instantiates a new Panel template record with empty content. @@ -37,10 +41,11 @@ public record PanelTemplateRecord(Panel.Type type, * @param title the title * @param border the border * @param background the background + * @param forcedRows the forced rows array */ - public PanelTemplateRecord(Panel.Type type, String title, TemplateItem border, TemplateItem background) + public PanelTemplateRecord(Panel.Type type, String title, TemplateItem border, TemplateItem background, boolean[] forcedRows) { - this(type, title, border, background, new ItemTemplateRecord[6][9]); + this(type, title, border, background, forcedRows, new ItemTemplateRecord[6][9]); } 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 27b6b9f67..2213cad90 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 @@ -96,7 +96,9 @@ public class TemplateReader } String title = configurationSection.getString("title"); - Panel.Type type = configurationSection.getObject("type", Panel.Type.class); + Panel.Type type = + Enums.getIfPresent(Panel.Type.class, configurationSection.getString("type", "INVENTORY")). + or(Panel.Type.INVENTORY); PanelTemplateRecord.TemplateItem borderItem = null; @@ -110,7 +112,7 @@ public class TemplateReader { borderItem = new PanelTemplateRecord.TemplateItem( ItemParser.parse((borderSection.getString("icon", Material.AIR.name()))), - borderSection.getString("name", null), + borderSection.getString("title", null), borderSection.getString("description", null)); } } @@ -134,7 +136,7 @@ public class TemplateReader { backgroundItem = new PanelTemplateRecord.TemplateItem( ItemParser.parse((backgroundSection.getString("icon", Material.AIR.name()))), - backgroundSection.getString("name", null), + backgroundSection.getString("title", null), backgroundSection.getString("description", null)); } } @@ -157,8 +159,11 @@ public class TemplateReader readPanelItemTemplate(reusable.getConfigurationSection(key), key, panelItemDataMap)); } + // Read content + boolean[] forcedRows = readForcedRows(configurationSection); + // Create template record. - PanelTemplateRecord template = new PanelTemplateRecord(type, title, borderItem, backgroundItem); + PanelTemplateRecord template = new PanelTemplateRecord(type, title, borderItem, backgroundItem, forcedRows); // Read content ConfigurationSection content = configurationSection.getConfigurationSection("content"); @@ -207,6 +212,41 @@ public class TemplateReader } + /** + * This method reads force shown rows that must be always displayed. + * @param section Configuration section that contains force-shown path. + * @return boolean array that contains which lines are force loaded. + */ + private static boolean[] readForcedRows(@Nullable ConfigurationSection section) + { + boolean[] forceShow = new boolean[6]; + + if (section != null && section.contains("force-shown")) + { + if (section.isInt("force-shown")) + { + int value = section.getInt("force-shown"); + + if (value > 0 && value < 7) + { + forceShow[value-1] = true; + } + } + else if (section.isList("force-shown")) + { + section.getIntegerList("force-shown").forEach(number -> { + if (number > 0 && number < 7) + { + forceShow[number-1] = true; + } + }); + } + } + + return forceShow; + } + + /** * This method creates PanelItemTemplate from a given configuration section. * @param section Section that should contain all information about the panel item template. diff --git a/src/main/java/world/bentobox/bentobox/util/ItemParser.java b/src/main/java/world/bentobox/bentobox/util/ItemParser.java index 7b7ab6836..feb21ae42 100644 --- a/src/main/java/world/bentobox/bentobox/util/ItemParser.java +++ b/src/main/java/world/bentobox/bentobox/util/ItemParser.java @@ -171,7 +171,7 @@ public class ItemParser { boolean isExtended = part[3].equalsIgnoreCase("EXTENDED"); PotionData data = new PotionData(type, isExtended, isUpgraded); potionMeta.setBasePotionData(data); - + result.setItemMeta(potionMeta); result.setAmount(Integer.parseInt(part[5])); return result; }