diff --git a/build.gradle b/build.gradle index c2f7493..64115d3 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'de.t14d3' -version = '0.1.5' +version = '0.1.6' repositories { mavenCentral() @@ -70,6 +70,9 @@ processResources { filesMatching('plugin.yml') { expand props } + filesMatching('paper-plugin.yml') { + expand props + } } tasks { diff --git a/src/main/java/de/t14d3/zones/PaperBootstrap.java b/src/main/java/de/t14d3/zones/PaperBootstrap.java new file mode 100644 index 0000000..2468e28 --- /dev/null +++ b/src/main/java/de/t14d3/zones/PaperBootstrap.java @@ -0,0 +1,48 @@ +package de.t14d3.zones; + +import de.t14d3.zones.brigadier.Command; +import de.t14d3.zones.utils.Flags; +import de.t14d3.zones.utils.Utils; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.plugin.bootstrap.BootstrapContext; +import io.papermc.paper.plugin.bootstrap.PluginBootstrap; +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +@SuppressWarnings("UnstableApiUsage") +public class PaperBootstrap implements PluginBootstrap { + + private Zones zones; + private Flags flags; + + @Override + public void bootstrap(BootstrapContext context) { + flags = new Flags(); + for (Map.Entry entry : Utils.defaultFlags().entrySet()) { + flags.registerFlag(entry.getKey(), entry.getValue()); + } + Command cmd = new Command(this); + LifecycleEventManager<@NotNull BootstrapContext> eventManager = context.getLifecycleManager(); + eventManager.registerEventHandler(LifecycleEvents.COMMANDS, event -> { + final Commands commands = event.registrar(); + commands.register(cmd.node()); + }); + } + + @Override + public @NotNull JavaPlugin createPlugin(@NotNull PluginProviderContext context) { + zones = new Zones(this); + return zones; + } + + public Flags getFlags() { + return flags; + } + + +} \ No newline at end of file diff --git a/src/main/java/de/t14d3/zones/PermissionManager.java b/src/main/java/de/t14d3/zones/PermissionManager.java index a3c6213..b3cf1d8 100644 --- a/src/main/java/de/t14d3/zones/PermissionManager.java +++ b/src/main/java/de/t14d3/zones/PermissionManager.java @@ -184,14 +184,14 @@ private boolean calculatePermission(String who, String permission, String type, return hasPermission(who, permission, type, region.getParentRegion(this.regionManager)); } if (permissions.containsKey("group")) { - if (who.startsWith(":group-") && !Zones.getInstance().getConfig().getBoolean("allow-group-recursion", false)) { + if (who.startsWith("+group-") && !Zones.getInstance().getConfig().getBoolean("allow-group-recursion", false)) { Zones.getInstance().getLogger().severe("Recursive group permissions detected!! Groups are not allowed to contain other groups!"); Zones.getInstance().getLogger().severe("Group '" + who.substring(7) + "' contains 'group' permission entry in region '" + region.getKey() + "'"); Zones.getInstance().getLogger().severe("If you are 100% sure this is fine, add 'allow-group-recursion: true' to your config.yml"); return false; } for (String group : permissions.get("group").split(",")) { - return hasPermission(":group-" + group, permission, type, region); + return hasPermission("+group-" + group, permission, type, region); } } // Nothing found, deny access @@ -203,6 +203,7 @@ private boolean calculatePermission(String who, String permission, String type, // Analyze permission values boolean explicitAllow = false; boolean explicitDeny = false; + boolean result = false; if (value != null) { for (String permittedValue : value.split(",")) { @@ -210,31 +211,24 @@ private boolean calculatePermission(String who, String permission, String type, // Check for wildcard allow if ("*".equals(permittedValue) || "true".equalsIgnoreCase(permittedValue)) { - explicitAllow = true; + result = true; } // Check for wildcard deny - else if ("! *".equals(permittedValue) || "false".equalsIgnoreCase(permittedValue)) { - explicitDeny = true; + else if ("!*".equals(permittedValue) || "false".equalsIgnoreCase(permittedValue)) { + result = false; } // Check for specific type allow else if (permittedValue.equalsIgnoreCase(type)) { - explicitAllow = true; + result = true; } // Check for specific type deny else if (permittedValue.equalsIgnoreCase("!" + type)) { - explicitDeny = true; + result = false; } } } - // Determine final access based on explicit allow/deny flags - if (explicitDeny) { - return false; - } else if (explicitAllow) { - return true; - } - // Deny by default - return false; + return result; } public boolean isAdmin(String who, Region region) { diff --git a/src/main/java/de/t14d3/zones/Region.java b/src/main/java/de/t14d3/zones/Region.java index 522126b..2995b41 100644 --- a/src/main/java/de/t14d3/zones/Region.java +++ b/src/main/java/de/t14d3/zones/Region.java @@ -122,16 +122,39 @@ public Map> getMembers() { return members; } + /** + * Get the names of all groups in this region + * + * @return List of group names + * @since 0.1.5 + */ public List getGroupNames() { List groupNames = new ArrayList<>(); for (Map.Entry> entry : members.entrySet()) { - if (entry.getKey().startsWith(":group-")) { + if (entry.getKey().startsWith("+group-")) { groupNames.add(entry.getKey()); } } return groupNames; } + /** + * Get the members of a group in this region + * + * @param group Group name + * @return List of members + * @since 0.1.6 + */ + public List getGroupMembers(String group) { + List groupMembers = new ArrayList<>(); + for (Map.Entry> entry : members.entrySet()) { + if (entry.getValue().containsKey("group") && entry.getValue().get("group").contains(group.substring(7))) { + groupMembers.add(entry.getKey()); + } + } + return groupMembers; + } + void setMembers(Map> members, RegionManager regionManager, String key) { this.members = members; regionManager.saveRegion(key, this); // Ensure changes are saved diff --git a/src/main/java/de/t14d3/zones/Zones.java b/src/main/java/de/t14d3/zones/Zones.java index ba861f1..3c38a97 100644 --- a/src/main/java/de/t14d3/zones/Zones.java +++ b/src/main/java/de/t14d3/zones/Zones.java @@ -9,12 +9,8 @@ import de.t14d3.zones.listeners.PlayerInteractListener; import de.t14d3.zones.listeners.PlayerQuitListener; import de.t14d3.zones.utils.*; -import io.papermc.paper.command.brigadier.Commands; -import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; -import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; import it.unimi.dsi.fastutil.Pair; import org.bukkit.Location; -import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.util.BoundingBox; @@ -37,9 +33,14 @@ public final class Zones extends JavaPlugin { private Types types; private Messages messages; private static Zones instance; + private CommandListener commandListener; + private PaperBootstrap bootstrap; + + public Zones(PaperBootstrap bootstrap) { + this.bootstrap = bootstrap; + } @Override - @SuppressWarnings("UnstableApiUsage") public void onEnable() { instance = this; // Initialize PermissionManager first without RegionManager @@ -85,13 +86,8 @@ public void onEnable() { types = new Types(); types.populateTypes(); - // Register command executor and tab completer - LifecycleEventManager manager = this.getLifecycleManager(); - manager.registerEventHandler(LifecycleEvents.COMMANDS, event -> { - final Commands commands = event.registrar(); - commands.register("zone", new CommandListener(this, regionManager)); - }); + this.commandListener = new CommandListener(this, regionManager); // Register saving task if (getSavingMode() == Utils.SavingModes.PERIODIC) { getServer().getScheduler().runTaskTimerAsynchronously(this, () -> { @@ -154,4 +150,8 @@ public Utils.SavingModes getSavingMode() { public static Zones getInstance() { return instance; } + + public CommandListener getCommandListener() { + return commandListener; + } } diff --git a/src/main/java/de/t14d3/zones/brigadier/Command.java b/src/main/java/de/t14d3/zones/brigadier/Command.java new file mode 100644 index 0000000..013154f --- /dev/null +++ b/src/main/java/de/t14d3/zones/brigadier/Command.java @@ -0,0 +1,194 @@ +package de.t14d3.zones.brigadier; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import de.t14d3.zones.PaperBootstrap; +import de.t14d3.zones.Region; +import de.t14d3.zones.Zones; +import de.t14d3.zones.utils.Flags; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@SuppressWarnings("UnstableApiUsage") +public class Command { + private final PaperBootstrap context; + + public Command(PaperBootstrap context) { + this.context = context; + } + + public LiteralCommandNode node() { + return Commands.literal("zone") + .executes(ctx -> { + MiniMessage mm = MiniMessage.miniMessage(); + Component comp = mm.deserialize(Zones.getInstance().getMessages().get("commands.invalid")); + ctx.getSource().getSender().sendMessage(comp); + return 1; + }) + .then(Commands.argument("subcommand", new SubCommandArgument()) + .suggests((ctx, builder) -> { + for (SubCommands subCommand : SubCommands.values()) { + if (!ctx.getSource().getSender().hasPermission("zones." + subCommand.name().toLowerCase())) { + continue; + } + String[] args = ctx.getInput().split(" "); + if (args.length == 1 || subCommand.name().toLowerCase().startsWith(args[1].toLowerCase())) { + builder.suggest(subCommand.name().toLowerCase(), MessageComponentSerializer.message().serialize(subCommand.getInfo())); + } + } + return builder.buildFuture(); + }) + .executes(ctx -> { + Zones.getInstance().getCommandListener().execute(ctx.getSource(), ctx.getInput()); + return 1; + }) + .then(Commands.argument("key", new RegionKeyArgument()) + .suggests((ctx, builder) -> { + switch (arg(ctx, 1).toUpperCase()) { + case "LIST", "CANCEL", "CREATE", "SAVE", "LOAD" -> { + return builder.buildFuture(); + } + default -> { + } + } + boolean hasPerm = ctx.getSource().getSender().hasPermission("zones." + arg(ctx, 1).toLowerCase() + ".other"); + for (Map.Entry region : Zones.getInstance().getRegionManager().regions().entrySet()) { + if (hasPerm || ctx.getSource().getSender() instanceof Player player && region.getValue().isAdmin(player.getUniqueId())) { + builder.suggest( + region.getKey(), + MessageComponentSerializer.message().serialize( + RegionKeyArgument.regionInfo(Map.entry(region.getKey(), region.getValue())) + ) + ); + } + } + return builder.buildFuture(); + }) + .executes(ctx -> { + Zones.getInstance().getCommandListener().execute(ctx.getSource(), ctx.getInput()); + return 1; + }) + .then(Commands.argument("name", StringArgumentType.string()) + .suggests((ctx, builder) -> { + if (arg(ctx, 1).equalsIgnoreCase("RENAME")) { + builder.suggest("", MessageComponentSerializer.message().serialize(Component.text("Type the new name for the region"))); + return builder.buildFuture(); + } + if (arg(ctx, 1).equalsIgnoreCase("SET")) { + for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { + if (ctx.getInput().split(" ").length <= 3 || (offlinePlayer.getName() != null ? offlinePlayer.getName() : offlinePlayer.getUniqueId().toString()).toLowerCase().startsWith(arg(ctx, 3).toLowerCase())) { + builder.suggest(offlinePlayer.getName()); + } + } + for (Map.Entry region : Zones.getInstance().getRegionManager().regions().entrySet()) { + String[] args = ctx.getInput().split(" "); + if (args.length <= 2 || !args[2].equalsIgnoreCase(region.getKey())) { + continue; + } + if (ctx.getSource().getSender().hasPermission("zones.info.other") + || (ctx.getSource().getSender() instanceof Player player && region.getValue().isAdmin(player.getUniqueId()))) { + region.getValue().getGroupNames().forEach(group -> { + List groupMembers = new ArrayList<>(); + region.getValue().getGroupMembers(group).forEach(val -> { + groupMembers.add(Bukkit.getOfflinePlayer(UUID.fromString(val)).getName()); + }); + if (ctx.getInput().split(" ").length <= 3 + || group.toLowerCase().startsWith(arg(ctx, 3).toLowerCase()) + || group.toLowerCase().replace("+", "").startsWith(arg(ctx, 3).toLowerCase())) { + builder.suggest(group, MessageComponentSerializer.message().serialize(Component.text(groupMembers.toString()))); + } + }); + } + } + } + return builder.buildFuture(); + }) + .executes(ctx -> { + Zones.getInstance().getCommandListener().execute(ctx.getSource(), ctx.getInput()); + return 1; + }) + .then(Commands.argument("flag", new FlagArgument(context)) + .suggests((ctx, builder) -> { + Flags flags = context.getFlags(); + MiniMessage mm = MiniMessage.miniMessage(); + for (Map.Entry entry : flags.getFlags().entrySet()) { + builder.suggest(entry.getKey(), + MessageComponentSerializer.message().serialize( + mm.deserialize(Zones.getInstance().getMessages().getOrDefault("flags." + entry.getKey(), entry.getValue())) + )); + } + return builder.buildFuture(); + }) + .then(Commands.argument("value", StringArgumentType.greedyString()) + .suggests((ctx, builder) -> { + Zones plugin = Zones.getInstance(); + List types; + switch (arg(ctx, 4).toUpperCase()) { + case "PLACE", "BREAK" -> + types = plugin.getTypes().blockTypes; + case "CONTAINER" -> + types = plugin.getTypes().containerTypes; + case "REDSTONE" -> + types = plugin.getTypes().redstoneTypes; + case "ENTITY", "DAMAGE" -> + types = plugin.getTypes().entityTypes; + case "IGNITE" -> types = List.of("TRUE", "FALSE"); + case "GROUP" -> { + types = new ArrayList<>(); + for (Map.Entry region : Zones.getInstance().getRegionManager().regions().entrySet()) { + if (!ctx.getInput().split(" ")[2].equalsIgnoreCase(region.getKey())) { + continue; + } + if (ctx.getSource().getSender().hasPermission("zones.info.other") + || (ctx.getSource().getSender() instanceof Player player && region.getValue().isAdmin(player.getUniqueId()))) { + types.addAll(region.getValue().getGroupNames()); + } + } + } + default -> types = plugin.getTypes().allTypes; + } + String[] args = ctx.getInput().split(" "); + for (String value : types) { + if (args.length == 5 || value.toLowerCase().startsWith(args[args.length - 1])) { + builder.suggest(value.toLowerCase(), MessageComponentSerializer.message().serialize( + Component.text(value).color(value.startsWith("!") ? NamedTextColor.RED : NamedTextColor.GREEN))); + } + } + return builder.buildFuture(); + }) + .executes(ctx -> { + Zones.getInstance().getCommandListener().execute(ctx.getSource(), ctx.getInput()); + return 1; + }) + ) + ) + ) + ) + ).build(); + } + + /** + * Helper function to get the argument of a command at the given index + * + * @param ctx Command context + * @param index Index of the argument + * @return Argument at the given index + */ + public static String arg(CommandContext ctx, int index) { + String arg = ctx.getInput().replace("zones:", ""); + return arg.split(" ")[index]; + } +} diff --git a/src/main/java/de/t14d3/zones/brigadier/FlagArgument.java b/src/main/java/de/t14d3/zones/brigadier/FlagArgument.java new file mode 100644 index 0000000..a366ef9 --- /dev/null +++ b/src/main/java/de/t14d3/zones/brigadier/FlagArgument.java @@ -0,0 +1,27 @@ +package de.t14d3.zones.brigadier; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import de.t14d3.zones.PaperBootstrap; +import de.t14d3.zones.utils.Flags; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("UnstableApiUsage") +public class FlagArgument implements CustomArgumentType.Converted { + private final Flags flags; + + public FlagArgument(PaperBootstrap context) { + this.flags = context.getFlags(); + } + @Override + public @NotNull String convert(@NotNull String nativeType) throws CommandSyntaxException { + return flags.getFlags().get(nativeType); + } + + @Override + public @NotNull ArgumentType getNativeType() { + return StringArgumentType.word(); + } +} diff --git a/src/main/java/de/t14d3/zones/brigadier/RegionKeyArgument.java b/src/main/java/de/t14d3/zones/brigadier/RegionKeyArgument.java new file mode 100644 index 0000000..4fa3d53 --- /dev/null +++ b/src/main/java/de/t14d3/zones/brigadier/RegionKeyArgument.java @@ -0,0 +1,74 @@ +package de.t14d3.zones.brigadier; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import de.t14d3.zones.Region; +import de.t14d3.zones.Zones; +import de.t14d3.zones.utils.Messages; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.UUID; + +import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; + +@SuppressWarnings("UnstableApiUsage") +public class RegionKeyArgument implements CustomArgumentType.Converted { + public RegionKeyArgument() { + + } + + /** + * Get a region object from a string + * + * @param nativeType native argument provided value + * @return Region object + * @throws CommandSyntaxException if region is not found + */ + @Override + public @NotNull Region convert(@NotNull String nativeType) throws CommandSyntaxException { + return Zones.getInstance().getRegionManager().regions().get(nativeType); + } + + @Override + public @NotNull ArgumentType getNativeType() { + return StringArgumentType.word(); + } + + + public static Component regionInfo(Map.Entry entry) { + Messages messages = Zones.getInstance().getMessages(); + var mm = MiniMessage.miniMessage(); + Component comp = Component.empty(); + comp = comp.append(mm.deserialize("Name: " + messages.get("region.info.name") + " ", parsed("name", entry.getValue().getName()))); + if (entry.getValue().getParent() != null) { + comp = comp.append(mm.deserialize(messages.get("region.info.parent") + " ", parsed("parent", entry.getValue().getParent()))); + } + comp = comp.append(mm.deserialize("(" + messages.get("region.info.min") + " - ", parsed("min", entry.getValue().getMinString()))); + comp = comp.append(mm.deserialize(messages.get("region.info.max") + ")", parsed("max", entry.getValue().getMaxString()))); + comp = comp.append(Component.text(" Members: ").color(NamedTextColor.LIGHT_PURPLE)); + + // Iterate over members to format permissions + for (Map.Entry> member : entry.getValue().getMembers().entrySet()) { + String playerName = null; + try { + playerName = Bukkit.getOfflinePlayer(UUID.fromString(member.getKey())).getName(); + } catch (IllegalArgumentException ignored) { + } + if (playerName == null) { + playerName = member.getKey(); + } + Component playerComponent = mm.deserialize(messages.get("region.info.members.name") + " ", parsed("name", playerName)); + comp = comp.append(playerComponent); + } + + comp = comp.append(mm.deserialize(" " + messages.get("region.info.key"), parsed("key", entry.getKey()))); + return comp; + } +} \ No newline at end of file diff --git a/src/main/java/de/t14d3/zones/brigadier/SubCommandArgument.java b/src/main/java/de/t14d3/zones/brigadier/SubCommandArgument.java new file mode 100644 index 0000000..8beb764 --- /dev/null +++ b/src/main/java/de/t14d3/zones/brigadier/SubCommandArgument.java @@ -0,0 +1,31 @@ +package de.t14d3.zones.brigadier; + +import com.mojang.brigadier.Message; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import de.t14d3.zones.Zones; +import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.jetbrains.annotations.NotNull; + + +@SuppressWarnings("UnstableApiUsage") +public class SubCommandArgument implements CustomArgumentType.Converted { + @Override + public @NotNull SubCommands convert(@NotNull String nativeType) throws CommandSyntaxException { + try { + return SubCommands.valueOf(nativeType.toUpperCase().split(" ")[0]); + } catch (IllegalArgumentException ignored) { + Message message = MessageComponentSerializer.message().serialize(MiniMessage.miniMessage().deserialize(Zones.getInstance().getMessages().get("commands.invalid"))); + throw new CommandSyntaxException(new SimpleCommandExceptionType(message), message); + } + } + + @Override + public @NotNull ArgumentType getNativeType() { + return StringArgumentType.string(); + } +} diff --git a/src/main/java/de/t14d3/zones/brigadier/SubCommands.java b/src/main/java/de/t14d3/zones/brigadier/SubCommands.java new file mode 100644 index 0000000..a50db39 --- /dev/null +++ b/src/main/java/de/t14d3/zones/brigadier/SubCommands.java @@ -0,0 +1,34 @@ +package de.t14d3.zones.brigadier; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +public enum SubCommands { + INFO(Component.text("Shows information about a region").color(NamedTextColor.GOLD)), + LIST(Component.text("Lists all regions").color(NamedTextColor.GOLD)), + DELETE(Component.text("Deletes a region").color(NamedTextColor.DARK_RED)), + CREATE(Component.text("Creates a new region").color(NamedTextColor.GREEN)), + EXPAND(Component.text("Expands a region").color(NamedTextColor.AQUA)), + SELECT(Component.text("Visually selects a region").color(NamedTextColor.LIGHT_PURPLE)), + SET(Component.text("Sets permissions for a region").color(NamedTextColor.BLUE)), + CANCEL(Component.text("Cancels the current operation").color(NamedTextColor.RED)), + RENAME(Component.text("Renames a region").color(NamedTextColor.YELLOW)), + SUBCREATE(Component.text("Creates a sub-region").color(NamedTextColor.DARK_PURPLE)), + SAVE(Component.text("Saves all regions to file").color(NamedTextColor.DARK_GREEN)), + LOAD(Component.text("Loads all regions from file").color(NamedTextColor.DARK_AQUA)); + + + private final Component info; + + SubCommands(Component info) { + this.info = info; + } + + public Component getInfo() { + return info; + } + + public static SubCommands get(String input) { + return SubCommands.valueOf(input.split(" ")[0]); + } +} \ No newline at end of file diff --git a/src/main/java/de/t14d3/zones/integrations/PlaceholderAPI.java b/src/main/java/de/t14d3/zones/integrations/PlaceholderAPI.java index 56fd31e..8ee77a9 100644 --- a/src/main/java/de/t14d3/zones/integrations/PlaceholderAPI.java +++ b/src/main/java/de/t14d3/zones/integrations/PlaceholderAPI.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.UUID; public class PlaceholderAPI extends PlaceholderExpansion { private final Zones plugin; @@ -79,16 +80,21 @@ public String onPlaceholderRequest(Player player, @NotNull String params) { } if (params.equalsIgnoreCase("get_members")) { final String[] members = {""}; - regions.get(0).getMembers().keySet().forEach(uuid -> { - String member = Bukkit.getOfflinePlayer(uuid).getName() != null ? Bukkit.getOfflinePlayer(uuid).getName() : uuid.toString(); + regions.get(0).getMembers().keySet().forEach(val -> { + String member; + try { + UUID uuid = UUID.fromString(val); + member = Bukkit.getOfflinePlayer(uuid).getName() != null ? Bukkit.getOfflinePlayer(uuid).getName() : String.valueOf(uuid); + } catch (IllegalArgumentException ignored) { + member = val; + } - if (members[0].isEmpty()) { - members[0] = member; - } else { - members[0] = members[0] + ", " + member; - } - } - ); + if (members[0].isEmpty()) { + members[0] = member; + } else { + members[0] = members[0] + ", " + member; + } + }); return members[0]; } if (params.equalsIgnoreCase("get_owner")) { diff --git a/src/main/java/de/t14d3/zones/listeners/CommandListener.java b/src/main/java/de/t14d3/zones/listeners/CommandListener.java index a9a7942..021a189 100644 --- a/src/main/java/de/t14d3/zones/listeners/CommandListener.java +++ b/src/main/java/de/t14d3/zones/listeners/CommandListener.java @@ -4,10 +4,8 @@ import de.t14d3.zones.Region; import de.t14d3.zones.RegionManager; import de.t14d3.zones.Zones; -import de.t14d3.zones.utils.Actions; import de.t14d3.zones.utils.Direction; import de.t14d3.zones.utils.Messages; -import io.papermc.paper.command.brigadier.BasicCommand; import io.papermc.paper.command.brigadier.CommandSourceStack; import it.unimi.dsi.fastutil.Pair; import net.kyori.adventure.text.Component; @@ -21,7 +19,6 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.util.BoundingBox; -import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -29,8 +26,7 @@ import static de.t14d3.zones.utils.BeaconUtils.resetBeacon; import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; -@SuppressWarnings("UnstableApiUsage") -public class CommandListener implements BasicCommand { +public class CommandListener { private final Zones plugin; private final RegionManager regionManager; @@ -45,12 +41,9 @@ public CommandListener(Zones plugin, RegionManager regionManager) { this.pm = plugin.getPermissionManager(); } - @Override - public void execute(CommandSourceStack stack, String[] args) { - if (args.length < 1) { - stack.getSender().sendMessage(miniMessage.deserialize(messages.get("commands.invalid"))); - return; - } + @SuppressWarnings("UnstableApiUsage") + public void execute(CommandSourceStack stack, String arg) { + String[] args = arg.replaceFirst("zone ", "").replaceFirst("zones:", "").split(" "); CommandSender sender = stack.getSender(); String command = args[0].toLowerCase(); @@ -106,14 +99,14 @@ public void execute(CommandSourceStack stack, String[] args) { break; case "save": if (sender.hasPermission("zones.save")) { - handleSaveCommand(sender, args); + handleSaveCommand(sender); } else { sender.sendMessage(miniMessage.deserialize(messages.get("commands.no-permission"))); } break; case "load": if (sender.hasPermission("zones.load")) { - handleLoadCommand(sender, args); + handleLoadCommand(sender); } else { sender.sendMessage(miniMessage.deserialize(messages.get("commands.no-permission"))); } @@ -140,111 +133,12 @@ public void execute(CommandSourceStack stack, String[] args) { } break; default: - stack.getSender().sendMessage(miniMessage.deserialize(messages.get("commands.invalid"))); + sender.sendMessage(miniMessage.deserialize(messages.get("commands.invalid"))); break; } } - @Override - public @NotNull Collection suggest(@NotNull CommandSourceStack stack, String[] args) { - if (args.length <= 1) { - List suggestions = new ArrayList<>(); - for (String command : List.of("info", "delete", "create", "subcreate", "cancel", "list", "set", "load", "save", "expand", "select")) { - if (stack.getSender().hasPermission("zones." + command) - && (args.length == 0 || command.startsWith(args[0].toLowerCase()))) { - suggestions.add(command); - } - } - return suggestions; - } - if (stack.getSender() instanceof Player player) { - if (args.length == 2 && (args[0].equalsIgnoreCase("info") - || args[0].equalsIgnoreCase("delete") - || args[0].equalsIgnoreCase("subcreate") - || args[0].equalsIgnoreCase("expand") - || args[0].equalsIgnoreCase("select") - || args[0].equalsIgnoreCase("set") - || args[0].equalsIgnoreCase("rename"))) { - List suggestions = new ArrayList<>(); - regionManager.regions().forEach((regionKey, region) -> { - if (region.isAdmin(player.getUniqueId()) && regionKey.startsWith(args[1])) { - suggestions.add(regionKey); - } - }); - return suggestions; - } - if (args[0].equalsIgnoreCase("set")) { - Region region = plugin.getRegionManager().regions().get(args[1]); - if (args.length == 3) { - List suggestions = new ArrayList<>(); - for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { - if (offlinePlayer.getName() != null && offlinePlayer.getName().toLowerCase().startsWith(args[2].toLowerCase())) { - suggestions.add(offlinePlayer.getName()); - } - } - if (region != null && region.isAdmin(player.getUniqueId())) { - suggestions.addAll(region.getGroupNames()); - } - return suggestions; - } - if (args.length == 4) { - List suggestions = new ArrayList<>(); - for (Actions action : Actions.values()) { - if (action.name().toLowerCase().startsWith(args[3].toLowerCase())) { - String who; - if (args[2].startsWith(":")) { - who = args[2]; - } else { - who = Bukkit.getOfflinePlayer(args[2]).getUniqueId().toString(); - } - String temp = action.name(); - if (region.getMemberPermissions(who).get(action.name()) != null) { - temp += " " + region.getMemberPermissions(who).get(action.name()).replace(",", "").toLowerCase(); - } - suggestions.add(temp); - } - } - return suggestions; - } - if (args.length >= 5) { - List suggestions = new ArrayList<>(); - List types; - switch (args[3].toUpperCase()) { - case "PLACE", "BREAK" -> types = plugin.getTypes().blockTypes; - case "CONTAINER" -> types = plugin.getTypes().containerTypes; - case "REDSTONE" -> types = plugin.getTypes().redstoneTypes; - case "ENTITY", "DAMAGE" -> types = plugin.getTypes().entityTypes; - case "IGNITE" -> { - types = List.of("TRUE", "FALSE"); - } - default -> types = plugin.getTypes().allTypes; - - } - for (String value : types) { - if (value.toLowerCase().startsWith(args[4].toLowerCase())) { - suggestions.add(value); - } - } - return suggestions; - } - } - if (args[0].equalsIgnoreCase("expand")) { - if (args.length == 3) { - List suggestions = new ArrayList<>(); - for (int i = 1; i < 10; i++) { - suggestions.add(String.valueOf(i)); - } - return suggestions; - } - if (args.length == 4 && stack.getSender().hasPermission("zones.expand.overlap")) { - return List.of("overlap"); - } - } - } - return List.of(); - } - - private void handleDeleteCommand(CommandSender sender, String[] args) { + public void handleDeleteCommand(CommandSender sender, String[] args) { if (args.length < 2) { sender.sendMessage(miniMessage.deserialize(messages.get("commands.invalid-region"))); return; @@ -267,7 +161,7 @@ private void handleDeleteCommand(CommandSender sender, String[] args) { regionManager.triggerSave(); } - private void handleCreateCommand(CommandSender sender) { + public void handleCreateCommand(CommandSender sender) { if (sender instanceof Player player) { if (!plugin.selection.containsKey(player.getUniqueId())) { plugin.selection.put(player.getUniqueId(), Pair.of(null, null)); @@ -298,7 +192,7 @@ private void handleCreateCommand(CommandSender sender) { } } - private void handleSubCreateCommand(CommandSender sender, String[] args) { + public void handleSubCreateCommand(CommandSender sender, String[] args) { if (sender instanceof Player player) { if (!plugin.selection.containsKey(player.getUniqueId())) { plugin.selection.put(player.getUniqueId(), Pair.of(null, null)); @@ -354,7 +248,7 @@ private void handleSubCreateCommand(CommandSender sender, String[] args) { } } - private void handleCancelCommand(CommandSender sender) { + public void handleCancelCommand(CommandSender sender) { if (sender instanceof Player player) { if (plugin.selection.containsKey(player.getUniqueId())) { Pair selection = plugin.selection.get(player.getUniqueId()); @@ -371,7 +265,7 @@ private void handleCancelCommand(CommandSender sender) { } } - private void handleListCommand(CommandSender sender) { + public void handleListCommand(CommandSender sender) { Map regions = regionManager.regions(); if (regions.isEmpty()) { sender.sendMessage(miniMessage.deserialize(messages.get("region.none-found"))); @@ -402,7 +296,7 @@ private void handleListCommand(CommandSender sender) { } } - private void handleInfoCommand(CommandSender sender, String[] args) { + public void handleInfoCommand(CommandSender sender, String[] args) { String regionKey; if (args.length < 2) { if (sender instanceof Player player) { @@ -433,9 +327,9 @@ private void handleInfoCommand(CommandSender sender, String[] args) { } - private void handleSetCommand(CommandSender sender, String[] args) { + public void handleSetCommand(CommandSender sender, String[] args) { - if (args.length < 4) { + if (args.length < 5) { sender.sendMessage(miniMessage.deserialize(messages.get("commands.set.invalid-usage"))); return; } @@ -445,13 +339,16 @@ private void handleSetCommand(CommandSender sender, String[] args) { if (sender instanceof Player) { player = (Player) sender; } - if (region == null || (player != null && !pm.hasPermission(player.getUniqueId(), "role", "owner", region))) { + if (region == null || (player != null && !pm.hasPermission(player.getUniqueId(), "role", "owner", region) && !player.hasPermission("zones.set.other"))) { sender.sendMessage(miniMessage.deserialize(messages.get("commands.invalid-region"))); return; } OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]); String permission = args[3]; - String value = args[4]; + // Yes, I know this is terrible. + // No, I will not change it. + List arg = Arrays.stream(args).toList().subList(4, args.length); + String value = String.join(", ", arg); regionManager.addMemberPermission(target.getUniqueId(), permission, value, regionKey); regionManager.triggerSave(); sender.sendMessage(miniMessage.deserialize(messages.get("commands.set.success"), @@ -461,7 +358,7 @@ private void handleSetCommand(CommandSender sender, String[] args) { parsed("value", value))); } - private void handleExpandCommand(CommandSender sender, String[] args) { + public void handleExpandCommand(CommandSender sender, String[] args) { if (args.length < 2) { sender.sendMessage(miniMessage.deserialize(messages.get("commands.invalid-region"))); return; @@ -495,7 +392,7 @@ private void handleExpandCommand(CommandSender sender, String[] args) { } } - private void handleRenameCommand(CommandSender sender, String[] args) { + public void handleRenameCommand(CommandSender sender, String[] args) { if (args.length < 2) { sender.sendMessage(miniMessage.deserialize(messages.get("commands.invalid-region"))); return; @@ -514,26 +411,27 @@ private void handleRenameCommand(CommandSender sender, String[] args) { sender.sendMessage(miniMessage.deserialize(messages.get("commands.rename.provide-name"))); return; } - region.setName(args[2], regionManager); + String name = String.join(" ", args).substring(16).replace("\"", "").replace("'", ""); + region.setName(name, regionManager); sender.sendMessage(miniMessage.deserialize(messages.get("commands.rename.success"), parsed("region", regionKey), - parsed("name", args[2]))); + parsed("name", name))); } - private void handleSaveCommand(CommandSender sender, String[] args) { + public void handleSaveCommand(CommandSender sender) { regionManager.saveRegions(); int count = regionManager.regions().size(); sender.sendMessage(miniMessage.deserialize(messages.get("commands.save"), parsed("count", String.valueOf(count)))); } - private void handleLoadCommand(CommandSender sender, String[] args) { + public void handleLoadCommand(CommandSender sender) { regionManager.loadRegions(); int count = regionManager.loadedRegions.size(); sender.sendMessage(miniMessage.deserialize(messages.get("commands.load"), parsed("count", String.valueOf(count)))); } - private void handleSelectCommand(CommandSender sender, String[] args) { + public void handleSelectCommand(CommandSender sender, String[] args) { if (sender instanceof Player player) { Region region; if (args.length == 1) { @@ -545,8 +443,13 @@ private void handleSelectCommand(CommandSender sender, String[] args) { return; } } else { + region = regionManager.regions().get(args[1]); } + if (region == null) { + player.sendMessage(miniMessage.deserialize(messages.get("commands.invalid-region"))); + return; + } if (!plugin.particles.containsKey(player.getUniqueId()) || args.length >= 2) { plugin.particles.put(player.getUniqueId(), BoundingBox.of(region.getMin(), region.getMax())); player.sendMessage(miniMessage.deserialize(messages.get("commands.select.selected"), parsed("region", region.getName()))); diff --git a/src/main/java/de/t14d3/zones/utils/Actions.java b/src/main/java/de/t14d3/zones/utils/Actions.java index 161f0d6..a405f75 100644 --- a/src/main/java/de/t14d3/zones/utils/Actions.java +++ b/src/main/java/de/t14d3/zones/utils/Actions.java @@ -1,12 +1,22 @@ package de.t14d3.zones.utils; public enum Actions { - BREAK, - PLACE, - INTERACT, - CONTAINER, - REDSTONE, - ENTITY, - IGNITE, - DAMAGE + BREAK("Allows breaking blocks"), + PLACE("Allows placing blocks"), + INTERACT("Allows interacting"), + CONTAINER("Allows opening containers"), + REDSTONE("Allows interacting with redstone"), + ENTITY("Allows interacting with entities"), + IGNITE("Allows igniting tnt"), + DAMAGE("Allows damaging entities"); + + private final String key; + + Actions(String key) { + this.key = key; + } + + public String getKey() { + return key; + } } diff --git a/src/main/java/de/t14d3/zones/utils/Flags.java b/src/main/java/de/t14d3/zones/utils/Flags.java new file mode 100644 index 0000000..6ba44bd --- /dev/null +++ b/src/main/java/de/t14d3/zones/utils/Flags.java @@ -0,0 +1,53 @@ +package de.t14d3.zones.utils; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class Flags { + + private Map flags = new HashMap<>(); + + public Flags() { + } + + /** + * Register a flag with a fallback description + * + * @param flag Flag to register + * @param desc Fallback description + * @return {@code true} if it was registered, {@code false} if it already exists + * @see #registerFlag(String, String, boolean) + */ + public boolean registerFlag(@NotNull String flag, @NotNull String desc) { + return registerFlag(flag, desc, false); + } + + /** + * Register a flag with a fallback description + * + * @param flag Flag to register + * @param desc Fallback description + * @param overwrite Whether to overwrite an existing flag + * @return {@code true} if it was registered, {@code false} if it already exists and {@code overwrite} is {@code false} + * @see #registerFlag(String, String) + */ + public boolean registerFlag(@NotNull String flag, @NotNull String desc, boolean overwrite) { + if (flags.containsKey(flag) && !overwrite) { + return false; + } + flags.put(flag, desc); + return true; + } + + /** + * Immutable List of all flags recognized by the plugin + * + * @return Map of {@code } + */ + public Map getFlags() { + return Collections.unmodifiableMap(flags); + } +} diff --git a/src/main/java/de/t14d3/zones/utils/Messages.java b/src/main/java/de/t14d3/zones/utils/Messages.java index 930d28a..d42f6aa 100644 --- a/src/main/java/de/t14d3/zones/utils/Messages.java +++ b/src/main/java/de/t14d3/zones/utils/Messages.java @@ -29,4 +29,8 @@ public Messages(Properties messagesConfig, Zones zones) { public @NotNull String get(String key) { return messages.getOrDefault(key, zones.getConfig().getString("messages.default", key).replaceAll("", key)); } + + public @NotNull String getOrDefault(String key, String defaultValue) { + return messages.getOrDefault(key, defaultValue); + } } diff --git a/src/main/java/de/t14d3/zones/utils/Utils.java b/src/main/java/de/t14d3/zones/utils/Utils.java index b86d176..b37c64a 100644 --- a/src/main/java/de/t14d3/zones/utils/Utils.java +++ b/src/main/java/de/t14d3/zones/utils/Utils.java @@ -10,6 +10,9 @@ import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; +import java.util.HashMap; +import java.util.Map; + /** * Utility methods for the plugin. *

@@ -58,4 +61,18 @@ public static SavingModes fromString(String string) { return mode; } } + + public static Map defaultFlags() { + Map flags = new HashMap<>(); + flags.put("break", "Allows breaking blocks"); + flags.put("place", "Allows placing blocks"); + flags.put("interact", "Allows interacting"); + flags.put("container", "Allows opening containers"); + flags.put("redstone", "Allows interacting with redstone"); + flags.put("entity", "Allows interacting with entities"); + flags.put("ignite", "Allows igniting tnt"); + flags.put("damage", "Allows damaging entities"); + flags.put("group", "Add a group to the player"); + return flags; + } } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 3e55fff..7bdcf12 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -33,3 +33,12 @@ region.info.members.name= region.info.members.permission= : region.info.members.values.allowed= region.info.members.values.denied= +flags.break=Allows breaking blocks +flags.place=Allows placing blocks +flags.interact=Allows interacting +flags.container=Allows opening containers +flags.redstone=Allows interacting with redstone +flags.entity=Allows interacting with entities +flags.ignite=Allows igniting tnt +flags.damage=Allows damaging entities +flags.group=Add a group to the player diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml new file mode 100644 index 0000000..df3f79a --- /dev/null +++ b/src/main/resources/paper-plugin.yml @@ -0,0 +1,95 @@ +name: Zones +version: '${version}' +main: de.t14d3.zones.Zones +api-version: '1.21' +bootstrapper: de.t14d3.zones.PaperBootstrap +permissions: + zones.info: + description: Allow players to view region information + default: true + children: + zones.info.other: true + zones.delete: + description: Allow players to delete regions + default: true + children: + zones.delete.other: true + zones.create: + description: Allow players to create regions + default: true + children: + zones.create.overlap: true + zones.subcreate: + description: Allow players to create sub-regions + default: true + children: + zones.subcreate.other: true + zones.cancel: + description: Allow players to cancel the current operation + default: true + zones.list: + description: Allow players to list all regions + default: true + zones.set: + description: Allow players to set permissions for regions + default: true + children: + zones.set.other: true + zones.expand: + description: Allow players to expand regions + default: true + children: + zones.expand.other: true + zones.expand.overlap: true + zones.select: + description: Allow players to select regions + default: true + children: + zones.select.other: true + zones.rename: + description: Allow players to rename regions + default: true + children: + zones.rename.other: true + + zones.save: + description: Allow players to save all regions to file + default: op + zones.load: + description: Allow players to load all regions from file + default: op + + zones.info.other: + description: Allow players to view other player's regions + default: op + zones.select.other: + description: Allow players to select other player's regions + default: op + zones.delete.other: + description: Allow players to delete other player's regions + default: op + zones.subcreate.other: + description: Allow players to create sub-regions for other player's regions + default: op + zones.expand.other: + description: Allow players to expand regions over other regions + default: op + zones.expand.overlap: + description: Allow players to expand regions over other regions + default: op + zones.set.other: + description: Allow players to set permissions for other player's regions + default: op + zones.create.overlap: + description: Allow players to create regions that overlap existing regions + default: op + zones.rename.other: + description: Allow players to rename other player's regions + default: op + + zones.bypass.unclaimed: + description: Allow players to interact with unclaimed regions + default: op + zones.bypass.claimed: + description: Allow players to interact with other players' regions + default: op \ No newline at end of file