Adds an island home panel. #2531

Also enhances the customizable panel API by making templates load
dynamically when requested.
This commit is contained in:
tastybento 2024-10-20 16:29:32 -07:00
parent 58690f203b
commit e0a3f48aed
8 changed files with 526 additions and 47 deletions

View File

@ -1,7 +1,5 @@
package world.bentobox.bentobox; package world.bentobox.bentobox;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -466,16 +464,6 @@ public class BentoBox extends JavaPlugin implements Listener {
return false; 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; return true;
} }

View File

@ -346,7 +346,7 @@ public abstract class CompositeCommand extends Command implements PluginIdentifi
* *
* @return IslandsManager * @return IslandsManager
*/ */
protected IslandsManager getIslands() { public IslandsManager getIslands() {
return plugin.getIslands(); return plugin.getIslands();
} }

View File

@ -61,9 +61,9 @@ public class IslandGoCommand extends DelayedTeleportCommand {
@Override @Override
public boolean execute(User user, String label, List<String> args) { public boolean execute(User user, String label, List<String> args) {
Map<String, IslandInfo> names = getNameIslandMap(user);
// Check if the home is known // Check if the home is known
if (!args.isEmpty()) { if (!args.isEmpty()) {
Map<String, IslandInfo> names = getNameIslandMap(user);
final String name = String.join(" ", args); final String name = String.join(" ", args);
if (!names.containsKey(name)) { if (!names.containsKey(name)) {
// Failed home name check // 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<String, IslandInfo> getNameIslandMap(User user) { private Map<String, IslandInfo> getNameIslandMap(User user) {
Map<String, IslandInfo> islandMap = new HashMap<>(); Map<String, IslandInfo> islandMap = new HashMap<>();
@ -129,7 +133,8 @@ public class IslandGoCommand extends DelayedTeleportCommand {
islandMap.put(text, new IslandInfo(island, true)); islandMap.put(text, new IslandInfo(island, true));
} }
// Add homes. Homes do not need an island specified // 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; return islandMap;

View File

@ -1,19 +1,12 @@
package world.bentobox.bentobox.api.commands.island; package world.bentobox.bentobox.api.commands.island;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import world.bentobox.bentobox.api.commands.CompositeCommand; 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.api.user.User;
import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.panels.customizable.IslandHomesPanel;
import world.bentobox.bentobox.util.Util;
public class IslandHomesCommand extends ConfirmableCommand { public class IslandHomesCommand extends CompositeCommand {
private List<Island> islands;
public IslandHomesCommand(CompositeCommand islandCommand) { public IslandHomesCommand(CompositeCommand islandCommand) {
super(islandCommand, "homes"); super(islandCommand, "homes");
@ -28,9 +21,8 @@ public class IslandHomesCommand extends ConfirmableCommand {
@Override @Override
public boolean canExecute(User user, String label, List<String> args) { public boolean canExecute(User user, String label, List<String> args) {
islands = getIslands().getIslands(getWorld(), user);
// Check island // Check island
if (islands.isEmpty()) { if (getIslands().getIslands(getWorld(), user).isEmpty()) {
user.sendMessage("general.errors.no-island"); user.sendMessage("general.errors.no-island");
return false; return false;
} }
@ -39,22 +31,8 @@ public class IslandHomesCommand extends ConfirmableCommand {
@Override @Override
public boolean execute(User user, String label, List<String> args) { public boolean execute(User user, String label, List<String> args) {
user.sendMessage("commands.island.sethome.homes-are"); IslandHomesPanel.openPanel(this, user);
islands.forEach(island ->
island.getHomes().keySet().stream().filter(s -> !s.isEmpty())
.forEach(s -> user.sendMessage("commands.island.sethome.home-list-syntax", TextVariables.NAME, s)));
return true; return true;
} }
@Override
public Optional<List<String>> tabComplete(User user, String alias, List<String> args) {
String lastArg = !args.isEmpty() ? args.get(args.size()-1) : "";
List<String> result = new ArrayList<>();
for (Island island : getIslands().getIslands(getWorld(), user.getUniqueId())) {
result.addAll(island.getHomes().keySet());
}
return Optional.of(Util.tabLimit(result, lastArg));
}
} }

View File

@ -90,15 +90,29 @@ public class TemplateReader
} }
File file = new File(panelLocation, templateName.endsWith(YML) ? templateName : templateName + YML); File file = new File(panelLocation, templateName.endsWith(YML) ? templateName : templateName + YML);
String absolutePath = file.getAbsolutePath();
if (!file.exists()) if (!file.exists())
{ {
BentoBox.getInstance().logError(file.getAbsolutePath() + " does not exist for panel template"); // Try to get it from the JAR
// Return as file does not exist.
return null; 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. // Check if panel is already crafted.
if (TemplateReader.loadedPanels.containsKey(panelKey)) if (TemplateReader.loadedPanels.containsKey(panelKey))

View File

@ -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<String, IslandInfo> islandMap;
private final Map<Integer, String> 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<String> 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<String> 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<String, IslandInfo> getNameIslandMap(User user) {
Map<String, IslandInfo> 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();
}
}

View File

@ -1906,6 +1906,12 @@ panel:
# This section contains values for BentoBox panels. # This section contains values for BentoBox panels.
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 # The section of translations used in Island Creation Panel
island_creation: island_creation:
title: "&2&l Pick an island" title: "&2&l Pick an island"

View File

@ -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