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