diff --git a/out/artifacts/protectionstones_jar/protectionstones.jar b/out/artifacts/protectionstones_jar/protectionstones.jar new file mode 100644 index 00000000..3258abfa Binary files /dev/null and b/out/artifacts/protectionstones_jar/protectionstones.jar differ diff --git a/pom.xml b/pom.xml index 3c7a417c..f243690c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 dev.espi protectionstones - 2.10.6 + 2.10.7 ProtectionStones A grief prevention plugin for Spigot Minecraft servers. https://github.com/espidev/ProtectionStones diff --git a/src/main/java/dev/espi/protectionstones/ListenerClass.java b/src/main/java/dev/espi/protectionstones/ListenerClass.java index 87175a00..3738b688 100644 --- a/src/main/java/dev/espi/protectionstones/ListenerClass.java +++ b/src/main/java/dev/espi/protectionstones/ListenerClass.java @@ -307,15 +307,17 @@ public void onPrepareItemCraft(PrepareItemCraftEvent e) { @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onCrafter(CrafterCraftEvent e) { - if (e.getBlock().getType() != Material.CRAFTER) return; - if (!(e.getBlock().getState() instanceof Container container)) return; - for (ItemStack item : container.getInventory().getContents()) { + Block block = e.getBlock(); + BlockState state = block.getState(); + if (block.getType() != Material.CRAFTER) return; + if (!(state instanceof Container container)) return; + Inventory inv = container.getInventory(); + for (ItemStack item : inv.getContents()) { if (item == null) continue; PSProtectBlock options = ProtectionStones.getBlockOptions(item); if (options != null && !options.allowUseInCrafting) { e.setCancelled(true); e.setResult(new ItemStack(Material.AIR)); - return; } } } diff --git a/src/main/java/dev/espi/protectionstones/PSConfig.java b/src/main/java/dev/espi/protectionstones/PSConfig.java index 1edf8b9b..d57c07ad 100644 --- a/src/main/java/dev/espi/protectionstones/PSConfig.java +++ b/src/main/java/dev/espi/protectionstones/PSConfig.java @@ -76,6 +76,40 @@ public class PSConfig { @Path("allow_home_teleport_for_members") public Boolean allowHomeTeleportForMembers; + // ------------------------------------------------------------------ + // Inventory GUI toggles + // If gui.enabled is false, all commands use legacy text-based output. + // If gui.enabled is true, individual commands can be enabled below. + @Path("gui.enabled") + public Boolean guiEnabled; + @Path("gui.commands.home") + public Boolean guiCommandHome; + @Path("gui.commands.flag") + public Boolean guiCommandFlag; + @Path("gui.commands.add") + public Boolean guiCommandAdd; + @Path("gui.commands.remove") + public Boolean guiCommandRemove; + @Path("gui.commands.addowner") + public Boolean guiCommandAddowner; + @Path("gui.commands.removeowner") + public Boolean guiCommandRemoveowner; + + // Additional command GUIs + @Path("gui.commands.list") + public Boolean guiCommandList; + @Path("gui.commands.info") + public Boolean guiCommandInfo; + @Path("gui.commands.tp") + public Boolean guiCommandTp; + @Path("gui.commands.unclaim") + public Boolean guiCommandUnclaim; + @Path("gui.commands.priority") + public Boolean guiCommandPriority; + + @Path("gui.commands.admin") + public Boolean guiCommandAdmin; + @Path("admin.cleanup_delete_regions_with_members_but_no_owners") public Boolean cleanupDeleteRegionsWithMembersButNoOwners; @@ -229,4 +263,4 @@ static void initConfig() { RecipeUtil.setupPSRecipes(); } } -} +} \ No newline at end of file diff --git a/src/main/java/dev/espi/protectionstones/ProtectionStones.java b/src/main/java/dev/espi/protectionstones/ProtectionStones.java index bd1775f9..1ac6feab 100644 --- a/src/main/java/dev/espi/protectionstones/ProtectionStones.java +++ b/src/main/java/dev/espi/protectionstones/ProtectionStones.java @@ -22,6 +22,7 @@ import com.sk89q.worldguard.protection.regions.ProtectedRegion; import dev.espi.protectionstones.commands.ArgHelp; import dev.espi.protectionstones.commands.PSCommandArg; +import dev.espi.protectionstones.gui.GuiManager; import dev.espi.protectionstones.placeholders.PSPlaceholderExpansion; import dev.espi.protectionstones.utils.BlockUtil; import dev.espi.protectionstones.utils.RecipeUtil; @@ -60,7 +61,7 @@ public class ProtectionStones extends JavaPlugin { // change this when the config version goes up - public static final int CONFIG_VERSION = 16; + public static final int CONFIG_VERSION = 19; private boolean debug = false; @@ -72,6 +73,9 @@ public class ProtectionStones extends JavaPlugin { private PSEconomy economy; + // Inventory GUI manager (used when gui.* config toggles are enabled) + private GuiManager guiManager; + // all configuration file options are stored in here private PSConfig configOptions; static HashMap protectionStonesOptions = new HashMap<>(); @@ -179,6 +183,10 @@ public PSConfig getConfigOptions() { return configOptions; } + public GuiManager getGuiManager() { + return guiManager; + } + /** * @param conf config object to replace current config */ @@ -555,6 +563,11 @@ public void onEnable() { Config.setInsertionOrderPreserved(true); // make sure that config upgrades aren't a complete mess plugin = this; + + // GUI manager (safe to register even if GUIs are disabled via config) + guiManager = new GuiManager(this); + guiManager.register(); + configLocation = new File(this.getDataFolder() + "/config.toml"); blockDataFolder = new File(this.getDataFolder() + "/blocks"); @@ -661,4 +674,11 @@ public void onEnable() { getLogger().info(ChatColor.WHITE + "ProtectionStones has successfully started!"); } + @Override + public void onDisable() { + if (guiManager != null) { + guiManager.unregister(); + } + } + } \ No newline at end of file diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgAddRemove.java b/src/main/java/dev/espi/protectionstones/commands/ArgAddRemove.java index b6f167e7..74fcac86 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgAddRemove.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgAddRemove.java @@ -18,6 +18,7 @@ import com.sk89q.worldguard.bukkit.WorldGuardPlugin; import dev.espi.protectionstones.*; import dev.espi.protectionstones.utils.LimitUtil; +import dev.espi.protectionstones.gui.screens.members.RegionPlayerSelectGui; import dev.espi.protectionstones.utils.UUIDCache; import dev.espi.protectionstones.utils.WGUtils; import org.bukkit.Bukkit; @@ -66,6 +67,41 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap Boolean.TRUE.equals(conf.guiCommandAdd); + case "remove" -> Boolean.TRUE.equals(conf.guiCommandRemove); + case "addowner" -> Boolean.TRUE.equals(conf.guiCommandAddowner); + case "removeowner" -> Boolean.TRUE.equals(conf.guiCommandRemoveowner); + default -> false; + }; + + // If GUI enabled for this command and no player argument provided (and not using -a), open selector GUI + if (guiGlobal && guiThis && args.length < 2 && !flags.containsKey("-a")) { + PSRegion r = PSRegion.fromLocationGroup(p.getLocation()); + if (r == null) { + return PSL.msg(p, PSL.NOT_IN_REGION.msg()); + } else if (WGUtils.hasNoAccess(r.getWGRegion(), p, WorldGuardPlugin.inst().wrapPlayer(p), false)) { + return PSL.msg(p, PSL.NO_ACCESS.msg()); + } + + RegionPlayerSelectGui.Mode mode = switch (operationType) { + case "add" -> RegionPlayerSelectGui.Mode.ADD_MEMBER; + case "remove" -> RegionPlayerSelectGui.Mode.REMOVE_MEMBER; + case "addowner" -> RegionPlayerSelectGui.Mode.ADD_OWNER; + case "removeowner" -> RegionPlayerSelectGui.Mode.REMOVE_OWNER; + default -> RegionPlayerSelectGui.Mode.ADD_MEMBER; + }; + + ProtectionStones.getInstance().getGuiManager().open(p, + new RegionPlayerSelectGui(ProtectionStones.getInstance().getGuiManager(), r, mode, 0)); + return true; + } + if (args.length < 2) { return PSL.msg(p, PSL.COMMAND_REQUIRES_PLAYER_NAME.msg()); } diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgAdmin.java b/src/main/java/dev/espi/protectionstones/commands/ArgAdmin.java index c6dea490..30c72626 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgAdmin.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgAdmin.java @@ -19,9 +19,11 @@ import dev.espi.protectionstones.utils.upgrade.LegacyUpgrade; import dev.espi.protectionstones.PSL; import dev.espi.protectionstones.ProtectionStones; +import dev.espi.protectionstones.gui.screens.admin.AdminMenuGui; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.bukkit.util.StringUtil; import java.util.*; @@ -86,6 +88,15 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap " + ChatColor.GRAY + bc; + String baseCommand = ProtectionStones.getInstance().getConfigOptions().base_command; + String bc = "/" + baseCommand; + String tx = ChatColor.AQUA + "> " + ChatColor.GRAY + "/" + bc; p.sendMessage(ChatColor.DARK_GRAY + "" + ChatColor.STRIKETHROUGH + "===============" + ChatColor.RESET + " PS Admin Help " + @@ -58,7 +59,7 @@ static boolean argumentAdminHelp(CommandSender p, String[] args) { send(p, tx + " admin version", "Show the version number of the plugin.\n\n" + bc + " admin version", - bc + " admin version", + baseCommand + " admin version", false); send(p, diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgFlag.java b/src/main/java/dev/espi/protectionstones/commands/ArgFlag.java index feee7e43..1a1fc270 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgFlag.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgFlag.java @@ -19,6 +19,7 @@ import com.sk89q.worldguard.protection.flags.*; import com.sk89q.worldguard.protection.regions.ProtectedRegion; import dev.espi.protectionstones.*; +import dev.espi.protectionstones.gui.screens.flags.FlagsGui; import dev.espi.protectionstones.utils.MiscUtil; import dev.espi.protectionstones.utils.WGUtils; import net.md_5.bungee.api.chat.*; @@ -118,7 +119,7 @@ private boolean openFlagGUI(Player p, PSRegion r, int page) { } // add line based on flag type - boolean isGroupValueAll = groupfValue.equalsIgnoreCase("all") || groupfValue.isEmpty(); + boolean isGroupValueAll = groupfValue.equalsIgnoreCase("all") || groupfValue.isEmpty();; if (f instanceof StateFlag) { // allow/deny TextComponent allow = new TextComponent((fValue == StateFlag.State.ALLOW ? ChatColor.WHITE : ChatColor.DARK_GRAY) + "Allow"), @@ -184,15 +185,25 @@ private boolean openFlagGUI(Player p, PSRegion r, int page) { } // set hover and click task for flag group - // HACK: Prevent pvp flag group from being changed when set to "all" to prevent exploit - boolean isPvpExploitCase = flag.equalsIgnoreCase("pvp") && isGroupValueAll; - if (isPvpExploitCase) { - BaseComponent[] hover = new ComponentBuilder(PSL.FLAG_PREVENT_EXPLOIT_HOVER.msg()).create(); + BaseComponent[] hover; + // HACK: Prevent pvp flag value from being changed to none/null + // Special handling for "pvp" flag with "all" group, disabling interaction. + if (flag.equalsIgnoreCase("pvp") && isGroupValueAll) { + hover = new ComponentBuilder(PSL.FLAG_PREVENT_EXPLOIT_HOVER.msg()).create(); + // Remove click action to fully disable changing this group. + groupChange.setClickEvent(null); + } else if (fValue == null) { + hover = new ComponentBuilder(PSL.FLAG_GUI_HOVER_CHANGE_GROUP_NULL.msg()).create(); + } else { + hover = new ComponentBuilder(PSL.FLAG_GUI_HOVER_CHANGE_GROUP.msg().replace("%group%", nextGroup)).create(); + } + + // Always set hover if the flag is pvp and group is "all" + if (flag.equalsIgnoreCase("pvp") && groupfValue.equalsIgnoreCase("all")) { + hover = new ComponentBuilder(PSL.FLAG_PREVENT_EXPLOIT_HOVER.msg()).create(); groupChange.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover)); + groupChange.setClickEvent(null); // Disable click event explicitly } else if (!nextGroup.equals(groupfValue)) { - BaseComponent[] hover = fValue == null - ? new ComponentBuilder(PSL.FLAG_GUI_HOVER_CHANGE_GROUP_NULL.msg()).create() - : new ComponentBuilder(PSL.FLAG_GUI_HOVER_CHANGE_GROUP.msg().replace("%group%", nextGroup)).create(); groupChange.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover)); groupChange.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, suggestedCommand + "-g " + nextGroup + " " + page + ":" + flag + " " + fValue)); } @@ -230,6 +241,10 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap tabComplete(CommandSender sender, String alias, String[] arg } // /ps flag logic (utilizing WG internal /region flag logic) - static void setFlag(PSRegion r, CommandSender p, String flagName, String value, String groupValue) { + public static void setFlag(PSRegion r, CommandSender p, String flagName, String value, String groupValue) { // correct the flag if gui flags are there String[] flagSplit = flagName.split(":"); if (flagSplit.length == 2) flagName = flagSplit[1]; diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgHome.java b/src/main/java/dev/espi/protectionstones/commands/ArgHome.java index 44871a13..1ad39068 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgHome.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgHome.java @@ -16,6 +16,7 @@ package dev.espi.protectionstones.commands; import dev.espi.protectionstones.*; +import dev.espi.protectionstones.gui.screens.home.HomeWorldsGui; import dev.espi.protectionstones.utils.ChatUtil; import dev.espi.protectionstones.utils.MiscUtil; import dev.espi.protectionstones.utils.TextGUI; @@ -121,6 +122,10 @@ private void openHomeGUI(PSPlayer psp, List homes, int page) { public boolean executeArgument(CommandSender s, String[] args, HashMap flags) { Player p = (Player) s; + // GUI toggle (inventory GUI vs legacy text-based output) + PSConfig conf = ProtectionStones.getInstance().getConfigOptions(); + boolean useInvGui = Boolean.TRUE.equals(conf.guiEnabled) && Boolean.TRUE.equals(conf.guiCommandHome); + // prelim checks if (!p.hasPermission("protectionstones.home")) return PSL.msg(p, PSL.NO_PERMISSION_HOME.msg()); @@ -132,11 +137,27 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap regions = psp.getHomes(p.getWorld()); - if (regions.size() == 1) { // teleport to home if there is only one home - ArgTp.teleportPlayer(p, regions.get(0)); - } else { // otherwise, open the GUI - openHomeGUI(psp, regions, (flags.get("-p") == null || !MiscUtil.isValidInteger(flags.get("-p")) ? 0 : Integer.parseInt(flags.get("-p")) - 1)); + if (useInvGui) { + // gather homes across all worlds; if only 1 total, teleport directly + List allHomes = new ArrayList<>(); + for (org.bukkit.World w : Bukkit.getWorlds()) { + allHomes.addAll(psp.getHomes(w)); + } + if (allHomes.size() == 1) { + ArgTp.teleportPlayer(p, allHomes.get(0)); + } else { + Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> + ProtectionStones.getInstance().getGuiManager().open(p, new HomeWorldsGui(ProtectionStones.getInstance().getGuiManager(), 0)) + ); + } + } else { + // legacy text-based GUI (current-world homes) + List regions = psp.getHomes(p.getWorld()); + if (regions.size() == 1) { // teleport to home if there is only one home + ArgTp.teleportPlayer(p, regions.get(0)); + } else { // otherwise, open the text GUI + openHomeGUI(psp, regions, (flags.get("-p") == null || !MiscUtil.isValidInteger(flags.get("-p")) ? 0 : Integer.parseInt(flags.get("-p")) - 1)); + } } } else {// /ps home [id] // get regions from the query diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgInfo.java b/src/main/java/dev/espi/protectionstones/commands/ArgInfo.java index bdcca788..6b65506b 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgInfo.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgInfo.java @@ -24,6 +24,9 @@ import dev.espi.protectionstones.*; import dev.espi.protectionstones.utils.UUIDCache; import dev.espi.protectionstones.utils.WGUtils; +import dev.espi.protectionstones.gui.screens.flags.FlagsGui; +import dev.espi.protectionstones.gui.screens.regions.RegionInfoGui; +import dev.espi.protectionstones.gui.screens.regions.RegionRosterGui; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -70,6 +73,39 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + if (!p.hasPermission("protectionstones.members")) + return PSL.NO_PERMISSION_MEMBERS.send(p); + gm.open(p, new RegionRosterGui(gm, p.getWorld().getUID(), r.getId(), RegionRosterGui.Mode.MEMBERS, 0, null)); + return true; + } + case "owners" -> { + if (!p.hasPermission("protectionstones.owners")) + return PSL.NO_PERMISSION_OWNERS.send(p); + gm.open(p, new RegionRosterGui(gm, p.getWorld().getUID(), r.getId(), RegionRosterGui.Mode.OWNERS, 0, null)); + return true; + } + case "flags" -> { + if (!p.hasPermission("protectionstones.flags")) + return PSL.NO_PERMISSION_FLAGS.send(p); + gm.open(p, new FlagsGui(gm, p.getWorld().getUID(), r.getId(), 0)); + return true; + } + } + } + } + if (args.length == 1) { // info of current region player is in if (!p.hasPermission("protectionstones.info")) return PSL.NO_PERMISSION_INFO.send(p); diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgList.java b/src/main/java/dev/espi/protectionstones/commands/ArgList.java index 3b91f07d..3eeb80bb 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgList.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgList.java @@ -19,6 +19,8 @@ import dev.espi.protectionstones.PSPlayer; import dev.espi.protectionstones.PSRegion; import dev.espi.protectionstones.ProtectionStones; +import dev.espi.protectionstones.gui.screens.common.LoadingGui; +import dev.espi.protectionstones.gui.screens.regions.RegionListGui; import dev.espi.protectionstones.utils.UUIDCache; import org.bukkit.Bukkit; import org.bukkit.ChatColor; @@ -56,6 +58,24 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + List regions = psp.getPSRegionsCrossWorld(psp.getPlayer().getWorld(), true); + Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> + gm.open(p, new RegionListGui(gm, RegionListGui.Mode.LIST, "Regions", regions, 0, null)) + ); + }); + return true; + } + } + if (args.length == 2 && !s.hasPermission("protectionstones.list.others")) return PSL.msg(s, PSL.NO_PERMISSION_LIST_OTHERS.msg()); diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgPriority.java b/src/main/java/dev/espi/protectionstones/commands/ArgPriority.java index 9b29d446..53f0873e 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgPriority.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgPriority.java @@ -18,6 +18,8 @@ import com.sk89q.worldguard.bukkit.WorldGuardPlugin; import dev.espi.protectionstones.PSL; import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.ProtectionStones; +import dev.espi.protectionstones.gui.screens.regions.RegionPriorityGui; import dev.espi.protectionstones.utils.WGUtils; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -67,9 +69,16 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap list your regions and teleport + if (args.length == 1) { + var cfg = ProtectionStones.getInstance().getConfigOptions(); + if (Boolean.TRUE.equals(cfg.guiEnabled) && Boolean.TRUE.equals(cfg.guiCommandTp)) { + var gm = ProtectionStones.getInstance().getGuiManager(); + gm.open(p, new LoadingGui(gm, "Teleport")); + + PSPlayer psp = PSPlayer.fromPlayer(p); + Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + List regions = psp.getPSRegionsCrossWorld(p.getWorld(), false); + Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> + gm.open(p, new RegionListGui(gm, RegionListGui.Mode.TELEPORT, "Teleport", regions, 0, null)) + ); + }); + return true; + } + } + if (args.length < 2 || args.length > 3) return PSL.msg(p, PSL.TP_HELP.msg()); @@ -127,7 +147,7 @@ public List tabComplete(CommandSender sender, String alias, String[] arg return null; } - static void teleportPlayer(Player p, PSRegion r) { + public static void teleportPlayer(Player p, PSRegion r) { if (r.getTypeOptions() == null) { PSL.msg(p, ChatColor.RED + "This region is problematic, and the block type (" + r.getType() + ") is not configured. Please contact an administrator."); Bukkit.getLogger().info(ChatColor.RED + "This region is problematic, and the block type (" + r.getType() + ") is not configured."); diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java b/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java index cbc633f6..c480df6f 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java @@ -16,6 +16,9 @@ package dev.espi.protectionstones.commands; import dev.espi.protectionstones.*; +import dev.espi.protectionstones.gui.screens.common.LoadingGui; +import dev.espi.protectionstones.gui.screens.regions.RegionListGui; +import dev.espi.protectionstones.gui.screens.regions.UnclaimConfirmGui; import dev.espi.protectionstones.utils.TextGUI; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ComponentBuilder; @@ -65,6 +68,9 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap= 2) { // /ps unclaim [list|region-id] (unclaim remote region) if (!p.hasPermission("protectionstones.unclaim.remote")) { @@ -78,7 +84,12 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap regions = psp.getPSRegionsCrossWorld(psp.getPlayer().getWorld(), false); if (args[1].equalsIgnoreCase("list")) { - displayPSRegions(s, regions, args.length == 2 ? 0 : tryParseInt(args[2]) - 1); + if (guiMode) { + var gm = ProtectionStones.getInstance().getGuiManager(); + gm.open(p, new RegionListGui(gm, RegionListGui.Mode.UNCLAIM, "Unclaim", regions, 0, null)); + } else { + displayPSRegions(s, regions, args.length == 2 ? 0 : tryParseInt(args[2]) - 1); + } } else { for (PSRegion psr : regions) { if (psr.getId().equalsIgnoreCase(args[1])) { @@ -87,6 +98,11 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap. + */ + +package dev.espi.protectionstones.gui; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; + +public abstract class BaseGui implements InventoryHolder { + protected final GuiManager gui; + protected final int size; // must be multiple of 9 + protected final String title; + + protected Inventory inv; + + protected BaseGui(GuiManager gui, int size, String title) { + this.gui = gui; + this.size = size; + this.title = title; + } + + public final void open(Player player) { + this.inv = Bukkit.createInventory(this, size, title); + draw(player); + gui.setOpenGui(player, this); + player.openInventory(inv); + } + + /** Populate inventory contents. Called each time the GUI opens. */ + protected abstract void draw(Player viewer); + + /** Called by listener. */ + protected abstract void onClick(Player viewer, InventoryClickEvent e); + + /** Called by listener. Optional. */ + protected void onClose(Player viewer, InventoryCloseEvent e) {} + + @Override + public final Inventory getInventory() { + return inv; + } +} + diff --git a/src/main/java/dev/espi/protectionstones/gui/Gui.java b/src/main/java/dev/espi/protectionstones/gui/Gui.java new file mode 100644 index 00000000..1e8d9a21 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/Gui.java @@ -0,0 +1,65 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.Inventory; + +/** + * Represents one GUI screen. + */ +public interface Gui { + + /** + * Create (or rebuild) the inventory for this GUI. + * Called right before opening. + */ + Inventory createInventory(Player player); + + /** + * Called after the inventory is opened. + */ + default void onOpen(Player player) {} + + /** + * Called when the player clicks in this GUI. + * Tip: cancel the event if you don't want item movement. + */ + default void onClick(Player player, InventoryClickEvent event) {} + + /** + * Called when the player drags items in this GUI. + */ + default void onDrag(Player player, InventoryDragEvent event) {} + + /** + * Called when this GUI is closed. + */ + default void onClose(Player player, InventoryCloseEvent event) {} + + /** + * If true, clicks are cancelled by default in GuiListener. + */ + default boolean cancelClicksByDefault() { return true; } + + /** + * If true, drags are cancelled by default in GuiListener. + */ + default boolean cancelDragsByDefault() { return true; } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/GuiItems.java b/src/main/java/dev/espi/protectionstones/gui/GuiItems.java new file mode 100644 index 00000000..7d9b850a --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/GuiItems.java @@ -0,0 +1,82 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** Small helper for building GUI items consistently. */ +public final class GuiItems { + private GuiItems() {} + + public static ItemStack item(Material material, String name, List lore) { + ItemStack is = new ItemStack(material); + ItemMeta im = is.getItemMeta(); + if (im != null) { + if (name != null) im.setDisplayName(name); + if (lore != null) { + List colored = new ArrayList<>(lore.size()); + for (String line : lore) colored.add(color(line)); + im.setLore(colored); + } + im.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_DESTROYS, ItemFlag.HIDE_PLACED_ON); + is.setItemMeta(im); + } + return is; + } + + public static ItemStack item(Material material, String name, String... lore) { + List l = null; + if (lore != null) { + l = new ArrayList<>(); + for (String s : lore) l.add(color(s)); + } + return item(material, name == null ? null : color(name), l); + } + + + public static ItemStack playerHead(java.util.UUID uuid, String name, String... lore) { + ItemStack is = new ItemStack(Material.PLAYER_HEAD); + ItemMeta im = is.getItemMeta(); + if (im instanceof org.bukkit.inventory.meta.SkullMeta sm) { + try { + sm.setOwningPlayer(org.bukkit.Bukkit.getOfflinePlayer(uuid)); + } catch (Exception ignored) {} + if (name != null) sm.setDisplayName(color(name)); + if (lore != null) { + List l = new ArrayList<>(); + for (String s : lore) l.add(color(s)); + sm.setLore(l); + } + sm.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_DESTROYS, ItemFlag.HIDE_PLACED_ON); + is.setItemMeta(sm); + return is; + } + // fallback + return item(Material.PLAYER_HEAD, name, lore); + } + + public static String color(String s) { + if (s == null) return null; + return ChatColor.translateAlternateColorCodes('&', s); + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/GuiListener.java b/src/main/java/dev/espi/protectionstones/gui/GuiListener.java new file mode 100644 index 00000000..e65a7600 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/GuiListener.java @@ -0,0 +1,78 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public final class GuiListener implements Listener { + private final GuiManager gui; + + GuiListener(GuiManager gui) { + this.gui = gui; + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onClick(InventoryClickEvent e) { + if (!(e.getWhoClicked() instanceof Player p)) return; + + var top = e.getView().getTopInventory(); + if (top == null) return; + if (!(top.getHolder() instanceof BaseGui g)) return; + + // Only handle clicks in our GUI (top inventory) + if (e.getClickedInventory() == null || e.getClickedInventory() != top) { + e.setCancelled(true); + return; + } + + e.setCancelled(true); + g.onClick(p, e); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onDrag(InventoryDragEvent e) { + var top = e.getView().getTopInventory(); + if (top == null) return; + if (!(top.getHolder() instanceof BaseGui)) return; + + // prevent dragging into GUI + e.setCancelled(true); + } + + @EventHandler + public void onClose(InventoryCloseEvent e) { + if (!(e.getPlayer() instanceof Player p)) return; + + var top = e.getView().getTopInventory(); + if (top == null) return; + if (!(top.getHolder() instanceof BaseGui g)) return; + + gui.setOpenGui(p, null); + g.onClose(p, e); + } + + @EventHandler + public void onQuit(PlayerQuitEvent e) { + gui.setOpenGui(e.getPlayer(), null); + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/GuiManager.java b/src/main/java/dev/espi/protectionstones/gui/GuiManager.java new file mode 100644 index 00000000..c30004bf --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/GuiManager.java @@ -0,0 +1,67 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.plugin.Plugin; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class GuiManager { + private final Plugin plugin; + private final GuiListener listener; + + // Track currently open GUI per player + private final Map openGuis = new ConcurrentHashMap<>(); + + public GuiManager(Plugin plugin) { + this.plugin = plugin; + this.listener = new GuiListener(this); + } + + public Plugin plugin() { return plugin; } + + public void register() { + Bukkit.getPluginManager().registerEvents(listener, plugin); + } + + public void unregister() { + HandlerList.unregisterAll(listener); + openGuis.clear(); + } + + void setOpenGui(Player player, BaseGui gui) { + if (gui == null) openGuis.remove(player.getUniqueId()); + else openGuis.put(player.getUniqueId(), gui); + } + + public BaseGui getOpenGui(Player player) { + return openGuis.get(player.getUniqueId()); + } + + // Convenience + public void open(Player player, BaseGui gui) { + gui.open(player); + } + + public void close(Player player) { + player.closeInventory(); + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/GuiSession.java b/src/main/java/dev/espi/protectionstones/gui/GuiSession.java new file mode 100644 index 00000000..15d49c81 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/GuiSession.java @@ -0,0 +1,31 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui; + +import org.bukkit.inventory.Inventory; + +public final class GuiSession { + private final Gui gui; + private final Inventory inventory; + + public GuiSession(Gui gui, Inventory inventory) { + this.gui = gui; + this.inventory = inventory; + } + + public Gui getGui() { return gui; } + public Inventory getInventory() { return inventory; } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/PaginatedGui.java b/src/main/java/dev/espi/protectionstones/gui/PaginatedGui.java new file mode 100644 index 00000000..683055b5 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/PaginatedGui.java @@ -0,0 +1,40 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui; + +import org.bukkit.entity.Player; + +import java.util.List; + +public abstract class PaginatedGui extends BaseGui { + protected int page = 0; + + protected PaginatedGui(GuiManager gui, int size, String title) { + super(gui, size, title); + } + + /** Items for current viewer. */ + protected abstract List entries(Player viewer); + + /** Render a single entry into a slot. */ + protected abstract void renderEntry(Player viewer, int slot, T entry); + + /** Slots used for entries (e.g., 0-44 for a 54-size GUI). */ + protected int entrySlots() { return size - 9; } + + protected int prevButtonSlot() { return size - 9; } + protected int nextButtonSlot() { return size - 1; } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/admin/AdminMenuGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/admin/AdminMenuGui.java new file mode 100644 index 00000000..2aaf6be2 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/admin/AdminMenuGui.java @@ -0,0 +1,191 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.admin; + +import dev.espi.protectionstones.PSConfig; +import dev.espi.protectionstones.PSL; +import dev.espi.protectionstones.ProtectionStones; +import dev.espi.protectionstones.commands.ArgAdmin; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import dev.espi.protectionstones.gui.screens.common.ConfirmGui; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** Inventory menu for /ps admin. Executes selected actions after confirmation. */ +public class AdminMenuGui extends BaseGui { + + private record Action(Material icon, String title, List lore, String commandSuffix, String requiresHelp) {} + + public AdminMenuGui(GuiManager gui) { + super(gui, 45, ChatColor.DARK_GRAY + "Admin"); + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + // Top row border + for (int i = 0; i < 9; i++) inv.setItem(i, GuiItems.item(Material.BLACK_STAINED_GLASS_PANE, " ")); + + String base = ProtectionStones.getInstance().getConfigOptions().base_command; + List actions = new ArrayList<>(); + + // Runnable (no-arg) admin actions + actions.add(new Action(Material.PAPER, ChatColor.translateAlternateColorCodes('&',"&bVersion"), + List.of("&7Show plugin/server versions", "&8", "&7Runs:", "&f/" + base + " admin version"), + "version", null)); + + actions.add(new Action(Material.BOOK, ChatColor.translateAlternateColorCodes('&',"&eStats"), + List.of("&7Show PS region stats", "&8", "&7Runs:", "&f/" + base + " admin stats"), + "stats", null)); + + actions.add(new Action(Material.REDSTONE_TORCH, ChatColor.translateAlternateColorCodes('&',"&dToggle Debug"), + List.of("&7Toggle debug mode", "&8", "&7Runs:", "&f/" + base + " admin debug"), + "debug", null)); + + actions.add(new Action(Material.ANVIL, ChatColor.translateAlternateColorCodes('&',"&cFix Regions"), + List.of("&7Run legacy region fix/upgrade", "&8", "&cThis may modify region data.", "&8", "&7Runs:", "&f/" + base + " admin fixregions"), + "fixregions", null)); + + actions.add(new Action(Material.GOLD_INGOT, ChatColor.translateAlternateColorCodes('&',"&6Set Tax Autopayers"), + List.of("&7Set missing tax autopayers", "&8", "&7Runs:", "&f/" + base + " admin settaxautopayers"), + "settaxautopayers", null)); + + // Commands that require additional args (show help instead) + actions.add(new Action(Material.HOPPER, ChatColor.translateAlternateColorCodes('&',"&fCleanup"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getCleanupHelp())); + + actions.add(new Action(Material.NAME_TAG, ChatColor.translateAlternateColorCodes('&',"&fFlag"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getFlagHelp())); + + actions.add(new Action(Material.IRON_BLOCK, ChatColor.translateAlternateColorCodes('&',"&fChange Block"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getChangeBlockHelp())); + + actions.add(new Action(Material.BEACON, ChatColor.translateAlternateColorCodes('&',"&fChange Region Type"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getChangeRegionTypeHelp())); + + actions.add(new Action(Material.SLIME_BALL, ChatColor.translateAlternateColorCodes('&',"&fForce Merge"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getForceMergeHelp())); + + // Layout (simple grid) + int[] slots = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34 + }; + for (int i = 0; i < actions.size() && i < slots.length; i++) { + Action a = actions.get(i); + inv.setItem(slots[i], GuiItems.item(a.icon(), a.title(), a.lore())); + } + + inv.setItem(44, GuiItems.item(Material.BARRIER, "&cClose")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + if (raw == 44) { + gui.close(viewer); + return; + } + + String base = ProtectionStones.getInstance().getConfigOptions().base_command; + + // Map raw slot to action index using same slot array + int[] slots = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34 + }; + + int idx = -1; + for (int i = 0; i < slots.length; i++) { + if (slots[i] == raw) { idx = i; break; } + } + if (idx == -1) return; + + // Keep this list in sync with draw() + List actions = new ArrayList<>(); + actions.add(new Action(Material.PAPER, ChatColor.translateAlternateColorCodes('&',"&bVersion"), + List.of("&7Show plugin/server versions", "&8", "&7Runs:", "&f/" + base + " admin version"), + "version", null)); + actions.add(new Action(Material.BOOK, ChatColor.translateAlternateColorCodes('&',"&eStats"), + List.of("&7Show PS region stats", "&8", "&7Runs:", "&f/" + base + " admin stats"), + "stats", null)); + actions.add(new Action(Material.REDSTONE_TORCH, ChatColor.translateAlternateColorCodes('&',"&dToggle Debug"), + List.of("&7Toggle debug mode", "&8", "&7Runs:", "&f/" + base + " admin debug"), + "debug", null)); + actions.add(new Action(Material.ANVIL, ChatColor.translateAlternateColorCodes('&',"&cFix Regions"), + List.of("&7Run legacy region fix/upgrade", "&8", "&cThis may modify region data.", "&8", "&7Runs:", "&f/" + base + " admin fixregions"), + "fixregions", null)); + actions.add(new Action(Material.GOLD_INGOT, ChatColor.translateAlternateColorCodes('&',"&6Set Tax Autopayers"), + List.of("&7Set missing tax autopayers", "&8", "&7Runs:", "&f/" + base + " admin settaxautopayers"), + "settaxautopayers", null)); + actions.add(new Action(Material.HOPPER, ChatColor.translateAlternateColorCodes('&',"&fCleanup"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getCleanupHelp())); + actions.add(new Action(Material.NAME_TAG, ChatColor.translateAlternateColorCodes('&',"&fFlag"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getFlagHelp())); + actions.add(new Action(Material.IRON_BLOCK, ChatColor.translateAlternateColorCodes('&',"&fChange Block"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getChangeBlockHelp())); + actions.add(new Action(Material.BEACON, ChatColor.translateAlternateColorCodes('&',"&fChange Region Type"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getChangeRegionTypeHelp())); + actions.add(new Action(Material.SLIME_BALL, ChatColor.translateAlternateColorCodes('&',"&fForce Merge"), + List.of("&7Requires arguments", "&8", "&7Click to show usage"), + null, ArgAdmin.getForceMergeHelp())); + + if (idx >= actions.size()) return; + Action a = actions.get(idx); + + if (a.requiresHelp() != null) { + PSL.msg(viewer, a.requiresHelp()); + return; + } + + if (a.commandSuffix() == null) return; + + String full = base + " admin " + a.commandSuffix(); + + Supplier back = () -> new AdminMenuGui(gui); + gui.open(viewer, new ConfirmGui( + gui, + "Confirm Admin", + a.icon(), + "&cRun: &f/" + full, + new String[]{"&7Are you sure?", "&8", "&7This will execute an admin command."}, + p -> Bukkit.dispatchCommand(p, full), + back, + back + )); + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/common/ConfirmGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/common/ConfirmGui.java new file mode 100644 index 00000000..9c4c2a6d --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/common/ConfirmGui.java @@ -0,0 +1,99 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.common; + +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** Generic confirmation GUI for running an action. */ +public class ConfirmGui extends BaseGui { + + private final Material icon; + private final String name; + private final String[] lore; + private final Consumer onConfirm; + private final Supplier back; + private final Supplier afterConfirm; + + public ConfirmGui( + GuiManager gui, + String title, + Material icon, + String name, + String[] lore, + Consumer onConfirm, + Supplier back, + Supplier afterConfirm + ) { + super(gui, 27, ChatColor.DARK_GRAY + title); + this.icon = icon == null ? Material.PAPER : icon; + this.name = name; + this.lore = lore; + this.onConfirm = onConfirm; + this.back = back; + this.afterConfirm = afterConfirm; + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + inv.setItem(13, GuiItems.item(icon, name, lore)); + inv.setItem(11, GuiItems.item(Material.LIME_WOOL, "&aConfirm")); + inv.setItem(15, GuiItems.item(Material.RED_WOOL, "&cCancel")); + + if (back != null) inv.setItem(18, GuiItems.item(Material.ARROW, "&bBack")); + inv.setItem(22, GuiItems.item(Material.BARRIER, "&cClose")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + + if (raw == 22) { + gui.close(viewer); + return; + } + + if (raw == 18 && back != null) { + gui.open(viewer, back.get()); + return; + } + + if (raw == 15) { + if (back != null) gui.open(viewer, back.get()); + else gui.close(viewer); + return; + } + + if (raw != 11) return; + + try { + if (onConfirm != null) onConfirm.accept(viewer); + } finally { + if (afterConfirm != null) gui.open(viewer, afterConfirm.get()); + else gui.close(viewer); + } + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/common/LoadingGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/common/LoadingGui.java new file mode 100644 index 00000000..2d7b5cf7 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/common/LoadingGui.java @@ -0,0 +1,46 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.common; + +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +/** Simple loading screen GUI used while running async region lookups. */ +public class LoadingGui extends BaseGui { + + public LoadingGui(GuiManager gui, String title) { + super(gui, 27, ChatColor.DARK_GRAY + title); + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + inv.setItem(13, GuiItems.item(Material.CLOCK, "&eLoading...", "&7Please wait")); + inv.setItem(22, GuiItems.item(Material.BARRIER, "&cClose")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + if (e.getRawSlot() == 22) { + gui.close(viewer); + } + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/flags/FlagsGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/flags/FlagsGui.java new file mode 100644 index 00000000..9c0cf24a --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/flags/FlagsGui.java @@ -0,0 +1,203 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.flags; + +import com.sk89q.worldguard.protection.flags.BooleanFlag; +import com.sk89q.worldguard.protection.flags.Flag; +import com.sk89q.worldguard.protection.flags.Flags; +import com.sk89q.worldguard.protection.flags.StateFlag; +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.PSL; +import dev.espi.protectionstones.ProtectionStones; +import dev.espi.protectionstones.commands.ArgFlag; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import dev.espi.protectionstones.utils.WGUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** /ps flag (GUI mode): basic inventory GUI for toggling common flag types. */ +public class FlagsGui extends BaseGui { + + private static final int SIZE = 54; + private static final int PER_PAGE = 45; + + private final UUID worldId; + private final String regionId; + private final int page; + + public FlagsGui(GuiManager gui, UUID worldId, String regionId, int page) { + super(gui, SIZE, ChatColor.DARK_GRAY + "Flags"); + this.worldId = worldId; + this.regionId = regionId; + this.page = Math.max(0, page); + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + PSRegion r = resolveRegion(); + if (r == null) { + inv.setItem(22, GuiItems.item(Material.BARRIER, "&cRegion not found", "&7Move back into the region and reopen /ps flag")); + inv.setItem(SIZE - 5, GuiItems.item(Material.BARRIER, "&cClose")); + return; + } + + // Title with region name/id + String titleLine = (r.getName() == null ? r.getId() : (r.getName() + " (" + r.getId() + ")")); + inv.setItem(SIZE - 5, GuiItems.item(Material.NAME_TAG, "&b" + titleLine, "&7Click flags above to change")); + + List allowed = new ArrayList<>(r.getTypeOptions().allowedFlags.keySet()); + + int start = page * PER_PAGE; + int end = Math.min(allowed.size(), start + PER_PAGE); + + int slot = 0; + for (int i = start; i < end; i++) { + String flagName = allowed.get(i); + Flag f = Flags.fuzzyMatchFlag(WGUtils.getFlagRegistry(), flagName); + if (f == null) continue; + + Object value = r.getWGRegion().getFlag(f); + Material icon = iconForFlag(f, value); + + List lore = new ArrayList<>(); + lore.add("&7Current: &f" + (value == null ? "none" : value.toString())); + lore.add("&8"); + if (f instanceof StateFlag) { + lore.add("&eClick: cycle Allow/Deny/None"); + } else if (f instanceof BooleanFlag) { + lore.add("&eClick: cycle True/False/None"); + } else { + lore.add("&eClick: print command help"); + } + lore.add("&8Perm: protectionstones.flags.edit." + flagName); + + inv.setItem(slot++, GuiItems.item(icon, ChatColor.AQUA + flagName, lore)); + } + + int base = SIZE - 9; + boolean hasPrev = page > 0; + boolean hasNext = end < allowed.size(); + if (hasPrev) inv.setItem(base, GuiItems.item(Material.ARROW, "&aPrevious", "&7Page " + page)); + inv.setItem(base + 4, GuiItems.item(Material.BARRIER, "&cClose")); + if (hasNext) inv.setItem(SIZE - 1, GuiItems.item(Material.ARROW, "&aNext", "&7Page " + (page + 2))); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + + // close + if (raw == (SIZE - 9) + 4) { + gui.close(viewer); + return; + } + + // prev + if (raw == (SIZE - 9) && page > 0) { + gui.open(viewer, new FlagsGui(gui, worldId, regionId, page - 1)); + return; + } + // next + if (raw == (SIZE - 1)) { + gui.open(viewer, new FlagsGui(gui, worldId, regionId, page + 1)); + return; + } + + if (raw >= PER_PAGE) return; + + PSRegion r = resolveRegion(); + if (r == null) { + PSL.msg(viewer, "&cRegion not found."); + gui.close(viewer); + return; + } + + List allowed = new ArrayList<>(r.getTypeOptions().allowedFlags.keySet()); + int index = page * PER_PAGE + raw; + if (index < 0 || index >= allowed.size()) return; + + String flagName = allowed.get(index); + + if (!viewer.hasPermission("protectionstones.flags.edit." + flagName)) { + PSL.msg(viewer, PSL.NO_PERMISSION_PER_FLAG.msg()); + return; + } + + Flag f = Flags.fuzzyMatchFlag(WGUtils.getFlagRegistry(), flagName); + if (f == null) return; + + Object value = r.getWGRegion().getFlag(f); + + if (f instanceof StateFlag) { + String next; + if (value == StateFlag.State.ALLOW) next = "deny"; + else if (value == StateFlag.State.DENY) next = "none"; + else next = "allow"; + ArgFlag.setFlag(r, viewer, flagName, next, "all"); + } else if (f instanceof BooleanFlag) { + String next; + if (Boolean.TRUE.equals(value)) next = "false"; + else if (Boolean.FALSE.equals(value)) next = "none"; + else next = "true"; + ArgFlag.setFlag(r, viewer, flagName, next, "all"); + } else { + // Non-simple flags: just tell them the command to use + String base = ProtectionStones.getInstance().getConfigOptions().base_command; + viewer.sendMessage(ChatColor.GRAY + "Use: /" + base + " flag " + flagName + " "); + } + + // refresh + gui.open(viewer, new FlagsGui(gui, worldId, regionId, page)); + } + + private PSRegion resolveRegion() { + World w = Bukkit.getWorld(worldId); + if (w == null) return null; + RegionManager rm = WGUtils.getRegionManagerWithWorld(w); + if (rm == null) return null; + ProtectedRegion pr = rm.getRegion(regionId); + if (pr == null) return null; + return PSRegion.fromWGRegion(w, pr); + } + + private Material iconForFlag(Flag f, Object value) { + if (f instanceof StateFlag) { + if (value == StateFlag.State.ALLOW) return Material.LIME_DYE; + if (value == StateFlag.State.DENY) return Material.RED_DYE; + return Material.GRAY_DYE; + } + if (f instanceof BooleanFlag) { + if (Boolean.TRUE.equals(value)) return Material.LIME_DYE; + if (Boolean.FALSE.equals(value)) return Material.RED_DYE; + return Material.GRAY_DYE; + } + return Material.PAPER; + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/home/HomeListGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/home/HomeListGui.java new file mode 100644 index 00000000..ad9d0c95 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/home/HomeListGui.java @@ -0,0 +1,114 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.home; + +import dev.espi.protectionstones.PSPlayer; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.commands.ArgTp; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.List; + +/** World -> homes list. */ +public class HomeListGui extends BaseGui { + + private static final int SIZE = 54; + private static final int PER_PAGE = 45; + + private final World world; + private final int page; + + public HomeListGui(GuiManager gui, World world, int page) { + super(gui, SIZE, ChatColor.DARK_GRAY + "Homes - " + world.getName()); + this.world = world; + this.page = Math.max(0, page); + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + PSPlayer psp = PSPlayer.fromPlayer(viewer); + List homes = psp.getHomes(world); + + int start = page * PER_PAGE; + int end = Math.min(homes.size(), start + PER_PAGE); + + int slot = 0; + for (int i = start; i < end; i++) { + PSRegion r = homes.get(i); + String display = (r.getName() == null ? r.getId() : (r.getName() + " (" + r.getId() + ")")); + inv.setItem(slot++, GuiItems.item( + Material.COMPASS, + "&b" + display, + "&7World: &f" + world.getName(), + "&8Click to teleport" + )); + } + + int base = SIZE - 9; + boolean hasPrev = page > 0; + boolean hasNext = end < homes.size(); + + if (hasPrev) inv.setItem(base, GuiItems.item(Material.ARROW, "&aPrevious", "&7Page " + page)); + inv.setItem(base + 3, GuiItems.item(Material.OAK_DOOR, "&eBack", "&7Back to worlds")); + inv.setItem(base + 4, GuiItems.item(Material.BARRIER, "&cClose")); + if (hasNext) inv.setItem(SIZE - 1, GuiItems.item(Material.ARROW, "&aNext", "&7Page " + (page + 2))); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + + // nav row + int base = SIZE - 9; + if (raw == base + 4) { + gui.close(viewer); + return; + } + if (raw == base + 3) { + gui.open(viewer, new HomeWorldsGui(gui, 0)); + return; + } + if (raw == base && page > 0) { + gui.open(viewer, new HomeListGui(gui, world, page - 1)); + return; + } + if (raw == SIZE - 1) { + gui.open(viewer, new HomeListGui(gui, world, page + 1)); + return; + } + + // entry click + if (raw < PER_PAGE) { + PSPlayer psp = PSPlayer.fromPlayer(viewer); + List homes = psp.getHomes(world); + + int index = page * PER_PAGE + raw; + if (index < 0 || index >= homes.size()) return; + + ArgTp.teleportPlayer(viewer, homes.get(index)); + gui.close(viewer); + } + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/home/HomeWorldsGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/home/HomeWorldsGui.java new file mode 100644 index 00000000..98e008af --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/home/HomeWorldsGui.java @@ -0,0 +1,128 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.home; + +import dev.espi.protectionstones.PSPlayer; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.ProtectionStones; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** /ps home (GUI mode): select a world, then select a home within that world. */ +public class HomeWorldsGui extends BaseGui { + + private static final int SIZE = 54; // 6 rows + private static final int PER_PAGE = 45; // top 5 rows + + private final int page; + + public HomeWorldsGui(GuiManager gui, int page) { + super(gui, SIZE, ChatColor.DARK_GRAY + "Homes - Select World"); + this.page = Math.max(0, page); + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + PSPlayer psp = PSPlayer.fromPlayer(viewer); + + List worlds = new ArrayList<>(Bukkit.getWorlds()); + worlds.sort(Comparator.comparing(World::getName, String.CASE_INSENSITIVE_ORDER)); + + int start = page * PER_PAGE; + int end = Math.min(worlds.size(), start + PER_PAGE); + + // entries + int slot = 0; + for (int i = start; i < end; i++) { + World w = worlds.get(i); + List homes = psp.getHomes(w); + Material icon = worldIcon(w); + inv.setItem(slot++, GuiItems.item( + icon, + "&b" + w.getName(), + "&7Homes: &f" + homes.size(), + "&8Click to view homes" + )); + } + + // nav row + int base = SIZE - 9; + boolean hasPrev = page > 0; + boolean hasNext = end < worlds.size(); + + if (hasPrev) inv.setItem(base, GuiItems.item(Material.ARROW, "&aPrevious", "&7Page " + page)); + inv.setItem(base + 4, GuiItems.item(Material.BARRIER, "&cClose")); + if (hasNext) inv.setItem(SIZE - 1, GuiItems.item(Material.ARROW, "&aNext", "&7Page " + (page + 2))); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + + // close + if (raw == (SIZE - 9) + 4) { + gui.close(viewer); + return; + } + + // prev + if (raw == (SIZE - 9) && page > 0) { + gui.open(viewer, new HomeWorldsGui(gui, page - 1)); + return; + } + + // next + if (raw == (SIZE - 1)) { + gui.open(viewer, new HomeWorldsGui(gui, page + 1)); + return; + } + + // entry click + if (raw < PER_PAGE) { + int worldIndex = page * PER_PAGE + raw; + List worlds = new ArrayList<>(Bukkit.getWorlds()); + worlds.sort(Comparator.comparing(World::getName, String.CASE_INSENSITIVE_ORDER)); + if (worldIndex < 0 || worldIndex >= worlds.size()) return; + + World w = worlds.get(worldIndex); + gui.open(viewer, new HomeListGui(gui, w, 0)); + } + } + + private Material worldIcon(World w) { + switch (w.getEnvironment()) { + case NETHER: + return Material.NETHERRACK; + case THE_END: + return Material.END_STONE; + default: + return Material.GRASS_BLOCK; + } + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/members/RegionPlayerSelectGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/members/RegionPlayerSelectGui.java new file mode 100644 index 00000000..2c95c908 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/members/RegionPlayerSelectGui.java @@ -0,0 +1,270 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.members; + +import dev.espi.protectionstones.PSPlayer; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.PSL; +import dev.espi.protectionstones.ProtectionStones; +import dev.espi.protectionstones.commands.ArgAddRemove; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import dev.espi.protectionstones.utils.UUIDCache; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Player selection GUI for /ps add, /ps remove, /ps addowner, /ps removeowner. + * - add/addowner: lists currently online players (visible to viewer) + * - remove/removeowner: lists players currently in the region (members/owners) + */ +public class RegionPlayerSelectGui extends BaseGui { + + public enum Mode { + ADD_MEMBER, + REMOVE_MEMBER, + ADD_OWNER, + REMOVE_OWNER + } + + private static final int SIZE = 54; // 6 rows + private static final int PER_PAGE = 45; // top 5 rows + + private final PSRegion region; + private final Mode mode; + private final int page; + + public RegionPlayerSelectGui(GuiManager gui, PSRegion region, Mode mode, int page) { + super(gui, SIZE, titleFor(region, mode)); + this.region = region; + this.mode = mode; + this.page = Math.max(0, page); + } + + private static String titleFor(PSRegion r, Mode m) { + String base = switch (m) { + case ADD_MEMBER -> "Add Member"; + case REMOVE_MEMBER -> "Remove Member"; + case ADD_OWNER -> "Add Owner"; + case REMOVE_OWNER -> "Remove Owner"; + }; + String rn = (r.getName() == null ? r.getId() : r.getName()); + return ChatColor.DARK_GRAY + base + " - " + rn; + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + List entries = getEntries(viewer); + + int start = page * PER_PAGE; + int end = Math.min(entries.size(), start + PER_PAGE); + + int slot = 0; + for (int i = start; i < end; i++) { + UUID uuid = entries.get(i); + String name = safeName(uuid); + + boolean isSelf = uuid.equals(viewer.getUniqueId()); + + // icon + lore + Material fallback = Material.NAME_TAG; + + if (mode == Mode.ADD_MEMBER) { + inv.setItem(slot++, GuiItems.playerHead(uuid, "&b" + name, + "&7Click to add as &fMember", + isSelf ? "&8(You)" : "&8")); + } else if (mode == Mode.ADD_OWNER) { + inv.setItem(slot++, GuiItems.playerHead(uuid, "&b" + name, + "&7Click to add as &fOwner", + isSelf ? "&8(You)" : "&8")); + } else if (mode == Mode.REMOVE_MEMBER) { + inv.setItem(slot++, GuiItems.playerHead(uuid, "&b" + name, + "&7Click to remove from &fMembers", + isSelf ? "&8(You)" : "&8")); + } else { // REMOVE_OWNER + int owners = region.getOwners().size(); + boolean lastOwnerSelf = isSelf && owners <= 1; + inv.setItem(slot++, GuiItems.playerHead(uuid, "&b" + name, + lastOwnerSelf ? "&cYou are the last owner" : "&7Click to remove from &fOwners", + lastOwnerSelf ? "&cCannot remove last owner" : "&8")); + } + } + + // nav row + int base = SIZE - 9; + boolean hasPrev = page > 0; + boolean hasNext = end < entries.size(); + + if (hasPrev) inv.setItem(base, GuiItems.item(Material.ARROW, "&aPrevious", "&7Page " + page)); + inv.setItem(base + 3, GuiItems.item(Material.PAPER, "&eRefresh")); + inv.setItem(base + 4, GuiItems.item(Material.BARRIER, "&cClose")); + if (hasNext) inv.setItem(SIZE - 1, GuiItems.item(Material.ARROW, "&aNext", "&7Page " + (page + 2))); + + // footer hint + inv.setItem(base + 8, GuiItems.item(Material.GRAY_STAINED_GLASS_PANE, "&7", + "&8Select a player")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + int base = SIZE - 9; + + // close + if (raw == base + 4) { + gui.close(viewer); + return; + } + + // refresh + if (raw == base + 3) { + gui.open(viewer, new RegionPlayerSelectGui(gui, region, mode, page)); + return; + } + + // prev + if (raw == base && page > 0) { + gui.open(viewer, new RegionPlayerSelectGui(gui, region, mode, page - 1)); + return; + } + + // next + if (raw == SIZE - 1) { + gui.open(viewer, new RegionPlayerSelectGui(gui, region, mode, page + 1)); + return; + } + + if (raw >= PER_PAGE) return; + + List entries = getEntries(viewer); + int idx = page * PER_PAGE + raw; + if (idx < 0 || idx >= entries.size()) return; + + UUID target = entries.get(idx); + + // Safety checks + if (mode == Mode.REMOVE_OWNER) { + if (target.equals(viewer.getUniqueId()) && region.getOwners().size() <= 1) { + PSL.msg(viewer, PSL.CANNOT_REMOVE_YOURSELF_LAST_OWNER.msg()); + return; + } + } + + // Apply (async like original command implementation) + Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + String name = safeName(target); + + switch (mode) { + case ADD_MEMBER -> { + if (region.isMember(target) || region.isOwner(target)) { + PSL.msg(viewer, "&e" + name + " &7is already added."); + return; + } + region.addMember(target); + PSL.msg(viewer, PSL.ADDED_TO_REGION.msg().replace("%player%", name)); + Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> UUIDCache.storeWGProfile(target, name)); + } + case REMOVE_MEMBER -> { + if (!region.isMember(target)) { + PSL.msg(viewer, "&e" + name + " &7is not a member."); + return; + } + region.removeMember(target); + PSL.msg(viewer, PSL.REMOVED_FROM_REGION.msg().replace("%player%", name)); + } + case ADD_OWNER -> { + if (region.isOwner(target)) { + PSL.msg(viewer, "&e" + name + " &7is already an owner."); + return; + } + // limit checks (reuse existing logic) + ArgAddRemove helper = new ArgAddRemove(); + if (helper.determinePlayerSurpassedLimit(viewer, Collections.singletonList(region), PSPlayer.fromUUID(target))) { + return; + } + region.addOwner(target); + PSL.msg(viewer, PSL.ADDED_TO_REGION.msg().replace("%player%", name)); + Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> UUIDCache.storeWGProfile(target, name)); + } + case REMOVE_OWNER -> { + if (!region.isOwner(target)) { + PSL.msg(viewer, "&e" + name + " &7is not an owner."); + return; + } + region.removeOwner(target); + PSL.msg(viewer, PSL.REMOVED_FROM_REGION.msg().replace("%player%", name)); + } + } + + // refresh GUI back on main thread + Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> gui.open(viewer, new RegionPlayerSelectGui(gui, region, mode, page))); + }); + } + + private List getEntries(Player viewer) { + if (mode == Mode.ADD_MEMBER || mode == Mode.ADD_OWNER) { + // online players only (visible) + List list = new ArrayList<>(); + for (Player p : Bukkit.getOnlinePlayers()) { + if (!viewer.canSee(p)) continue; + list.add(p.getUniqueId()); + } + + // filter by already-added status + if (mode == Mode.ADD_MEMBER) { + list = list.stream() + .filter(u -> !region.isMember(u) && !region.isOwner(u)) + .collect(Collectors.toList()); + } else { + list = list.stream() + .filter(u -> !region.isOwner(u)) + .collect(Collectors.toList()); + } + + // sort by name + list.sort(Comparator.comparing(this::safeName, String.CASE_INSENSITIVE_ORDER)); + return list; + } + + // remove modes: list members/owners currently on region + // Note: region.getMembers() and region.getOwners() are not guaranteed to return the same concrete type. + // Use Collection to avoid conditional type mismatch. + Collection uuids = (mode == Mode.REMOVE_MEMBER) ? region.getMembers() : region.getOwners(); + List list = new ArrayList<>(uuids); + list.sort(Comparator.comparing(this::safeName, String.CASE_INSENSITIVE_ORDER)); + return list; + } + + private String safeName(UUID uuid) { + String n = UUIDCache.getNameFromUUID(uuid); + if (n == null || n.isEmpty() || n.equalsIgnoreCase("null")) { + try { + n = Bukkit.getOfflinePlayer(uuid).getName(); + } catch (Exception ignored) {} + } + if (n == null || n.isEmpty()) n = uuid.toString().substring(0, 8); + return n; + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionActions.java b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionActions.java new file mode 100644 index 00000000..6304591c --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionActions.java @@ -0,0 +1,75 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.regions; + +import dev.espi.protectionstones.PSGroupRegion; +import dev.espi.protectionstones.PSProtectBlock; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.PSL; +import dev.espi.protectionstones.ProtectionStones; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** Shared region actions used by inventory GUIs. */ +public final class RegionActions { + private RegionActions() {} + + /** + * Unclaims a region (returns protection stone items if configured, then deletes the region). + * This mirrors the behavior in ArgUnclaim. + */ + public static boolean unclaimRegion(PSRegion r, Player p) { + PSProtectBlock cpb = r.getTypeOptions(); + if (cpb != null && !cpb.noDrop) { + // return protection stone(s) + List items = new ArrayList<>(); + + if (r instanceof PSGroupRegion) { + for (PSRegion rp : ((PSGroupRegion) r).getMergedRegions()) { + if (rp.getTypeOptions() != null) items.add(rp.getTypeOptions().createItem()); + } + } else { + items.add(cpb.createItem()); + } + + for (ItemStack item : items) { + if (!p.getInventory().addItem(item).isEmpty()) { + if (ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull) { + PSL.msg(p, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); + p.getWorld().dropItem(p.getLocation(), item); + } else { + PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); + return true; + } + } + } + } + + // remove region (respect the same safeguards) + if (!r.deleteRegion(true, p)) { + if (!ProtectionStones.getInstance().getConfigOptions().allowMergingHoles) { + PSL.msg(p, PSL.DELETE_REGION_PREVENTED_NO_HOLES.msg()); + } + return true; + } + + PSL.msg(p, PSL.NO_LONGER_PROTECTED.msg()); + return true; + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionInfoGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionInfoGui.java new file mode 100644 index 00000000..6b9fde3a --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionInfoGui.java @@ -0,0 +1,185 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.regions; + +import com.sk89q.worldguard.bukkit.WorldGuardPlugin; +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.PSL; +import dev.espi.protectionstones.ProtectionStones; +import dev.espi.protectionstones.commands.ArgTp; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import dev.espi.protectionstones.gui.screens.flags.FlagsGui; +import dev.espi.protectionstones.gui.screens.members.RegionPlayerSelectGui; +import dev.espi.protectionstones.utils.UUIDCache; +import dev.espi.protectionstones.utils.WGUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; + +/** + * Region information GUI, with quick actions (members/owners/flags/priority/unclaim/teleport). + */ +public class RegionInfoGui extends BaseGui { + + private static final int SIZE = 36; + + private final UUID worldId; + private final String regionId; + private final Supplier back; + + public RegionInfoGui(GuiManager gui, UUID worldId, String regionId, Supplier back) { + super(gui, SIZE, ChatColor.DARK_GRAY + "Region Info"); + this.worldId = worldId; + this.regionId = regionId; + this.back = back; + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + PSRegion r = resolveRegion(); + if (r == null) { + inv.setItem(13, GuiItems.item(Material.BARRIER, "&cRegion not found")); + inv.setItem(31, GuiItems.item(Material.BARRIER, "&cClose")); + return; + } + + // access check (same behavior as text info) + if (!viewer.hasPermission("protectionstones.info.others") + && WGUtils.hasNoAccess(r.getWGRegion(), viewer, WorldGuardPlugin.inst().wrapPlayer(viewer), true)) { + inv.setItem(13, GuiItems.item(Material.BARRIER, "&cNo access", "&7You don't have permission")); + inv.setItem(31, GuiItems.item(Material.BARRIER, "&cClose")); + return; + } + + String display = r.getName() == null ? r.getId() : (r.getName() + " (" + r.getId() + ")"); + World w = r.getWorld(); + + List lore = new ArrayList<>(); + lore.add("&7World: &f" + (w == null ? "?" : w.getName())); + if (r.getTypeOptions() != null) lore.add("&7Type: &f" + r.getTypeOptions().alias); + lore.add("&7Priority: &f" + r.getWGRegion().getPriority()); + lore.add("&7Owners: &f" + r.getOwners().size() + " &7Members: &f" + r.getMembers().size()); + if (r.isHidden()) lore.add("&8Hidden"); + if (r.forSale()) lore.add("&eFor sale: &f" + String.format("%.2f", r.getPrice())); + if (r.getRentStage() != PSRegion.RentStage.NOT_RENTING) lore.add("&eRent: &f" + String.format("%.2f", r.getPrice())); + + inv.setItem(13, GuiItems.item(Material.NAME_TAG, ChatColor.AQUA + display, lore)); + + // actions + inv.setItem(10, GuiItems.item(Material.ENDER_PEARL, "&aTeleport", "&7Teleport to region home")); + inv.setItem(11, GuiItems.item(Material.PLAYER_HEAD, "&bMembers", "&7View members")); + inv.setItem(12, GuiItems.item(Material.GOLDEN_HELMET, "&6Owners", "&7View owners")); + inv.setItem(14, GuiItems.item(Material.COMPARATOR, "&dFlags", "&7Edit common flags")); + inv.setItem(15, GuiItems.item(Material.ANVIL, "&ePriority", "&7View/set priority")); + + boolean canUnclaim = viewer.hasPermission("protectionstones.unclaim") + && (r.isOwner(viewer.getUniqueId()) || viewer.hasPermission("protectionstones.superowner")); + if (canUnclaim) { + inv.setItem(16, GuiItems.item(Material.TNT, "&cUnclaim", "&7Unclaim this region")); + } else { + inv.setItem(16, GuiItems.item(Material.BARRIER, "&cUnclaim", "&8No permission")); + } + + // footer + if (back != null) inv.setItem(27, GuiItems.item(Material.ARROW, "&bBack")); + inv.setItem(31, GuiItems.item(Material.BARRIER, "&cClose")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + + if (raw == 31) { + gui.close(viewer); + return; + } + if (raw == 27 && back != null) { + gui.open(viewer, back.get()); + return; + } + + PSRegion r = resolveRegion(); + if (r == null) { + gui.close(viewer); + return; + } + + switch (raw) { + case 10 -> { + teleport(viewer, r); + gui.close(viewer); + } + case 11 -> gui.open(viewer, new RegionRosterGui(gui, worldId, regionId, RegionRosterGui.Mode.MEMBERS, 0, () -> new RegionInfoGui(gui, worldId, regionId, back))); + case 12 -> gui.open(viewer, new RegionRosterGui(gui, worldId, regionId, RegionRosterGui.Mode.OWNERS, 0, () -> new RegionInfoGui(gui, worldId, regionId, back))); + case 14 -> { + if (!viewer.hasPermission("protectionstones.flags")) { + PSL.msg(viewer, PSL.NO_PERMISSION_FLAGS.msg()); + return; + } + gui.open(viewer, new FlagsGui(gui, worldId, regionId, 0)); + } + case 15 -> { + if (!viewer.hasPermission("protectionstones.priority")) { + PSL.msg(viewer, PSL.NO_PERMISSION_PRIORITY.msg()); + return; + } + gui.open(viewer, new RegionPriorityGui(gui, worldId, regionId, () -> new RegionInfoGui(gui, worldId, regionId, back))); + } + case 16 -> { + boolean canUnclaim = viewer.hasPermission("protectionstones.unclaim") + && (r.isOwner(viewer.getUniqueId()) || viewer.hasPermission("protectionstones.superowner")); + if (!canUnclaim) { + PSL.msg(viewer, PSL.NO_REGION_PERMISSION.msg()); + return; + } + if (r.getRentStage() == PSRegion.RentStage.RENTING && !viewer.hasPermission("protectionstones.superowner")) { + PSL.msg(viewer, PSL.RENT_CANNOT_BREAK_WHILE_RENTING.msg()); + return; + } + gui.open(viewer, new UnclaimConfirmGui(gui, worldId, regionId, () -> new RegionInfoGui(gui, worldId, regionId, back))); + } + } + } + + public static void teleport(Player viewer, PSRegion r) { + // use existing tp behavior (delay / no-move rules etc) + ArgTp.teleportPlayer(viewer, r); + } + + private PSRegion resolveRegion() { + World w = Bukkit.getWorld(worldId); + if (w == null) return null; + RegionManager rm = WGUtils.getRegionManagerWithWorld(w); + if (rm == null) return null; + ProtectedRegion pr = rm.getRegion(regionId); + if (pr == null) return null; + return PSRegion.fromWGRegion(w, pr); + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionListGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionListGui.java new file mode 100644 index 00000000..7394c293 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionListGui.java @@ -0,0 +1,203 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.regions; + +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import dev.espi.protectionstones.utils.UUIDCache; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; + +/** + * Generic region list GUI used by /ps list, /ps tp (no args), and /ps unclaim list. + */ +public class RegionListGui extends BaseGui { + + public enum Mode { + LIST, + TELEPORT, + UNCLAIM + } + + private static final int SIZE = 54; + private static final int PER_PAGE = 45; + + private final Mode mode; + private final List regions; + private final int page; + private final Supplier back; + + public RegionListGui(GuiManager gui, Mode mode, String title, List regions, int page, Supplier back) { + super(gui, SIZE, ChatColor.DARK_GRAY + title); + this.mode = mode; + this.regions = regions == null ? new ArrayList<>() : new ArrayList<>(regions); + this.page = Math.max(0, page); + this.back = back; + + // stable order + this.regions.sort(Comparator.comparing(RegionListGui::displayName, String.CASE_INSENSITIVE_ORDER)); + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + int start = page * PER_PAGE; + int end = Math.min(regions.size(), start + PER_PAGE); + + int slot = 0; + for (int i = start; i < end; i++) { + PSRegion r = regions.get(i); + + ItemStack icon = iconFor(r); + ItemMeta im = icon.getItemMeta(); + if (im != null) { + im.setDisplayName(ChatColor.AQUA + displayName(r)); + + List lore = new ArrayList<>(); + lore.add(ChatColor.GRAY + "ID: " + ChatColor.WHITE + r.getId()); + + World w = r.getWorld(); + lore.add(ChatColor.GRAY + "World: " + ChatColor.WHITE + (w == null ? "?" : w.getName())); + + if (r.getTypeOptions() != null) { + lore.add(ChatColor.GRAY + "Type: " + ChatColor.WHITE + r.getTypeOptions().alias); + } + + int owners = safeSize(r.getOwners()); + int members = safeSize(r.getMembers()); + lore.add(ChatColor.GRAY + "Owners: " + ChatColor.WHITE + owners + ChatColor.GRAY + " Members: " + ChatColor.WHITE + members); + + if (r.isHidden()) { + lore.add(ChatColor.DARK_GRAY + "Hidden"); + } + + lore.add(ChatColor.DARK_GRAY + ""); + switch (mode) { + case LIST -> { + lore.add(ChatColor.YELLOW + "Left-click" + ChatColor.GRAY + " to open info"); + lore.add(ChatColor.YELLOW + "Shift-left" + ChatColor.GRAY + " to teleport"); + } + case TELEPORT -> lore.add(ChatColor.YELLOW + "Click" + ChatColor.GRAY + " to teleport"); + case UNCLAIM -> lore.add(ChatColor.RED + "Click" + ChatColor.GRAY + " to unclaim"); + } + + im.setLore(lore); + icon.setItemMeta(im); + } + + inv.setItem(slot++, icon); + } + + int base = SIZE - 9; + boolean hasPrev = page > 0; + boolean hasNext = end < regions.size(); + + if (hasPrev) inv.setItem(base, GuiItems.item(Material.ARROW, "&aPrevious", "&7Page " + page)); + if (back != null) inv.setItem(base + 3, GuiItems.item(Material.ARROW, "&bBack")); + inv.setItem(base + 4, GuiItems.item(Material.BARRIER, "&cClose")); + if (hasNext) inv.setItem(SIZE - 1, GuiItems.item(Material.ARROW, "&aNext", "&7Page " + (page + 2))); + + // footer + inv.setItem(base + 8, GuiItems.item(Material.GRAY_STAINED_GLASS_PANE, "&7", "&8" + regions.size() + " region(s)")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + int base = SIZE - 9; + + if (raw == base + 4) { + gui.close(viewer); + return; + } + if (raw == base + 3 && back != null) { + gui.open(viewer, back.get()); + return; + } + if (raw == base && page > 0) { + gui.open(viewer, new RegionListGui(gui, mode, stripColor(title), regions, page - 1, back)); + return; + } + if (raw == SIZE - 1) { + gui.open(viewer, new RegionListGui(gui, mode, stripColor(title), regions, page + 1, back)); + return; + } + + if (raw >= PER_PAGE) return; + + int idx = page * PER_PAGE + raw; + if (idx < 0 || idx >= regions.size()) return; + + PSRegion r = regions.get(idx); + + if (mode == Mode.LIST) { + if (e.isShiftClick()) { + RegionInfoGui.teleport(viewer, r); + return; + } + gui.open(viewer, new RegionInfoGui(gui, r.getWorld().getUID(), r.getId(), () -> new RegionListGui(gui, mode, stripColor(title), regions, page, back))); + return; + } + + if (mode == Mode.TELEPORT) { + RegionInfoGui.teleport(viewer, r); + gui.close(viewer); + return; + } + + // UNCLAIM + gui.open(viewer, new UnclaimConfirmGui(gui, r.getWorld().getUID(), r.getId(), () -> new RegionListGui(gui, mode, stripColor(title), regions, page, back))); + } + + private static String stripColor(String s) { + return ChatColor.stripColor(s == null ? "" : s); + } + + private ItemStack iconFor(PSRegion r) { + try { + if (r.getTypeOptions() != null) { + ItemStack is = r.getTypeOptions().createItem(); + if (is != null) return is; + } + } catch (Exception ignored) {} + return new ItemStack(Material.NAME_TAG); + } + + private static String displayName(PSRegion r) { + if (r == null) return "?"; + return r.getName() == null ? r.getId() : (r.getName() + " (" + r.getId() + ")"); + } + + private static int safeSize(Object maybeCollection) { + if (maybeCollection instanceof java.util.Collection c) return c.size(); + return 0; + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionPriorityGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionPriorityGui.java new file mode 100644 index 00000000..bff706a1 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionPriorityGui.java @@ -0,0 +1,115 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.regions; + +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.PSL; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import dev.espi.protectionstones.utils.WGUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.UUID; +import java.util.function.Supplier; + +/** Small GUI to view and adjust region priority. */ +public class RegionPriorityGui extends BaseGui { + + private final UUID worldId; + private final String regionId; + private final Supplier back; + + public RegionPriorityGui(GuiManager gui, UUID worldId, String regionId, Supplier back) { + super(gui, 27, ChatColor.DARK_GRAY + "Priority"); + this.worldId = worldId; + this.regionId = regionId; + this.back = back; + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + PSRegion r = resolveRegion(); + if (r == null) { + inv.setItem(13, GuiItems.item(Material.BARRIER, "&cRegion not found")); + inv.setItem(22, GuiItems.item(Material.BARRIER, "&cClose")); + return; + } + + int priority = r.getWGRegion().getPriority(); + + inv.setItem(13, GuiItems.item(Material.PAPER, "&eCurrent Priority", "&f" + priority, "&8Higher = takes precedence")); + + inv.setItem(10, GuiItems.item(Material.RED_DYE, "&c-10")); + inv.setItem(11, GuiItems.item(Material.RED_DYE, "&c-1")); + inv.setItem(15, GuiItems.item(Material.LIME_DYE, "&a+1")); + inv.setItem(16, GuiItems.item(Material.LIME_DYE, "&a+10")); + + inv.setItem(22, GuiItems.item(Material.BARRIER, "&cClose")); + if (back != null) inv.setItem(18, GuiItems.item(Material.ARROW, "&bBack")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + + if (raw == 22) { + gui.close(viewer); + return; + } + if (raw == 18 && back != null) { + gui.open(viewer, back.get()); + return; + } + + PSRegion r = resolveRegion(); + if (r == null) { + gui.close(viewer); + return; + } + + int delta = 0; + if (raw == 10) delta = -10; + else if (raw == 11) delta = -1; + else if (raw == 15) delta = 1; + else if (raw == 16) delta = 10; + else return; + + int newPriority = r.getWGRegion().getPriority() + delta; + r.getWGRegion().setPriority(newPriority); + PSL.msg(viewer, PSL.PRIORITY_SET.msg()); + + gui.open(viewer, new RegionPriorityGui(gui, worldId, regionId, back)); + } + + private PSRegion resolveRegion() { + World w = Bukkit.getWorld(worldId); + if (w == null) return null; + RegionManager rm = WGUtils.getRegionManagerWithWorld(w); + if (rm == null) return null; + ProtectedRegion pr = rm.getRegion(regionId); + if (pr == null) return null; + return PSRegion.fromWGRegion(w, pr); + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionRosterGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionRosterGui.java new file mode 100644 index 00000000..d6e187e2 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/regions/RegionRosterGui.java @@ -0,0 +1,141 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.regions; + +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import dev.espi.protectionstones.utils.UUIDCache; +import dev.espi.protectionstones.utils.WGUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; + +/** Read-only roster GUI for region members or owners. */ +public class RegionRosterGui extends BaseGui { + + public enum Mode { MEMBERS, OWNERS } + + private static final int SIZE = 54; + private static final int PER_PAGE = 45; + + private final UUID worldId; + private final String regionId; + private final Mode mode; + private final int page; + private final Supplier back; + + public RegionRosterGui(GuiManager gui, UUID worldId, String regionId, Mode mode, int page, Supplier back) { + super(gui, SIZE, ChatColor.DARK_GRAY + (mode == Mode.MEMBERS ? "Members" : "Owners")); + this.worldId = worldId; + this.regionId = regionId; + this.mode = mode; + this.page = Math.max(0, page); + this.back = back; + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + PSRegion r = resolveRegion(); + if (r == null) { + inv.setItem(22, GuiItems.item(Material.BARRIER, "&cRegion not found")); + inv.setItem(SIZE - 5, GuiItems.item(Material.BARRIER, "&cClose")); + return; + } + + Collection uuids = (mode == Mode.MEMBERS) ? r.getMembers() : r.getOwners(); + List entries = new ArrayList<>(uuids); + entries.sort(Comparator.comparing(this::safeName, String.CASE_INSENSITIVE_ORDER)); + + int start = page * PER_PAGE; + int end = Math.min(entries.size(), start + PER_PAGE); + + int slot = 0; + for (int i = start; i < end; i++) { + UUID u = entries.get(i); + String name = safeName(u); + inv.setItem(slot++, GuiItems.playerHead(u, "&b" + name, "&7" + u.toString())); + } + + int base = SIZE - 9; + boolean hasPrev = page > 0; + boolean hasNext = end < entries.size(); + if (hasPrev) inv.setItem(base, GuiItems.item(Material.ARROW, "&aPrevious", "&7Page " + page)); + if (back != null) inv.setItem(base + 3, GuiItems.item(Material.ARROW, "&bBack")); + inv.setItem(base + 4, GuiItems.item(Material.BARRIER, "&cClose")); + if (hasNext) inv.setItem(SIZE - 1, GuiItems.item(Material.ARROW, "&aNext", "&7Page " + (page + 2))); + + inv.setItem(base + 8, GuiItems.item(Material.GRAY_STAINED_GLASS_PANE, "&7", "&8" + entries.size() + " player(s)")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + int base = SIZE - 9; + + if (raw == base + 4) { + gui.close(viewer); + return; + } + if (raw == base + 3 && back != null) { + gui.open(viewer, back.get()); + return; + } + if (raw == base && page > 0) { + gui.open(viewer, new RegionRosterGui(gui, worldId, regionId, mode, page - 1, back)); + return; + } + if (raw == SIZE - 1) { + gui.open(viewer, new RegionRosterGui(gui, worldId, regionId, mode, page + 1, back)); + return; + } + } + + private PSRegion resolveRegion() { + World w = Bukkit.getWorld(worldId); + if (w == null) return null; + RegionManager rm = WGUtils.getRegionManagerWithWorld(w); + if (rm == null) return null; + ProtectedRegion pr = rm.getRegion(regionId); + if (pr == null) return null; + return PSRegion.fromWGRegion(w, pr); + } + + private String safeName(UUID uuid) { + String n = UUIDCache.getNameFromUUID(uuid); + if (n == null || n.isEmpty() || n.equalsIgnoreCase("null")) { + try { + n = Bukkit.getOfflinePlayer(uuid).getName(); + } catch (Exception ignored) {} + } + if (n == null || n.isEmpty()) n = uuid.toString().substring(0, 8); + return n; + } +} diff --git a/src/main/java/dev/espi/protectionstones/gui/screens/regions/UnclaimConfirmGui.java b/src/main/java/dev/espi/protectionstones/gui/screens/regions/UnclaimConfirmGui.java new file mode 100644 index 00000000..2fa67b32 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/gui/screens/regions/UnclaimConfirmGui.java @@ -0,0 +1,118 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dev.espi.protectionstones.gui.screens.regions; + +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import dev.espi.protectionstones.PSRegion; +import dev.espi.protectionstones.PSL; +import dev.espi.protectionstones.gui.BaseGui; +import dev.espi.protectionstones.gui.GuiItems; +import dev.espi.protectionstones.gui.GuiManager; +import dev.espi.protectionstones.utils.WGUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.UUID; +import java.util.function.Supplier; + +/** Confirmation GUI for unclaiming a region. */ +public class UnclaimConfirmGui extends BaseGui { + + private final UUID worldId; + private final String regionId; + private final Supplier back; + + public UnclaimConfirmGui(GuiManager gui, UUID worldId, String regionId, Supplier back) { + super(gui, 27, ChatColor.DARK_GRAY + "Confirm Unclaim"); + this.worldId = worldId; + this.regionId = regionId; + this.back = back; + } + + @Override + protected void draw(Player viewer) { + inv.clear(); + + PSRegion r = resolveRegion(); + if (r == null) { + inv.setItem(13, GuiItems.item(Material.BARRIER, "&cRegion not found")); + inv.setItem(22, GuiItems.item(Material.BARRIER, "&cClose")); + return; + } + + String display = r.getName() == null ? r.getId() : (r.getName() + " (" + r.getId() + ")"); + + inv.setItem(13, GuiItems.item(Material.TNT, "&cUnclaim", "&7You are about to unclaim:", "&f" + display, + "&8", "&7This will delete the region.")); + + inv.setItem(11, GuiItems.item(Material.LIME_WOOL, "&aConfirm")); + inv.setItem(15, GuiItems.item(Material.RED_WOOL, "&cCancel")); + + if (back != null) inv.setItem(18, GuiItems.item(Material.ARROW, "&bBack")); + inv.setItem(22, GuiItems.item(Material.BARRIER, "&cClose")); + } + + @Override + protected void onClick(Player viewer, InventoryClickEvent e) { + int raw = e.getRawSlot(); + + if (raw == 22) { + gui.close(viewer); + return; + } + if (raw == 18 && back != null) { + gui.open(viewer, back.get()); + return; + } + if (raw == 15) { + if (back != null) gui.open(viewer, back.get()); + else gui.close(viewer); + return; + } + if (raw != 11) return; + + PSRegion r = resolveRegion(); + if (r == null) { + gui.close(viewer); + return; + } + + // safety checks + if (r.getRentStage() == PSRegion.RentStage.RENTING && !viewer.hasPermission("protectionstones.superowner")) { + PSL.msg(viewer, PSL.RENT_CANNOT_BREAK_WHILE_RENTING.msg()); + gui.close(viewer); + return; + } + + RegionActions.unclaimRegion(r, viewer); + gui.close(viewer); + } + + private PSRegion resolveRegion() { + World w = Bukkit.getWorld(worldId); + if (w == null) return null; + RegionManager rm = WGUtils.getRegionManagerWithWorld(w); + if (rm == null) return null; + ProtectedRegion pr = rm.getRegion(regionId); + if (pr == null) return null; + return PSRegion.fromWGRegion(w, pr); + } +} diff --git a/src/main/java/dev/espi/protectionstones/utils/upgrade/ConfigUpgrades.java b/src/main/java/dev/espi/protectionstones/utils/upgrade/ConfigUpgrades.java index 12cde62a..194c37d4 100644 --- a/src/main/java/dev/espi/protectionstones/utils/upgrade/ConfigUpgrades.java +++ b/src/main/java/dev/espi/protectionstones/utils/upgrade/ConfigUpgrades.java @@ -141,6 +141,29 @@ public static boolean doConfigUpgrades() { ProtectionStones.config.set("allow_home_teleport_for_members", true); ProtectionStones.config.setComment("allow_home_teleport_for_members", " Whether or not members of a region can /ps home to the region."); break; + case 16: + ProtectionStones.config.set("config_version", 17); + // Inventory GUI toggles (defaults preserve current behavior) + ProtectionStones.config.set("gui.enabled", false); + ProtectionStones.config.setComment("gui.enabled", " Whether to enable inventory-based GUIs for commands. If false, commands use legacy text-based output."); + ProtectionStones.config.set("gui.commands.home", true); + ProtectionStones.config.set("gui.commands.flag", true); + ProtectionStones.config.set("gui.commands.add", true); + ProtectionStones.config.set("gui.commands.remove", true); + ProtectionStones.config.set("gui.commands.addowner", true); + ProtectionStones.config.set("gui.commands.removeowner", true); + ProtectionStones.config.setComment("gui.commands", " Per-command GUI toggles (only used if gui.enabled = true).\n Supported: home, flag, add, remove, addowner, removeowner"); + // Expand GUI toggles for additional commands + ProtectionStones.config.set("gui.commands.list", true); + ProtectionStones.config.set("gui.commands.info", true); + ProtectionStones.config.set("gui.commands.tp", true); + ProtectionStones.config.set("gui.commands.unclaim", true); + ProtectionStones.config.set("gui.commands.priority", true); + ProtectionStones.config.setComment("gui.commands", " Per-command GUI toggles (only used if gui.enabled = true).\n Supported: home, flag, add, remove, addowner, removeowner, list, info, tp, unclaim, priority"); + // Admin GUI toggle + ProtectionStones.config.set("gui.commands.admin", true); + ProtectionStones.config.setComment("gui.commands", " Per-command GUI toggles (only used if gui.enabled = true).\n Supported: home, flag, add, remove, addowner, removeowner, list, info, tp, unclaim, priority, admin"); + break; case ProtectionStones.CONFIG_VERSION: leaveLoop = true; break; diff --git a/src/main/resources/block1.toml b/src/main/resources/block1.toml index c4dee39d..112b524a 100644 --- a/src/main/resources/block1.toml +++ b/src/main/resources/block1.toml @@ -25,7 +25,7 @@ restrict_obtaining = true # Enable or disable the use of this protection stone in specific worlds # "blacklist" mode prevents this protect block from being used in the worlds in "worlds" # "whitelist" mode allows this protect block to only be used in the worlds in "worlds" -# Can be overridden with protectionstones.admin permission (including OP)! +# Can be overriden with protectionstones.admin permission (including OP)! world_list_type = "blacklist" worlds = [ "exampleworld1", diff --git a/src/main/resources/config.toml b/src/main/resources/config.toml index 539f62a7..8ec42b35 100644 --- a/src/main/resources/config.toml +++ b/src/main/resources/config.toml @@ -1,5 +1,5 @@ # Please do not change the config version unless you know what you are doing! -config_version = 16 +config_version = 17 uuidupdated = true region_negative_min_max_updated = true @@ -36,6 +36,7 @@ aliases = [ "protectionstones" ] + # Whether or not to drop items on the ground if the inventory is full (ex. during /ps unclaim) # If set to false, the event will be prevented from happening, and say that inventory is full drop_item_when_inventory_full = true @@ -68,6 +69,31 @@ allow_addowner_for_offline_players_without_lp = false # Whether or not members of a region can /ps home to the region. allow_home_teleport_for_members = true +# --------------------------------------------------------------------------------------- +# Inventory GUI toggles +# If enabled, selected commands will open inventory-based GUIs. +# If disabled, commands will use the legacy text-based output. +# --------------------------------------------------------------------------------------- + +[gui] + # Whether to enable inventory-based GUIs for commands. + enabled = false + + [gui.commands] + # Per-command GUI toggles (only used if gui.enabled = true) + home = true + flag = true + add = true + remove = true + addowner = true + removeowner = true + list = true + info = true + tp = true + unclaim = true + priority = true + admin = true + [admin] # Whether /ps admin cleanup remove should delete regions that have members, but don't have owners (after inactive # owners are removed). @@ -91,4 +117,4 @@ allow_home_teleport_for_members = true tax_enabled = false # Notify players of outstanding tax payments for the regions they own. - tax_message_on_join = true + tax_message_on_join = true \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d29b6003..c5ad41a3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -6,7 +6,7 @@ authors: [EspiDev] depend: [WorldGuard, WorldEdit] softdepend: [Vault, PlaceholderAPI, LuckPerms] main: dev.espi.protectionstones.ProtectionStones -api-version: 1.21.10 +api-version: 1.17 permissions: protectionstones.create: @@ -116,4 +116,4 @@ permissions: default: op protectionstones.superowner: description: Allows players to override region permissions. - default: op + default: op \ No newline at end of file