From 7e9b7e3b677b3684e4385282aaa052a0f50f4543 Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:26:15 +0100 Subject: [PATCH 1/9] We do a little refactoring... Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- .gitignore | 2 + README.md | 46 +- api/build.gradle.kts | 12 +- api/src/main/java/de/t14d3/zones/Region.java | 340 +++++---- .../main/java/de/t14d3/zones/RegionKey.java | 4 +- .../java/de/t14d3/zones/RegionManager.java | 479 ++++++------- .../de/t14d3/zones/WorldRegionManager.java | 208 ++++++ api/src/main/java/de/t14d3/zones/Zones.java | 122 ++-- .../java/de/t14d3/zones/ZonesPlatform.java | 44 +- .../t14d3/zones/datasource/SQLDataSource.java | 178 ++--- .../zones/datasource/YamlDataSource.java | 376 +++++++--- .../de/t14d3/zones/objects/BlockLocation.java | 95 --- .../main/java/de/t14d3/zones/objects/Box.java | 91 ++- .../java/de/t14d3/zones/objects/Flag.java | 85 ++- .../java/de/t14d3/zones/objects/Player.java | 97 --- .../t14d3/zones/objects/PlayerRepository.java | 52 -- .../t14d3/zones/objects/RegionFlagEntry.java | 2 +- .../java/de/t14d3/zones/objects/World.java | 37 - .../zones/objects/shapes/CuboidShape.java | 115 +++ .../zones/objects/shapes/GlobalShape.java | 61 ++ .../zones/objects/shapes/ShapeProvider.java | 66 ++ .../t14d3/zones/permissions/CacheEntry.java | 42 -- .../t14d3/zones/permissions/CacheUtils.java | 141 ---- .../de/t14d3/zones/permissions/Decision.java | 19 + .../t14d3/zones/permissions/GlobPattern.java | 93 +++ .../permissions/PermissionKeyRegistry.java | 51 ++ .../zones/permissions/PermissionManager.java | 393 ++++++----- .../zones/permissions/RegionMembership.java | 113 +++ .../RegionPermissionEvaluator.java | 263 +++++++ .../zones/permissions/RegionPermissions.java | 407 +++++++++++ .../permissions/RegionPermissionsCodec.java | 157 +++++ .../permissions/RegionSecurityCodec.java | 235 +++++++ .../permissions/flags/DefaultFlagHandler.java | 2 +- .../zones/permissions/flags/FlagContext.java | 40 ++ .../zones/permissions/flags/FlagRegistry.java | 58 ++ .../permissions/flags/FlagValueKind.java | 7 + .../t14d3/zones/permissions/flags/Flags.java | 131 ++-- .../zones/permissions/flags/IFlagHandler.java | 127 +--- .../permissions/flags/IFlagRegistrar.java | 16 + .../permissions/subjects/GroupSubject.java | 19 + .../permissions/subjects/PlayerSubject.java | 17 + .../permissions/subjects/SubjectRef.java | 14 + .../zones/permissions/subjects/Subjects.java | 47 ++ .../subjects/UniversalSubject.java | 32 + .../zones/rapunzellib/ZonesActionbar.java | 112 +++ .../zones/rapunzellib/ZonesExtraKeys.java | 18 + .../rapunzellib/ZonesPermissionCache.java | 68 ++ .../zones/rapunzellib/ZonesRapunzelHooks.java | 56 ++ .../listeners/ZonesBlockListener.java | 128 ++++ .../listeners/ZonesBucketListener.java | 63 ++ .../listeners/ZonesEntityListener.java | 71 ++ .../listeners/ZonesEventListeners.java | 36 + .../listeners/ZonesExplosionListener.java | 47 ++ .../listeners/ZonesSelectionListener.java | 77 ++ .../listeners/ZonesWorldListener.java | 24 + .../de/t14d3/zones/utils/ConfigManager.java | 93 --- .../de/t14d3/zones/utils/ConfigUpdater.java | 44 -- .../java/de/t14d3/zones/utils/Messages.java | 181 +++-- .../java/de/t14d3/zones/utils/TypeKeys.java | 40 ++ .../main/java/de/t14d3/zones/utils/Utils.java | 8 +- .../de/t14d3/zones/visuals/BeaconUtils.java | 41 +- .../de/t14d3/zones/visuals/FindBossbar.java | 14 +- .../t14d3/zones/visuals/ParticleHandler.java | 92 +-- api/src/main/resources/config.yml | 14 +- api/src/main/resources/messages.properties | 15 +- api/src/main/resources/messages.yml | 100 +++ api/src/main/resources/regions.yml | 45 +- build.gradle.kts | 30 +- bukkit/build.gradle.kts | 96 ++- .../zones/bukkit/BukkitPermissionManager.java | 27 +- .../de/t14d3/zones/bukkit/BukkitPlatform.java | 153 ++-- .../de/t14d3/zones/bukkit/BukkitTypes.java | 115 ++- .../de/t14d3/zones/bukkit/ZonesBukkit.java | 93 ++- .../zones/bukkit/commands/CancelCommand.java | 36 +- .../zones/bukkit/commands/CreateCommand.java | 80 ++- .../zones/bukkit/commands/CustomArgument.java | 81 --- .../zones/bukkit/commands/DeleteCommand.java | 22 +- .../zones/bukkit/commands/ExpandCommand.java | 36 +- .../zones/bukkit/commands/FindCommand.java | 35 + .../zones/bukkit/commands/ImportCommand.java | 20 +- .../zones/bukkit/commands/InfoCommand.java | 56 +- .../zones/bukkit/commands/ListCommand.java | 42 +- .../zones/bukkit/commands/LoadCommand.java | 12 +- .../zones/bukkit/commands/ModeCommand.java | 21 +- .../zones/bukkit/commands/PermCommand.java | 664 ++++++++++++++++++ .../zones/bukkit/commands/RenameCommand.java | 35 +- .../zones/bukkit/commands/RootCommand.java | 2 +- .../zones/bukkit/commands/SaveCommand.java | 12 +- .../zones/bukkit/commands/SelectCommand.java | 63 +- .../zones/bukkit/commands/SetCommand.java | 137 ---- .../bukkit/commands/SubCreateCommand.java | 100 +++ .../bukkit/commands/utils/CustomArgument.java | 41 ++ .../utils/CustomArgumentSuggestion.java | 63 ++ .../bukkit/integrations/FAWEIntegration.java | 57 +- .../bukkit/integrations/PlaceholderAPI.java | 46 +- .../bukkit/integrations/WorldEditExtent.java | 47 +- .../bukkit/integrations/WorldEditSession.java | 30 +- .../integrations/WorldGuardImporter.java | 213 +++++- .../bukkit/listeners/BlockEventListener.java | 152 ---- .../bukkit/listeners/ChunkEventListener.java | 14 - .../bukkit/listeners/EntityEventListener.java | 51 -- .../bukkit/listeners/ExplosivesListener.java | 238 ------- .../bukkit/listeners/PlayerEventListener.java | 441 ------------ .../bukkit/listeners/PlayerQuitListener.java | 8 +- .../bukkit/listeners/WorldEventListener.java | 3 +- fabric/build.gradle.kts | 31 +- .../zones/fabric/FabricPermissionManager.java | 12 +- .../de/t14d3/zones/fabric/FabricPlatform.java | 210 ++---- .../de/t14d3/zones/fabric/FabricTypes.java | 65 +- .../de/t14d3/zones/fabric/ZonesFabric.java | 105 +-- .../zones/fabric/commands/CancelCommand.java | 33 +- .../fabric/commands/CommandPermissions.java | 44 ++ .../zones/fabric/commands/CreateCommand.java | 105 +-- .../zones/fabric/commands/DeleteCommand.java | 54 +- .../zones/fabric/commands/ExpandCommand.java | 44 +- .../zones/fabric/commands/FindCommand.java | 27 +- .../zones/fabric/commands/InfoCommand.java | 61 +- .../zones/fabric/commands/ListCommand.java | 60 +- .../zones/fabric/commands/LoadCommand.java | 9 +- .../zones/fabric/commands/ModeCommand.java | 40 +- .../zones/fabric/commands/PermCommand.java | 616 ++++++++++++++++ .../zones/fabric/commands/RenameCommand.java | 37 +- .../zones/fabric/commands/RootCommand.java | 72 +- .../zones/fabric/commands/SaveCommand.java | 10 +- .../zones/fabric/commands/SelectCommand.java | 60 +- .../zones/fabric/commands/SetCommand.java | 138 ---- .../fabric/commands/SubCreateCommand.java | 141 ++-- .../fabric/listeners/PlayerListener.java | 196 ++---- gradle/libs.versions.toml | 76 ++ gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 5 +- 131 files changed, 7534 insertions(+), 4414 deletions(-) create mode 100644 api/src/main/java/de/t14d3/zones/WorldRegionManager.java delete mode 100644 api/src/main/java/de/t14d3/zones/objects/BlockLocation.java delete mode 100644 api/src/main/java/de/t14d3/zones/objects/Player.java delete mode 100644 api/src/main/java/de/t14d3/zones/objects/PlayerRepository.java delete mode 100644 api/src/main/java/de/t14d3/zones/objects/World.java create mode 100644 api/src/main/java/de/t14d3/zones/objects/shapes/CuboidShape.java create mode 100644 api/src/main/java/de/t14d3/zones/objects/shapes/GlobalShape.java create mode 100644 api/src/main/java/de/t14d3/zones/objects/shapes/ShapeProvider.java delete mode 100644 api/src/main/java/de/t14d3/zones/permissions/CacheEntry.java delete mode 100644 api/src/main/java/de/t14d3/zones/permissions/CacheUtils.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/Decision.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/GlobPattern.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/PermissionKeyRegistry.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/RegionMembership.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/RegionPermissionEvaluator.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/RegionPermissions.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/RegionPermissionsCodec.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/RegionSecurityCodec.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/flags/FlagContext.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/flags/FlagRegistry.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/flags/FlagValueKind.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/flags/IFlagRegistrar.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/subjects/GroupSubject.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/subjects/PlayerSubject.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/subjects/SubjectRef.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/subjects/Subjects.java create mode 100644 api/src/main/java/de/t14d3/zones/permissions/subjects/UniversalSubject.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/ZonesActionbar.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/ZonesExtraKeys.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/ZonesPermissionCache.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/ZonesRapunzelHooks.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesBlockListener.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesBucketListener.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesEntityListener.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesEventListeners.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesExplosionListener.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesSelectionListener.java create mode 100644 api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesWorldListener.java delete mode 100644 api/src/main/java/de/t14d3/zones/utils/ConfigManager.java delete mode 100644 api/src/main/java/de/t14d3/zones/utils/ConfigUpdater.java create mode 100644 api/src/main/java/de/t14d3/zones/utils/TypeKeys.java create mode 100644 api/src/main/resources/messages.yml delete mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CustomArgument.java create mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/commands/PermCommand.java delete mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SetCommand.java create mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/commands/utils/CustomArgument.java create mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/commands/utils/CustomArgumentSuggestion.java delete mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/BlockEventListener.java delete mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/ChunkEventListener.java delete mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/EntityEventListener.java delete mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/ExplosivesListener.java delete mode 100644 bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/PlayerEventListener.java create mode 100644 fabric/src/main/java/de/t14d3/zones/fabric/commands/CommandPermissions.java create mode 100644 fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java delete mode 100644 fabric/src/main/java/de/t14d3/zones/fabric/commands/SetCommand.java create mode 100644 gradle/libs.versions.toml diff --git a/.gitignore b/.gitignore index bd6b21a..c36898e 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,5 @@ runs/ # Include Project icon !.idea/icon.svg + +**/.profileconfig.json diff --git a/README.md b/README.md index b6fb550..9dc1bf8 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,17 @@ The plugin provides the following commands: * Only shows regions the player is a member of, unless the player has the `zones.list.other` permission * `/zone cancel`: Cancels the current region selection. * Usage: `/zone cancel` -* `/zone set`: Sets a permission for a member of a region. - * Usage: `/zone set ` - * `` is the key of the region. - * `` who to set the permission for. - * e.g., `Player1` - to create or modify a group, use `+group-GROUPNAME`, where GROUPNAME can be any name. - * `` is the permission to set (e.g., `role`, `break`, `place`, `interact`). - * `` is the value to set for the permission ( - * e.g., `owner`, `true`, `false`, `*`, `!*`, `GRASS_BLOCK`, `!GRASS_BLOCK`). +* `/zone perm`: Manage region permissions. + * Usage: + * `/zone perm allow ` + * `/zone perm deny ` + * `/zone perm unset ` + * `/zone perm clear ` + * `` can be a player name/UUID, `+universal`, `group:`, or `player:`. + * For pattern-based permissions (e.g. `break`/`place`/`interact`), `` supports `*` wildcards (glob, not + regex). + Use `*` or `all` for "everything". + * For membership permissions, `role`/`group` use `` as a role/group name. * `/zone rename`: Renames a region. * Usage: `/zone rename ` * `` is the key of the region, `` is the new name of the region. @@ -103,19 +106,20 @@ The plugin provides the following commands: ### Setting Permissions -1. Use the `/zone set ` command to set permissions for a member of a region. - * Example: `/zone set Player1 break true` allows "Player1" to break blocks in the specified region. - * Example: `/zone set Player2 break GRASS_BLOCK` allows "Player2" to break only `GRASS_BLOCK` blocks - in the specified region. - * Example: `/zone set Player3 interact !OAK_DOOR` denies "Player3" to interact with `OAK_DOOR`in - the specified region. - * Example: `/zone set Player4 place *` allows "Player4" to place all blocks in the specified region. - * Example: `/zone set Player5 break !*` denies "Player5" to break all blocks in the specified region. -* Groups: - * To assign a group, set the value of the permission `group` to the group name. - * e.g.: Create a group and assign permission: `/zone set +group-some-group-name break true` - * e.g. `/zone set ExamplePlayer group some-group-name` - * ExamplePlayer now inherits the permission `break` from the group `some-group-name` +1. Use `/zone perm ...` to add/remove rules for players, groups, or universal. + +* Allow everyone to break everything in a region: `/zone perm allow +universal break *` +* Allow Player1 to break everything in a region: `/zone perm allow Player1 break all` +* Allow only grass blocks: `/zone perm allow Player2 break grass_block` +* Deny interacting with oak doors: `/zone perm deny Player3 interact oak_door` +* Allow placing everything: `/zone perm allow Player4 place all` +* Deny breaking everything: `/zone perm deny Player5 break all` + +2. Groups: + +* Create/edit a group subject: `/zone perm allow group:builders break *` +* Group inheritance (group includes group): `/zone perm allow group:builders group members` +* Add a player to a group: `/zone perm allow ExamplePlayer group builders` ### Viewing Region Information diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 22bba10..27a6dba 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -11,11 +11,13 @@ repositories { } dependencies { - compileOnly("org.apache.logging.log4j:log4j-core:2.24.3") - compileOnly("it.unimi.dsi:fastutil:8.5.8") + compileOnly(rootProject.libs.log4j.core) + compileOnly(rootProject.libs.fastutil) - compileOnly("com.h2database:h2:2.3.232") - compileOnly("org.postgresql:postgresql:42.7.2") + compileOnly(rootProject.libs.h2) + compileOnly(rootProject.libs.postgresql) - implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") + + compileOnly(rootProject.libs.rapunzellib.api) + compileOnly(rootProject.libs.rapunzellib.events) } diff --git a/api/src/main/java/de/t14d3/zones/Region.java b/api/src/main/java/de/t14d3/zones/Region.java index 65a439a..23094db 100644 --- a/api/src/main/java/de/t14d3/zones/Region.java +++ b/api/src/main/java/de/t14d3/zones/Region.java @@ -1,8 +1,16 @@ package de.t14d3.zones; -import de.t14d3.zones.objects.*; -import de.t14d3.zones.permissions.PermissionManager; -import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.objects.shapes.CuboidShape; +import de.t14d3.zones.objects.shapes.GlobalShape; +import de.t14d3.zones.objects.shapes.ShapeProvider; +import de.t14d3.zones.permissions.RegionMembership; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.permissions.subjects.GroupSubject; +import de.t14d3.zones.permissions.subjects.PlayerSubject; +import de.t14d3.zones.permissions.subjects.SubjectRef; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -16,59 +24,38 @@ */ public class Region { private String name; - private BlockLocation min; - private BlockLocation max; - private World world; - private Map> members; + private ShapeProvider shape; + private final RegionPermissions permissions; + private final RegionMembership membership; private RegionKey key; private RegionKey parent; private int priority; - /** - * Constructs a new region with the given name, minimum and maximum locations, - * members and parent. - * - * @param name The name of the region (not unique). - * @param min The minimum BlockLocation of the region. - * @param max The maximum BlockLocation of the region. - * @param world The world of the region. - * @param members The members of the region. - * @param key The key of the region. - * @param parent The parent (if any) of the region. - * @param priority The priority of the region. - * @see #Region(String, BlockLocation, BlockLocation, World, Map, RegionKey, int) - */ - public Region(@NotNull String name, @NotNull BlockLocation min, @NotNull BlockLocation max, @NotNull World world, - Map> members, @NotNull RegionKey key, @Nullable RegionKey parent, + public Region(@NotNull String name, @NotNull ShapeProvider shape, + @NotNull RegionPermissions permissions, @NotNull RegionMembership membership, + @NotNull RegionKey key, @Nullable RegionKey parent, int priority) { // noinspection ConstantConditions this.name = name == null ? key.toString() : name; - this.min = min; - this.max = max; - this.world = world; - this.members = (members != null) ? members : new HashMap<>(); + this.shape = shape; + this.permissions = permissions != null ? permissions : new RegionPermissions(); + this.membership = membership != null ? membership : new RegionMembership(); this.key = key; this.parent = parent; this.priority = priority; } // Constructor overload for regions without parent + public Region(String name, ShapeProvider shape, RegionPermissions permissions, RegionMembership membership, + RegionKey key, int priority) { + this(name, shape, permissions, membership, key, null, priority); + } - /** - * Constructs a new region with the given name, minimum and maximum locations, - * and members. - * - * @param name The name of the region (not unique). - * @param min The minimum location of the region. - * @param max The maximum location of the region. - * @param members The members of the region. - * @param key The key of the region. - * @param priority The priority of the region. - * @see #Region(String, BlockLocation, BlockLocation, World, Map, RegionKey, int) - */ - public Region(String name, BlockLocation min, BlockLocation max, World world, Map> members, - RegionKey key, int priority) { - this(name, min, max, world, members, key, null, priority); + public Region(@NotNull String name, @NotNull RBlockPos min, @NotNull RBlockPos max, @NotNull RWorldRef world, + @NotNull RegionPermissions permissions, @NotNull RegionMembership membership, + @NotNull RegionKey key, @Nullable RegionKey parent, + int priority) { + this(name, new CuboidShape(min, max, world), permissions, membership, key, parent, priority); } // Getters and Setters @@ -81,38 +68,60 @@ public void setName(String name, RegionManager regionManager) { regionManager.saveRegion(key, this); // Ensure changes are saved } - public BlockLocation getMin() { - return min; + public RBlockPos getMin() { + if (shape instanceof CuboidShape cuboid) { + return cuboid.getMin(); + } + return null; // For non-cuboid shapes } - void setMin(BlockLocation min) { - this.min = min; + void setMin(RBlockPos min) { + if (shape instanceof CuboidShape cuboid) { + // Note: This creates a new shape, may break references + this.shape = new CuboidShape(min, cuboid.getMax(), cuboid.getWorld()); + } } public String getMinString() { - return min.getX() + "," + min.getY() + "," + min.getZ(); + RBlockPos min = getMin(); + return min != null ? min.x() + "," + min.y() + "," + min.z() : null; } - public BlockLocation getMax() { - return max; + public RBlockPos getMax() { + if (shape instanceof CuboidShape cuboid) { + return cuboid.getMax(); + } + return null; // For non-cuboid shapes } - void setMax(BlockLocation max) { - this.max = max; + void setMax(RBlockPos max) { + if (shape instanceof CuboidShape cuboid) { + this.shape = new CuboidShape(cuboid.getMin(), max, cuboid.getWorld()); + } } public String getMaxString() { - return max.getX() + "," + max.getY() + "," + max.getZ(); + RBlockPos max = getMax(); + return max != null ? max.x() + "," + max.y() + "," + max.z() : null; } /** - * Get the members of this region and their permissions.
- * Use {@link PermissionManager} to check player permissions. - * - * @return {@code Map permissions>} + * Returns the permission set backing this region. + */ + public @NotNull RegionPermissions getPermissions() { + return permissions; + } + + public @NotNull RegionMembership getMembership() { + return membership; + } + + /** + * @deprecated Use {@link #getPermissions()}. */ - public Map> getMembers() { - return members; + @Deprecated(since = "0.6.0", forRemoval = false) + public RegionPermissions getPermissionsV2() { + return getPermissions(); } /** @@ -122,13 +131,12 @@ public Map> getMembers() { * @since 0.1.5 */ public List getGroupNames() { - List groupNames = new ArrayList<>(); - members.keySet().forEach(key -> { - if (key.startsWith("+group-")) { - groupNames.add(key); - } - }); - return groupNames; + LinkedHashSet names = new LinkedHashSet<>(); + for (SubjectRef subject : permissions.subjects().keySet()) { + if (subject instanceof GroupSubject g) names.add(g.name()); + } + names.addAll(membership.groups().keySet()); + return new ArrayList<>(names); } /** @@ -140,33 +148,74 @@ public List getGroupNames() { */ public List getGroupMembers(String group) { List groupMembers = new ArrayList<>(); - members.forEach((subject, flags) -> { - flags.forEach(flag -> { - if (flag.getFlag() == Flags.GROUP && flag.getValue(group.substring(7)).equals(Result.TRUE)) { - groupMembers.add(subject); - } - }); - }); + String groupName = RegionMembership.normalizeName(group); + if (groupName == null) return groupMembers; + + for (Map.Entry e : membership.players().entrySet()) { + UUID uuid = e.getKey(); + RegionMembership.PlayerEntry entry = e.getValue(); + if (uuid == null || entry == null) continue; + if (entry.groups().allowValues().contains(groupName)) groupMembers.add(uuid.toString()); + } + return groupMembers; } public boolean isMember(UUID uuid) { - return this.members.containsKey(uuid.toString()); + return isMember(uuid, null); } public boolean isAdmin(UUID uuid) { - if (members.containsKey(uuid.toString())) { - for (RegionFlagEntry entry : members.get(uuid.toString())) { - if (entry.getFlag().name().equalsIgnoreCase("role")) { - return entry.getValue("admin").equals(Result.TRUE) || entry.getValue("owner").equals(Result.TRUE); - } - } + return isAdmin(uuid, null); + } + + public boolean isMember(UUID uuid, @Nullable RegionManager regionManager) { + if (uuid == null) return false; + if (membership.players().containsKey(uuid)) return true; + if (permissions.subjects().containsKey(new PlayerSubject(uuid))) return true; + + if (regionManager == null) return false; + Region current = this; + Set visited = null; + while (current != null) { + if (current.membership.players().containsKey(uuid)) return true; + if (current.permissions.subjects().containsKey(new PlayerSubject(uuid))) return true; + + if (current.parent == null) break; + if (visited == null) visited = new HashSet<>(); + if (!visited.add(current.key.getValue())) break; + current = current.getParentRegion(regionManager); } - return false; // Default to false + return false; } - public List getMemberPermissions(String who) { - return this.members.get(who); + public boolean isAdmin(UUID uuid, @Nullable RegionManager regionManager) { + if (uuid == null) return false; + if (regionManager == null) { + RegionMembership.PlayerEntry entry = membership.players().get(uuid); + if (entry == null) return false; + return entry.roles().allowValues().contains("owner") || entry.roles().allowValues().contains("admin"); + } + + ArrayDeque lineage = new ArrayDeque<>(); + Region current = this; + Set visited = null; + while (current != null) { + lineage.addFirst(current); + if (current.parent == null) break; + if (visited == null) visited = new HashSet<>(); + if (!visited.add(current.key.getValue())) break; + current = current.getParentRegion(regionManager); + } + + Set roles = new HashSet<>(); + for (Region r : lineage) { + RegionMembership.PlayerEntry entry = r.membership.players().get(uuid); + if (entry == null) continue; + for (String deny : entry.roles().denyValues()) roles.remove(deny); + for (String allow : entry.roles().allowValues()) roles.add(allow); + } + return roles.contains("owner") || roles.contains("admin"); } public RegionKey getParent() { @@ -175,22 +224,24 @@ public RegionKey getParent() { void setParent(RegionKey parent, RegionManager regionManager) { this.parent = parent; - regionManager.saveRegion(key, this); // Ensure changes are saved + regionManager.saveRegion(key, this); // Ensure changes are saved } - public Region getParentRegion(RegionManager regionManager) { - return regionManager.regions().get(parent.getValue()); - } + public @Nullable Region getParentRegion(RegionManager regionManager) { + if (regionManager == null) return null; + if (parent == null) return null; - @ApiStatus.Experimental - public Region getParentRegion() { - return RegionManager.getRegion(parent); + Region parentRegion = regionManager.getRegion(parent); + if (parentRegion == null) return null; + if (!sameWorld(parentRegion.getWorld(), getWorld())) return null; + return parentRegion; } public List getChildren(RegionManager regionManager) { List children = new ArrayList<>(); for (Region region : regionManager.regions().values()) { - if (region.getParent().equals(key)) { + RegionKey p = region.getParent(); + if (p != null && p.equals(key) && sameWorld(region.getWorld(), getWorld())) { children.add(region); } } @@ -210,64 +261,71 @@ void setKey(RegionKey key, RegionManager regionManager) { } - public boolean contains(BlockLocation vec) { - return getBounds().contains(vec); + public boolean contains(RBlockPos vec) { + return shape.contains(vec); } - public boolean intersects(@NotNull BlockLocation min, @NotNull BlockLocation max, World world) { - return getBounds().intersects(min, max, world); + public boolean intersects(@NotNull RBlockPos min, @NotNull RBlockPos max, RWorldRef world) { + if (sameWorld(shape.getWorld(), world) && shape instanceof CuboidShape cuboid) { + return getBoundingBox() != null && getBoundingBox().intersects(min, max, world); + } + // For global or other shapes, check if any point is inside + return shape.intersects(new CuboidShape(min, max, world)); } - public Box getBounds() { - return new Box(min, max, world); + private static boolean sameWorld(@Nullable RWorldRef a, @Nullable RWorldRef b) { + if (a == null || b == null) return false; + return Objects.equals(a.identifier(), b.identifier()); } - public void addMemberPermission(UUID uuid, String permission, String value, RegionManager regionManager) { - addMemberPermission(uuid.toString(), permission, value, regionManager); + public Box getBoundingBox() { + return shape.getBoundingBox(); } - public void addMemberPermission(String who, String permission, String value, RegionManager regionManager) { - List entries = this.members.computeIfAbsent(who, k -> new ArrayList<>()); - for (RegionFlagEntry entry : entries) { - if (entry.getFlagValue().equalsIgnoreCase(permission)) { - boolean inverted = value.startsWith("!"); - entry.setValue(value.replaceFirst("!", ""), inverted); - regionManager.saveRegion(key, this); - return; - } - } - Zones.getInstance().getDebugLogger().log("Entry does not exist, adding", permission); - entries.add(new RegionFlagEntry(permission)); - regionManager.saveRegion(key, this); + @Deprecated + public Box getBounds() { + return getBoundingBox(); } - public void addMemberPermissions(String who, List entries, RegionManager regionManager) { - this.members.put(who, entries); - regionManager.saveRegion(key, this); + public @Nullable UUID getOwner() { + return getOwner(null); } - public void removeMemberPermission(String who, String permission, String value, RegionManager regionManager) { - List entries = this.members.get(who); - if (entries != null) { - for (RegionFlagEntry entry : entries) { - if (entry.getFlagValue().equalsIgnoreCase(permission)) { - entry.removeValue(value); - regionManager.saveRegion(key, this); - return; - } + public @Nullable UUID getOwner(@Nullable RegionManager regionManager) { + if (regionManager == null) { + for (Map.Entry e : membership.players().entrySet()) { + UUID uuid = e.getKey(); + RegionMembership.PlayerEntry entry = e.getValue(); + if (uuid == null || entry == null) continue; + if (entry.roles().allowValues().contains("owner")) return uuid; } + return null; } - } - public @Nullable UUID getOwner() { - for (Map.Entry> e : members.entrySet()) { - for (RegionFlagEntry entry : e.getValue()) { - if (entry.getFlagValue().equalsIgnoreCase("role")) { - if (entry.getValue("owner").equals(Result.TRUE)) { - return UUID.fromString(e.getKey()); - } - } + ArrayDeque lineage = new ArrayDeque<>(); + Region current = this; + Set visited = null; + while (current != null) { + lineage.addFirst(current); + if (current.parent == null) break; + if (visited == null) visited = new HashSet<>(); + if (!visited.add(current.key.getValue())) break; + current = current.getParentRegion(regionManager); + } + + LinkedHashSet candidates = new LinkedHashSet<>(); + for (Region r : lineage) candidates.addAll(r.membership.players().keySet()); + + for (UUID uuid : candidates) { + if (uuid == null) continue; + Set roles = new HashSet<>(); + for (Region r : lineage) { + RegionMembership.PlayerEntry entry = r.membership.players().get(uuid); + if (entry == null) continue; + for (String deny : entry.roles().denyValues()) roles.remove(deny); + for (String allow : entry.roles().allowValues()) roles.add(allow); } + if (roles.contains("owner")) return uuid; } return null; } @@ -284,16 +342,26 @@ public void setPriority(int priority) { this.priority = priority; } - public World getWorld() { - return world; + public RWorldRef getWorld() { + return shape.getWorld(); } /** * Set the world of the region. * Should very likely never be used. + * This creates a new shape with the new world. */ @ApiStatus.Internal - public void setWorld(World world) { - this.world = world; + public void setWorld(RWorldRef world) { + if (shape instanceof CuboidShape cuboid) { + this.shape = new CuboidShape(cuboid.getMin(), cuboid.getMax(), world); + } else if (shape instanceof GlobalShape) { + this.shape = new GlobalShape(world); + } + // For future shapes, add handling + } + + public ShapeProvider getShape() { + return shape; } } diff --git a/api/src/main/java/de/t14d3/zones/RegionKey.java b/api/src/main/java/de/t14d3/zones/RegionKey.java index 0ed866c..96ab088 100644 --- a/api/src/main/java/de/t14d3/zones/RegionKey.java +++ b/api/src/main/java/de/t14d3/zones/RegionKey.java @@ -29,7 +29,7 @@ public RegionKey() { this.value = RegionKey.generate().getValue(); } - // Retrieve the integer value + // Retrieve the integer name public int getValue() { return value; } @@ -40,7 +40,7 @@ public String toString() { return String.format("%08x", value); } - // Equality and hash code based on the stored value + // Equality and hash code based on the stored name @Override public boolean equals(Object obj) { if (this == obj) return true; diff --git a/api/src/main/java/de/t14d3/zones/RegionManager.java b/api/src/main/java/de/t14d3/zones/RegionManager.java index e990032..03a66ba 100644 --- a/api/src/main/java/de/t14d3/zones/RegionManager.java +++ b/api/src/main/java/de/t14d3/zones/RegionManager.java @@ -1,41 +1,57 @@ package de.t14d3.zones; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.datasource.DataSourceManager; -import de.t14d3.zones.objects.*; -import de.t14d3.zones.permissions.CacheUtils; -import de.t14d3.zones.permissions.PermissionManager; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.objects.Direction; +import de.t14d3.zones.objects.shapes.CuboidShape; +import de.t14d3.zones.objects.shapes.ShapeProvider; +import de.t14d3.zones.permissions.RegionMembership; +import de.t14d3.zones.permissions.RegionPermissions; import de.t14d3.zones.utils.Utils; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.jetbrains.annotations.Nullable; import java.util.*; - +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * Loads, stores, and queries regions. + * + *

Regions are kept in memory and additionally indexed per-world for efficient spatial queries. + * Callers should use the provided {@code withWorldReadLock}/{@code withWorldWriteLock} helpers when + * interacting with world-backed state.

+ */ public class RegionManager { private final DataSourceManager dataSourceManager; private final Zones plugin; - private static RegionManager instance; - private final ZonesPlatform platform; + // Global facade; per-world managers are stored separately. - private final Int2ObjectOpenHashMap loadedRegions = new Int2ObjectOpenHashMap<>(); - private final Map> worldRegions = new HashMap<>(); - private final Map>> chunkRegions = new HashMap<>(); + private final ConcurrentHashMap loadedRegions = new ConcurrentHashMap<>(); + private final ConcurrentHashMap worldManagers = new ConcurrentHashMap<>(); - public RegionManager(Zones plugin, PermissionManager permissionManager) { + public RegionManager(Zones plugin) { this.plugin = plugin; this.dataSourceManager = new DataSourceManager(plugin); - this.platform = plugin.getPlatform(); - instance = this; } + /** + * Returns the data source manager responsible for loading/saving regions. + */ public DataSourceManager getDataSourceManager() { return dataSourceManager; } + /** + * Forces saving all currently loaded regions. + */ public void saveRegions() { - dataSourceManager.saveRegions(loadedRegions.values().stream().toList()); - CacheUtils.getInstance().invalidateInteractionCaches(); + withAllWorldReadLocks(() -> { + dataSourceManager.saveRegions(new ArrayList<>(loadedRegions.values())); + return null; + }); } /** @@ -51,19 +67,17 @@ public void triggerSave() { } /** - * Loads regions from the YAML file into memory. + * Reloads all regions from the configured data source into memory. */ public void loadRegions() { - loadedRegions.clear(); - worldRegions.clear(); - platform.getWorlds().forEach(world -> worldRegions.put(world.getUID(), new Int2ObjectOpenHashMap<>())); - dataSourceManager.loadRegions(); - } - - public void loadRegions(World world) { - worldRegions.computeIfAbsent(world.getUID(), k -> new Int2ObjectOpenHashMap<>()); - worldRegions.get(world.getUID()).clear(); - dataSourceManager.loadRegions(); + clearAll(); + List regions = dataSourceManager.loadRegions(); + if (regions != null) { + for (Region region : regions) { + addRegion(region); + } + } + plugin.getPermissionManager().invalidateAll(); } /** @@ -71,12 +85,35 @@ public void loadRegions(World world) { * * @return A map of region keys and their corresponding {@link de.t14d3.zones.Region} objects. */ - public Int2ObjectOpenHashMap regions() { - return this.loadedRegions; + public Map regions() { + return Collections.unmodifiableMap(loadedRegions); } - public Int2ObjectOpenHashMap regions(World world) { - return this.worldRegions.get(world.getUID()); + public @Nullable Region getRegion(@Nullable RegionKey key) { + if (key == null) return null; + return loadedRegions.get(key.getValue()); + } + + public void clearAll() { + List managers = new ArrayList<>(worldManagers.values()); + for (WorldRegionManager manager : managers) { + manager.withWriteLock(manager::clearLocked); + } + loadedRegions.clear(); + worldManagers.clear(); + } + + private T withAllWorldReadLocks(Supplier action) { + List worldIds = new ArrayList<>(worldManagers.keySet()); + worldIds.sort(String::compareTo); + return withAllWorldReadLocks(worldIds, 0, action); + } + + private T withAllWorldReadLocks(List worldIds, int index, Supplier action) { + if (index >= worldIds.size()) return action.get(); + WorldRegionManager manager = worldManagers.get(worldIds.get(index)); + if (manager == null) return withAllWorldReadLocks(worldIds, index + 1, action); + return manager.withReadLock(() -> withAllWorldReadLocks(worldIds, index + 1, action)); } // Save a region to the configured data source @@ -91,35 +128,56 @@ public void saveRegion(RegionKey key, Region region) { * @param regionKey The key of the region to delete */ public void deleteRegion(RegionKey regionKey) { - regions().remove(regionKey.getValue()); + if (regionKey == null) return; + Region region = loadedRegions.get(regionKey.getValue()); + if (region == null) return; + + RWorldRef world = region.getWorld(); + if (world != null) { + WorldRegionManager manager = getOrCreateWorldManager(world); + manager.withWriteLock(() -> { + loadedRegions.remove(regionKey.getValue()); + manager.removeRegionLocked(region); + }); + } else { + loadedRegions.remove(regionKey.getValue()); + } + triggerSave(); - CacheUtils.getInstance().invalidateInteractionCaches(); + plugin.getPermissionManager().invalidateAll(); } /** - * Creates a new region with the specified key, name, minimum and maximum locations, members and their permissions. + * Creates a new region with the specified shape. * * @param key The key of the new region. * @param name The name of the new region. - * @param min The minimum location of the new region. - * @param max The maximum location of the new region. - * @param members The members of the new region. + * @param shape The shape of the new region. + * @param permissions The permissions of the new region. + * @param parent The parent region. + * @param priority The priority of the region. * @return The newly created region. */ - public Region createNewRegion(String name, BlockLocation min, BlockLocation max, World world, Map> members, RegionKey key, RegionKey parent, int priority) { - Region newRegion = new Region(name, min, max, world, members, key, 0); + public Region createNewRegion(String name, ShapeProvider shape, RegionPermissions permissions, RegionKey key, RegionKey parent, int priority) { + Region newRegion = new Region(name, shape, permissions, new RegionMembership(), key, parent, priority); - CacheUtils.getInstance().invalidateInteractionCaches(); + plugin.getPermissionManager().invalidateAll(); saveRegion(key, newRegion); - loadedRegions.put(key.getValue(), newRegion); - worldRegions.computeIfAbsent(newRegion.getWorld().getUID(), k -> new Int2ObjectOpenHashMap<>()) - .put(newRegion.getKey().getValue(), newRegion); - indexRegion(newRegion); + addRegion(newRegion); return newRegion; } - public Region createNewRegion(RegionKey key, String name, BlockLocation min, BlockLocation max, World world, Map> members, int priority) { - return createNewRegion(name, min, max, world, members, key, null, priority); + public Region createNewRegion(RegionKey key, String name, ShapeProvider shape, RegionPermissions permissions, int priority) { + return createNewRegion(name, shape, permissions, key, null, priority); + } + + public Region createNewRegion(String name, RBlockPos min, RBlockPos max, RWorldRef world, RegionPermissions permissions, RegionKey key, RegionKey parent, int priority) { + CuboidShape shape = new CuboidShape(min, max, world); + return createNewRegion(name, shape, permissions, key, parent, priority); + } + + public Region createNewRegion(RegionKey key, String name, RBlockPos min, RBlockPos max, RWorldRef world, RegionPermissions permissions, int priority) { + return createNewRegion(name, min, max, world, permissions, key, null, priority); } /** @@ -130,57 +188,25 @@ public Region createNewRegion(RegionKey key, String name, BlockLocation min, Blo * @param min The minimum location of the new region. * @param max The maximum location of the new region. * @param playerUUID The UUID of the player who will own the new region. - * @param ownerPermissions The permissions that the player will have for the new region. * @param parentRegion The parent region of the new region. */ - public Region createSubRegion(String name, BlockLocation min, BlockLocation max, World world, UUID playerUUID, List ownerPermissions, Region parentRegion) { + public Region createSubRegion(String name, RBlockPos min, RBlockPos max, RWorldRef world, UUID playerUUID, Region parentRegion) { RegionKey regionKey = RegionKey.generate(); - Map> members = new HashMap<>(); - members.put(playerUUID.toString(), ownerPermissions); - Region newRegion = new Region(name, min, max, world, members, regionKey, parentRegion.getKey(), 0); + RegionPermissions permissions = new RegionPermissions(); + RegionMembership membership = new RegionMembership(); + membership.player(playerUUID).roles().allow("owner"); - CacheUtils.getInstance().invalidateInteractionCaches(); + Region newRegion = new Region(name, min, max, world, permissions, membership, regionKey, parentRegion.getKey(), + 0); + + plugin.getPermissionManager().invalidateAll(); saveRegion(regionKey, newRegion); - loadedRegions.put(regionKey.getValue(), newRegion); - worldRegions.computeIfAbsent(newRegion.getWorld().getUID(), k -> new Int2ObjectOpenHashMap<>()) - .put(newRegion.getKey().getValue(), newRegion); - indexRegion(newRegion); + addRegion(newRegion); return newRegion; } - /** - * Adds a permission to a player's membership in a region. - * - * @param uuid The UUID of the player. - * @param permission The permission to add. - * @param value The value of the permission. - * @param key The key of the region. - */ - public void addMemberPermission(UUID uuid, String permission, String value, RegionKey key) { - CacheUtils.getInstance().invalidateInteractionCache(uuid); - CacheUtils.getInstance().invalidateCache(uuid.toString()); - Region region = regions().get(key.getValue()); - region.addMemberPermission(uuid, permission, value, this); - } - - public void addMemberPermission(String who, String permission, String value, RegionKey key) { - CacheUtils.getInstance().invalidateInteractionCache(who); - CacheUtils.getInstance().invalidateCache(who); - Region region = regions().get(key.getValue()); - region.addMemberPermission(who, permission, value, this); - } - - public void addMemberPermissions(String who, String permission, List values, RegionKey key) { - CacheUtils.getInstance().invalidateInteractionCache(who); - CacheUtils.getInstance().invalidateCache(who); - Region region = regions().get(key.getValue()); - for (String value : values) { - region.addMemberPermission(who, permission, value, this); - } - } - - public boolean overlapsExistingRegion(BlockLocation min, BlockLocation max, World world) { + public boolean overlapsExistingRegion(RBlockPos min, RBlockPos max, RWorldRef world) { return overlapsExistingRegion(min, max, world, null); } @@ -188,13 +214,10 @@ public boolean overlapsExistingRegion(Box box) { return overlapsExistingRegion(box.getMin(), box.getMax(), box.getWorld(), null); } - public boolean overlapsExistingRegion(BlockLocation min, BlockLocation max, World world, @Nullable RegionKey keyToIgnore) { - for (Region region : worldRegions.get(world.getUID()).values()) { - if (region.intersects(min, max, world) && !region.getKey().equals(keyToIgnore)) { - return true; - } - } - return false; + public boolean overlapsExistingRegion(RBlockPos min, RBlockPos max, RWorldRef world, @Nullable RegionKey keyToIgnore) { + if (min == null || max == null || world == null) return false; + WorldRegionManager manager = getOrCreateWorldManager(world); + return manager.withReadLock(() -> manager.overlapsExistingRegionLocked(min, max, world, keyToIgnore)); } /** @@ -203,20 +226,10 @@ public boolean overlapsExistingRegion(BlockLocation min, BlockLocation max, Worl * @param location The location to check for overlaps. * @return A list of regions that overlap with the given location. */ - public List getRegionsAt(BlockLocation location, World world) { - List foundRegions = new ArrayList<>(); - if (world == null) return foundRegions; - - int xChunk = location.getX() >> 4; - int zChunk = location.getZ() >> 4; - long key = ((long) xChunk << 32) | (zChunk & 0xFFFFFFFFL); - - List candidates = chunkRegions.getOrDefault(world.getUID(), new Long2ObjectOpenHashMap<>()) - .getOrDefault(key, Collections.emptyList()); - for (Region region : candidates) { - if (region.contains(location)) foundRegions.add(region); - } - return foundRegions; + public List getRegionsAt(RBlockPos location, RWorldRef world) { + if (world == null || location == null) return Collections.emptyList(); + WorldRegionManager manager = getOrCreateWorldManager(world); + return manager.withReadLock(() -> manager.getRegionsAtLocked(location)); } /** @@ -225,38 +238,10 @@ public List getRegionsAt(BlockLocation location, World world) { * @param location Location to check * @return Region at location, or null if no region found */ - public @Nullable Region getEffectiveRegionAt(BlockLocation location, World world) { - List regions = getRegionsAt(location, world); - int priority = Integer.MIN_VALUE; - Region effectiveRegion = null; - for (Region region : regions) { - if (region.getPriority() > priority) { - effectiveRegion = region; - priority = region.getPriority(); - } - } - return effectiveRegion; - } - - private void indexRegion(Region region) { - World world = region.getWorld(); - Long2ObjectOpenHashMap> worldChunks = chunkRegions.computeIfAbsent(world.getUID(), - k -> new Long2ObjectOpenHashMap<>()); - - BlockLocation min = region.getMin(); - BlockLocation max = region.getMax(); - - int minXChunk = min.getX() >> 4; - int minZChunk = min.getZ() >> 4; - int maxXChunk = max.getX() >> 4; - int maxZChunk = max.getZ() >> 4; - - for (int x = minXChunk; x <= maxXChunk; x++) { - for (int z = minZChunk; z <= maxZChunk; z++) { - long key = ((long) x << 32) | (z & 0xFFFFFFFFL); - worldChunks.computeIfAbsent(key, k -> new ArrayList<>()).add(region); - } - } + public @Nullable Region getEffectiveRegionAt(RBlockPos location, RWorldRef world) { + if (world == null || location == null) return null; + WorldRegionManager manager = getOrCreateWorldManager(world); + return manager.withReadLock(() -> manager.getEffectiveRegionAtLocked(location)); } /** @@ -267,59 +252,22 @@ private void indexRegion(Region region) { * @param min The new minimum location of the region. * @param max The new maximum location of the region. */ - public void redefineBounds(Region region, BlockLocation min, BlockLocation max) { - BlockLocation oldMin = region.getMin(); - BlockLocation oldMax = region.getMax(); - region.setMin(min); - region.setMax(max); - updateRegionInSpatialIndex(region, oldMin, oldMax); + public void redefineBounds(Region region, RBlockPos min, RBlockPos max) { + if (region == null) return; + RWorldRef world = region.getWorld(); + if (world == null) return; + + WorldRegionManager manager = getOrCreateWorldManager(world); + manager.withWriteLock(() -> { + RBlockPos oldMin = region.getMin(); + RBlockPos oldMax = region.getMax(); + region.setMin(min); + region.setMax(max); + manager.updateCuboidLocked(region, oldMin, oldMax); + }); triggerSave(); } - public void updateRegionInSpatialIndex(Region region, BlockLocation oldMin, BlockLocation oldMax) { - World world = region.getWorld(); - Map> worldChunks = chunkRegions.get(world.getUID()); - if (worldChunks == null) return; - - // Remove from old chunks - removeRegionFromChunks(region, oldMin, oldMax, worldChunks); - - // Add to new chunks - addRegionToChunks(region, region.getMin(), region.getMax(), worldChunks); - } - - private static void removeRegionFromChunks(Region region, BlockLocation min, BlockLocation max, Map> worldChunks) { - int minXChunk = min.getX() >> 4; - int minZChunk = min.getZ() >> 4; - int maxXChunk = max.getX() >> 4; - int maxZChunk = max.getZ() >> 4; - - for (int x = minXChunk; x <= maxXChunk; x++) { - for (int z = minZChunk; z <= maxZChunk; z++) { - long key = ((long) x << 32) | (z & 0xFFFFFFFFL); - List regions = worldChunks.get(key); - if (regions != null) { - regions.remove(region); - if (regions.isEmpty()) worldChunks.remove(key); - } - } - } - } - - private static void addRegionToChunks(Region region, BlockLocation min, BlockLocation max, Map> worldChunks) { - int minXChunk = min.getX() >> 4; - int minZChunk = min.getZ() >> 4; - int maxXChunk = max.getX() >> 4; - int maxZChunk = max.getZ() >> 4; - - for (int x = minXChunk; x <= maxXChunk; x++) { - for (int z = minZChunk; z <= maxZChunk; z++) { - long key = ((long) x << 32) | (z & 0xFFFFFFFFL); - worldChunks.computeIfAbsent(key, k -> new ArrayList<>()).add(region); - } - } - } - /** * Expands the bounds of a region in a given direction by a given amount. * Only succeeds if the region does not overlap with any other regions @@ -333,75 +281,44 @@ private static void addRegionToChunks(Region region, BlockLocation min, BlockLoc * @see #expandBounds(Region, Direction, int) */ public boolean expandBounds(Region region, Direction direction, int amount, boolean allowOverlap) { - BlockLocation newMin = region.getMin().clone(); - BlockLocation newMax = region.getMax().clone(); - - switch (direction) { - case NORTH: - newMin.setZ(newMin.getZ() - amount); - break; - case SOUTH: - newMax.setZ(newMax.getZ() + amount); - break; - case EAST: - newMax.setX(newMax.getX() + amount); - break; - case WEST: - newMin.setX(newMin.getX() - amount); - break; - case UP: - newMax.setY(newMax.getY() + amount); - break; - case DOWN: - newMin.setY(newMin.getY() - amount); - break; - } + if (region == null) return false; + RWorldRef world = region.getWorld(); + if (world == null) return false; + + WorldRegionManager manager = getOrCreateWorldManager(world); + boolean success = manager.withWriteLock(() -> { + RBlockPos oldMin = region.getMin(); + RBlockPos oldMax = region.getMax(); + if (oldMin == null || oldMax == null) return false; + + RBlockPos newMin = oldMin; + RBlockPos newMax = oldMax; + + switch (direction) { + case NORTH -> newMin = new RBlockPos(newMin.x(), newMin.y(), newMin.z() - amount); + case SOUTH -> newMax = new RBlockPos(newMax.x(), newMax.y(), newMax.z() + amount); + case EAST -> newMax = new RBlockPos(newMax.x() + amount, newMax.y(), newMax.z()); + case WEST -> newMin = new RBlockPos(newMin.x() - amount, newMin.y(), newMin.z()); + case UP -> newMax = new RBlockPos(newMax.x(), newMax.y() + amount, newMax.z()); + case DOWN -> newMin = new RBlockPos(newMin.x(), newMin.y() - amount, newMin.z()); + } + + if (!allowOverlap && manager.overlapsExistingRegionLocked(newMin, newMax, world, region.getKey())) { + return false; + } - if (allowOverlap) { region.setMin(newMin); region.setMax(newMax); - triggerSave(); + manager.updateCuboidLocked(region, oldMin, oldMax); return true; - } + }); - if (overlapsExistingRegion(newMin, newMax, region.getWorld(), region.getKey())) { - return false; - } - - region.setMin(newMin); - region.setMax(newMax); - triggerSave(); - return true; + if (success) triggerSave(); + return success; } public void expandBounds(Region region, Direction direction, int amount) { - BlockLocation newMin = region.getMin().clone(); - BlockLocation newMax = region.getMax().clone(); - - switch (direction) { - case NORTH: - newMin.setZ(newMin.getZ() - amount); - break; - case SOUTH: - newMax.setZ(newMax.getZ() + amount); - break; - case EAST: - newMax.setX(newMax.getX() + amount); - break; - case WEST: - newMin.setX(newMin.getX() - amount); - break; - case UP: - newMax.setY(newMax.getY() + amount); - break; - case DOWN: - newMin.setY(newMin.getY() - amount); - break; - } - - region.setMin(newMin); - region.setMax(newMax); - triggerSave(); + expandBounds(region, direction, amount, true); } /** @@ -412,18 +329,58 @@ public void expandBounds(Region region, Direction direction, int amount) { * @see #createNewRegion */ public void addRegion(Region region) { - loadedRegions.put(region.getKey().getValue(), region); - worldRegions.computeIfAbsent(region.getWorld().getUID(), k -> new Int2ObjectOpenHashMap<>()) - .put(region.getKey().getValue(), region); - indexRegion(region); // Ensure the region is indexed + if (region == null) return; + RWorldRef world = region.getWorld(); + if (world == null) { + loadedRegions.put(region.getKey().getValue(), region); + return; + } + + WorldRegionManager manager = getOrCreateWorldManager(world); + manager.withWriteLock(() -> { + loadedRegions.put(region.getKey().getValue(), region); + manager.addRegionLocked(region); + }); } - public static Region getRegion(RegionKey key) { - if (instance == null) { - throw new IllegalStateException("RegionManager is not yet initialized!"); - } - return instance.loadedRegions.get(key.getValue()); + public T withWorldReadLock(@Nullable RWorldRef world, Supplier action) { + if (action == null) throw new IllegalArgumentException("action"); + if (world == null) return action.get(); + WorldRegionManager manager = getOrCreateWorldManager(world); + return manager.withReadLock(action); + } + + public void withWorldReadLock(@Nullable RWorldRef world, Runnable action) { + withWorldReadLock(world, () -> { + action.run(); + return null; + }); + } + + public T withWorldWriteLock(@Nullable RWorldRef world, Supplier action) { + if (action == null) throw new IllegalArgumentException("action"); + if (world == null) return action.get(); + WorldRegionManager manager = getOrCreateWorldManager(world); + return manager.withWriteLock(action); } + public void withWorldWriteLock(@Nullable RWorldRef world, Runnable action) { + withWorldWriteLock(world, () -> { + action.run(); + return null; + }); + } + + private WorldRegionManager getOrCreateWorldManager(RWorldRef world) { + String worldId = world == null ? "" : world.identifier(); + if (worldId == null) worldId = ""; + + WorldRegionManager existing = worldManagers.get(worldId); + if (existing != null) return existing; + + WorldRegionManager created = new WorldRegionManager(worldId); + WorldRegionManager race = worldManagers.putIfAbsent(worldId, created); + return race != null ? race : created; + } } diff --git a/api/src/main/java/de/t14d3/zones/WorldRegionManager.java b/api/src/main/java/de/t14d3/zones/WorldRegionManager.java new file mode 100644 index 0000000..89696c0 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/WorldRegionManager.java @@ -0,0 +1,208 @@ +package de.t14d3.zones; + +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.objects.shapes.CuboidShape; +import de.t14d3.zones.objects.shapes.GlobalShape; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; + +/** + * World-scoped region manager for fast lookups and isolated locking (Folia-friendly). + * + *

This class is intentionally stateful and uses a per-world lock to keep the + * hot-path lookups safe under concurrent reads/writes.

+ */ +final class WorldRegionManager { + private final String worldId; + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + private final Int2ObjectOpenHashMap regions = new Int2ObjectOpenHashMap<>(); + private final Long2ObjectOpenHashMap> chunkRegions = new Long2ObjectOpenHashMap<>(); + private final Set globalRegions = new HashSet<>(); + + WorldRegionManager(String worldId) { + this.worldId = worldId == null ? "" : worldId; + } + + String worldId() { + return worldId; + } + + T withReadLock(Supplier action) { + var read = lock.readLock(); + read.lock(); + try { + return action.get(); + } finally { + read.unlock(); + } + } + + void withReadLock(Runnable action) { + withReadLock(() -> { + action.run(); + return null; + }); + } + + T withWriteLock(Supplier action) { + var write = lock.writeLock(); + write.lock(); + try { + return action.get(); + } finally { + write.unlock(); + } + } + + void withWriteLock(Runnable action) { + withWriteLock(() -> { + action.run(); + return null; + }); + } + + int sizeLocked() { + return regions.size(); + } + + boolean isEmptyLocked() { + return regions.isEmpty(); + } + + void clearLocked() { + regions.clear(); + chunkRegions.clear(); + globalRegions.clear(); + } + + void addRegionLocked(Region region) { + if (region == null) return; + regions.put(region.getKey().getValue(), region); + + if (region.getShape() instanceof GlobalShape) { + globalRegions.add(region); + return; + } + if (region.getShape() instanceof CuboidShape) { + indexCuboidLocked(region); + } + } + + void removeRegionLocked(Region region) { + if (region == null) return; + regions.remove(region.getKey().getValue()); + + if (region.getShape() instanceof GlobalShape) { + globalRegions.remove(region); + return; + } + if (region.getShape() instanceof CuboidShape) { + RBlockPos min = region.getMin(); + RBlockPos max = region.getMax(); + if (min != null && max != null) { + removeRegionFromChunksLocked(region, min, max); + } + } + } + + void indexCuboidLocked(Region region) { + if (!(region.getShape() instanceof CuboidShape)) return; + RBlockPos min = region.getMin(); + RBlockPos max = region.getMax(); + if (min == null || max == null) return; + addRegionToChunksLocked(region, min, max); + } + + void updateCuboidLocked(Region region, RBlockPos oldMin, RBlockPos oldMax) { + if (!(region.getShape() instanceof CuboidShape)) return; + if (oldMin != null && oldMax != null) { + removeRegionFromChunksLocked(region, oldMin, oldMax); + } + RBlockPos min = region.getMin(); + RBlockPos max = region.getMax(); + if (min == null || max == null) return; + addRegionToChunksLocked(region, min, max); + } + + boolean overlapsExistingRegionLocked(RBlockPos min, RBlockPos max, RWorldRef world, @Nullable RegionKey keyToIgnore) { + if (min == null || max == null || world == null) return false; + for (Region region : regions.values()) { + if (region.intersects(min, max, world) && !region.getKey().equals(keyToIgnore)) { + return true; + } + } + return false; + } + + List getRegionsAtLocked(RBlockPos location) { + if (location == null) return Collections.emptyList(); + ArrayList found = new ArrayList<>(); + + int xChunk = location.x() >> 4; + int zChunk = location.z() >> 4; + long key = ((long) xChunk << 32) | (zChunk & 0xFFFFFFFFL); + + List candidates = chunkRegions.get(key); + if (candidates != null) { + for (Region region : candidates) { + if (region.contains(location)) found.add(region); + } + } + + if (!globalRegions.isEmpty()) found.addAll(globalRegions); + return found; + } + + @Nullable Region getEffectiveRegionAtLocked(RBlockPos location) { + List regions = getRegionsAtLocked(location); + int priority = Integer.MIN_VALUE; + Region effectiveRegion = null; + for (Region region : regions) { + if (region.getPriority() > priority) { + effectiveRegion = region; + priority = region.getPriority(); + } + } + return effectiveRegion; + } + + private void removeRegionFromChunksLocked(Region region, RBlockPos min, RBlockPos max) { + int minXChunk = min.x() >> 4; + int minZChunk = min.z() >> 4; + int maxXChunk = max.x() >> 4; + int maxZChunk = max.z() >> 4; + + for (int x = minXChunk; x <= maxXChunk; x++) { + for (int z = minZChunk; z <= maxZChunk; z++) { + long key = ((long) x << 32) | (z & 0xFFFFFFFFL); + List regions = chunkRegions.get(key); + if (regions != null) { + regions.remove(region); + if (regions.isEmpty()) chunkRegions.remove(key); + } + } + } + } + + private void addRegionToChunksLocked(Region region, RBlockPos min, RBlockPos max) { + int minXChunk = min.x() >> 4; + int minZChunk = min.z() >> 4; + int maxXChunk = max.x() >> 4; + int maxZChunk = max.z() >> 4; + + for (int x = minXChunk; x <= maxXChunk; x++) { + for (int z = minZChunk; z <= maxZChunk; z++) { + long key = ((long) x << 32) | (z & 0xFFFFFFFFL); + chunkRegions.computeIfAbsent(key, k -> new ArrayList<>()).add(region); + } + } + } +} + diff --git a/api/src/main/java/de/t14d3/zones/Zones.java b/api/src/main/java/de/t14d3/zones/Zones.java index 67c53df..349c068 100644 --- a/api/src/main/java/de/t14d3/zones/Zones.java +++ b/api/src/main/java/de/t14d3/zones/Zones.java @@ -1,28 +1,31 @@ package de.t14d3.zones; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.permissions.CacheUtils; +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.config.YamlConfig; +import de.t14d3.rapunzellib.message.MessageFormatService; import de.t14d3.zones.permissions.PermissionManager; import de.t14d3.zones.permissions.flags.Flags; -import de.t14d3.zones.utils.*; +import de.t14d3.zones.utils.DebugLoggerManager; +import de.t14d3.zones.utils.Types; +import de.t14d3.zones.utils.Utils; import de.t14d3.zones.visuals.FindBossbar; -import de.t14d3.zones.visuals.ParticleHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileInputStream; -import java.nio.file.Files; import java.util.Objects; -import java.util.Properties; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @SuppressWarnings("ThisEscapedInObjectConstruction") +/** + * Core Zones service container. + * + *

This class wires up managers (regions, permissions, config, visuals) and exposes + * them to the platform implementations (Bukkit/Fabric) and integrations.

+ */ public class Zones { - private final CacheUtils cacheUtils; - private final Messages messages; private static Zones instance; private final RegionManager regionManager; @@ -30,10 +33,9 @@ public class Zones { private final ZonesPlatform platform; private final DebugLoggerManager debugLogger; private final Logger logger = LoggerFactory.getLogger(getClass()); - private final ConfigManager configManager; + private final YamlConfig config; private final ThreadPoolExecutor executor; private final FindBossbar findBossbar; - private final ParticleHandler particleHandler; public boolean debug = false; @@ -44,99 +46,113 @@ public Zones(ZonesPlatform platform) { Types types = platform.getTypes(); types.populateTypes(); - new ConfigUpdater(this); + // Load config.yml and merge missing keys/comments from the bundled default. + this.config = Rapunzel.context().configs() + .load(new File(platform.getDataFolder(), "config.yml").toPath(), "config.yml"); - this.configManager = new ConfigManager(this, new File(platform.getDataFolder(), "config.yml")); - - boolean debug = configManager.getBoolean("debug", false) || Objects.equals(System.getenv("ZONES_DEBUG"), + boolean debug = config.getBoolean("debug", false) || Objects.equals(System.getenv("ZONES_DEBUG"), "true"); this.debugLogger = new DebugLoggerManager(this, debug); - this.cacheUtils = new CacheUtils(this); - Flags flags = new Flags(); - - Properties messagesConfig = new Properties(); - File messagesFile = new File(platform.getDataFolder(), "messages.properties"); - if (!messagesFile.exists()) { - try { - //noinspection DataFlowIssue // File is always included in jar - Files.copy(getClass().getResourceAsStream("/messages.properties"), messagesFile.toPath()); - } catch (Exception e1) { - getLogger().error("Failed to copy default messages.properties: {}", e1.getMessage()); - } - } - try { - messagesConfig.load(new FileInputStream(messagesFile)); - } catch (Exception e) { - getLogger().error("Failed to load messages.properties: {}", e.getMessage()); - - } - this.messages = new Messages(messagesConfig, this); - - PlayerRepository playerRepository = new PlayerRepository(); - - this.permissionManager = platform.getPermissionManager(); - this.regionManager = new RegionManager(this, permissionManager); + // Ensure flags are registered (via class initialization) before regions are loaded/edited. + new Flags(); + + + this.regionManager = new RegionManager(this); + this.permissionManager = platform.createPermissionManager(this); this.executor = new ThreadPoolExecutor( 0, - configManager.getInt("advanced.thread-pool.max-size", Runtime.getRuntime().availableProcessors() * 2), - configManager.getInt("advanced.thread-pool.keepalive", 60), + config.getInt("advanced.thread-pool.max-size", Runtime.getRuntime().availableProcessors() * 2), + config.getInt("advanced.thread-pool.keepalive", 60), TimeUnit.SECONDS, new SynchronousQueue<>() ); this.findBossbar = new FindBossbar(this); - this.particleHandler = new ParticleHandler(this); - particleHandler.particleScheduler(); } + /** + * Returns the global Zones instance for this JVM. + */ public static Zones getInstance() { return instance; } + /** + * Returns the platform bridge (Bukkit/Fabric) hosting Zones. + */ public ZonesPlatform getPlatform() { return platform; } + /** + * Returns the debug logger facility (may be disabled). + */ public DebugLoggerManager getDebugLogger() { return debugLogger; } + /** + * Returns the SLF4J logger for Zones. + */ public Logger getLogger() { return logger; } + /** + * Returns the region manager responsible for loading and querying regions. + */ public RegionManager getRegionManager() { return regionManager; } + + /** + * Returns the permission manager used for action checks. + */ public PermissionManager getPermissionManager() { return permissionManager; } - public Messages getMessages() { - return messages; + /** + * Returns the message formatting service used to build user-facing messages. + */ + public MessageFormatService getMessages() { + return Rapunzel.context().messages(); } - public ConfigManager getConfig() { - return configManager; + /** + * Returns the config manager wrapper around {@code config.yml}. + */ + public YamlConfig getConfig() { + return config; } + /** + * Returns the platform data folder. + */ public File getDataFolder() { return platform.getDataFolder(); } + /** + * Returns the thread pool used for asynchronous/auxiliary work. + */ public ThreadPoolExecutor getThreadPool() { return executor; } - public CacheUtils getCacheUtils() { - return cacheUtils; - } - + /** + * Returns the configured region saving mode. + */ public Utils.SavingModes getSavingMode() { - return configManager.getSavingMode(); + return Utils.SavingModes.fromString( + config.getString("zone-saving.mode", "MODIFIED") + ); } + /** + * Returns the bossbar handler used for "find region" visuals. + */ public FindBossbar getFindBossbar() { return findBossbar; } diff --git a/api/src/main/java/de/t14d3/zones/ZonesPlatform.java b/api/src/main/java/de/t14d3/zones/ZonesPlatform.java index 421f666..5491a55 100644 --- a/api/src/main/java/de/t14d3/zones/ZonesPlatform.java +++ b/api/src/main/java/de/t14d3/zones/ZonesPlatform.java @@ -1,52 +1,26 @@ package de.t14d3.zones; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.objects.World; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.permissions.PermissionManager; import de.t14d3.zones.utils.Types; -import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.format.NamedTextColor; import java.io.File; -import java.util.List; -import java.util.UUID; public interface ZonesPlatform { - - List getWorlds(); - - Player getPlayer(UUID uuid); - - Audience getAudience(Player player); - - boolean hasPermission(Player player, String permission); - - ZonesPlatform getPlatform(); - - default World getWorld(String world) { - return getWorlds().stream().filter(w -> w.getName().equals(world)).findFirst().orElse(null); - } - File getDataFolder(); - default PermissionManager getPermissionManager() { - return Zones.getInstance().getPermissionManager(); - } - Types getTypes(); - World getWorld(Player player); - - BlockLocation getLocation(Player player); - - String getMetadata(Player player, String key); - - void setMetadata(Player player, String key, String value); + default PermissionManager createPermissionManager(Zones zones) { + return new PermissionManager(zones); + } - void spawnParticle(int type, BlockLocation particleLocation, Player player); + void spawnParticle(int type, RBlockPos particleLocation, RPlayer player); - void showBeacon(Player player, BlockLocation location, World world, NamedTextColor color); + void showBeacon(RPlayer player, RBlockPos location, RWorldRef world, NamedTextColor color); - void removeBeacon(Player player, World world, BlockLocation location); + void removeBeacon(RPlayer player, RWorldRef world, RBlockPos location); } diff --git a/api/src/main/java/de/t14d3/zones/datasource/SQLDataSource.java b/api/src/main/java/de/t14d3/zones/datasource/SQLDataSource.java index 4403918..9e456a8 100644 --- a/api/src/main/java/de/t14d3/zones/datasource/SQLDataSource.java +++ b/api/src/main/java/de/t14d3/zones/datasource/SQLDataSource.java @@ -1,18 +1,19 @@ package de.t14d3.zones.datasource; import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.RegionFlagEntry; -import de.t14d3.zones.objects.World; +import de.t14d3.zones.objects.shapes.CuboidShape; +import de.t14d3.zones.objects.shapes.GlobalShape; +import de.t14d3.zones.objects.shapes.ShapeProvider; +import de.t14d3.zones.permissions.RegionMembership; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.permissions.RegionSecurityCodec; import java.sql.*; import java.util.ArrayList; import java.util.List; -import java.util.Map; public class SQLDataSource extends AbstractDataSource { private Connection connection; @@ -104,16 +105,12 @@ public SQLDataSource(Zones zones, DataSourceManager.DataSourceTypes type) { "CREATE TABLE IF NOT EXISTS " + tableName + " (" + "\"key\" INT PRIMARY KEY, " + "name VARCHAR(255), " + - "minX INT, " + - "minY INT, " + - "minZ INT, " + - "maxX INT, " + - "maxY INT, " + - "maxZ INT, " + "world VARCHAR(255), " + "members TEXT, " + "parent INT, " + - "priority INT" + + "priority INT, " + + "shape_type VARCHAR(255), " + + "shape_data TEXT" + ")"; connection.prepareStatement(createTableSQL).execute(); } catch (SQLException e) { @@ -137,42 +134,44 @@ public void close() { } private String buildUpsertSQL() { - String columns = "(\"key\", name, minX, minY, minZ, maxX, maxY, maxZ, world, members, parent, priority)"; - String values = "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + String columns = "(\"key\", name, world, members, parent, priority, shape_type, shape_data)"; + String values = "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; String updates; - switch (dbType) { - case MYSQL: - updates = "name=VALUES(name), minX=VALUES(minX), minY=VALUES(minY), minZ=VALUES(minZ), " + - "maxX=VALUES(maxX), maxY=VALUES(maxY), maxZ=VALUES(maxZ), world=VALUES(world), " + - "members=VALUES(members), parent=VALUES(parent), priority=VALUES(priority)"; - return String.format("INSERT INTO %s %s %s ON DUPLICATE KEY UPDATE %s", + return switch (dbType) { + case MYSQL -> { + updates = "name=VALUES(name), world=VALUES(world), members=VALUES(members), parent=VALUES(parent), priority=VALUES(priority), " + + "shape_type=VALUES(shape_type), shape_data=VALUES(shape_data)"; + yield String.format("INSERT INTO %s %s %s ON DUPLICATE KEY UPDATE %s", tableName, columns, values, updates); - case SQLITE: - return String.format("INSERT OR REPLACE INTO %s %s %s", tableName, columns, values); - case POSTGRESQL: - updates = "name=EXCLUDED.name, minX=EXCLUDED.minX, minY=EXCLUDED.minY, " + - "minZ=EXCLUDED.minZ, maxX=EXCLUDED.maxX, maxY=EXCLUDED.maxY, maxZ=EXCLUDED.maxZ, " + - "world=EXCLUDED.world, members=EXCLUDED.members, parent=EXCLUDED.parent, priority=EXCLUDED.priority"; - return String.format("INSERT INTO %s %s %s ON CONFLICT (key) DO UPDATE SET %s", + } + case SQLITE -> String.format("INSERT OR REPLACE INTO %s %s %s", tableName, columns, values); + case POSTGRESQL -> { + updates = "name=EXCLUDED.name, world=EXCLUDED.world, members=EXCLUDED.members, parent=EXCLUDED.parent, priority=EXCLUDED.priority, " + + "shape_type=EXCLUDED.shape_type, shape_data=EXCLUDED.shape_data"; + yield String.format("INSERT INTO %s %s %s ON CONFLICT (key) DO UPDATE SET %s", tableName, columns, values, updates); - case H2: - return String.format("MERGE INTO %s %s %s", tableName, columns, values); - default: - throw new IllegalArgumentException("Unsupported database type: " + dbType); - } + } + case H2 -> String.format("MERGE INTO %s %s %s", tableName, columns, values); + default -> throw new IllegalArgumentException("Unsupported database type: " + dbType); + }; } @Override public List loadRegions() { List regions = new ArrayList<>(); String sql = "SELECT * FROM " + tableName; - try (PreparedStatement statement = connection.prepareStatement(sql); + try (PreparedStatement statement = connection.prepareStatement(sql); ResultSet rs = statement.executeQuery()) { while (rs.next()) { - Region region = parseRegion(rs); - zones.getRegionManager().addRegion(region); - regions.add(region); + Region region; + try { + region = parseRegion(rs); + } catch (SQLException e) { + zones.getLogger().error("Failed to parse a stored region row; skipping", e); + continue; + } + if (region != null) regions.add(region); } } catch (SQLException e) { zones.getLogger().error("Failed to load regions! Error: {}", e.getMessage()); @@ -186,48 +185,71 @@ public List loadRegions() { private Region parseRegion(ResultSet rs) throws SQLException { int key = rs.getInt("key"); String name = rs.getString("name"); - BlockLocation min = new BlockLocation( - rs.getInt("minX"), - rs.getInt("minY"), - rs.getInt("minZ") - ); - BlockLocation max = new BlockLocation( - rs.getInt("maxX"), - rs.getInt("maxY"), - rs.getInt("maxZ") - ); - World world = Zones.getInstance().getPlatform().getWorld(rs.getString("world")); String membersJson = rs.getString("members"); - Map> members = gson.fromJson(membersJson, - new TypeToken>>() { - }.getType()); + + RegionPermissions permissions; + RegionMembership membership; + if (membersJson != null && !membersJson.isBlank()) { + RegionSecurityCodec.Persisted persisted = gson.fromJson(membersJson, RegionSecurityCodec.Persisted.class); + RegionSecurityCodec.Decoded decoded = RegionSecurityCodec.decode(persisted); + permissions = decoded.acl(); + membership = decoded.membership(); + } else { + permissions = new RegionPermissions(); + membership = new RegionMembership(); + } int parentKey = rs.getInt("parent"); RegionKey parent = parentKey != 0 ? RegionKey.fromInt(parentKey) : null; int priority = rs.getInt("priority"); - return new Region(name, min, max, world, members, - RegionKey.fromInt(key), parent, priority); + String shapeType = rs.getString("shape_type"); + if (shapeType == null || shapeType.isBlank()) { + zones.getLogger().error("Region {} is missing required shape_type; skipping", key); + return null; + } + String shapeData = rs.getString("shape_data"); + if (shapeData == null || shapeData.isBlank()) { + zones.getLogger().error("Region {} is missing required shape_data; skipping", key); + return null; + } + + ShapeProvider shape; + try { + switch (shapeType) { + case "global" -> shape = GlobalShape.fromJson(shapeData); + case "cuboid" -> shape = CuboidShape.fromJson(shapeData); + default -> { + zones.getLogger().error("Unknown shape type {} for region {}; skipping", shapeType, key); + return null; + } + } + } catch (Exception e) { + zones.getLogger().error("Failed to parse shape for region {}; skipping", key, e); + return null; + } + + return new Region(name, shape, permissions, membership, RegionKey.fromInt(key), parent, priority); } + @Override public void saveRegions(List regions) { String sql = buildUpsertSQL(); - try (PreparedStatement stmt = connection.prepareStatement(sql)) { + try (PreparedStatement stmt = connection.prepareStatement(sql)) { for (Region region : regions) { stmt.setInt(1, region.getKey().getValue()); stmt.setString(2, region.getName()); - BlockLocation min = region.getMin(); - stmt.setInt(3, min.getX()); - stmt.setInt(4, min.getY()); - stmt.setInt(5, min.getZ()); - BlockLocation max = region.getMax(); - stmt.setInt(6, max.getX()); - stmt.setInt(7, max.getY()); - stmt.setInt(8, max.getZ()); - stmt.setString(9, region.getWorld() != null ? region.getWorld().getName() : null); - stmt.setString(10, gson.toJson(region.getMembers())); - stmt.setInt(11, region.getParent() != null ? region.getParent().getValue() : 0); - stmt.setInt(12, region.getPriority()); + stmt.setString(3, region.getWorld() != null ? region.getWorld().identifier() : null); + stmt.setString(4, + gson.toJson(RegionSecurityCodec.encode(region.getPermissions(), region.getMembership()))); + stmt.setInt(5, region.getParent() != null ? region.getParent().getValue() : 0); + stmt.setInt(6, region.getPriority()); + + // Shape data + ShapeProvider shape = region.getShape(); + stmt.setString(7, shape.getShapeType()); + stmt.setString(8, shape.toJson()); + stmt.addBatch(); } stmt.executeBatch(); @@ -242,7 +264,7 @@ public void saveRegions(List regions) { @Override public Region loadRegion(String key) { String sql = "SELECT * FROM " + tableName + " WHERE key = ?"; - try (PreparedStatement stmt = connection.prepareStatement(sql)) { + try (PreparedStatement stmt = connection.prepareStatement(sql)) { stmt.setString(1, key); ResultSet rs = stmt.executeQuery(); if (rs.next()) { @@ -260,21 +282,19 @@ public Region loadRegion(String key) { @Override public void saveRegion(String key, Region region) { String sql = buildUpsertSQL(); - try (PreparedStatement stmt = connection.prepareStatement(sql)) { + try (PreparedStatement stmt = connection.prepareStatement(sql)) { stmt.setInt(1, RegionKey.fromString(key).getValue()); stmt.setString(2, region.getName()); - BlockLocation min = region.getMin(); - stmt.setInt(3, min.getX()); - stmt.setInt(4, min.getY()); - stmt.setInt(5, min.getZ()); - BlockLocation max = region.getMax(); - stmt.setInt(6, max.getX()); - stmt.setInt(7, max.getY()); - stmt.setInt(8, max.getZ()); - stmt.setString(9, region.getWorld() != null ? region.getWorld().getName() : null); - stmt.setString(10, gson.toJson(region.getMembers())); - stmt.setInt(11, region.getParent() != null ? region.getParent().getValue() : 0); - stmt.setInt(12, region.getPriority()); + stmt.setString(3, region.getWorld() != null ? region.getWorld().identifier() : null); + stmt.setString(4, gson.toJson(RegionSecurityCodec.encode(region.getPermissions(), region.getMembership()))); + stmt.setInt(5, region.getParent() != null ? region.getParent().getValue() : 0); + stmt.setInt(6, region.getPriority()); + + // Shape data + ShapeProvider shape = region.getShape(); + stmt.setString(7, shape.getShapeType()); + stmt.setString(8, shape.toJson()); + stmt.executeUpdate(); } catch (SQLException e) { zones.getLogger() diff --git a/api/src/main/java/de/t14d3/zones/datasource/YamlDataSource.java b/api/src/main/java/de/t14d3/zones/datasource/YamlDataSource.java index 1221f1d..93ff360 100644 --- a/api/src/main/java/de/t14d3/zones/datasource/YamlDataSource.java +++ b/api/src/main/java/de/t14d3/zones/datasource/YamlDataSource.java @@ -1,42 +1,50 @@ package de.t14d3.zones.datasource; +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.config.ConfigurationSection; +import de.t14d3.rapunzellib.config.YamlConfig; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.RegionFlagEntry; -import de.t14d3.zones.objects.World; -import org.simpleyaml.configuration.ConfigurationSection; -import org.simpleyaml.configuration.file.YamlFile; +import de.t14d3.zones.objects.shapes.CuboidShape; +import de.t14d3.zones.objects.shapes.GlobalShape; +import de.t14d3.zones.objects.shapes.ShapeProvider; +import de.t14d3.zones.permissions.PermissionKeyRegistry; +import de.t14d3.zones.permissions.RegionMembership; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.permissions.flags.FlagValueKind; +import de.t14d3.zones.permissions.subjects.Subjects; import java.io.File; -import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CompletableFuture; public class YamlDataSource extends AbstractDataSource { - private final YamlFile regionsFile; + private final YamlConfig regionsFile; private final Zones zones; + /** + * Creates a YAML data source rooted at the given data folder. + * + *

This loads (or creates) {@code regions.yml} immediately. Any initialization errors are logged.

+ * + * @param dataFolder plugin/mod data directory + * @param zones owning plugin instance + */ public YamlDataSource(File dataFolder, Zones zones) { super(zones); this.zones = zones; - this.regionsFile = new YamlFile(new File(dataFolder, "regions.yml")); - - try { - if (!regionsFile.exists()) { - regionsFile.createOrLoadWithComments(); - regionsFile.save(); - } - regionsFile.loadWithComments(); - } catch (IOException e) { - zones.getLogger().error("Failed to initialize regions file", e); - } + this.regionsFile = Rapunzel.context().configs().load(new File(dataFolder, "regions.yml").toPath()); } + /** + * Loads all regions found under the {@code regions} root node. + * + * @return list of loaded regions + */ @Override public List loadRegions() { List regions = new ArrayList<>(); @@ -46,7 +54,6 @@ public List loadRegions() { for (String regionKey : regionsSection.getKeys(false)) { Region region = loadRegion(regionKey); if (region != null) { - zones.getRegionManager().addRegion(region); regions.add(region); } } @@ -54,6 +61,12 @@ public List loadRegions() { return regions; } + /** + * Loads a single region by key. + * + * @param key region key string + * @return the loaded region, or {@code null} if it does not exist + */ @Override public Region loadRegion(String key) { final String pathPrefix = "regions." + key + "."; @@ -62,89 +75,187 @@ public Region loadRegion(String key) { final String name = regionsFile.getString(pathPrefix + "name"); final int priority = regionsFile.getInt(pathPrefix + "priority", 0); - // World data - final String worldName = regionsFile.getString(pathPrefix + "world"); - World world = zones.getPlatform().getWorld(worldName); - if (world == null) { - zones.getLogger().warn("World {} for region {} not found, using default", worldName, key); - world = zones.getPlatform().getWorlds().get(0); + // Shape data + ShapeProvider shape; + ConfigurationSection shapeSection = regionsFile.getConfigurationSection(pathPrefix + "shape"); + if (shapeSection == null) { + zones.getLogger().error("Region {} is missing required shape data; skipping", key); + return null; } - // Location data - final BlockLocation min = new BlockLocation( - regionsFile.getInt(pathPrefix + "min.x"), - regionsFile.getInt(pathPrefix + "min.y"), - regionsFile.getInt(pathPrefix + "min.z") - ); + String type = shapeSection.getString("type"); + String data = shapeSection.getString("data"); + if (type == null || type.isBlank() || data == null || data.isBlank()) { + zones.getLogger().error("Region {} has invalid shape data; skipping", key); + return null; + } - final BlockLocation max = new BlockLocation( - regionsFile.getInt(pathPrefix + "max.x"), - regionsFile.getInt(pathPrefix + "max.y"), - regionsFile.getInt(pathPrefix + "max.z") - ); + try { + switch (type) { + case "global" -> shape = GlobalShape.fromJson(data); + case "cuboid" -> shape = CuboidShape.fromJson(data); + default -> { + zones.getLogger().error("Unknown shape type {} for region {}; skipping", type, key); + return null; + } + } + } catch (Exception e) { + zones.getLogger().error("Failed to parse shape for region {}; skipping", key, e); + return null; + } // Parent relationship final String parentKey = regionsFile.getString(pathPrefix + "parent"); final RegionKey parent = parentKey != null ? RegionKey.fromString(parentKey) : null; - // Member permissions - final Map> members = parseMembers( - regionsFile.getConfigurationSection(pathPrefix + "members") - ); + RegionPermissions permissions = new RegionPermissions(); + RegionMembership membership = new RegionMembership(); - return new Region(name, min, max, world, members, RegionKey.fromString(key), parent, priority); - } + // ACL rules + ConfigurationSection aclSection = regionsFile.getConfigurationSection(pathPrefix + "acl"); + if (aclSection != null) { + ConfigurationSection universalSection = aclSection.getConfigurationSection("universal"); + if (universalSection != null) { + readSubjectPermissions(universalSection, permissions.subject(Subjects.universal())); + } - private Map> parseMembers(ConfigurationSection membersSection) { - final Map> members = new HashMap<>(); - - if (membersSection != null) { - for (String who : membersSection.getKeys(false)) { - final ConfigurationSection flagsSection = membersSection.getConfigurationSection(who); - final List flags = new ArrayList<>(); - - if (flagsSection != null) { - for (String flagName : flagsSection.getKeys(false)) { - final String valuesStr = flagsSection.getString(flagName); - final List values = new ArrayList<>(); - - for (String value : valuesStr.split(" ")) { - boolean inverted = value.startsWith("!"); - String cleanValue = inverted ? value.substring(1) : value; - values.add(new RegionFlagEntry.FlagValue(cleanValue.toLowerCase(), inverted)); - } - - flags.add(new RegionFlagEntry( - flagName.toLowerCase().replaceFirst("!", ""), - values - )); + ConfigurationSection playersSection = aclSection.getConfigurationSection("players"); + if (playersSection != null) { + for (String playerKey : playersSection.getKeys(false)) { + ConfigurationSection subjectSection = playersSection.getConfigurationSection(playerKey); + if (subjectSection == null) continue; + try { + UUID uuid = UUID.fromString(playerKey); + readSubjectPermissions(subjectSection, permissions.subject(Subjects.player(uuid))); + } catch (IllegalArgumentException ignored) { } } + } - members.put(who, flags); + ConfigurationSection groupsSection = aclSection.getConfigurationSection("groups"); + if (groupsSection != null) { + for (String groupName : groupsSection.getKeys(false)) { + ConfigurationSection subjectSection = groupsSection.getConfigurationSection(groupName); + if (subjectSection == null) continue; + String normalized = Subjects.normalizeGroupName(groupName); + if (normalized == null) continue; + readSubjectPermissions(subjectSection, permissions.subject(Subjects.group(normalized))); + } + } + } + + // Membership + ConfigurationSection membershipSection = regionsFile.getConfigurationSection(pathPrefix + "membership"); + if (membershipSection != null) { + readMembership(membershipSection, membership); + } + + return new Region(name, shape, permissions, membership, RegionKey.fromString(key), parent, priority); + } + + private static void readSubjectPermissions(ConfigurationSection subjectSection, RegionPermissions.SubjectPermissions subject) { + for (String permName : subjectSection.getKeys(false)) { + ConfigurationSection permSection = subjectSection.getConfigurationSection(permName); + if (permSection == null) continue; + + String kind = permSection.getString("type", "targets"); + int permId = PermissionKeyRegistry.instance().getOrCreateId(permName); + + if ("set".equalsIgnoreCase(kind)) { + RegionPermissions.StringSetValue set = (RegionPermissions.StringSetValue) subject.getOrCreate(permId, + FlagValueKind.STRING_SET); + permSection.getStringList("allow").forEach(set::allow); + permSection.getStringList("deny").forEach(set::deny); + } else { + RegionPermissions.TargetDecisionValue targeted = (RegionPermissions.TargetDecisionValue) subject.getOrCreate( + permId, FlagValueKind.TARGET_DECISION); + permSection.getStringList("allow").forEach(targeted::allow); + permSection.getStringList("deny").forEach(targeted::deny); } } - return members; } + private static void readMembership(ConfigurationSection membershipSection, RegionMembership membership) { + ConfigurationSection players = membershipSection.getConfigurationSection("players"); + if (players != null) { + for (String playerKey : players.getKeys(false)) { + ConfigurationSection playerSection = players.getConfigurationSection(playerKey); + if (playerSection == null) continue; + UUID uuid; + try { + uuid = UUID.fromString(playerKey); + } catch (IllegalArgumentException ignored) { + continue; + } + + RegionMembership.PlayerEntry entry = membership.player(uuid); + + ConfigurationSection rolesSection = playerSection.getConfigurationSection("roles"); + if (rolesSection != null) { + rolesSection.getStringList("allow").forEach(entry.roles()::allow); + rolesSection.getStringList("deny").forEach(entry.roles()::deny); + } + + ConfigurationSection groupsSection = playerSection.getConfigurationSection("groups"); + if (groupsSection != null) { + groupsSection.getStringList("allow").forEach(entry.groups()::allow); + groupsSection.getStringList("deny").forEach(entry.groups()::deny); + } + } + } + + ConfigurationSection groups = membershipSection.getConfigurationSection("groups"); + if (groups != null) { + for (String groupName : groups.getKeys(false)) { + ConfigurationSection groupSection = groups.getConfigurationSection(groupName); + if (groupSection == null) continue; + String normalized = Subjects.normalizeGroupName(groupName); + if (normalized == null) continue; + + RegionMembership.GroupEntry entry = membership.group(normalized); + ConfigurationSection includesSection = groupSection.getConfigurationSection("includes"); + if (includesSection != null) { + includesSection.getStringList("allow").forEach(entry.includes()::allow); + includesSection.getStringList("deny").forEach(entry.includes()::deny); + } + } + } + } + + /** + * Saves the full region list asynchronously, replacing the entire {@code regions} section. + * + *

This performs the actual disk write by calling {@link YamlConfig#save()}.

+ * + * @param regions regions to persist + */ @Override public void saveRegions(List regions) { CompletableFuture.runAsync(() -> { try { - regionsFile.set("regions", null); // Clear existing regions + regionsFile.set("regions", null); // Clear existing regions for (Region region : regions) { saveRegion(region.getKey().toString(), region); } - regionsFile.setComment("regions", "All registered regions"); + regionsFile.setComment("regions", "All registered regions"); regionsFile.save(); - } catch (IOException e) { + } catch (Exception e) { zones.getLogger().error("Failed to save regions", e); } }); } + /** + * Writes a single region into the in-memory YAML representation. + * + *

This method does not call {@link YamlConfig#save()}; flushing is handled by {@link #saveRegions(List)} and the + * region manager's save mode.

+ * + * @param key region key string + * @param region region to persist + */ @Override public void saveRegion(String key, Region region) { final String pathPrefix = "regions." + key + "."; @@ -156,19 +267,20 @@ public void saveRegion(String key, Region region) { regionsFile.set(pathPrefix + "priority", region.getPriority()); regionsFile.setComment(pathPrefix + "priority", "Region priority (higher = stronger)"); - regionsFile.set(pathPrefix + "world", region.getWorld().getName()); + regionsFile.set(pathPrefix + "world", region.getWorld().identifier()); regionsFile.setComment(pathPrefix + "world", "World where the region exists"); - // Location data - regionsFile.set(pathPrefix + "min.x", region.getMin().getX()); - regionsFile.set(pathPrefix + "min.y", region.getMin().getY()); - regionsFile.set(pathPrefix + "min.z", region.getMin().getZ()); - regionsFile.setComment(pathPrefix + "min", "Minimum bounding box coordinates"); + // Shape data + ConfigurationSection shapeSection = regionsFile.createSection(pathPrefix + "shape"); + ShapeProvider shape = region.getShape(); + shapeSection.set("type", shape.getShapeType()); + shapeSection.set("data", shape.toJson()); + regionsFile.setComment(pathPrefix + "shape", "Region shape specification"); + + // Remove legacy min/max fields if they exist. + regionsFile.set(pathPrefix + "min", null); + regionsFile.set(pathPrefix + "max", null); - regionsFile.set(pathPrefix + "max.x", region.getMax().getX()); - regionsFile.set(pathPrefix + "max.y", region.getMax().getY()); - regionsFile.set(pathPrefix + "max.z", region.getMax().getZ()); - regionsFile.setComment(pathPrefix + "max", "Maximum bounding box coordinates"); // Parent relationship if (region.getParent() != null) { @@ -176,26 +288,96 @@ public void saveRegion(String key, Region region) { regionsFile.setComment(pathPrefix + "parent", "Parent region key"); } - // Member permissions - final ConfigurationSection membersSection = regionsFile.createSection(pathPrefix + "members"); - serializeMembers(membersSection, region.getMembers()); - regionsFile.setComment(pathPrefix + "members", "Region members and their permissions"); + // ACL + regionsFile.set(pathPrefix + "acl", null); + final ConfigurationSection aclSection = regionsFile.createSection(pathPrefix + "acl"); + RegionPermissions permissions = region.getPermissions(); + if (permissions != null) { + writeAcl(aclSection, permissions); + } + regionsFile.setComment(pathPrefix + "acl", "Region ACL rules"); + + // Membership + regionsFile.set(pathPrefix + "membership", null); + final ConfigurationSection membershipSection = regionsFile.createSection(pathPrefix + "membership"); + RegionMembership membership = region.getMembership(); + if (membership != null) { + writeMembership(membershipSection, membership); + } + regionsFile.setComment(pathPrefix + "membership", "Region membership (roles/groups and group inheritance)"); } - private void serializeMembers(ConfigurationSection membersSection, Map> members) { - for (Map.Entry> entry : members.entrySet()) { - final ConfigurationSection flagSection = membersSection.createSection(entry.getKey()); + private static void writeAcl(ConfigurationSection aclSection, RegionPermissions permissions) { + ConfigurationSection universal = aclSection.createSection("universal"); + ConfigurationSection players = aclSection.createSection("players"); + ConfigurationSection groups = aclSection.createSection("groups"); + + for (Map.Entry subjectEntry : permissions.subjects() + .entrySet()) { + de.t14d3.zones.permissions.subjects.SubjectRef subject = subjectEntry.getKey(); + RegionPermissions.SubjectPermissions subjectPermissions = subjectEntry.getValue(); + if (subject == null || subjectPermissions == null) continue; + + ConfigurationSection subjectSection; + if (subject instanceof de.t14d3.zones.permissions.subjects.UniversalSubject) { + subjectSection = universal; + } else if (subject instanceof de.t14d3.zones.permissions.subjects.PlayerSubject p) { + subjectSection = players.createSection(p.uuid().toString()); + } else if (subject instanceof de.t14d3.zones.permissions.subjects.GroupSubject g) { + subjectSection = groups.createSection(g.name()); + } else { + continue; + } - for (RegionFlagEntry flag : entry.getValue()) { - final StringBuilder values = new StringBuilder(); - for (RegionFlagEntry.FlagValue value : flag.getValues()) { - values.append(value.isInverted() ? "!" : "") - .append(value.getValue()) - .append(" "); + for (var permEntry : subjectPermissions.values().int2ObjectEntrySet()) { + int permId = permEntry.getIntKey(); + String permName = PermissionKeyRegistry.instance().getName(permId); + if (permName == null) continue; + + RegionPermissions.PermissionValue pv = permEntry.getValue(); + if (pv instanceof RegionPermissions.TargetDecisionValue targeted) { + ConfigurationSection permSection = subjectSection.createSection(permName); + permSection.set("type", "targets"); + permSection.set("allow", new ArrayList<>(targeted.allowTargets())); + permSection.set("deny", new ArrayList<>(targeted.denyTargets())); + } else if (pv instanceof RegionPermissions.StringSetValue set) { + ConfigurationSection permSection = subjectSection.createSection(permName); + permSection.set("type", "set"); + permSection.set("allow", new ArrayList<>(set.allowValues())); + permSection.set("deny", new ArrayList<>(set.denyValues())); } - - flagSection.set(flag.getFlagValue(), values.toString().trim()); } } } -} \ No newline at end of file + + private static void writeMembership(ConfigurationSection membershipSection, RegionMembership membership) { + ConfigurationSection players = membershipSection.createSection("players"); + for (Map.Entry e : membership.players().entrySet()) { + UUID uuid = e.getKey(); + RegionMembership.PlayerEntry entry = e.getValue(); + if (uuid == null || entry == null) continue; + + ConfigurationSection playerSection = players.createSection(uuid.toString()); + + ConfigurationSection roles = playerSection.createSection("roles"); + roles.set("allow", new ArrayList<>(entry.roles().allowValues())); + roles.set("deny", new ArrayList<>(entry.roles().denyValues())); + + ConfigurationSection groups = playerSection.createSection("groups"); + groups.set("allow", new ArrayList<>(entry.groups().allowValues())); + groups.set("deny", new ArrayList<>(entry.groups().denyValues())); + } + + ConfigurationSection groups = membershipSection.createSection("groups"); + for (Map.Entry e : membership.groups().entrySet()) { + String name = e.getKey(); + RegionMembership.GroupEntry entry = e.getValue(); + if (name == null || entry == null) continue; + + ConfigurationSection groupSection = groups.createSection(name); + ConfigurationSection includes = groupSection.createSection("includes"); + includes.set("allow", new ArrayList<>(entry.includes().allowValues())); + includes.set("deny", new ArrayList<>(entry.includes().denyValues())); + } + } +} diff --git a/api/src/main/java/de/t14d3/zones/objects/BlockLocation.java b/api/src/main/java/de/t14d3/zones/objects/BlockLocation.java deleted file mode 100644 index a9d8317..0000000 --- a/api/src/main/java/de/t14d3/zones/objects/BlockLocation.java +++ /dev/null @@ -1,95 +0,0 @@ -package de.t14d3.zones.objects; - -public class BlockLocation { - private int x; - private int y; - private int z; - - public BlockLocation(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - } - - public int getX() { - return x; - } - - public int getY() { - return y; - } - - public int getZ() { - return z; - } - - public BlockLocation setX(int x) { - this.x = x; - return this; - } - - public BlockLocation setY(int y) { - this.y = y; - return this; - } - - public BlockLocation setZ(int z) { - this.z = z; - return this; - } - - public org.bukkit.Location toLocation(org.bukkit.World world) { - return BlockLocation.asLocation(this, world); - } - - public double getDistance(BlockLocation other) { - return Math.sqrt(Math.pow(other.getX() - this.getX(), 2) + Math.pow(other.getY() - this.getY(), 2) + Math.pow( - other.getZ() - this.getZ(), 2)); - } - - public static BlockLocation of(int x, int y, int z) { - return new BlockLocation(x, y, z); - } - - public static BlockLocation of(org.bukkit.Location location) { - if (location == null) { - return null; - } - return new BlockLocation(location.getBlockX(), location.getBlockY(), location.getBlockZ()); - } - - public static BlockLocation of(org.bukkit.util.BlockVector blockVector) { - return new BlockLocation(blockVector.getBlockX(), blockVector.getBlockY(), blockVector.getBlockZ()); - } - - public static org.bukkit.Location asLocation(BlockLocation blockLocation, org.bukkit.World world) { - return new org.bukkit.Location(world, blockLocation.getX(), blockLocation.getY(), blockLocation.getZ()); - } - - @Override - public BlockLocation clone() { - return new BlockLocation(this.x, this.y, this.z); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BlockLocation that = (BlockLocation) o; - return x == that.x && y == that.y && z == that.z; - } - - @Override - public String toString() { - return "BlockLocation{" + - "x=" + x + - ", y=" + y + - ", z=" + z + - '}'; - } - - public double distance(BlockLocation end) { - return Math.sqrt(Math.pow(end.getX() - this.getX(), 2) + Math.pow(end.getY() - this.getY(), 2) + Math.pow( - end.getZ() - this.getZ(), 2)); - } -} diff --git a/api/src/main/java/de/t14d3/zones/objects/Box.java b/api/src/main/java/de/t14d3/zones/objects/Box.java index 839f421..5fc00f5 100644 --- a/api/src/main/java/de/t14d3/zones/objects/Box.java +++ b/api/src/main/java/de/t14d3/zones/objects/Box.java @@ -1,21 +1,40 @@ package de.t14d3.zones.objects; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; + +import java.util.Objects; + public class Box { - private final BlockLocation min; - private final BlockLocation max; - private final World world; + private final RBlockPos min; + private final RBlockPos max; + private final RWorldRef world; @SuppressWarnings("ConstantConditions") - public Box(BlockLocation pos1, BlockLocation pos2, World world) { + public Box(RBlockPos pos1, RBlockPos pos2, RWorldRef world) { this(pos1, pos2, world, true); } - public Box(BlockLocation pos1, BlockLocation pos2, World world, boolean normalize) { + /** + * Creates a new box from two points. + * + * @param pos1 first point + * @param pos2 second point + * @param world world of the box + * @param normalize whether to normalize the points + */ + public Box(RBlockPos pos1, RBlockPos pos2, RWorldRef world, boolean normalize) { if (normalize) { - this.min = new BlockLocation(Math.min(pos1.getX(), pos2.getX()), Math.min(pos1.getY(), pos2.getY()), - Math.min(pos1.getZ(), pos2.getZ())); - this.max = new BlockLocation(Math.max(pos1.getX(), pos2.getX()), Math.max(pos1.getY(), pos2.getY()), - Math.max(pos1.getZ(), pos2.getZ())); + this.min = new RBlockPos( + Math.min(pos1.x(), pos2.x()), + Math.min(pos1.y(), pos2.y()), + Math.min(pos1.z(), pos2.z()) + ); + this.max = new RBlockPos( + Math.max(pos1.x(), pos2.x()), + Math.max(pos1.y(), pos2.y()), + Math.max(pos1.z(), pos2.z()) + ); } else { this.min = pos1; this.max = pos2; @@ -24,71 +43,67 @@ public Box(BlockLocation pos1, BlockLocation pos2, World world, boolean normaliz } - public Box(int x1, int y1, int z1, int x2, int y2, int z2, World world) { - this(new BlockLocation(x1, y1, z1), new BlockLocation(x2, y2, z2), world); - } - - public Box(org.bukkit.Location min, org.bukkit.Location max, org.bukkit.World world, boolean normalize) { - this(BlockLocation.of(min), BlockLocation.of(max), World.of(world), normalize); + public Box(int x1, int y1, int z1, int x2, int y2, int z2, RWorldRef world) { + this(new RBlockPos(x1, y1, z1), new RBlockPos(x2, y2, z2), world); } - public Box(World world) { + public Box(RWorldRef world) { this.min = null; this.max = null; this.world = world; } - public BlockLocation getMin() { + public RBlockPos getMin() { return min; } - public BlockLocation getMax() { + public RBlockPos getMax() { return max; } - public World getWorld() { + public RWorldRef getWorld() { return world; } - public BlockLocation getCenter() { - return new BlockLocation((min.getX() + max.getX()) / 2, (min.getY() + max.getY()) / 2, - (min.getZ() + max.getZ()) / 2); + public RBlockPos getCenter() { + return new RBlockPos((min.x() + max.x()) / 2, (min.y() + max.y()) / 2, (min.z() + max.z()) / 2); } public int getVolume() { - return (max.getX() - min.getX() + 1) * (max.getY() - min.getY() + 1) * (max.getZ() - min.getZ() + 1); + return (max.x() - min.x() + 1) * (max.y() - min.y() + 1) * (max.z() - min.z() + 1); } public int getArea() { - return (max.getX() - min.getX() + 1) * (max.getY() - min.getY() + 1); + return (max.x() - min.x() + 1) * (max.y() - min.y() + 1); } - public boolean contains(BlockLocation location) { - return location.getX() >= min.getX() && location.getX() <= max.getX() - && location.getY() >= min.getY() && location.getY() <= max.getY() - && location.getZ() >= min.getZ() && location.getZ() <= max.getZ(); + public boolean contains(RBlockPos location) { + return location.x() >= min.x() && location.x() <= max.x() + && location.y() >= min.y() && location.y() <= max.y() + && location.z() >= min.z() && location.z() <= max.z(); } public boolean intersects(Box other) { - return other.world.equals(world) && - other.min.getX() <= max.getX() && other.max.getX() >= min.getX() - && other.min.getY() <= max.getY() && other.max.getY() >= min.getY() - && other.min.getZ() <= max.getZ() && other.max.getZ() >= min.getZ(); + return sameWorld(other.world, world) && + other.min.x() <= max.x() && other.max.x() >= min.x() + && other.min.y() <= max.y() && other.max.y() >= min.y() + && other.min.z() <= max.z() && other.max.z() >= min.z(); } - public boolean intersects(BlockLocation min, BlockLocation max, World world) { + public boolean intersects(RBlockPos min, RBlockPos max, RWorldRef world) { return intersects(new Box(min, max, world)); } - public org.bukkit.util.BoundingBox toBoundingBox() { - return new org.bukkit.util.BoundingBox(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); - } - @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Box box = (Box) o; - return world.equals(box.world) && min.equals(box.min) && max.equals(box.max); + return sameWorld(world, box.world) && min.equals(box.min) && max.equals(box.max); + } + + private static boolean sameWorld(RWorldRef a, RWorldRef b) { + if (a == null || b == null) return false; + return Objects.equals(a.identifier(), b.identifier()); } } diff --git a/api/src/main/java/de/t14d3/zones/objects/Flag.java b/api/src/main/java/de/t14d3/zones/objects/Flag.java index 75bae14..ab34b3d 100644 --- a/api/src/main/java/de/t14d3/zones/objects/Flag.java +++ b/api/src/main/java/de/t14d3/zones/objects/Flag.java @@ -1,56 +1,93 @@ package de.t14d3.zones.objects; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.ZonesPlatform; import de.t14d3.zones.permissions.flags.DefaultFlagHandler; -import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.permissions.flags.FlagContext; +import de.t14d3.zones.permissions.flags.FlagValueKind; import de.t14d3.zones.permissions.flags.IFlagHandler; -import de.t14d3.zones.utils.Types; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.Objects; /** - * A flag is a permission that can be set for a region. - * Flags are registered in the {@link Flags} class, and can be accessed using the {@link Flags#getFlag(String)} method. - * New flags always have a name and a fallback description (in case the translation ís missing). - * Flags can be registered with a default value, which is used when a region doesn't have a value set for it, which defaults to false. + * Flag definition used by Zones' permission engine. + * + *

Flags are metadata: value kind, defaults, and suggestions. Permission evaluation and caching live in the + * permission engine; flags do not "execute" permissions.

*/ -public class Flag implements IFlagHandler { +public final class Flag { + @FunctionalInterface + public interface CanSetRule { + boolean canSet(@Nullable RPlayer player, @NotNull ZonesPlatform platform); + } + + private static final IFlagHandler DEFAULT_HANDLER = new DefaultFlagHandler(true, List.of()); + private static final CanSetRule ALWAYS_CAN_SET = (player, platform) -> true; + + private final int id; private final String name; private final String description; - private final IFlagHandler customHandler; // Optional custom logic + private final IFlagHandler handler; + private final CanSetRule canSetRule; + private final FlagValueKind valueKind; public Flag(String name, String description) { - this(name, description, new DefaultFlagHandler(false, Types.all())); + this(-1, name, description, null, null, FlagValueKind.TARGET_DECISION); + } + + public Flag(String name, String description, IFlagHandler handler, CanSetRule canSetRule) { + this(-1, name, description, handler, canSetRule, FlagValueKind.TARGET_DECISION); } - // Constructor for custom handlers - public Flag(String name, String description, IFlagHandler customHandler) { - super(); - this.name = name; - this.description = description; - this.customHandler = customHandler; + public Flag(int id, @NotNull String name, @NotNull String description, @Nullable IFlagHandler handler, @Nullable CanSetRule canSetRule, @Nullable FlagValueKind valueKind) { + this.id = id; + this.name = Objects.requireNonNull(name, "name").trim(); + this.description = Objects.requireNonNull(description, "description"); + this.canSetRule = Objects.requireNonNullElse(canSetRule, ALWAYS_CAN_SET); + this.handler = Objects.requireNonNullElse(handler, DEFAULT_HANDLER); + this.valueKind = Objects.requireNonNullElse(valueKind, FlagValueKind.TARGET_DECISION); + } + + public int id() { + return id; } public String name() { return name; } - public List getValidValues() { - return customHandler.getValidValues(); + public String description() { + return description; + } + + public FlagValueKind valueKind() { + return valueKind; + } + + public IFlagHandler handler() { + return handler; + } + + public CanSetRule canSetRule() { + return canSetRule; } public boolean getDefaultValue() { - return this.customHandler.getDefaultValue(); + return getDefaultValue(FlagContext.none()); } - public boolean getDefaultValue(Object... optional) { - return this.customHandler.getDefaultValue(optional); + public boolean getDefaultValue(FlagContext context) { + return handler.getDefaultValue(context); } - public String getDescription() { - return description; + public List getValidValues() { + return handler.getValidValues(); } - public IFlagHandler getCustomHandler() { - return customHandler; + public boolean canSet(RPlayer player, ZonesPlatform platform) { + return canSetRule.canSet(player, Objects.requireNonNull(platform, "platform")); } } diff --git a/api/src/main/java/de/t14d3/zones/objects/Player.java b/api/src/main/java/de/t14d3/zones/objects/Player.java deleted file mode 100644 index 11a425b..0000000 --- a/api/src/main/java/de/t14d3/zones/objects/Player.java +++ /dev/null @@ -1,97 +0,0 @@ -package de.t14d3.zones.objects; - -import de.t14d3.zones.Zones; -import net.kyori.adventure.audience.Audience; -import net.kyori.adventure.bossbar.BossBar; -import net.kyori.adventure.text.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -public class Player implements Audience { - private final UUID uuid; - private final String name; - private Box selection; - private boolean selectionIsCreating = false; - - protected Player(UUID uuid, String name) { - this.uuid = uuid; - this.name = name; - } - - public static Player of(UUID uuid, String name) { - return PlayerRepository.getOrAdd(name, uuid); - } - - public UUID getUUID() { - return uuid; - } - - public UUID getUniqueId() { - return uuid; - } - - public String getName() { - return name; - } - - public boolean hasPermission(String permission) { - return Zones.getInstance().getPlatform().hasPermission(this, permission); - } - - public Box getSelection() { - return selection; - } - - public void setSelection(Box selection) { - this.selection = selection; - } - - public boolean isSelectionCreating() { - return selectionIsCreating; - } - - public void setSelectionCreating(boolean selectionIsCreating) { - this.selectionIsCreating = selectionIsCreating; - } - - @Override - public void sendMessage(@NotNull Component component) { - Zones.getInstance().getPlatform().getAudience(this).sendMessage(component); - } - - @Override - public void sendActionBar(@NotNull Component component) { - Zones.getInstance().getPlatform().getAudience(this).sendActionBar(component); - } - - public BossBar showBossbar(@NotNull Component component, float percent, BossBar.Color barColor, BossBar.Overlay type) { - BossBar bar = BossBar.bossBar(component, percent, barColor, type); - Zones.getInstance().getPlatform().getAudience(this).showBossBar(bar); - return bar; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Player player = (Player) o; - return uuid.equals(player.uuid); - } - - public World getWorld() { - return Zones.getInstance().getPlatform().getWorld(this); - } - - public BlockLocation getLocation() { - return Zones.getInstance().getPlatform().getLocation(this); - } - - public String getMetadata(String key) { - return Zones.getInstance().getPlatform().getMetadata(this, key); - } - - public void setMetadata(String key, String value) { - Zones.getInstance().getPlatform().setMetadata(this, key, value); - } -} diff --git a/api/src/main/java/de/t14d3/zones/objects/PlayerRepository.java b/api/src/main/java/de/t14d3/zones/objects/PlayerRepository.java deleted file mode 100644 index a8e3566..0000000 --- a/api/src/main/java/de/t14d3/zones/objects/PlayerRepository.java +++ /dev/null @@ -1,52 +0,0 @@ -package de.t14d3.zones.objects; - -import de.t14d3.zones.Zones; - -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; - -public class PlayerRepository { - private static final Set knownPlayers = new HashSet<>(); - - protected static Player add(Player player) { - knownPlayers.add(player); - return player; - } - - public static Player getOrAdd(String name, UUID uuid) { - Player player = null; - for (Player p : knownPlayers) { - if (p.getUUID().equals(uuid)) { - player = p; - break; - } - } - if (player == null) { - player = new Player(uuid, name); - knownPlayers.add(player); - } - return player; - } - - public static Player get(UUID uuid) { - for (Player player : knownPlayers) { - if (player.getUUID().equals(uuid)) { - return player; - } - } - Player player = Zones.getInstance().getPlatform().getPlayer(uuid); - if (player != null) { - PlayerRepository.add(player); - } - return player; - } - - public static void remove(UUID uuid) { - knownPlayers.removeIf(player -> player.getUUID().equals(uuid)); - } - - public static Set getPlayers() { - return knownPlayers; - } -} diff --git a/api/src/main/java/de/t14d3/zones/objects/RegionFlagEntry.java b/api/src/main/java/de/t14d3/zones/objects/RegionFlagEntry.java index b7194b4..c423c64 100644 --- a/api/src/main/java/de/t14d3/zones/objects/RegionFlagEntry.java +++ b/api/src/main/java/de/t14d3/zones/objects/RegionFlagEntry.java @@ -82,7 +82,7 @@ public boolean isInverted() { @Override public String toString() { return "FlagValue{" + - "value='" + value + '\'' + + "name='" + value + '\'' + ", inverted=" + inverted + '}'; } diff --git a/api/src/main/java/de/t14d3/zones/objects/World.java b/api/src/main/java/de/t14d3/zones/objects/World.java deleted file mode 100644 index d1f46b0..0000000 --- a/api/src/main/java/de/t14d3/zones/objects/World.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.t14d3.zones.objects; - -import java.util.UUID; - -public class World { - private final String name; - private final UUID uid; - - private World(String name, UUID uid) { - this.name = name; - this.uid = uid; - } - - public String getName() { - return name; - } - - public UUID getUID() { - return uid; - } - - public static World of(org.bukkit.World world) { - return new World(world.getName(), world.getUID()); - } - - public static World of(String name, UUID uid) { - return new World(name, uid); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - World world = (World) o; - return uid.equals(world.uid); - } -} diff --git a/api/src/main/java/de/t14d3/zones/objects/shapes/CuboidShape.java b/api/src/main/java/de/t14d3/zones/objects/shapes/CuboidShape.java new file mode 100644 index 0000000..25ffb70 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/objects/shapes/CuboidShape.java @@ -0,0 +1,115 @@ +package de.t14d3.zones.objects.shapes; + +import com.google.gson.Gson; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.objects.Box; + +import java.util.HashMap; +import java.util.Map; + +/** + * Cuboid (rectangular) shape implementation using Box for AABB checks. + */ +public class CuboidShape implements ShapeProvider { + + private static final Gson gson = new Gson(); + + private final Box box; + + public CuboidShape(Box box) { + this.box = box; + } + + public CuboidShape(RBlockPos min, RBlockPos max, RWorldRef world, boolean normalize) { + this.box = new Box(min, max, world, normalize); + } + + public CuboidShape(RBlockPos min, RBlockPos max, RWorldRef world) { + this(min, max, world, true); + } + + @Override + public boolean contains(RBlockPos location) { + return box.contains(location); + } + + @Override + public boolean intersects(ShapeProvider other) { + if (other instanceof CuboidShape cuboid) { + return box.intersects(cuboid.box); + } else if (other instanceof GlobalShape global) { + // Global always intersects + return true; + } + return false; + } + + @Override + public RWorldRef getWorld() { + return box.getWorld(); + } + + @Override + public Box getBoundingBox() { + return box; + } + + @Override + public String toJson() { + Map data = new HashMap<>(); + Map minMap = new HashMap<>(); + minMap.put("x", box.getMin().x()); + minMap.put("y", box.getMin().y()); + minMap.put("z", box.getMin().z()); + data.put("min", minMap); + Map maxMap = new HashMap<>(); + maxMap.put("x", box.getMax().x()); + maxMap.put("y", box.getMax().y()); + maxMap.put("z", box.getMax().z()); + data.put("max", maxMap); + data.put("world", getWorld().identifier()); + return gson.toJson(data); + } + + public static CuboidShape fromJson(String json) { + try { + @SuppressWarnings("unchecked") + Map map = (Map) gson.fromJson(json, Map.class); + @SuppressWarnings("unchecked") + Map minMap = (Map) map.get("min"); + @SuppressWarnings("unchecked") + Map maxMap = (Map) map.get("max"); + String worldId = (String) map.get("world"); + + RBlockPos min = new RBlockPos(minMap.get("x").intValue(), minMap.get("y").intValue(), + minMap.get("z").intValue()); + RBlockPos max = new RBlockPos(maxMap.get("x").intValue(), maxMap.get("y").intValue(), + maxMap.get("z").intValue()); + + if (worldId == null || worldId.isBlank()) { + throw new IllegalArgumentException("Missing world id for CuboidShape"); + } + RWorldRef world = new RWorldRef(null, worldId); + return new CuboidShape(min, max, world); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize CuboidShape from JSON", e); + } + } + + public RBlockPos getMin() { + return box.getMin(); + } + + public RBlockPos getMax() { + return box.getMax(); + } + + public int getVolume() { + return box.getVolume(); + } + + public int getArea() { + return box.getArea(); + } +} diff --git a/api/src/main/java/de/t14d3/zones/objects/shapes/GlobalShape.java b/api/src/main/java/de/t14d3/zones/objects/shapes/GlobalShape.java new file mode 100644 index 0000000..b2d3ed9 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/objects/shapes/GlobalShape.java @@ -0,0 +1,61 @@ +package de.t14d3.zones.objects.shapes; + +import com.google.gson.Gson; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; + +import java.util.Map; + +/** + * Global shape that covers the entire world. + * Used for global/default permissions that apply everywhere. + */ +public class GlobalShape implements ShapeProvider { + + private static final Gson gson = new Gson(); + + private final RWorldRef world; + + public GlobalShape(RWorldRef world) { + this.world = world; + } + + @Override + public boolean contains(RBlockPos location) { + // Global shape contains all locations in its world + return true; + } + + @Override + public boolean intersects(ShapeProvider other) { + // Global shape intersects with any shape in the same world + if (other == null || other.getWorld() == null) return false; + return world.identifier().equals(other.getWorld().identifier()); + } + + @Override + public RWorldRef getWorld() { + return world; + } + + + @Override + public String toJson() { + return "{\"world\":\"" + getWorld().identifier() + "\"}"; + } + + public static GlobalShape fromJson(String json) { + try { + @SuppressWarnings("unchecked") + Map map = (Map) gson.fromJson(json, Map.class); + String worldId = (String) map.get("world"); + if (worldId == null || worldId.isBlank()) { + throw new IllegalArgumentException("Missing world id for GlobalShape"); + } + RWorldRef world = new RWorldRef(null, worldId); + return new GlobalShape(world); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize GlobalShape from JSON", e); + } + } +} diff --git a/api/src/main/java/de/t14d3/zones/objects/shapes/ShapeProvider.java b/api/src/main/java/de/t14d3/zones/objects/shapes/ShapeProvider.java new file mode 100644 index 0000000..0574099 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/objects/shapes/ShapeProvider.java @@ -0,0 +1,66 @@ +package de.t14d3.zones.objects.shapes; + +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.objects.Box; + +/** + * Interface for defining different region shapes. + * Implementations should provide methods to check if a location is contained + * within the shape and if shapes intersect. + */ +public interface ShapeProvider { + + /** + * Checks if the given block location is contained within this shape. + * + * @param location The location to check + * @return true if the location is inside this shape + */ + boolean contains(RBlockPos location); + + /** + * Checks if this shape intersects with another shape. + * Shapes intersect if there's any overlap in their areas. + * + * @param other The other shape to check intersection with + * @return true if shapes intersect + */ + boolean intersects(ShapeProvider other); + + /** + * Gets the world this shape belongs to. + * + * @return The world containing this shape + */ + RWorldRef getWorld(); + + /** + * Gets the axis-aligned bounding box for this shape if possible. + * For simple shapes like cuboids, this should return the exact bounds. + * For complex shapes, this can return a bounding box that fully contains the shape. + * For infinite shapes like global, this should return null. + * + * @return Bounding box if applicable, null otherwise + */ + default Box getBoundingBox() { + return null; + } + + /** + * Gets the shape type for serialization. + * + * @return The shape type string + */ + default String getShapeType() { + if (this instanceof GlobalShape) return "global"; + if (this instanceof CuboidShape) return "cuboid"; + return "unknown"; + } + + String toJson(); + + static ShapeProvider fromJson(String json) { + throw new UnsupportedOperationException(); + } +} diff --git a/api/src/main/java/de/t14d3/zones/permissions/CacheEntry.java b/api/src/main/java/de/t14d3/zones/permissions/CacheEntry.java deleted file mode 100644 index 3c69cfd..0000000 --- a/api/src/main/java/de/t14d3/zones/permissions/CacheEntry.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.t14d3.zones.permissions; - -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.Result; - -public class CacheEntry { - private final Object flag; - private final String value; - private final Object key; - - public Result result; - public final long timestamp; - - public CacheEntry(Object flag, String value, Object key, Result result) { - this.flag = flag; - this.value = value; - this.key = key; - this.result = result; - this.timestamp = System.currentTimeMillis() >> 10; // Convert to seconds, inaccuracy is negligible - } - - // Getters - public Object getFlag() { - return flag; - } - - public String getValue() { - return value; - } - - public Object getKey() { - return key; - } - - public boolean isEqual(Object flag, String value, Object key) { - // Weird stupid special handling for BlockLocation caches - if (flag instanceof BlockLocation loc) { - return loc.equals(this.flag) && this.value.equals(value) && this.key.equals(key); - } - return this.flag.equals(flag) && this.value.equals(value) && this.key.equals(key); - } -} diff --git a/api/src/main/java/de/t14d3/zones/permissions/CacheUtils.java b/api/src/main/java/de/t14d3/zones/permissions/CacheUtils.java deleted file mode 100644 index 81d79b3..0000000 --- a/api/src/main/java/de/t14d3/zones/permissions/CacheUtils.java +++ /dev/null @@ -1,141 +0,0 @@ -package de.t14d3.zones.permissions; - -import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.utils.DebugLoggerManager; - -import java.util.UUID; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; - -public class CacheUtils { - private final int ttl; - private final int limit; - private final int checkInterval; - private ScheduledFuture cacheTask; - private static CacheUtils instance; - private final Zones plugin; - - final ConcurrentHashMap> interactionCache = new ConcurrentHashMap<>(); - public final ConcurrentHashMap> permissionCache = new ConcurrentHashMap<>(); - - public CacheUtils(Zones plugin) { - instance = this; - this.ttl = plugin.getConfig().getInt("cache.ttl", 300); - this.checkInterval = plugin.getConfig().getInt("cache.check-interval", 10); - this.limit = plugin.getConfig().getInt("cache.limit", 0); - this.plugin = plugin; - } - - public static CacheUtils getInstance() { - if (instance == null) throw new IllegalStateException("CacheUtils is not yet initialized!"); - return instance; - } - - public void startCacheRunnable() { - CacheRunnable runnable = new CacheRunnable(plugin.getDebugLogger()); - cacheTask = Executors.newSingleThreadScheduledExecutor() - .scheduleAtFixedRate(runnable, checkInterval, checkInterval, TimeUnit.MINUTES); - plugin.getLogger() - .info("Cache scheduler has been started! (TTL: {} seconds, Interval: {} minutes, Limit: {})", ttl, - checkInterval, limit); - } - - public void invalidateInteractionCache(UUID target) { - interactionCache.remove(target.toString()); - } - - public void invalidateInteractionCache(String target) { - interactionCache.remove(target); - } - - public void invalidateInteractionCaches() { - interactionCache.clear(); - } - - public void invalidateInteractionCacheForChunk(int chunkX, int chunkZ, String world) { - Zones.getInstance().getThreadPool().execute(() -> { - synchronized (interactionCache) { - interactionCache.forEach((uuid, cacheEntries) -> { - for (CacheEntry entry : cacheEntries) { - BlockLocation location = (BlockLocation) entry.getFlag(); - int locX = location.getX() >> 4; - int locZ = location.getZ() >> 4; - if (chunkX == locX && chunkZ == locZ) { - interactionCache.computeIfAbsent(uuid, k -> new ConcurrentLinkedQueue<>()).remove(entry); - } - } - }); - } - }); - } - - public void invalidateCache(String target) { - permissionCache.remove(target); - } - - public void invalidateCaches() { - permissionCache.clear(); - } - - public class CacheRunnable implements Runnable { - private final DebugLoggerManager logger; - - public CacheRunnable(DebugLoggerManager logger) { - this.logger = logger; - } - - @Override - public void run() { - logger.log("Running cache scheduler..."); - synchronized (interactionCache) { - synchronized (permissionCache) { - AtomicInteger initialSize = new AtomicInteger(); - interactionCache.forEach( - (uuid, entries) -> initialSize.addAndGet(entries.size()) - ); - AtomicInteger initialPermSize = new AtomicInteger(); - permissionCache.forEach( - (uuid, entries) -> initialPermSize.addAndGet(entries.size()) - ); - logger.log( - "Cache size: " + initialSize.get() + " (interaction cache) and " + initialPermSize.get() + " (permission cache)"); - AtomicInteger size = new AtomicInteger(); - AtomicInteger permSize = new AtomicInteger(); - final long current = System.currentTimeMillis() >> 10; - if (limit > 0) { - if (initialSize.get() > limit) { - interactionCache.clear(); - } - if (initialPermSize.get() > limit) { - permissionCache.clear(); - } - } - interactionCache.values().forEach( - entries -> entries.removeIf(entry -> current - entry.timestamp > ttl) - ); - interactionCache.forEach((key, value) -> { - if (value.isEmpty()) { - interactionCache.remove(key); - } else { - size.addAndGet(value.size()); - } - }); - permissionCache.values().forEach( - entries -> entries.removeIf(entry -> current - entry.timestamp > ttl) - ); - permissionCache.forEach((key, value) -> { - if (value.isEmpty()) { - permissionCache.remove(key); - } else { - permSize.addAndGet(value.size()); - } - }); - - logger.log("Removed " + (initialSize.get() - size.get()) + " interaction cache entries and " - + (initialPermSize.get() - permSize.get()) + " permission cache entries."); - } - } - } - } -} diff --git a/api/src/main/java/de/t14d3/zones/permissions/Decision.java b/api/src/main/java/de/t14d3/zones/permissions/Decision.java new file mode 100644 index 0000000..6e8045f --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/Decision.java @@ -0,0 +1,19 @@ +package de.t14d3.zones.permissions; + +import de.t14d3.zones.objects.Result; + +/** + * A resolved permission decision with a specificity score. + * + *

Higher specificity wins. If two matches have equal specificity, deny wins.

+ */ +public record Decision(Result result, int specificity) { + public static Decision undefined() { + return new Decision(Result.UNDEFINED, -1); + } + + public static Decision global(Result result) { + return new Decision(result, 0); + } +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/GlobPattern.java b/api/src/main/java/de/t14d3/zones/permissions/GlobPattern.java new file mode 100644 index 0000000..5688cb8 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/GlobPattern.java @@ -0,0 +1,93 @@ +package de.t14d3.zones.permissions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Simple '*' glob matcher (not regex). + */ +public final class GlobPattern { + private final String raw; + private final boolean startsWithWildcard; + private final boolean endsWithWildcard; + private final String[] parts; + private final int specificity; + + private GlobPattern(String raw, boolean startsWithWildcard, boolean endsWithWildcard, String[] parts, int specificity) { + this.raw = raw; + this.startsWithWildcard = startsWithWildcard; + this.endsWithWildcard = endsWithWildcard; + this.parts = parts; + this.specificity = specificity; + } + + public static GlobPattern compile(String pattern) { + if (pattern == null) pattern = ""; + String p = pattern.trim().toLowerCase(Locale.ROOT); + boolean starts = p.startsWith("*"); + boolean ends = p.endsWith("*"); + + // Split on '*' and drop empty segments. + List segments = new ArrayList<>(); + int last = 0; + for (int i = 0; i < p.length(); i++) { + if (p.charAt(i) != '*') continue; + if (i > last) { + String seg = p.substring(last, i); + if (!seg.isEmpty()) segments.add(seg); + } + last = i + 1; + } + if (last < p.length()) { + String seg = p.substring(last); + if (!seg.isEmpty()) segments.add(seg); + } + + int spec = 0; + for (String s : segments) spec += s.length(); + return new GlobPattern(p, starts, ends, segments.toArray(new String[0]), spec); + } + + public int specificity() { + return specificity; + } + + public boolean matches(String input) { + if (input == null) return false; + String in = input.toLowerCase(Locale.ROOT); + if (raw.equals("*")) return true; + if (parts.length == 0) return raw.equals(in); + + int idx = 0; + int partIndex = 0; + + if (!startsWithWildcard) { + String first = parts[0]; + if (!in.startsWith(first)) return false; + idx = first.length(); + partIndex = 1; + } + + int lastPart = parts.length - 1; + if (!endsWithWildcard) { + // Reserve the last part for the suffix check. + lastPart -= 1; + } + + for (int i = partIndex; i <= lastPart; i++) { + String part = parts[i]; + int found = in.indexOf(part, idx); + if (found < 0) return false; + idx = found + part.length(); + } + + if (!endsWithWildcard) { + String suffix = parts[parts.length - 1]; + int suffixIdx = in.lastIndexOf(suffix); + return suffixIdx >= idx && suffixIdx + suffix.length() == in.length(); + } + + return true; + } +} diff --git a/api/src/main/java/de/t14d3/zones/permissions/PermissionKeyRegistry.java b/api/src/main/java/de/t14d3/zones/permissions/PermissionKeyRegistry.java new file mode 100644 index 0000000..a4d4acc --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/PermissionKeyRegistry.java @@ -0,0 +1,51 @@ +package de.t14d3.zones.permissions; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Runtime registry mapping permission/flag names to compact int ids for fast lookups. + * + *

These ids are used for in-memory performance; on-disk formats should still prefer + * stable names unless an explicit persisted mapping is introduced.

+ */ +public final class PermissionKeyRegistry { + private static final PermissionKeyRegistry INSTANCE = new PermissionKeyRegistry(); + + public static PermissionKeyRegistry instance() { + return INSTANCE; + } + + private final Map nameToId = new HashMap<>(); + private final List idToName = new ArrayList<>(); + + private PermissionKeyRegistry() { + } + + public synchronized int getOrCreateId(String name) { + name = normalize(name); + Integer existing = nameToId.get(name); + if (existing != null) return existing; + int id = idToName.size(); + idToName.add(name); + nameToId.put(name, id); + return id; + } + + public synchronized Integer getId(String name) { + return nameToId.get(normalize(name)); + } + + public synchronized String getName(int id) { + if (id < 0 || id >= idToName.size()) return null; + return idToName.get(id); + } + + private static String normalize(String name) { + if (name == null) return ""; + return name.trim().toLowerCase(); + } +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/PermissionManager.java b/api/src/main/java/de/t14d3/zones/permissions/PermissionManager.java index 59c7951..0f920db 100644 --- a/api/src/main/java/de/t14d3/zones/permissions/PermissionManager.java +++ b/api/src/main/java/de/t14d3/zones/permissions/PermissionManager.java @@ -4,36 +4,104 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.stream.JsonReader; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.*; +import de.t14d3.zones.objects.Flag; +import de.t14d3.zones.objects.Result; +import de.t14d3.zones.permissions.flags.FlagContext; +import de.t14d3.zones.permissions.subjects.GroupSubject; +import de.t14d3.zones.permissions.subjects.PlayerSubject; +import de.t14d3.zones.permissions.subjects.SubjectRef; +import de.t14d3.zones.permissions.subjects.Subjects; +import de.t14d3.zones.rapunzellib.ZonesPermissionCache; import de.t14d3.zones.utils.DebugLoggerManager; +import de.t14d3.zones.utils.TypeKeys; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; public class PermissionManager { - private final CacheUtils cacheUtils; private final DebugLoggerManager debugLogger; private final Zones zones; - public static final String UNIVERSAL = "+universal"; private final List permissionMap; + private final AtomicLong cacheVersion = new AtomicLong(1); + private final RegionPermissionEvaluator evaluator; + private final long permissionCacheTtlNanos; + public PermissionManager(Zones zones) { this.zones = zones; this.debugLogger = zones.getDebugLogger(); - this.cacheUtils = CacheUtils.getInstance(); - this.permissionMap = readPermissions(); + + // Load the permission registry shipped with the plugin (permissions.json). + // This is exposed for help/inspection; permission evaluation itself is based on region data. + List permissions = new ArrayList<>(); + Gson gson = new Gson(); + JsonObject obj = gson.fromJson( + new JsonReader(new InputStreamReader(zones.getClass().getResourceAsStream("/permissions.json"))), + JsonObject.class); + for (Map.Entry entry : obj.getAsJsonObject("permissions").entrySet()) { + String key = entry.getKey(); + JsonObject permission = entry.getValue().getAsJsonObject(); + String description = permission.get("description").getAsString(); + int level = permission.get("level").getAsInt(); + permissions.add(new Permission(key, description, level)); + } + this.permissionMap = permissions; + + long ttlSeconds = zones.getConfig().getInt("cache.ttl", 300); + int limit = zones.getConfig().getInt("cache.limit", 0); + int permissionTtlSeconds = zones.getConfig().getInt("cache.permission-ttl", 2); + this.permissionCacheTtlNanos = TimeUnit.SECONDS.toNanos(Math.max(0, permissionTtlSeconds)); + + this.evaluator = new RegionPermissionEvaluator( + zones.getRegionManager(), + cacheVersion::get, + ttlSeconds * 1000L, + limit + ); } - public boolean checkAction(BlockLocation location, World world, UUID playerUUID, Flag action, String type, Object... extra) { - return checkAction(location, world, playerUUID.toString(), action, type, extra); + /** + * Invalidates all cached permission decisions. + */ + public void invalidateAll() { + cacheVersion.incrementAndGet(); + evaluator.invalidateAll(); } + /** + * Invalidates cached decisions for a specific subject (player UUID). + */ + public void invalidateSubject(UUID uuid) { + if (uuid == null) return; + evaluator.invalidateSubject(new PlayerSubject(uuid)); + } + + + /** + * Checks whether a player is allowed to perform an action at a given location. + * + * @param location block position + * @param world world reference + * @param playerUUID player UUID + * @param action action flag + * @param type type key involved in the interaction + */ + public boolean checkAction(RBlockPos location, RWorldRef world, UUID playerUUID, Flag action, String type) { + if (playerUUID == null) return false; + return checkAction(location, world, new PlayerSubject(playerUUID), action, type, FlagContext.none()); + } + + /** * Checks if a player can interact with a region. * @@ -41,83 +109,65 @@ public boolean checkAction(BlockLocation location, World world, UUID playerUUID, * @param who The UUID of the player. * @param action The action the player wants to perform. * @param type The type of the block or entity the interaction happened with. - * @param extra Additional, optional information, for example a spawn reason. * @return True if the player can interact with the region, false otherwise. */ - public boolean checkAction(BlockLocation location, World world, String who, Flag action, String type, Object... extra) { - debugLogger.log(DebugLoggerManager.CHECK, action.name(), who, location, type); - boolean nonplayer = who.equalsIgnoreCase(UNIVERSAL) || extra.length != 0 && (boolean) extra[0]; - boolean base = extra.length == 0; - if (nonplayer) { - debugLogger.log(DebugLoggerManager.UNI_CHECK, action.name(), location, type); - return checkAction(location, world, action, type, extra); - } - // Check interaction cache - if (base && cacheUtils.interactionCache.containsKey(who)) { - ConcurrentLinkedQueue entries = cacheUtils.interactionCache.get(who); - for (CacheEntry entry : entries) { - if (entry.isEqual(location, action.name(), type)) { - debugLogger.log(DebugLoggerManager.CACHE_HIT_ACTION, action.name(), who, location, type); - return entry.result.equals(Result.TRUE); - } - } - } + public boolean checkAction(RBlockPos location, RWorldRef world, String who, Flag action, String type) { + return checkAction(location, world, parseSubject(who), action, type, FlagContext.none()); + } + + /** + * Checks if a player can interact with a region. + * + * @param location The location of the interaction. + * @param subject The subject of the interaction. + * @param action The action the player wants to perform. + * @param type The type of the block or entity the interaction happened with. + * @param context Additional, optional information, for example a spawn reason. + * @return True if the player can interact with the region, false otherwise. + */ + public boolean checkAction(RBlockPos location, RWorldRef world, SubjectRef subject, Flag action, String type, FlagContext context) { + if (action == null) return true; + FlagContext ctx = context != null ? context : FlagContext.none(); + if (location == null || world == null) return action.getDefaultValue(ctx); - List regions = zones.getRegionManager().getRegionsAt(location, world); - if (!regions.isEmpty()) { - Result result = Result.UNDEFINED; - int priority = Integer.MIN_VALUE; + final SubjectRef effectiveSubject = subject != null ? subject : Subjects.universal(); + final String subjectLabel = Subjects.format(effectiveSubject); - for (Region region : regions) { - debugLogger.log("Checking region " + region.getKey() - .toString() + " for " + action.name() + " with type " + type, DebugLoggerManager.CHECK); - - // Only check regions with a higher priority than the current value - if (region.getPriority() > priority) { - Result hasPermission = action.getCustomHandler().evaluate(region, who, action.name(), type); - if (!hasPermission.equals(Result.UNDEFINED)) { - result = hasPermission; - priority = region.getPriority(); - continue; - } - } - // If same priority, both have to be true, otherwise will assume false - else if (region.getPriority() == priority) { - Result hasPermission = action.getCustomHandler().evaluate(region, who, action.name(), type); - if (hasPermission.equals(Result.FALSE) || result.equals(Result.FALSE)) { - result = Result.FALSE; - priority = region.getPriority(); - continue; - } + String normalizedType = TypeKeys.normalize(type); + debugLogger.log(DebugLoggerManager.CHECK, action.name(), subjectLabel, location, normalizedType); + + return zones.getRegionManager().withWorldReadLock(world, () -> { + List regions = zones.getRegionManager().getRegionsAt(location, world); + if (regions.isEmpty()) { + if (effectiveSubject instanceof PlayerSubject p) { + boolean bypass = RPlayer.get(p.uuid()) + .map(player -> ZonesPermissionCache.hasPermissionCached(player, "zones.bypass.unclaimed", + permissionCacheTtlNanos)) + .orElse(false); + debugLogger.log(DebugLoggerManager.PERM, action.name(), subjectLabel, location, normalizedType, + bypass); + return bypass; } + return action.getDefaultValue(ctx); } - if (result.equals(Result.UNDEFINED)) { - result = Result.valueOf(action.getDefaultValue(who)); + int maxPriority = Integer.MIN_VALUE; + for (Region region : regions) { + if (region.getPriority() > maxPriority) maxPriority = region.getPriority(); } - // Update cache if needed - if (base) { - cacheUtils.interactionCache.computeIfAbsent(who, k -> new ConcurrentLinkedQueue<>()) - .add(new CacheEntry(location, action.name(), type, result)); - } - debugLogger.log(DebugLoggerManager.CACHE_MISS_ACTION, action.name(), who, location, type, result); - return result.equals(Result.TRUE); - - } else { - // No region found, check player permissions - boolean bypass = false; - Player player = PlayerRepository.get(UUID.fromString(who)); - if (player != null && player.hasPermission("zones.bypass.unclaimed")) { - bypass = true; - } - debugLogger.log(DebugLoggerManager.PERM, action.name(), who, location, type, bypass); - if (base) { - cacheUtils.interactionCache.computeIfAbsent(who, k -> new ConcurrentLinkedQueue<>()) - .add(new CacheEntry(location, action.name(), type, bypass ? Result.TRUE : Result.UNDEFINED)); + boolean anyAllow = false; + for (Region region : regions) { + if (region.getPriority() != maxPriority) continue; + + Decision decision = evaluator.evaluate(region, effectiveSubject, action, normalizedType); + if (decision.result() == Result.FALSE) return false; + if (decision.result() == Result.TRUE) anyAllow = true; } - return bypass; - } + + if (anyAllow) return true; + return action.getDefaultValue(ctx); + }); } /** @@ -127,88 +177,120 @@ else if (region.getPriority() == priority) { * @param location The location of the interaction * @param action The action being performed * @param type The type of block/entity involved - * @param extra Additional context (used for cache control) * @return true if the action is allowed, false otherwise */ - public boolean checkAction(BlockLocation location, World world, Flag action, String type, Object... extra) { - boolean base = extra == null || extra.length == 0; - - if (base && cacheUtils.interactionCache.containsKey(UNIVERSAL)) { - ConcurrentLinkedQueue entries = cacheUtils.interactionCache.get(UNIVERSAL); - for (CacheEntry entry : entries) { - if (entry.isEqual(location, action.name(), type)) { - debugLogger.log(DebugLoggerManager.CACHE_HIT_ACTION, DebugLoggerManager.UNI_CHECK, action.name(), - location, type, entry.result); - return entry.result.equals(Result.TRUE); - } + public boolean checkAction(RBlockPos location, RWorldRef world, Flag action, String type) { + return checkAction(location, world, Subjects.universal(), action, type, FlagContext.none()); + } + + /** + * Checks if a universal/non-player action is allowed at a location. + * This method bypasses player-specific checks for efficiency. + * + * @param location The location of the interaction + * @param action The action being performed + * @param type The type of block/entity involved + * @param context Additional context (used for flag defaults) + * @return true if the action is allowed, false otherwise + */ + public boolean checkAction(RBlockPos location, RWorldRef world, Flag action, String type, FlagContext context) { + return checkAction(location, world, Subjects.universal(), action, type, context); + } + + /** + * Checks whether a player is allowed to perform multiple actions at a location. + * + *

This batches evaluation under a single world read-lock and reuses the same region query for all flags. + * Semantics match calling {@link #checkAction(RBlockPos, RWorldRef, UUID, Flag, String)} for each flag.

+ * + * @param location block position + * @param world world reference + * @param playerUUID player UUID + * @param type type key involved in the interaction + * @param actions action flags to evaluate + * @return {@code true} if all actions are allowed, otherwise {@code false} + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean checkActions(RBlockPos location, RWorldRef world, UUID playerUUID, String type, Flag... actions) { + if (playerUUID == null) return false; + if (actions == null || actions.length == 0) return true; + if (location == null || world == null) { + for (Flag action : actions) { + if (action == null) continue; + if (!action.getDefaultValue()) return false; } + return true; } - List regions = zones.getRegionManager().getRegionsAt(location, world); - Result result = Result.UNDEFINED; - if (!regions.isEmpty()) { - int priority = Integer.MIN_VALUE; + SubjectRef subject = new PlayerSubject(playerUUID); + String normalizedType = TypeKeys.normalize(type); - for (Region region : regions) { - if (region.getPriority() > priority) { - Result regionResult = action.getCustomHandler().evaluate(region, action.name(), type); - if (regionResult != Result.UNDEFINED) { - result = regionResult; - priority = region.getPriority(); - } - } else if (region.getPriority() == priority) { - Result regionResult = action.getCustomHandler().evaluate(region, action.name(), type); - if (regionResult == Result.FALSE || result == Result.FALSE) { - result = Result.FALSE; - priority = region.getPriority(); - } + return zones.getRegionManager().withWorldReadLock(world, () -> { + List regions = zones.getRegionManager().getRegionsAt(location, world); + if (regions.isEmpty()) { + boolean bypass = RPlayer.get(playerUUID) + .map(p -> ZonesPermissionCache.hasPermissionCached(p, "zones.bypass.unclaimed", + permissionCacheTtlNanos)) + .orElse(false); + + for (Flag action : actions) { + if (action == null) continue; + debugLogger.log(DebugLoggerManager.CHECK, action.name(), Subjects.format(subject), location, + normalizedType); + debugLogger.log(DebugLoggerManager.PERM, action.name(), Subjects.format(subject), location, + normalizedType, bypass); } + + // In unclaimed land, bypass is a global allow/deny for players (matches single-flag behavior). + return bypass; + } - } - // No regions at location - use default value - if (result == Result.UNDEFINED) { - result = Result.valueOf(action.getDefaultValue(UNIVERSAL)); - } + int maxPriority = Integer.MIN_VALUE; + for (Region region : regions) { + int priority = region.getPriority(); + if (priority > maxPriority) maxPriority = priority; + } - cacheUtils.interactionCache.computeIfAbsent(UNIVERSAL, k -> new ConcurrentLinkedQueue<>()) - .add(new CacheEntry(location, action.name(), type, result)); + for (Flag action : actions) { + if (action == null) continue; - debugLogger.log(DebugLoggerManager.CACHE_MISS_ACTION, DebugLoggerManager.UNI_CHECK, action.name(), location, - type, result, extra); + debugLogger.log(DebugLoggerManager.CHECK, action.name(), Subjects.format(subject), location, + normalizedType); - return result == Result.TRUE; - } + boolean anyAllow = false; + for (Region region : regions) { + if (region.getPriority() != maxPriority) continue; + Decision decision = evaluator.evaluate(region, subject, action, normalizedType); + if (decision.result() == Result.FALSE) return false; + if (decision.result() == Result.TRUE) anyAllow = true; + } - public static Result isAllowed(String perm, String type, Result result) { - perm = perm.toLowerCase(); - type = type.toLowerCase(); - if (perm.equals("true") || perm.equals("*")) { - result = Result.TRUE; - } else if (perm.equals("false") || perm.equals("!*")) { - result = Result.FALSE; - } else if (perm.equals(type)) { - result = Result.TRUE; - } else if (perm.equals("!" + type)) { - result = Result.FALSE; - } - return result; + if (anyAllow) continue; + if (!action.getDefaultValue()) return false; + } + + return true; + }); } - public List readPermissions() { - List permissions = new ArrayList<>(); - Gson gson = new Gson(); - JsonObject obj = gson.fromJson( - new JsonReader(new InputStreamReader(zones.getClass().getResourceAsStream("/permissions.json"))), - JsonObject.class); - for (Map.Entry entry : obj.getAsJsonObject("permissions").entrySet()) { - String key = entry.getKey(); - JsonObject permission = entry.getValue().getAsJsonObject(); - String description = permission.get("description").getAsString(); - int level = permission.get("level").getAsInt(); - permissions.add(new Permission(key, description, level)); + private static SubjectRef parseSubject(String raw) { + if (raw == null || raw.isBlank()) return Subjects.universal(); + String s = raw.trim(); + if ("universal".equalsIgnoreCase(s) + || "+universal".equalsIgnoreCase(s)) { + return Subjects.universal(); + } + if (s.regionMatches(true, 0, "group:", 0, "group:".length())) { + String name = Subjects.normalizeGroupName(s.substring("group:".length())); + return name == null ? Subjects.universal() : new GroupSubject(name); + } + if (s.regionMatches(true, 0, "player:", 0, "player:".length())) s = s.substring("player:".length()); + try { + return new PlayerSubject(UUID.fromString(s)); + } catch (IllegalArgumentException ignored) { + return Subjects.universal(); } - return permissions; } /** @@ -224,34 +306,15 @@ public List getPermissions() { * Simple permission object. * Contains a name, description, and level (Vanilla operator level equivalent). */ - public static class Permission { - private final String value; - private final String description; - private final int level; - + public record Permission(String name, String description, int level) { /** * Creates a new permission object. - * @param value The permission name. + * + * @param name The permission name. * @param description The permission description. - * @param level The permission level. + * @param level The permission level. */ - public Permission(String value, String description, int level) { - this.value = value; - this.description = description; - this.level = level; + public Permission {} } - public String getName() { - return this.value; - } - - public String getDescription() { - return this.description; - } - - public int getLevel() { - return this.level; - } - } - } diff --git a/api/src/main/java/de/t14d3/zones/permissions/RegionMembership.java b/api/src/main/java/de/t14d3/zones/permissions/RegionMembership.java new file mode 100644 index 0000000..963199f --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/RegionMembership.java @@ -0,0 +1,113 @@ +package de.t14d3.zones.permissions; + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * Region membership data (roles, groups, and group inheritance). + * + *

This is separate from the region ACL (see {@link RegionPermissions}) so membership semantics don't leak into + * permission rules.

+ */ +public final class RegionMembership { + private final Map players = new HashMap<>(); + private final Map groups = new HashMap<>(); + + public Map players() { + return players; + } + + public Map groups() { + return groups; + } + + public @NotNull PlayerEntry player(@NotNull UUID uuid) { + return players.computeIfAbsent(uuid, ignored -> new PlayerEntry()); + } + + public @NotNull GroupEntry group(@NotNull String name) { + String key = normalizeName(name); + return groups.computeIfAbsent(key, ignored -> new GroupEntry()); + } + + public static @Nullable String normalizeName(@Nullable String raw) { + if (raw == null) return null; + String n = raw.trim().toLowerCase(Locale.ROOT); + return n.isEmpty() ? null : n; + } + + public static final class PlayerEntry { + private final AllowDenyStringSet roles = new AllowDenyStringSet(); + private final AllowDenyStringSet groups = new AllowDenyStringSet(); + + public AllowDenyStringSet roles() { + return roles; + } + + public AllowDenyStringSet groups() { + return groups; + } + } + + /** + * Per-group metadata (currently only inheritance via includes). + */ + public static final class GroupEntry { + private final AllowDenyStringSet includes = new AllowDenyStringSet(); + + public AllowDenyStringSet includes() { + return includes; + } + } + + /** + * Simple allow/deny set with normalization and "deny removes allow" behavior. + */ + public static final class AllowDenyStringSet { + private final ObjectOpenHashSet allow = new ObjectOpenHashSet<>(); + private final ObjectOpenHashSet deny = new ObjectOpenHashSet<>(); + + public Set allowValues() { + return allow; + } + + public Set denyValues() { + return deny; + } + + public void allow(String value) { + String v = normalizeName(value); + if (v == null) return; + deny.remove(v); + allow.add(v); + } + + public void deny(String value) { + String v = normalizeName(value); + if (v == null) return; + allow.remove(v); + deny.add(v); + } + + public boolean removeAllow(String value) { + String v = normalizeName(value); + if (v == null) return false; + return allow.remove(v); + } + + public boolean removeDeny(String value) { + String v = normalizeName(value); + if (v == null) return false; + return deny.remove(v); + } + + public void clear() { + allow.clear(); + deny.clear(); + } + } +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/RegionPermissionEvaluator.java b/api/src/main/java/de/t14d3/zones/permissions/RegionPermissionEvaluator.java new file mode 100644 index 0000000..b6054dd --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/RegionPermissionEvaluator.java @@ -0,0 +1,263 @@ +package de.t14d3.zones.permissions; + +import de.t14d3.zones.Region; +import de.t14d3.zones.RegionManager; +import de.t14d3.zones.objects.Flag; +import de.t14d3.zones.objects.Result; +import de.t14d3.zones.permissions.subjects.GroupSubject; +import de.t14d3.zones.permissions.subjects.PlayerSubject; +import de.t14d3.zones.permissions.subjects.SubjectRef; +import de.t14d3.zones.permissions.subjects.Subjects; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.LongSupplier; + +final class RegionPermissionEvaluator { + private final RegionManager regionManager; + private final LongSupplier versionSupplier; + private final long ttlMillis; + private final int limit; + + private final IdRegistry subjectIds = new IdRegistry<>(); + private final StringIdRegistry typeIds = new StringIdRegistry(); + + private final ConcurrentHashMap decisionCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap memberCache = new ConcurrentHashMap<>(); + + RegionPermissionEvaluator( + RegionManager regionManager, + LongSupplier versionSupplier, + long ttlMillis, + int limit + ) { + this.regionManager = regionManager; + this.versionSupplier = versionSupplier; + this.ttlMillis = ttlMillis; + this.limit = limit; + } + + Decision evaluate(Region region, SubjectRef subject, Flag flag, String typeKey) { + if (region == null || flag == null) return Decision.undefined(); + + String type = typeKey == null ? "" : typeKey; + + long now = System.currentTimeMillis(); + long version = versionSupplier.getAsLong(); + + int regionId = region.getKey().getValue(); + int subjectId = subjectIds.id(subject != null ? subject : Subjects.universal()); + int typeId = typeIds.id(type); + + EvalKey key = new EvalKey(pack(regionId, subjectId), pack(flag.id(), typeId)); + CachedDecision cached = decisionCache.get(key); + if (cached != null && cached.version == version && cached.expiresAtMillis > now) { + return cached.decision; + } + + // Evaluation order: + // 1) Explicit subject rules (including targeted rules by type) + // 2) Admin/owner role-based bypass for player subjects + // 3) Group subjects (best specificity wins; deny wins ties) + // 4) Universal subject + + Decision direct = decisionForSubject(region, subject, flag, type); + Decision computed; + if (direct.result() != Result.UNDEFINED) { + computed = direct; + } else if (subject instanceof PlayerSubject && memberContext(region, subject).isAdmin) { + computed = new Decision(Result.TRUE, Integer.MAX_VALUE); + } else { + CachedMemberContext ctx = memberContext(region, subject); + Decision bestGroup = Decision.undefined(); + for (String group : ctx.groups) { + Decision d = decisionForSubject(region, new GroupSubject(group), flag, type); + if (d.result() == Result.UNDEFINED) continue; + if (bestGroup.result() == Result.UNDEFINED + || d.specificity() > bestGroup.specificity() + || (d.specificity() == bestGroup.specificity() && d.result() == Result.FALSE)) { + bestGroup = d; + } + } + + if (bestGroup.result() != Result.UNDEFINED) { + computed = bestGroup; + } else { + Decision universal = decisionForSubject(region, Subjects.universal(), flag, type); + computed = (universal.result() != Result.UNDEFINED) ? universal : Decision.undefined(); + } + } + + if (limit > 0 && decisionCache.size() > limit) { + decisionCache.clear(); + } + decisionCache.put(key, new CachedDecision(computed, version, now + ttlMillis)); + return computed; + } + + void invalidateAll() { + decisionCache.clear(); + memberCache.clear(); + } + + void invalidateSubject(SubjectRef subject) { + if (subject == null) return; + Integer subjectId = subjectIds.findId(subject); + if (subjectId == null) return; + + // Remove member contexts. + memberCache.keySet().removeIf(k -> (int) (k & 0xFFFFFFFFL) == subjectId); + + // Remove cached decisions. + decisionCache.keySet().removeIf(k -> (int) (k.regionAndSubject & 0xFFFFFFFFL) == subjectId); + } + + private CachedMemberContext memberContext(Region region, SubjectRef subject) { + long now = System.currentTimeMillis(); + long version = versionSupplier.getAsLong(); + + int regionId = region.getKey().getValue(); + int subjectId = subjectIds.id(subject); + long key = pack(regionId, subjectId); + + CachedMemberContext cached = memberCache.get(key); + if (cached != null && cached.version == version && cached.expiresAtMillis > now) { + return cached; + } + + // Only player subjects have membership context. + if (!(subject instanceof PlayerSubject player)) { + CachedMemberContext computed = new CachedMemberContext(false, new String[0], version, now + ttlMillis); + memberCache.put(key, computed); + return computed; + } + + // Build lineage root->leaf to apply parent roles/groups first. + ArrayDeque lineage = new ArrayDeque<>(); + Region current = region; + Set visited = null; + while (current != null) { + lineage.addFirst(current); + if (current.getParent() == null) break; + if (visited == null) visited = new HashSet<>(); + if (!visited.add(current.getKey().getValue())) break; + current = current.getParentRegion(regionManager); + } + + Set roles = new HashSet<>(); + Set directGroups = new HashSet<>(); + Map> includes = new HashMap<>(); + + for (Region r : lineage) { + RegionMembership.PlayerEntry entry = r.getMembership().players().get(player.uuid()); + if (entry != null) { + for (String deny : entry.roles().denyValues()) roles.remove(deny); + for (String allow : entry.roles().allowValues()) roles.add(allow); + for (String deny : entry.groups().denyValues()) directGroups.remove(deny); + for (String allow : entry.groups().allowValues()) directGroups.add(allow); + } + + for (Map.Entry ge : r.getMembership().groups().entrySet()) { + String groupName = ge.getKey(); + RegionMembership.GroupEntry groupEntry = ge.getValue(); + if (groupName == null || groupEntry == null) continue; + Set set = includes.computeIfAbsent(groupName, ignored -> new HashSet<>()); + for (String deny : groupEntry.includes().denyValues()) set.remove(deny); + for (String allow : groupEntry.includes().allowValues()) set.add(allow); + } + } + + boolean isAdmin = roles.contains("owner") || roles.contains("admin"); + + Set expandedGroups = new HashSet<>(directGroups); + ArrayDeque queue = new ArrayDeque<>(directGroups); + while (!queue.isEmpty()) { + String g = queue.removeFirst(); + Set inc = includes.get(g); + if (inc == null) continue; + for (String child : inc) { + if (child == null || child.isBlank()) continue; + if (expandedGroups.add(child)) queue.addLast(child); + } + } + + CachedMemberContext computed = new CachedMemberContext(isAdmin, expandedGroups.toArray(new String[0]), version, + now + ttlMillis); + if (limit > 0 && memberCache.size() > limit) { + memberCache.clear(); + } + memberCache.put(key, computed); + return computed; + } + + private Decision decisionForSubject(Region region, SubjectRef subject, Flag flag, String typeKey) { + Region current = region; + Set visited = null; + while (current != null) { + RegionPermissions perms = current.getPermissions(); + RegionPermissions.SubjectPermissions subjectPerms = perms.subjects().get(subject); + if (subjectPerms != null) { + RegionPermissions.PermissionValue pv = subjectPerms.get(flag.id()); + if (pv instanceof RegionPermissions.TargetDecisionValue targeted) { + Decision d = targeted.evaluateMatch(typeKey); + if (d.result() != Result.UNDEFINED) return d; + } else if (pv instanceof RegionPermissions.StringSetValue set) { + Result r = set.evaluate(typeKey); + if (r != Result.UNDEFINED) return new Decision(r, Integer.MAX_VALUE - 1); + } + } + + if (current.getParent() == null) break; + if (visited == null) visited = new HashSet<>(); + if (!visited.add(current.getKey().getValue())) break; + current = current.getParentRegion(regionManager); + } + return Decision.undefined(); + } + + private static long pack(int high, int low) { + return ((long) high << 32) | (low & 0xFFFFFFFFL); + } + + private static final class IdRegistry { + private final ConcurrentHashMap ids = new ConcurrentHashMap<>(); + private final AtomicInteger next = new AtomicInteger(1); + + int id(T key) { + if (key == null) return 0; + T k = key; + return ids.computeIfAbsent(k, ignored -> next.getAndIncrement()); + } + + Integer findId(T key) { + if (key == null) return null; + return ids.get(key); + } + } + + private static final class StringIdRegistry { + private final ConcurrentHashMap ids = new ConcurrentHashMap<>(); + private final AtomicInteger next = new AtomicInteger(1); + + int id(String key) { + if (key == null) key = ""; + String k = key; + return ids.computeIfAbsent(k, ignored -> next.getAndIncrement()); + } + + Integer findId(String key) { + if (key == null) return null; + return ids.get(key); + } + } + + private record EvalKey(long regionAndSubject, long flagAndType) { + } + + private record CachedDecision(Decision decision, long version, long expiresAtMillis) { + } + + private record CachedMemberContext(boolean isAdmin, String[] groups, long version, long expiresAtMillis) { + } +} diff --git a/api/src/main/java/de/t14d3/zones/permissions/RegionPermissions.java b/api/src/main/java/de/t14d3/zones/permissions/RegionPermissions.java new file mode 100644 index 0000000..ab348cc --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/RegionPermissions.java @@ -0,0 +1,407 @@ +package de.t14d3.zones.permissions; + +import de.t14d3.zones.objects.Result; +import de.t14d3.zones.permissions.flags.FlagValueKind; +import de.t14d3.zones.permissions.subjects.SubjectRef; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Per-region permission overrides grouped by "subject" (player, group, universal, ...). + * + *

This is intentionally a low-level data container: it only stores explicit allow/deny entries. + * Any fallback behavior (e.g. per-flag defaults) is handled by the permission evaluation layer.

+ * + *

All inputs are normalized (trimmed, lowercased). This class is not thread-safe; callers should + * use the region/world locks provided by {@code RegionManager} when mutating or iterating.

+ */ +public final class RegionPermissions { + private final Map subjects = new HashMap<>(); + + /** + * Returns the mutable map of subjects to their stored permission values. + * + *

The returned map is backed by this instance; changes are reflected immediately.

+ */ + public Map subjects() { + return subjects; + } + + /** + * Returns (and creates if necessary) the permissions container for a subject. + * + * @param subjectKey the subject key (typically a UUID string, a group subject, or a universal subject) + * @return the subject permissions container + */ + public SubjectPermissions subject(SubjectRef subject) { + return subjects.computeIfAbsent(subject, ignored -> new SubjectPermissions()); + } + + /** + * Permission values for a single subject within a region. + * + *

Permissions are stored by numeric id (see {@link PermissionKeyRegistry}).

+ */ + public static final class SubjectPermissions { + private final Int2ObjectOpenHashMap values = new Int2ObjectOpenHashMap<>(); + + /** + * Returns the stored name for a permission id. + * + * @param permissionId numeric permission id + * @return the stored name, or {@code null} if no name is present + */ + public PermissionValue get(int permissionId) { + return values.get(permissionId); + } + + /** + * Returns the stored name for a permission id, creating an empty name if missing. + * + * @param permissionId numeric permission id + * @param kind expected name kind to create when missing + * @return the existing or newly created name + */ + public PermissionValue getOrCreate(int permissionId, FlagValueKind kind) { + PermissionValue existing = values.get(permissionId); + if (existing != null) return existing; + PermissionValue created = switch (kind) { + case TARGET_DECISION -> new TargetDecisionValue(); + case STRING_SET -> new StringSetValue(); + }; + values.put(permissionId, created); + return created; + } + + /** + * Returns the mutable backing map of all values for this subject. + * + *

This is primarily exposed for persistence/inspection.

+ * + * @return backing map (mutable) + */ + public Int2ObjectOpenHashMap values() { + return values; + } + } + + /** + * Stored permission name for a flag. + * + *

Regions store different name shapes depending on the flag's {@link FlagValueKind}.

+ */ + public sealed interface PermissionValue permits TargetDecisionValue, StringSetValue { + /** + * Returns the {@link FlagValueKind} of this stored name. + * + *

Callers must ensure that the kind matches the owning flag.

+ * + * @return the name kind + */ + FlagValueKind kind(); + } + + /** + * Target-based allow/deny decision name. + * + *

Stores explicit allow and deny entries for concrete "target keys" (e.g. block ids). Each entry can be + * either an exact key or a glob pattern containing {@code *}. When evaluating a key:

+ * + *
    + *
  • Exact deny wins over exact allow
  • + *
  • Otherwise the most specific matching glob wins
  • + *
  • If allow and deny globs have equal specificity, deny wins
  • + *
+ */ + public static final class TargetDecisionValue implements PermissionValue { + /** + * Bonus added to exact-match specificity so exact keys always outrank wildcard patterns. + */ + private static final int EXACT_BONUS = 1_000_000; + + private final ObjectOpenHashSet allowTargets = new ObjectOpenHashSet<>(); + private final ObjectOpenHashSet denyTargets = new ObjectOpenHashSet<>(); + + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private volatile GlobPattern[] allowPatterns = new GlobPattern[0]; + private volatile GlobPattern[] denyPatterns = new GlobPattern[0]; + private volatile boolean patternsDirty = true; + + /** + * {@inheritDoc} + */ + @Override + public FlagValueKind kind() { + return FlagValueKind.TARGET_DECISION; + } + + /** + * Adds an allow entry for a target key or glob pattern. + * + *

If the entry is currently denied, it is removed from the deny set.

+ * + * @param targetKey exact key or glob pattern (supports {@code *}) + */ + public void allow(String targetKey) { + targetKey = normalize(targetKey); + if (targetKey == null) return; + denyTargets.remove(targetKey); + allowTargets.add(targetKey); + patternsDirty = true; + cache.clear(); + } + + /** + * Adds a deny entry for a target key or glob pattern. + * + *

If the entry is currently allowed, it is removed from the allow set.

+ * + * @param targetKey exact key or glob pattern (supports {@code *}) + */ + public void deny(String targetKey) { + targetKey = normalize(targetKey); + if (targetKey == null) return; + allowTargets.remove(targetKey); + denyTargets.add(targetKey); + patternsDirty = true; + cache.clear(); + } + + /** + * Returns all allow entries (exact keys and patterns). + * + * @return an unmodifiable set of normalized entries + */ + public Set allowTargets() { + return Collections.unmodifiableSet(allowTargets); + } + + /** + * Returns all deny entries (exact keys and patterns). + * + * @return an unmodifiable set of normalized entries + */ + public Set denyTargets() { + return Collections.unmodifiableSet(denyTargets); + } + + /** + * Removes an allow entry. + * + * @param targetKey exact key or glob pattern (supports {@code *}) + * @return {@code true} if the entry existed and was removed + */ + public boolean removeAllow(String targetKey) { + targetKey = normalize(targetKey); + if (targetKey == null) return false; + boolean changed = allowTargets.remove(targetKey); + if (changed) { + patternsDirty = true; + cache.clear(); + } + return changed; + } + + /** + * Removes a deny entry. + * + * @param targetKey exact key or glob pattern (supports {@code *}) + * @return {@code true} if the entry existed and was removed + */ + public boolean removeDeny(String targetKey) { + targetKey = normalize(targetKey); + if (targetKey == null) return false; + boolean changed = denyTargets.remove(targetKey); + if (changed) { + patternsDirty = true; + cache.clear(); + } + return changed; + } + + /** + * Removes all allow/deny entries for this name. + */ + public void clear() { + allowTargets.clear(); + denyTargets.clear(); + patternsDirty = true; + cache.clear(); + } + + /** + * Evaluates this name for a concrete query key. + * + *

Supports wildcard patterns using '*' (glob, not regex).

+ */ + public Decision evaluateMatch(String targetKey) { + targetKey = normalize(targetKey); + if (targetKey == null) return Decision.undefined(); + + Decision cached = cache.get(targetKey); + if (cached != null) return cached; + + Decision decided = evaluateMatchNoCache(targetKey); + cache.put(targetKey, decided); + return decided; + } + + /** + * Convenience wrapper for {@link #evaluateMatch(String)} returning only the {@link Result}. + * + * @param targetKey concrete target key + * @return the evaluated result + */ + public Result evaluate(String targetKey) { + return evaluateMatch(targetKey).result(); + } + + Decision evaluateMatchNoCache(String targetKey) { + // Exact checks always win. + if (denyTargets.contains(targetKey)) { + return new Decision(Result.FALSE, EXACT_BONUS + targetKey.length()); + } + if (allowTargets.contains(targetKey)) { + return new Decision(Result.TRUE, EXACT_BONUS + targetKey.length()); + } + + rebuildPatternsIfNeeded(); + + Decision bestAllow = Decision.undefined(); + for (GlobPattern p : allowPatterns) { + if (!p.matches(targetKey)) continue; + int spec = p.specificity(); + if (spec > bestAllow.specificity()) bestAllow = new Decision(Result.TRUE, spec); + } + + Decision bestDeny = Decision.undefined(); + for (GlobPattern p : denyPatterns) { + if (!p.matches(targetKey)) continue; + int spec = p.specificity(); + if (spec > bestDeny.specificity()) bestDeny = new Decision(Result.FALSE, spec); + } + + if (bestAllow.result() != Result.UNDEFINED && bestDeny.result() != Result.UNDEFINED) { + if (bestDeny.specificity() > bestAllow.specificity()) return bestDeny; + if (bestAllow.specificity() > bestDeny.specificity()) return bestAllow; + // Same specificity: deny wins. + return bestDeny; + } + if (bestDeny.result() != Result.UNDEFINED) return bestDeny; + if (bestAllow.result() != Result.UNDEFINED) return bestAllow; + + return Decision.undefined(); + } + + void rebuildPatternsIfNeeded() { + if (!patternsDirty) return; + synchronized (this) { + if (!patternsDirty) return; + // Only entries containing '*' are compiled into glob patterns; exact keys are handled via set lookup. + allowPatterns = compilePatterns(allowTargets); + denyPatterns = compilePatterns(denyTargets); + patternsDirty = false; + } + } + + private static GlobPattern[] compilePatterns(Set entries) { + if (entries == null || entries.isEmpty()) return new GlobPattern[0]; + ArrayList out = new ArrayList<>(); + for (String e : entries) { + if (e == null || !e.contains("*")) continue; + out.add(GlobPattern.compile(e)); + } + return out.toArray(new GlobPattern[0]); + } + + private static String normalize(String targetKey) { + if (targetKey == null || targetKey.isBlank()) return null; + return targetKey.trim().toLowerCase(); + } + } + + /** + * String set allow/deny name. + * + *

This is used for flags where the name is a finite set of arbitrary strings.

+ */ + public static final class StringSetValue implements PermissionValue { + private final ObjectOpenHashSet allow = new ObjectOpenHashSet<>(); + private final ObjectOpenHashSet deny = new ObjectOpenHashSet<>(); + + /** + * {@inheritDoc} + */ + @Override + public FlagValueKind kind() { + return FlagValueKind.STRING_SET; + } + + /** + * Adds an allowed name to the set. + * + *

If the name is currently denied, it is removed from the deny set.

+ * + * @param value name to allow (normalized to lower-case) + */ + public void allow(String value) { + if (value == null || value.isBlank()) return; + value = value.trim().toLowerCase(); + deny.remove(value); + allow.add(value); + } + + /** + * Adds a denied name to the set. + * + *

If the name is currently allowed, it is removed from the allow set.

+ * + * @param value name to deny (normalized to lower-case) + */ + public void deny(String value) { + if (value == null || value.isBlank()) return; + value = value.trim().toLowerCase(); + allow.remove(value); + deny.add(value); + } + + /** + * Returns the allowed values. + * + *

The returned set is the backing set; callers should treat it as read-only and only insert normalized values.

+ * + * @return backing allow set + */ + public Set allowValues() { + return allow; + } + + /** + * Returns the denied values. + * + *

The returned set is the backing set; callers should treat it as read-only and only insert normalized values.

+ * + * @return backing deny set + */ + public Set denyValues() { + return deny; + } + + /** + * Evaluates the decision for a concrete query name. + * + * @param query query name + * @return {@link Result#TRUE} if allowed, {@link Result#FALSE} if denied, otherwise {@link Result#UNDEFINED} + */ + public Result evaluate(String query) { + if (query == null || query.isBlank()) return Result.UNDEFINED; + query = query.trim().toLowerCase(); + if (deny.contains(query)) return Result.FALSE; + if (allow.contains(query)) return Result.TRUE; + return Result.UNDEFINED; + } + } +} diff --git a/api/src/main/java/de/t14d3/zones/permissions/RegionPermissionsCodec.java b/api/src/main/java/de/t14d3/zones/permissions/RegionPermissionsCodec.java new file mode 100644 index 0000000..ff09a79 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/RegionPermissionsCodec.java @@ -0,0 +1,157 @@ +package de.t14d3.zones.permissions; + +import de.t14d3.zones.permissions.flags.FlagValueKind; +import de.t14d3.zones.permissions.subjects.*; + +import java.util.*; + +/** + * POJO codec for {@link RegionPermissions}. + * + *

The in-memory representation stores permission ids for fast lookups, while the persisted representation stores + * permission names (via {@link PermissionKeyRegistry}) so the data stays stable across restarts.

+ */ +public final class RegionPermissionsCodec { + private RegionPermissionsCodec() { + } + + /** + * Encodes a {@link RegionPermissions} instance into a serializable structure. + * + *

Unknown permission ids (missing from {@link PermissionKeyRegistry}) are skipped.

+ * + * @param perms permissions to encode (may be {@code null}) + * @return encoded representation (never {@code null}) + */ + public static Persisted encode(RegionPermissions perms) { + Persisted persisted = new Persisted(); + if (perms == null) return persisted; + + for (Map.Entry subjectEntry : perms.subjects().entrySet()) { + SubjectRef subjectRef = subjectEntry.getKey(); + RegionPermissions.SubjectPermissions subject = subjectEntry.getValue(); + if (subject == null) continue; + + Map encodedPerms = new HashMap<>(); + for (var permEntry : subject.values().int2ObjectEntrySet()) { + int permId = permEntry.getIntKey(); + // Persist permission names (stable) rather than numeric ids (runtime-only). + String permName = PermissionKeyRegistry.instance().getName(permId); + if (permName == null) continue; + + RegionPermissions.PermissionValue pv = permEntry.getValue(); + if (pv instanceof RegionPermissions.TargetDecisionValue targeted) { + Entry e = new Entry(); + e.kind = "targets"; + e.allowTargets = new ArrayList<>(targeted.allowTargets()); + e.denyTargets = new ArrayList<>(targeted.denyTargets()); + encodedPerms.put(permName, e); + } else if (pv instanceof RegionPermissions.StringSetValue set) { + Entry e = new Entry(); + e.kind = "set"; + e.allowValues = new ArrayList<>(set.allowValues()); + e.denyValues = new ArrayList<>(set.denyValues()); + encodedPerms.put(permName, e); + } + } + String subjectKey = encodeSubjectKey(subjectRef); + if (subjectKey != null) persisted.subjects.put(subjectKey, encodedPerms); + } + + return persisted; + } + + /** + * Decodes a persisted representation back into the in-memory format. + * + *

Missing or invalid entries are ignored.

+ * + * @param persisted persisted representation (may be {@code null}) + * @return decoded permissions (never {@code null}) + */ + public static RegionPermissions decode(Persisted persisted) { + RegionPermissions perms = new RegionPermissions(); + if (persisted == null || persisted.subjects == null) return perms; + + for (Map.Entry> subjectEntry : persisted.subjects.entrySet()) { + String subjectKey = subjectEntry.getKey(); + if (subjectKey == null || subjectKey.isBlank()) continue; + + SubjectRef subjectRef = decodeSubjectKey(subjectKey); + if (subjectRef == null) continue; + RegionPermissions.SubjectPermissions subject = perms.subject(subjectRef); + Map encodedPerms = subjectEntry.getValue(); + if (encodedPerms == null) continue; + + for (Map.Entry permEntry : encodedPerms.entrySet()) { + String permName = permEntry.getKey(); + Entry encoded = permEntry.getValue(); + if (permName == null || permName.isBlank() || encoded == null) continue; + + // Ensure the permission name has a numeric id for in-memory storage. + int permId = PermissionKeyRegistry.instance().getOrCreateId(permName); + if ("set".equalsIgnoreCase(encoded.kind)) { + RegionPermissions.StringSetValue set = (RegionPermissions.StringSetValue) subject.getOrCreate( + permId, FlagValueKind.STRING_SET); + if (encoded.allowValues != null) encoded.allowValues.forEach(set::allow); + if (encoded.denyValues != null) encoded.denyValues.forEach(set::deny); + } else { + RegionPermissions.TargetDecisionValue targeted = (RegionPermissions.TargetDecisionValue) subject.getOrCreate( + permId, FlagValueKind.TARGET_DECISION); + if (encoded.allowTargets != null) encoded.allowTargets.forEach(targeted::allow); + if (encoded.denyTargets != null) encoded.denyTargets.forEach(targeted::deny); + } + } + } + return perms; + } + + private static String encodeSubjectKey(SubjectRef subject) { + if (subject == null) return null; + if (subject instanceof UniversalSubject) return "universal"; + if (subject instanceof PlayerSubject p) return p.uuid().toString(); + if (subject instanceof GroupSubject g) return "group:" + g.name(); + return null; + } + + private static SubjectRef decodeSubjectKey(String key) { + if (key == null || key.isBlank()) return null; + String k = key.trim(); + if ("universal".equalsIgnoreCase(k) + || "+universal".equalsIgnoreCase(k) + || "@all".equalsIgnoreCase(k) + || "@*".equalsIgnoreCase(k) + || "+universal".equalsIgnoreCase(k)) { + return Subjects.universal(); + } + if (k.regionMatches(true, 0, "group:", 0, "group:".length())) { + String name = Subjects.normalizeGroupName(k.substring("group:".length())); + return name == null ? null : Subjects.group(name); + } + try { + return Subjects.player(UUID.fromString(k)); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + /** + * Root persisted representation. Designed to be directly consumable by YAML/JSON serializers. + */ + public static final class Persisted { + public Map> subjects = new HashMap<>(); + } + + /** + * Persisted representation of one permission name. + * + *

Depending on {@link #kind}, either the {@code *Targets} lists or the {@code *Values} lists are used.

+ */ + public static final class Entry { + public String kind; + public List allowTargets; + public List denyTargets; + public List allowValues; + public List denyValues; + } +} diff --git a/api/src/main/java/de/t14d3/zones/permissions/RegionSecurityCodec.java b/api/src/main/java/de/t14d3/zones/permissions/RegionSecurityCodec.java new file mode 100644 index 0000000..a8d6ab9 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/RegionSecurityCodec.java @@ -0,0 +1,235 @@ +package de.t14d3.zones.permissions; + +import de.t14d3.zones.permissions.flags.FlagValueKind; +import de.t14d3.zones.permissions.subjects.*; + +import java.util.*; + +/** + * JSON-friendly codec for region security state (ACL + membership). + * + *

This is primarily used by SQL storage.

+ */ +public final class RegionSecurityCodec { + private RegionSecurityCodec() { + } + + public static Persisted encode(RegionPermissions acl, RegionMembership membership) { + Persisted out = new Persisted(); + + if (acl != null) { + for (Map.Entry subjectEntry : acl.subjects().entrySet()) { + SubjectRef subject = subjectEntry.getKey(); + RegionPermissions.SubjectPermissions perms = subjectEntry.getValue(); + if (subject == null || perms == null) continue; + + Map encodedPerms = encodeSubjectPermissions(perms); + if (encodedPerms.isEmpty()) continue; + + if (subject instanceof UniversalSubject) { + out.acl.universal = encodedPerms; + } else if (subject instanceof GroupSubject g) { + out.acl.groups.put(g.name(), encodedPerms); + } else if (subject instanceof PlayerSubject p) { + out.acl.players.put(p.uuid().toString(), encodedPerms); + } + } + } + + if (membership != null) { + for (Map.Entry player : membership.players().entrySet()) { + UUID uuid = player.getKey(); + RegionMembership.PlayerEntry entry = player.getValue(); + if (uuid == null || entry == null) continue; + + Persisted.PlayerMembership pm = new Persisted.PlayerMembership(); + pm.allowRoles = new ArrayList<>(entry.roles().allowValues()); + pm.denyRoles = new ArrayList<>(entry.roles().denyValues()); + pm.allowGroups = new ArrayList<>(entry.groups().allowValues()); + pm.denyGroups = new ArrayList<>(entry.groups().denyValues()); + + if (!pm.isEmpty()) out.membership.players.put(uuid.toString(), pm); + } + + for (Map.Entry group : membership.groups().entrySet()) { + String name = group.getKey(); + RegionMembership.GroupEntry entry = group.getValue(); + if (name == null || entry == null) continue; + + Persisted.GroupMembership gm = new Persisted.GroupMembership(); + gm.allowIncludes = new ArrayList<>(entry.includes().allowValues()); + gm.denyIncludes = new ArrayList<>(entry.includes().denyValues()); + if (!gm.isEmpty()) out.membership.groups.put(name, gm); + } + } + + return out; + } + + public static Decoded decode(Persisted persisted) { + RegionPermissions acl = new RegionPermissions(); + RegionMembership membership = new RegionMembership(); + + if (persisted != null && persisted.acl != null) { + Persisted.Acl a = persisted.acl; + if (a.universal != null && !a.universal.isEmpty()) { + decodeSubjectPermissions(acl.subject(Subjects.universal()), a.universal); + } + if (a.players != null) { + for (Map.Entry> player : a.players.entrySet()) { + UUID uuid; + try { + uuid = UUID.fromString(player.getKey()); + } catch (IllegalArgumentException ignored) { + continue; + } + Map perms = player.getValue(); + if (perms == null || perms.isEmpty()) continue; + decodeSubjectPermissions(acl.subject(Subjects.player(uuid)), perms); + } + } + if (a.groups != null) { + for (Map.Entry> group : a.groups.entrySet()) { + String name = Subjects.normalizeGroupName(group.getKey()); + if (name == null) continue; + Map perms = group.getValue(); + if (perms == null || perms.isEmpty()) continue; + decodeSubjectPermissions(acl.subject(Subjects.group(name)), perms); + } + } + } + + if (persisted != null && persisted.membership != null) { + Persisted.Membership m = persisted.membership; + if (m.players != null) { + for (Map.Entry player : m.players.entrySet()) { + UUID uuid; + try { + uuid = UUID.fromString(player.getKey()); + } catch (IllegalArgumentException ignored) { + continue; + } + Persisted.PlayerMembership pm = player.getValue(); + if (pm == null) continue; + RegionMembership.PlayerEntry entry = membership.player(uuid); + if (pm.allowRoles != null) pm.allowRoles.forEach(entry.roles()::allow); + if (pm.denyRoles != null) pm.denyRoles.forEach(entry.roles()::deny); + if (pm.allowGroups != null) pm.allowGroups.forEach(entry.groups()::allow); + if (pm.denyGroups != null) pm.denyGroups.forEach(entry.groups()::deny); + } + } + if (m.groups != null) { + for (Map.Entry group : m.groups.entrySet()) { + String name = Subjects.normalizeGroupName(group.getKey()); + if (name == null) continue; + Persisted.GroupMembership gm = group.getValue(); + if (gm == null) continue; + RegionMembership.GroupEntry entry = membership.group(name); + if (gm.allowIncludes != null) gm.allowIncludes.forEach(entry.includes()::allow); + if (gm.denyIncludes != null) gm.denyIncludes.forEach(entry.includes()::deny); + } + } + } + + return new Decoded(acl, membership); + } + + private static Map encodeSubjectPermissions(RegionPermissions.SubjectPermissions perms) { + Map encodedPerms = new HashMap<>(); + for (var permEntry : perms.values().int2ObjectEntrySet()) { + int permId = permEntry.getIntKey(); + String permName = PermissionKeyRegistry.instance().getName(permId); + if (permName == null || permName.isBlank()) continue; + + RegionPermissions.PermissionValue pv = permEntry.getValue(); + if (pv instanceof RegionPermissions.TargetDecisionValue targeted) { + Entry e = new Entry(); + e.kind = "targets"; + e.allowTargets = new ArrayList<>(targeted.allowTargets()); + e.denyTargets = new ArrayList<>(targeted.denyTargets()); + encodedPerms.put(permName, e); + } else if (pv instanceof RegionPermissions.StringSetValue set) { + Entry e = new Entry(); + e.kind = "set"; + e.allowValues = new ArrayList<>(set.allowValues()); + e.denyValues = new ArrayList<>(set.denyValues()); + encodedPerms.put(permName, e); + } + } + return encodedPerms; + } + + private static void decodeSubjectPermissions(RegionPermissions.SubjectPermissions into, Map encodedPerms) { + if (encodedPerms == null) return; + for (Map.Entry permEntry : encodedPerms.entrySet()) { + String permName = permEntry.getKey(); + Entry encoded = permEntry.getValue(); + if (permName == null || permName.isBlank() || encoded == null) continue; + + int permId = PermissionKeyRegistry.instance().getOrCreateId(permName); + if ("set".equalsIgnoreCase(encoded.kind)) { + RegionPermissions.StringSetValue set = (RegionPermissions.StringSetValue) into + .getOrCreate(permId, FlagValueKind.STRING_SET); + if (encoded.allowValues != null) encoded.allowValues.forEach(set::allow); + if (encoded.denyValues != null) encoded.denyValues.forEach(set::deny); + } else { + RegionPermissions.TargetDecisionValue targeted = (RegionPermissions.TargetDecisionValue) into + .getOrCreate(permId, FlagValueKind.TARGET_DECISION); + if (encoded.allowTargets != null) encoded.allowTargets.forEach(targeted::allow); + if (encoded.denyTargets != null) encoded.denyTargets.forEach(targeted::deny); + } + } + } + + public record Decoded(RegionPermissions acl, RegionMembership membership) { + } + + public static final class Persisted { + public Acl acl = new Acl(); + public Membership membership = new Membership(); + + public static final class Acl { + public Map universal = new HashMap<>(); + public Map> players = new HashMap<>(); + public Map> groups = new HashMap<>(); + } + + public static final class Membership { + public Map players = new HashMap<>(); + public Map groups = new HashMap<>(); + } + + public static final class PlayerMembership { + public List allowRoles; + public List denyRoles; + public List allowGroups; + public List denyGroups; + + boolean isEmpty() { + return (allowRoles == null || allowRoles.isEmpty()) + && (denyRoles == null || denyRoles.isEmpty()) + && (allowGroups == null || allowGroups.isEmpty()) + && (denyGroups == null || denyGroups.isEmpty()); + } + } + + public static final class GroupMembership { + public List allowIncludes; + public List denyIncludes; + + boolean isEmpty() { + return (allowIncludes == null || allowIncludes.isEmpty()) + && (denyIncludes == null || denyIncludes.isEmpty()); + } + } + } + + public static final class Entry { + public String kind; + public List allowTargets; + public List denyTargets; + public List allowValues; + public List denyValues; + } +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/flags/DefaultFlagHandler.java b/api/src/main/java/de/t14d3/zones/permissions/flags/DefaultFlagHandler.java index 8c43862..a5c70fe 100644 --- a/api/src/main/java/de/t14d3/zones/permissions/flags/DefaultFlagHandler.java +++ b/api/src/main/java/de/t14d3/zones/permissions/flags/DefaultFlagHandler.java @@ -13,7 +13,7 @@ public DefaultFlagHandler(boolean defaultValue, List validValues) { } @Override - public boolean getDefaultValue(Object... optional) { + public boolean getDefaultValue(FlagContext context) { return this.defaultValue; } diff --git a/api/src/main/java/de/t14d3/zones/permissions/flags/FlagContext.java b/api/src/main/java/de/t14d3/zones/permissions/flags/FlagContext.java new file mode 100644 index 0000000..70680f7 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/flags/FlagContext.java @@ -0,0 +1,40 @@ +package de.t14d3.zones.permissions.flags; + +import org.jetbrains.annotations.Nullable; + +/** + * Additional context for permission checks. + * + *

Zones' permission engine primarily matches a single {@code type} key against patterns stored in regions. Some + * flags still need extra information to decide their fallback/default behavior when no region rule applies. + * This replaces the old {@code Object... extra} "magic values" API with a small, typed carrier.

+ */ +public record FlagContext( + @Nullable Boolean defaultDecisionOverride, + @Nullable String reasonKey, + @Nullable String causeKey, + @Nullable String damageTypeKey +) { + private static final FlagContext NONE = new FlagContext(null, null, null, null); + + public static FlagContext none() { + return NONE; + } + + public static FlagContext defaultDecision(boolean decision) { + return new FlagContext(decision, null, null, null); + } + + public FlagContext withReason(@Nullable String reason) { + return new FlagContext(defaultDecisionOverride, reason, causeKey, damageTypeKey); + } + + public FlagContext withCause(@Nullable String cause) { + return new FlagContext(defaultDecisionOverride, reasonKey, cause, damageTypeKey); + } + + public FlagContext withDamageType(@Nullable String damageType) { + return new FlagContext(defaultDecisionOverride, reasonKey, causeKey, damageType); + } +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/flags/FlagRegistry.java b/api/src/main/java/de/t14d3/zones/permissions/flags/FlagRegistry.java new file mode 100644 index 0000000..58c9925 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/flags/FlagRegistry.java @@ -0,0 +1,58 @@ +package de.t14d3.zones.permissions.flags; + +import de.t14d3.zones.objects.Flag; +import de.t14d3.zones.permissions.PermissionKeyRegistry; + +import java.util.*; + +/** + * Registry for flags. Allows registering new flags and their platform-specific logic. + */ +public class FlagRegistry { + private static final List flags = new ArrayList<>(); + private static final Map flagsByName = new HashMap<>(); + + public static Flag registerFlag(String name, String description) { + return registerFlag(name, description, null, null, FlagValueKind.TARGET_DECISION); + } + + public static Flag registerFlag(String name, String description, IFlagHandler handler) { + return registerFlag(name, description, handler, null, FlagValueKind.TARGET_DECISION); + } + + public static Flag registerFlag(String name, String description, IFlagHandler handler, Flag.CanSetRule canSetRule) { + return registerFlag(name, description, handler, canSetRule, FlagValueKind.TARGET_DECISION); + } + + public static Flag registerFlag(String name, String description, IFlagHandler handler, Flag.CanSetRule canSetRule, FlagValueKind valueKind) { + String normalized = normalizeName(name); + int id = PermissionKeyRegistry.instance().getOrCreateId(normalized); + Flag flag = new Flag(id, normalized, description, handler, canSetRule, valueKind); + flags.add(flag); + flagsByName.put(normalized, flag); + return flag; + } + + public static Flag registerFlag(Flag flag) { + String normalized = normalizeName(flag.name()); + int id = PermissionKeyRegistry.instance().getOrCreateId(normalized); + Flag wrapped = new Flag(id, normalized, flag.description(), flag.handler(), flag.canSetRule(), + flag.valueKind()); + flags.add(wrapped); + flagsByName.put(normalized, wrapped); + return wrapped; + } + + public static List getFlags() { + return Collections.unmodifiableList(flags); + } + + public static Flag getFlag(String name) { + return flagsByName.get(normalizeName(name)); + } + + private static String normalizeName(String name) { + if (name == null) return ""; + return name.trim().toLowerCase(); + } +} diff --git a/api/src/main/java/de/t14d3/zones/permissions/flags/FlagValueKind.java b/api/src/main/java/de/t14d3/zones/permissions/flags/FlagValueKind.java new file mode 100644 index 0000000..66cef64 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/flags/FlagValueKind.java @@ -0,0 +1,7 @@ +package de.t14d3.zones.permissions.flags; + +public enum FlagValueKind { + TARGET_DECISION, + STRING_SET +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/flags/Flags.java b/api/src/main/java/de/t14d3/zones/permissions/flags/Flags.java index a263392..473d37a 100644 --- a/api/src/main/java/de/t14d3/zones/permissions/flags/Flags.java +++ b/api/src/main/java/de/t14d3/zones/permissions/flags/Flags.java @@ -1,12 +1,8 @@ package de.t14d3.zones.permissions.flags; -import de.t14d3.zones.Region; import de.t14d3.zones.objects.Flag; -import de.t14d3.zones.objects.Result; import de.t14d3.zones.utils.Types; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; @SuppressWarnings("ClassWithTooManyFields") @@ -22,6 +18,7 @@ public class Flags { public static Flag ENTITY; public static Flag IGNITE; public static Flag DAMAGE; + public static Flag ROLE; public static Flag GROUP; public static Flag SPAWN; public static Flag EXPLOSION; @@ -33,56 +30,53 @@ public class Flags { public static Flag PHYSICS; public static Flag HURT; - private static List flags; - public Flags() { - flags = new ArrayList<>(); - - FALLBACK = registerFlag(new Flag("fallback", "Fallback description")); - - BREAK = registerFlag( - new Flag("break", "Allows breaking blocks", new DefaultFlagHandler(false, Types.blocks()))); - PLACE = registerFlag( - new Flag("place", "Allows placing blocks", new DefaultFlagHandler(false, Types.blocks()))); - INTERACT = registerFlag( - new Flag("interact", "Allows interacting", new DefaultFlagHandler(false, Types.all()))); - CONTAINER = registerFlag( - new Flag("container", "Allows opening containers", - new DefaultFlagHandler(false, Types.containers()))); - REDSTONE = registerFlag(new Flag("redstone", "Allows interacting with redstone", - new DefaultFlagHandler(false, Types.redstone()))); - ENTITY = registerFlag(new Flag("entity", "Allows interacting with entities", - new DefaultFlagHandler(false, Types.entities()))); - IGNITE = registerFlag( - new Flag("ignite", "Allows igniting tnt", new IgnitionFlagHandler(false, Types.blocks()))); - DAMAGE = registerFlag( - new Flag("damage", "Allows damaging entities", new DefaultFlagHandler(false, Types.entities()))); - - GROUP = registerFlag(new Flag("group", "Add a group to the player")); - - SPAWN = registerFlag(new Flag("spawn", "Controls the spawning of entities", - new DefaultFlagHandler(true, Types.entities()))); - EXPLOSION = registerFlag(new Flag("explosion", "Controls the explosion of entities", - new DefaultFlagHandler(true, Types.entities()))); - CREATE = registerFlag( - new Flag("create", "Controls the creation of blocks through world events", - new DefaultFlagHandler(true, Types.blocks()))); - DESTROY = registerFlag( - new Flag("destroy", "Controls the removal of blocks through world events", - new DefaultFlagHandler(true, Types.blocks()))); - TRANSFORM = registerFlag( - new Flag("transform", "Controls the transformation of blocks into other blocks", - new DefaultFlagHandler(true, Types.blocks()))); - SPREAD = registerFlag( - new Flag("spread", "Controls the spread of blocks through world events", - new DefaultFlagHandler(true, Types.blocks()))); - RELOCATE = registerFlag(new Flag("relocate", "Controls the ability for blocks to change their location", - new DefaultFlagHandler(true, Types.blocks()))); - HURT = registerFlag(new Flag("hurt", "Controls the ability for entities to be hurt", - new DefaultFlagHandler(true, Types.entities()))); - - PHYSICS = registerFlag(new Flag("physics", "Controls the physics of blocks", - new DefaultFlagHandler(true, Types.blocks()))); + FALLBACK = FlagRegistry.registerFlag("fallback", "Fallback description"); + + ROLE = FlagRegistry.registerFlag("role", "Defines a member role", + new DefaultFlagHandler(false, List.of("owner", "admin")), + null, FlagValueKind.STRING_SET); + + BREAK = FlagRegistry.registerFlag("break", "Allows breaking blocks", + new DefaultFlagHandler(false, Types.blocks())); + PLACE = FlagRegistry.registerFlag("place", "Allows placing blocks", + new DefaultFlagHandler(false, Types.blocks())); + INTERACT = FlagRegistry.registerFlag("interact", "Allows interacting", + new DefaultFlagHandler(false, Types.all())); + CONTAINER = FlagRegistry.registerFlag("container", "Allows opening containers", + new DefaultFlagHandler(false, Types.containers())); + REDSTONE = FlagRegistry.registerFlag("redstone", "Allows interacting with redstone", + new DefaultFlagHandler(false, Types.redstone())); + ENTITY = FlagRegistry.registerFlag("entity", "Allows interacting with entities", + new DefaultFlagHandler(false, Types.entities())); + IGNITE = FlagRegistry.registerFlag("ignite", "Allows igniting tnt", + new IgnitionFlagHandler(false, Types.blocks())); + DAMAGE = FlagRegistry.registerFlag("damage", "Allows damaging entities", + new DefaultFlagHandler(false, Types.entities())); + + GROUP = FlagRegistry.registerFlag("group", "Add a group to the player", + new DefaultFlagHandler(false, List.of()), + null, FlagValueKind.STRING_SET); + + SPAWN = FlagRegistry.registerFlag("spawn", "Controls the spawning of entities", + new DefaultFlagHandler(true, Types.entities())); + EXPLOSION = FlagRegistry.registerFlag("explosion", "Controls the explosion of entities", + new DefaultFlagHandler(true, Types.entities())); + CREATE = FlagRegistry.registerFlag("create", "Controls the creation of blocks through world events", + new DefaultFlagHandler(true, Types.blocks())); + DESTROY = FlagRegistry.registerFlag("destroy", "Controls the removal of blocks through world events", + new DefaultFlagHandler(true, Types.blocks())); + TRANSFORM = FlagRegistry.registerFlag("transform", "Controls the transformation of blocks into other blocks", + new DefaultFlagHandler(true, Types.blocks())); + SPREAD = FlagRegistry.registerFlag("spread", "Controls the spread of blocks through world events", + new DefaultFlagHandler(true, Types.blocks())); + RELOCATE = FlagRegistry.registerFlag("relocate", "Controls the ability for blocks to change their location", + new DefaultFlagHandler(true, Types.blocks())); + HURT = FlagRegistry.registerFlag("hurt", "Controls the ability for entities to be hurt", + new DefaultFlagHandler(true, Types.entities())); + + PHYSICS = FlagRegistry.registerFlag("physics", "Controls the physics of blocks", + new DefaultFlagHandler(true, Types.blocks())); } private static final class IgnitionFlagHandler extends DefaultFlagHandler { @@ -92,17 +86,9 @@ public IgnitionFlagHandler(boolean defaultValue, List validValues) { } @Override - public Result evaluate(Region region, String who, String permission, String type, Object... optionals) { - return super.evaluate(region, who, permission, type, optionals); - } - - @Override - public boolean getDefaultValue(Object... optional) { - if (optional.length > 0 && optional[0] instanceof Boolean) { - return (boolean) optional[0]; - } else { - return super.getDefaultValue(); - } + public boolean getDefaultValue(FlagContext context) { + if (context != null && context.defaultDecisionOverride() != null) return context.defaultDecisionOverride(); + return super.getDefaultValue(FlagContext.none()); } } @@ -112,7 +98,6 @@ public boolean getDefaultValue(Object... optional) { * @param name 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 static boolean registerFlag(String name, String desc) { return registerFlag(name, desc, false); @@ -125,13 +110,12 @@ public static boolean registerFlag(String name, String desc) { * @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 static boolean registerFlag(String name, String desc, boolean overwrite) { - if (flags.stream().anyMatch(f -> f.name().equals(name)) && !overwrite) { + if (FlagRegistry.getFlags().stream().anyMatch(f -> f.name().equals(name)) && !overwrite) { return false; } else { - flags.add(new Flag(name, desc)); + FlagRegistry.registerFlag(name, desc); return true; } } @@ -141,15 +125,14 @@ public static Flag registerFlag(Flag flag) { } public static Flag registerFlag(Flag flag, boolean overwrite) { - if (flags.stream().anyMatch(f -> f.name().equals(flag.name()))) { + if (FlagRegistry.getFlags().stream().anyMatch(f -> f.name().equals(flag.name()))) { if (overwrite) { - flags.removeIf(f -> f.name().equals(flag.name())); - flags.add(flag); + FlagRegistry.registerFlag(flag); return flag; } return null; } else { - flags.add(flag); + FlagRegistry.registerFlag(flag); return flag; } } @@ -160,10 +143,10 @@ public static Flag registerFlag(Flag flag, boolean overwrite) { * @return List of {@link Flag} objects */ public static List getFlags() { - return Collections.unmodifiableList(flags); + return FlagRegistry.getFlags(); } public static Flag getFlag(String name) { - return flags.stream().filter(f -> f.name().equals(name)).findFirst().orElse(FALLBACK); + return FlagRegistry.getFlag(name); } } diff --git a/api/src/main/java/de/t14d3/zones/permissions/flags/IFlagHandler.java b/api/src/main/java/de/t14d3/zones/permissions/flags/IFlagHandler.java index 3df9336..a4795ff 100644 --- a/api/src/main/java/de/t14d3/zones/permissions/flags/IFlagHandler.java +++ b/api/src/main/java/de/t14d3/zones/permissions/flags/IFlagHandler.java @@ -1,133 +1,20 @@ package de.t14d3.zones.permissions.flags; -import de.t14d3.zones.Region; -import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.RegionFlagEntry; -import de.t14d3.zones.objects.Result; -import de.t14d3.zones.permissions.CacheEntry; -import de.t14d3.zones.permissions.CacheUtils; -import de.t14d3.zones.utils.DebugLoggerManager; - import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicReference; +/** + * Flag metadata hook: defaults and name suggestions. + * + *

All permission evaluation and caching lives in the Zones permission engine + * (not in individual flag handlers).

+ */ public interface IFlagHandler { - String UNIVERSAL = "universal"; - CacheUtils cacheUtils = CacheUtils.getInstance(); - FlagTypes flagType = FlagTypes.OTHER; default List getValidValues() { return List.of(); } - default boolean getDefaultValue(Object... optional) { + default boolean getDefaultValue(FlagContext context) { return true; } - - default Result evaluate(Region region, String permission, String type, Object... optionals) { - return evaluate(region, UNIVERSAL, permission, type, optionals); - } - - default Result evaluate(Region region, String who, String permission, String type, Object... optionals) { - permission = permission.toLowerCase(); - if (cacheUtils.permissionCache.containsKey(who)) { - ConcurrentLinkedQueue entries = cacheUtils.permissionCache.get(who); - for (CacheEntry entry : entries) { - if (entry.isEqual(permission, type, region.getKey())) { - DebugLoggerManager.Logger() - .log(DebugLoggerManager.CACHE_HIT_PERM, permission, who, region.getKey(), type); - return entry.result; - } - } - } - - - final AtomicReference result = new AtomicReference<>(Result.UNDEFINED); // Initialize result as null - - // Check if player is an admin, but only if not checking - // for "role" permission (StackOverflowException prevention) - if (!permission.equalsIgnoreCase("role")) { - if (isAdmin(who, region)) { - result.set(Result.TRUE); - } - } - - // Retrieve the permissions for the player in the specified region - Map> members = region.getMembers(); - - // Get the permissions for the player - List permissions = members.get(who); - if (permissions == null) { - return result.get(); // Return null if no permission was set - } - - String finalPermission = permission.toLowerCase(); - permissions.stream().filter(entry -> entry.getFlag().name().equalsIgnoreCase(finalPermission)).findFirst() - .ifPresentOrElse(entry -> { - int priority = 0; - for (RegionFlagEntry.FlagValue value : entry.getValues()) { - if (value.getValue().equalsIgnoreCase(type) && priority < 2) { - priority = 2; - result.set(value.isInverted() ? Result.FALSE : Result.TRUE); - } else if (value.getValue().equalsIgnoreCase("true") && priority < 1) { - priority = 1; - result.set(Result.TRUE); - } else if (value.getValue().equalsIgnoreCase("false") && priority < 1) { - priority = 1; - result.set(Result.FALSE); - } - } - if (priority == 0) { - result.set(Result.FALSE); - } - - }, () -> { - if (region.getParent() != null) { - result.set(evaluate(region.getParentRegion(), who, finalPermission, type)); - return; - } - if (permissions.stream().anyMatch(entry -> entry.getFlag().name().equalsIgnoreCase("group"))) { - if (who.startsWith("+group-") && !Zones.getInstance().getConfig() - .getBoolean("allow-group-recursion", false)) { - Zones.getInstance().getLogger() - .error("Recursive group permissions detected!! Groups are not allowed to contain other groups!"); - Zones.getInstance().getLogger() - .error("Group '{}' contains 'group' permission entry in region '{}'", who.substring( - 7), region.getKey()); - Zones.getInstance().getLogger() - .error("If you are 100% sure this is fine, add 'allow-group-recursion: true' to your config.yml"); - result.set(Result.FALSE); - return; - } - RegionFlagEntry groups = permissions.stream() - .filter(entry -> entry.getFlag().name().equalsIgnoreCase("group")).findFirst().get(); - for (String group : groups.getValues().stream().map(RegionFlagEntry.FlagValue::getValue) - .toList()) { - Result temp = evaluate(region, "+group-" + group, finalPermission, type); - if (temp.equals(Result.TRUE) || temp.equals(Result.FALSE)) { - result.set(temp); - } - } - } - - }); - cacheUtils.permissionCache.computeIfAbsent(who, k -> new ConcurrentLinkedQueue<>()) - .add(new CacheEntry(permission, type, region.getKey(), result.get())); - DebugLoggerManager.Logger().log(DebugLoggerManager.CACHE_MISS_PERM, permission, who, region.getKey(), type); - return result.get(); // Return null if no permission was set - } - - private boolean isAdmin(String who, Region region) { - return evaluate(region, who, "role", "owner").equals(Result.TRUE) || evaluate(region, who, "role", - "admin").equals(Result.TRUE); - } - - enum FlagTypes { - PLAYER, - UNIVERSAL, - OTHER - } - } diff --git a/api/src/main/java/de/t14d3/zones/permissions/flags/IFlagRegistrar.java b/api/src/main/java/de/t14d3/zones/permissions/flags/IFlagRegistrar.java new file mode 100644 index 0000000..55681e9 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/flags/IFlagRegistrar.java @@ -0,0 +1,16 @@ +package de.t14d3.zones.permissions.flags; + +import de.t14d3.zones.ZonesPlatform; + +/** + * Interface for registering platform-specific logic for flags, such as event handlers. + */ +public interface IFlagRegistrar { + + /** + * Registers the flag's platform-specific logic. + * + * @param platform The platform to register with. + */ + void register(ZonesPlatform platform); +} diff --git a/api/src/main/java/de/t14d3/zones/permissions/subjects/GroupSubject.java b/api/src/main/java/de/t14d3/zones/permissions/subjects/GroupSubject.java new file mode 100644 index 0000000..9fed7dd --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/subjects/GroupSubject.java @@ -0,0 +1,19 @@ +package de.t14d3.zones.permissions.subjects; + +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; + +public record GroupSubject(@NotNull String name) implements SubjectRef { + public GroupSubject { + if (name == null) throw new IllegalArgumentException("name"); + name = name.trim().toLowerCase(Locale.ROOT); + if (name.isEmpty()) throw new IllegalArgumentException("name"); + } + + @Override + public String kindKey() { + return "group"; + } +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/subjects/PlayerSubject.java b/api/src/main/java/de/t14d3/zones/permissions/subjects/PlayerSubject.java new file mode 100644 index 0000000..fbf2d2b --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/subjects/PlayerSubject.java @@ -0,0 +1,17 @@ +package de.t14d3.zones.permissions.subjects; + +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public record PlayerSubject(@NotNull UUID uuid) implements SubjectRef { + public PlayerSubject { + if (uuid == null) throw new IllegalArgumentException("uuid"); + } + + @Override + public String kindKey() { + return "player"; + } +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/subjects/SubjectRef.java b/api/src/main/java/de/t14d3/zones/permissions/subjects/SubjectRef.java new file mode 100644 index 0000000..3a19d69 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/subjects/SubjectRef.java @@ -0,0 +1,14 @@ +package de.t14d3.zones.permissions.subjects; + +/** + * Typed subject reference used by region ACLs. + * + *

Subjects represent "who" a rule applies to (players, groups, universal/non-player).

+ */ +public sealed interface SubjectRef permits PlayerSubject, GroupSubject, UniversalSubject { + /** + * Stable, human-readable kind key for persistence/debugging. + */ + String kindKey(); +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/subjects/Subjects.java b/api/src/main/java/de/t14d3/zones/permissions/subjects/Subjects.java new file mode 100644 index 0000000..bb7c5f6 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/subjects/Subjects.java @@ -0,0 +1,47 @@ +package de.t14d3.zones.permissions.subjects; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; +import java.util.UUID; + +public final class Subjects { + private Subjects() { + } + + public static @NotNull UniversalSubject universal() { + return UniversalSubject.INSTANCE; + } + + public static @NotNull PlayerSubject player(@NotNull UUID uuid) { + return new PlayerSubject(uuid); + } + + public static @NotNull GroupSubject group(@NotNull String name) { + return new GroupSubject(name); + } + + /** + * Formats a subject for logs/debug output. + */ + public static @NotNull String format(@Nullable SubjectRef subject) { + return switch (subject) { + case null -> "unknown"; + case UniversalSubject u -> "universal"; + case PlayerSubject p -> "player:" + p.uuid(); + case GroupSubject g -> "group:" + g.name(); + default -> subject.kindKey(); + }; + } + + /** + * Normalizes a group name. + */ + public static @Nullable String normalizeGroupName(@Nullable String name) { + if (name == null) return null; + String n = name.trim().toLowerCase(Locale.ROOT); + return n.isEmpty() ? null : n; + } +} + diff --git a/api/src/main/java/de/t14d3/zones/permissions/subjects/UniversalSubject.java b/api/src/main/java/de/t14d3/zones/permissions/subjects/UniversalSubject.java new file mode 100644 index 0000000..5d4e861 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/permissions/subjects/UniversalSubject.java @@ -0,0 +1,32 @@ +package de.t14d3.zones.permissions.subjects; + +/** + * Universal/non-player subject singleton. + */ +public final class UniversalSubject implements SubjectRef { + public static final UniversalSubject INSTANCE = new UniversalSubject(); + + private UniversalSubject() { + } + + @Override + public String kindKey() { + return "universal"; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof UniversalSubject; + } + + @Override + public int hashCode() { + return 31_337; + } + + @Override + public String toString() { + return "universal"; + } +} + diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesActionbar.java b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesActionbar.java new file mode 100644 index 0000000..fee4dfa --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesActionbar.java @@ -0,0 +1,112 @@ +package de.t14d3.zones.rapunzellib; + +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.Region; +import de.t14d3.zones.Zones; +import de.t14d3.zones.objects.Flag; +import net.kyori.adventure.text.Component; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; + +/** + * Utility for sending actionbar messages related to Zones. + */ +public final class ZonesActionbar { + private static final long COOLDOWN_NANOS = Duration.ofMillis(250).toNanos(); + + private ZonesActionbar() { + } + + /** + * Sends an actionbar message if the player isn't on cooldown. + * + * @param player target player + * @param component actionbar component + * @return {@code true} if the message was sent, {@code false} if throttled + */ + public static boolean sendThrottled(RPlayer player, Component component) { + long now = System.nanoTime(); + Long last = player.extras().get(ZonesExtraKeys.LAST_ACTIONBAR_NANOS).orElse(null); + if (last != null && now - last < COOLDOWN_NANOS) return false; + + player.extras().put(ZonesExtraKeys.LAST_ACTIONBAR_NANOS, now); + player.sendActionBar(component); + return true; + } + + /** + * Sends a standard "denied" actionbar message listing the regions and missing permissions. + * + *

Messages are throttled per-player (shared cooldown with {@link #sendThrottled(RPlayer, Component)}).

+ */ + public static void sendDenied( + Zones zones, + RPlayer player, + RBlockPos location, + RWorldRef world, + List actions, + String type + ) { + sendDenied(zones, player, location, world, actions.toArray(Flag[]::new), type); + } + + /** + * Same as {@link #sendDenied(Zones, RPlayer, RBlockPos, RWorldRef, List, String)} but avoids list allocations on hot paths. + */ + public static void sendDenied( + Zones zones, + RPlayer player, + RBlockPos location, + RWorldRef world, + Flag[] actions, + String type + ) { + long now = System.nanoTime(); + Long last = player.extras().get(ZonesExtraKeys.LAST_ACTIONBAR_NANOS).orElse(null); + if (last != null && now - last < COOLDOWN_NANOS) return; + + List regions = zones.getRegionManager().getRegionsAt(location, world); + + String regionNames; + if (regions.isEmpty()) { + regionNames = ""; + } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < regions.size(); i++) { + if (i != 0) sb.append(", "); + sb.append(regions.get(i).getName()); + } + regionNames = sb.toString(); + } + + String permissionsString; + if (actions == null || actions.length == 0) { + permissionsString = ""; + } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < actions.length; i++) { + if (i != 0) sb.append(", "); + sb.append(actions[i].name()); + } + permissionsString = sb.toString(); + } + + Component component = Rapunzel.context().messages().component( + "region.no-interact-permission", + Placeholders.builder() + .string("region", regionNames) + .string("actions", permissionsString) + .string("type", Objects.toString(type, "")) + .build() + ); + + player.extras().put(ZonesExtraKeys.LAST_ACTIONBAR_NANOS, now); + player.sendActionBar(component); + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesExtraKeys.java b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesExtraKeys.java new file mode 100644 index 0000000..baf2caa --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesExtraKeys.java @@ -0,0 +1,18 @@ +package de.t14d3.zones.rapunzellib; + +import de.t14d3.rapunzellib.objects.RExtraKey; +import de.t14d3.zones.objects.Box; + +public final class ZonesExtraKeys { + private ZonesExtraKeys() { + } + + public static final RExtraKey SELECTION = RExtraKey.of("zones", "selection", Box.class); + public static final RExtraKey SELECTION_CREATING = RExtraKey.of("zones", "selection_creating", + Boolean.class); + public static final RExtraKey SELECTION_MODE = RExtraKey.of("zones", "selection_mode", String.class); + public static final RExtraKey LAST_ACTIONBAR_NANOS = RExtraKey.of("zones", "last_actionbar_nanos", + Long.class); + public static final RExtraKey PERMISSION_CACHE = RExtraKey.of("zones", "permission_cache", + ZonesPermissionCache.class); +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesPermissionCache.java b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesPermissionCache.java new file mode 100644 index 0000000..8f7d541 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesPermissionCache.java @@ -0,0 +1,68 @@ +package de.t14d3.zones.rapunzellib; + +import de.t14d3.rapunzellib.objects.RPlayer; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Small per-player permission cache backed by {@link de.t14d3.rapunzellib.objects.RExtras}. + * + *

This is intentionally tiny and platform-agnostic: it caches only boolean results for a + * permission node for a short TTL to reduce repeated {@code hasPermission} calls in very hot paths + * (e.g. block-break spam).

+ */ +public final class ZonesPermissionCache { + private final ConcurrentHashMap entries = new ConcurrentHashMap<>(); + + /** + * Evaluates {@code player.hasPermission(permission)} with a per-player TTL cache. + * + *

The cache is stored in {@link RPlayer#extras()} using {@link ZonesExtraKeys#PERMISSION_CACHE}.

+ * + * @param player player to check + * @param permission permission node + * @param ttlNanos time-to-live in nanoseconds; {@code <= 0} disables caching + * @return {@code true} if permitted + */ + public static boolean hasPermissionCached(RPlayer player, String permission, long ttlNanos) { + if (player == null) return false; + if (permission == null || permission.isBlank()) return false; + if (ttlNanos <= 0) return player.hasPermission(permission); + + try { + ZonesPermissionCache cache = player.extras() + .get(ZonesExtraKeys.PERMISSION_CACHE) + .orElse(null); + + if (cache == null) { + cache = new ZonesPermissionCache(); + player.extras().put(ZonesExtraKeys.PERMISSION_CACHE, cache); + } + + Objects.requireNonNull(cache, "cache"); + + long now = System.nanoTime(); + Entry cached = cache.entries.get(permission); + if (cached != null && cached.expiresAtNanos > now) return cached.value; + + boolean computed = player.hasPermission(permission); + long expiresAt = now + ttlNanos; + if (expiresAt < now) expiresAt = Long.MAX_VALUE; + cache.entries.put(permission, new Entry(computed, expiresAt)); + return computed; + } catch (UnsupportedOperationException ignored) { + return player.hasPermission(permission); + } + } + + private static final class Entry { + private final boolean value; + private final long expiresAtNanos; + + private Entry(boolean value, long expiresAtNanos) { + this.value = value; + this.expiresAtNanos = expiresAtNanos; + } + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesRapunzelHooks.java b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesRapunzelHooks.java new file mode 100644 index 0000000..2d7353b --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesRapunzelHooks.java @@ -0,0 +1,56 @@ +package de.t14d3.zones.rapunzellib; + +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.rapunzellib.events.GameEvents; +import de.t14d3.zones.Zones; +import de.t14d3.zones.rapunzellib.listeners.ZonesEventListeners; + +import java.util.ArrayList; +import java.util.List; + +/** + * Installs Zones listeners onto RapunzelLib's game event bus. + * + *

This is an optional integration layer for servers using RapunzelLib.

+ */ +public final class ZonesRapunzelHooks implements AutoCloseable { + private final List subscriptions = new ArrayList<>(); + + private ZonesRapunzelHooks() { + } + + /** + * Installs Zones hooks once and registers the instance as a Rapunzel service. + * + * @param zones zones instance + * @return the installed hooks instance (existing one if already installed) + */ + public static ZonesRapunzelHooks install(Zones zones) { + var ctx = Rapunzel.context(); + var already = ctx.services().find(ZonesRapunzelHooks.class).orElse(null); + if (already != null) return already; + + ZonesRapunzelHooks hooks = new ZonesRapunzelHooks(); + + GameEventBus bus = GameEvents.bus(); + hooks.subscriptions.add(ZonesEventListeners.registerAll(zones, bus)); + + ctx.services().register(ZonesRapunzelHooks.class, hooks); + return hooks; + } + + /** + * Unregisters all installed subscriptions. + */ + @Override + public void close() { + for (var s : subscriptions) { + try { + s.close(); + } catch (Exception ignored) { + } + } + subscriptions.clear(); + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesBlockListener.java b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesBlockListener.java new file mode 100644 index 0000000..899b0cf --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesBlockListener.java @@ -0,0 +1,128 @@ +package de.t14d3.zones.rapunzellib.listeners; + +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.rapunzellib.events.block.BlockBreakPre; +import de.t14d3.rapunzellib.events.block.BlockPlacePre; +import de.t14d3.rapunzellib.events.interact.UseBlockPre; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.Zones; +import de.t14d3.zones.objects.Flag; +import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.rapunzellib.ZonesActionbar; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; +import de.t14d3.zones.rapunzellib.ZonesPermissionCache; +import de.t14d3.zones.utils.TypeKeys; +import de.t14d3.zones.utils.Types; + +import java.util.concurrent.TimeUnit; + +final class ZonesBlockListener { + private final Zones zones; + private final long bypassPermissionTtlNanos; + + private static final Flag[] BREAK = {Flags.BREAK}; + private static final Flag[] BREAK_CONTAINER = {Flags.BREAK, Flags.CONTAINER}; + private static final Flag[] BREAK_REDSTONE = {Flags.BREAK, Flags.REDSTONE}; + private static final Flag[] BREAK_CONTAINER_REDSTONE = {Flags.BREAK, Flags.CONTAINER, Flags.REDSTONE}; + + private static final Flag[] PLACE = {Flags.PLACE}; + private static final Flag[] PLACE_CONTAINER = {Flags.PLACE, Flags.CONTAINER}; + private static final Flag[] PLACE_REDSTONE = {Flags.PLACE, Flags.REDSTONE}; + private static final Flag[] PLACE_CONTAINER_REDSTONE = {Flags.PLACE, Flags.CONTAINER, Flags.REDSTONE}; + + private static final Flag[] INTERACT_CONTAINER = {Flags.INTERACT, Flags.CONTAINER}; + private static final Flag[] INTERACT_REDSTONE = {Flags.INTERACT, Flags.REDSTONE}; + private static final Flag[] INTERACT_TNT = {Flags.INTERACT, Flags.IGNITE}; + private static final Flag[] INTERACT_CONTAINER_REDSTONE = {Flags.INTERACT, Flags.CONTAINER, Flags.REDSTONE}; + private static final Flag[] INTERACT_CONTAINER_TNT = {Flags.INTERACT, Flags.CONTAINER, Flags.IGNITE}; + private static final Flag[] INTERACT_REDSTONE_TNT = {Flags.INTERACT, Flags.REDSTONE, Flags.IGNITE}; + private static final Flag[] INTERACT_CONTAINER_REDSTONE_TNT = {Flags.INTERACT, Flags.CONTAINER, Flags.REDSTONE, Flags.IGNITE}; + + ZonesBlockListener(Zones zones) { + this.zones = zones; + int ttlSeconds = zones.getConfig().getInt("cache.permission-ttl", 2); + this.bypassPermissionTtlNanos = TimeUnit.SECONDS.toNanos(Math.max(0, ttlSeconds)); + } + + GameEventBus.Subscription register(GameEventBus bus) { + var s1 = bus.onPre(BlockBreakPre.class, event -> { + RPlayer player = event.player(); + if (ZonesPermissionCache.hasPermissionCached(player, "zones.bypass.claimed", bypassPermissionTtlNanos)) + return; + if (player.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false)) { + event.deny(); + return; + } + + String type = TypeKeys.normalize(event.block().typeKey()); + RWorldRef world = event.block().world().ref(); + RBlockPos loc = event.block().pos(); + + boolean isContainer = Types.containers().contains(type); + boolean isRedstone = Types.redstone().contains(type); + Flag[] required = isContainer + ? (isRedstone ? BREAK_CONTAINER_REDSTONE : BREAK_CONTAINER) + : (isRedstone ? BREAK_REDSTONE : BREAK); + + if (!zones.getPermissionManager().checkActions(loc, world, player.uuid(), type, required)) { + event.deny(); + ZonesActionbar.sendDenied(zones, player, loc, world, required, type); + } + }); + var s2 = bus.onPre(BlockPlacePre.class, event -> { + RPlayer player = event.player(); + if (ZonesPermissionCache.hasPermissionCached(player, "zones.bypass.claimed", bypassPermissionTtlNanos)) + return; + + String type = TypeKeys.normalize(event.blockTypeKey()); + RWorldRef world = player.worldOrThrow().ref(); + RBlockPos loc = event.pos(); + + boolean isContainer = Types.containers().contains(type); + boolean isRedstone = Types.redstone().contains(type); + Flag[] required = isContainer + ? (isRedstone ? PLACE_CONTAINER_REDSTONE : PLACE_CONTAINER) + : (isRedstone ? PLACE_REDSTONE : PLACE); + + if (!zones.getPermissionManager().checkActions(loc, world, player.uuid(), type, required)) { + event.deny(); + ZonesActionbar.sendDenied(zones, player, loc, world, required, type); + } + }); + var s3 = bus.onPre(UseBlockPre.class, event -> { + RPlayer player = event.player(); + if (ZonesPermissionCache.hasPermissionCached(player, "zones.bypass.claimed", bypassPermissionTtlNanos)) + return; + + String type = TypeKeys.normalize(event.block().typeKey()); + + boolean isContainer = Types.containers().contains(type); + boolean isRedstone = Types.redstone().contains(type); + boolean isTnt = type.equals("tnt"); + if (!isContainer && !isRedstone && !isTnt) return; + + RWorldRef world = event.block().world().ref(); + RBlockPos loc = event.block().pos(); + + Flag[] required = isContainer + ? (isRedstone + ? (isTnt ? INTERACT_CONTAINER_REDSTONE_TNT : INTERACT_CONTAINER_REDSTONE) + : (isTnt ? INTERACT_CONTAINER_TNT : INTERACT_CONTAINER)) + : (isRedstone + ? (isTnt ? INTERACT_REDSTONE_TNT : INTERACT_REDSTONE) + : INTERACT_TNT); + + if (!zones.getPermissionManager().checkActions(loc, world, player.uuid(), type, required)) { + event.deny(); + ZonesActionbar.sendDenied(zones, player, loc, world, required, type); + } + }); + return () -> { + s1.close(); + s2.close(); + s3.close(); + }; + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesBucketListener.java b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesBucketListener.java new file mode 100644 index 0000000..66060b1 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesBucketListener.java @@ -0,0 +1,63 @@ +package de.t14d3.zones.rapunzellib.listeners; + +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.rapunzellib.events.item.BucketEmptyPre; +import de.t14d3.rapunzellib.events.item.BucketEntityPre; +import de.t14d3.rapunzellib.events.item.BucketFillPre; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.Zones; +import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.utils.TypeKeys; + +final class ZonesBucketListener { + private final Zones zones; + + ZonesBucketListener(Zones zones) { + this.zones = zones; + } + + GameEventBus.Subscription register(GameEventBus bus) { + var s1 = bus.onPre(BucketFillPre.class, event -> { + RPlayer player = event.player(); + if (player.hasPermission("zones.bypass.claimed")) return; + + String type = TypeKeys.normalize(event.blockTypeKey()); + if (!zones.getPermissionManager() + .checkAction(event.pos(), event.world(), player.uuid(), Flags.BREAK, type)) { + event.deny(); + } + }); + var s2 = bus.onPre(BucketEmptyPre.class, event -> { + RPlayer player = event.player(); + if (player.hasPermission("zones.bypass.claimed")) return; + + String raw = TypeKeys.normalize(event.bucketTypeKey()); + String type = switch (raw) { + case "water_bucket" -> "water"; + case "lava_bucket" -> "lava"; + case "powder_snow_bucket" -> "powder_snow"; + default -> raw; + }; + + if (!zones.getPermissionManager() + .checkAction(event.pos(), event.world(), player.uuid(), Flags.PLACE, type)) { + event.deny(); + } + }); + var s3 = bus.onPre(BucketEntityPre.class, event -> { + RPlayer player = event.player(); + if (player.hasPermission("zones.bypass.claimed")) return; + + String type = TypeKeys.normalize(event.entityTypeKey()); + if (!zones.getPermissionManager() + .checkAction(event.pos(), event.world(), player.uuid(), Flags.ENTITY, type)) { + event.deny(); + } + }); + return () -> { + s1.close(); + s2.close(); + s3.close(); + }; + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesEntityListener.java b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesEntityListener.java new file mode 100644 index 0000000..ec4f1f7 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesEntityListener.java @@ -0,0 +1,71 @@ +package de.t14d3.zones.rapunzellib.listeners; + +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.rapunzellib.events.entity.AttackEntityPre; +import de.t14d3.rapunzellib.events.entity.EntityHurtPre; +import de.t14d3.rapunzellib.events.entity.EntitySpawnPre; +import de.t14d3.rapunzellib.events.entity.InteractEntityPre; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.Zones; +import de.t14d3.zones.objects.Flag; +import de.t14d3.zones.permissions.flags.FlagContext; +import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.rapunzellib.ZonesActionbar; +import de.t14d3.zones.utils.TypeKeys; + +final class ZonesEntityListener { + private final Zones zones; + + private static final Flag[] INTERACT_ENTITY = {Flags.INTERACT, Flags.ENTITY}; + private static final Flag[] INTERACT_ENTITY_CONTAINER = {Flags.INTERACT, Flags.ENTITY, Flags.CONTAINER}; + private static final Flag[] DAMAGE_ENTITY = {Flags.DAMAGE, Flags.ENTITY}; + + ZonesEntityListener(Zones zones) { + this.zones = zones; + } + + GameEventBus.Subscription register(GameEventBus bus) { + var s1 = bus.onPre(InteractEntityPre.class, event -> { + RPlayer player = event.player(); + if (player.hasPermission("zones.bypass.claimed")) return; + + String type = TypeKeys.normalize(event.entityTypeKey()); + Flag[] required = type.equals("armor_stand") ? INTERACT_ENTITY_CONTAINER : INTERACT_ENTITY; + if (!zones.getPermissionManager().checkActions(event.pos(), event.world(), player.uuid(), type, required)) { + event.deny(); + ZonesActionbar.sendDenied(zones, player, event.pos(), event.world(), required, type); + } + }); + var s2 = bus.onPre(AttackEntityPre.class, event -> { + RPlayer player = event.player(); + if (player.hasPermission("zones.bypass.claimed")) return; + + String type = TypeKeys.normalize(event.entityTypeKey()); + if (!zones.getPermissionManager() + .checkActions(event.pos(), event.world(), player.uuid(), type, DAMAGE_ENTITY)) { + event.deny(); + ZonesActionbar.sendDenied(zones, player, event.pos(), event.world(), DAMAGE_ENTITY, type); + } + }); + var s3 = bus.onPre(EntitySpawnPre.class, event -> { + String type = TypeKeys.normalize(event.entityTypeKey()); + if (!zones.getPermissionManager().checkAction(event.pos(), event.world(), Flags.SPAWN, type, + FlagContext.none().withReason(event.reason()))) { + event.deny(); + } + }); + var s4 = bus.onPre(EntityHurtPre.class, event -> { + String type = TypeKeys.normalize(event.entityTypeKey()); + if (!zones.getPermissionManager().checkAction(event.pos(), event.world(), Flags.HURT, type, + FlagContext.none().withDamageType(event.damageTypeKey()))) { + event.deny(); + } + }); + return () -> { + s1.close(); + s2.close(); + s3.close(); + s4.close(); + }; + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesEventListeners.java b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesEventListeners.java new file mode 100644 index 0000000..0d40186 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesEventListeners.java @@ -0,0 +1,36 @@ +package de.t14d3.zones.rapunzellib.listeners; + +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.zones.Zones; + +/** + * Installs all Zones listeners onto a {@link GameEventBus}. + */ +public final class ZonesEventListeners { + private ZonesEventListeners() { + } + + /** + * Registers all Zones listeners and returns a subscription that un-registers them. + * + * @param zones zones instance + * @param bus event bus to register listeners on + */ + public static GameEventBus.Subscription registerAll(Zones zones, GameEventBus bus) { + var selection = new ZonesSelectionListener(zones).register(bus); + var blocks = new ZonesBlockListener(zones).register(bus); + var entities = new ZonesEntityListener(zones).register(bus); + var explosions = new ZonesExplosionListener(zones).register(bus); + var buckets = new ZonesBucketListener(zones).register(bus); + var world = new ZonesWorldListener(zones).register(bus); + + return () -> { + selection.close(); + blocks.close(); + entities.close(); + explosions.close(); + buckets.close(); + world.close(); + }; + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesExplosionListener.java b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesExplosionListener.java new file mode 100644 index 0000000..2946849 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesExplosionListener.java @@ -0,0 +1,47 @@ +package de.t14d3.zones.rapunzellib.listeners; + +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.rapunzellib.events.world.ExplosionPre; +import de.t14d3.rapunzellib.events.world.TntPrimePre; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.Zones; +import de.t14d3.zones.permissions.flags.FlagContext; +import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.utils.TypeKeys; + +final class ZonesExplosionListener { + private final Zones zones; + + ZonesExplosionListener(Zones zones) { + this.zones = zones; + } + + GameEventBus.Subscription register(GameEventBus bus) { + var s1 = bus.onPre(TntPrimePre.class, event -> { + String type = TypeKeys.normalize(event.blockTypeKey()); + if (event.player().isPresent()) { + RPlayer player = event.player().get(); + if (player.hasPermission("zones.bypass.claimed")) return; + if (!zones.getPermissionManager() + .checkAction(event.pos(), event.world(), player.uuid(), Flags.IGNITE, type)) { + event.deny(); + } + } else { + if (!zones.getPermissionManager().checkAction(event.pos(), event.world(), Flags.IGNITE, type, + FlagContext.defaultDecision(true).withCause(String.valueOf(event.cause())))) { + event.deny(); + } + } + }); + var s2 = bus.onPre(ExplosionPre.class, event -> { + String type = TypeKeys.normalize(event.sourceTypeKey()); + if (!zones.getPermissionManager().checkAction(event.origin(), event.world(), Flags.EXPLOSION, type)) { + event.deny(); + } + }); + return () -> { + s1.close(); + s2.close(); + }; + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesSelectionListener.java b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesSelectionListener.java new file mode 100644 index 0000000..8d6ec17 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesSelectionListener.java @@ -0,0 +1,77 @@ +package de.t14d3.zones.rapunzellib.listeners; + +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.rapunzellib.events.player.InteractBlockPre; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.Zones; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; +import de.t14d3.zones.utils.Utils; +import net.kyori.adventure.text.format.NamedTextColor; + +final class ZonesSelectionListener { + private final Zones zones; + + ZonesSelectionListener(Zones zones) { + this.zones = zones; + } + + GameEventBus.Subscription register(GameEventBus bus) { + return bus.onPre(InteractBlockPre.class, event -> { + RPlayer player = event.player(); + if (event.hand() == InteractBlockPre.Hand.OFF_HAND) return; + + boolean selecting = player.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false); + if (!selecting) return; + + Box selection = player.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + if (selection == null) { + selection = new Box(null, null, event.block().world().ref(), false); + } + + RWorldRef world = selection.getWorld() != null ? selection.getWorld() : event.block().world().ref(); + RBlockPos min = selection.getMin(); + RBlockPos max = selection.getMax(); + RBlockPos clicked = event.block().pos(); + + event.deny(); + + if (event.action() == InteractBlockPre.Action.LEFT_CLICK_BLOCK) { + if (min != null) zones.getPlatform().removeBeacon(player, world, min); + min = clicked; + zones.getPlatform().showBeacon(player, min, world, NamedTextColor.GREEN); + player.sendMessage(zones.getMessages().component( + "create.primary", + Placeholders.builder() + .string("x", String.valueOf(clicked.x())) + .string("y", String.valueOf(clicked.y())) + .string("z", String.valueOf(clicked.z())) + .build() + )); + } else { + if (max != null) zones.getPlatform().removeBeacon(player, world, max); + max = clicked; + zones.getPlatform().showBeacon(player, max, world, NamedTextColor.RED); + player.sendMessage(zones.getMessages().component( + "create.secondary", + Placeholders.builder() + .string("x", String.valueOf(clicked.x())) + .string("y", String.valueOf(clicked.y())) + .string("z", String.valueOf(clicked.z())) + .build() + )); + } + + Utils.SelectionMode mode = Utils.SelectionMode.getPlayerMode(player); + if (mode != Utils.SelectionMode.CUBOID_3D) { + if (min != null) min = new RBlockPos(min.x(), -63, min.z()); + if (max != null) max = new RBlockPos(max.x(), 319, max.z()); + } + + player.extras().put(ZonesExtraKeys.SELECTION, new Box(min, max, world, false)); + }); + } +} diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesWorldListener.java b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesWorldListener.java new file mode 100644 index 0000000..d0dd382 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/listeners/ZonesWorldListener.java @@ -0,0 +1,24 @@ +package de.t14d3.zones.rapunzellib.listeners; + +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.rapunzellib.events.player.PlayerQuitPost; +import de.t14d3.rapunzellib.events.world.WorldLoadPost; +import de.t14d3.zones.Zones; + +final class ZonesWorldListener { + private final Zones zones; + + ZonesWorldListener(Zones zones) { + this.zones = zones; + } + + GameEventBus.Subscription register(GameEventBus bus) { + var s1 = bus.onPost(WorldLoadPost.class, event -> zones.getRegionManager().loadRegions()); + var s2 = bus.onPost(PlayerQuitPost.class, + event -> zones.getPermissionManager().invalidateSubject(event.uuid())); + return () -> { + s1.close(); + s2.close(); + }; + } +} diff --git a/api/src/main/java/de/t14d3/zones/utils/ConfigManager.java b/api/src/main/java/de/t14d3/zones/utils/ConfigManager.java deleted file mode 100644 index 5355c38..0000000 --- a/api/src/main/java/de/t14d3/zones/utils/ConfigManager.java +++ /dev/null @@ -1,93 +0,0 @@ -package de.t14d3.zones.utils; - -import de.t14d3.zones.Zones; -import org.simpleyaml.configuration.file.YamlFile; - -import java.io.File; -import java.io.IOException; - -public class ConfigManager { - private final Zones zones; - private final YamlFile configFile; - private Utils.SavingModes savingMode; - - public ConfigManager(Zones zones, File file) { - this.zones = zones; - this.configFile = new YamlFile(file); - try { - configFile.loadWithComments(); - } catch (IOException e) { - zones.getLogger().error("Failed to load config: {}", e.getMessage()); - } - - loadConfig(); - } - - private void loadConfig() { - savingMode = Utils.SavingModes.fromString( - configFile.getString("zone-saving.mode", "MODIFIED") - ); - } - - public void saveConfig() { - try { - configFile.save(); - } catch (IOException e) { - zones.getLogger().error("Failed to save config: {}", e.getMessage()); - } - } - - /** - * Gets an integer from the config. - * @param path {@code .} separated path to the value. - * @return The value at the given path. - * @see #getInt(String, int) - */ - public int getInt(String path, int def) { - return configFile.getInt(path, def); - } - - /** - * Gets a String from the config. - * @param path {@code .} separated path to the value. - * @param def Default value to return if the value is not found. - * @return The value at the given path. - */ - public String getString(String path, String def) { - return configFile.getString(path, def); - } - - public String getString(String path) { - return getString(path, null); - } - - public void set(String path, Object value) { - configFile.set(path, value); - } - - /** - * Gets a boolean from the config. - * @param path {@code .} separated path to the value. - * @param def Default value to return if the value is not found. - * @return The value at the given path. - */ - public boolean getBoolean(String path, boolean def) { - return configFile.getBoolean(path, def); - } - - public float getFloat(String path, float def) { - return (float) configFile.getDouble(path, def); - } - - public double getDouble(String path, double def) { - return configFile.getDouble(path, def); - } - - public YamlFile getConfig() { - return configFile; - } - - public Utils.SavingModes getSavingMode() { - return savingMode; - } -} \ No newline at end of file diff --git a/api/src/main/java/de/t14d3/zones/utils/ConfigUpdater.java b/api/src/main/java/de/t14d3/zones/utils/ConfigUpdater.java deleted file mode 100644 index bfbca24..0000000 --- a/api/src/main/java/de/t14d3/zones/utils/ConfigUpdater.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.t14d3.zones.utils; - -import de.t14d3.zones.Zones; -import org.simpleyaml.configuration.file.YamlFile; - -import java.io.File; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; - -public class ConfigUpdater { - private final Zones plugin; - - public ConfigUpdater(Zones plugin) { - this.plugin = plugin; - update(); - } - - private void update() { - try { - final YamlFile currentConfig = new YamlFile(new File(plugin.getDataFolder(), "config.yml")); - currentConfig.loadWithComments(); - InputStream defaultConfigStream = plugin.getClass().getResourceAsStream("/config.yml"); - Path tempFile = Files.createTempFile("defaultConfig", ".yml"); - Files.copy(defaultConfigStream, tempFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - final YamlFile defaultConfig = new YamlFile(tempFile.toFile()); - defaultConfig.loadWithComments(); - - defaultConfig.getKeys(true).forEach(key -> { - if (!currentConfig.contains(key)) { - currentConfig.set(key, defaultConfig.get(key)); - } - if (currentConfig.getComment(key) == null) { - currentConfig.setComment(key, defaultConfig.getComment(key)); - } - }); - - currentConfig.save(); - Files.delete(tempFile); - } catch (Exception e) { - plugin.getLogger().error("Failed to update config: {}", e.getMessage()); - } - } -} diff --git a/api/src/main/java/de/t14d3/zones/utils/Messages.java b/api/src/main/java/de/t14d3/zones/utils/Messages.java index 3fae05d..edce4c9 100644 --- a/api/src/main/java/de/t14d3/zones/utils/Messages.java +++ b/api/src/main/java/de/t14d3/zones/utils/Messages.java @@ -1,130 +1,125 @@ package de.t14d3.zones.utils; +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.Region; -import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.RegionFlagEntry; +import de.t14d3.zones.permissions.PermissionKeyRegistry; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.permissions.subjects.*; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.minimessage.MiniMessage; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - -public class Messages { - private final Zones zones; - private final Map messages = new HashMap<>(); - private final static MiniMessage mm = MiniMessage.miniMessage(); - public static Messages INSTANCE; - - public Messages(Properties messagesConfig, Zones zones) { - this.zones = zones; - INSTANCE = this; - messagesConfig.keySet().forEach( - key -> - messages.put(key.toString(), messagesConfig.getProperty(key.toString())) - ); - } - /** - * Gets a message from the messages.yml file. - * Defaults to "messages.default" if not found. - * - * @param key The key of the message. - * @return The message. - */ - public @NotNull String get(String key) { - return messages.getOrDefault(key, - zones.getConfig().getString("messages.default", key).replaceAll("", key)); - } +import java.util.ArrayList; +import java.util.List; +import java.util.Map; - public @NotNull Component getCmp(String key) { - return mm.deserialize(messages.getOrDefault(key, - zones.getConfig().getString("messages.default", key).replaceAll("", key))); +/** + * Shared message/formatting helpers. + */ +public final class Messages { + private Messages() { } - public @NotNull String getOrDefault(String key, String defaultValue) { - return messages.getOrDefault(key, defaultValue); - } + /** + * Formats a region summary for chat output. + * + *

If {@code showMembers} is enabled, this includes all member subjects and their stored permissions. For + * performance reasons this method assumes callers already hold any required region/world locks.

+ * + * @param region region to format + * @param showMembers whether to include member permissions + * @return formatted component + */ public static Component regionInfo(Region region, boolean showMembers) { - Messages messages = Zones.getInstance().getMessages(); - Component comp = Component.empty(); - comp = comp.append( - mm.deserialize("Name: " + messages.get("region.info.name") + " ", - parsed("name", region.getName()))); + MessageFormatService messages = Rapunzel.context().messages(); + Component comp = messages.component("region.info.details.name", + Placeholders.builder().string("name", region.getName()).build()); + if (region.getParent() != null) { - comp = comp.append(mm.deserialize(messages.get("region.info.parent") + " ", - parsed("parent", region.getParent().toString()))); + comp = comp.appendNewline().append(messages.component("region.info.details.parent", + Placeholders.builder().string("parent", region.getParent().toString()).build())); } - comp = comp.append(mm.deserialize("(" + messages.get("region.info.min") + " - ", - parsed("min", region.getMinString()))); - comp = comp.append(mm.deserialize(messages.get("region.info.max") + ")", - parsed("max", region.getMaxString()))); - comp = comp.append(Component.text(" Members: ").color(NamedTextColor.LIGHT_PURPLE)); + + comp = comp.appendNewline().append(messages.component("region.info.details.bounds", + Placeholders.builder() + .string("min", region.getMinString()) + .string("max", region.getMaxString()) + .build())); + + comp = comp.appendNewline().append(messages.component("region.info.details.members")); if (showMembers) { - // Iterate over members to format permissions - for (Map.Entry> member : region.getMembers().entrySet()) { - String playerName = null; - try { - playerName = Zones.getInstance().getPlatform().getPlayer(UUID.fromString(member.getKey())) - .getName(); - } catch (IllegalArgumentException ignored) { - } - if (playerName == null) { - playerName = member.getKey(); + RegionPermissions perms = region.getPermissions(); + for (Map.Entry member : perms.subjects().entrySet()) { + SubjectRef subjectRef = member.getKey(); + RegionPermissions.SubjectPermissions subject = member.getValue(); + if (subjectRef == null || subject == null) continue; + + String subjectName; + if (subjectRef instanceof PlayerSubject p) { + subjectName = RPlayer.get(p.uuid()).map(RPlayer::name).orElse(p.uuid().toString()); + } else if (subjectRef instanceof GroupSubject g) { + subjectName = "group:" + g.name(); + } else if (subjectRef instanceof UniversalSubject) { + subjectName = "+universal"; + } else { + subjectName = Subjects.format(subjectRef); } - Component playerComponent = mm.deserialize(messages.get("region.info.members.name"), - parsed("name", playerName)); - playerComponent = playerComponent.appendNewline(); - // Extract permissions and format them using Adventure Components - List permissions = member.getValue(); + Component subjectComponent = messages.component("region.info.members.name", + Placeholders.builder().string("name", subjectName).build()).appendNewline(); + Component permissionsComponent = Component.empty(); + for (var permEntry : subject.values().int2ObjectEntrySet()) { + int permId = permEntry.getIntKey(); + String permName = PermissionKeyRegistry.instance().getName(permId); + if (permName == null || permName.isBlank()) continue; - for (RegionFlagEntry permEntry : permissions) { - String permKey = permEntry.getFlagValue(); - List permValues = permEntry.getValues(); + RegionPermissions.PermissionValue value = permEntry.getValue(); List formattedComponents = new ArrayList<>(); - for (RegionFlagEntry.FlagValue val : permValues) { - String value = val.getValue(); - Component formattedValue; - if (("true".equalsIgnoreCase(value) || "*".equals(value)) && !val.isInverted()) { - formattedValue = mm.deserialize(messages.get("region.info.members.values.allowed"), - parsed("value", value)); - } else if ("false".equalsIgnoreCase(value) || val.isInverted()) { - formattedValue = mm.deserialize(messages.get("region.info.members.values.denied"), - parsed("value", "!" + value)); - } else { - formattedValue = mm.deserialize(messages.get("region.info.members.values.allowed"), - parsed("value", value)); + if (value instanceof RegionPermissions.TargetDecisionValue targeted) { + for (String t : targeted.allowTargets()) { + formattedComponents.add(messages.component("region.info.members.values.allowed", + Placeholders.builder().string("value", t).build())); + } + for (String t : targeted.denyTargets()) { + formattedComponents.add(messages.component("region.info.members.values.denied", + Placeholders.builder().string("value", t).build())); + } + } else if (value instanceof RegionPermissions.StringSetValue set) { + for (String s : set.allowValues()) { + formattedComponents.add(messages.component("region.info.members.values.allowed", + Placeholders.builder().string("value", s).build())); + } + for (String s : set.denyValues()) { + formattedComponents.add(messages.component("region.info.members.values.denied", + Placeholders.builder().string("value", s).build())); } - formattedComponents.add(formattedValue); } - // Combine all formatted components into one line - Component permLine = mm.deserialize(messages.get("region.info.members.permission"), - parsed("permission", permKey)); - // Append all value components with comma separators + if (formattedComponents.isEmpty()) continue; + + Component permLine = messages.component("region.info.members.permission", + Placeholders.builder().string("permission", permName).build()); + for (int i = 0; i < formattedComponents.size(); i++) { permLine = permLine.append(formattedComponents.get(i)); if (i < formattedComponents.size() - 1) { permLine = permLine.append(Component.text(", ").color(NamedTextColor.GRAY)); } } - // Append the permission line and a newline permissionsComponent = permissionsComponent.append(permLine).append(Component.newline()); } - // Append the player component and their permissions to hover text - comp = comp.appendNewline() - .append(playerComponent) - .append(permissionsComponent); + + comp = comp.appendNewline().append(subjectComponent).append(permissionsComponent); } } - comp = comp.append(mm.deserialize(messages.get("region.info.key"), parsed("key", region.getKey().toString()))); + comp = comp.appendNewline().append(messages.component("region.info.key", + Placeholders.builder().string("key", region.getKey().toString()).build())); return comp; } } diff --git a/api/src/main/java/de/t14d3/zones/utils/TypeKeys.java b/api/src/main/java/de/t14d3/zones/utils/TypeKeys.java new file mode 100644 index 0000000..2745840 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/utils/TypeKeys.java @@ -0,0 +1,40 @@ +package de.t14d3.zones.utils; + +import java.util.Locale; + +/** + * Normalizes type keys (blocks/entities/items/etc) to the shortest stable key used by Zones. + * + *

Examples: + *

    + *
  • {@code minecraft:stone -> stone}
  • + *
  • {@code my_mod:cool_block -> cool_block}
  • + *
  • {@code block.minecraft.stone -> stone}
  • + *
  • {@code entity.minecraft.zombie -> zombie}
  • + *
+ */ +public final class TypeKeys { + private TypeKeys() { + } + + public static String normalize(String rawKey) { + if (rawKey == null) return ""; + String key = rawKey.trim().toLowerCase(Locale.ROOT); + if (key.isEmpty()) return ""; + + int colon = key.indexOf(':'); + if (colon >= 0 && colon + 1 < key.length()) { + key = key.substring(colon + 1); + } + + // Common translation keys use dot-delimited namespaces (e.g. block.minecraft.stone) + // - drop everything up to the last dot to keep the shortest stable identifier. + int lastDot = key.lastIndexOf('.'); + if (lastDot >= 0 && lastDot + 1 < key.length()) { + key = key.substring(lastDot + 1); + } + + return key; + } +} + diff --git a/api/src/main/java/de/t14d3/zones/utils/Utils.java b/api/src/main/java/de/t14d3/zones/utils/Utils.java index dbe989a..c3d0b12 100644 --- a/api/src/main/java/de/t14d3/zones/utils/Utils.java +++ b/api/src/main/java/de/t14d3/zones/utils/Utils.java @@ -1,6 +1,7 @@ package de.t14d3.zones.utils; -import de.t14d3.zones.objects.Player; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; /** * Utility methods for the plugin. @@ -33,10 +34,11 @@ public enum SelectionMode { this.name = name; } - public static SelectionMode getPlayerMode(Player player) { + public static SelectionMode getPlayerMode(RPlayer player) { SelectionMode mode; try { - mode = SelectionMode.valueOf(player.getMetadata("mode").toUpperCase()); + String raw = player.extras().get(ZonesExtraKeys.SELECTION_MODE).orElse("CUBOID_2D"); + mode = SelectionMode.valueOf(raw.toUpperCase()); } catch (IllegalArgumentException | NullPointerException e) { mode = SelectionMode.CUBOID_2D; } diff --git a/api/src/main/java/de/t14d3/zones/visuals/BeaconUtils.java b/api/src/main/java/de/t14d3/zones/visuals/BeaconUtils.java index c384cea..94eaa5e 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/BeaconUtils.java +++ b/api/src/main/java/de/t14d3/zones/visuals/BeaconUtils.java @@ -1,22 +1,22 @@ package de.t14d3.zones.visuals; -import de.t14d3.zones.objects.BlockLocation; +import de.t14d3.rapunzellib.objects.RBlockPos; import net.kyori.adventure.text.format.NamedTextColor; import java.util.ArrayList; import java.util.List; public class BeaconUtils { - public static List resetList(BlockLocation location) { + public static List resetList(RBlockPos location) { List changes = new ArrayList<>(); if (location == null) { return changes; } - int x = location.getX(); + int x = location.x(); int y = -62; - int z = location.getZ(); + int z = location.z(); changes.add(new BlockChange(x, 1, z, null)); // Assuming null will be handled as default block data @@ -33,16 +33,16 @@ public static List resetList(BlockLocation location) { return changes; } - public static List createList(BlockLocation location, NamedTextColor color) { + public static List createList(RBlockPos location, NamedTextColor color) { List changes = new ArrayList<>(); if (location == null) { return changes; } - int x = location.getX(); + int x = location.x(); int y = -62; - int z = location.getZ(); + int z = location.z(); for (int xPoint = x - 1; xPoint <= x + 1; xPoint++) { for (int zPoint = z - 1; zPoint <= z + 1; zPoint++) { @@ -64,31 +64,6 @@ public static List createList(BlockLocation location, NamedTextColo return changes; } - public static class BlockChange { - private final int x, y, z; - private final String blockData; - - public BlockChange(int x, int y, int z, String blockData) { - this.x = x; - this.y = y; - this.z = z; - this.blockData = blockData; - } - - public int getX() { - return x; - } - - public int getY() { - return y; - } - - public int getZ() { - return z; - } - - public String getBlockData() { - return blockData; - } + public record BlockChange(int x, int y, int z, String blockData) { } } diff --git a/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java b/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java index fce66cd..8d81d5a 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java +++ b/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java @@ -1,8 +1,8 @@ package de.t14d3.zones.visuals; +import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.Region; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.Player; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -14,7 +14,7 @@ public class FindBossbar { private final Zones zones; - public Map players = new HashMap<>(); + public Map players = new HashMap<>(); private final BossBar.Color color; private final BossBar.Overlay overlay; private final float progress; @@ -34,9 +34,13 @@ public FindBossbar(Zones zones) { private class FindBossbarRunnable implements Runnable { @Override public void run() { - for (Map.Entry entry : players.entrySet()) { + for (Map.Entry entry : players.entrySet()) { + RPlayer player = entry.getKey(); + var loc = player.location().orElse(null); + if (loc == null) continue; + List regions = zones.getRegionManager() - .getRegionsAt(entry.getKey().getLocation(), entry.getKey().getWorld()); + .getRegionsAt(loc.blockPos(), loc.world()); BossBar bar = entry.getValue() == null ? BossBar.bossBar(Component.text("Initializing..."), progress, color, overlay) : entry.getValue(); Component[] names = new Component[regions.size()]; @@ -48,7 +52,7 @@ public void run() { Component text = Component.textOfChildren(names).color(textColor); bar.name(text); entry.setValue(bar); - zones.getPlatform().getAudience(entry.getKey()).showBossBar(bar); + player.audience().showBossBar(bar); } } diff --git a/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java b/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java index 5b7e577..d127bb9 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java +++ b/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java @@ -1,21 +1,19 @@ package de.t14d3.zones.visuals; +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.Zones; import de.t14d3.zones.ZonesPlatform; -import de.t14d3.zones.objects.BlockLocation; import de.t14d3.zones.objects.Box; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.objects.PlayerRepository; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.time.Duration; public class ParticleHandler { private final Zones zones; private final ZonesPlatform platform; private final double range; - private ScheduledFuture particleScheduler; public ParticleHandler(Zones zones) { this.zones = zones; @@ -23,13 +21,21 @@ public ParticleHandler(Zones zones) { this.range = zones.getConfig().getInt("visuals.particles.range", 15); } - void spawnParticleOutline(Player player, BlockLocation min, BlockLocation max) { - int x1 = min.getX(); - int y1 = min.getY(); - int z1 = min.getZ(); - int x2 = max.getX() + 1; - int y2 = max.getY() + 1; - int z2 = max.getZ() + 1; + void spawnParticleOutline(RPlayer player, RBlockPos min, RBlockPos max) { + // Normalize coordinates so function works regardless of which corner is "min" or "max" + int minX = Math.min(min.x(), max.x()); + int maxX = Math.max(min.x(), max.x()); + int minY = Math.min(min.y(), max.y()); + int maxY = Math.max(min.y(), max.y()); + int minZ = Math.min(min.z(), max.z()); + int maxZ = Math.max(min.z(), max.z()); + + int x1 = minX; + int y1 = minY; + int z1 = minZ; + int x2 = maxX + 1; + int y2 = maxY + 1; + int z2 = maxZ + 1; // Generate particles for all 6 faces spawnFace(player, 'y', y1, x1, x2, z1, z2, x1, x2, y1, y2, z1, z2); // Floor @@ -40,54 +46,60 @@ void spawnParticleOutline(Player player, BlockLocation min, BlockLocation max) { spawnFace(player, 'z', z2, x1, x2, y1, y2, x1, x2, y1, y2, z1, z2); // Back wall } - public void spawnFace(Player player, char axis, int fixedVal, + public void spawnFace(RPlayer player, char axis, int fixedVal, int axis1Start, int axis1End, int axis2Start, int axis2End, int x1, int x2, int y1, int y2, int z1, int z2) { - BlockLocation loc = new BlockLocation(0, 0, 0); + var playerLoc = player.location().orElse(null); + if (playerLoc == null) return; + RBlockPos playerBlock = playerLoc.blockPos(); + for (int a1 = axis1Start; a1 <= axis1End; a1++) { for (int a2 = axis2Start; a2 <= axis2End; a2++) { - loc = createLocation(axis, fixedVal, a1, a2, loc); - if (loc.distance(player.getLocation()) >= range) continue; + RBlockPos loc = createLocation(axis, fixedVal, a1, a2); + + // Use squared distance to avoid sqrt() call (speeeeed) + long rangeSq = (long) range * (long) range; + if (distanceSquared(loc, playerBlock) >= rangeSq) continue; platform.spawnParticle(isCorner(loc, x1, x2, y1, y2, z1, z2) ? 2 : 1, loc, player); } } } - private static BlockLocation createLocation(char axis, int fixedVal, int a1, int a2, BlockLocation locToChange) { + static RBlockPos createLocation(char axis, int fixedVal, int a1, int a2) { return switch (axis) { - case 'x' -> locToChange.setX(fixedVal).setY(a1).setZ(a2); - case 'y' -> locToChange.setX(a1).setY(fixedVal).setZ(a2); - case 'z' -> locToChange.setX(a1).setY(a2).setZ(fixedVal); + case 'x' -> new RBlockPos(fixedVal, a1, a2); + case 'y' -> new RBlockPos(a1, fixedVal, a2); + case 'z' -> new RBlockPos(a1, a2, fixedVal); default -> throw new IllegalArgumentException("Invalid axis"); }; } - private static boolean isCorner(BlockLocation loc, int x1, int x2, int y1, int y2, int z1, int z2) { - boolean xEdge = loc.getX() == x1 || loc.getX() == x2; - boolean yEdge = loc.getY() == y1 || loc.getY() == y2; - boolean zEdge = loc.getZ() == z1 || loc.getZ() == z2; + static boolean isCorner(RBlockPos loc, int x1, int x2, int y1, int y2, int z1, int z2) { + boolean xEdge = loc.x() == x1 || loc.x() == x2; + boolean yEdge = loc.y() == y1 || loc.y() == y2; + boolean zEdge = loc.z() == z1 || loc.z() == z2; return (xEdge && yEdge) || (xEdge && zEdge) || (yEdge && zEdge); } + static long distanceSquared(RBlockPos a, RBlockPos b) { + long dx = (long) b.x() - a.x(); + long dy = (long) b.y() - a.y(); + long dz = (long) b.z() - a.z(); + return dx * dx + dy * dy + dz * dz; + } + public void particleScheduler() { if (!zones.getConfig().getBoolean("visuals.particles.enabled", false)) { return; } - Runnable runnable = () -> { - for (Player player : PlayerRepository.getPlayers()) { - Box selection = player.getSelection(); - if (selection == null) { - continue; - } else if (selection.getMin() == null || selection.getMax() == null) { - continue; - } - spawnParticleOutline( - player, - selection.getMin(), - selection.getMax()); + + Rapunzel.context().scheduler().runRepeatingAsync(Duration.ofMillis(200), Duration.ofMillis(200), () -> { + for (RPlayer player : RPlayer.online()) { + Box selection = player.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + if (selection == null || selection.getMin() == null || selection.getMax() == null) continue; + spawnParticleOutline(player, selection.getMin(), selection.getMax()); } - }; - Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(runnable, 0L, 200, TimeUnit.MILLISECONDS); + }); } } diff --git a/api/src/main/resources/config.yml b/api/src/main/resources/config.yml index af98352..1ac917c 100644 --- a/api/src/main/resources/config.yml +++ b/api/src/main/resources/config.yml @@ -87,21 +87,17 @@ events: limit-exceeded-action: CANCEL cache: - # For how long should Cache entries be stored for? - # Time is in seconds, defaults to 300 - # Has an inaccuracy of ~0.024s/second due to - # using bitwise shifting to convert to seconds - # e.g. 300 is actually 307.2 seconds + # Permission lookup caches (per-region + per-subject + per-flag + per-type). + # TTL time is in seconds, defaults to 300. ttl: 300 - # How often should the cache be checked for expired entries? - # Time in minutes, defaults to 10 - check-interval: 10 # Should there be a limit on the number of cached entries? # Set to 0 to disable, defaults to 0 # Higher values may improve performance, but also increase memory usage # For reference, 10.000 cache entries will use ~0.8-1.0MB of memory limit: 0 + + storage: # Controls how the plugin should store regions # Valid options are: @@ -136,4 +132,4 @@ storage: # Example: url: "jdbc:postgresql://localhost:5432/zones?serverTimezone=UTC&autoReconnect=true" url: "" # Example: driver: "org.postgresql.Driver" - driver: "" \ No newline at end of file + driver: "" diff --git a/api/src/main/resources/messages.properties b/api/src/main/resources/messages.properties index 6d4d47f..9a40029 100644 --- a/api/src/main/resources/messages.properties +++ b/api/src/main/resources/messages.properties @@ -1,5 +1,7 @@ commands.cancel.success=Cancelled selection. -commands.invalid=Invalid subcommand. Use /zone +commands.invalid=Invalid subcommand. Use /zone <...> +commands.invalid-player=Invalid player. +commands.invalid-permission=Invalid permission. commands.no-permission=You don't have permission to execute this command! commands.invalid-region=Invalid region. commands.only-player=This command can only be executed by a player. @@ -13,8 +15,13 @@ commands.expand.invalid-usage=Invalid usage. Usage: /zone expand < commands.save=Saved regions to file commands.select.selected=Selected region '' commands.select.deselected=Deselected region. -commands.set.success=Permission for set to for region -commands.set.invalid-usage=Invalid usage. Usage: /zone set +commands.perm.allow=Rule set: allow in region +commands.perm.deny=Rule set: deny in region +commands.perm.unset=Rule removed: in region +commands.perm.clear=Cleared all rules for in region +commands.perm.invalid-target=Invalid target. Use +universal, group:, player:, or a player name/UUID. +commands.perm.invalid-flag-kind=Permission can't be managed with /zone perm. +commands.perm.nothing-to-remove=No matching rule found. commands.subcreate.no-parent=Error: No parent region found. commands.subcreate.outside-parent=Error: Selection is outside parent region. commands.subcreate.success=Sub-region created successfully! @@ -40,6 +47,7 @@ region.info.members.values.allowed= region.info.members.values.denied= create.primary=Set first location to create.secondary=Set second location to +flags.universal=Universal flags.break=Allows breaking blocks flags.place=Allows placing blocks @@ -49,4 +57,5 @@ flags.redstone=Allows interacting with redstone flags.entity=Allows interacting with entities flags.ignite=Allows igniting tnt flags.damage=Allows damaging entities +flags.role=Defines a member role flags.group=Add a group to the player diff --git a/api/src/main/resources/messages.yml b/api/src/main/resources/messages.yml new file mode 100644 index 0000000..263fb8c --- /dev/null +++ b/api/src/main/resources/messages.yml @@ -0,0 +1,100 @@ +prefix: "" + +messages: + default: "Message not found! Contact a Server Administrator" + +commands: + cancel: + success: "Cancelled selection." + invalid: "Invalid subcommand. Use /zone <...>" + invalid-player: "Invalid player." + invalid-permission: "Invalid permission." + no-permission: "You don't have permission to execute this command!" + invalid-region: "Invalid region." + only-player: "This command can only be executed by a player." + create: + overlap: "Error: Region overlaps existing region." + click-corners: "Click two corners to select the region." + success: "Region '' created successfully!" + delete: + success: "Region '' deleted successfully!" + expand: + direction: "Direction to expand" + success: "Region '' expanded successfully!" + fail: "Error: Region '' could not be expanded." + invalid-usage: "Invalid usage. Usage: /zone expand [overlap]" + save: "Saved regions to file" + select: + selected: "Selected region ''" + deselected: "Deselected region." + perm: + allow: "Allowed for to in region " + deny: "Denied for to in region " + remove: "Removed for name in region " + global: "Set global for to in region " + clear: "Cleared for in region " + invalid-flag-kind: "Permission does not support this operation." + nothing-to-remove: "Nothing to remove." + subcreate: + no-parent: "Error: No parent region found." + outside-parent: "Error: Selection is outside parent region." + success: "Sub-region created successfully!" + load: "Loaded regions!" + rename: + provide-name: "Provide a new name for the region!" + success: "Region '' renamed to ''!" + import: + not-loaded: "Error: Plugin must be loaded for this command!" + no-plugin: "Provide a plugin name to import regions from!" + success: "Imported regions!" + worldguard-tooltip: "Imports regions from WorldGuard" + mode: + set: "Mode set to " + +region: + none-found: "No regions found!" + no-interact-permission: "You are lacking the permission for type in region(s) " + info: + name: "" + min: "" + max: "" + key: "Region key: #" + parent: "Parent region: " + details: + name: "Name: " + parent: "Parent region: " + bounds: "( - )" + members: "Members: " + members: + name: "" + permission: " :" + values: + allowed: "" + denied: "" + +create: + primary: "Set first location to " + secondary: "Set second location to " + +flags: + universal: "Universal" + fallback: "Fallback description" + 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" + role: "Defines a member role" + group: "Add a group to the player" + spawn: "Controls the spawning of entities" + explosion: "Controls the explosion of entities" + create: "Controls the creation of blocks through world events" + destroy: "Controls the removal of blocks through world events" + transform: "Controls the transformation of blocks into other blocks" + spread: "Controls the spread of blocks through world events" + relocate: "Controls the ability for blocks to change their location" + physics: "Controls the physics of blocks" + hurt: "Controls the ability for entities to be hurt" diff --git a/api/src/main/resources/regions.yml b/api/src/main/resources/regions.yml index 585dc27..de2470a 100644 --- a/api/src/main/resources/regions.yml +++ b/api/src/main/resources/regions.yml @@ -11,12 +11,39 @@ regions: x: 100 y: 319 z: 100 - members: - +group-members: - break: 'true' - container: 'true' - damage: 'true' - entity: 'true' - interact: 'true' - place: 'true' - redstone: 'true' \ No newline at end of file + acl: + universal: { } + players: { } + groups: + members: + break: + type: targets + allow: [ "*" ] + deny: [ ] + container: + type: targets + allow: [ "*" ] + deny: [ ] + damage: + type: targets + allow: [ "*" ] + deny: [ ] + entity: + type: targets + allow: [ "*" ] + deny: [ ] + interact: + type: targets + allow: [ "*" ] + deny: [ ] + place: + type: targets + allow: [ "*" ] + deny: [ ] + redstone: + type: targets + allow: [ "*" ] + deny: [ ] + membership: + players: { } + groups: { } diff --git a/build.gradle.kts b/build.gradle.kts index 0da23bb..642d11e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,19 +1,25 @@ plugins { id("maven-publish") - id("com.gradleup.shadow") version "9.0.0-beta8" - id("java-library") - id("fabric-loom") version "1.10-SNAPSHOT" apply false + alias(libs.plugins.shadow) + `java-library` + alias(libs.plugins.fabric.loom) apply false } + group = "de.t14d3" version = "0.2.2" repositories { + mavenLocal() mavenCentral() maven { name = "sonatype" url = uri("https://oss.sonatype.org/content/groups/public/") } + maven { + name = "t14d3-snapshots" + url = uri("https://maven.t14d3.de/snapshots") + } maven { name = "papermc-repo" url = uri("https://repo.papermc.io/repository/maven-public/") @@ -28,10 +34,15 @@ repositories { allprojects { plugins.apply("java") repositories { + mavenLocal() mavenCentral() + maven { + name = "t14d3-snapshots" + url = uri("https://maven.t14d3.de/snapshots") + } maven { name = "papermc-repo" - url = uri("https://repo.papermc.io/repository/maven-public/") + url = uri("https://repo.papermc.io/repository/maven-public/") } maven { name = "sonatype" @@ -43,11 +54,11 @@ allprojects { } } dependencies { - compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") - compileOnly("org.jetbrains:annotations:23.0.0") + compileOnly(rootProject.libs.paper.api) + compileOnly(rootProject.libs.annotations) - compileOnly("net.kyori:adventure-api:4.19.0") - compileOnly("net.kyori:adventure-text-minimessage:4.19.0") + compileOnly(rootProject.libs.adventure.api) + compileOnly(rootProject.libs.adventure.minimessage) } } @@ -80,13 +91,12 @@ tasks.withType().configureEach { tasks { shadowJar { archiveClassifier.set("") + mergeServiceFiles() relocate("dev.jorel.commandapi", "de.t14d3.zones.dependencies.commandapi") relocate("org.simpleyaml", "de.t14d3.zones.dependencies.simpleyaml") relocate("org.yaml.snakeyaml", "de.t14d3.zones.dependencies.snakeyaml") - relocate("me.lucko.fabric.api.permissions", "de.t14d3.zones.dependencies.fabricpermissions") - dependencies { exclude(dependency("org.checkerframework:checker-qual")) diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index bd15cd2..48c8368 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -1,9 +1,9 @@ plugins { id("java") - id("xyz.jpenilla.run-paper") version "2.3.1" + alias(libs.plugins.run.paper) + alias(libs.plugins.paperweight.userdev) } - repositories { mavenCentral() maven { @@ -18,20 +18,82 @@ repositories { name = "CodeMC" url = uri("https://repo.codemc.io/repository/maven-public/") } + maven { + name = "Central Portal Snapshots" + url = uri("https://central.sonatype.com/repository/maven-snapshots/") + // Only search this repository for the specific dependency + content { + includeModule("dev.jorel", "commandapi") + } + } } dependencies { implementation(project(":api")) - compileOnly("me.clip:placeholderapi:2.11.6") - compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10") - compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.13") - implementation(platform("com.intellectualsites.bom:bom-newest:1.52")) - compileOnly("com.fastasyncworldedit:FastAsyncWorldEdit-Core") - compileOnly("com.fastasyncworldedit:FastAsyncWorldEdit-Bukkit") - implementation("dev.jorel:commandapi-bukkit-shade-mojang-mapped:9.7.0") + compileOnly(rootProject.libs.placeholderapi) + compileOnly(rootProject.libs.worldedit.bukkit) + compileOnly(rootProject.libs.worldguard.bukkit) + implementation(platform(rootProject.libs.intellectualsites.bom)) + compileOnly(rootProject.libs.fawe.core) + compileOnly(rootProject.libs.fawe.bukkit) + implementation(rootProject.libs.commandapi.paper.shade) + + implementation(rootProject.libs.adventure.platform.bukkit) + + val localRapunzelLibsDir = rootProject.file("../RapunzelLib/build/libs") + + val localRapunzelPlatformPaper = localRapunzelLibsDir + .takeIf { it.isDirectory } + ?.listFiles() + ?.filter { it.isFile && it.name.startsWith("rapunzellib-platform-paper-") && it.name.endsWith("-shaded.jar") } + ?.maxByOrNull { it.lastModified() } + + val localRapunzelEventsPaper = localRapunzelLibsDir + .takeIf { it.isDirectory } + ?.listFiles() + ?.filter { + it.isFile && + it.name.startsWith("rapunzellib-events-paper-") && + it.name.endsWith(".jar") && + !it.name.endsWith("-sources.jar") && + !it.name.endsWith("-javadoc.jar") + } + ?.maxByOrNull { it.lastModified() } + + val localRapunzelEvents = localRapunzelLibsDir + .takeIf { it.isDirectory } + ?.listFiles() + ?.filter { + it.isFile && + it.name.startsWith("rapunzellib-events-") && + it.name.endsWith(".jar") && + !it.name.contains("-paper-") && + !it.name.contains("-fabric-") && + !it.name.endsWith("-sources.jar") && + !it.name.endsWith("-javadoc.jar") + } + ?.maxByOrNull { it.lastModified() } + + if (localRapunzelPlatformPaper != null) { + implementation(files(localRapunzelPlatformPaper)) + } else { + implementation("de.t14d3.rapunzellib:platform-paper:${libs.versions.rapunzellib.get()}:shaded") + } + + if (localRapunzelEvents != null) { + implementation(files(localRapunzelEvents)) + } else { + implementation(rootProject.libs.rapunzellib.events) + } + + if (localRapunzelEventsPaper != null) { + implementation(files(localRapunzelEventsPaper)) + } else { + implementation(rootProject.libs.rapunzellib.events.paper) + } - implementation("net.kyori:adventure-platform-bukkit:4.3.4") + paperweight.paperDevBundle(rootProject.libs.versions.paper.dev.bundle.get()) } tasks.processResources { @@ -43,16 +105,24 @@ tasks.processResources { } } runPaper.disablePluginJarDetection() + +val zonesActiveProcessors: Int? = providers.gradleProperty("zones.activeProcessors") + .orNull + ?.toIntOrNull() + tasks.runServer { - minecraftVersion("1.21.4") - pluginJars(rootProject.tasks.named("shadowJar").get().outputs.files) + minecraftVersion("1.21.10") + pluginJars(rootProject.tasks.named("shadowJar").get().outputs.files) javaLauncher = javaToolchains.launcherFor { vendor.set(JvmVendorSpec.JETBRAINS) languageVersion.set(JavaLanguageVersion.of(21)) } jvmArgs("-XX:+AllowEnhancedClassRedefinition") + if (zonesActiveProcessors != null && zonesActiveProcessors > 0) { + jvmArgs("-XX:ActiveProcessorCount=$zonesActiveProcessors") + } } tasks.test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPermissionManager.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPermissionManager.java index 6fb440d..4cefa9b 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPermissionManager.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPermissionManager.java @@ -1,28 +1,37 @@ package de.t14d3.zones.bukkit; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.BlockLocation; import de.t14d3.zones.objects.Flag; -import de.t14d3.zones.objects.World; import de.t14d3.zones.permissions.PermissionManager; +import de.t14d3.zones.permissions.flags.FlagContext; import org.bukkit.Location; import java.util.UUID; public class BukkitPermissionManager extends PermissionManager { - private final Zones zones; - public BukkitPermissionManager(Zones zones) { super(zones); - this.zones = zones; } - public boolean checkAction(Location loc, String who, Flag action, String type, Object... extra) { - return super.checkAction(BlockLocation.of(loc), World.of(loc.getWorld()), who, action, type, extra); + public boolean checkAction(Location loc, String who, Flag action, String type) { + RWorldRef world = new RWorldRef(loc.getWorld().getName(), loc.getWorld().getKey().toString()); + RBlockPos pos = new RBlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + return super.checkAction(pos, world, who, action, type); + } + + + public boolean checkAction(Location loc, Flag action, String type) { + RWorldRef world = new RWorldRef(loc.getWorld().getName(), loc.getWorld().getKey().toString()); + RBlockPos pos = new RBlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + return super.checkAction(pos, world, action, type); } - public boolean checkAction(Location loc, Flag action, String type, Object... extra) { - return super.checkAction(BlockLocation.of(loc), World.of(loc.getWorld()), action, type, extra); + public boolean checkAction(Location loc, Flag action, String type, FlagContext context) { + RWorldRef world = new RWorldRef(loc.getWorld().getName(), loc.getWorld().getKey().toString()); + RBlockPos pos = new RBlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + return super.checkAction(pos, world, action, type, context); } public boolean checkAction(Location location, UUID playerUUID, Flag action, String name) { diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPlatform.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPlatform.java index 8311a0c..f7cff6c 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPlatform.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPlatform.java @@ -1,29 +1,22 @@ package de.t14d3.zones.bukkit; import com.destroystokyo.paper.ParticleBuilder; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.Zones; import de.t14d3.zones.ZonesPlatform; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.objects.World; import de.t14d3.zones.permissions.PermissionManager; import de.t14d3.zones.utils.Types; import de.t14d3.zones.visuals.BeaconUtils; -import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.block.data.BlockData; -import org.bukkit.metadata.FixedMetadataValue; -import org.bukkit.metadata.MetadataValue; -import org.jetbrains.annotations.Nullable; import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; public class BukkitPlatform implements ZonesPlatform { private final ZonesBukkit plugin; @@ -36,55 +29,14 @@ public BukkitPlatform(ZonesBukkit plugin) { this.secondary = Particle.valueOf(plugin.getConfig().getString("visuals.particles.secondary", "WAX_ON")); } - @Override - public List getWorlds() { - List worlds = new ArrayList<>(); - plugin.getServer().getWorlds().forEach(world -> { - worlds.add(World.of(world)); - }); - return worlds; - } - - public static org.bukkit.World getWorld(World world) { - return Bukkit.getServer().getWorld(world.getUID()); - } - - @Override - public Player getPlayer(UUID uuid) { - org.bukkit.OfflinePlayer player = plugin.getServer().getOfflinePlayer(uuid); - String name = player.getName(); - if (name == null) { - name = player.getUniqueId().toString(); - } - return PlayerRepository.getOrAdd(name, player.getUniqueId()); - } - - public org.bukkit.entity.Player getNativePlayer(Player player) { - return plugin.getServer().getPlayer(player.getUUID()); - } - - @Override - public boolean hasPermission(Player player, String permission) { - org.bukkit.entity.Player bukkitPlayer = plugin.getServer().getPlayer(player.getUUID()); - if (bukkitPlayer != null) { - return bukkitPlayer.hasPermission(permission); - } - return false; - } - - @Override - public ZonesPlatform getPlatform() { - return this; - } - @Override public File getDataFolder() { return plugin.getDataFolder(); } @Override - public PermissionManager getPermissionManager() { - return plugin.getPermissionManager(); + public PermissionManager createPermissionManager(Zones zones) { + return new BukkitPermissionManager(zones); } @Override @@ -92,52 +44,16 @@ public Types getTypes() { return plugin.getTypes(); } - @Override - public Audience getAudience(Player player) { - return plugin.getServer().getPlayer(player.getUUID()); - } - public ZonesBukkit getPlugin() { return plugin; } @Override - public World getWorld(Player player) { - return plugin.getPlatform().getWorld(plugin.getServer().getPlayer(player.getUUID()).getWorld().getName()); - } - - @Override - public BlockLocation getLocation(Player player) { - return BlockLocation.of( - plugin.getServer().getPlayer(player.getUUID()).getLocation().toVector().toBlockVector()); - } - - @SuppressWarnings("DataFlowIssue") - @Override - public @Nullable String getMetadata(Player player, String key) { - try { - return plugin.getServer().getPlayer(player.getUUID()).getMetadata(key).stream().filter(value -> - value.getOwningPlugin().equals(plugin)).findFirst().map(MetadataValue::asString).orElse(null); - } catch (NullPointerException e) { - return null; - } - } - - @SuppressWarnings("DataFlowIssue") - @Override - public void setMetadata(Player player, String key, String value) { - try { - plugin.getServer().getPlayer(player.getUUID()).removeMetadata(key, plugin); - plugin.getServer().getPlayer(player.getUUID()).setMetadata(key, new FixedMetadataValue(plugin, value)); - } catch (NullPointerException ignored) { - } - } - - @Override - public void spawnParticle(int type, BlockLocation particleLocation, Player player) { - org.bukkit.entity.Player bukkitPlayer = plugin.getServer().getPlayer(player.getUUID()); + public void spawnParticle(int type, RBlockPos particleLocation, RPlayer player) { + org.bukkit.entity.Player bukkitPlayer = player.tryHandle(org.bukkit.entity.Player.class).orElse(null); if (bukkitPlayer == null) return; - org.bukkit.Location bukkitLocation = particleLocation.toLocation(Bukkit.getWorld(player.getWorld().getUID())); + org.bukkit.Location bukkitLocation = new Location(bukkitPlayer.getWorld(), particleLocation.x(), + particleLocation.y(), particleLocation.z()); ParticleBuilder particleBuilder = new ParticleBuilder(type == 1 ? primary : secondary); particleBuilder.location(bukkitLocation); particleBuilder.receivers(bukkitPlayer); @@ -148,17 +64,20 @@ public void spawnParticle(int type, BlockLocation particleLocation, Player playe } @Override - public void showBeacon(de.t14d3.zones.objects.Player player, de.t14d3.zones.objects.BlockLocation location, de.t14d3.zones.objects.World world, NamedTextColor color) { + public void showBeacon(RPlayer player, RBlockPos location, RWorldRef world, NamedTextColor color) { if (location == null) return; - showBeacon(((BukkitPlatform) plugin.getPlatform()).getNativePlayer(player), - location.toLocation(plugin.getServer().getWorld(world.getUID())), color); + org.bukkit.entity.Player bukkitPlayer = player.tryHandle(org.bukkit.entity.Player.class).orElse(null); + if (bukkitPlayer == null) return; + org.bukkit.World bukkitWorld = resolveWorld(world, bukkitPlayer); + showBeacon(bukkitPlayer, new Location(bukkitWorld, location.x(), location.y(), location.z()), color); } public void showBeacon(org.bukkit.entity.Player player, Location location, NamedTextColor color) { plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { - for (BeaconUtils.BlockChange change : BeaconUtils.createList(BlockLocation.of(location), color)) { - BlockData data = Bukkit.createBlockData(Material.valueOf(change.getBlockData())); - Location loc = new Location(player.getWorld(), change.getX(), change.getY(), change.getZ()); + RBlockPos pos = new RBlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + for (BeaconUtils.BlockChange change : BeaconUtils.createList(pos, color)) { + BlockData data = Bukkit.createBlockData(Material.valueOf(change.blockData())); + Location loc = new Location(location.getWorld(), change.x(), change.y(), change.z()); if (!loc.getBlock().getType().isOccluding()) { continue; } @@ -168,19 +87,43 @@ public void showBeacon(org.bukkit.entity.Player player, Location location, Named } @Override - public void removeBeacon(de.t14d3.zones.objects.Player player, de.t14d3.zones.objects.World world, de.t14d3.zones.objects.BlockLocation location) { + public void removeBeacon(RPlayer player, RWorldRef world, RBlockPos location) { if (location == null) return; - removeBeacon(((BukkitPlatform) plugin.getPlatform()).getNativePlayer(player), - location.toLocation(plugin.getServer().getWorld(world.getUID()))); + org.bukkit.entity.Player bukkitPlayer = player.tryHandle(org.bukkit.entity.Player.class).orElse(null); + if (bukkitPlayer == null) return; + org.bukkit.World bukkitWorld = resolveWorld(world, bukkitPlayer); + removeBeacon(bukkitPlayer, new Location(bukkitWorld, location.x(), location.y(), location.z())); } public void removeBeacon(org.bukkit.entity.Player player, Location location) { plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { - for (BeaconUtils.BlockChange change : BeaconUtils.resetList(BlockLocation.of(location))) { - Location loc = new Location(player.getWorld(), change.getX(), change.getY(), change.getZ()); + RBlockPos pos = new RBlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + for (BeaconUtils.BlockChange change : BeaconUtils.resetList(pos)) { + Location loc = new Location(location.getWorld(), change.x(), change.y(), change.z()); BlockData data = loc.getBlock().getBlockData(); player.sendBlockChange(loc, data); } }); } + + private static org.bukkit.World resolveWorld(RWorldRef world, org.bukkit.entity.Player fallbackPlayer) { + if (world == null) return fallbackPlayer.getWorld(); + if (world.key() != null) { + try { + var key = org.bukkit.NamespacedKey.fromString(world.key()); + if (key != null) { + var byKey = Bukkit.getWorld(key); + if (byKey != null) return byKey; + } + } catch (Exception ignored) { + } + } + if (world.name() != null) { + var byName = Bukkit.getWorld(world.name()); + if (byName != null) return byName; + } + var byIdentifier = Bukkit.getWorld(world.identifier()); + if (byIdentifier != null) return byIdentifier; + return fallbackPlayer.getWorld(); + } } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitTypes.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitTypes.java index 644fe00..64000e6 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitTypes.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitTypes.java @@ -1,20 +1,14 @@ package de.t14d3.zones.bukkit; import de.t14d3.zones.utils.Types; -import io.papermc.paper.registry.RegistryAccess; -import io.papermc.paper.registry.RegistryKey; -import org.bukkit.Material; -import org.bukkit.block.BlockState; -import org.bukkit.block.Container; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.Powerable; -import org.bukkit.entity.EntityType; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Container; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.state.BlockState; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; public class BukkitTypes extends Types { @@ -24,69 +18,42 @@ public BukkitTypes() { @Override public void populateTypes() { - - blockTypes = Arrays.stream(Material.values()) - .parallel() - .filter(material -> !material.isLegacy() && material.isBlock()) - .flatMap(material -> Stream.of(material.name().toLowerCase(), "!" + material.name().toLowerCase())) - .collect(Collectors.toList()); - - entityTypes = Arrays.stream(EntityType.values()) - .parallel() - .flatMap( - entityType -> Stream.of(entityType.name().toLowerCase(), - "!" + entityType.name().toLowerCase())) - .collect(Collectors.toList()); - allTypes = new ArrayList<>(); - allTypes.addAll(blockTypes); - allTypes.addAll(entityTypes); - - allTypes.addAll(List.of("owner", "admin", "true", "false")); - blockTypes.addAll(List.of("owner", "admin", "true", "false")); - entityTypes.addAll(List.of("owner", "admin", "true", "false")); - - - new Thread(() -> { - containerTypes = Arrays.stream(Material.values()) - .parallel() - .filter(material -> material.isBlock() && !material.isLegacy()) - .flatMap(material -> { - try { - BlockData data = material.createBlockData(); - BlockState state = data.createBlockState(); - return (state instanceof Container) ? Stream.of(material) : Stream.empty(); - } catch (Exception e) { - return Stream.empty(); - } - }) - .flatMap(material -> Stream.of(material.name().toLowerCase(), "!" + material.name().toLowerCase())) - .collect(Collectors.toList()); - containerTypes.addAll(List.of("owner", "admin", "true", "false")); - - redstoneTypes = Arrays.stream(Material.values()) - .parallel() - .filter(material -> material.isBlock() && !material.isLegacy()) - .flatMap(material -> { - try { - BlockData data = material.createBlockData(); - return (data instanceof Powerable) ? Stream.of(material) : Stream.empty(); - } catch (Exception e) { - return Stream.empty(); - } - }) - .flatMap(material -> Stream.of(material.name().toLowerCase(), "!" + material.name().toLowerCase())) - .collect(Collectors.toList()); - redstoneTypes.addAll(List.of("owner", "admin", "true", "false")); - }).start(); + blockTypes = new ArrayList<>(); + entityTypes = new ArrayList<>(); + containerTypes = new ArrayList<>(); + redstoneTypes = new ArrayList<>(); + damageTypes = new ArrayList<>(); + + BuiltInRegistries.BLOCK.forEach(block -> { + ResourceLocation key = BuiltInRegistries.BLOCK.getKey(block); + if (key == null) return; + String id = key.getPath(); + + blockTypes.add(id); + allTypes.add(id); + + BlockState state = block.defaultBlockState(); + if (state.hasBlockEntity() && block instanceof EntityBlock entityBlock) { + var blockEntity = entityBlock.newBlockEntity(BlockPos.ZERO, state); + if (blockEntity instanceof Container) { + containerTypes.add(id); + } + } + + if (state.isSignalSource()) { + redstoneTypes.add(id); + } + }); + + BuiltInRegistries.ENTITY_TYPE.forEach(entityType -> { + ResourceLocation key = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + if (key == null) return; + String id = key.getPath(); + + entityTypes.add(id); + allTypes.add(id); + }); - damageTypes = RegistryAccess.registryAccess().getRegistry(RegistryKey.DAMAGE_TYPE).stream() - .parallel() - .flatMap(damageType -> Stream.of( - damageType.getTranslationKey().toLowerCase(), - "!" + damageType.getTranslationKey().toLowerCase() - )) - .collect(Collectors.toList()); - damageTypes.addAll(List.of("owner", "admin", "true", "false")); } } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java index 58bf5a1..6ab49f1 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java @@ -1,25 +1,35 @@ package de.t14d3.zones.bukkit; import com.sk89q.worldedit.WorldEdit; +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.events.GameEventBridge; +import de.t14d3.rapunzellib.events.GameEventBus; +import de.t14d3.rapunzellib.events.paper.PaperGameEventBridgeInstaller; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.platform.paper.PaperRapunzelBootstrap; import de.t14d3.zones.RegionManager; import de.t14d3.zones.Zones; import de.t14d3.zones.ZonesPlatform; import de.t14d3.zones.bukkit.commands.RootCommand; -import de.t14d3.zones.bukkit.listeners.*; -import de.t14d3.zones.integrations.FAWEIntegration; -import de.t14d3.zones.integrations.PlaceholderAPI; -import de.t14d3.zones.integrations.WorldEditSession; -import de.t14d3.zones.permissions.CacheUtils; +import de.t14d3.zones.bukkit.integrations.FAWEIntegration; +import de.t14d3.zones.bukkit.integrations.PlaceholderAPI; +import de.t14d3.zones.bukkit.integrations.WorldEditSession; +import de.t14d3.zones.rapunzellib.ZonesRapunzelHooks; import de.t14d3.zones.utils.DebugLoggerManager; -import de.t14d3.zones.utils.Messages; import de.t14d3.zones.utils.Types; import de.t14d3.zones.utils.Utils; +import de.t14d3.zones.visuals.ParticleHandler; import dev.jorel.commandapi.CommandAPI; -import dev.jorel.commandapi.CommandAPIBukkitConfig; +import dev.jorel.commandapi.CommandAPIPaperConfig; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.java.JavaPlugin; +/** + * Bukkit/Paper plugin entrypoint for Zones. + * + *

Bootstraps the core {@link Zones} instance and wires Paper/Bukkit integrations (commands, events, visuals).

+ */ public final class ZonesBukkit extends JavaPlugin { private static ZonesBukkit instance; @@ -28,17 +38,19 @@ public final class ZonesBukkit extends JavaPlugin { private Zones zones; private BukkitPermissionManager permissionManager; private RegionManager regionManager; - private Messages messages; private DebugLoggerManager debugLogger; private BukkitTypes types; + /** + * Returns the active plugin instance. + */ public static ZonesBukkit getInstance() { return instance; } @Override public void onLoad() { - + PaperRapunzelBootstrap.bootstrap(this); this.platform = new BukkitPlatform(this); instance = this; @@ -47,34 +59,36 @@ public void onLoad() { this.zones = new Zones(platform); this.debug = zones.debug; // Configure CommandAPI - CommandAPI.onLoad(new CommandAPIBukkitConfig(this) + CommandAPI.onLoad(new CommandAPIPaperConfig(this) .verboseOutput(debug) - .skipReloadDatapacks(true) .silentLogs(!debug) - .usePluginNamespace() ); this.regionManager = zones.getRegionManager(); - this.messages = zones.getMessages(); this.debugLogger = zones.getDebugLogger(); - this.permissionManager = new BukkitPermissionManager(zones); + this.permissionManager = (BukkitPermissionManager) zones.getPermissionManager(); } @Override public void onEnable() { + + GameEventBus bus = new GameEventBus(Rapunzel.context().scheduler(), Rapunzel.context().logger()); + Rapunzel.context().services().register(GameEventBus.class, bus); + + GameEventBridge bridge = new PaperGameEventBridgeInstaller().install(Rapunzel.context(), bus, this); + Rapunzel.context().services().register(GameEventBridge.class, bridge); + + ZonesRapunzelHooks.install(zones); + CommandAPI.onEnable(); this.regionManager.loadRegions(); this.saveDefaultConfig(); - // Register listeners - this.getServer().getPluginManager().registerEvents(new PlayerEventListener(this), this); - this.getServer().getPluginManager().registerEvents(new PlayerQuitListener(zones), this); - this.getServer().getPluginManager().registerEvents(new WorldEventListener(zones), this); - this.getServer().getPluginManager().registerEvents(new ChunkEventListener(), this); - ExplosivesListener explosivesListener = new ExplosivesListener(this); - BlockEventListener blockEventListener = new BlockEventListener(zones); + // Create particle handler/scheduler + var particleHandler = new ParticleHandler(zones); + particleHandler.particleScheduler(); // Register mode permissions for (Utils.SelectionMode mode : Utils.SelectionMode.values()) { @@ -86,8 +100,8 @@ public void onEnable() { permissionManager.getPermissions().forEach(permission -> { this.getServer().getPluginManager().addPermission( - new Permission(permission.getName(), permission.getDescription(), - permission.getLevel() >= 2 ? PermissionDefault.OP : PermissionDefault.TRUE) + new Permission(permission.name(), permission.description(), + permission.level() >= 2 ? PermissionDefault.OP : PermissionDefault.TRUE) ); }); @@ -114,7 +128,6 @@ public void onEnable() { WorldEdit.getInstance().getEventBus().register(new WorldEditSession(zones)); getLogger().info("WorldEdit Integration enabled."); } - CacheUtils.getInstance().startCacheRunnable(); RootCommand rootCommand = new RootCommand(this); @@ -125,42 +138,68 @@ public void onEnable() { public void onDisable() { // Save regions to datasource before plugin shutdown regionManager.saveRegions(); - regionManager.regions().clear(); + regionManager.clearAll(); CommandAPI.onDisable(); regionManager.getDataSourceManager().close(); + Rapunzel.shutdown(this); getLogger().info("Zones plugin is disabling and regions are saved."); } // Getters + + /** + * Returns the shared region manager. + */ public RegionManager getRegionManager() { return regionManager; } + /** + * Returns the permission manager implementation for Bukkit. + */ public BukkitPermissionManager getPermissionManager() { return permissionManager; } + /** + * Returns the configured region saving mode. + */ public Utils.SavingModes getSavingMode() { return Utils.SavingModes.fromString(this.getConfig().getString("zone-saving.mode", "MODIFIED")); } + /** + * Returns the debug logger facility. + */ public DebugLoggerManager getDebugLogger() { return debugLogger; } - public Messages getMessages() { - return messages; + /** + * Returns the message formatter service used by Zones. + */ + public MessageFormatService getMessages() { + return zones.getMessages(); } + /** + * Returns the platform bridge. + */ public ZonesPlatform getPlatform() { return platform; } + /** + * Returns the core Zones instance. + */ public Zones getZones() { return zones; } + /** + * Returns the types registry used by Zones. + */ public Types getTypes() { return types; } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CancelCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CancelCommand.java index dec5247..2ced00b 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CancelCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CancelCommand.java @@ -1,18 +1,19 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.utils.Messages; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import dev.jorel.commandapi.CommandAPICommand; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; public class CancelCommand { - private final MiniMessage mm = MiniMessage.miniMessage(); private final RegionManager regionManager; - private Messages messages; + private MessageFormatService messages; private ZonesBukkit plugin; public CancelCommand(ZonesBukkit plugin) { @@ -25,20 +26,23 @@ public CancelCommand(ZonesBukkit plugin) { .withPermission("zones.cancel") .executes((sender, args) -> { if (sender instanceof Player player) { - de.t14d3.zones.objects.Player zplayer = PlayerRepository.get(player.getUniqueId()); - if (zplayer.getSelection() != null) { - plugin.getPlatform().removeBeacon(zplayer, zplayer.getSelection().getWorld(), - zplayer.getSelection().getMin()); - plugin.getPlatform().removeBeacon(zplayer, zplayer.getSelection().getWorld(), - zplayer.getSelection().getMax()); + RPlayer rPlayer = RPlayer.wrap(player).orElse(null); + if (rPlayer == null) return; - zplayer.setSelection(null); - player.sendMessage(mm.deserialize(messages.get("commands.cancel.success"))); + Box selection = rPlayer.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + if (selection != null) { + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : rPlayer.worldOrThrow() + .ref(); + plugin.getPlatform().removeBeacon(rPlayer, selectionWorld, selection.getMin()); + plugin.getPlatform().removeBeacon(rPlayer, selectionWorld, selection.getMax()); + rPlayer.extras().remove(ZonesExtraKeys.SELECTION); + rPlayer.extras().remove(ZonesExtraKeys.SELECTION_CREATING); + player.sendMessage(messages.component("commands.cancel.success")); } else { - player.sendMessage(mm.deserialize(messages.get("commands.cancel.success"))); + player.sendMessage(messages.component("commands.cancel.success")); } } else { - sender.sendMessage(mm.deserialize(messages.get("commands.only-player"))); + sender.sendMessage(messages.component("commands.only-player")); } }); -} \ No newline at end of file +} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CreateCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CreateCommand.java index 3a8c676..95c4e11 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CreateCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CreateCommand.java @@ -1,30 +1,22 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; import de.t14d3.zones.objects.Box; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.objects.RegionFlagEntry; -import de.t14d3.zones.objects.World; -import de.t14d3.zones.utils.Messages; -import de.t14d3.zones.utils.Utils; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import dev.jorel.commandapi.CommandAPICommand; import dev.jorel.commandapi.arguments.StringArgument; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class CreateCommand { - private final MiniMessage mm = MiniMessage.miniMessage(); private RegionManager regionManager; - private Messages messages; + private MessageFormatService messages; private ZonesBukkit plugin; public CreateCommand(ZonesBukkit plugin) { @@ -38,41 +30,53 @@ public CreateCommand(ZonesBukkit plugin) { .withOptionalArguments(new StringArgument("name")) .executes((sender, args) -> { if (sender instanceof Player player) { - de.t14d3.zones.objects.Player zplayer = PlayerRepository.get(player.getUniqueId()); - if (zplayer.getSelection() == null) { - zplayer.setSelection(new Box(null, null, World.of(player.getWorld()), false)); - zplayer.setSelectionCreating(true); - sender.sendMessage(mm.deserialize(messages.get("commands.create.click-corners"))); + RPlayer rPlayer = RPlayer.wrap(player).orElse(null); + if (rPlayer == null) return; + + Box selection = rPlayer.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + if (selection == null) { + var world = rPlayer.worldOrThrow().ref(); + RBlockPos pos = new RBlockPos(player.getLocation().getBlockX(), + player.getLocation().getBlockY(), player.getLocation().getBlockZ()); + selection = new Box(pos, pos, world, false); + rPlayer.extras().put(ZonesExtraKeys.SELECTION, selection); + rPlayer.extras().put(ZonesExtraKeys.SELECTION_CREATING, true); + sender.sendMessage(messages.component("commands.create.click-corners")); return; } - Box selection = zplayer.getSelection(); if (selection.getMin() != null && selection.getMax() != null) { if (regionManager.overlapsExistingRegion(selection) && !sender.hasPermission( "zones.create.overlap")) { - sender.sendMessage(mm.deserialize(messages.get("commands.create.overlap"))); + sender.sendMessage(messages.component("commands.create.overlap")); return; } - Map> members = new HashMap<>(); - List ownerPermissions = new ArrayList<>(); - ownerPermissions.add(new RegionFlagEntry("role", "owner", false)); - members.put(player.getUniqueId().toString(), ownerPermissions); + RegionPermissions permissions = new RegionPermissions(); + RegionKey key = RegionKey.generate(); - regionManager.createNewRegion(key.toString(), selection.getMin(), - selection.getMax(), selection.getWorld(), members, key, null, 0); - Utils.SelectionMode mode = Utils.SelectionMode.getPlayerMode(zplayer); + String name = (String) args.get("name"); + if (name == null || name.isBlank()) name = key.toString(); + var created = regionManager.createNewRegion(name, selection.getMin(), + selection.getMax(), selection.getWorld(), permissions, key, null, 0); + if (created != null) { + regionManager.withWorldWriteLock(created.getWorld(), () -> { + created.getMembership().player(player.getUniqueId()).roles().allow("owner"); + regionManager.saveRegion(created.getKey(), created); + }); + } - plugin.getPlatform().removeBeacon(zplayer, selection.getWorld(), selection.getMin()); - plugin.getPlatform().removeBeacon(zplayer, selection.getWorld(), selection.getMax()); - sender.sendMessage( - mm.deserialize(messages.get("commands.create.success"), - parsed("region", key.toString()))); - zplayer.setSelection(null); - zplayer.setSelectionCreating(false); + plugin.getPlatform().removeBeacon(rPlayer, selection.getWorld(), selection.getMin()); + plugin.getPlatform().removeBeacon(rPlayer, selection.getWorld(), selection.getMax()); + sender.sendMessage(messages.component( + "commands.create.success", + Placeholders.builder().string("region", key.toString()).build() + )); + rPlayer.extras().remove(ZonesExtraKeys.SELECTION); + rPlayer.extras().put(ZonesExtraKeys.SELECTION_CREATING, false); } else { - sender.sendMessage(mm.deserialize(messages.get("commands.create.click-corners"))); + sender.sendMessage(messages.component("commands.create.click-corners")); } } else { - sender.sendMessage(mm.deserialize(messages.get("commands.only-player"))); + sender.sendMessage(messages.component("commands.only-player")); } }); } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CustomArgument.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CustomArgument.java deleted file mode 100644 index 44a5b46..0000000 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CustomArgument.java +++ /dev/null @@ -1,81 +0,0 @@ -package de.t14d3.zones.bukkit.commands; - -import de.t14d3.zones.Region; -import de.t14d3.zones.RegionKey; -import de.t14d3.zones.Zones; -import de.t14d3.zones.utils.Messages; -import dev.jorel.commandapi.BukkitTooltip; -import dev.jorel.commandapi.StringTooltip; -import dev.jorel.commandapi.arguments.Argument; -import dev.jorel.commandapi.arguments.ArgumentSuggestions; -import dev.jorel.commandapi.arguments.StringArgument; -import net.kyori.adventure.text.minimessage.MiniMessage; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -public class CustomArgument { - private static final MiniMessage mm = MiniMessage.miniMessage(); - - public static Argument region(String name, String permission, MemberType type) { - return new dev.jorel.commandapi.arguments.CustomArgument(new StringArgument(name), info -> { - Region region = Zones.getInstance().getRegionManager().regions() - .get(RegionKey.fromString(info.input()).getValue()); - if (region != null) { - if (info.sender() instanceof org.bukkit.entity.Player player) { - switch (type) { - case OWNER: - if (region.isOwner(player.getUniqueId())) { - return region; - } - break; - case ADMIN: - if (region.isAdmin(player.getUniqueId())) { - return region; - } - break; - case MEMBER: - if (region.isMember(player.getUniqueId())) { - return region; - } - break; - default: - return region; - } - } else if (info.sender().hasPermission(permission)) { - return region; - } - } - return null; - }).replaceSuggestions(ArgumentSuggestions.stringsWithTooltipsAsync(info -> { - return CompletableFuture.supplyAsync(() -> { - List regions = new ArrayList<>(); - if (info.sender().hasPermission(permission)) { - regions.addAll(Zones.getInstance().getRegionManager().regions().values()); - } else if (info.sender() instanceof org.bukkit.entity.Player player) { - for (Region region : Zones.getInstance().getRegionManager().regions().values()) { - if (region.isMember(player.getUniqueId())) { - regions.add(region); - } - } - } - StringTooltip[] suggestions = new StringTooltip[regions.size()]; - int i = 0; - for (Region region : regions) { - suggestions[i++] = StringTooltip.ofMessage(region.getKey().toString(), - BukkitTooltip.messageFromAdventureComponent( - Messages.regionInfo(region, false))); - } - return suggestions; - }); - })); - } - - public enum MemberType { - OWNER, - ADMIN, - MEMBER, - ANY - } -} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/DeleteCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/DeleteCommand.java index cfe9569..20543f1 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/DeleteCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/DeleteCommand.java @@ -1,20 +1,18 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.utils.Messages; +import de.t14d3.zones.bukkit.commands.utils.CustomArgument; import dev.jorel.commandapi.CommandAPICommand; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class DeleteCommand { - private final MiniMessage mm = MiniMessage.miniMessage(); private RegionManager regionManager; - private Messages messages; + private MessageFormatService messages; private final ZonesBukkit plugin; public DeleteCommand(ZonesBukkit plugin) { @@ -29,19 +27,21 @@ public DeleteCommand(ZonesBukkit plugin) { .executes((sender, args) -> { Region region = regionManager.regions().get(RegionKey.fromString(args.getRaw("key")).getValue()); if (region == null) { - sender.sendMessage(mm.deserialize(messages.get("commands.invalid-region"))); + sender.sendMessage(messages.component("commands.invalid-region")); return; } if (!sender.hasPermission("zones.delete.other")) { - if (sender instanceof Player player && !region.isAdmin(player.getUniqueId())) { - sender.sendMessage(mm.deserialize(messages.get("commands.invalid-region"))); + if (sender instanceof Player player + && !regionManager.withWorldReadLock(region.getWorld(), + () -> region.isAdmin(player.getUniqueId()))) { + sender.sendMessage(messages.component("commands.invalid-region")); return; } } regionManager.deleteRegion(region.getKey()); sender.sendMessage( - mm.deserialize(messages.get("commands.delete.success"), - parsed("region", region.getKey().toString()))); + messages.component("commands.delete.success", + Placeholders.builder().string("region", region.getKey().toString()).build())); regionManager.triggerSave(); }); } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java index 3c7f26e..19b944c 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java @@ -1,11 +1,13 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; +import de.t14d3.zones.bukkit.commands.utils.CustomArgument; import de.t14d3.zones.objects.Direction; -import de.t14d3.zones.utils.Messages; import dev.jorel.commandapi.BukkitTooltip; import dev.jorel.commandapi.CommandAPICommand; import dev.jorel.commandapi.StringTooltip; @@ -13,21 +15,16 @@ import dev.jorel.commandapi.arguments.BooleanArgument; import dev.jorel.commandapi.arguments.IntegerArgument; import dev.jorel.commandapi.arguments.StringArgument; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.concurrent.CompletableFuture; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class ExpandCommand { private final ZonesBukkit plugin; private RegionManager regionManager; - private Messages messages; - private final MiniMessage mm = MiniMessage.miniMessage(); + private MessageFormatService messages; public ExpandCommand(ZonesBukkit plugin) { this.plugin = plugin; @@ -52,7 +49,7 @@ public ExpandCommand(ZonesBukkit plugin) { for (String direction : directions) { suggestions[i++] = StringTooltip.ofMessage(direction, BukkitTooltip.messageFromAdventureComponent( - mm.deserialize(messages.get("commands.expand.direction")))); + messages.component("commands.expand.direction"))); } return suggestions; }); @@ -68,31 +65,30 @@ public ExpandCommand(ZonesBukkit plugin) { if (args.getRaw("direction") == null) { if (sender instanceof Player player) { direction = Direction.fromYaw(player.getLocation().getYaw()); - if (region == null || !region.isAdmin(player.getUniqueId())) { - sender.sendMessage(mm.deserialize(messages.get("commands.invalid-region"))); + if (region == null + || !regionManager.withWorldReadLock(region.getWorld(), + () -> region.isAdmin(player.getUniqueId()))) { + sender.sendMessage(messages.component("commands.invalid-region")); return; } } else { - sender.sendMessage(mm.deserialize(messages.get("commands.invalid"))); + sender.sendMessage(messages.component("commands.invalid")); return; } } else { direction = Direction.valueOf(args.getRaw("direction").toUpperCase()); } int amount = Integer.parseInt(args.getRaw("amount")); - boolean allowOverlap = false; - if (args.getRaw("overlap") == null) { - allowOverlap = Objects.equals(args.getRaw("overlap"), "overlap") && sender.hasPermission( - "zones.expand.overlap"); - } + Boolean overlapArg = (Boolean) args.get("overlap"); + boolean allowOverlap = Boolean.TRUE.equals(overlapArg) && sender.hasPermission("zones.expand.overlap"); if (regionManager.expandBounds(region, direction, amount, allowOverlap)) { sender.sendMessage( - mm.deserialize(messages.get("commands.expand.success"), - parsed("region", regionKey.toString()))); + messages.component("commands.expand.success", + Placeholders.builder().string("region", regionKey.toString()).build())); } else { sender.sendMessage( - mm.deserialize(messages.get("commands.expand.fail"), - parsed("region", regionKey.toString()))); + messages.component("commands.expand.fail", + Placeholders.builder().string("region", regionKey.toString()).build())); } } ); diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/FindCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/FindCommand.java index 90c0989..8ca4a3b 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/FindCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/FindCommand.java @@ -1,5 +1,6 @@ package de.t14d3.zones.bukkit.commands; +/* import de.t14d3.zones.bukkit.ZonesBukkit; import de.t14d3.zones.objects.Player; import dev.jorel.commandapi.CommandAPICommand; @@ -29,3 +30,37 @@ public FindCommand(ZonesBukkit plugin) { } }); } +*/ + +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.bukkit.ZonesBukkit; +import dev.jorel.commandapi.CommandAPICommand; + +public class FindCommand { + private ZonesBukkit plugin; + + public FindCommand(ZonesBukkit plugin) { + this.plugin = plugin; + } + + public CommandAPICommand find = new CommandAPICommand("find") + .withPermission("zones.find") + .executes((sender, args) -> { + if (sender instanceof org.bukkit.entity.Player nativePlayer) { + RPlayer player = RPlayer.wrap(nativePlayer).orElse(null); + if (player == null) return; + + if (plugin.getZones().getFindBossbar().players.containsKey(player)) { + var bar = plugin.getZones().getFindBossbar().players.get(player); + if (bar != null) { + player.audience().hideBossBar(bar); + } + plugin.getZones().getFindBossbar().players.remove(player); + } else { + plugin.getZones().getFindBossbar().players.put(player, null); + } + } else { + sender.sendMessage(plugin.getMessages().component("commands.only-player")); + } + }); +} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ImportCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ImportCommand.java index 2174a4b..e13e4c4 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ImportCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ImportCommand.java @@ -1,24 +1,20 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.integrations.WorldGuardImporter; -import de.t14d3.zones.utils.Messages; +import de.t14d3.zones.bukkit.integrations.WorldGuardImporter; import dev.jorel.commandapi.BukkitTooltip; import dev.jorel.commandapi.CommandAPICommand; import dev.jorel.commandapi.StringTooltip; import dev.jorel.commandapi.arguments.ArgumentSuggestions; import dev.jorel.commandapi.arguments.StringArgument; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import java.util.concurrent.CompletableFuture; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class ImportCommand { private ZonesBukkit plugin; - private Messages messages; - private final MiniMessage mm = MiniMessage.miniMessage(); + private MessageFormatService messages; public ImportCommand(ZonesBukkit plugin) { this.plugin = plugin; @@ -35,20 +31,20 @@ public ImportCommand(ZonesBukkit plugin) { StringTooltip[] suggestions = new StringTooltip[1]; suggestions[0] = StringTooltip.ofMessage("worldguard", BukkitTooltip.messageFromAdventureComponent( - Component.text("Imports regions from WorldGuard"))); + messages.component("commands.import.worldguard-tooltip"))); return suggestions; }); }))) .executes((sender, args) -> { if (args.getRaw("source").equalsIgnoreCase("worldguard")) { if (plugin.getServer().getPluginManager().getPlugin("WorldGuard") == null) { - sender.sendMessage(mm.deserialize(messages.get("commands.import.not-loaded"), - parsed("plugin", "WorldGuard"))); + sender.sendMessage(messages.component("commands.import.not-loaded", + Placeholders.builder().string("plugin", "WorldGuard").build())); return; } WorldGuardImporter worldGuardImporter = new WorldGuardImporter(plugin); worldGuardImporter.importRegions(); - sender.sendMessage(mm.deserialize(messages.get("commands.import.success"))); + sender.sendMessage(messages.component("commands.import.success")); } } ); diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/InfoCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/InfoCommand.java index 186a0fe..15a922b 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/InfoCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/InfoCommand.java @@ -1,11 +1,13 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; -import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.World; +import de.t14d3.zones.bukkit.commands.utils.CustomArgument; import de.t14d3.zones.utils.Messages; import dev.jorel.commandapi.CommandAPICommand; import org.bukkit.entity.Player; @@ -14,7 +16,7 @@ public class InfoCommand { private RegionManager regionManager; - private Messages messages; + private MessageFormatService messages; public InfoCommand(ZonesBukkit plugin) { this.regionManager = plugin.getRegionManager(); @@ -25,36 +27,36 @@ public InfoCommand(ZonesBukkit plugin) { .withOptionalArguments(CustomArgument.region("key", "zones.info.other", CustomArgument.MemberType.MEMBER)) .executes((sender, args) -> { List regions; - Player player = null; - if (sender instanceof Player temp) { - player = temp; - } - if (args.get("key") == null) { + Player player = sender instanceof Player ? (Player) sender : null; + Region region = (Region) args.get("key"); + if (region == null) { if (player != null) { - regions = regionManager.getRegionsAt(BlockLocation.of(player.getLocation()), - World.of(player.getWorld())); + RPlayer rPlayer = RPlayer.wrap(player).orElse(null); + if (rPlayer == null) return; + RBlockPos pos = rPlayer.locationOrThrow().blockPos(); + RWorldRef world = rPlayer.worldOrThrow().ref(); + regions = regionManager.getRegionsAt(pos, world); + if (regions.isEmpty()) { + sender.sendMessage(messages.component("region.none-found")); + return; + } } else { - sender.sendMessage(messages.getCmp("commands.invalid-region")); + sender.sendMessage(messages.component("commands.invalid-region")); return; } } else { - try { - regions = List.of(RegionManager.getRegion(RegionKey.fromString((String) args.get("key")))); - } catch (Exception e) { - sender.sendMessage(messages.getCmp("commands.invalid-region")); - return; - } - } - - if (regions.isEmpty()) { - sender.sendMessage(messages.getCmp("commands.invalid-region")); - return; + regions = List.of(region); } - for (Region region : regions) { + for (Region region_ : regions) { if (sender.hasPermission("zones.info.other")) { - sender.sendMessage(Messages.regionInfo(region, true)); - } else if (player != null && region.isMember(player.getUniqueId())) { - sender.sendMessage(Messages.regionInfo(region, region.isAdmin(player.getUniqueId()))); + sender.sendMessage(regionManager.withWorldReadLock(region_.getWorld(), + () -> Messages.regionInfo(region_, true))); + } else if (player != null) { + var msg = regionManager.withWorldReadLock(region_.getWorld(), () -> { + if (!region_.isMember(player.getUniqueId())) return null; + return Messages.regionInfo(region_, region_.isAdmin(player.getUniqueId())); + }); + if (msg != null) sender.sendMessage(msg); } } }); diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ListCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ListCommand.java index 7a2a8cf..d591c66 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ListCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ListCommand.java @@ -1,5 +1,7 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.Region; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; @@ -9,17 +11,13 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; import java.util.List; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class ListCommand { - private final MiniMessage mm = MiniMessage.miniMessage(); private RegionManager regionManager; - private Messages messages; + private MessageFormatService messages; public ListCommand(ZonesBukkit plugin) { this.regionManager = plugin.getRegionManager(); @@ -36,27 +34,33 @@ public ListCommand(ZonesBukkit plugin) { page = 1; } - Player player; - if (sender instanceof Player temp) { - player = temp; - } else { - player = null; - } - List regions = regionManager.regions().values().parallelStream() - .filter(region -> perm || (player != null && region.isMember(player.getUniqueId()))).toList(); + Player player = sender instanceof Player temp ? temp : null; + var uuid = player != null ? player.getUniqueId() : null; + List regions = regionManager.regions().values().stream() + .filter(region -> perm || (uuid != null && regionManager.withWorldReadLock(region.getWorld(), + () -> region.isMember(uuid)))) + .toList(); if (regions.isEmpty()) { - sender.sendMessage(messages.getCmp("region.none-found")); + sender.sendMessage(messages.component("region.none-found")); + return; + } + int start = (page - 1) * 10; + if (start >= regions.size()) { + sender.sendMessage(messages.component("region.none-found")); return; } - regions = regions.subList((page - 1) * 10, Math.min(regions.size(), page * 10)); + regions = regions.subList(start, Math.min(regions.size(), start + 10)); Component[] msgs = new Component[regions.size()]; int i = 0; for (Region region : regions) { + Component hoverText = regionManager.withWorldReadLock(region.getWorld(), () -> { + boolean showMembers = perm || (uuid != null && region.isAdmin(uuid)); + return Messages.regionInfo(region, showMembers); + }); msgs[i] = Component.newline() - .append(mm.deserialize(messages.get("region.info.name"), parsed("name", region.getName()), - parsed("key", region.getKey().toString())) - .hoverEvent(HoverEvent.showText(Messages.regionInfo(region, - (perm || region.isAdmin(player.getUniqueId()))))) + .append(messages.component("region.info.name", + Placeholders.builder().string("name", region.getName()).build()) + .hoverEvent(HoverEvent.showText(hoverText)) .clickEvent(ClickEvent.runCommand("/zone info " + region.getKey()))); i++; } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/LoadCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/LoadCommand.java index 7ee0672..4793226 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/LoadCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/LoadCommand.java @@ -1,18 +1,15 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.utils.Messages; import dev.jorel.commandapi.CommandAPICommand; -import net.kyori.adventure.text.minimessage.MiniMessage; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; public class LoadCommand { private final ZonesBukkit plugin; private RegionManager regionManager; - private Messages messages; - private final MiniMessage mm = MiniMessage.miniMessage(); + private MessageFormatService messages; public LoadCommand(ZonesBukkit plugin) { this.plugin = plugin; @@ -26,6 +23,7 @@ public LoadCommand(ZonesBukkit plugin) { regionManager.loadRegions(); int count = regionManager.regions().size(); sender.sendMessage( - mm.deserialize(messages.get("commands.load"), parsed("count", String.valueOf(count)))); + messages.component("commands.load", + Placeholders.builder().string("count", String.valueOf(count)).build())); }); } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ModeCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ModeCommand.java index 4fde3bc..ec6c7c2 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ModeCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ModeCommand.java @@ -1,18 +1,20 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.bukkit.ZonesBukkit; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; +import de.t14d3.zones.utils.Utils; import dev.jorel.commandapi.CommandAPICommand; import dev.jorel.commandapi.StringTooltip; import dev.jorel.commandapi.arguments.ArgumentSuggestions; import dev.jorel.commandapi.arguments.StringArgument; import org.bukkit.entity.Player; -import org.bukkit.persistence.PersistentDataContainer; -import org.bukkit.persistence.PersistentDataType; import java.util.concurrent.CompletableFuture; public class ModeCommand { - private final ZonesBukkit plugin; + private ZonesBukkit plugin; public ModeCommand(ZonesBukkit plugin) { this.plugin = plugin; @@ -32,13 +34,16 @@ public ModeCommand(ZonesBukkit plugin) { }))) .executes((sender, args) -> { if (sender instanceof Player player) { - PersistentDataContainer pdc = player.getPersistentDataContainer(); + RPlayer rPlayer = RPlayer.wrap(player).orElse(null); + if (rPlayer == null) return; if (args.getRaw("mode").equalsIgnoreCase("2D")) { - pdc.set(new org.bukkit.NamespacedKey("zones", "mode"), PersistentDataType.STRING, "2D"); - sender.sendMessage("2D Selection Mode Enabled"); + rPlayer.extras().put(ZonesExtraKeys.SELECTION_MODE, Utils.SelectionMode.CUBOID_2D.name()); + sender.sendMessage(plugin.getMessages().component("commands.mode.set", + Placeholders.builder().string("mode", "2D").build())); } else if (args.getRaw("mode").equalsIgnoreCase("3D")) { - pdc.set(new org.bukkit.NamespacedKey("zones", "mode"), PersistentDataType.STRING, "3D"); - sender.sendMessage("3D Selection Mode Enabled"); + rPlayer.extras().put(ZonesExtraKeys.SELECTION_MODE, Utils.SelectionMode.CUBOID_3D.name()); + sender.sendMessage(plugin.getMessages().component("commands.mode.set", + Placeholders.builder().string("mode", "3D").build())); } } }); diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/PermCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/PermCommand.java new file mode 100644 index 0000000..65b1584 --- /dev/null +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/PermCommand.java @@ -0,0 +1,664 @@ +package de.t14d3.zones.bukkit.commands; + +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.Region; +import de.t14d3.zones.RegionManager; +import de.t14d3.zones.bukkit.ZonesBukkit; +import de.t14d3.zones.bukkit.commands.utils.CustomArgument; +import de.t14d3.zones.objects.Flag; +import de.t14d3.zones.permissions.RegionMembership; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.permissions.flags.FlagValueKind; +import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.permissions.subjects.GroupSubject; +import de.t14d3.zones.permissions.subjects.PlayerSubject; +import de.t14d3.zones.permissions.subjects.SubjectRef; +import de.t14d3.zones.permissions.subjects.Subjects; +import de.t14d3.zones.utils.TypeKeys; +import dev.jorel.commandapi.BukkitTooltip; +import dev.jorel.commandapi.CommandAPICommand; +import dev.jorel.commandapi.StringTooltip; +import dev.jorel.commandapi.arguments.ArgumentSuggestions; +import dev.jorel.commandapi.arguments.StringArgument; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +/** + * Bukkit implementation of the {@code /zone perm} command tree. + * + *

Uses explicit subcommands (allow/deny/unset/clear). For target-based flags, the {@code pattern} supports + * {@code *} wildcards (glob, not regex). For convenience, {@code all} is treated as {@code *}.

+ */ +public class PermCommand { + private final RegionManager regionManager; + private final MessageFormatService messages; + private final ZonesBukkit plugin; + + /** + * Creates a new permission command tree builder. + * + * @param plugin owning plugin instance + */ + public PermCommand(ZonesBukkit plugin) { + this.plugin = plugin; + this.regionManager = plugin.getRegionManager(); + this.messages = plugin.getMessages(); + } + + /** + * Root {@code /zone perm} command with all subcommands attached. + */ + public CommandAPICommand perm = new CommandAPICommand("perm") + .withPermission("zones.set") + .withSubcommand(allow()) + .withSubcommand(deny()) + .withSubcommand(unset()) + .withSubcommand(clear()); + + private CommandAPICommand allow() { + return new CommandAPICommand("allow") + .withPermission("zones.set") + .withArguments( + CustomArgument.region("key", "zones.set.other", CustomArgument.MemberType.OWNER), + new StringArgument("target").replaceSuggestions( + ArgumentSuggestions.stringsWithTooltipsAsync(info -> + CompletableFuture.supplyAsync(() -> suggestTargets(info.sender(), + (Region) info.previousArgs().get("key"))))), + new StringArgument("flag").replaceSuggestions( + ArgumentSuggestions.stringsWithTooltipsAsync(info -> + CompletableFuture.supplyAsync(this::suggestFlags))), + new StringArgument("pattern").replaceSuggestions(ArgumentSuggestions.stringsAsync(info -> + CompletableFuture.supplyAsync( + () -> suggestPatterns((String) info.previousArgs().get("flag"))))) + ) + .executes((sender, args) -> { + Region region = (Region) args.get("key"); + if (region == null) { + sender.sendMessage(messages.component("commands.invalid-region")); + return; + } + + String rawTarget = args.getRaw("target"); + String flagName = (String) args.get("flag"); + String rawPattern = (String) args.get("pattern"); + + Flag flag = Flags.getFlag(flagName); + if (flag == null) { + sender.sendMessage(messages.component("commands.invalid-permission")); + return; + } + + if (sender instanceof Player bukkitPlayer) { + RPlayer rPlayer = RPlayer.wrap(bukkitPlayer).orElse(null); + if (rPlayer != null && !flag.canSet(rPlayer, plugin.getPlatform())) { + sender.sendMessage(messages.component("commands.invalid-permission")); + return; + } + } + + SubjectRef subject = parseTargetSubject(rawTarget); + if (subject == null) { + sender.sendMessage(messages.component("commands.perm.invalid-target")); + return; + } + + FlagValueKind kind = flag.valueKind(); + if (kind != FlagValueKind.TARGET_DECISION && kind != FlagValueKind.STRING_SET) { + sender.sendMessage(messages.component("commands.perm.invalid-flag-kind", + Placeholders.builder().string("permission", flag.name()).build())); + return; + } + + regionManager.withWorldWriteLock(region.getWorld(), () -> { + if (kind == FlagValueKind.TARGET_DECISION) { + RegionPermissions perms = region.getPermissions(); + String pattern = normalizeTargetDecisionValue(rawPattern); + RegionPermissions.TargetDecisionValue v = (RegionPermissions.TargetDecisionValue) perms + .subject(subject) + .getOrCreate(flag.id(), FlagValueKind.TARGET_DECISION); + v.allow(pattern); + cleanupEmptyValue(perms, subject, flag.id()); + } else { + String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); + if (flag == Flags.ROLE) { + if (!(subject instanceof PlayerSubject p)) return; + RegionMembership membership = region.getMembership(); + membership.player(p.uuid()).roles().allow(value); + cleanupEmptyMembership(membership, p.uuid()); + } else if (flag == Flags.GROUP) { + RegionMembership membership = region.getMembership(); + if (subject instanceof PlayerSubject p) { + membership.player(p.uuid()).groups().allow(value); + cleanupEmptyMembership(membership, p.uuid()); + } else if (subject instanceof GroupSubject g) { + membership.group(g.name()).includes().allow(value); + cleanupEmptyGroupDefinition(membership, g.name()); + } else { + return; + } + } else { + RegionPermissions perms = region.getPermissions(); + RegionPermissions.StringSetValue v = (RegionPermissions.StringSetValue) perms + .subject(subject) + .getOrCreate(flag.id(), FlagValueKind.STRING_SET); + v.allow(value); + cleanupEmptyValue(perms, subject, flag.id()); + } + } + + regionManager.saveRegion(region.getKey(), region); + }); + + plugin.getZones().getPermissionManager().invalidateAll(); + + sender.sendMessage(messages.component( + "commands.perm.allow", + Placeholders.builder() + .string("region", region.getKey().toString()) + .string("target", rawTarget) + .string("permission", flag.name()) + .string("value", rawPattern) + .build() + )); + }); + } + + private CommandAPICommand deny() { + return new CommandAPICommand("deny") + .withPermission("zones.set") + .withArguments( + CustomArgument.region("key", "zones.set.other", CustomArgument.MemberType.OWNER), + new StringArgument("target").replaceSuggestions( + ArgumentSuggestions.stringsWithTooltipsAsync(info -> + CompletableFuture.supplyAsync(() -> suggestTargets(info.sender(), + (Region) info.previousArgs().get("key"))))), + new StringArgument("flag").replaceSuggestions( + ArgumentSuggestions.stringsWithTooltipsAsync(info -> + CompletableFuture.supplyAsync(this::suggestFlags))), + new StringArgument("pattern").replaceSuggestions(ArgumentSuggestions.stringsAsync(info -> + CompletableFuture.supplyAsync( + () -> suggestPatterns((String) info.previousArgs().get("flag"))))) + ) + .executes((sender, args) -> { + Region region = (Region) args.get("key"); + if (region == null) { + sender.sendMessage(messages.component("commands.invalid-region")); + return; + } + + String rawTarget = args.getRaw("target"); + String flagName = (String) args.get("flag"); + String rawPattern = (String) args.get("pattern"); + + Flag flag = Flags.getFlag(flagName); + if (flag == null) { + sender.sendMessage(messages.component("commands.invalid-permission")); + return; + } + + if (sender instanceof Player bukkitPlayer) { + RPlayer rPlayer = RPlayer.wrap(bukkitPlayer).orElse(null); + if (rPlayer != null && !flag.canSet(rPlayer, plugin.getPlatform())) { + sender.sendMessage(messages.component("commands.invalid-permission")); + return; + } + } + + SubjectRef subject = parseTargetSubject(rawTarget); + if (subject == null) { + sender.sendMessage(messages.component("commands.perm.invalid-target")); + return; + } + + FlagValueKind kind = flag.valueKind(); + if (kind != FlagValueKind.TARGET_DECISION && kind != FlagValueKind.STRING_SET) { + sender.sendMessage(messages.component("commands.perm.invalid-flag-kind", + Placeholders.builder().string("permission", flag.name()).build())); + return; + } + + regionManager.withWorldWriteLock(region.getWorld(), () -> { + if (kind == FlagValueKind.TARGET_DECISION) { + RegionPermissions perms = region.getPermissions(); + String pattern = normalizeTargetDecisionValue(rawPattern); + RegionPermissions.TargetDecisionValue v = (RegionPermissions.TargetDecisionValue) perms + .subject(subject) + .getOrCreate(flag.id(), FlagValueKind.TARGET_DECISION); + v.deny(pattern); + cleanupEmptyValue(perms, subject, flag.id()); + } else { + String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); + if (flag == Flags.ROLE) { + if (!(subject instanceof PlayerSubject p)) return; + RegionMembership membership = region.getMembership(); + membership.player(p.uuid()).roles().deny(value); + cleanupEmptyMembership(membership, p.uuid()); + } else if (flag == Flags.GROUP) { + RegionMembership membership = region.getMembership(); + if (subject instanceof PlayerSubject p) { + membership.player(p.uuid()).groups().deny(value); + cleanupEmptyMembership(membership, p.uuid()); + } else if (subject instanceof GroupSubject g) { + membership.group(g.name()).includes().deny(value); + cleanupEmptyGroupDefinition(membership, g.name()); + } else { + return; + } + } else { + RegionPermissions perms = region.getPermissions(); + RegionPermissions.StringSetValue v = (RegionPermissions.StringSetValue) perms + .subject(subject) + .getOrCreate(flag.id(), FlagValueKind.STRING_SET); + v.deny(value); + cleanupEmptyValue(perms, subject, flag.id()); + } + } + + regionManager.saveRegion(region.getKey(), region); + }); + + plugin.getZones().getPermissionManager().invalidateAll(); + + sender.sendMessage(messages.component( + "commands.perm.deny", + Placeholders.builder() + .string("region", region.getKey().toString()) + .string("target", rawTarget) + .string("permission", flag.name()) + .string("value", rawPattern) + .build() + )); + }); + } + + CommandAPICommand unset() { + return new CommandAPICommand("unset") + .withPermission("zones.set") + .withArguments( + CustomArgument.region("key", "zones.set.other", CustomArgument.MemberType.OWNER), + new StringArgument("target").replaceSuggestions( + ArgumentSuggestions.stringsWithTooltipsAsync(info -> + CompletableFuture.supplyAsync(() -> suggestTargets(info.sender(), + (Region) info.previousArgs().get("key"))))), + new StringArgument("flag").replaceSuggestions( + ArgumentSuggestions.stringsWithTooltipsAsync(info -> + CompletableFuture.supplyAsync(this::suggestFlags))), + new StringArgument("pattern").replaceSuggestions(ArgumentSuggestions.stringsAsync(info -> + CompletableFuture.supplyAsync( + () -> suggestPatterns((String) info.previousArgs().get("flag"))))) + ) + .executes((sender, args) -> { + Region region = (Region) args.get("key"); + if (region == null) { + sender.sendMessage(messages.component("commands.invalid-region")); + return; + } + + String rawTarget = args.getRaw("target"); + String flagName = (String) args.get("flag"); + String rawPattern = (String) args.get("pattern"); + + Flag flag = Flags.getFlag(flagName); + if (flag == null) { + sender.sendMessage(messages.component("commands.invalid-permission")); + return; + } + + if (sender instanceof Player bukkitPlayer) { + RPlayer rPlayer = RPlayer.wrap(bukkitPlayer).orElse(null); + if (rPlayer != null && !flag.canSet(rPlayer, plugin.getPlatform())) { + sender.sendMessage(messages.component("commands.invalid-permission")); + return; + } + } + + SubjectRef subject = parseTargetSubject(rawTarget); + if (subject == null) { + sender.sendMessage(messages.component("commands.perm.invalid-target")); + return; + } + + boolean changed = regionManager.withWorldWriteLock(region.getWorld(), () -> { + FlagValueKind kind = flag.valueKind(); + boolean didChange = false; + + if (kind == FlagValueKind.TARGET_DECISION) { + RegionPermissions perms = region.getPermissions(); + RegionPermissions.SubjectPermissions subjectPerms = perms.subjects().get(subject); + if (subjectPerms == null) return false; + RegionPermissions.PermissionValue pv = subjectPerms.get(flag.id()); + if (!(pv instanceof RegionPermissions.TargetDecisionValue targeted)) return false; + String pattern = normalizeTargetDecisionValue(rawPattern); + didChange = targeted.removeAllow(pattern) || targeted.removeDeny(pattern); + if (didChange) cleanupEmptyValue(perms, subject, flag.id()); + } else if (kind == FlagValueKind.STRING_SET) { + String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); + if (value.isEmpty()) return false; + RegionMembership membership = region.getMembership(); + + if (flag == Flags.ROLE) { + if (!(subject instanceof PlayerSubject p)) return false; + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return false; + didChange = entry.roles().removeAllow(value) || entry.roles().removeDeny(value); + if (didChange) cleanupEmptyMembership(membership, p.uuid()); + } else if (flag == Flags.GROUP) { + if (subject instanceof PlayerSubject p) { + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return false; + didChange = entry.groups().removeAllow(value) || entry.groups().removeDeny(value); + if (didChange) cleanupEmptyMembership(membership, p.uuid()); + } else if (subject instanceof GroupSubject g) { + RegionMembership.GroupEntry entry = membership.groups().get(g.name()); + if (entry == null) return false; + didChange = entry.includes().removeAllow(value) || entry.includes() + .removeDeny(value); + if (didChange) cleanupEmptyGroupDefinition(membership, g.name()); + } else { + return false; + } + } else { + RegionPermissions perms = region.getPermissions(); + RegionPermissions.SubjectPermissions subjectPerms = perms.subjects().get(subject); + if (subjectPerms == null) return false; + RegionPermissions.PermissionValue pv = subjectPerms.get(flag.id()); + if (!(pv instanceof RegionPermissions.StringSetValue set)) return false; + didChange = set.allowValues().remove(value) || set.denyValues().remove(value); + if (didChange) cleanupEmptyValue(perms, subject, flag.id()); + } + } else { + return false; + } + + if (!didChange) return false; + regionManager.saveRegion(region.getKey(), region); + return true; + }); + + if (!changed) { + sender.sendMessage(messages.component("commands.perm.nothing-to-remove")); + return; + } + + plugin.getZones().getPermissionManager().invalidateAll(); + + sender.sendMessage(messages.component( + "commands.perm.unset", + Placeholders.builder() + .string("region", region.getKey().toString()) + .string("target", rawTarget) + .string("permission", flag.name()) + .string("value", rawPattern) + .build() + )); + }); + } + + CommandAPICommand clear() { + return new CommandAPICommand("clear") + .withPermission("zones.set") + .withArguments( + CustomArgument.region("key", "zones.set.other", CustomArgument.MemberType.OWNER), + new StringArgument("target").replaceSuggestions( + ArgumentSuggestions.stringsWithTooltipsAsync(info -> + CompletableFuture.supplyAsync(() -> suggestTargets(info.sender(), + (Region) info.previousArgs().get("key"))))), + new StringArgument("flag").replaceSuggestions( + ArgumentSuggestions.stringsWithTooltipsAsync(info -> + CompletableFuture.supplyAsync(this::suggestFlags))) + ) + .executes((sender, args) -> { + Region region = (Region) args.get("key"); + if (region == null) { + sender.sendMessage(messages.component("commands.invalid-region")); + return; + } + + String rawTarget = args.getRaw("target"); + String flagName = (String) args.get("flag"); + + Flag flag = Flags.getFlag(flagName); + if (flag == null) { + sender.sendMessage(messages.component("commands.invalid-permission")); + return; + } + + if (sender instanceof Player bukkitPlayer) { + RPlayer rPlayer = RPlayer.wrap(bukkitPlayer).orElse(null); + if (rPlayer != null && !flag.canSet(rPlayer, plugin.getPlatform())) { + sender.sendMessage(messages.component("commands.invalid-permission")); + return; + } + } + + SubjectRef subject = parseTargetSubject(rawTarget); + if (subject == null) { + sender.sendMessage(messages.component("commands.perm.invalid-target")); + return; + } + + boolean changed = regionManager.withWorldWriteLock(region.getWorld(), () -> { + FlagValueKind kind = flag.valueKind(); + boolean didChange = false; + + if (kind == FlagValueKind.STRING_SET && flag == Flags.ROLE) { + if (!(subject instanceof PlayerSubject p)) return false; + RegionMembership membership = region.getMembership(); + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return false; + didChange = !entry.roles().allowValues().isEmpty() || !entry.roles().denyValues().isEmpty(); + entry.roles().clear(); + cleanupEmptyMembership(membership, p.uuid()); + } else if (kind == FlagValueKind.STRING_SET && flag == Flags.GROUP) { + RegionMembership membership = region.getMembership(); + if (subject instanceof PlayerSubject p) { + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return false; + didChange = !entry.groups().allowValues().isEmpty() || !entry.groups().denyValues() + .isEmpty(); + entry.groups().clear(); + cleanupEmptyMembership(membership, p.uuid()); + } else if (subject instanceof GroupSubject g) { + RegionMembership.GroupEntry entry = membership.groups().get(g.name()); + if (entry == null) return false; + didChange = !entry.includes().allowValues().isEmpty() || !entry.includes().denyValues() + .isEmpty(); + entry.includes().clear(); + cleanupEmptyGroupDefinition(membership, g.name()); + } else { + return false; + } + } else if (kind == FlagValueKind.TARGET_DECISION || kind == FlagValueKind.STRING_SET) { + RegionPermissions perms = region.getPermissions(); + RegionPermissions.SubjectPermissions subjectPerms = perms.subjects().get(subject); + if (subjectPerms == null || subjectPerms.get(flag.id()) == null) return false; + subjectPerms.values().remove(flag.id()); + if (subjectPerms.values().isEmpty()) perms.subjects().remove(subject); + didChange = true; + } else { + return false; + } + + if (!didChange) return false; + regionManager.saveRegion(region.getKey(), region); + return true; + }); + + if (!changed) { + sender.sendMessage(messages.component("commands.perm.nothing-to-remove")); + return; + } + + plugin.getZones().getPermissionManager().invalidateAll(); + + sender.sendMessage(messages.component( + "commands.perm.clear", + Placeholders.builder() + .string("region", region.getKey().toString()) + .string("target", rawTarget) + .string("permission", flag.name()) + .build() + )); + }); + } + + + private static void cleanupEmptyValue(RegionPermissions perms, SubjectRef subjectRef, int flagId) { + if (perms == null || subjectRef == null) return; + RegionPermissions.SubjectPermissions subject = perms.subjects().get(subjectRef); + if (subject == null) return; + + RegionPermissions.PermissionValue pv = subject.get(flagId); + if (pv instanceof RegionPermissions.TargetDecisionValue targeted) { + if (targeted.allowTargets().isEmpty() && targeted.denyTargets().isEmpty()) { + subject.values().remove(flagId); + } + } else if (pv instanceof RegionPermissions.StringSetValue set) { + if (set.allowValues().isEmpty() && set.denyValues().isEmpty()) { + subject.values().remove(flagId); + } + } + + if (subject.values().isEmpty()) perms.subjects().remove(subjectRef); + } + + private static void cleanupEmptyMembership(RegionMembership membership, UUID uuid) { + if (membership == null || uuid == null) return; + RegionMembership.PlayerEntry entry = membership.players().get(uuid); + if (entry == null) return; + + boolean rolesEmpty = entry.roles().allowValues().isEmpty() && entry.roles().denyValues().isEmpty(); + boolean groupsEmpty = entry.groups().allowValues().isEmpty() && entry.groups().denyValues().isEmpty(); + if (rolesEmpty && groupsEmpty) membership.players().remove(uuid); + } + + private static void cleanupEmptyGroupDefinition(RegionMembership membership, String groupName) { + if (membership == null || groupName == null) return; + RegionMembership.GroupEntry entry = membership.groups().get(groupName); + if (entry == null) return; + if (entry.includes().allowValues().isEmpty() && entry.includes().denyValues().isEmpty()) { + membership.groups().remove(groupName); + } + } + + private StringTooltip[] suggestTargets(CommandSender sender, Region region) { + if (region == null) return new StringTooltip[0]; + + boolean canEdit = sender.hasPermission("zones.set.other"); + if (!canEdit && sender instanceof Player p) { + canEdit = regionManager.withWorldReadLock(region.getWorld(), () -> region.isOwner(p.getUniqueId())); + } + if (!canEdit) return new StringTooltip[0]; + + Map targets = new HashMap<>(); + targets.put("+universal", messages.component("flags.universal")); + + regionManager.withWorldReadLock(region.getWorld(), () -> { + for (String group : region.getGroupNames()) { + targets.put("group:" + group, Component.text(region.getGroupMembers(group).toString())); + } + }); + + for (var player : RPlayer.online()) { + targets.put(player.name(), Component.text(player.uuid().toString())); + } + + StringTooltip[] suggestions = new StringTooltip[targets.size()]; + int i = 0; + for (Map.Entry target : targets.entrySet()) { + suggestions[i++] = StringTooltip.ofMessage( + target.getKey(), + BukkitTooltip.messageFromAdventureComponent(target.getValue()) + ); + } + return suggestions; + } + + private StringTooltip[] suggestFlags() { + List out = new ArrayList<>(); + for (Flag flag : Flags.getFlags()) { + out.add(StringTooltip.ofMessage( + flag.name(), + BukkitTooltip.messageFromAdventureComponent(messages.component("flags." + flag.name())) + )); + } + return out.toArray(new StringTooltip[0]); + } + + + private static String[] suggestPatterns(String flagName) { + if (flagName == null) return new String[]{"all"}; + Flag flag = Flags.getFlag(flagName); + if (flag == null) return new String[]{"all"}; + + FlagValueKind kind = flag.valueKind(); + LinkedHashSet values = new LinkedHashSet<>(); + values.add("*"); + values.add("all"); + for (String v : flag.getValidValues()) { + if (v == null || v.isBlank()) continue; + String normalized = v.trim().toLowerCase(Locale.ROOT); + if (normalized.startsWith("!")) continue; + if (kind == FlagValueKind.TARGET_DECISION + && ("true".equals(normalized) + || "false".equals(normalized) + || "owner".equals(normalized) + || "admin".equals(normalized))) { + continue; + } + values.add(normalized); + } + return values.toArray(new String[0]); + } + + private static SubjectRef parseTargetSubject(String rawTarget) { + if (rawTarget == null || rawTarget.isBlank()) return null; + String raw = rawTarget.trim(); + + if ("+universal".equalsIgnoreCase(raw)) { + return Subjects.universal(); + } + + if (raw.regionMatches(true, 0, "player:", 0, "player:".length())) { + String nameOrUuid = raw.substring("player:".length()).trim(); + if (nameOrUuid.isEmpty()) return null; + try { + return new PlayerSubject(UUID.fromString(nameOrUuid)); + } catch (IllegalArgumentException ignored) { + return new PlayerSubject(Bukkit.getOfflinePlayer(nameOrUuid).getUniqueId()); + } + } + + if (raw.regionMatches(true, 0, "group:", 0, "group:".length())) { + String name = Subjects.normalizeGroupName(raw.substring("group:".length())); + return name == null ? null : new GroupSubject(name); + } + + if (raw.regionMatches(true, 0, "group.", 0, "group.".length())) { + String name = Subjects.normalizeGroupName(raw.substring("group.".length())); + return name == null ? null : new GroupSubject(name); + } + + try { + return new PlayerSubject(UUID.fromString(raw)); + } catch (IllegalArgumentException ignored) { + } + + return new PlayerSubject(Bukkit.getOfflinePlayer(raw).getUniqueId()); + } + + private static String normalizeTargetDecisionValue(String rawValue) { + if (rawValue == null) return ""; + String v = rawValue.trim(); + if (v.isEmpty()) return ""; + if ("all".equalsIgnoreCase(v)) return "*"; + return TypeKeys.normalize(v); + } +} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/RenameCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/RenameCommand.java index 8ad0408..d7a157a 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/RenameCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/RenameCommand.java @@ -1,20 +1,19 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.utils.Messages; +import de.t14d3.zones.bukkit.commands.utils.CustomArgument; import dev.jorel.commandapi.CommandAPICommand; -import net.kyori.adventure.text.minimessage.MiniMessage; +import dev.jorel.commandapi.arguments.StringArgument; import org.bukkit.entity.Player; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class RenameCommand { - private final MiniMessage mm = MiniMessage.miniMessage(); private RegionManager regionManager; - private Messages messages; + private MessageFormatService messages; private final ZonesBukkit plugin; public RenameCommand(ZonesBukkit plugin) { @@ -25,24 +24,30 @@ public RenameCommand(ZonesBukkit plugin) { public CommandAPICommand rename = new CommandAPICommand("rename") .withPermission("zones.rename") - .withArguments(CustomArgument.region("key", "zones.rename.other", CustomArgument.MemberType.ADMIN)) + .withArguments(CustomArgument.region("key", "zones.rename.other", CustomArgument.MemberType.ADMIN), + new StringArgument("New Name")) .executes((sender, args) -> { Region region = regionManager.regions().get(RegionKey.fromString(args.getRaw("key")).getValue()); if (region == null) { - sender.sendMessage(mm.deserialize(messages.get("commands.invalid-region"))); + sender.sendMessage(messages.component("commands.invalid-region")); return; } if (!sender.hasPermission("zones.rename.other")) { - if (sender instanceof Player player && !region.isAdmin(player.getUniqueId())) { - sender.sendMessage(mm.deserialize(messages.get("commands.invalid-region"))); + if (sender instanceof Player player + && !regionManager.withWorldReadLock(region.getWorld(), + () -> region.isAdmin(player.getUniqueId()))) { + sender.sendMessage(messages.component("commands.invalid-region")); return; } } String name = args.getRaw("New Name"); - region.setName(name, regionManager); - sender.sendMessage( - mm.deserialize(messages.get("commands.rename.success"), - parsed("region", region.getKey().toString()), - parsed("name", name))); + regionManager.withWorldWriteLock(region.getWorld(), () -> region.setName(name, regionManager)); + sender.sendMessage(messages.component( + "commands.rename.success", + Placeholders.builder() + .string("region", region.getKey().toString()) + .string("name", name) + .build() + )); }); } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/RootCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/RootCommand.java index a858946..b5d0239 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/RootCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/RootCommand.java @@ -15,7 +15,7 @@ public RootCommand(ZonesBukkit plugin) { .withSubcommand(new CreateCommand(plugin).create) .withSubcommand(new SubCreateCommand(plugin).subcreate) .withSubcommand(new CancelCommand(plugin).cancel) - .withSubcommand(new SetCommand(plugin).set) + .withSubcommand(new PermCommand(plugin).perm) .withSubcommand(new ExpandCommand(plugin).expand) .withSubcommand(new SelectCommand(plugin).select) .withSubcommand(new RenameCommand(plugin).rename) diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SaveCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SaveCommand.java index 28916bf..b8cf9c7 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SaveCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SaveCommand.java @@ -1,18 +1,15 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.utils.Messages; import dev.jorel.commandapi.CommandAPICommand; -import net.kyori.adventure.text.minimessage.MiniMessage; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; public class SaveCommand { private final ZonesBukkit plugin; private RegionManager regionManager; - private Messages messages; - private final MiniMessage mm = MiniMessage.miniMessage(); + private MessageFormatService messages; public SaveCommand(ZonesBukkit plugin) { this.plugin = plugin; @@ -26,6 +23,7 @@ public SaveCommand(ZonesBukkit plugin) { regionManager.saveRegions(); int count = regionManager.regions().size(); sender.sendMessage( - mm.deserialize(messages.get("commands.save"), parsed("count", String.valueOf(count)))); + messages.component("commands.save", + Placeholders.builder().string("count", String.valueOf(count)).build())); }); } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SelectCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SelectCommand.java index b8295a6..fda79a9 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SelectCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SelectCommand.java @@ -1,23 +1,23 @@ package de.t14d3.zones.bukkit.commands; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.objects.World; -import de.t14d3.zones.utils.Messages; +import de.t14d3.zones.bukkit.commands.utils.CustomArgument; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import dev.jorel.commandapi.CommandAPICommand; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class SelectCommand { - private final MiniMessage mm = MiniMessage.miniMessage(); private RegionManager regionManager; - private Messages messages; + private MessageFormatService messages; private ZonesBukkit plugin; public SelectCommand(ZonesBukkit plugin) { @@ -32,36 +32,43 @@ public SelectCommand(ZonesBukkit plugin) { .executes((sender, args) -> { if (sender instanceof Player player) { Region region; - de.t14d3.zones.objects.Player zplayer = PlayerRepository.get(player.getUniqueId()); - if (zplayer.isSelectionCreating()) { - plugin.getPlatform().removeBeacon(zplayer, zplayer.getSelection().getWorld(), - zplayer.getSelection().getMin()); - plugin.getPlatform().removeBeacon(zplayer, zplayer.getSelection().getWorld(), - zplayer.getSelection().getMax()); - zplayer.setSelectionCreating(false); + RPlayer rPlayer = RPlayer.wrap(player).orElse(null); + if (rPlayer == null) return; + + Box selection = rPlayer.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + boolean selecting = rPlayer.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false); + if (selecting && selection != null) { + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : rPlayer.worldOrThrow() + .ref(); + plugin.getPlatform().removeBeacon(rPlayer, selectionWorld, selection.getMin()); + plugin.getPlatform().removeBeacon(rPlayer, selectionWorld, selection.getMax()); + rPlayer.extras().put(ZonesExtraKeys.SELECTION_CREATING, false); } if (args.get("key") == null) { - region = regionManager.getEffectiveRegionAt(BlockLocation.of(player.getLocation()), - World.of(player.getWorld())); - if (region == null || region.getBounds().equals(zplayer.getSelection())) { - zplayer.setSelection(null); - player.sendMessage(mm.deserialize(messages.get("commands.select.deselected"))); + RBlockPos pos = rPlayer.locationOrThrow().blockPos(); + RWorldRef world = rPlayer.worldOrThrow().ref(); + region = regionManager.getEffectiveRegionAt(pos, world); + if (region == null || (selection != null && region.getBounds().equals(selection))) { + rPlayer.extras().remove(ZonesExtraKeys.SELECTION); + player.sendMessage(messages.component("commands.select.deselected")); return; } } else { region = regionManager.regions().get(RegionKey.fromString(args.getRaw("key")).getValue()); } if (region == null) { - player.sendMessage(mm.deserialize(messages.get("commands.invalid-region"))); + player.sendMessage(messages.component("commands.invalid-region")); return; } - if (zplayer.getSelection() == null || args.get("key") == null) { - zplayer.setSelection(region.getBounds()); - player.sendMessage(mm.deserialize(messages.get("commands.select.selected"), - parsed("region", region.getName()))); + if (selection == null || args.get("key") == null) { + rPlayer.extras().put(ZonesExtraKeys.SELECTION, region.getBounds()); + player.sendMessage(messages.component( + "commands.select.selected", + Placeholders.builder().string("region", region.getName()).build() + )); } else { - zplayer.setSelection(null); - player.sendMessage(mm.deserialize(messages.get("commands.select.deselected"))); + rPlayer.extras().remove(ZonesExtraKeys.SELECTION); + player.sendMessage(messages.component("commands.select.deselected")); } } }); diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SetCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SetCommand.java deleted file mode 100644 index c01eb08..0000000 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SetCommand.java +++ /dev/null @@ -1,137 +0,0 @@ -package de.t14d3.zones.bukkit.commands; - -import de.t14d3.zones.Region; -import de.t14d3.zones.RegionKey; -import de.t14d3.zones.RegionManager; -import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.Flag; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.permissions.flags.Flags; -import de.t14d3.zones.utils.Messages; -import dev.jorel.commandapi.BukkitTooltip; -import dev.jorel.commandapi.CommandAPICommand; -import dev.jorel.commandapi.StringTooltip; -import dev.jorel.commandapi.arguments.ArgumentSuggestions; -import dev.jorel.commandapi.arguments.ListArgumentBuilder; -import dev.jorel.commandapi.arguments.StringArgument; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.*; -import java.util.concurrent.CompletableFuture; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - -public class SetCommand { - private final MiniMessage mm = MiniMessage.miniMessage(); - private RegionManager regionManager; - private Messages messages; - private ZonesBukkit plugin; - - public SetCommand(ZonesBukkit plugin) { - this.plugin = plugin; - this.regionManager = plugin.getRegionManager(); - this.messages = plugin.getMessages(); - } - - @SuppressWarnings({"unchecked", "DataFlowIssue"}) - public CommandAPICommand set = new CommandAPICommand("set") - .withPermission("zones.set") - .withArguments( - CustomArgument.region("key", "zones.set.other", CustomArgument.MemberType.OWNER), - new StringArgument("target") - .replaceSuggestions(ArgumentSuggestions.stringsWithTooltipsAsync(info -> { - return CompletableFuture.supplyAsync(() -> { - Region region = regionManager.regions() - .get(RegionKey.fromString((String) info.previousArgs().get("key")) - .getValue()); - if (region == null) { - return new StringTooltip[0]; - } else if (!info.sender().hasPermission("zones.set.other")) { - if (!(info.sender() instanceof Player player && region.isOwner( - player.getUniqueId()))) { - return new StringTooltip[0]; - } - } - Map targets = new HashMap<>(); - region.getGroupNames().forEach(group -> { - List groupMembers = new ArrayList<>(); - region.getGroupMembers(group).forEach( - val -> groupMembers.add( - plugin.getPlatform().getPlayer(UUID.fromString(val)) - .getName())); - targets.put(group, Component.text(groupMembers.toString())); - }); - PlayerRepository.getPlayers().forEach(player -> - targets.put(player.getName(), - Component.text(player.getUniqueId().toString()))); - targets.put("+universal", (mm.deserialize(messages.get("flags.universal")))); - StringTooltip[] suggestions = new StringTooltip[targets.size()]; - int i = 0; - for (Map.Entry target : targets.entrySet()) { - suggestions[i++] = StringTooltip.ofMessage(target.getKey(), - BukkitTooltip.messageFromAdventureComponent(target.getValue())); - } - return suggestions; - }); - })), - new StringArgument("flag") - .replaceSuggestions(ArgumentSuggestions.stringsWithTooltipsAsync(info -> { - return CompletableFuture.supplyAsync(() -> { - List flags = new ArrayList<>(); - boolean universal = ((String) info.previousArgs().get("target")).equalsIgnoreCase( - "+universal"); - for (Flag flag : Flags.getFlags()) { - if (flag.getDefaultValue() != universal) { - continue; - } - flags.add(flag.name()); - } - StringTooltip[] suggestions = new StringTooltip[flags.size()]; - int i = 0; - for (String flag : flags) { - suggestions[i++] = StringTooltip.ofMessage(flag, - BukkitTooltip.messageFromAdventureComponent( - mm.deserialize(messages.get("flags." + flag)))); - } - return suggestions; - }); - })), - new ListArgumentBuilder("values") - .withList(info -> Flags.getFlag((String) info.previousArgs().get("flag")).getValidValues()) - .withStringMapper() - .buildGreedy() - ) - .executes((sender, args) -> { - RegionKey regionKey = RegionKey.fromString((String) args.get("key")); - Region region = regionManager.regions().get(regionKey.getValue()); - if (sender.hasPermission("zones.set.other") || (sender instanceof Player player && region.isOwner( - player.getUniqueId()))) { - List values = (List) args.get("values"); - String display = args.getRaw("target"); - String target; - if (display.startsWith("+")) { - target = display; - } else { - try { - target = Bukkit.getOfflinePlayerIfCached(display).getUniqueId().toString(); - } catch (Exception e) { - sender.sendMessage(mm.deserialize(messages.get("commands.invalid-player"))); - return; - } - } - regionManager.addMemberPermissions(target, (String) args.get("flag"), - values, regionKey); - sender.sendMessage(mm.deserialize(messages.get("commands.set.success"), - parsed("region", regionKey.toString()), - parsed("target", display), - parsed("permission", (String) args.get("flag")), - parsed("value", values.toString()))); - } else { - sender.sendMessage(mm.deserialize(messages.get("commands.invalid-region"))); - } - }); - -} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SubCreateCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SubCreateCommand.java index 26d06ac..a217980 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SubCreateCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SubCreateCommand.java @@ -1,9 +1,11 @@ package de.t14d3.zones.bukkit.commands; +/* import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; +import de.t14d3.zones.bukkit.commands.utils.CustomArgument; import de.t14d3.zones.objects.*; import de.t14d3.zones.utils.Messages; import dev.jorel.commandapi.CommandAPICommand; @@ -90,3 +92,101 @@ public SubCreateCommand(ZonesBukkit plugin) { } }); } +*/ + +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.Region; +import de.t14d3.zones.RegionManager; +import de.t14d3.zones.bukkit.ZonesBukkit; +import de.t14d3.zones.bukkit.commands.utils.CustomArgument; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; +import dev.jorel.commandapi.CommandAPICommand; +import org.bukkit.entity.Player; + +public class SubCreateCommand { + private RegionManager regionManager; + private MessageFormatService messages; + private ZonesBukkit plugin; + + public SubCreateCommand(ZonesBukkit plugin) { + this.plugin = plugin; + this.regionManager = plugin.getRegionManager(); + this.messages = plugin.getMessages(); + } + + public CommandAPICommand subcreate = new CommandAPICommand("subcreate") + .withPermission("zones.subcreate") + .withOptionalArguments(CustomArgument.region("key", "zones.set.other", CustomArgument.MemberType.ADMIN)) + .executes((sender, args) -> { + if (!(sender instanceof Player player)) { + sender.sendMessage(messages.component("commands.only-player")); + return; + } + + RPlayer rPlayer = RPlayer.wrap(player).orElse(null); + if (rPlayer == null) return; + + Box selection = rPlayer.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + boolean selecting = rPlayer.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false); + + if (selection == null || !selecting) { + rPlayer.extras().put(ZonesExtraKeys.SELECTION, new Box(rPlayer.worldOrThrow().ref())); + rPlayer.extras().put(ZonesExtraKeys.SELECTION_CREATING, true); + player.sendMessage(messages.component("commands.create.click-corners")); + return; + } + + if (selection.getMin() == null || selection.getMax() == null) { + player.sendMessage(messages.component("commands.create.click-corners")); + return; + } + + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : rPlayer.worldOrThrow() + .ref(); + + Region parentRegion = (Region) args.get("key"); + if (parentRegion == null) { + RBlockPos pos = rPlayer.locationOrThrow().blockPos(); + parentRegion = regionManager.withWorldReadLock(selectionWorld, () -> { + for (Region region : regionManager.getRegionsAt(pos, selectionWorld)) { + if (region.isAdmin(player.getUniqueId())) return region; + } + return null; + }); + } + + if (parentRegion == null) { + player.sendMessage(messages.component("commands.subcreate.no-parent")); + return; + } + + Region finalParentRegion = parentRegion; + boolean insideParent = regionManager.withWorldReadLock(finalParentRegion.getWorld(), () -> + finalParentRegion.contains(selection.getMin()) && finalParentRegion.contains( + selection.getMax())); + if (!insideParent) { + player.sendMessage(messages.component("commands.subcreate.outside-parent")); + return; + } + + regionManager.createSubRegion( + finalParentRegion.getName() + "_subb", + selection.getMin(), + selection.getMax(), + selectionWorld, + player.getUniqueId(), + finalParentRegion + ); + + plugin.getPlatform().removeBeacon(rPlayer, selectionWorld, selection.getMin()); + plugin.getPlatform().removeBeacon(rPlayer, selectionWorld, selection.getMax()); + rPlayer.extras().remove(ZonesExtraKeys.SELECTION); + rPlayer.extras().put(ZonesExtraKeys.SELECTION_CREATING, false); + + player.sendMessage(messages.component("commands.subcreate.success")); + }); +} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/utils/CustomArgument.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/utils/CustomArgument.java new file mode 100644 index 0000000..08c3a6f --- /dev/null +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/utils/CustomArgument.java @@ -0,0 +1,41 @@ +package de.t14d3.zones.bukkit.commands.utils; + +import de.t14d3.zones.Region; +import de.t14d3.zones.RegionKey; +import de.t14d3.zones.Zones; +import dev.jorel.commandapi.arguments.Argument; +import dev.jorel.commandapi.arguments.StringArgument; + +public class CustomArgument { + public static Argument region(String name, String permission, MemberType type) { + return new dev.jorel.commandapi.arguments.CustomArgument(new StringArgument(name), info -> { + var regionManager = Zones.getInstance().getRegionManager(); + Region region = regionManager.regions() + .get(RegionKey.fromString(info.input()).getValue()); + if (region != null) { + if (info.sender().hasPermission(permission)) { + return region; + } + if (!(info.sender() instanceof org.bukkit.entity.Player player)) { + return null; + } + boolean accessible = regionManager.withWorldReadLock(region.getWorld(), () -> switch (type) { + case OWNER -> region.isOwner(player.getUniqueId()); + case ADMIN -> region.isAdmin(player.getUniqueId()); + case MEMBER, ANY -> region.isMember(player.getUniqueId()); + }); + if (accessible) { + return region; + } + } + return null; + }).replaceSuggestions(new CustomArgumentSuggestion(permission, type)); + } + + public enum MemberType { + OWNER, + ADMIN, + MEMBER, + ANY + } +} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/utils/CustomArgumentSuggestion.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/utils/CustomArgumentSuggestion.java new file mode 100644 index 0000000..29977e2 --- /dev/null +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/utils/CustomArgumentSuggestion.java @@ -0,0 +1,63 @@ +package de.t14d3.zones.bukkit.commands.utils; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import de.t14d3.zones.Region; +import de.t14d3.zones.Zones; +import de.t14d3.zones.utils.Messages; +import dev.jorel.commandapi.BukkitTooltip; +import dev.jorel.commandapi.SuggestionInfo; +import dev.jorel.commandapi.arguments.ArgumentSuggestions; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class CustomArgumentSuggestion implements ArgumentSuggestions { + + private final String permission; + private final CustomArgument.MemberType type; + + public CustomArgumentSuggestion(String permission, CustomArgument.MemberType type) { + this.permission = permission; + this.type = type; + } + + @Override + public CompletableFuture suggest(SuggestionInfo info, SuggestionsBuilder builder) throws CommandSyntaxException { + return CompletableFuture.supplyAsync(() -> { + List regions = new ArrayList<>(); + var regionManager = Zones.getInstance().getRegionManager(); + if (info.sender().hasPermission(permission)) { + regions.addAll(regionManager.regions().values()); + } else if (info.sender() instanceof Player player) { + for (Region region : regionManager.regions().values()) { + boolean accessible = regionManager.withWorldReadLock(region.getWorld(), () -> switch (type) { + case OWNER -> region.isOwner(player.getUniqueId()); + case ADMIN -> region.isAdmin(player.getUniqueId()); + case MEMBER, ANY -> region.isMember(player.getUniqueId()); + }); + if (accessible) { + regions.add(region); + } + } + } + + // Filter based on remaining input (case insensitive) + String remaining = builder.getRemaining().toLowerCase(); + for (Region region : regions) { + String key = region.getKey().toString(); + String name = region.getName(); + if (key.toLowerCase().contains(remaining) || (name != null && name.toLowerCase().contains(remaining))) { + builder.suggest(key, + BukkitTooltip.messageFromAdventureComponent(Messages.regionInfo(region, false))); + } + } + + return builder.build(); + }); + } +} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/FAWEIntegration.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/FAWEIntegration.java index 3001873..c9e4fd6 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/FAWEIntegration.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/FAWEIntegration.java @@ -1,18 +1,18 @@ -package de.t14d3.zones.integrations; +package de.t14d3.zones.bukkit.integrations; import com.fastasyncworldedit.bukkit.regions.BukkitMaskManager; import com.fastasyncworldedit.core.regions.FaweMask; import com.fastasyncworldedit.core.util.WEManager; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.regions.CuboidRegion; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; -import de.t14d3.zones.bukkit.BukkitPlatform; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.Result; -import de.t14d3.zones.objects.World; import de.t14d3.zones.permissions.flags.Flags; +import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; public class FAWEIntegration extends BukkitMaskManager { @@ -29,25 +29,33 @@ public void register() { } public boolean isAllowed(Player player, Region region, MaskType type) { - return region != null && (region.isAdmin( - player.getUniqueId()) || type == MaskType.MEMBER && (Flags.BREAK.getCustomHandler() - .evaluate(region, player.getUniqueId().toString(), "break", "true") - .equals(Result.TRUE) || Flags.PLACE.getCustomHandler() - .evaluate(region, player.getUniqueId().toString(), "place", "true").equals(Result.TRUE))); + if (player == null || region == null) return false; + if (player.hasPermission("zones.bypass.claimed")) return true; + if (plugin.getRegionManager().withWorldReadLock(region.getWorld(), () -> region.isAdmin(player.getUniqueId()))) + return true; + if (type != MaskType.MEMBER) return false; + + Location location = player.getLocation(); + return plugin.getPermissionManager().checkAction(location, player.getUniqueId(), Flags.BREAK, "*") + || plugin.getPermissionManager().checkAction(location, player.getUniqueId(), Flags.PLACE, "*"); } @Override public FaweMask getMask(final com.sk89q.worldedit.entity.Player wePlayer, final MaskType type, boolean isWhitelist) { final Player player = BukkitAdapter.adapt(wePlayer); final Location location = player.getLocation(); - Region region = plugin.getRegionManager() - .getEffectiveRegionAt(BlockLocation.of(location), World.of(location.getWorld())); + RBlockPos pos = new RBlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + RWorldRef world = new RWorldRef(location.getWorld().getName(), location.getWorld().getKey().toString()); + Region region = plugin.getRegionManager().getEffectiveRegionAt(pos, world); if (region == null) { return null; } if (isAllowed(player, region, type)) { - final Location pos1 = region.getMin().toLocation(BukkitPlatform.getWorld(region.getWorld())); - final Location pos2 = region.getMax().toLocation(BukkitPlatform.getWorld(region.getWorld())); + org.bukkit.World bukkitWorld = resolveWorld(region.getWorld(), player); + RBlockPos min = region.getMin(); + RBlockPos max = region.getMax(); + final Location pos1 = new Location(bukkitWorld, min.x(), min.y(), min.z()); + final Location pos2 = new Location(bukkitWorld, max.x(), max.y(), max.z()); return new FaweMask( new CuboidRegion(BukkitAdapter.asBlockVector(pos1), BukkitAdapter.asBlockVector(pos2))) { @Override @@ -58,4 +66,25 @@ public boolean isValid(com.sk89q.worldedit.entity.Player player, MaskType type) } return null; } + + private static org.bukkit.World resolveWorld(RWorldRef world, Player fallbackPlayer) { + if (world == null) return fallbackPlayer.getWorld(); + if (world.key() != null) { + try { + NamespacedKey key = NamespacedKey.fromString(world.key()); + if (key != null) { + org.bukkit.World byKey = Bukkit.getWorld(key); + if (byKey != null) return byKey; + } + } catch (Exception ignored) { + } + } + if (world.name() != null) { + org.bukkit.World byName = Bukkit.getWorld(world.name()); + if (byName != null) return byName; + } + org.bukkit.World byIdentifier = Bukkit.getWorld(world.identifier()); + if (byIdentifier != null) return byIdentifier; + return fallbackPlayer.getWorld(); + } } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/PlaceholderAPI.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/PlaceholderAPI.java index 71cbb8f..eb8fab4 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/PlaceholderAPI.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/PlaceholderAPI.java @@ -1,9 +1,9 @@ -package de.t14d3.zones.integrations; +package de.t14d3.zones.bukkit.integrations; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.World; import de.t14d3.zones.permissions.flags.Flags; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.Bukkit; @@ -11,7 +11,6 @@ import org.jetbrains.annotations.NotNull; import java.util.List; -import java.util.UUID; public class PlaceholderAPI extends PlaceholderExpansion { private final ZonesBukkit plugin; @@ -67,8 +66,11 @@ public boolean persist() { @Override public String onPlaceholderRequest(Player player, @NotNull String params) { - List regions = plugin.getRegionManager() - .getRegionsAt(BlockLocation.of(player.getLocation()), World.of(player.getWorld())); + var loc = player.getLocation(); + List regions = plugin.getRegionManager().getRegionsAt( + new RBlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()), + new RWorldRef(loc.getWorld().getName(), loc.getWorld().getKey().toString()) + ); if (params.equalsIgnoreCase("get_name")) { String name = ""; if (!regions.isEmpty()) { @@ -84,16 +86,13 @@ public String onPlaceholderRequest(Player player, @NotNull String params) { return key; } if (params.equalsIgnoreCase("get_members")) { + if (regions.isEmpty()) return ""; + final String[] members = {""}; - 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; - } + regions.get(0).getMembership().players().keySet().forEach(uuid -> { + String member = Bukkit.getOfflinePlayer(uuid).getName() != null + ? Bukkit.getOfflinePlayer(uuid).getName() + : String.valueOf(uuid); if (members[0].isEmpty()) { members[0] = member; @@ -121,20 +120,20 @@ public String onPlaceholderRequest(Player player, @NotNull String params) { } if (params.equalsIgnoreCase("get_min_x")) { if (!regions.isEmpty()) { - return String.valueOf(regions.get(0).getMin().getX()); + return String.valueOf(regions.get(0).getMin().x()); } return ""; } if (params.equalsIgnoreCase("get_min_y")) { int minY = 0; if (!regions.isEmpty()) { - minY = regions.get(0).getMin().getY(); + minY = regions.get(0).getMin().y(); } return String.valueOf(minY); } if (params.equalsIgnoreCase("get_min_z")) { if (!regions.isEmpty()) { - return String.valueOf(regions.get(0).getMin().getZ()); + return String.valueOf(regions.get(0).getMin().z()); } return ""; } @@ -146,25 +145,28 @@ public String onPlaceholderRequest(Player player, @NotNull String params) { } if (params.equalsIgnoreCase("get_max_x")) { if (!regions.isEmpty()) { - return String.valueOf(regions.get(0).getMax().getX()); + return String.valueOf(regions.get(0).getMax().x()); } return ""; } if (params.equalsIgnoreCase("get_max_y")) { if (!regions.isEmpty()) { - return String.valueOf(regions.get(0).getMax().getY()); + return String.valueOf(regions.get(0).getMax().y()); } return ""; } if (params.equalsIgnoreCase("get_max_z")) { if (!regions.isEmpty()) { - return String.valueOf(regions.get(0).getMax().getZ()); + return String.valueOf(regions.get(0).getMax().z()); } return ""; } if (params.equalsIgnoreCase("is_member")) { if (!regions.isEmpty()) { - return regions.get(0).isMember(player.getUniqueId()) ? "true" : "false"; + Region region = regions.get(0); + boolean isMember = plugin.getRegionManager() + .withWorldReadLock(region.getWorld(), () -> region.isMember(player.getUniqueId())); + return isMember ? "true" : "false"; } return "false"; } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldEditExtent.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldEditExtent.java index 8aa2075..061ef7f 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldEditExtent.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldEditExtent.java @@ -1,4 +1,4 @@ -package de.t14d3.zones.integrations; +package de.t14d3.zones.bukkit.integrations; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.extent.AbstractDelegateExtent; @@ -6,21 +6,58 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.world.block.BlockStateHolder; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.Zones; +import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.utils.TypeKeys; +import org.bukkit.entity.Player; import java.util.Set; +import java.util.UUID; public class WorldEditExtent extends AbstractDelegateExtent { private final Set mask; + private final Zones zones; + private final UUID playerUuid; + private final RWorldRef world; + private final boolean enforceMask; + private final boolean bypassClaimed; - public WorldEditExtent(Set mask, Extent extent) { + public WorldEditExtent(Set mask, Extent extent, Zones zones, Player player) { super(extent); this.mask = mask; + this.zones = zones; + + if (player != null) { + this.playerUuid = player.getUniqueId(); + this.world = new RWorldRef(player.getWorld().getName(), player.getWorld().getKey().toString()); + this.enforceMask = !player.hasPermission("zones.bypass.unclaimed"); + this.bypassClaimed = player.hasPermission("zones.bypass.claimed"); + } else { + this.playerUuid = null; + this.world = null; + this.enforceMask = true; + this.bypassClaimed = true; + } } - @SuppressWarnings("unchecked") @Override - public boolean setBlock(BlockVector3 location, BlockStateHolder block) throws WorldEditException { - if (!WorldEditSession.WorldEditUtils.maskContains(this.mask, location.x(), location.y(), location.z())) { + public > boolean setBlock(BlockVector3 location, T block) throws WorldEditException { + if (bypassClaimed) return super.setBlock(location, block); + if (enforceMask && (mask == null || mask.isEmpty() + || !WorldEditSession.WorldEditUtils.maskContains(this.mask, location.x(), location.y(), + location.z()))) { + return false; + } + if (zones == null || world == null || playerUuid == null) return super.setBlock(location, block); + + String typeKey = block.getBlockType().id(); + boolean isAir = TypeKeys.normalize(typeKey).endsWith("air"); + + RBlockPos pos = new RBlockPos(location.x(), location.y(), location.z()); + if (!zones.getPermissionManager() + .checkAction(pos, world, playerUuid, isAir ? Flags.BREAK : Flags.PLACE, typeKey)) { return false; } return super.setBlock(location, block); diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldEditSession.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldEditSession.java index 28c598a..1bfccdb 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldEditSession.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldEditSession.java @@ -1,4 +1,4 @@ -package de.t14d3.zones.integrations; +package de.t14d3.zones.bukkit.integrations; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.event.extent.EditSessionEvent; @@ -7,8 +7,6 @@ import com.sk89q.worldedit.util.eventbus.Subscribe; import de.t14d3.zones.Region; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.Result; -import de.t14d3.zones.permissions.flags.Flags; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -33,7 +31,7 @@ public void onEditSessionEvent(EditSessionEvent event) { player = Bukkit.getPlayer(event.getActor().getName()); } Set mask = utils.getMask(player); - event.setExtent(new WorldEditExtent(mask, event.getExtent())); + event.setExtent(new WorldEditExtent(mask, event.getExtent(), plugin, player)); } } @@ -55,18 +53,22 @@ public static boolean maskContains(Set mask, int x, int y, int z) public HashSet getMask(Player player) { HashSet mask = new HashSet<>(); + if (player == null) return mask; + + String worldKey = player.getWorld().getKey().toString(); for (Region region : plugin.getRegionManager().regions().values()) { - if ((Flags.BREAK.getCustomHandler().evaluate(region, player.getUniqueId().toString(), "break", "true") - .equals(Result.TRUE) || Flags.PLACE.getCustomHandler() - .evaluate(region, player.getUniqueId().toString(), "place", "true") - .equals(Result.TRUE)) || region.isAdmin(player.getUniqueId())) { - mask.add(new CuboidRegion( - BlockVector3.at(region.getMin().getX(), region.getMin().getY(), region.getMin().getZ()), - BlockVector3.at(region.getMax().getX() - 1, region.getMax().getY() - 1, - region.getMax().getZ() - 1) - // Don't ask me why, but it works - )); + if (region.getMin() == null || region.getMax() == null) continue; + if (region.getWorld() != null && region.getWorld().key() != null && !region.getWorld().key() + .equals(worldKey)) { + continue; } + + mask.add(new CuboidRegion( + BlockVector3.at(region.getMin().x(), region.getMin().y(), region.getMin().z()), + BlockVector3.at(region.getMax().x() - 1, region.getMax().y() - 1, + region.getMax().z() - 1) + // Don't ask me why, but it works + )); } return mask; } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldGuardImporter.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldGuardImporter.java index 62b5bb2..d96b826 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldGuardImporter.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/integrations/WorldGuardImporter.java @@ -1,70 +1,105 @@ -package de.t14d3.zones.integrations; +package de.t14d3.zones.bukkit.integrations; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldguard.WorldGuard; +import com.sk89q.worldguard.protection.flags.RegionGroup; +import com.sk89q.worldguard.protection.flags.StateFlag; import com.sk89q.worldguard.protection.regions.ProtectedRegion; import com.sk89q.worldguard.protection.regions.RegionContainer; import com.sk89q.worldguard.protection.regions.RegionType; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.RegionFlagEntry; +import de.t14d3.zones.objects.Flag; +import de.t14d3.zones.permissions.RegionMembership; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.permissions.flags.FlagValueKind; +import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.permissions.subjects.Subjects; import org.bukkit.World; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +/** + * Imports WorldGuard regions into Zones. + * + *

Currently supports cuboid regions only. Import runs on a background thread and creates new Zones regions with + * generated keys.

+ */ public class WorldGuardImporter { private final ZonesBukkit plugin; + private static final String MEMBER_GROUP = "member"; + /** + * Creates a new importer instance. + * + * @param plugin owning plugin instance + */ public WorldGuardImporter(ZonesBukkit plugin) { this.plugin = plugin; } + /** + * Starts importing regions from WorldGuard. + * + *

This method returns immediately; the import runs on a background thread and triggers a save after completion.

+ */ public void importRegions() { final int[] count = {0}; new Thread(() -> { RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer(); for (World world : plugin.getServer().getWorlds()) { - for (Map.Entry entry : container.get(BukkitAdapter.adapt(world)).getRegions() - .entrySet()) { + var manager = container.get(BukkitAdapter.adapt(world)); + if (manager == null) continue; + + for (var entry : manager.getRegions().entrySet()) { ProtectedRegion region = entry.getValue(); if (!region.getType().equals(RegionType.CUBOID)) { continue; } RegionKey key = RegionKey.generate(); String name = entry.getKey(); - BlockLocation min = BlockLocation.of(BukkitAdapter.adapt(world, region.getMinimumPoint())); - BlockLocation max = BlockLocation.of(BukkitAdapter.adapt(world, region.getMaximumPoint())); + var minLoc = BukkitAdapter.adapt(world, region.getMinimumPoint()); + var maxLoc = BukkitAdapter.adapt(world, region.getMaximumPoint()); + RBlockPos min = new RBlockPos(minLoc.getBlockX(), minLoc.getBlockY(), minLoc.getBlockZ()); + RBlockPos max = new RBlockPos(maxLoc.getBlockX(), maxLoc.getBlockY(), maxLoc.getBlockZ()); + RWorldRef worldRef = new RWorldRef(world.getName(), world.getKey().toString()); - Map> members = new HashMap<>(); - region.getMembers().getUniqueIds().forEach(uuid -> { - members.put(uuid.toString(), List.of(new RegionFlagEntry("group", "member", false))); - }); + Region newRegion = plugin.getRegionManager() + .createNewRegion(key, name, min, max, worldRef, new RegionPermissions(), + region.getPriority()); - region.getOwners().getUniqueIds().forEach(uuid -> { - members.put(uuid.toString(), List.of(new RegionFlagEntry("role", "owner", false))); - }); + plugin.getRegionManager().withWorldWriteLock(newRegion.getWorld(), () -> { + RegionPermissions perms = newRegion.getPermissions(); + RegionMembership membership = newRegion.getMembership(); - List memberPermissions = new ArrayList<>(); - memberPermissions.add(new RegionFlagEntry("break", "true", false)); - memberPermissions.add(new RegionFlagEntry("place", "true", false)); - memberPermissions.add(new RegionFlagEntry("container", "true", false)); - memberPermissions.add(new RegionFlagEntry("redstone", "true", false)); - memberPermissions.add(new RegionFlagEntry("interact", "true", false)); - memberPermissions.add(new RegionFlagEntry("entity", "true", false)); - memberPermissions.add(new RegionFlagEntry("damage", "true", false)); - members.put("+group-members", memberPermissions); + for (var uuid : region.getMembers().getUniqueIds()) { + membership.player(uuid).groups().allow(MEMBER_GROUP); + } + for (var uuid : region.getOwners().getUniqueIds()) { + membership.player(uuid).roles().allow("owner"); + } - Region newRegion = plugin.getRegionManager() - .createNewRegion(key, name, min, max, de.t14d3.zones.objects.World.of(world), members, - region.getPriority()); + var membersSubject = Subjects.group(MEMBER_GROUP); + Flag[] defaultMemberAllowed = { + Flags.BREAK, + Flags.PLACE, + Flags.CONTAINER, + Flags.REDSTONE, + Flags.INTERACT, + Flags.ENTITY, + Flags.DAMAGE + }; + + allowAll(perms, membersSubject, defaultMemberAllowed); + importWorldGuardFlags(region, perms); + + plugin.getRegionManager().saveRegion(key, newRegion); + }); + plugin.getZones().getPermissionManager().invalidateAll(); count[0]++; } @@ -74,4 +109,120 @@ public void importRegions() { plugin.getRegionManager().triggerSave(); }).start(); } + + private static void allowAll(RegionPermissions perms, de.t14d3.zones.permissions.subjects.SubjectRef subject, Flag[] flags) { + if (perms == null || subject == null || flags == null) return; + for (Flag flag : flags) { + if (flag == null) continue; + RegionPermissions.TargetDecisionValue v = (RegionPermissions.TargetDecisionValue) perms + .subject(subject) + .getOrCreate(flag.id(), FlagValueKind.TARGET_DECISION); + v.allow("*"); + } + } + + private static void importWorldGuardFlags(ProtectedRegion wgRegion, RegionPermissions perms) { + if (wgRegion == null || perms == null) return; + + // Prefer the specific action flags (block-break/place) and fall back to build. + importStateOrFallback(wgRegion, perms, + com.sk89q.worldguard.protection.flags.Flags.BLOCK_BREAK, + com.sk89q.worldguard.protection.flags.Flags.BUILD, + Flags.BREAK); + importStateOrFallback(wgRegion, perms, + com.sk89q.worldguard.protection.flags.Flags.BLOCK_PLACE, + com.sk89q.worldguard.protection.flags.Flags.BUILD, + Flags.PLACE); + + // Interactions and "use" are separate in WorldGuard; either can express the intended behavior. + importStateOrFallback(wgRegion, perms, + com.sk89q.worldguard.protection.flags.Flags.INTERACT, + com.sk89q.worldguard.protection.flags.Flags.USE, + Flags.INTERACT, Flags.REDSTONE); + + // Container access maps reasonably well to Zones' container flag. + importStateOrFallback(wgRegion, perms, + com.sk89q.worldguard.protection.flags.Flags.CHEST_ACCESS, + com.sk89q.worldguard.protection.flags.Flags.USE, + Flags.CONTAINER); + + // Note: WorldGuard doesn't expose a single 1:1 flag for all redstone interactions; we approximate this by + // mapping INTERACT/USE into Zones' redstone flag above. + } + + private static void importStateOrFallback( + ProtectedRegion wgRegion, + RegionPermissions perms, + StateFlag primary, + StateFlag fallback, + Flag... zonesFlags + ) { + if (wgRegion == null || perms == null) return; + if (primary != null) { + StateFlag.State state = wgRegion.getFlag(primary); + if (state != null) { + applyState(perms, state, resolveGroup(wgRegion, primary), zonesFlags); + return; + } + } + if (fallback != null) { + importStateFlag(wgRegion, perms, fallback, zonesFlags); + } + } + + private static void importStateFlag(ProtectedRegion wgRegion, RegionPermissions perms, StateFlag flag, Flag... zonesFlags) { + if (wgRegion == null || perms == null || flag == null) return; + StateFlag.State state = wgRegion.getFlag(flag); + if (state == null) return; + applyState(perms, state, resolveGroup(wgRegion, flag), zonesFlags); + } + + private static RegionGroup resolveGroup(ProtectedRegion wgRegion, StateFlag flag) { + if (wgRegion == null || flag == null) return RegionGroup.ALL; + RegionGroup group = wgRegion.getFlag(flag.getRegionGroupFlag()); + return group != null ? group : RegionGroup.ALL; + } + + private static void applyState(RegionPermissions perms, StateFlag.State state, RegionGroup group, Flag... zonesFlags) { + if (perms == null || state == null || zonesFlags == null) return; + if (group == null) group = RegionGroup.ALL; + + // WorldGuard can target different membership groups per flag. Zones can't express "nonmembers" directly, + // so we approximate by applying to universal and (for DENY) explicitly allowing members. + for (Flag zoneFlag : zonesFlags) { + if (zoneFlag == null) continue; + + switch (group) { + case MEMBERS -> applyStateToSubject(perms, Subjects.group(MEMBER_GROUP), zoneFlag, state); + case NON_MEMBERS -> { + applyStateToSubject(perms, Subjects.universal(), zoneFlag, state); + if (state == StateFlag.State.DENY) { + applyStateToSubject(perms, Subjects.group(MEMBER_GROUP), zoneFlag, StateFlag.State.ALLOW); + } + } + case OWNERS -> { + // Owners are treated as admins in Zones (bypass), so importing owner-only flags is skipped. + } + case ALL -> { + applyStateToSubject(perms, Subjects.universal(), zoneFlag, state); + // The importer creates explicit member allows as a baseline (WorldGuard-like default behavior), + // so ALL-group flags must also apply to members to override those defaults. + applyStateToSubject(perms, Subjects.group(MEMBER_GROUP), zoneFlag, state); + } + } + } + } + + private static void applyStateToSubject( + RegionPermissions perms, + de.t14d3.zones.permissions.subjects.SubjectRef subject, + Flag zoneFlag, + StateFlag.State state + ) { + RegionPermissions.TargetDecisionValue v = (RegionPermissions.TargetDecisionValue) perms + .subject(subject) + .getOrCreate(zoneFlag.id(), FlagValueKind.TARGET_DECISION); + if (state == StateFlag.State.ALLOW) v.allow("*"); + if (state == StateFlag.State.DENY) v.deny("*"); + } } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/BlockEventListener.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/BlockEventListener.java deleted file mode 100644 index fdbbe50..0000000 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/BlockEventListener.java +++ /dev/null @@ -1,152 +0,0 @@ -package de.t14d3.zones.bukkit.listeners; - -import de.t14d3.zones.RegionManager; -import de.t14d3.zones.Zones; -import de.t14d3.zones.bukkit.BukkitPermissionManager; -import de.t14d3.zones.bukkit.BukkitPlatform; -import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.Flag; -import de.t14d3.zones.permissions.flags.Flags; -import org.bukkit.block.Block; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.*; -import org.bukkit.event.world.StructureGrowEvent; - -public class BlockEventListener implements Listener { - - private final Zones zones; - private final ZonesBukkit plugin; - private final RegionManager regionManager; - private final BukkitPermissionManager permissionManager; - - public BlockEventListener(Zones zones) { - this.zones = zones; - this.regionManager = zones.getRegionManager(); - this.plugin = ((BukkitPlatform) zones.getPlatform()).getPlugin(); - this.permissionManager = plugin.getPermissionManager(); - - plugin.getServer().getPluginManager().registerEvents(this, plugin); - if (zones.getConfig().getBoolean("events.block-physics.enabled", false)) { - plugin.getServer().getPluginManager().registerEvents(new BlockPhysicsEventListener(), plugin); - } - } - - @EventHandler - public void onBlockGrow(BlockGrowEvent event) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.CREATE, - event.getBlock().getType().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onBlockFade(BlockFadeEvent event) { - Flag flag = switch (event.getBlock().getType()) { - case ICE, FROSTED_ICE, SNOW, FIRE -> Flags.DESTROY; - default -> Flags.TRANSFORM; - }; - if (!permissionManager.checkAction(event.getBlock().getLocation(), flag, event.getBlock().getType().name(), - event.getNewState().getType().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onBlockForm(BlockFormEvent event) { - Flag flag = switch (event.getNewState().getType()) { - case ICE, FROSTED_ICE, SNOW -> Flags.CREATE; - default -> Flags.TRANSFORM; - }; - if (!permissionManager.checkAction(event.getBlock().getLocation(), flag, event.getNewState().getType().name(), - event.getBlock().getType().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onBlockFromTo(BlockFromToEvent event) { - Flag flag = switch (event.getBlock().getType()) { - case WATER, LAVA -> Flags.SPREAD; - default -> Flags.RELOCATE; - }; - if (!permissionManager.checkAction(event.getBlock().getLocation(), flag, event.getBlock().getType().name(), - event.getToBlock().getType().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onBlockSpread(BlockSpreadEvent event) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.SPREAD, - event.getBlock().getType().name(), event.getNewState().getType().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onSculkSpread(SculkBloomEvent event) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.SPREAD, - event.getBlock().getType().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onStructureGrow(StructureGrowEvent event) { - event.getBlocks().removeIf( - state -> !permissionManager.checkAction(state.getLocation(), Flags.CREATE, state.getType().name())); - } - - @EventHandler - public void onLeavesDecay(LeavesDecayEvent event) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.DESTROY, - event.getBlock().getType().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onPistonExtend(BlockPistonExtendEvent event) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.RELOCATE, - event.getBlock().getType().name())) { - event.setCancelled(true); - return; - } - for (Block block : event.getBlocks()) { - if (!permissionManager.checkAction(block.getLocation(), Flags.RELOCATE, - event.getBlock().getType().name())) { - event.setCancelled(true); - return; - } - } - } - - @EventHandler - public void onPistonRetract(BlockPistonRetractEvent event) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.RELOCATE, - event.getBlock().getType().name())) { - event.setCancelled(true); - return; - } - for (Block block : event.getBlocks()) { - if (!permissionManager.checkAction(block.getLocation(), Flags.RELOCATE, - event.getBlock().getType().name())) { - event.setCancelled(true); - return; - } - } - } - - // Separate Listener class for BlockPhysicsEvent, so we can register it selectively - // (High-Frequency event) - private class BlockPhysicsEventListener implements Listener { - @EventHandler - public void onBlockPhysics(org.bukkit.event.block.BlockPhysicsEvent event) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.PHYSICS, - event.getBlock().getType().name())) { - event.setCancelled(true); - } - } - } -} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/ChunkEventListener.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/ChunkEventListener.java deleted file mode 100644 index f5e3e86..0000000 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/ChunkEventListener.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.t14d3.zones.bukkit.listeners; - -import de.t14d3.zones.permissions.CacheUtils; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.world.ChunkUnloadEvent; - -public class ChunkEventListener implements Listener { - @EventHandler - public void onChunkUnload(ChunkUnloadEvent event) { - CacheUtils.getInstance().invalidateInteractionCacheForChunk(event.getChunk().getX(), event.getChunk().getZ(), - event.getWorld().getName()); - } -} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/EntityEventListener.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/EntityEventListener.java deleted file mode 100644 index ae8358e..0000000 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/EntityEventListener.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.t14d3.zones.bukkit.listeners; - -import de.t14d3.zones.RegionManager; -import de.t14d3.zones.Zones; -import de.t14d3.zones.bukkit.BukkitPermissionManager; -import de.t14d3.zones.bukkit.BukkitPlatform; -import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.permissions.flags.Flags; -import org.bukkit.damage.DamageSource; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.event.entity.EntityDamageEvent; - -public class EntityEventListener implements Listener { - - private final Zones zones; - private final ZonesBukkit plugin; - private final RegionManager regionManager; - private final BukkitPermissionManager permissionManager; - - public EntityEventListener(Zones zones) { - this.zones = zones; - this.regionManager = zones.getRegionManager(); - this.plugin = ((BukkitPlatform) zones.getPlatform()).getPlugin(); - this.permissionManager = plugin.getPermissionManager(); - } - - @EventHandler - public void onEntitySpawn(CreatureSpawnEvent event) { - if (event.getSpawnReason().equals(CreatureSpawnEvent.SpawnReason.SPAWNER_EGG)) { - return; - } - if (!permissionManager.checkAction(event.getLocation(), Flags.SPAWN, event.getEntity().getType().name(), - event.getSpawnReason().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onEntityHurt(EntityDamageEvent event) { - DamageSource source = event.getDamageSource(); - if (source.getCausingEntity() instanceof org.bukkit.entity.Player) { - return; // Checked separately - } - if (!permissionManager.checkAction(event.getEntity().getLocation(), Flags.HURT, event.getEntity().getType() - .name(), source.getDamageType())) { - event.setCancelled(true); - } - } -} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/ExplosivesListener.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/ExplosivesListener.java deleted file mode 100644 index 0771bbe..0000000 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/ExplosivesListener.java +++ /dev/null @@ -1,238 +0,0 @@ -package de.t14d3.zones.bukkit.listeners; - -import de.t14d3.zones.Region; -import de.t14d3.zones.bukkit.BukkitPermissionManager; -import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.World; -import de.t14d3.zones.permissions.flags.Flags; -import de.t14d3.zones.utils.DebugLoggerManager; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.block.Block; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.BlockExplodeEvent; -import org.bukkit.event.block.TNTPrimeEvent; -import org.bukkit.event.entity.EntityExplodeEvent; - -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; - -public class ExplosivesListener { - private final ZonesBukkit plugin; - private final BukkitPermissionManager permissionManager; - - private final int limit; - private final boolean limitExceededCancel; - private final AtomicInteger explosionCount = new AtomicInteger(0); - private final DebugLoggerManager logger; - - public ExplosivesListener(ZonesBukkit plugin) { - this.plugin = plugin; - this.permissionManager = plugin.getPermissionManager(); - this.logger = plugin.getDebugLogger(); - - String explosionMode = plugin.getConfig().getString("events.explosion.explosion-mode", "ALL").toUpperCase(); - limit = plugin.getConfig().getInt("events.explosion.limit", 50); - limitExceededCancel = plugin.getConfig().getString("events.explosion.limit-exceeded-action", "CANCEL") - .equalsIgnoreCase("CANCEL"); - - // Reset explosion counter every tick - Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> explosionCount.set(0), 1L, 1L); - - switch (plugin.getConfig().getString("events.explosion.mode", "ALL").toUpperCase()) { - case "ALL": - registerExplosionListener(explosionMode); - plugin.getServer().getPluginManager().registerEvents(new IgnitionListener(), plugin); - break; - case "IGNITION": - registerExplosionListener(explosionMode); - break; - case "EXPLOSION": - plugin.getServer().getPluginManager().registerEvents(new IgnitionListener(), plugin); - break; - case "NONE": - break; - } - } - - private boolean limitExceeded() { - return explosionCount.incrementAndGet() > limit; - } - - private void registerExplosionListener(String explosionMode) { - switch (explosionMode.toUpperCase()) { - case "ANY": - class ExplosionListenerAny implements Listener { - @EventHandler - public void onBlockExplode(BlockExplodeEvent event) { - if (limitExceeded()) { - if (limitExceededCancel) { - event.setCancelled(true); - } - return; - } - for (Block block : event.blockList()) { - if (!permissionManager.checkAction(block.getLocation(), Flags.EXPLOSION, - block.getType().name(), event.getExplodedBlockState().getType())) { - event.setCancelled(true); - break; - } - } - } - - @EventHandler - public void onEntityExplode(EntityExplodeEvent event) { - if (limitExceeded()) { - if (limitExceededCancel) { - event.setCancelled(true); - } - return; - } - for (Block block : event.blockList()) { - if (!permissionManager.checkAction(block.getLocation(), Flags.EXPLOSION, - block.getType().name(), event.getEntityType())) { - event.setCancelled(true); - break; - } - } - } - } - plugin.getServer().getPluginManager().registerEvents(new ExplosionListenerAny(), plugin); - break; - case "ALL": - class ExplosionListenerAll implements Listener { - @EventHandler - public void onBlockExplode(BlockExplodeEvent event) { - if (limitExceeded()) { - if (limitExceededCancel) { - event.setCancelled(true); - } - return; - } - event.blockList().removeIf( - block -> !permissionManager.checkAction(block.getLocation(), Flags.EXPLOSION, - block.getType().name(), event.getExplodedBlockState().getType())); - } - - @EventHandler - public void onEntityExplode(EntityExplodeEvent event) { - if (limitExceeded()) { - if (limitExceededCancel) { - event.setCancelled(true); - } - return; - } - event.blockList().removeIf( - block -> !permissionManager.checkAction(block.getLocation(), Flags.EXPLOSION, - block.getType().name(), event.getEntityType().name())); - } - } - plugin.getServer().getPluginManager().registerEvents(new ExplosionListenerAll(), plugin); - return; - case "REGION": - class ExplosionListenerRegion implements Listener { - @EventHandler - public void onBlockExplode(BlockExplodeEvent event) { - if (limitExceeded()) { - if (limitExceededCancel) { - event.setCancelled(true); - } - return; - } - if (permissionManager.checkAction(event.getBlock().getLocation(), Flags.EXPLOSION, - event.getBlock().getType().name(), event.getExplodedBlockState().getType())) { - Region region = plugin.getRegionManager() - .getEffectiveRegionAt(BlockLocation.of(event.getBlock().getLocation()), - World.of(event.getBlock().getWorld())); - event.blockList().removeIf(block -> !Objects.equals( - plugin.getRegionManager() - .getEffectiveRegionAt(BlockLocation.of(block.getLocation()), - World.of(block.getWorld())), region)); - } else { - event.setCancelled(true); - } - } - - @EventHandler - public void onEntityExplode(EntityExplodeEvent event) { - if (limitExceeded()) { - if (limitExceededCancel) { - event.setCancelled(true); - } - return; - } - Location location = switch (event.getEntity().getType()) { - case TNT, TNT_MINECART -> event.getEntity().getOrigin(); - default -> event.getEntity().getLocation(); - }; - if (permissionManager.checkAction(location, Flags.EXPLOSION, - event.getEntityType().name())) { - Region region = plugin.getRegionManager() - .getEffectiveRegionAt(BlockLocation.of(location), World.of(location.getWorld())); - event.blockList().removeIf(block -> !Objects.equals( - plugin.getRegionManager() - .getEffectiveRegionAt(BlockLocation.of(block.getLocation()), - World.of(block.getWorld())), region)); - } else { - event.setCancelled(true); - } - } - } - plugin.getServer().getPluginManager().registerEvents(new ExplosionListenerRegion(), plugin); - return; - case "SOURCE": - class ExplosionListenerBlock implements Listener { - @EventHandler - public void onBlockExplode(BlockExplodeEvent event) { - if (limitExceeded()) { - if (limitExceededCancel) { - event.setCancelled(true); - } - return; - } - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.EXPLOSION, - event.getExplodedBlockState().getType().name())) { - event.setCancelled(true); - } - } - - @EventHandler - public void onEntityExplode(EntityExplodeEvent event) { - if (limitExceeded()) { - if (limitExceededCancel) { - event.setCancelled(true); - } - return; - } - if (!permissionManager.checkAction(event.getEntity().getOrigin(), Flags.EXPLOSION, - event.getEntityType().name())) { - event.setCancelled(true); - } - } - } - plugin.getServer().getPluginManager().registerEvents(new ExplosionListenerBlock(), plugin); - return; - default: - } - } - - private class IgnitionListener implements Listener { - @EventHandler - public void onTNTPrime(TNTPrimeEvent event) { - if (event.getPrimingEntity() instanceof org.bukkit.entity.Player player) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), player.getUniqueId().toString(), - Flags.IGNITE, - event.getBlock().getType().name())) { - event.setCancelled(true); - } - } else { - if (!permissionManager.checkAction(event.getBlock().getLocation(), Flags.IGNITE, - event.getBlock().getType().name(), true, event.getCause())) { - event.setCancelled(true); - } - } - } - } -} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/PlayerEventListener.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/PlayerEventListener.java deleted file mode 100644 index 155fa7c..0000000 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/PlayerEventListener.java +++ /dev/null @@ -1,441 +0,0 @@ -package de.t14d3.zones.bukkit.listeners; - -import de.t14d3.zones.Region; -import de.t14d3.zones.RegionManager; -import de.t14d3.zones.bukkit.BukkitPermissionManager; -import de.t14d3.zones.bukkit.BukkitPlatform; -import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.*; -import de.t14d3.zones.permissions.flags.Flags; -import de.t14d3.zones.utils.Messages; -import de.t14d3.zones.utils.Utils; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.minimessage.MiniMessage; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.BlockState; -import org.bukkit.block.Container; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.Powerable; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityPlaceEvent; -import org.bukkit.event.hanging.HangingBreakByEntityEvent; -import org.bukkit.event.hanging.HangingPlaceEvent; -import org.bukkit.event.player.*; -import org.bukkit.event.vehicle.VehicleDamageEvent; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.metadata.FixedMetadataValue; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - -public class PlayerEventListener implements Listener { - - private final RegionManager regionManager; - private final BukkitPermissionManager permissionManager; - private final ZonesBukkit plugin; - private final MiniMessage miniMessage = MiniMessage.miniMessage(); - private final Messages messages; - private final BukkitPlatform platform; - private final String ACTIONBAR_MESSAGE; - - public PlayerEventListener(ZonesBukkit plugin) { - this.plugin = plugin; - this.regionManager = plugin.getRegionManager(); - this.permissionManager = plugin.getPermissionManager(); - this.messages = plugin.getMessages(); - this.platform = (BukkitPlatform) plugin.getPlatform(); - this.ACTIONBAR_MESSAGE = messages.get("region.no-interact-permission"); - } - - @EventHandler - private void onPlayerInteract(PlayerInteractEvent event) { - Player player = event.getPlayer(); - de.t14d3.zones.objects.Player zplayer = PlayerRepository.get(player.getUniqueId()); - if (event.getClickedBlock() == null) { - return; // No block clicked, exit early - } - - Location location = event.getClickedBlock().getLocation(); - UUID playerUUID = player.getUniqueId(); - - if (zplayer.getSelection() != null && zplayer.isSelectionCreating()) { - if (event.getHand() == EquipmentSlot.OFF_HAND) { - return; - } - event.setCancelled(true); - - Location min = zplayer.getSelection().getMin() == null ? null : zplayer.getSelection().getMin() - .toLocation(player.getWorld()); - Location max = zplayer.getSelection().getMax() == null ? null : zplayer.getSelection().getMax() - .toLocation(player.getWorld()); - - if (event.getAction() == Action.LEFT_CLICK_BLOCK) { - platform.removeBeacon(player, min); - min = location; - platform.showBeacon(player, min, NamedTextColor.GREEN); - player.sendMessage(miniMessage.deserialize(messages.get("create.primary") - , parsed("x", String.valueOf(location.getBlockX())) - , parsed("y", String.valueOf(location.getBlockY())) - , parsed("z", String.valueOf(location.getBlockZ())) - )); - } else if (event.getAction() == Action.RIGHT_CLICK_BLOCK) { - platform.removeBeacon(player, max); - max = location; - platform.showBeacon(player, max, NamedTextColor.RED); - player.sendMessage(miniMessage.deserialize(messages.get("create.secondary") - , parsed("x", String.valueOf(location.getBlockX())) - , parsed("y", String.valueOf(location.getBlockY())) - , parsed("z", String.valueOf(location.getBlockZ())) - )); - } - if (min != null || max != null) { - Utils.SelectionMode mode = Utils.SelectionMode.getPlayerMode(zplayer); - if (mode == Utils.SelectionMode.CUBOID_3D) { - zplayer.setSelection(new Box(min, max, player.getWorld(), false)); - } else { - if (min != null) { - min.setY(-63); - } - if (max != null) { - max.setY(319); - } - zplayer.setSelection(new Box(min, max, player.getWorld(), false)); - } - - } - return; - } - - List requiredPermissions = new ArrayList<>(); // Collect required permissions - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - // Interactable blocks - if ((isContainer(event.getClickedBlock().getState(false)) || isPowerable( - event.getClickedBlock().getBlockData())) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { - requiredPermissions.add(Flags.INTERACT); - if (isContainer(event.getClickedBlock().getState(false))) { - requiredPermissions.add(Flags.CONTAINER); - } - if (isPowerable(event.getClickedBlock().getBlockData())) { - requiredPermissions.add(Flags.REDSTONE); - } - if (event.getClickedBlock().getType() == Material.TNT) { - requiredPermissions.add(Flags.IGNITE); - } - } else return; - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, playerUUID, action, - event.getClickedBlock().getType().name())) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, event.getClickedBlock().getType().name()); - } - } - } - - @EventHandler - private void onBlockPlace(BlockPlaceEvent event) { - Player player = event.getPlayer(); - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - String type = event.getBlockPlaced().getType().name(); - Location location = event.getBlockPlaced().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.PLACE); - if (isContainer(event.getBlockPlaced().getState(false))) { - requiredPermissions.add(Flags.CONTAINER); - } - if (isPowerable(event.getBlockPlaced().getBlockData())) { - requiredPermissions.add(Flags.REDSTONE); - } - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(event.getBlockPlaced().getLocation(), player.getUniqueId(), action, - type.toLowerCase())) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - - } - - @EventHandler - private void onBlockBreak(BlockBreakEvent event) { - Player player = event.getPlayer(); - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - String type = event.getBlock().getType().name(); - Location location = event.getBlock().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.BREAK); - if (isContainer(event.getBlock().getState(false))) { - requiredPermissions.add(Flags.CONTAINER); - } - if (isPowerable(event.getBlock().getBlockData())) { - requiredPermissions.add(Flags.REDSTONE); - } - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(event.getBlock().getLocation(), player.getUniqueId(), action, - type.toLowerCase())) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - - } - - - @EventHandler - private void onEntityInteract(PlayerInteractEntityEvent event) { - Player player = event.getPlayer(); - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getRightClicked().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.INTERACT); - requiredPermissions.add(Flags.ENTITY); - String type = event.getRightClicked().getType().name(); - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - - @EventHandler - private void onEntityDamage(EntityDamageByEntityEvent event) { - if (event.getDamager() instanceof Player player) { - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getEntity().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.DAMAGE); - String type = event.getEntity().getType().name(); - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - } - - @EventHandler - private void onVehicleDamage(VehicleDamageEvent event) { - if (event.getAttacker() instanceof Player player) { - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getVehicle().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.DAMAGE); - String type = event.getVehicle().getType().name(); - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - } - - @EventHandler - private void onArmorStandManipulate(PlayerArmorStandManipulateEvent event) { - Player player = event.getPlayer(); - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getRightClicked().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.ENTITY); - String type = event.getRightClicked().getType().name(); - requiredPermissions.add(Flags.CONTAINER); - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - - @EventHandler - private void onHangingPlace(HangingPlaceEvent event) { - Player player = event.getPlayer(); - if (player == null || player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getEntity().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.PLACE); - String type = event.getEntity().getType().name(); - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - - @EventHandler - private void onHangingBreak(HangingBreakByEntityEvent event) { - Player player = (Player) event.getRemover(); - if (player == null || player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getEntity().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.BREAK); - requiredPermissions.add(Flags.ENTITY); - String type = event.getEntity().getType().name(); - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - - @EventHandler - private void onEntityPlace(EntityPlaceEvent event) { - Player player = event.getPlayer(); - if (player == null || player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getEntity().getLocation(); - List requiredPermissions = new ArrayList<>(); - requiredPermissions.add(Flags.PLACE); - requiredPermissions.add(Flags.ENTITY); - String type = event.getEntity().getType().name(); - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - - @EventHandler - private void onBucketFill(PlayerBucketFillEvent event) { - Player player = event.getPlayer(); - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getBlockClicked().getLocation(); - List requiredPermissions = new ArrayList<>(); - String type = event.getBlockClicked().getType().name(); - requiredPermissions.add(Flags.BREAK); - - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - - @EventHandler - private void onBucketFillEntity(PlayerBucketEntityEvent event) { - Player player = event.getPlayer(); - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getEntity().getLocation(); - List requiredPermissions = new ArrayList<>(); - String type = event.getEntity().getType().name(); - requiredPermissions.add(Flags.ENTITY); - - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - - @EventHandler - private void onBucketEmpty(PlayerBucketEmptyEvent event) { - Player player = event.getPlayer(); - if (player.hasPermission("zones.bypass.claimed")) { - return; - } - Location location = event.getBlockClicked().getLocation(); - List requiredPermissions = new ArrayList<>(); - String type; - switch (event.getBucket()) { - case WATER_BUCKET -> type = "water"; - case LAVA_BUCKET -> type = "lava"; - case POWDER_SNOW_BUCKET -> type = "powder_snow"; - case TROPICAL_FISH_BUCKET, COD_BUCKET, SALMON_BUCKET, PUFFERFISH_BUCKET, TADPOLE_BUCKET, AXOLOTL_BUCKET -> { - type = "water"; - requiredPermissions.add(Flags.ENTITY); - } - default -> type = event.getBlockClicked().getType().name(); - } - requiredPermissions.add(Flags.PLACE); - - for (Flag action : requiredPermissions) { - if (!permissionManager.checkAction(location, player.getUniqueId(), action, type)) { - event.setCancelled(true); - actionBar(player, location, requiredPermissions, type); - } - } - } - - // Small util for message - static final String METADATA_KEY = "zones_last_actionbar"; - private void actionBar(Player player, Location location, List actions, String type) { - - // Cooldown on the actionbar processing, since spamming it several times per second causes lag - int currentTick = Bukkit.getCurrentTick(); - if (player.hasMetadata(METADATA_KEY)) { - int lastSent = player.getMetadata(METADATA_KEY).get(0).asInt(); - if (currentTick - lastSent < 5) { // 0.25 second cooldown - return; // Cooldown active - } - } - - // Proceed to send action bar - List regions = regionManager.getRegionsAt(BlockLocation.of(location), World.of(location.getWorld())); - String regionNames = regions.stream().map(Region::getName).collect(Collectors.joining(", ")); - - StringBuilder permissionsString = new StringBuilder(); - for (Flag action : actions) { - permissionsString.append(action.name()).append(", "); - } - if (!actions.isEmpty()) { - permissionsString.setLength(permissionsString.length() - 2); // Remove trailing ", " - } - - player.sendActionBar(miniMessage.deserialize(ACTIONBAR_MESSAGE, - parsed("region", regionNames), - parsed("actions", permissionsString.toString()), - parsed("type", type))); - - // Update last sent tick - player.setMetadata(METADATA_KEY, new FixedMetadataValue(plugin, currentTick)); - } - - public static boolean isContainer(BlockState state) { - return state instanceof Container; - } - - public static boolean isPowerable(BlockData data) { - return data instanceof Powerable; - } -} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/PlayerQuitListener.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/PlayerQuitListener.java index 9234145..bc350c2 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/PlayerQuitListener.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/PlayerQuitListener.java @@ -1,8 +1,6 @@ package de.t14d3.zones.bukkit.listeners; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.permissions.CacheUtils; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; @@ -10,18 +8,14 @@ public class PlayerQuitListener implements Listener { private final Zones zones; - private final CacheUtils cacheUtils; public PlayerQuitListener(Zones zones) { this.zones = zones; - this.cacheUtils = CacheUtils.getInstance(); } @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { java.util.UUID uuid = event.getPlayer().getUniqueId(); - PlayerRepository.remove(uuid); - cacheUtils.invalidateInteractionCache(uuid); - cacheUtils.invalidateCache(uuid.toString()); + zones.getPermissionManager().invalidateSubject(uuid); } } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/WorldEventListener.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/WorldEventListener.java index bd0971e..cbe911f 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/WorldEventListener.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/listeners/WorldEventListener.java @@ -1,7 +1,6 @@ package de.t14d3.zones.bukkit.listeners; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.World; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.world.WorldLoadEvent; @@ -16,6 +15,6 @@ public WorldEventListener(Zones plugin) { @EventHandler public void onWorldLoad(WorldLoadEvent event) { - plugin.getRegionManager().loadRegions(World.of(event.getWorld())); + plugin.getRegionManager().loadRegions(); } } diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 1cb24d1..5aa6ddb 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("java") id("fabric-loom") - id("com.gradleup.shadow") version "9.0.0-beta8" + alias(libs.plugins.shadow) } repositories { @@ -25,22 +25,33 @@ loom { } } +// Dedicated server only: exclude client rendering deps that fail remapping under Mojmap. +configurations.configureEach { + exclude(group = "net.fabricmc.fabric-api", module = "fabric-rendering-v1") + exclude(group = "net.fabricmc.fabric-api", module = "fabric-rendering-data-attachment-v1") + exclude(group = "net.fabricmc.fabric-api", module = "fabric-renderer-indigo") + exclude(group = "net.fabricmc.fabric-api", module = "fabric-renderer-api-v1") + exclude(group = "net.fabricmc.fabric-api", module = "fabric-model-loading-api-v1") + exclude(group = "net.fabricmc.fabric-api", module = "fabric-models-v0") +} + dependencies { implementation(project(":api")) - minecraft("com.mojang:minecraft:${rootProject.property("minecraft_version")}") + minecraft(rootProject.libs.minecraft) mappings(loom.officialMojangMappings()) - modImplementation(compileOnly("net.fabricmc:fabric-loader:${rootProject.property("loader_version")}")!!) + modImplementation(rootProject.libs.fabric.loader) + modImplementation(rootProject.libs.fabric.api) + // Required because Loom injects Fabric API interfaces (e.g. DataResourceStore) into Minecraft classes at compile-time. + // The umbrella fabric-api artifact doesn't currently add this v1 module to the compile classpath for this setup. + modImplementation(rootProject.libs.fabric.resource.loader.v1) - val fabricVersion = rootProject.property("fabric_version") as String - modImplementation(fabricApi.module("fabric-lifecycle-events-v1", fabricVersion)) - modImplementation(fabricApi.module("fabric-command-api-v2", fabricVersion)) - modImplementation(fabricApi.module("fabric-events-interaction-v0", fabricVersion)) + modImplementation(include("net.kyori:adventure-platform-fabric:${libs.versions.adventure.platform.fabric.get()}")!!) + implementation(rootProject.libs.adventure.minimessage) - modImplementation(include("me.lucko:fabric-permissions-api:0.3.1")!!) - modImplementation(include("net.kyori:adventure-platform-fabric:6.2.0")!!) - implementation("net.kyori:adventure-text-minimessage:4.19.0") + modImplementation(include("de.t14d3.rapunzellib:platform-fabric:${libs.versions.rapunzellib.get()}")!!) + modImplementation(include("de.t14d3.rapunzellib:events-fabric:${libs.versions.rapunzellib.get()}")!!) } tasks.processResources { diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/FabricPermissionManager.java b/fabric/src/main/java/de/t14d3/zones/fabric/FabricPermissionManager.java index fe24d2e..4e7b8c3 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/FabricPermissionManager.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/FabricPermissionManager.java @@ -1,18 +1,16 @@ package de.t14d3.zones.fabric; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Zones; -import de.t14d3.zones.objects.BlockLocation; import de.t14d3.zones.objects.Flag; -import de.t14d3.zones.objects.World; import de.t14d3.zones.permissions.PermissionManager; import net.minecraft.core.BlockPos; import net.minecraft.world.entity.player.Player; public class FabricPermissionManager extends PermissionManager { - private final Zones zones; public FabricPermissionManager(Zones zones) { super(zones); - this.zones = zones; } public boolean checkAction(BlockPos pos, net.minecraft.world.level.Level nativeWorld, Player nativePlayer, Flag flag) { @@ -21,9 +19,9 @@ public boolean checkAction(BlockPos pos, net.minecraft.world.level.Level nativeW } public boolean checkAction(BlockPos pos, net.minecraft.world.level.Level nativeWorld, Player nativePlayer, String type, Flag flag) { - type = type.replace("minecraft:", ""); - BlockLocation location = BlockLocation.of(pos.getX(), pos.getY(), pos.getZ()); - World world = ((FabricPlatform) zones.getPlatform()).getWorld(nativeWorld); + RBlockPos location = new RBlockPos(pos.getX(), pos.getY(), pos.getZ()); + String worldKey = nativeWorld.dimension().location().toString(); + RWorldRef world = new RWorldRef(null, worldKey); return super.checkAction(location, world, nativePlayer.getStringUUID(), flag, type); } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java b/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java index 9ef105c..c3737fe 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java @@ -1,98 +1,36 @@ package de.t14d3.zones.fabric; + +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.Zones; import de.t14d3.zones.ZonesPlatform; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.objects.World; import de.t14d3.zones.permissions.PermissionManager; import de.t14d3.zones.utils.Types; import de.t14d3.zones.visuals.BeaconUtils; -import me.lucko.fabric.api.permissions.v0.Permissions; -import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.format.NamedTextColor; import net.minecraft.core.BlockPos; -import net.minecraft.core.Registry; import net.minecraft.core.particles.ParticleOptions; import net.minecraft.core.particles.SimpleParticleType; -import net.minecraft.core.registries.Registries; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; -import org.jetbrains.annotations.Nullable; import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.Executors; public class FabricPlatform implements ZonesPlatform { private final ZonesFabric mod; - private FabricPermissionManager permissionManager; - private List worlds; + public SimpleParticleType primary; public ParticleOptions secondary; public FabricPlatform(ZonesFabric mod) { this.mod = mod; - - } - - public void loadWorlds(MinecraftServer server) { - worlds = new ArrayList<>(); - server.getAllLevels().forEach(world -> worlds.add(World.of( - world.dimension().location().toString(), - UUID.nameUUIDFromBytes(world.dimension().location().toString().getBytes()) - ))); - } - - @Override - public List getWorlds() { - return worlds; - } - - @Override - public Player getPlayer(UUID uuid) { - String name = mod.getServer().getProfileCache().get(uuid).get().getName(); - return PlayerRepository.getOrAdd(name, uuid); - } - - public @Nullable Player getPlayer(String name) { - return PlayerRepository.getPlayers().stream().filter(player -> player.getName().equals(name)).findFirst() - .orElse(null); - } - - public ServerPlayer getNativePlayer(Player player) { - return mod.getServer().getPlayerList().getPlayer(player.getUUID()); - } - - @Override - public Audience getAudience(Player player) { - return getNativePlayer(player); - } - - @Override - public boolean hasPermission(Player player, String permission) { - return Permissions.check(player.getUUID(), permission).join(); - } - - @Override - public ZonesPlatform getPlatform() { - return this; - } - - public World getWorld(net.minecraft.world.level.Level nativeWorld) { - return getWorld(nativeWorld.dimension().location().toString()); - } - - @Override - public World getWorld(String world) { - return ZonesPlatform.super.getWorld(world); } @Override @@ -101,8 +39,8 @@ public File getDataFolder() { } @Override - public PermissionManager getPermissionManager() { - return mod.getPermissionManager(); + public PermissionManager createPermissionManager(Zones zones) { + return new FabricPermissionManager(zones); } @Override @@ -110,89 +48,83 @@ public Types getTypes() { return mod.getTypes(); } - public ZonesFabric getMod() { - return mod; - } - @Override - public World getWorld(Player player) { - return getWorld(getNativePlayer(player).level()); - } + public void spawnParticle(int type, RBlockPos particleLocation, RPlayer player) { + ServerPlayer serverPlayer = player.tryHandle(ServerPlayer.class).orElse(null); + if (serverPlayer == null) return; - public ServerLevel getNativeWorld(World world) { - var ref = new Object() { - ServerLevel level = null; - }; - mod.getServer().getAllLevels().forEach(serverLevel -> { - if (UUID.nameUUIDFromBytes(serverLevel.dimension().location().toString().getBytes()) - .equals(world.getUID())) { - ref.level = serverLevel; - } - }); - return ref.level; + serverPlayer.serverLevel().sendParticles( + serverPlayer, + (type == 1 ? primary : secondary), + false, + false, + particleLocation.x(), + particleLocation.y(), + particleLocation.z(), + 1, + 0.00D, + 0.0D, + 0.0D, + 0.0D + ); } @Override - public BlockLocation getLocation(Player player) { - return BlockLocation.of(getNativePlayer(player).getBlockX(), getNativePlayer(player).getBlockY(), - getNativePlayer(player).getBlockZ()); - } + public void showBeacon(RPlayer player, RBlockPos location, RWorldRef world, NamedTextColor color) { + if (location == null) return; + ServerPlayer nativePlayer = player.tryHandle(ServerPlayer.class).orElse(null); + if (nativePlayer == null) return; + ServerLevel nativeWorld = resolveWorld(world, nativePlayer); + showBeacon(nativePlayer, nativeWorld, location, color); + } + + private void showBeacon(ServerPlayer player, ServerLevel world, RBlockPos location, NamedTextColor color) { + RBlockPos adjusted = new RBlockPos(location.x(), location.y() + 1, location.z()); + for (BeaconUtils.BlockChange change : BeaconUtils.createList(adjusted, color)) { + BlockPos nativePos = new BlockPos(change.x(), change.y(), change.z()); + if (world.getBlockState(nativePos).getLightBlock() < 15) { + continue; + } - @Override - public String getMetadata(Player player, String key) { - // Format: zones:key:value - return getNativePlayer(player).getTags().stream().filter(tag -> - tag.startsWith("zones:" + key)).findFirst().map(tag -> - tag.substring(tag.indexOf(':') + 1).substring(tag.indexOf(':') + 1)).orElse(null); - } + Block block = BuiltInRegistries.BLOCK + .get(ResourceLocation.withDefaultNamespace(change.blockData().toLowerCase())) + .map(ref -> ref.value()) + .orElse(null); + if (block == null) { + continue; + } - @Override - public void setMetadata(Player player, String key, String value) { - getNativePlayer(player).getTags().removeIf(tag -> - tag.startsWith("zones:" + key)); - getNativePlayer(player).getTags().add("zones:" + key + ":" + value); + BlockState state = block.defaultBlockState(); + player.connection.send(new ClientboundBlockUpdatePacket(nativePos, state)); + } } @Override - public void spawnParticle(int type, BlockLocation particleLocation, Player player) { - ServerPlayer serverPlayer = getNativePlayer(player); - serverPlayer.serverLevel() - .sendParticles(serverPlayer, (type == 1 ? primary : secondary), false, false, particleLocation.getX(), - particleLocation.getY(), particleLocation.getZ(), 1, 0.0D, 0.0D, 0.0D, 0.0D); + public void removeBeacon(RPlayer player, RWorldRef world, RBlockPos location) { + if (location == null) return; + ServerPlayer nativePlayer = player.tryHandle(ServerPlayer.class).orElse(null); + if (nativePlayer == null) return; + ServerLevel nativeWorld = resolveWorld(world, nativePlayer); + removeBeacon(nativePlayer, nativeWorld, location); } - @Override - public void showBeacon(Player player, BlockLocation location, World world, NamedTextColor color) { - showBeacon(mod.getPlatform().getNativePlayer(player), location, color); + private void removeBeacon(ServerPlayer player, ServerLevel world, RBlockPos location) { + for (BeaconUtils.BlockChange change : BeaconUtils.resetList(location)) { + BlockPos nativePos = new BlockPos(change.x(), change.y(), change.z()); + BlockState state = world.getBlockState(nativePos); + player.connection.send(new ClientboundBlockUpdatePacket(nativePos, state)); + } } - private void showBeacon(ServerPlayer player, BlockLocation location, NamedTextColor color) { - Executors.newSingleThreadExecutor().execute(() -> { - Registry blocks = mod.getServer().registryAccess().lookupOrThrow(Registries.BLOCK); - for (BeaconUtils.BlockChange change : BeaconUtils.createList(location.setY(location.getY() + 1), color)) { - BlockPos nativePos = new BlockPos(change.getX(), change.getY(), change.getZ()); - if (player.serverLevel().getBlockState(nativePos).getLightBlock() < 15) { - continue; - } - BlockState state = blocks.get( - ResourceLocation.withDefaultNamespace(change.getBlockData().toLowerCase())) - .get().value().defaultBlockState(); - player.connection.send(new ClientboundBlockUpdatePacket(nativePos, state)); + private ServerLevel resolveWorld(RWorldRef world, ServerPlayer fallbackPlayer) { + if (world == null) return fallbackPlayer.serverLevel(); + String id = world.identifier(); + for (ServerLevel level : mod.getServer().getAllLevels()) { + if (level.dimension().location().toString().equals(id)) { + return level; } - }); - } - - @Override - public void removeBeacon(Player player, World world, BlockLocation location) { - if (location == null) return; - removeBeacon(mod.getPlatform().getNativePlayer(player), world, location); - } - - private void removeBeacon(ServerPlayer nativePlayer, World world, BlockLocation location) { - for (BeaconUtils.BlockChange change : BeaconUtils.resetList(location)) { - BlockPos nativePos = new BlockPos(change.getX(), change.getY(), change.getZ()); - BlockState state = getNativeWorld(world).getBlockState(nativePos); - nativePlayer.connection.send(new ClientboundBlockUpdatePacket(nativePos, state)); } + return fallbackPlayer.serverLevel(); } } + diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/FabricTypes.java b/fabric/src/main/java/de/t14d3/zones/fabric/FabricTypes.java index 65f39ec..6a6b4a3 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/FabricTypes.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/FabricTypes.java @@ -1,42 +1,57 @@ package de.t14d3.zones.fabric; import de.t14d3.zones.utils.Types; -import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; -import net.minecraft.core.registries.Registries; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Container; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.state.BlockState; public class FabricTypes extends Types { - private final ZonesFabric mod; public FabricTypes(ZonesFabric mod) { super(); - this.mod = mod; } @Override public void populateTypes() { - ServerLifecycleEvents.SERVER_STARTING.register(server -> { - server.registryAccess().lookupOrThrow(Registries.BLOCK).forEach(block -> { - final String id = block.getDescriptionId().replace("block.minecraft.", ""); - blockTypes.add(id); - blockTypes.add("!" + id); - allTypes.add(id); - allTypes.add("!" + id); - if (block.defaultBlockState().hasBlockEntity()) { + allTypes.clear(); + blockTypes.clear(); + entityTypes.clear(); + containerTypes.clear(); + redstoneTypes.clear(); + damageTypes.clear(); + + BuiltInRegistries.BLOCK.forEach(block -> { + ResourceLocation key = BuiltInRegistries.BLOCK.getKey(block); + if (key == null) return; + String id = key.getPath(); + + blockTypes.add(id); + allTypes.add(id); + + BlockState state = block.defaultBlockState(); + if (state.hasBlockEntity() && block instanceof EntityBlock entityBlock) { + var blockEntity = entityBlock.newBlockEntity(BlockPos.ZERO, state); + if (blockEntity instanceof Container) { containerTypes.add(id); - containerTypes.add("!" + id); } - if (block.defaultBlockState().isSignalSource()) { - redstoneTypes.add(id); - redstoneTypes.add("!" + id); - } - }); - server.registryAccess().lookupOrThrow(Registries.ENTITY_TYPE).forEach(entity -> { - final String id = entity.getDescriptionId().replace("entity.minecraft.", ""); - entityTypes.add(id); - entityTypes.add("!" + id); - allTypes.add(id); - allTypes.add("!" + id); - }); + } + + if (state.isSignalSource()) { + redstoneTypes.add(id); + } }); + + BuiltInRegistries.ENTITY_TYPE.forEach(entityType -> { + ResourceLocation key = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + if (key == null) return; + String id = key.getPath(); + + entityTypes.add(id); + allTypes.add(id); + }); + } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java b/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java index 9b93c6e..a4196c4 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java @@ -1,39 +1,43 @@ package de.t14d3.zones.fabric; +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.events.GameEvents; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.platform.fabric.FabricRapunzelBootstrap; import de.t14d3.zones.RegionManager; import de.t14d3.zones.Zones; import de.t14d3.zones.ZonesPlatform; import de.t14d3.zones.fabric.commands.RootCommand; -import de.t14d3.zones.fabric.listeners.PlayerListener; -import de.t14d3.zones.utils.Messages; +import de.t14d3.zones.rapunzellib.ZonesRapunzelHooks; import de.t14d3.zones.utils.Types; -import me.lucko.fabric.api.permissions.v0.PermissionCheckEvent; import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; -import net.fabricmc.fabric.api.util.TriState; import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.core.Registry; import net.minecraft.core.particles.ParticleType; import net.minecraft.core.particles.SimpleParticleType; -import net.minecraft.core.registries.Registries; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import org.jetbrains.annotations.NotNull; import java.io.File; -import java.util.concurrent.atomic.AtomicReference; +/** + * Fabric entrypoint for Zones. + * + *

Initializes Zones on dedicated servers and wires Fabric lifecycle callbacks into RapunzelLib.

+ */ public class ZonesFabric implements DedicatedServerModInitializer { + private static final String MOD_ID = "zones"; + private ZonesPlatform platform; private FabricTypes types; private File dataFolder; private MinecraftServer server; private Zones zones; private FabricPermissionManager permissionManager; - private PlayerListener playerListener; private RegionManager regionManager; - private Messages messages; private RootCommand rootCommand; @Override @@ -49,85 +53,98 @@ public void onInitializeServer() { this.zones = new Zones(platform); this.regionManager = zones.getRegionManager(); - this.messages = zones.getMessages(); - - this.permissionManager = new FabricPermissionManager(zones); - this.rootCommand = new RootCommand(this); + this.permissionManager = (FabricPermissionManager) zones.getPermissionManager(); Zones.getInstance().getLogger().info("Zones Fabric mod initialized!"); } - private void onEnable(@NotNull MinecraftServer server) { + void onEnable(@NotNull MinecraftServer server) { this.server = server; - this.playerListener = new PlayerListener(this); - - // If luckperms is not loaded, register our own scuffed permission system, - // otherwise skip to have luckperms handle permissions - if (!FabricLoader.getInstance().isModLoaded("luckperms")) { - PermissionCheckEvent.EVENT.register((source, permission) -> { - if (!permission.startsWith("zones.")) { - return TriState.DEFAULT; - } - AtomicReference result = new AtomicReference<>(TriState.DEFAULT); - permissionManager.getPermissions().stream().filter(p -> p.getName().equals(permission)).findFirst() - .ifPresent(p -> { - if (source.hasPermission(p.getLevel())) { - result.set(TriState.TRUE); - } - }); - return result.get(); - }); - } - } - private void onStarted(MinecraftServer server) { - ((FabricPlatform) zones.getPlatform()).loadWorlds(server); + FabricRapunzelBootstrap.bootstrap(MOD_ID, server, ZonesFabric.class); + GameEvents.install(this); + ZonesRapunzelHooks.install(zones); + this.rootCommand = new RootCommand(this); + // Fabric callbacks are bridged into RapunzelLib GameEvents; Zones subscribes via ZonesRapunzelHooks. + } + void onStarted(MinecraftServer server) { regionManager.loadRegions(); zones.getLogger().info("Mod enabled, loaded {} regions.", zones.getRegionManager().regions().size()); String primaryType = zones.getConfig().getString("visuals.particles.primary", "WAX_OFF").toLowerCase(); String secondaryType = zones.getConfig().getString("visuals.particles.secondary", "WAX_ON").toLowerCase(); - Registry> registry = server.registryAccess().lookupOrThrow(Registries.PARTICLE_TYPE); - ((FabricPlatform) platform).primary = (SimpleParticleType) registry.get( - ResourceLocation.withDefaultNamespace(primaryType)).get().value(); - ((FabricPlatform) platform).secondary = (SimpleParticleType) registry.get( - ResourceLocation.withDefaultNamespace(secondaryType)).get().value(); + ParticleType primary = BuiltInRegistries.PARTICLE_TYPE + .get(ResourceLocation.withDefaultNamespace(primaryType)) + .map(ref -> ref.value()) + .orElseThrow(); + ParticleType secondary = BuiltInRegistries.PARTICLE_TYPE + .get(ResourceLocation.withDefaultNamespace(secondaryType)) + .map(ref -> ref.value()) + .orElseThrow(); + ((FabricPlatform) platform).primary = (SimpleParticleType) primary; + ((FabricPlatform) platform).secondary = (SimpleParticleType) secondary; + } - private void onDisable(MinecraftServer server) { + void onDisable(MinecraftServer server) { regionManager.saveRegions(); + Rapunzel.shutdown(MOD_ID); } + /** + * Returns the mod's data folder (config directory). + */ public File getDataFolder() { return dataFolder; } + /** + * Returns the permission manager implementation for Fabric. + */ public FabricPermissionManager getPermissionManager() { return this.permissionManager; } + /** + * Returns the platform types registry for Fabric. + */ public Types getTypes() { return types; } + /** + * Returns the Minecraft server instance (available after SERVER_STARTING). + */ public MinecraftServer getServer() { return server; } + /** + * Returns the shared region manager. + */ public RegionManager getRegionManager() { return regionManager; } - public Messages getMessages() { - return messages; + /** + * Returns the message formatter service used by Zones. + */ + public MessageFormatService getMessages() { + return zones.getMessages(); } + /** + * Returns the core Zones instance. + */ public Zones getZones() { return zones; } + /** + * Returns the Fabric platform bridge. + */ public FabricPlatform getPlatform() { return (FabricPlatform) platform; } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/CancelCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/CancelCommand.java index 92f8a4f..04e8d1e 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/CancelCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/CancelCommand.java @@ -1,14 +1,16 @@ package de.t14d3.zones.fabric.commands; import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.utils.Messages; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import net.minecraft.commands.CommandSourceStack; public class CancelCommand { - private final Messages messages; + private final MessageFormatService messages; private final ZonesFabric mod; public CancelCommand(ZonesFabric mod) { @@ -17,14 +19,23 @@ public CancelCommand(ZonesFabric mod) { } int execute(CommandContext context) { - if (context.getSource().getPlayer() != null) { - Player player = PlayerRepository.get(context.getSource().getPlayer().getUUID()); - mod.getPlatform().removeBeacon(player, player.getSelection().getWorld(), player.getSelection().getMin()); - mod.getPlatform().removeBeacon(player, player.getSelection().getWorld(), player.getSelection().getMax()); - player.setSelection(null); - player.setSelectionCreating(false); - player.sendMessage(messages.getCmp("commands.cancel.success")); + if (context.getSource().getPlayer() == null) { + return 0; } + RPlayer player = RPlayer.wrap(context.getSource().getPlayer()).orElse(null); + if (player == null) return 0; + + Box selection = player.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + if (selection != null) { + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : player.worldOrThrow() + .ref(); + mod.getPlatform().removeBeacon(player, selectionWorld, selection.getMin()); + mod.getPlatform().removeBeacon(player, selectionWorld, selection.getMax()); + } + + player.extras().remove(ZonesExtraKeys.SELECTION); + player.extras().put(ZonesExtraKeys.SELECTION_CREATING, false); + player.sendMessage(messages.component("commands.cancel.success")); return 1; } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/CommandPermissions.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/CommandPermissions.java new file mode 100644 index 0000000..282a79b --- /dev/null +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/CommandPermissions.java @@ -0,0 +1,44 @@ +package de.t14d3.zones.fabric.commands; + +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.Zones; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.level.ServerPlayer; + +/** + * Fabric command permission checks. + * + *

Uses vanilla permission levels (via {@code permissions.json}) as a fallback, and + * delegates to RapunzelLib's {@link RPlayer#hasPermission(String)} for node-based + * permissions when available.

+ */ +public final class CommandPermissions { + private CommandPermissions() { + } + + public static boolean check(CommandSourceStack source, String permission) { + if (source == null || permission == null || permission.isBlank()) return false; + + // First try vanilla permission level (permissions.json), then fall back to node-based checks for players. + int level = 4; + Zones zones = Zones.getInstance(); + if (zones != null) { + var manager = zones.getPermissionManager(); + if (manager != null) { + for (var p : manager.getPermissions()) { + if (p != null && permission.equalsIgnoreCase(p.name())) { + level = p.level(); + break; + } + } + } + } + if (source.hasPermission(level)) return true; + + ServerPlayer player = source.getPlayer(); + if (player == null) return false; + + return RPlayer.wrap(player).map(p -> p.hasPermission(permission)).orElse(false); + } + +} diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/CreateCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/CreateCommand.java index fa1815c..55410fc 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/CreateCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/CreateCommand.java @@ -1,29 +1,23 @@ package de.t14d3.zones.fabric.commands; import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.fabric.FabricPlatform; import de.t14d3.zones.fabric.ZonesFabric; import de.t14d3.zones.objects.Box; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.objects.RegionFlagEntry; -import de.t14d3.zones.utils.Messages; -import me.lucko.fabric.api.permissions.v0.Permissions; -import net.kyori.adventure.text.minimessage.MiniMessage; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import net.minecraft.commands.CommandSourceStack; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class CreateCommand { private final RegionManager regionManager; - private final Messages messages; + private final MessageFormatService messages; private final FabricPlatform platform; - private final MiniMessage mm = MiniMessage.miniMessage(); public CreateCommand(ZonesFabric mod) { this.regionManager = mod.getRegionManager(); @@ -32,35 +26,64 @@ public CreateCommand(ZonesFabric mod) { } int execute(CommandContext context) { - if (context.getSource().getPlayer() != null) { - Player player = platform.getPlayer(context.getSource().getPlayer().getUUID()); - if (player.getSelection() == null) { - player.setSelection(new Box(null, null, platform.getWorld(context.getSource().getLevel()), false)); - player.setSelectionCreating(true); - player.sendMessage(messages.getCmp("commands.create.click-corners")); - return 1; - } - Box selection = player.getSelection(); - if (selection.getMin() != null && selection.getMax() != null) { - if (regionManager.overlapsExistingRegion(selection) - && !Permissions.check(context.getSource(), "zones.create.overlap")) { - player.sendMessage(messages.getCmp("commands.create.overlap")); - return 1; - } - Map> members = new HashMap<>(); - members.put(player.getUniqueId().toString(), List.of(new RegionFlagEntry("role", "owner", false))); - RegionKey key = RegionKey.generate(); - regionManager.createNewRegion(key.toString(), selection.getMin(), - selection.getMax(), selection.getWorld(), members, key, null, 0); - player.sendMessage( - mm.deserialize(messages.get("commands.create.success"), - parsed("region", key.toString()))); - player.setSelection(null); - player.setSelectionCreating(false); - } else { - context.getSource().sendMessage(mm.deserialize(messages.get("commands.create.click-corners"))); - } + if (context.getSource().getPlayer() == null) { + return 0; + } + + var nativePlayer = context.getSource().getPlayer(); + RPlayer player = RPlayer.wrap(nativePlayer).orElse(null); + if (player == null) return 0; + + Box selection = player.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + boolean selecting = player.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false); + + if (selection == null || !selecting) { + player.extras().put(ZonesExtraKeys.SELECTION, new Box(player.worldOrThrow().ref())); + player.extras().put(ZonesExtraKeys.SELECTION_CREATING, true); + player.sendMessage(messages.component("commands.create.click-corners")); + return 1; + } + + if (selection.getMin() == null || selection.getMax() == null) { + player.sendMessage(messages.component("commands.create.click-corners")); + return 1; } + + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : player.worldOrThrow().ref(); + + if (regionManager.overlapsExistingRegion(selection) && !CommandPermissions.check(context.getSource(), + "zones.create.overlap")) { + player.sendMessage(messages.component("commands.create.overlap")); + return 1; + } + + RegionPermissions permissions = new RegionPermissions(); + + RegionKey key = RegionKey.generate(); + var created = regionManager.createNewRegion( + key.toString(), + selection.getMin(), + selection.getMax(), + selectionWorld, + permissions, + key, + null, + 0 + ); + if (created != null) { + regionManager.withWorldWriteLock(created.getWorld(), () -> { + created.getMembership().player(player.uuid()).roles().allow("owner"); + regionManager.saveRegion(created.getKey(), created); + }); + } + + platform.removeBeacon(player, selectionWorld, selection.getMin()); + platform.removeBeacon(player, selectionWorld, selection.getMax()); + player.extras().remove(ZonesExtraKeys.SELECTION); + player.extras().put(ZonesExtraKeys.SELECTION_CREATING, false); + + player.sendMessage(messages.component("commands.create.success", + Placeholders.builder().string("region", key.toString()).build())); return 1; } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/DeleteCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/DeleteCommand.java index bdf0734..c759df5 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/DeleteCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/DeleteCommand.java @@ -1,53 +1,47 @@ package de.t14d3.zones.fabric.commands; import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; -import de.t14d3.zones.fabric.FabricPlatform; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.utils.Messages; -import me.lucko.fabric.api.permissions.v0.Permissions; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.minecraft.commands.CommandSourceStack; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class DeleteCommand { private final RegionManager regionManager; - private final Messages messages; - private final FabricPlatform platform; - private final MiniMessage mm = MiniMessage.miniMessage(); + private final MessageFormatService messages; public DeleteCommand(ZonesFabric mod) { this.regionManager = mod.getRegionManager(); this.messages = mod.getMessages(); - this.platform = mod.getPlatform(); } int execute(CommandContext context) { - if (context.getSource().getPlayer() != null) { - Player player = platform.getPlayer(context.getSource().getPlayer().getUUID()); - Region region = regionManager.regions() - .get(RegionKey.fromString(context.getArgument("key", String.class)).getValue()); - if (region == null) { - context.getSource().sendMessage(messages.getCmp("commands.invalid-region")); + if (context.getSource().getPlayer() == null) { + context.getSource().sendMessage(messages.component("commands.only-player")); + return 0; + } + + RegionKey key = RegionKey.fromString(context.getArgument("key", String.class)); + Region region = regionManager.regions().get(key.getValue()); + if (region == null) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); + return 1; + } + + if (!CommandPermissions.check(context.getSource(), "zones.delete.other")) { + if (!regionManager.withWorldReadLock(region.getWorld(), + () -> region.isAdmin(context.getSource().getPlayer().getUUID()))) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); return 1; } - if (!Permissions.check(context.getSource(), "zones.delete.other")) { - if (context.getSource().getPlayer() != null && !region.isAdmin( - context.getSource().getPlayer().getUUID())) { - context.getSource().sendMessage(messages.getCmp("commands.invalid-region")); - return 1; - } - } - regionManager.deleteRegion(region.getKey()); - context.getSource().sendMessage( - mm.deserialize(messages.get("commands.delete.success"), - parsed("region", region.getKey().toString()))); - return 1; } - return 0; + + regionManager.deleteRegion(region.getKey()); + context.getSource().sendMessage(messages.component("commands.delete.success", + Placeholders.builder().string("region", region.getKey().toString()).build())); + return 1; } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java index cd2affa..cf4482b 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java @@ -5,23 +5,19 @@ import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.fabric.ZonesFabric; import de.t14d3.zones.objects.Direction; -import de.t14d3.zones.utils.Messages; -import me.lucko.fabric.api.permissions.v0.Permissions; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class ExpandCommand { private final RegionManager regionManager; - private final Messages messages; - private final MiniMessage mm = MiniMessage.miniMessage(); + private final MessageFormatService messages; public ExpandCommand(ZonesFabric mod) { this.regionManager = mod.getRegionManager(); @@ -32,19 +28,22 @@ int execute(CommandContext context) { Region region = regionManager.regions() .get(RegionKey.fromString(context.getArgument("key", String.class)).getValue()); if (region == null) { - context.getSource().sendMessage(messages.getCmp("commands.invalid-region")); + context.getSource().sendMessage(messages.component("commands.invalid-region")); return 1; } - if (!Permissions.check(context.getSource(), "zones.expand.other") && - (context.getSource().getPlayer() == null || !region.isOwner( - context.getSource().getPlayer().getUUID()))) { - context.getSource().sendMessage(messages.getCmp("commands.invalid-region")); - return 1; + if (!CommandPermissions.check(context.getSource(), "zones.expand.other")) { + if (context.getSource().getPlayer() == null + || !regionManager.withWorldReadLock(region.getWorld(), + () -> region.isOwner(context.getSource().getPlayer().getUUID()))) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); + return 1; + } } int amount = context.getArgument("amount", Integer.class); boolean allowOverlap = false; try { - allowOverlap = context.getArgument("overlap", Boolean.class) && Permissions.check(context.getSource(), + allowOverlap = context.getArgument("overlap", Boolean.class) && CommandPermissions.check( + context.getSource(), "zones.expand.overlap"); } catch (Exception ignored) { } @@ -56,25 +55,23 @@ int execute(CommandContext context) { if (context.getSource().getPlayer() != null) { direction = Direction.fromYaw(context.getSource().getPlayer().getViewYRot(1.0f)); } else { - context.getSource().sendMessage(messages.getCmp("commands.invalid")); + context.getSource().sendMessage(messages.component("commands.invalid")); return 1; } } if (regionManager.expandBounds(region, direction, amount, allowOverlap)) { - context.getSource().sendMessage( - mm.deserialize(messages.get("commands.expand.success"), - parsed("region", region.getKey().toString()))); + context.getSource().sendMessage(messages.component("commands.expand.success", + Placeholders.builder().string("region", region.getKey().toString()).build())); } else { - context.getSource().sendMessage( - mm.deserialize(messages.get("commands.expand.fail"), - parsed("region", region.getKey().toString()))); + context.getSource().sendMessage(messages.component("commands.expand.fail", + Placeholders.builder().string("region", region.getKey().toString()).build())); } return 1; } LiteralArgumentBuilder command() { return Commands.literal("expand") - .requires(source -> Permissions.check(source, "zones.expand")) + .requires(source -> CommandPermissions.check(source, "zones.expand")) .then(Commands.argument("key", StringArgumentType.string()) .suggests(RootCommand::regionKeySuggestion) .then(Commands.argument("amount", IntegerArgumentType.integer()) @@ -90,7 +87,8 @@ LiteralArgumentBuilder command() { }) .executes(this::execute) .then(Commands.argument("overlap", BoolArgumentType.bool()) - .requires(source -> Permissions.check(source, "zones.expand.overlap")) + .requires(source -> CommandPermissions.check(source, + "zones.expand.overlap")) .executes(this::execute))))); } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/FindCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/FindCommand.java index d2bcf1b..d725375 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/FindCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/FindCommand.java @@ -1,8 +1,9 @@ package de.t14d3.zones.fabric.commands; + import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.Player; import net.minecraft.commands.CommandSourceStack; public class FindCommand { @@ -13,18 +14,24 @@ public FindCommand(ZonesFabric mod) { } int execute(CommandContext context) { - if (context.getSource().getPlayer() != null) { - Player player = mod.getPlatform().getPlayer(context.getSource().getPlayer().getUUID()); - if (mod.getZones().getFindBossbar().players.containsKey(player)) { - mod.getPlatform().getAudience(player).hideBossBar(mod.getZones().getFindBossbar().players.get(player)); - mod.getZones().getFindBossbar().players.remove(player); - } else { - mod.getZones().getFindBossbar().players.put(player, - null); // null to let the bossbar handler do the creation + if (context.getSource().getPlayer() == null) { + context.getSource().sendMessage(mod.getMessages().component("commands.only-player")); + return 0; + } + + RPlayer player = RPlayer.wrap(context.getSource().getPlayer()).orElse(null); + if (player == null) return 0; + + if (mod.getZones().getFindBossbar().players.containsKey(player)) { + var bar = mod.getZones().getFindBossbar().players.get(player); + if (bar != null) { + player.audience().hideBossBar(bar); } + mod.getZones().getFindBossbar().players.remove(player); } else { - context.getSource().sendMessage(mod.getMessages().getCmp("commands.only-player")); + mod.getZones().getFindBossbar().players.put(player, null); } return 1; } } + diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/InfoCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/InfoCommand.java index 7f6ea65..d9d7564 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/InfoCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/InfoCommand.java @@ -1,24 +1,25 @@ package de.t14d3.zones.fabric.commands; import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.objects.World; import de.t14d3.zones.utils.Messages; -import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.commands.CommandSourceStack; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class InfoCommand { private final ZonesFabric mod; private final RegionManager regionManager; - private final Messages messages; + private final MessageFormatService messages; public InfoCommand(ZonesFabric mod) { this.mod = mod; @@ -28,31 +29,43 @@ public InfoCommand(ZonesFabric mod) { int execute(CommandContext context) { List regions = new ArrayList<>(); - Player player = null; + UUID playerUuid = null; + try { - regions.add(regionManager.regions() - .get(RegionKey.fromString(context.getArgument("key", String.class)).getValue())); + Region region = regionManager.regions() + .get(RegionKey.fromString(context.getArgument("key", String.class)).getValue()); + if (region != null) { + regions.add(region); + } } catch (Exception ignored) { - if (context.getSource().getPlayer() != null) { - player = mod.getPlatform().getPlayer(context.getSource().getPlayer().getUUID()); - - BlockLocation location = new BlockLocation(context.getSource().getPlayer().getBlockX(), - context.getSource().getPlayer().getBlockY(), - context.getSource().getPlayer().getBlockZ()); - World world = (mod.getPlatform()).getWorld( - context.getSource().getPlayer().getCommandSenderWorld()); - regions = regionManager.getRegionsAt(location, world); - } else { - // Sender is not player and no region key provided - context.getSource().sendMessage(messages.getCmp("commands.invalid-region")); + } + + if (regions.isEmpty()) { + if (context.getSource().getPlayer() == null) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); return 1; } + + RPlayer player = RPlayer.wrap(context.getSource().getPlayer()).orElse(null); + if (player == null) return 0; + playerUuid = player.uuid(); + + RBlockPos pos = player.locationOrThrow().blockPos(); + RWorldRef world = player.worldOrThrow().ref(); + regions = regionManager.getRegionsAt(pos, world); } + for (Region region : regions) { - if (Permissions.check(context.getSource(), "zones.info.other")) { - context.getSource().sendMessage(Messages.regionInfo(region, true)); - } else if (player != null && region.isMember(player.getUniqueId())) { - context.getSource().sendMessage(Messages.regionInfo(region, region.isAdmin(player.getUniqueId()))); + if (CommandPermissions.check(context.getSource(), "zones.info.other")) { + context.getSource().sendMessage( + regionManager.withWorldReadLock(region.getWorld(), () -> Messages.regionInfo(region, true))); + } else if (playerUuid != null) { + UUID uuid = playerUuid; + var msg = regionManager.withWorldReadLock(region.getWorld(), () -> { + if (!region.isMember(uuid)) return null; + return Messages.regionInfo(region, region.isAdmin(uuid)); + }); + if (msg != null) context.getSource().sendMessage(msg); } } return 1; diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ListCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ListCommand.java index 9677434..b2a7642 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ListCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ListCommand.java @@ -1,66 +1,66 @@ package de.t14d3.zones.fabric.commands; + import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.Region; import de.t14d3.zones.RegionManager; -import de.t14d3.zones.fabric.FabricPlatform; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.Player; import de.t14d3.zones.utils.Messages; -import me.lucko.fabric.api.permissions.v0.Permissions; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.minecraft.commands.CommandSourceStack; import java.util.List; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; +import java.util.UUID; public class ListCommand { - private final MiniMessage mm = MiniMessage.miniMessage(); private final RegionManager regionManager; - private final Messages messages; - private final FabricPlatform platform; + private final MessageFormatService messages; public ListCommand(ZonesFabric mod) { this.regionManager = mod.getRegionManager(); this.messages = mod.getMessages(); - this.platform = mod.getPlatform(); } int execute(CommandContext context, int page) { - boolean perm = Permissions.check(context.getSource(), "zones.info.other"); - if (page < 1) { - page = 1; - } - Player player = context.getSource().getPlayer() != null ? platform.getPlayer( - context.getSource().getPlayer().getUUID()) : null; - List regions = regionManager.regions().values().parallelStream() - .filter(region -> perm || (player != null && region.isMember(player.getUniqueId()))) + boolean perm = CommandPermissions.check(context.getSource(), "zones.info.other"); + if (page < 1) page = 1; + + UUID uuid = context.getSource().getPlayer() != null ? context.getSource().getPlayer().getUUID() : null; + List regions = regionManager.regions().values().stream() + .filter(region -> perm || (uuid != null && regionManager.withWorldReadLock(region.getWorld(), + () -> region.isMember(uuid)))) .toList(); + if (regions.isEmpty()) { - context.getSource().sendMessage(messages.getCmp("region.none-found")); + context.getSource().sendMessage(messages.component("region.none-found")); return 1; } - regions = regions.subList((page - 1) * 10, Math.min(regions.size(), page * 10)); + + int from = (page - 1) * 10; + if (from >= regions.size()) from = 0; + regions = regions.subList(from, Math.min(regions.size(), from + 10)); + Component[] msgs = new Component[regions.size()]; - int i = 0; - for (Region region : regions) { + for (int i = 0; i < regions.size(); i++) { + Region region = regions.get(i); + Component hoverText = regionManager.withWorldReadLock(region.getWorld(), () -> { + boolean canSeeAdminInfo = perm || (uuid != null && region.isAdmin(uuid)); + return Messages.regionInfo(region, canSeeAdminInfo); + }); msgs[i] = Component.newline() - .append(mm.deserialize(messages.get("region.info.name"), - parsed("name", region.getName()), - parsed("key", region.getKey().toString())) - .hoverEvent(HoverEvent.showText(Messages.regionInfo(region, - (perm || region.isAdmin(player.getUniqueId()))))) + .append(messages.component("region.info.name", + Placeholders.builder().string("name", region.getName()).build()) + .hoverEvent(HoverEvent.showText(hoverText)) .clickEvent(ClickEvent.runCommand("/zone info " + region.getKey())) ); - i++; } - Component msg = Component.textOfChildren(msgs); - context.getSource().sendMessage(msg); + context.getSource().sendMessage(Component.textOfChildren(msgs)); return 1; } } + diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/LoadCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/LoadCommand.java index 3b21f0e..593f890 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/LoadCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/LoadCommand.java @@ -1,15 +1,12 @@ package de.t14d3.zones.fabric.commands; import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.fabric.ZonesFabric; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.minecraft.commands.CommandSourceStack; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class LoadCommand { private final ZonesFabric mod; - private final MiniMessage mm = MiniMessage.miniMessage(); public LoadCommand(ZonesFabric mod) { this.mod = mod; @@ -18,8 +15,8 @@ public LoadCommand(ZonesFabric mod) { int execute(CommandContext context) { mod.getRegionManager().loadRegions(); int count = mod.getRegionManager().regions().size(); - context.getSource().sendMessage( - mm.deserialize(mod.getMessages().get("commands.load"), parsed("count", String.valueOf(count)))); + context.getSource().sendMessage(mod.getMessages().component("commands.load", + Placeholders.builder().string("count", String.valueOf(count)).build())); return 1; } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ModeCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ModeCommand.java index a21393d..54d80d3 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ModeCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ModeCommand.java @@ -1,37 +1,41 @@ package de.t14d3.zones.fabric.commands; + import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.Player; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import de.t14d3.zones.utils.Utils; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.minecraft.commands.CommandSourceStack; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class ModeCommand { private final ZonesFabric mod; - private final MiniMessage mm = MiniMessage.miniMessage(); public ModeCommand(ZonesFabric mod) { this.mod = mod; } int execute(CommandContext context) { - if (context.getSource().getPlayer() != null) { - Player player = mod.getPlatform().getPlayer(context.getSource().getPlayer().getUUID()); - Utils.SelectionMode mode; - try { - mode = Utils.SelectionMode.valueOf(context.getArgument("mode", String.class)); - } catch (Exception e) { - mode = Utils.SelectionMode.CUBOID_3D; - } - player.setMetadata("mode", mode.name()); - context.getSource().sendMessage(mm.deserialize(mod.getMessages().get("commands.mode.set"), - parsed("mode", mode.name()))); - } else { - context.getSource().sendMessage(mod.getMessages().getCmp("commands.only-player")); + if (context.getSource().getPlayer() == null) { + context.getSource().sendMessage(mod.getMessages().component("commands.only-player")); + return 0; } + + RPlayer player = RPlayer.wrap(context.getSource().getPlayer()).orElse(null); + if (player == null) return 0; + + Utils.SelectionMode mode; + try { + mode = Utils.SelectionMode.getMode(context.getArgument("mode", String.class)); + } catch (Exception e) { + mode = Utils.SelectionMode.CUBOID_2D; + } + + player.extras().put(ZonesExtraKeys.SELECTION_MODE, mode.name()); + context.getSource().sendMessage(mod.getMessages().component("commands.mode.set", + Placeholders.builder().string("mode", mode.getName()).build())); return 1; } } + diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java new file mode 100644 index 0000000..563e18c --- /dev/null +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java @@ -0,0 +1,616 @@ +package de.t14d3.zones.fabric.commands; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.zones.Region; +import de.t14d3.zones.RegionKey; +import de.t14d3.zones.RegionManager; +import de.t14d3.zones.fabric.ZonesFabric; +import de.t14d3.zones.objects.Flag; +import de.t14d3.zones.permissions.RegionMembership; +import de.t14d3.zones.permissions.RegionPermissions; +import de.t14d3.zones.permissions.flags.FlagValueKind; +import de.t14d3.zones.permissions.flags.Flags; +import de.t14d3.zones.permissions.subjects.GroupSubject; +import de.t14d3.zones.permissions.subjects.PlayerSubject; +import de.t14d3.zones.permissions.subjects.SubjectRef; +import de.t14d3.zones.permissions.subjects.Subjects; +import de.t14d3.zones.utils.TypeKeys; +import net.kyori.adventure.platform.modcommon.impl.NonWrappingComponentSerializer; +import net.kyori.adventure.platform.modcommon.impl.WrappedComponent; +import net.kyori.adventure.text.Component; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; + +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Fabric implementation of the {@code /zone perm} command tree. + * + *

Uses explicit subcommands (allow/deny/unset/clear). For target-based flags, use {@code all} as the wildcard + * pattern (stored internally as {@code *}).

+ */ +public class PermCommand { + private final RegionManager regionManager; + private final MessageFormatService messages; + private final ZonesFabric mod; + + /** + * Creates a new permission command tree builder. + * + * @param mod owning mod instance + */ + public PermCommand(ZonesFabric mod) { + this.mod = mod; + this.regionManager = mod.getRegionManager(); + this.messages = mod.getMessages(); + } + + /** + * Builds the brigadier command node for {@code /zone perm}. + * + * @return command node builder + */ + public LiteralArgumentBuilder command() { + return Commands.literal("perm") + .requires(source -> CommandPermissions.check(source, "zones.set")) + .then(allow()) + .then(deny()) + .then(unset()) + .then(clear()); + } + + private LiteralArgumentBuilder allow() { + return patternCommand("allow", true, false); + } + + private LiteralArgumentBuilder deny() { + return patternCommand("deny", false, false); + } + + LiteralArgumentBuilder unset() { + return patternCommand("unset", true, true); + } + + LiteralArgumentBuilder clear() { + return Commands.literal("clear") + .then(Commands.argument("key", StringArgumentType.string()) + .suggests(RootCommand::regionKeySuggestion) + .then(Commands.argument("target", StringArgumentType.string()) + .suggests(this::suggestTargets) + .then(Commands.argument("flag", StringArgumentType.string()) + .suggests(this::suggestFlags) + .executes(this::executeClear)))); + } + + private LiteralArgumentBuilder patternCommand(String name, boolean allow, boolean remove) { + return Commands.literal(name) + .then(Commands.argument("key", StringArgumentType.string()) + .suggests(RootCommand::regionKeySuggestion) + .then(Commands.argument("target", StringArgumentType.string()) + .suggests(this::suggestTargets) + .then(Commands.argument("flag", StringArgumentType.string()) + .suggests(this::suggestFlags) + .then(Commands.argument("pattern", StringArgumentType.string()) + .suggests(this::suggestPatterns) + .executes(context -> executePattern(context, allow, remove)))))); + } + + private CompletableFuture suggestTargets(CommandContext context, SuggestionsBuilder builder) { + Region region = resolveRegion(context); + if (region == null) return builder.buildFuture(); + if (!canEditRegion(context.getSource(), region)) return builder.buildFuture(); + + builder.suggest("+universal", new WrappedComponent( + messages.component("flags.universal"), + null, + null, + NonWrappingComponentSerializer.INSTANCE + )); + + regionManager.withWorldReadLock(region.getWorld(), () -> { + for (String group : region.getGroupNames()) { + builder.suggest("group:" + group); + } + }); + + for (RPlayer p : RPlayer.online()) { + builder.suggest(p.name(), new WrappedComponent( + Component.text(p.uuid().toString()), + null, + null, + NonWrappingComponentSerializer.INSTANCE + )); + } + + return builder.buildFuture(); + } + + CompletableFuture suggestPatterns(CommandContext context, SuggestionsBuilder builder) { + builder.suggest("*"); + builder.suggest("all"); + + String flagName = null; + try { + flagName = context.getArgument("flag", String.class); + } catch (Exception ignored) { + } + Flag flag = flagName == null ? null : Flags.getFlag(flagName); + if (flag != null) { + FlagValueKind kind = flag.valueKind(); + for (String v : flag.getValidValues()) { + if (v == null || v.isBlank()) continue; + String normalized = v.trim().toLowerCase(Locale.ROOT); + if (normalized.startsWith("!")) continue; + if (kind == FlagValueKind.TARGET_DECISION + && ("true".equals(normalized) + || "false".equals(normalized) + || "owner".equals(normalized) + || "admin".equals(normalized))) { + continue; + } + builder.suggest(normalized); + } + } + return builder.buildFuture(); + } + + private CompletableFuture suggestFlags(CommandContext context, SuggestionsBuilder builder) { + for (Flag flag : Flags.getFlags()) { + builder.suggest(flag.name(), new WrappedComponent( + messages.component("flags." + flag.name()), + null, + null, + NonWrappingComponentSerializer.INSTANCE + )); + } + return builder.buildFuture(); + } + + int executePattern(CommandContext context, boolean allow, boolean remove) { + Region region = resolveRegion(context); + if (region == null) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); + return 1; + } + if (!canEditRegion(context.getSource(), region)) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); + return 1; + } + + String rawTarget = context.getArgument("target", String.class); + SubjectRef subject = parseTargetSubject(context.getSource(), rawTarget); + if (subject == null) { + context.getSource().sendMessage(messages.component("commands.perm.invalid-target")); + return 1; + } + + String flagName = context.getArgument("flag", String.class); + Flag flag = Flags.getFlag(flagName); + if (flag == null) { + context.getSource().sendMessage(messages.component("commands.invalid-permission")); + return 1; + } + + if (context.getSource().getPlayer() != null) { + RPlayer player = RPlayer.wrap(context.getSource().getPlayer()).orElse(null); + if (player != null && !flag.canSet(player, mod.getPlatform())) { + context.getSource().sendMessage(messages.component("commands.invalid-permission")); + return 1; + } + } + + String rawPattern = context.getArgument("pattern", String.class); + + boolean changed = regionManager.withWorldWriteLock(region.getWorld(), () -> { + RegionPermissions perms = region.getPermissions(); + RegionMembership membership = region.getMembership(); + + boolean didChange; + if (flag.valueKind() == FlagValueKind.STRING_SET && (flag == Flags.ROLE || flag == Flags.GROUP)) { + didChange = remove + ? applyMembershipUnset(membership, subject, flag, rawPattern) + : applyMembership(membership, subject, flag, rawPattern, allow, false); + } else { + if (!remove) { + didChange = applyAdd(perms, subject, flag, rawPattern, allow); + } else { + didChange = applyUnset(perms, subject, flag, rawPattern); + } + if (didChange) cleanupEmptyValue(perms, subject, flag.id()); + } + + if (!didChange) return false; + regionManager.saveRegion(region.getKey(), region); + return true; + }); + + if (!changed) { + context.getSource().sendMessage(messages.component("commands.perm.nothing-to-remove")); + return 1; + } + + mod.getPermissionManager().invalidateAll(); + + context.getSource().sendMessage(messages.component( + remove ? "commands.perm.unset" : (allow ? "commands.perm.allow" : "commands.perm.deny"), + Placeholders.builder() + .string("region", region.getKey().toString()) + .string("target", rawTarget) + .string("permission", flag.name()) + .string("value", rawPattern) + .build() + )); + return 1; + } + + boolean applyAdd(RegionPermissions perms, SubjectRef subject, Flag flag, String rawPattern, boolean allow) { + FlagValueKind kind = flag.valueKind(); + if (kind == FlagValueKind.TARGET_DECISION) { + String pattern = normalizeTargetDecisionValue(rawPattern); + RegionPermissions.TargetDecisionValue v = (RegionPermissions.TargetDecisionValue) perms + .subject(subject) + .getOrCreate(flag.id(), FlagValueKind.TARGET_DECISION); + if (allow) { + v.allow(pattern); + } else { + v.deny(pattern); + } + return true; + } + + if (kind == FlagValueKind.STRING_SET) { + String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); + if (value.isEmpty()) return false; + RegionPermissions.StringSetValue v = (RegionPermissions.StringSetValue) perms + .subject(subject) + .getOrCreate(flag.id(), FlagValueKind.STRING_SET); + if (allow) v.allow(value); + else v.deny(value); + return true; + } + + return false; + } + + boolean applyUnset(RegionPermissions perms, SubjectRef subject, Flag flag, String rawPattern) { + RegionPermissions.SubjectPermissions subjectPerms = perms.subjects().get(subject); + if (subjectPerms == null) return false; + RegionPermissions.PermissionValue pv = subjectPerms.get(flag.id()); + if (pv == null) return false; + + if (pv instanceof RegionPermissions.TargetDecisionValue targeted) { + String pattern = normalizeTargetDecisionValue(rawPattern); + return targeted.removeAllow(pattern) || targeted.removeDeny(pattern); + } + + if (pv instanceof RegionPermissions.StringSetValue set) { + String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); + if (value.isEmpty()) return false; + return set.allowValues().remove(value) || set.denyValues().remove(value); + } + + return false; + } + + private static boolean applyMembership(RegionMembership membership, SubjectRef subject, Flag flag, String rawPattern, boolean allow, boolean remove) { + if (membership == null || subject == null || flag == null) return false; + String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); + if (value.isEmpty()) return false; + + boolean changed; + if (flag == Flags.ROLE) { + if (!(subject instanceof PlayerSubject p)) return false; + RegionMembership.PlayerEntry entry = membership.player(p.uuid()); + if (remove) { + changed = allow ? entry.roles().removeAllow(value) : entry.roles().removeDeny(value); + } else { + if (allow) entry.roles().allow(value); + else entry.roles().deny(value); + changed = true; + } + cleanupEmptyMembership(membership, p.uuid()); + return changed; + } + + if (flag == Flags.GROUP) { + if (subject instanceof PlayerSubject p) { + RegionMembership.PlayerEntry entry = membership.player(p.uuid()); + if (remove) { + changed = allow ? entry.groups().removeAllow(value) : entry.groups().removeDeny(value); + } else { + if (allow) entry.groups().allow(value); + else entry.groups().deny(value); + changed = true; + } + cleanupEmptyMembership(membership, p.uuid()); + return changed; + } + if (subject instanceof GroupSubject g) { + RegionMembership.GroupEntry entry = membership.group(g.name()); + if (remove) { + changed = allow ? entry.includes().removeAllow(value) : entry.includes().removeDeny(value); + } else { + if (allow) entry.includes().allow(value); + else entry.includes().deny(value); + changed = true; + } + cleanupEmptyGroupDefinition(membership, g.name()); + return changed; + } + return false; + } + + return false; + } + + private static boolean applyMembershipUnset(RegionMembership membership, SubjectRef subject, Flag flag, String rawPattern) { + if (membership == null || subject == null || flag == null) return false; + String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); + if (value.isEmpty()) return false; + + boolean changed; + if (flag == Flags.ROLE) { + if (!(subject instanceof PlayerSubject p)) return false; + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return false; + changed = entry.roles().removeAllow(value) || entry.roles().removeDeny(value); + if (changed) cleanupEmptyMembership(membership, p.uuid()); + return changed; + } + + if (flag == Flags.GROUP) { + if (subject instanceof PlayerSubject p) { + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return false; + changed = entry.groups().removeAllow(value) || entry.groups().removeDeny(value); + if (changed) cleanupEmptyMembership(membership, p.uuid()); + return changed; + } + if (subject instanceof GroupSubject g) { + RegionMembership.GroupEntry entry = membership.groups().get(g.name()); + if (entry == null) return false; + changed = entry.includes().removeAllow(value) || entry.includes().removeDeny(value); + if (changed) cleanupEmptyGroupDefinition(membership, g.name()); + return changed; + } + return false; + } + + return false; + } + + private static void cleanupEmptyMembership(RegionMembership membership, UUID uuid) { + if (membership == null || uuid == null) return; + RegionMembership.PlayerEntry entry = membership.players().get(uuid); + if (entry == null) return; + boolean rolesEmpty = entry.roles().allowValues().isEmpty() && entry.roles().denyValues().isEmpty(); + boolean groupsEmpty = entry.groups().allowValues().isEmpty() && entry.groups().denyValues().isEmpty(); + if (rolesEmpty && groupsEmpty) membership.players().remove(uuid); + } + + private static void cleanupEmptyGroupDefinition(RegionMembership membership, String groupName) { + if (membership == null || groupName == null) return; + RegionMembership.GroupEntry entry = membership.groups().get(groupName); + if (entry == null) return; + if (entry.includes().allowValues().isEmpty() && entry.includes().denyValues().isEmpty()) { + membership.groups().remove(groupName); + } + } + + int executeClear(CommandContext context) { + Region region = resolveRegion(context); + if (region == null) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); + return 1; + } + if (!canEditRegion(context.getSource(), region)) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); + return 1; + } + + String rawTarget = context.getArgument("target", String.class); + SubjectRef subject = parseTargetSubject(context.getSource(), rawTarget); + if (subject == null) { + context.getSource().sendMessage(messages.component("commands.perm.invalid-target")); + return 1; + } + + String flagName = context.getArgument("flag", String.class); + Flag flag = Flags.getFlag(flagName); + if (flag == null) { + context.getSource().sendMessage(messages.component("commands.invalid-permission")); + return 1; + } + + boolean changed = regionManager.withWorldWriteLock(region.getWorld(), () -> { + FlagValueKind kind = flag.valueKind(); + boolean didChange = false; + + if (kind == FlagValueKind.STRING_SET && flag == Flags.ROLE) { + if (!(subject instanceof PlayerSubject p)) return false; + RegionMembership membership = region.getMembership(); + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return false; + didChange = !entry.roles().allowValues().isEmpty() || !entry.roles().denyValues().isEmpty(); + entry.roles().clear(); + cleanupEmptyMembership(membership, p.uuid()); + } else if (kind == FlagValueKind.STRING_SET && flag == Flags.GROUP) { + RegionMembership membership = region.getMembership(); + if (subject instanceof PlayerSubject p) { + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return false; + didChange = !entry.groups().allowValues().isEmpty() || !entry.groups().denyValues().isEmpty(); + entry.groups().clear(); + cleanupEmptyMembership(membership, p.uuid()); + } else if (subject instanceof GroupSubject g) { + RegionMembership.GroupEntry entry = membership.groups().get(g.name()); + if (entry == null) return false; + didChange = !entry.includes().allowValues().isEmpty() || !entry.includes().denyValues().isEmpty(); + entry.includes().clear(); + cleanupEmptyGroupDefinition(membership, g.name()); + } else { + return false; + } + } else if (kind == FlagValueKind.TARGET_DECISION || kind == FlagValueKind.STRING_SET) { + RegionPermissions perms = region.getPermissions(); + RegionPermissions.SubjectPermissions subjectPerms = perms.subjects().get(subject); + if (subjectPerms == null || subjectPerms.get(flag.id()) == null) return false; + subjectPerms.values().remove(flag.id()); + if (subjectPerms.values().isEmpty()) perms.subjects().remove(subject); + didChange = true; + } else { + return false; + } + + if (!didChange) return false; + regionManager.saveRegion(region.getKey(), region); + return true; + }); + + if (!changed) { + context.getSource().sendMessage(messages.component("commands.perm.nothing-to-remove")); + return 1; + } + + mod.getPermissionManager().invalidateAll(); + + context.getSource().sendMessage(messages.component( + "commands.perm.clear", + Placeholders.builder() + .string("region", region.getKey().toString()) + .string("target", rawTarget) + .string("permission", flag.name()) + .build() + )); + return 1; + } + + private Region resolveRegion(CommandContext context) { + String keyRaw; + try { + keyRaw = context.getArgument("key", String.class); + } catch (Exception ignored) { + return null; + } + RegionKey key; + try { + key = RegionKey.fromString(keyRaw); + } catch (Exception ignored) { + return null; + } + return regionManager.regions().get(key.getValue()); + } + + private boolean canEditRegion(CommandSourceStack source, Region region) { + if (region == null || source == null) return false; + if (CommandPermissions.check(source, "zones.set.other")) return true; + if (source.getPlayer() == null) return false; + return regionManager.withWorldReadLock(region.getWorld(), () -> region.isOwner(source.getPlayer().getUUID())); + } + + private static SubjectRef parseTargetSubject(CommandSourceStack source, String rawTarget) { + if (rawTarget == null || rawTarget.isBlank()) return null; + String raw = rawTarget.trim(); + + if ("+universal".equalsIgnoreCase(raw) + || "@all".equalsIgnoreCase(raw) + || "@*".equalsIgnoreCase(raw) + || "+universal".equalsIgnoreCase(raw)) { + return Subjects.universal(); + } + + if (raw.regionMatches(true, 0, "player:", 0, "player:".length())) { + String nameOrUuid = raw.substring("player:".length()).trim(); + if (nameOrUuid.isEmpty()) return null; + try { + return new PlayerSubject(UUID.fromString(nameOrUuid)); + } catch (IllegalArgumentException ignored) { + } + + if (source != null) { + var server = source.getServer(); + if (server != null) { + var online = server.getPlayerList().getPlayerByName(nameOrUuid); + if (online != null) return new PlayerSubject(online.getUUID()); + try { + GameProfile profile = server.getProfileCache() != null + ? server.getProfileCache().get(nameOrUuid).orElse(null) + : null; + if (profile != null) return new PlayerSubject(profile.getId()); + } catch (Exception ignored) { + } + } + } + return null; + } + + if (raw.regionMatches(true, 0, "group:", 0, "group:".length())) { + String name = Subjects.normalizeGroupName(raw.substring("group:".length())); + return name == null ? null : new GroupSubject(name); + } + + if (raw.regionMatches(true, 0, "group.", 0, "group.".length())) { + String name = Subjects.normalizeGroupName(raw.substring("group.".length())); + return name == null ? null : new GroupSubject(name); + } + + try { + return new PlayerSubject(UUID.fromString(raw)); + } catch (IllegalArgumentException ignored) { + } + + if (source != null) { + var server = source.getServer(); + if (server != null) { + var online = server.getPlayerList().getPlayerByName(raw); + if (online != null) return new PlayerSubject(online.getUUID()); + try { + GameProfile profile = server.getProfileCache() != null + ? server.getProfileCache().get(raw).orElse(null) + : null; + if (profile != null) return new PlayerSubject(profile.getId()); + } catch (Exception ignored) { + } + } + } + + return null; + } + + private static String normalizeTargetDecisionValue(String rawValue) { + if (rawValue == null) return ""; + String v = rawValue.trim(); + if (v.isEmpty()) return ""; + if ("all".equalsIgnoreCase(v)) return "*"; + return TypeKeys.normalize(v); + } + + static void cleanupEmptyValue(RegionPermissions perms, SubjectRef subjectRef, int flagId) { + if (perms == null || subjectRef == null) return; + RegionPermissions.SubjectPermissions subject = perms.subjects().get(subjectRef); + if (subject == null) return; + + RegionPermissions.PermissionValue pv = subject.get(flagId); + if (pv instanceof RegionPermissions.TargetDecisionValue targeted) { + if (targeted.allowTargets().isEmpty() && targeted.denyTargets().isEmpty()) { + subject.values().remove(flagId); + } + } else if (pv instanceof RegionPermissions.StringSetValue set) { + if (set.allowValues().isEmpty() && set.denyValues().isEmpty()) { + subject.values().remove(flagId); + } + } + + if (subject.values().isEmpty()) perms.subjects().remove(subjectRef); + } +} diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/RenameCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/RenameCommand.java index 1853c43..f3d5012 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/RenameCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/RenameCommand.java @@ -1,21 +1,17 @@ package de.t14d3.zones.fabric.commands; import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.utils.Messages; -import me.lucko.fabric.api.permissions.v0.Permissions; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.minecraft.commands.CommandSourceStack; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class RenameCommand { private final RegionManager regionManager; - private final Messages messages; - private final MiniMessage mm = MiniMessage.miniMessage(); + private final MessageFormatService messages; public RenameCommand(ZonesFabric mod) { this.regionManager = mod.getRegionManager(); @@ -25,19 +21,26 @@ public RenameCommand(ZonesFabric mod) { int execute(CommandContext context) { Region region = regionManager.regions() .get(RegionKey.fromString(context.getArgument("key", String.class)).getValue()); - if (!Permissions.check(context.getSource(), "zones.rename.other") && - (context.getSource().getPlayer() == null || !region.isOwner( - context.getSource().getPlayer().getUUID()))) { - context.getSource().sendMessage(messages.getCmp("commands.invalid-region")); + if (region == null) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); return 1; } + + if (!CommandPermissions.check(context.getSource(), "zones.rename.other")) { + if (context.getSource().getPlayer() == null + || !regionManager.withWorldReadLock(region.getWorld(), + () -> region.isOwner(context.getSource().getPlayer().getUUID()))) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); + return 1; + } + } String name = context.getArgument("New Name", String.class); - region.setName(name, regionManager); - context.getSource().sendMessage( - mm.deserialize(messages.get("commands.rename.success"), - parsed("region", region.getKey().toString()), - parsed("name", name)) - ); + regionManager.withWorldWriteLock(region.getWorld(), () -> region.setName(name, regionManager)); + context.getSource().sendMessage(messages.component("commands.rename.success", + Placeholders.builder() + .string("region", region.getKey().toString()) + .string("name", name) + .build())); return 1; } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java index 0b1aef1..4c0ac95 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java @@ -1,5 +1,6 @@ package de.t14d3.zones.fabric.commands; + import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; @@ -9,9 +10,7 @@ import de.t14d3.zones.RegionManager; import de.t14d3.zones.Zones; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.Player; import de.t14d3.zones.utils.Messages; -import me.lucko.fabric.api.permissions.v0.Permissions; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.kyori.adventure.platform.modcommon.impl.NonWrappingComponentSerializer; import net.kyori.adventure.platform.modcommon.impl.WrappedComponent; @@ -20,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.CompletableFuture; public class RootCommand { @@ -27,7 +27,7 @@ public class RootCommand { private final CreateCommand createCommand; private final DeleteCommand deleteCommand; private final ListCommand listCommand; - private final SetCommand setCommand; + private final PermCommand permCommand; private final InfoCommand infoCommand; private final RenameCommand renameCommand; private final ExpandCommand expandCommand; @@ -40,12 +40,11 @@ public class RootCommand { private final MigrateCommand migrateCommand; public RootCommand(ZonesFabric mod) { - RegionManager regionManager = mod.getRegionManager(); this.cancelCommand = new CancelCommand(mod); this.createCommand = new CreateCommand(mod); this.deleteCommand = new DeleteCommand(mod); this.listCommand = new ListCommand(mod); - this.setCommand = new SetCommand(mod); + this.permCommand = new PermCommand(mod); this.infoCommand = new InfoCommand(mod); this.renameCommand = new RenameCommand(mod); this.expandCommand = new ExpandCommand(mod); @@ -61,18 +60,25 @@ public RootCommand(ZonesFabric mod) { protected static CompletableFuture regionKeySuggestion(CommandContext context, SuggestionsBuilder builder) { final Zones zones = Zones.getInstance(); + RegionManager regionManager = zones.getRegionManager(); + + boolean canSeeAll = CommandPermissions.check(context.getSource(), "zones.info.other") + || CommandPermissions.check(context.getSource(), "zones.set.other") + || CommandPermissions.check(context.getSource(), "zones.delete.other") + || CommandPermissions.check(context.getSource(), "zones.expand.other"); + + UUID uuid = context.getSource().getPlayer() != null ? context.getSource().getPlayer().getUUID() : null; List regions = new ArrayList<>(); - Player player = context.getSource().getPlayer() != null ? zones.getPlatform().getPlayer( - context.getSource().getPlayer().getUUID()) : null; - if (Permissions.check(context.getSource(), "zones.set.other")) { - regions.addAll(zones.getRegionManager().regions().values()); - } else if (player != null) { - for (Region region : zones.getRegionManager().regions().values()) { - if (region.isMember(player.getUniqueId())) { + if (canSeeAll) { + regions.addAll(regionManager.regions().values()); + } else if (uuid != null) { + for (Region region : regionManager.regions().values()) { + if (regionManager.withWorldReadLock(region.getWorld(), () -> region.isMember(uuid))) { regions.add(region); } } } + for (Region region : regions) { builder.suggest(region.getKey().toString(), new WrappedComponent( Messages.regionInfo(region, false), @@ -88,42 +94,38 @@ public void register() { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register( Commands.literal("zone") .then(Commands.literal("cancel") - .requires(source -> Permissions.check(source, "zones.cancel")) + .requires(source -> CommandPermissions.check(source, "zones.cancel")) .executes(cancelCommand::execute)) .then(Commands.literal("create") - .requires(source -> Permissions.check(source, "zones.create")) + .requires(source -> CommandPermissions.check(source, "zones.create")) .executes(createCommand::execute)) .then(Commands.literal("delete") - .requires(source -> Permissions.check(source, "zones.delete")) + .requires(source -> CommandPermissions.check(source, "zones.delete")) .then(Commands.argument("key", StringArgumentType.string()) .suggests(RootCommand::regionKeySuggestion) .executes(deleteCommand::execute))) .then(Commands.literal("list") - .requires(source -> Permissions.check(source, "zones.list")) + .requires(source -> CommandPermissions.check(source, "zones.list")) .executes(context -> listCommand.execute(context, 1)) .then(Commands.argument("page", IntegerArgumentType.integer()) .executes(context -> listCommand.execute(context, - context.getArgument("page", Integer.class))) - )) - .then(setCommand.command() - .requires(source -> Permissions.check(source, "zones.set")) - ) + context.getArgument("page", Integer.class))))) + .then(permCommand.command()) .then(Commands.literal("info") - .requires(source -> Permissions.check(source, "zones.info")) + .requires(source -> CommandPermissions.check(source, "zones.info")) .executes(infoCommand::execute) .then(Commands.argument("key", StringArgumentType.string()) .suggests(RootCommand::regionKeySuggestion) .executes(infoCommand::execute))) .then(Commands.literal("rename") - .requires(source -> Permissions.check(source, "zones.rename")) + .requires(source -> CommandPermissions.check(source, "zones.rename")) .then(Commands.argument("key", StringArgumentType.string()) .suggests(RootCommand::regionKeySuggestion) .then(Commands.argument("New Name", StringArgumentType.string()) .executes(renameCommand::execute)))) - .then(expandCommand.command() - .requires(source -> Permissions.check(source, "zones.expand"))) + .then(expandCommand.command()) .then(Commands.literal("mode") - .requires(source -> Permissions.check(source, "zones.mode")) + .requires(source -> CommandPermissions.check(source, "zones.mode")) .then(Commands.argument("mode", StringArgumentType.string()) .suggests((context, builder) -> { builder.suggest("2D"); @@ -132,26 +134,28 @@ public void register() { }) .executes(modeCommand::execute))) .then(Commands.literal("find") - .requires(source -> Permissions.check(source, "zones.find")) + .requires(source -> CommandPermissions.check(source, "zones.find")) .executes(findCommand::execute)) .then(Commands.literal("select") - .requires(source -> Permissions.check(source, "zones.select")) + .requires(source -> CommandPermissions.check(source, "zones.select")) + .executes(selectCommand::execute) .then(Commands.argument("key", StringArgumentType.string()) .suggests(RootCommand::regionKeySuggestion) .executes(selectCommand::execute))) .then(Commands.literal("subcreate") - .requires(source -> Permissions.check(source, "zones.subcreate")) + .requires(source -> CommandPermissions.check(source, "zones.subcreate")) + .executes(subCreateCommand::execute) .then(Commands.argument("key", StringArgumentType.string()) .suggests(RootCommand::regionKeySuggestion) .executes(subCreateCommand::execute))) .then(Commands.literal("save") - .requires(source -> Permissions.check(source, "zones.save")) + .requires(source -> CommandPermissions.check(source, "zones.save")) .executes(saveCommand::execute)) .then(Commands.literal("load") - .requires(source -> Permissions.check(source, "zones.load")) + .requires(source -> CommandPermissions.check(source, "zones.load")) .executes(loadCommand::execute)) .then(Commands.literal("migrate") - .requires(source -> Permissions.check(source, "zones.migrate")) + .requires(source -> CommandPermissions.check(source, "zones.migrate")) .then(Commands.argument("type", StringArgumentType.string()) .suggests((context, builder) -> { builder.suggest("yaml"); @@ -162,8 +166,8 @@ public void register() { builder.suggest("custom"); return builder.buildFuture(); }) - .executes(migrateCommand::execute)) - ) + .executes(migrateCommand::execute))) )); } } + diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SaveCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SaveCommand.java index a795988..ddd8af8 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SaveCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SaveCommand.java @@ -1,15 +1,12 @@ package de.t14d3.zones.fabric.commands; import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.Placeholders; import de.t14d3.zones.fabric.ZonesFabric; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.minecraft.commands.CommandSourceStack; -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - public class SaveCommand { private final ZonesFabric mod; - private final MiniMessage mm = MiniMessage.miniMessage(); public SaveCommand(ZonesFabric mod) { this.mod = mod; @@ -17,8 +14,9 @@ public SaveCommand(ZonesFabric mod) { int execute(CommandContext context) { mod.getRegionManager().saveRegions(); - context.getSource().sendMessage(mm.deserialize(mod.getMessages().get("commands.save"), - parsed("count", String.valueOf(mod.getRegionManager().regions().size())))); + context.getSource().sendMessage(mod.getMessages().component("commands.save", + Placeholders.builder().string("count", String.valueOf(mod.getRegionManager().regions().size())) + .build())); return 1; } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SelectCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SelectCommand.java index 569970f..5b5ffc7 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SelectCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SelectCommand.java @@ -1,22 +1,22 @@ package de.t14d3.zones.fabric.commands; + import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.BlockLocation; -import de.t14d3.zones.objects.PlayerRepository; -import net.kyori.adventure.text.minimessage.MiniMessage; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.server.level.ServerPlayer; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; public class SelectCommand { private final ZonesFabric mod; private final RegionManager regionManager; - private final MiniMessage mm = MiniMessage.miniMessage(); public SelectCommand(ZonesFabric mod) { this.mod = mod; @@ -27,34 +27,42 @@ int execute(CommandContext context) { if (context.getSource().getPlayer() == null) { return 0; } - ServerPlayer player = context.getSource().getPlayer(); - de.t14d3.zones.objects.Player zplayer = PlayerRepository.get(player.getUUID()); - if (zplayer.isSelectionCreating()) { - mod.getPlatform().removeBeacon(zplayer, zplayer.getSelection().getWorld(), - zplayer.getSelection().getMin()); - mod.getPlatform().removeBeacon(zplayer, zplayer.getSelection().getWorld(), - zplayer.getSelection().getMax()); - zplayer.setSelectionCreating(false); + + RPlayer player = RPlayer.wrap(context.getSource().getPlayer()).orElse(null); + if (player == null) return 0; + + Box selection = player.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + boolean selecting = player.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false); + if (selecting && selection != null) { + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : player.worldOrThrow() + .ref(); + mod.getPlatform().removeBeacon(player, selectionWorld, selection.getMin()); + mod.getPlatform().removeBeacon(player, selectionWorld, selection.getMax()); + player.extras().put(ZonesExtraKeys.SELECTION_CREATING, false); } + Region region = null; try { - region = regionManager.regions() - .get(RegionKey.fromString(context.getArgument("key", String.class)).getValue()); + String key = context.getArgument("key", String.class); + region = regionManager.regions().get(RegionKey.fromString(key).getValue()); } catch (Exception ignored) { } + + RBlockPos pos = player.locationOrThrow().blockPos(); + RWorldRef world = player.worldOrThrow().ref(); if (region == null) { - region = regionManager.getEffectiveRegionAt( - BlockLocation.of(player.getBlockX(), player.getBlockY(), player.getBlockZ()), - mod.getPlatform().getWorld(zplayer)); + region = regionManager.getEffectiveRegionAt(pos, world); } - if (region == null || region.getBounds().equals(zplayer.getSelection())) { - zplayer.setSelection(null); - player.sendMessage(mm.deserialize(mod.getMessages().get("commands.select.deselected"))); + + if (region == null || (selection != null && region.getBounds().equals(selection))) { + player.extras().remove(ZonesExtraKeys.SELECTION); + player.sendMessage(mod.getMessages().component("commands.select.deselected")); } else { - zplayer.setSelection(region.getBounds()); - player.sendMessage(mm.deserialize(mod.getMessages().get("commands.select.selected"), - parsed("region", region.getName()))); + player.extras().put(ZonesExtraKeys.SELECTION, region.getBounds()); + player.sendMessage(mod.getMessages().component("commands.select.selected", + Placeholders.builder().string("region", region.getName()).build())); } return 1; } } + diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SetCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SetCommand.java deleted file mode 100644 index 750f4fd..0000000 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SetCommand.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.t14d3.zones.fabric.commands; - -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import de.t14d3.zones.Region; -import de.t14d3.zones.RegionKey; -import de.t14d3.zones.RegionManager; -import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.Flag; -import de.t14d3.zones.objects.Player; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.permissions.flags.Flags; -import de.t14d3.zones.utils.Messages; -import me.lucko.fabric.api.permissions.v0.Permissions; -import net.kyori.adventure.platform.modcommon.impl.NonWrappingComponentSerializer; -import net.kyori.adventure.platform.modcommon.impl.WrappedComponent; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.Commands; - -import java.util.*; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; - -public class SetCommand { - private final MiniMessage miniMessage = MiniMessage.miniMessage(); - private final RegionManager regionManager; - private final Messages messages; - private final ZonesFabric mod; - - public SetCommand(ZonesFabric mod) { - this.mod = mod; - this.regionManager = mod.getRegionManager(); - this.messages = mod.getMessages(); - } - - public LiteralArgumentBuilder command() { - return Commands.literal("set") - .requires(source -> Permissions.check(source, "zones.set")) - .then(Commands.argument("key", StringArgumentType.string()) - .suggests(RootCommand::regionKeySuggestion) - .then(Commands.argument("target", StringArgumentType.string()) - .suggests((context, builder) -> { - Region region = regionManager.regions() - .get(RegionKey.fromString(context.getArgument("key", String.class)) - .getValue()); - if (region == null) { - return builder.buildFuture(); - } else if (!Permissions.check(context.getSource(), "zones.set.other")) { - if (context.getSource().getPlayer() == null || !region.isOwner( - context.getSource().getPlayer().getUUID())) { - return builder.buildFuture(); - } - } - Map targets = new HashMap<>(); - region.getGroupNames().forEach(group -> { - List groupMembers = new ArrayList<>(); - region.getGroupMembers(group).forEach( - val -> groupMembers.add( - mod.getPlatform().getPlayer(UUID.fromString(val)) - .getName())); - targets.put(group, Component.text(groupMembers.toString())); - }); - PlayerRepository.getPlayers().forEach(player -> - targets.put(player.getName(), Component.text(player.getUUID().toString()))); - targets.put("+universal", (messages.getCmp("flags.universal"))); - - for (Map.Entry target : targets.entrySet()) { - builder.suggest(target.getKey(), new WrappedComponent( - target.getValue(), - null, - null, - NonWrappingComponentSerializer.INSTANCE - )); - } - return builder.buildFuture(); - }) - .then(Commands.argument("flag", StringArgumentType.string()) - .suggests((context, builder) -> { - for (Flag flag : Flags.getFlags()) { - builder.suggest(flag.name(), new WrappedComponent( - messages.getCmp("flags." + flag.name()), - null, - null, - NonWrappingComponentSerializer.INSTANCE - )); - } - return builder.buildFuture(); - }) - .then(Commands.argument("values", StringArgumentType.greedyString()) - .suggests((context, builder) -> { - Flag flag = Flags.getFlag( - context.getArgument("flag", String.class)); - for (String type : flag.getValidValues()) { - builder.suggest(type); - } - return builder.buildFuture(); - }) - .executes(this::execute))) - ) - ); - } - - - int execute(CommandContext context) { - RegionKey regionKey = RegionKey.fromString(context.getArgument("key", String.class)); - Region region = regionManager.regions().get(regionKey.getValue()); - if (Permissions.check(context.getSource(), "zones.set.other") || - (context.getSource().getPlayer() != null && region.isOwner( - context.getSource().getPlayer().getUUID()))) { - StringBuilder builder = new StringBuilder(); - String[] values = context.getArgument("values", String.class).split(" "); - for (String value : values) { - builder.append(value).append(" "); - } - String target = context.getArgument("target", String.class); - String display = target; - if (!display.startsWith("+")) { - Player temp = mod.getPlatform().getPlayer(target); - if (temp != null) { - target = temp.getUniqueId().toString(); - } - } - regionManager.addMemberPermission(target, context.getArgument("flag", String.class), builder.toString(), - regionKey); - context.getSource().sendMessage(miniMessage.deserialize(messages.get("commands.set.success"), - parsed("region", regionKey.toString()), - parsed("target", display), - parsed("permission", context.getArgument("flag", String.class)), - parsed("value", builder.toString()))); - } else { - context.getSource().sendMessage(miniMessage.deserialize(messages.get("commands.invalid-region"))); - } - return 1; - } -} diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SubCreateCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SubCreateCommand.java index af2f824..02a7762 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SubCreateCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SubCreateCommand.java @@ -1,92 +1,105 @@ package de.t14d3.zones.fabric.commands; + import com.mojang.brigadier.context.CommandContext; +import de.t14d3.rapunzellib.message.MessageFormatService; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; -import de.t14d3.zones.fabric.FabricPlatform; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.BlockLocation; import de.t14d3.zones.objects.Box; -import de.t14d3.zones.objects.PlayerRepository; -import de.t14d3.zones.objects.RegionFlagEntry; -import de.t14d3.zones.utils.Messages; -import net.kyori.adventure.text.minimessage.MiniMessage; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import net.minecraft.commands.CommandSourceStack; -import java.util.List; - public class SubCreateCommand { private final ZonesFabric mod; - private final FabricPlatform platform; - private final Messages messages; - private final MiniMessage mm = MiniMessage.miniMessage(); + private final MessageFormatService messages; public SubCreateCommand(ZonesFabric mod) { this.mod = mod; - this.platform = mod.getPlatform(); this.messages = mod.getMessages(); } int execute(CommandContext context) { - if (context.getSource().getPlayer() != null) { - de.t14d3.zones.objects.Player zplayer = PlayerRepository.get(context.getSource().getPlayer().getUUID()); - if (zplayer.getSelection() == null) { - zplayer.setSelection( - new Box(null, null, platform.getWorld(context.getSource().getPlayer().serverLevel()), false)); - zplayer.setSelectionCreating(true); - zplayer.sendMessage(mm.deserialize(messages.get("commands.create.click-corners"))); - return 1; - } - Box selection = zplayer.getSelection(); - if (selection.getMin() == null || selection.getMax() == null) { - zplayer.sendMessage(mm.deserialize(messages.get("commands.create.click-corners"))); - return 1; - } + if (context.getSource().getPlayer() == null) { + context.getSource().sendMessage(messages.component("commands.only-player")); + return 0; + } - Region parentRegion = null; - try { - context.getArgument("key", String.class); - RegionKey regionKey = RegionKey.fromString(context.getArgument("key", String.class)); - Region tempRegion = mod.getRegionManager().regions().get(regionKey.getValue()); - if (tempRegion == null || !tempRegion.isAdmin(context.getSource().getPlayer().getUUID())) { - zplayer.sendMessage(mm.deserialize(messages.get("commands.invalid-region"))); - return 1; - } - } catch (Exception e) { - for (Region region : mod.getRegionManager().getRegionsAt( - BlockLocation.of(context.getSource().getPlayer().getBlockX(), - context.getSource().getPlayer().getBlockY(), - context.getSource().getPlayer().getBlockZ()), - platform.getWorld(context.getSource().getPlayer().serverLevel()))) { - if (region.isAdmin(context.getSource().getPlayer().getUUID())) { - parentRegion = region; - break; - } - } - } - if (parentRegion == null) { - zplayer.sendMessage(mm.deserialize(messages.get("commands.subcreate.no-parent"))); - return 1; - } + RPlayer player = RPlayer.wrap(context.getSource().getPlayer()).orElse(null); + if (player == null) return 0; + + Box selection = player.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + boolean selecting = player.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false); - if (!parentRegion.contains(selection.getMin()) - || !parentRegion.contains(selection.getMax())) { - zplayer.sendMessage(mm.deserialize(messages.get("commands.subcreate.outside-parent"))); + if (selection == null || !selecting) { + player.extras().put(ZonesExtraKeys.SELECTION, new Box(player.worldOrThrow().ref())); + player.extras().put(ZonesExtraKeys.SELECTION_CREATING, true); + player.sendMessage(messages.component("commands.create.click-corners")); + return 1; + } + + if (selection.getMin() == null || selection.getMax() == null) { + player.sendMessage(messages.component("commands.create.click-corners")); + return 1; + } + + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : player.worldOrThrow().ref(); + var regionManager = mod.getRegionManager(); + + Region parentRegion = null; + try { + String key = context.getArgument("key", String.class); + RegionKey regionKey = RegionKey.fromString(key); + Region tempRegion = regionManager.regions().get(regionKey.getValue()); + if (tempRegion != null && regionManager.withWorldReadLock(tempRegion.getWorld(), + () -> tempRegion.isAdmin(player.uuid()))) { + parentRegion = tempRegion; + } else { + player.sendMessage(messages.component("commands.invalid-region")); return 1; } + } catch (Exception ignored) { + RBlockPos pos = player.locationOrThrow().blockPos(); + parentRegion = regionManager.withWorldReadLock(selectionWorld, () -> { + for (Region region : regionManager.getRegionsAt(pos, selectionWorld)) { + if (region.isAdmin(player.uuid())) return region; + } + return null; + }); + } + + if (parentRegion == null) { + player.sendMessage(messages.component("commands.subcreate.no-parent")); + return 1; + } - mod.getRegionManager().createSubRegion(parentRegion.getName() + "_sub", selection.getMin(), - selection.getMax(), selection.getWorld(), zplayer.getUniqueId(), - List.of(new RegionFlagEntry("role", "owner", false)), parentRegion); - mod.getPlatform().removeBeacon(zplayer, selection.getWorld(), selection.getMin()); - mod.getPlatform().removeBeacon(zplayer, selection.getWorld(), selection.getMax()); - zplayer.sendMessage(mm.deserialize(messages.get("commands.subcreate.success"))); - zplayer.setSelection(null); - zplayer.setSelectionCreating(false); - } else { - context.getSource().sendMessage(mm.deserialize(messages.get("commands.only-player"))); + Region finalParentRegion = parentRegion; + boolean insideParent = regionManager.withWorldReadLock(finalParentRegion.getWorld(), () -> + finalParentRegion.contains(selection.getMin()) && finalParentRegion.contains(selection.getMax())); + if (!insideParent) { + player.sendMessage(messages.component("commands.subcreate.outside-parent")); + return 1; } + regionManager.createSubRegion( + finalParentRegion.getName() + "_sub", + selection.getMin(), + selection.getMax(), + selectionWorld, + player.uuid(), + finalParentRegion + ); + + mod.getPlatform().removeBeacon(player, selectionWorld, selection.getMin()); + mod.getPlatform().removeBeacon(player, selectionWorld, selection.getMax()); + player.extras().remove(ZonesExtraKeys.SELECTION); + player.extras().put(ZonesExtraKeys.SELECTION_CREATING, false); + + player.sendMessage(messages.component("commands.subcreate.success")); return 1; } } + diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/listeners/PlayerListener.java b/fabric/src/main/java/de/t14d3/zones/fabric/listeners/PlayerListener.java index 2a5f89a..b4d7299 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/listeners/PlayerListener.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/listeners/PlayerListener.java @@ -1,148 +1,97 @@ package de.t14d3.zones.fabric.listeners; -import de.t14d3.zones.Region; +import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.fabric.FabricPlatform; import de.t14d3.zones.fabric.ZonesFabric; -import de.t14d3.zones.objects.BlockLocation; import de.t14d3.zones.objects.Box; import de.t14d3.zones.objects.Flag; import de.t14d3.zones.permissions.flags.Flags; -import net.fabricmc.fabric.api.event.player.*; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; +import net.fabricmc.fabric.api.event.player.AttackBlockCallback; +import net.fabricmc.fabric.api.event.player.AttackEntityCallback; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.phys.BlockHitResult; -import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - -import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; public class PlayerListener { private final ZonesFabric mod; private final FabricPlatform platform; - private final MiniMessage mm = MiniMessage.miniMessage(); public PlayerListener(ZonesFabric mod) { this.mod = mod; this.platform = mod.getPlatform(); UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> { - de.t14d3.zones.objects.Player zplayer = platform.getPlayer(player.getUUID()); - if (zplayer.getSelection() != null && zplayer.isSelectionCreating()) { + RPlayer rPlayer = RPlayer.wrap(player).orElse(null); + if (rPlayer == null) return InteractionResult.PASS; + + boolean selecting = rPlayer.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false); + Box selection = rPlayer.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + if (selection != null && selecting) { if (hand.equals(InteractionHand.MAIN_HAND)) { - BlockLocation min = zplayer.getSelection().getMin(); - BlockLocation loc = new BlockLocation( + RBlockPos min = selection.getMin(); + RBlockPos max = selection.getMax(); + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : rPlayer.worldOrThrow() + .ref(); + + RBlockPos loc = new RBlockPos( hitResult.getBlockPos().getX(), hitResult.getBlockPos().getY(), hitResult.getBlockPos().getZ() ); - platform.removeBeacon(zplayer, zplayer.getSelection().getWorld(), min); + platform.removeBeacon(rPlayer, selectionWorld, min); min = loc; - platform.showBeacon(zplayer, min, zplayer.getSelection().getWorld(), NamedTextColor.GREEN); - zplayer.setSelection( - new Box(min, zplayer.getSelection().getMax(), zplayer.getSelection().getWorld(), false)); - zplayer.setSelectionCreating(true); - zplayer.sendMessage(mm.deserialize(mod.getMessages().get("create.primary"), - parsed("x", String.valueOf(loc.getX())), - parsed("y", String.valueOf(loc.getY())), - parsed("z", String.valueOf(loc.getZ())) - )); + platform.showBeacon(rPlayer, min, selectionWorld, NamedTextColor.GREEN); + rPlayer.extras().put(ZonesExtraKeys.SELECTION, new Box(min, max, selectionWorld, false)); + rPlayer.sendMessage(mod.getMessages().component("create.primary", + Placeholders.builder() + .string("x", String.valueOf(loc.x())) + .string("y", String.valueOf(loc.y())) + .string("z", String.valueOf(loc.z())) + .build())); return InteractionResult.FAIL; } return InteractionResult.PASS; } - if (player.getMainHandItem().isEmpty()) { - return InteractionResult.PASS; - } - ItemStack itemStack = player.getMainHandItem(); - List flags = new ArrayList<>(); - flags.add(Flags.PLACE); - Block block = Block.byItem(itemStack.getItem()) != Blocks.AIR - ? Block.byItem(itemStack.getItem()) - : world.getBlockState(getOffset(hitResult)).getBlock(); - - if (block.defaultBlockState().hasBlockEntity()) { - flags.add(Flags.CONTAINER); - } - if (block.defaultBlockState().isSignalSource()) { - flags.add(Flags.REDSTONE); - } - - InteractionResult result = InteractionResult.PASS; - for (Flag flag : flags) { - if (!mod.getPermissionManager() - .checkAction(getOffset(hitResult), world, player, itemStack.getItem().getDescriptionId(), - flag)) { - result = InteractionResult.FAIL; - break; - } - } - if (result == InteractionResult.FAIL) { - sendActionBar(player, getOffset(hitResult), flags, itemStack.getItem().getDescriptionId()); - } - return result; + return InteractionResult.PASS; }); AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> { - de.t14d3.zones.objects.Player zplayer = platform.getPlayer(player.getUUID()); - if (zplayer.getSelection() != null && zplayer.isSelectionCreating()) { - BlockLocation max = zplayer.getSelection().getMax(); - BlockLocation loc = new BlockLocation( - pos.getX(), - pos.getY(), - pos.getZ() - ); - platform.removeBeacon(zplayer, zplayer.getSelection().getWorld(), max); + RPlayer rPlayer = RPlayer.wrap(player).orElse(null); + if (rPlayer == null) return InteractionResult.PASS; + + boolean selecting = rPlayer.extras().get(ZonesExtraKeys.SELECTION_CREATING).orElse(false); + Box selection = rPlayer.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + if (selection != null && selecting) { + RBlockPos min = selection.getMin(); + RBlockPos max = selection.getMax(); + RWorldRef selectionWorld = selection.getWorld() != null ? selection.getWorld() : rPlayer.worldOrThrow() + .ref(); + + RBlockPos loc = new RBlockPos(pos.getX(), pos.getY(), pos.getZ()); + platform.removeBeacon(rPlayer, selectionWorld, max); max = loc; - platform.showBeacon(zplayer, max, zplayer.getSelection().getWorld(), NamedTextColor.RED); - zplayer.setSelection( - new Box(zplayer.getSelection().getMin(), max, zplayer.getSelection().getWorld(), false)); - zplayer.setSelectionCreating(true); - zplayer.sendMessage(mm.deserialize(mod.getMessages().get("create.secondary"), - parsed("x", String.valueOf(loc.getX())), - parsed("y", String.valueOf(loc.getY())), - parsed("z", String.valueOf(loc.getZ())) - )); + platform.showBeacon(rPlayer, max, selectionWorld, NamedTextColor.RED); + rPlayer.extras().put(ZonesExtraKeys.SELECTION, new Box(min, max, selectionWorld, false)); + rPlayer.sendMessage(mod.getMessages().component("create.secondary", + Placeholders.builder() + .string("x", String.valueOf(loc.x())) + .string("y", String.valueOf(loc.y())) + .string("z", String.valueOf(loc.z())) + .build())); return InteractionResult.FAIL; } return InteractionResult.PASS; }); - PlayerBlockBreakEvents.BEFORE.register((world, player, pos, state, blockEntity) -> { - AtomicBoolean result = new AtomicBoolean(true); - List flags = new ArrayList<>(); - flags.add(Flags.BREAK); - if (state.hasBlockEntity()) { - flags.add(Flags.CONTAINER); - } - if (state.isSignalSource()) { - flags.add(Flags.REDSTONE); - } - for (Flag flag : flags) { - if (!mod.getPermissionManager() - .checkAction(pos, world, player, state.getBlock().getDescriptionId(), flag)) { - result.set(false); - break; - } - } - if (!result.get()) { - sendActionBar(player, pos, flags, state.getBlock().getDescriptionId()); - } - - return result.get(); - }); - UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { if (entity == null) { return InteractionResult.PASS; @@ -175,45 +124,4 @@ public PlayerListener(ZonesFabric mod) { return InteractionResult.PASS; }); } - - @NotNull - private static BlockPos getOffset(BlockHitResult hitResult) { - BlockPos pos = hitResult.getBlockPos(); - Direction side = hitResult.getDirection(); - if (side == Direction.DOWN) { - return new BlockPos(pos.getX(), pos.getY() - 1, pos.getZ()); - } else if (side == Direction.UP) { - return new BlockPos(pos.getX(), pos.getY() + 1, pos.getZ()); - } else if (side == Direction.NORTH) { - return new BlockPos(pos.getX(), pos.getY(), pos.getZ() - 1); - } else if (side == Direction.SOUTH) { - return new BlockPos(pos.getX(), pos.getY(), pos.getZ() + 1); - } else if (side == Direction.WEST) { - return new BlockPos(pos.getX() - 1, pos.getY(), pos.getZ()); - } else if (side == Direction.EAST) { - return new BlockPos(pos.getX() + 1, pos.getY(), pos.getZ()); - } - //noinspection DataFlowIssue - return null; - } - - private void sendActionBar(Player player, BlockPos pos, List requiredPermissions, String type) { - List regions = mod.getRegionManager().getRegionsAt(BlockLocation.of(pos.getX(), pos.getY(), pos.getZ()), - platform.getWorld(player.getCommandSenderWorld())); - String regionNames = regions.stream().map(Region::getName).collect(Collectors.joining(", ")); - StringBuilder permissionsString = new StringBuilder(); - for (Flag action : requiredPermissions) { - permissionsString.append(action.name()).append(", "); - } - if (!requiredPermissions.isEmpty()) { - permissionsString.setLength(permissionsString.length() - 2); // Remove trailing ", " - } - de.t14d3.zones.objects.Player zplayer = platform.getPlayer(player.getUUID()); - zplayer.sendActionBar(mm.deserialize(mod.getMessages().get("region.no-interact-permission"), - parsed("region", regionNames), - parsed("actions", permissionsString.toString()), - parsed("type", type)) - ); - - } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..c4a178e --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,76 @@ +[versions] +# Plugins +shadow = "9.0.0-beta8" +fabric-loom = "1.11.7" +run-paper = "2.3.1" +paperweight-userdev = "2.0.0-beta.19" + +# Common +paper-api = "1.21.4-R0.1-SNAPSHOT" +paper-dev-bundle = "1.21.10-R0.1-SNAPSHOT" +annotations = "23.0.0" + +adventure = "4.19.0" +adventure-platform-bukkit = "4.3.4" +adventure-platform-fabric = "6.2.0" + +# Zones: api +log4j-core = "2.25.3" +fastutil = "8.5.8" +h2 = "2.3.232" +postgresql = "42.7.2" + +# Zones: bukkit +placeholderapi = "2.11.6" +worldedit = "7.3.10" +worldguard = "7.0.13" +intellectualsites-bom = "1.52" +commandapi-paper-shade = "11.0.1-SNAPSHOT" + +# Zones: shared +rapunzellib = "0.1.9-SNAPSHOT" + +# Zones: fabric +minecraft = "1.21.4" +fabric-loader = "0.16.10" +fabric-api = "0.117.0+1.21.4" +fabric-resource-loader-v1 = "1.0.4+14959b836f" + +[plugins] +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } +fabric-loom = { id = "fabric-loom", version.ref = "fabric-loom" } +run-paper = { id = "xyz.jpenilla.run-paper", version.ref = "run-paper" } +paperweight-userdev = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight-userdev" } + +[libraries] +paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper-api" } +annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" } + +adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure" } +adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } +adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform-bukkit" } +adventure-platform-fabric = { module = "net.kyori:adventure-platform-fabric", version.ref = "adventure-platform-fabric" } + +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j-core" } +fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" } +h2 = { module = "com.h2database:h2", version.ref = "h2" } +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } + +placeholderapi = { module = "me.clip:placeholderapi", version.ref = "placeholderapi" } +worldedit-bukkit = { module = "com.sk89q.worldedit:worldedit-bukkit", version.ref = "worldedit" } +worldguard-bukkit = { module = "com.sk89q.worldguard:worldguard-bukkit", version.ref = "worldguard" } +intellectualsites-bom = { module = "com.intellectualsites.bom:bom-newest", version.ref = "intellectualsites-bom" } +fawe-core = { module = "com.fastasyncworldedit:FastAsyncWorldEdit-Core" } +fawe-bukkit = { module = "com.fastasyncworldedit:FastAsyncWorldEdit-Bukkit" } +commandapi-paper-shade = { module = "dev.jorel:commandapi-paper-shade", version.ref = "commandapi-paper-shade" } + +rapunzellib-api = { module = "de.t14d3.rapunzellib:api", version.ref = "rapunzellib" } +rapunzellib-events = { module = "de.t14d3.rapunzellib:events", version.ref = "rapunzellib" } +rapunzellib-events-paper = { module = "de.t14d3.rapunzellib:events-paper", version.ref = "rapunzellib" } +rapunzellib-platform-fabric = { module = "de.t14d3.rapunzellib:platform-fabric", version.ref = "rapunzellib" } +rapunzellib-events-fabric = { module = "de.t14d3.rapunzellib:events-fabric", version.ref = "rapunzellib" } + +minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } +fabric-resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader-v1", version.ref = "fabric-resource-loader-v1" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a79..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 3188d50..e0a898a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,11 +8,12 @@ pluginManagement { } } } - plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" } + rootProject.name = "Zones" + include("api") include("bukkit") -include("fabric") \ No newline at end of file +include("fabric") From 8561f49e68ce5ac3e8d9d2844d0b13fa49501397 Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:38:30 +0100 Subject: [PATCH 2/9] Cleanup, better visuals, fabric parity fixes Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- .../java/de/t14d3/zones/RegionManager.java | 12 + .../de/t14d3/zones/WorldRegionManager.java | 24 +- api/src/main/java/de/t14d3/zones/Zones.java | 7 + .../de/t14d3/zones/ZonesParticleRole.java | 13 + .../java/de/t14d3/zones/ZonesPlatform.java | 2 +- .../zones/rapunzellib/ZonesExtraKeys.java | 1 + .../java/de/t14d3/zones/utils/Messages.java | 238 +++++++--- .../t14d3/zones/visuals/ParticleHandler.java | 373 +++++++++++++--- .../visuals/particles/ParticlePalette.java | 31 ++ .../visuals/particles/ParticleRenderMode.java | 8 + .../particles/ParticleVisualManager.java | 406 ++++++++++++++++++ api/src/main/resources/config.yml | 27 ++ api/src/main/resources/messages.properties | 61 --- api/src/main/resources/messages.yml | 4 + .../de/t14d3/zones/bukkit/BukkitPlatform.java | 12 +- .../zones/bukkit/commands/CancelCommand.java | 4 - .../zones/bukkit/commands/DeleteCommand.java | 2 - .../zones/bukkit/commands/ExpandCommand.java | 265 +++++++++--- .../zones/bukkit/commands/FindCommand.java | 56 +-- .../de/t14d3/zones/fabric/FabricPlatform.java | 13 +- .../de/t14d3/zones/fabric/ZonesFabric.java | 8 +- .../zones/fabric/commands/ExpandCommand.java | 197 ++++++++- .../zones/fabric/commands/FindCommand.java | 27 +- .../zones/fabric/commands/RootCommand.java | 11 +- 24 files changed, 1512 insertions(+), 290 deletions(-) create mode 100644 api/src/main/java/de/t14d3/zones/ZonesParticleRole.java create mode 100644 api/src/main/java/de/t14d3/zones/visuals/particles/ParticlePalette.java create mode 100644 api/src/main/java/de/t14d3/zones/visuals/particles/ParticleRenderMode.java create mode 100644 api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java delete mode 100644 api/src/main/resources/messages.properties diff --git a/api/src/main/java/de/t14d3/zones/RegionManager.java b/api/src/main/java/de/t14d3/zones/RegionManager.java index 03a66ba..8f26130 100644 --- a/api/src/main/java/de/t14d3/zones/RegionManager.java +++ b/api/src/main/java/de/t14d3/zones/RegionManager.java @@ -232,6 +232,18 @@ public List getRegionsAt(RBlockPos location, RWorldRef world) { return manager.withReadLock(() -> manager.getRegionsAtLocked(location)); } + /** + * Returns regions near the given location by chunk index. + * + *

This returns candidates from the chunk index, not necessarily regions within the + * exact distance.

+ */ + public List getRegionsNear(RBlockPos center, RWorldRef world, int blockRange) { + if (world == null || center == null) return Collections.emptyList(); + WorldRegionManager manager = getOrCreateWorldManager(world); + return manager.withReadLock(() -> manager.getRegionsNearLocked(center, blockRange)); + } + /** * Gets the region with the highest priority at the given location * diff --git a/api/src/main/java/de/t14d3/zones/WorldRegionManager.java b/api/src/main/java/de/t14d3/zones/WorldRegionManager.java index 89696c0..b71a963 100644 --- a/api/src/main/java/de/t14d3/zones/WorldRegionManager.java +++ b/api/src/main/java/de/t14d3/zones/WorldRegionManager.java @@ -160,6 +160,29 @@ List getRegionsAtLocked(RBlockPos location) { return found; } + List getRegionsNearLocked(RBlockPos center, int blockRange) { + if (center == null) return Collections.emptyList(); + int r = Math.max(0, blockRange); + + int minXChunk = (center.x() - r) >> 4; + int minZChunk = (center.z() - r) >> 4; + int maxXChunk = (center.x() + r) >> 4; + int maxZChunk = (center.z() + r) >> 4; + + LinkedHashSet found = new LinkedHashSet<>(); + for (int x = minXChunk; x <= maxXChunk; x++) { + for (int z = minZChunk; z <= maxZChunk; z++) { + long key = ((long) x << 32) | (z & 0xFFFFFFFFL); + List candidates = chunkRegions.get(key); + if (candidates != null) found.addAll(candidates); + } + } + + if (!globalRegions.isEmpty()) found.addAll(globalRegions); + if (found.isEmpty()) return Collections.emptyList(); + return new ArrayList<>(found); + } + @Nullable Region getEffectiveRegionAtLocked(RBlockPos location) { List regions = getRegionsAtLocked(location); int priority = Integer.MIN_VALUE; @@ -205,4 +228,3 @@ private void addRegionToChunksLocked(Region region, RBlockPos min, RBlockPos max } } } - diff --git a/api/src/main/java/de/t14d3/zones/Zones.java b/api/src/main/java/de/t14d3/zones/Zones.java index 349c068..bff21b4 100644 --- a/api/src/main/java/de/t14d3/zones/Zones.java +++ b/api/src/main/java/de/t14d3/zones/Zones.java @@ -9,6 +9,7 @@ import de.t14d3.zones.utils.Types; import de.t14d3.zones.utils.Utils; import de.t14d3.zones.visuals.FindBossbar; +import de.t14d3.zones.visuals.particles.ParticleVisualManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +37,7 @@ public class Zones { private final YamlConfig config; private final ThreadPoolExecutor executor; private final FindBossbar findBossbar; + private final ParticleVisualManager particleVisualManager; public boolean debug = false; @@ -60,6 +62,7 @@ public Zones(ZonesPlatform platform) { this.regionManager = new RegionManager(this); this.permissionManager = platform.createPermissionManager(this); + this.particleVisualManager = new ParticleVisualManager(this); this.executor = new ThreadPoolExecutor( 0, config.getInt("advanced.thread-pool.max-size", Runtime.getRuntime().availableProcessors() * 2), @@ -156,4 +159,8 @@ public Utils.SavingModes getSavingMode() { public FindBossbar getFindBossbar() { return findBossbar; } + + public ParticleVisualManager getParticleVisualManager() { + return particleVisualManager; + } } diff --git a/api/src/main/java/de/t14d3/zones/ZonesParticleRole.java b/api/src/main/java/de/t14d3/zones/ZonesParticleRole.java new file mode 100644 index 0000000..35cdf2c --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/ZonesParticleRole.java @@ -0,0 +1,13 @@ +package de.t14d3.zones; + +/** + * Semantic particle channel used by {@link ZonesPlatform}. + * + *

Platform implementations map these roles to concrete particle effects (configured as + * {@code visuals.particles.primary}/{@code visuals.particles.secondary}).

+ */ +public enum ZonesParticleRole { + PRIMARY, + SECONDARY, + PREVIEW +} diff --git a/api/src/main/java/de/t14d3/zones/ZonesPlatform.java b/api/src/main/java/de/t14d3/zones/ZonesPlatform.java index 5491a55..43f9fa9 100644 --- a/api/src/main/java/de/t14d3/zones/ZonesPlatform.java +++ b/api/src/main/java/de/t14d3/zones/ZonesPlatform.java @@ -18,7 +18,7 @@ default PermissionManager createPermissionManager(Zones zones) { return new PermissionManager(zones); } - void spawnParticle(int type, RBlockPos particleLocation, RPlayer player); + void spawnParticle(ZonesParticleRole role, RBlockPos particleLocation, RPlayer player); void showBeacon(RPlayer player, RBlockPos location, RWorldRef world, NamedTextColor color); diff --git a/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesExtraKeys.java b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesExtraKeys.java index baf2caa..00dbc97 100644 --- a/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesExtraKeys.java +++ b/api/src/main/java/de/t14d3/zones/rapunzellib/ZonesExtraKeys.java @@ -13,6 +13,7 @@ private ZonesExtraKeys() { public static final RExtraKey SELECTION_MODE = RExtraKey.of("zones", "selection_mode", String.class); public static final RExtraKey LAST_ACTIONBAR_NANOS = RExtraKey.of("zones", "last_actionbar_nanos", Long.class); + public static final RExtraKey NEARBY_VISUALS = RExtraKey.of("zones", "nearby_visuals", Boolean.class); public static final RExtraKey PERMISSION_CACHE = RExtraKey.of("zones", "permission_cache", ZonesPermissionCache.class); } diff --git a/api/src/main/java/de/t14d3/zones/utils/Messages.java b/api/src/main/java/de/t14d3/zones/utils/Messages.java index edce4c9..3020824 100644 --- a/api/src/main/java/de/t14d3/zones/utils/Messages.java +++ b/api/src/main/java/de/t14d3/zones/utils/Messages.java @@ -6,14 +6,13 @@ import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.Region; import de.t14d3.zones.permissions.PermissionKeyRegistry; +import de.t14d3.zones.permissions.RegionMembership; import de.t14d3.zones.permissions.RegionPermissions; import de.t14d3.zones.permissions.subjects.*; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Shared message/formatting helpers. @@ -53,73 +52,190 @@ public static Component regionInfo(Region region, boolean showMembers) { if (showMembers) { RegionPermissions perms = region.getPermissions(); - for (Map.Entry member : perms.subjects().entrySet()) { + RegionMembership membership = region.getMembership(); + + LinkedHashMap subjects = collectSubjects(perms, + membership); + + if (subjects.isEmpty()) { + comp = comp.appendNewline().append(Component.text(" (none)").color(NamedTextColor.GRAY)); + } + + for (Map.Entry member : subjects.entrySet()) { SubjectRef subjectRef = member.getKey(); - RegionPermissions.SubjectPermissions subject = member.getValue(); - if (subjectRef == null || subject == null) continue; - - String subjectName; - if (subjectRef instanceof PlayerSubject p) { - subjectName = RPlayer.get(p.uuid()).map(RPlayer::name).orElse(p.uuid().toString()); - } else if (subjectRef instanceof GroupSubject g) { - subjectName = "group:" + g.name(); - } else if (subjectRef instanceof UniversalSubject) { - subjectName = "+universal"; - } else { - subjectName = Subjects.format(subjectRef); - } + RegionPermissions.SubjectPermissions subjectPermissions = member.getValue(); + if (subjectRef == null) continue; + + String subjectName = subjectDisplayName(subjectRef); Component subjectComponent = messages.component("region.info.members.name", Placeholders.builder().string("name", subjectName).build()).appendNewline(); - Component permissionsComponent = Component.empty(); - for (var permEntry : subject.values().int2ObjectEntrySet()) { - int permId = permEntry.getIntKey(); - String permName = PermissionKeyRegistry.instance().getName(permId); - if (permName == null || permName.isBlank()) continue; - - RegionPermissions.PermissionValue value = permEntry.getValue(); - List formattedComponents = new ArrayList<>(); - - if (value instanceof RegionPermissions.TargetDecisionValue targeted) { - for (String t : targeted.allowTargets()) { - formattedComponents.add(messages.component("region.info.members.values.allowed", - Placeholders.builder().string("value", t).build())); - } - for (String t : targeted.denyTargets()) { - formattedComponents.add(messages.component("region.info.members.values.denied", - Placeholders.builder().string("value", t).build())); - } - } else if (value instanceof RegionPermissions.StringSetValue set) { - for (String s : set.allowValues()) { - formattedComponents.add(messages.component("region.info.members.values.allowed", - Placeholders.builder().string("value", s).build())); - } - for (String s : set.denyValues()) { - formattedComponents.add(messages.component("region.info.members.values.denied", - Placeholders.builder().string("value", s).build())); - } - } - - if (formattedComponents.isEmpty()) continue; - - Component permLine = messages.component("region.info.members.permission", - Placeholders.builder().string("permission", permName).build()); - - for (int i = 0; i < formattedComponents.size(); i++) { - permLine = permLine.append(formattedComponents.get(i)); - if (i < formattedComponents.size() - 1) { - permLine = permLine.append(Component.text(", ").color(NamedTextColor.GRAY)); - } - } - permissionsComponent = permissionsComponent.append(permLine).append(Component.newline()); - } - - comp = comp.appendNewline().append(subjectComponent).append(permissionsComponent); + Component membershipComponent = membershipDetails(messages, membership, subjectRef); + + Component permissionsComponent = permissionDetails(messages, subjectPermissions); + + comp = comp.appendNewline().append(subjectComponent).append(membershipComponent) + .append(permissionsComponent); } } comp = comp.appendNewline().append(messages.component("region.info.key", Placeholders.builder().string("key", region.getKey().toString()).build())); return comp; } + + private static LinkedHashMap collectSubjects( + RegionPermissions perms, + RegionMembership membership + ) { + LinkedHashMap subjects = new LinkedHashMap<>(); + if (perms == null) return subjects; + + if (membership != null) { + List playerUuids = new ArrayList<>(membership.players().keySet()); + playerUuids.sort(Comparator.comparing(UUID::toString)); + for (UUID uuid : playerUuids) { + if (uuid == null) continue; + SubjectRef ref = Subjects.player(uuid); + subjects.putIfAbsent(ref, perms.subjects().get(ref)); + } + + List groupNames = new ArrayList<>(membership.groups().keySet()); + groupNames.sort(String::compareTo); + for (String name : groupNames) { + if (name == null || name.isBlank()) continue; + SubjectRef ref = Subjects.group(name); + subjects.putIfAbsent(ref, perms.subjects().get(ref)); + } + } + + for (Map.Entry e : perms.subjects().entrySet()) { + subjects.putIfAbsent(e.getKey(), e.getValue()); + } + + return subjects; + } + + private static String subjectDisplayName(SubjectRef subjectRef) { + if (subjectRef instanceof PlayerSubject p) { + return RPlayer.get(p.uuid()).map(RPlayer::name).orElse(p.uuid().toString()); + } + if (subjectRef instanceof GroupSubject g) { + return "group:" + g.name(); + } + if (subjectRef instanceof UniversalSubject) { + return "+universal"; + } + return Subjects.format(subjectRef); + } + + private static Component membershipDetails( + MessageFormatService messages, + RegionMembership membership, + SubjectRef subjectRef + ) { + if (membership == null || subjectRef == null) return Component.empty(); + + Component out = Component.empty(); + if (subjectRef instanceof PlayerSubject p) { + RegionMembership.PlayerEntry entry = membership.players().get(p.uuid()); + if (entry == null) return out; + + Component rolesLine = formatAllowDenyLine(messages, "roles", entry.roles()); + if (rolesLine != null) out = out.append(rolesLine).append(Component.newline()); + + Component groupsLine = formatAllowDenyLine(messages, "groups", entry.groups()); + if (groupsLine != null) out = out.append(groupsLine).append(Component.newline()); + } else if (subjectRef instanceof GroupSubject g) { + RegionMembership.GroupEntry entry = membership.groups().get(g.name()); + if (entry == null) return out; + + Component includesLine = formatAllowDenyLine(messages, "includes", entry.includes()); + if (includesLine != null) out = out.append(includesLine).append(Component.newline()); + } + + return out; + } + + private static Component permissionDetails( + MessageFormatService messages, + RegionPermissions.SubjectPermissions subjectPermissions + ) { + if (messages == null || subjectPermissions == null) return Component.empty(); + + Component out = Component.empty(); + for (var permEntry : subjectPermissions.values().int2ObjectEntrySet()) { + int permId = permEntry.getIntKey(); + String permName = PermissionKeyRegistry.instance().getName(permId); + if (permName == null || permName.isBlank()) continue; + + RegionPermissions.PermissionValue value = permEntry.getValue(); + List formattedComponents = new ArrayList<>(); + + if (value instanceof RegionPermissions.TargetDecisionValue targeted) { + formattedComponents.addAll( + formatAllowedDenied(messages, targeted.allowTargets(), targeted.denyTargets())); + } else if (value instanceof RegionPermissions.StringSetValue set) { + formattedComponents.addAll(formatAllowedDenied(messages, set.allowValues(), set.denyValues())); + } + + if (formattedComponents.isEmpty()) continue; + + Component permLine = messages.component( + "region.info.members.permission", + Placeholders.builder().string("permission", permName).build() + ); + permLine = appendCommaSeparated(permLine, formattedComponents); + out = out.append(permLine).append(Component.newline()); + } + + return out; + } + + private static Component appendCommaSeparated(Component line, List values) { + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + line = line.append(Component.text(", ").color(NamedTextColor.GRAY)); + } + line = line.append(values.get(i)); + } + return line; + } + + private static List formatAllowedDenied(MessageFormatService messages, Iterable allowed, Iterable denied) { + List allowList = new ArrayList<>(); + for (String v : allowed) { + if (v != null && !v.isBlank()) allowList.add(v); + } + allowList.sort(String::compareTo); + + List denyList = new ArrayList<>(); + for (String v : denied) { + if (v != null && !v.isBlank()) denyList.add(v); + } + denyList.sort(String::compareTo); + + List formattedComponents = new ArrayList<>(allowList.size() + denyList.size()); + for (String v : allowList) { + formattedComponents.add(messages.component("region.info.members.values.allowed", + Placeholders.builder().string("name", v).build())); + } + for (String v : denyList) { + formattedComponents.add(messages.component("region.info.members.values.denied", + Placeholders.builder().string("name", v).build())); + } + return formattedComponents; + } + + private static Component formatAllowDenyLine(MessageFormatService messages, String label, RegionMembership.AllowDenyStringSet set) { + if (messages == null || label == null || set == null) return null; + if (set.allowValues().isEmpty() && set.denyValues().isEmpty()) return null; + + List values = formatAllowedDenied(messages, set.allowValues(), set.denyValues()); + if (values.isEmpty()) return null; + + Component line = messages.component("region.info.members.permission", + Placeholders.builder().string("permission", label).build()); + return appendCommaSeparated(line, values); + } } diff --git a/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java b/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java index d127bb9..0675253 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java +++ b/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java @@ -1,28 +1,39 @@ package de.t14d3.zones.visuals; -import de.t14d3.rapunzellib.Rapunzel; import de.t14d3.rapunzellib.objects.RBlockPos; -import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.Zones; -import de.t14d3.zones.ZonesPlatform; -import de.t14d3.zones.objects.Box; -import de.t14d3.zones.rapunzellib.ZonesExtraKeys; - -import java.time.Duration; +import de.t14d3.zones.visuals.particles.ParticleRenderMode; public class ParticleHandler { private final Zones zones; - private final ZonesPlatform platform; - private final double range; public ParticleHandler(Zones zones) { this.zones = zones; - this.platform = zones.getPlatform(); - this.range = zones.getConfig().getInt("visuals.particles.range", 15); } - void spawnParticleOutline(RPlayer player, RBlockPos min, RBlockPos max) { - // Normalize coordinates so function works regardless of which corner is "min" or "max" + public void particleScheduler() { + zones.getParticleVisualManager().start(); + } + + @FunctionalInterface + public interface ParticlePointConsumer { + /** + * @return whether to continue emitting points + */ + boolean accept(int x, int y, int z, boolean edgeOrCorner); + } + + public static void emit( + ParticleRenderMode mode, + RBlockPos min, + RBlockPos max, + RBlockPos viewerBlock, + int range, + int step, + ParticlePointConsumer consumer + ) { + if (mode == null || min == null || max == null || viewerBlock == null || consumer == null) return; + int minX = Math.min(min.x(), max.x()); int maxX = Math.max(min.x(), max.x()); int minY = Math.min(min.y(), max.y()); @@ -37,69 +48,313 @@ void spawnParticleOutline(RPlayer player, RBlockPos min, RBlockPos max) { int y2 = maxY + 1; int z2 = maxZ + 1; - // Generate particles for all 6 faces - spawnFace(player, 'y', y1, x1, x2, z1, z2, x1, x2, y1, y2, z1, z2); // Floor - spawnFace(player, 'y', y2, x1, x2, z1, z2, x1, x2, y1, y2, z1, z2); // Roof - spawnFace(player, 'x', x1, y1, y2, z1, z2, x1, x2, y1, y2, z1, z2); // Left wall - spawnFace(player, 'x', x2, y1, y2, z1, z2, x1, x2, y1, y2, z1, z2); // Right wall - spawnFace(player, 'z', z1, x1, x2, y1, y2, x1, x2, y1, y2, z1, z2); // Front wall - spawnFace(player, 'z', z2, x1, x2, y1, y2, x1, x2, y1, y2, z1, z2); // Back wall + int cx1 = viewerBlock.x() - range; + int cy1 = viewerBlock.y() - range; + int cz1 = viewerBlock.z() - range; + int cx2 = viewerBlock.x() + range; + int cy2 = viewerBlock.y() + range; + int cz2 = viewerBlock.z() + range; + + int s = Math.max(1, step); + + switch (mode) { + case PERIMETER_AT_VIEWER_Y -> + emitPerimeterAtY(viewerBlock.y(), x1, x2, y1, y2, z1, z2, cx1, cx2, cz1, cz2, s, consumer); + case WIRE_EDGES -> emitWireEdges(x1, x2, y1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, s, consumer); + case FACES -> emitFaces(x1, x2, y1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, s, consumer); + } + } + + private static void emitPerimeterAtY( + int y, + int x1, int x2, int y1, int y2, int z1, int z2, + int cx1, int cx2, int cz1, int cz2, + int step, + ParticlePointConsumer consumer + ) { + if (y < y1 || y > y2) return; + + CornerPredicate corners = (x, yy, z) -> isPerimeterCorner(x, z, x1, x2, z1, z2); + if (!emitLineX(y, z1, x1, x2, cx1, cx2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineX(y, z2, x1, x2, cx1, cx2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineZ(x1, y, z1, z2, cx1, cx2, cz1, cz2, step, corners, consumer)) return; + emitLineZ(x2, y, z1, z2, cx1, cx2, cz1, cz2, step, corners, consumer); + } + + private static void emitWireEdges( + int x1, int x2, int y1, int y2, int z1, int z2, + int cx1, int cx2, int cy1, int cy2, int cz1, int cz2, + int step, + ParticlePointConsumer consumer + ) { + CornerPredicate corners = (x, y, z) -> isWireCorner(x, y, z, x1, x2, y1, y2, z1, z2); + + // Bottom rectangle + if (!emitLineX(y1, z1, x1, x2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineX(y1, z2, x1, x2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineZ(x1, y1, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineZ(x2, y1, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + + // Top rectangle + if (!emitLineX(y2, z1, x1, x2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineX(y2, z2, x1, x2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineZ(x1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineZ(x2, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + + // Vertical edges + if (!emitLineY(x1, z1, y1, y2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineY(x1, z2, y1, y2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + if (!emitLineY(x2, z1, y1, y2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer)) return; + emitLineY(x2, z2, y1, y2, cx1, cx2, cy1, cy2, cz1, cz2, step, corners, consumer); + } + + private static void emitFaces( + int x1, int x2, int y1, int y2, int z1, int z2, + int cx1, int cx2, int cy1, int cy2, int cz1, int cz2, + int step, + ParticlePointConsumer consumer + ) { + // y faces + if (!emitFaceY(y1, x1, x2, z1, z2, x1, x2, y1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, consumer)) + return; + if (!emitFaceY(y2, x1, x2, z1, z2, x1, x2, y1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, consumer)) + return; + + // x faces + if (!emitFaceX(x1, y1, y2, z1, z2, x1, x2, y1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, consumer)) + return; + if (!emitFaceX(x2, y1, y2, z1, z2, x1, x2, y1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, consumer)) + return; + + // z faces + if (!emitFaceZ(z1, x1, x2, y1, y2, x1, x2, y1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, consumer)) + return; + emitFaceZ(z2, x1, x2, y1, y2, x1, x2, y1, y2, z1, z2, cx1, cx2, cy1, cy2, cz1, cz2, step, consumer); } - public void spawnFace(RPlayer player, char axis, int fixedVal, - int axis1Start, int axis1End, int axis2Start, int axis2End, - int x1, int x2, int y1, int y2, int z1, int z2) { - var playerLoc = player.location().orElse(null); - if (playerLoc == null) return; - RBlockPos playerBlock = playerLoc.blockPos(); + private static boolean emitFaceY( + int y, + int xStart, int xEnd, int zStart, int zEnd, + int x1, int x2, int y1, int y2, int z1, int z2, + int cx1, int cx2, int cy1, int cy2, int cz1, int cz2, + int step, + ParticlePointConsumer consumer + ) { + if (y < cy1 || y > cy2) return true; + + int xs = Math.max(xStart, cx1); + int xe = Math.min(xEnd, cx2); + int zs = Math.max(zStart, cz1); + int ze = Math.min(zEnd, cz2); + if (xs > xe || zs > ze) return true; + + for (int x = xs; x <= xe; x += step) { + for (int z = zs; z <= ze; z += step) { + boolean edge = isOutlineEdge(x, y, z, x1, x2, y1, y2, z1, z2); + if (!consumer.accept(x, y, z, edge)) return false; + } + } + return true; + } + + private static boolean emitFaceX( + int x, + int yStart, int yEnd, int zStart, int zEnd, + int x1, int x2, int y1, int y2, int z1, int z2, + int cx1, int cx2, int cy1, int cy2, int cz1, int cz2, + int step, + ParticlePointConsumer consumer + ) { + if (x < cx1 || x > cx2) return true; + + int ys = Math.max(yStart, cy1); + int ye = Math.min(yEnd, cy2); + int zs = Math.max(zStart, cz1); + int ze = Math.min(zEnd, cz2); + if (ys > ye || zs > ze) return true; + + for (int y = ys; y <= ye; y += step) { + for (int z = zs; z <= ze; z += step) { + boolean edge = isOutlineEdge(x, y, z, x1, x2, y1, y2, z1, z2); + if (!consumer.accept(x, y, z, edge)) return false; + } + } + return true; + } - for (int a1 = axis1Start; a1 <= axis1End; a1++) { - for (int a2 = axis2Start; a2 <= axis2End; a2++) { - RBlockPos loc = createLocation(axis, fixedVal, a1, a2); + private static boolean emitFaceZ( + int z, + int xStart, int xEnd, int yStart, int yEnd, + int x1, int x2, int y1, int y2, int z1, int z2, + int cx1, int cx2, int cy1, int cy2, int cz1, int cz2, + int step, + ParticlePointConsumer consumer + ) { + if (z < cz1 || z > cz2) return true; - // Use squared distance to avoid sqrt() call (speeeeed) - long rangeSq = (long) range * (long) range; - if (distanceSquared(loc, playerBlock) >= rangeSq) continue; + int xs = Math.max(xStart, cx1); + int xe = Math.min(xEnd, cx2); + int ys = Math.max(yStart, cy1); + int ye = Math.min(yEnd, cy2); + if (xs > xe || ys > ye) return true; - platform.spawnParticle(isCorner(loc, x1, x2, y1, y2, z1, z2) ? 2 : 1, loc, player); + for (int x = xs; x <= xe; x += step) { + for (int y = ys; y <= ye; y += step) { + boolean edge = isOutlineEdge(x, y, z, x1, x2, y1, y2, z1, z2); + if (!consumer.accept(x, y, z, edge)) return false; } } + return true; } - static RBlockPos createLocation(char axis, int fixedVal, int a1, int a2) { - return switch (axis) { - case 'x' -> new RBlockPos(fixedVal, a1, a2); - case 'y' -> new RBlockPos(a1, fixedVal, a2); - case 'z' -> new RBlockPos(a1, a2, fixedVal); - default -> throw new IllegalArgumentException("Invalid axis"); - }; + @FunctionalInterface + private interface CornerPredicate { + boolean isCorner(int x, int y, int z); } - static boolean isCorner(RBlockPos loc, int x1, int x2, int y1, int y2, int z1, int z2) { - boolean xEdge = loc.x() == x1 || loc.x() == x2; - boolean yEdge = loc.y() == y1 || loc.y() == y2; - boolean zEdge = loc.z() == z1 || loc.z() == z2; + private static boolean emitLineX( + int y, int z, + int xStart, int xEnd, + int cx1, int cx2, int cy1, int cy2, int cz1, int cz2, + int step, + CornerPredicate corner, + ParticlePointConsumer consumer + ) { + if (y < cy1 || y > cy2) return true; + if (z < cz1 || z > cz2) return true; + + int xs = Math.max(xStart, cx1); + int xe = Math.min(xEnd, cx2); + if (xs > xe) return true; + + for (int x = xs; x <= xe; x += step) { + if (!consumer.accept(x, y, z, corner.isCorner(x, y, z))) return false; + } + return true; + } + + private static boolean emitLineX( + int y, int z, + int xStart, int xEnd, + int cx1, int cx2, int cz1, int cz2, + int step, + CornerPredicate corner, + ParticlePointConsumer consumer + ) { + if (z < cz1 || z > cz2) return true; + + int xs = Math.max(xStart, cx1); + int xe = Math.min(xEnd, cx2); + if (xs > xe) return true; + + for (int x = xs; x <= xe; x += step) { + if (!consumer.accept(x, y, z, corner.isCorner(x, y, z))) return false; + } + return true; + } + + private static boolean emitLineZ( + int x, int y, + int zStart, int zEnd, + int cx1, int cx2, int cy1, int cy2, int cz1, int cz2, + int step, + CornerPredicate corner, + ParticlePointConsumer consumer + ) { + if (x < cx1 || x > cx2) return true; + if (y < cy1 || y > cy2) return true; + + int zs = Math.max(zStart, cz1); + int ze = Math.min(zEnd, cz2); + if (zs > ze) return true; + + for (int z = zs; z <= ze; z += step) { + if (!consumer.accept(x, y, z, corner.isCorner(x, y, z))) return false; + } + return true; + } + + private static boolean emitLineZ( + int x, int y, + int zStart, int zEnd, + int cx1, int cx2, int cz1, int cz2, + int step, + CornerPredicate corner, + ParticlePointConsumer consumer + ) { + if (x < cx1 || x > cx2) return true; + + int zs = Math.max(zStart, cz1); + int ze = Math.min(zEnd, cz2); + if (zs > ze) return true; + + for (int z = zs; z <= ze; z += step) { + if (!consumer.accept(x, y, z, corner.isCorner(x, y, z))) return false; + } + return true; + } + + private static boolean emitLineY( + int x, int z, + int yStart, int yEnd, + int cx1, int cx2, int cy1, int cy2, int cz1, int cz2, + int step, + CornerPredicate corner, + ParticlePointConsumer consumer + ) { + if (x < cx1 || x > cx2) return true; + if (z < cz1 || z > cz2) return true; + + int ys = Math.max(yStart, cy1); + int ye = Math.min(yEnd, cy2); + if (ys > ye) return true; + + for (int y = ys; y <= ye; y += step) { + if (!consumer.accept(x, y, z, corner.isCorner(x, y, z))) return false; + } + return true; + } + + private static boolean isOutlineEdge(int x, int y, int z, int x1, int x2, int y1, int y2, int z1, int z2) { + boolean xEdge = x == x1 || x == x2; + boolean yEdge = y == y1 || y == y2; + boolean zEdge = z == z1 || z == z2; return (xEdge && yEdge) || (xEdge && zEdge) || (yEdge && zEdge); } - static long distanceSquared(RBlockPos a, RBlockPos b) { - long dx = (long) b.x() - a.x(); - long dy = (long) b.y() - a.y(); - long dz = (long) b.z() - a.z(); + private static boolean isPerimeterCorner(int x, int z, int x1, int x2, int z1, int z2) { + boolean xEdge = x == x1 || x == x2; + boolean zEdge = z == z1 || z == z2; + return xEdge && zEdge; + } + + private static boolean isWireCorner(int x, int y, int z, int x1, int x2, int y1, int y2, int z1, int z2) { + boolean xEdge = x == x1 || x == x2; + boolean yEdge = y == y1 || y == y2; + boolean zEdge = z == z1 || z == z2; + return xEdge && yEdge && zEdge; + } + + public static long distanceSquared(int ax, int ay, int az, int bx, int by, int bz) { + long dx = (long) bx - ax; + long dy = (long) by - ay; + long dz = (long) bz - az; return dx * dx + dy * dy + dz * dz; } - public void particleScheduler() { - if (!zones.getConfig().getBoolean("visuals.particles.enabled", false)) { - return; - } + public static long packBlockPos(int x, int y, int z) { + return ((long) (x & 0x3FFFFFF) << 38) | ((long) (z & 0x3FFFFFF) << 12) | (long) (y & 0xFFF); + } - Rapunzel.context().scheduler().runRepeatingAsync(Duration.ofMillis(200), Duration.ofMillis(200), () -> { - for (RPlayer player : RPlayer.online()) { - Box selection = player.extras().get(ZonesExtraKeys.SELECTION).orElse(null); - if (selection == null || selection.getMin() == null || selection.getMax() == null) continue; - spawnParticleOutline(player, selection.getMin(), selection.getMax()); - } - }); + public static int unpackX(long packed) { + return (int) (packed >> 38); + } + + public static int unpackY(long packed) { + return (int) (packed << 52 >> 52); + } + + public static int unpackZ(long packed) { + return (int) (packed << 26 >> 38); } } diff --git a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticlePalette.java b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticlePalette.java new file mode 100644 index 0000000..89e619a --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticlePalette.java @@ -0,0 +1,31 @@ +package de.t14d3.zones.visuals.particles; + +import de.t14d3.zones.ZonesParticleRole; + +/** + * Maps a visual to platform particle roles. + */ +public final class ParticlePalette { + public static final ParticlePalette PRIMARY_CORNERS = + new ParticlePalette(ZonesParticleRole.PRIMARY, ZonesParticleRole.SECONDARY); + public static final ParticlePalette SECONDARY_CORNERS = + new ParticlePalette(ZonesParticleRole.SECONDARY, ZonesParticleRole.PRIMARY); + public static final ParticlePalette PREVIEW = + new ParticlePalette(ZonesParticleRole.PREVIEW, ZonesParticleRole.PREVIEW); + + private final ZonesParticleRole edgeRole; + private final ZonesParticleRole cornerRole; + + public ParticlePalette(ZonesParticleRole edgeRole, ZonesParticleRole cornerRole) { + this.edgeRole = edgeRole == null ? ZonesParticleRole.PRIMARY : edgeRole; + this.cornerRole = cornerRole == null ? ZonesParticleRole.SECONDARY : cornerRole; + } + + public ZonesParticleRole edgeRole() { + return edgeRole; + } + + public ZonesParticleRole cornerRole() { + return cornerRole; + } +} diff --git a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleRenderMode.java b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleRenderMode.java new file mode 100644 index 0000000..76b2149 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleRenderMode.java @@ -0,0 +1,8 @@ +package de.t14d3.zones.visuals.particles; + +public enum ParticleRenderMode { + WIRE_EDGES, + PERIMETER_AT_VIEWER_Y, + FACES +} + diff --git a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java new file mode 100644 index 0000000..3bfc960 --- /dev/null +++ b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java @@ -0,0 +1,406 @@ +package de.t14d3.zones.visuals.particles; + +import de.t14d3.rapunzellib.Rapunzel; +import de.t14d3.rapunzellib.objects.RBlockPos; +import de.t14d3.rapunzellib.objects.RPlayer; +import de.t14d3.rapunzellib.objects.RWorldRef; +import de.t14d3.zones.Region; +import de.t14d3.zones.Zones; +import de.t14d3.zones.ZonesParticleRole; +import de.t14d3.zones.ZonesPlatform; +import de.t14d3.zones.objects.Box; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; +import de.t14d3.zones.visuals.ParticleHandler; +import it.unimi.dsi.fastutil.longs.Long2LongMap; +import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class ParticleVisualManager { + private static final long NO_ENTRY = Long.MIN_VALUE; + + private final Zones zones; + private final ZonesPlatform platform; + private final ConcurrentHashMap> overlays = new ConcurrentHashMap<>(); + private volatile boolean started; + + public ParticleVisualManager(Zones zones) { + this.zones = zones; + this.platform = zones.getPlatform(); + } + + public void start() { + if (started) return; + started = true; + + long periodMs = zones.getConfig().getLong("visuals.particles.period-ms", 200); + Duration period = Duration.ofMillis(Math.max(50, periodMs)); + Rapunzel.context().scheduler().runRepeatingAsync(period, period, this::tickSafely); + } + + private void tickSafely() { + try { + tick(); + } catch (Throwable t) { + zones.getLogger().warn("Particle visuals tick failed", t); + } + } + + private void tick() { + if (!zones.getConfig().getBoolean("visuals.particles.enabled", false)) { + return; + } + + int range = zones.getConfig().getInt("visuals.particles.range", 15); + int budget = zones.getConfig().getInt("visuals.particles.budget-per-player", 2500); + long rangeSq = (long) range * (long) range; + long now = System.nanoTime(); + + for (RPlayer player : RPlayer.online()) { + var loc = player.location().orElse(null); + if (loc == null) continue; + + RBlockPos viewerBlock = loc.blockPos(); + RWorldRef viewerWorld = loc.world(); + + Long2LongOpenHashMap points = new Long2LongOpenHashMap(Math.min(budget * 2, 16_384)); + points.defaultReturnValue(NO_ENTRY); + + appendSelection(points, budget, player, viewerBlock, viewerWorld, range, rangeSq); + appendNearby(points, budget, player, viewerBlock, viewerWorld, range, rangeSq); + appendOverlays(points, budget, player, viewerBlock, viewerWorld, range, rangeSq, now); + + for (Long2LongMap.Entry entry : points.long2LongEntrySet()) { + long packed = entry.getLongKey(); + ZonesParticleRole role = decodeRole(entry.getLongValue()); + + int x = ParticleHandler.unpackX(packed); + int y = ParticleHandler.unpackY(packed); + int z = ParticleHandler.unpackZ(packed); + + platform.spawnParticle(role, new RBlockPos(x, y, z), player); + } + } + } + + public void showOverlay( + UUID viewerUuid, + String id, + Box box, + ParticleRenderMode mode, + ParticlePalette palette, + int priority, + int step, + Duration ttl + ) { + if (viewerUuid == null || id == null || id.isBlank() || box == null) return; + long expiresAtNanos = ttl == null ? Long.MAX_VALUE : (System.nanoTime() + Math.max(0, ttl.toNanos())); + Overlay overlay = new Overlay(box, mode, palette, priority, step, expiresAtNanos); + overlays.computeIfAbsent(viewerUuid, u -> new ConcurrentHashMap<>()).put(id, overlay); + } + + public void removeOverlay(UUID viewerUuid, String id) { + if (viewerUuid == null || id == null) return; + ConcurrentHashMap byId = overlays.get(viewerUuid); + if (byId == null) return; + byId.remove(id); + if (byId.isEmpty()) overlays.remove(viewerUuid, byId); + } + + private void appendSelection( + Long2LongOpenHashMap points, + int budget, + RPlayer player, + RBlockPos viewerBlock, + RWorldRef viewerWorld, + int range, + long rangeSq + ) { + Box selection = player.extras().get(ZonesExtraKeys.SELECTION).orElse(null); + if (selection == null || selection.getMin() == null || selection.getMax() == null) return; + + ParticleRenderMode mode = parseMode( + zones.getConfig().getString("visuals.particles.selection.mode", "FACES"), + ParticleRenderMode.FACES + ); + int step = zones.getConfig().getInt("visuals.particles.selection.step", 1); + + renderBox( + points, + budget, + viewerBlock, + viewerWorld, + selection.getMin(), + selection.getMax(), + selection.getWorld(), + mode, + ParticlePalette.PRIMARY_CORNERS, + 100, + step, + range, + rangeSq + ); + } + + private void appendNearby( + Long2LongOpenHashMap points, + int budget, + RPlayer player, + RBlockPos viewerBlock, + RWorldRef viewerWorld, + int range, + long rangeSq + ) { + boolean enabled = player.extras().get(ZonesExtraKeys.NEARBY_VISUALS).orElse(false); + if (!enabled) return; + + boolean highlight = zones.getConfig().getBoolean("visuals.particles.nearby.highlight-effective", true); + Region effective = highlight + ? zones.getRegionManager().getEffectiveRegionAt(viewerBlock, viewerWorld) + : null; + + ParticleRenderMode mode = parseMode( + zones.getConfig().getString("visuals.particles.nearby.mode", "PERIMETER_AT_VIEWER_Y"), + ParticleRenderMode.PERIMETER_AT_VIEWER_Y + ); + int step = zones.getConfig().getInt("visuals.particles.nearby.step", 1); + + List candidates = zones.getRegionManager().getRegionsNear(viewerBlock, viewerWorld, range); + for (Region region : candidates) { + if (points.size() >= budget) break; + if (region == null) continue; + RBlockPos min = region.getMin(); + RBlockPos max = region.getMax(); + if (min == null || max == null) continue; + + boolean isEffective = effective != null && Objects.equals(effective.getKey(), region.getKey()); + ParticlePalette palette = isEffective ? ParticlePalette.SECONDARY_CORNERS : ParticlePalette.PRIMARY_CORNERS; + + renderBox( + points, + budget, + viewerBlock, + viewerWorld, + min, + max, + region.getWorld(), + mode, + palette, + isEffective ? 10 : 0, + step, + range, + rangeSq + ); + } + } + + private void appendOverlays( + Long2LongOpenHashMap points, + int budget, + RPlayer player, + RBlockPos viewerBlock, + RWorldRef viewerWorld, + int range, + long rangeSq, + long nowNanos + ) { + ConcurrentHashMap byId = overlays.get(player.uuid()); + if (byId == null || byId.isEmpty()) return; + + for (var entry : byId.entrySet()) { + if (points.size() >= budget) break; + Overlay overlay = entry.getValue(); + if (overlay == null) continue; + if (overlay.expiresAtNanos <= nowNanos) { + byId.remove(entry.getKey(), overlay); + continue; + } + + Box box = overlay.box; + if (box == null || box.getMin() == null || box.getMax() == null) continue; + + renderBox( + points, + budget, + viewerBlock, + viewerWorld, + box.getMin(), + box.getMax(), + box.getWorld(), + overlay.mode, + overlay.palette, + overlay.priority, + overlay.step, + range, + rangeSq + ); + } + + if (byId.isEmpty()) overlays.remove(player.uuid(), byId); + } + + private void renderBox( + Long2LongOpenHashMap points, + int budget, + RBlockPos viewerBlock, + RWorldRef viewerWorld, + RBlockPos min, + RBlockPos max, + RWorldRef world, + ParticleRenderMode mode, + ParticlePalette palette, + int priority, + int configuredStep, + int range, + long rangeSq + ) { + if (points.size() >= budget) return; + if (min == null || max == null) return; + if (world != null && !sameWorld(viewerWorld, world)) return; + + if (distanceSquaredToAabb(viewerBlock, min, max) > rangeSq) return; + + int remaining = budget - points.size(); + int step = computeAutoStep(mode, min, max, remaining, Math.max(1, configuredStep)); + + ParticleHandler.emit( + mode, + min, + max, + viewerBlock, + range, + step, + (x, y, z, corner) -> { + long dSq = ParticleHandler.distanceSquared(viewerBlock.x(), viewerBlock.y(), viewerBlock.z(), x, y, + z); + if (dSq > rangeSq) return true; + + long packed = ParticleHandler.packBlockPos(x, y, z); + ZonesParticleRole role = corner ? palette.cornerRole() : palette.edgeRole(); + long encoded = encode(priority, corner, role); + + long existing = points.get(packed); + if (existing == NO_ENTRY) { + points.put(packed, encoded); + return points.size() < budget; + } + + if (shouldReplace(existing, encoded)) { + points.put(packed, encoded); + } + return true; + } + ); + } + + private static ParticleRenderMode parseMode(String raw, ParticleRenderMode fallback) { + if (raw == null) return fallback; + try { + return ParticleRenderMode.valueOf(raw.trim().toUpperCase()); + } catch (Exception ignored) { + return fallback; + } + } + + private static int computeAutoStep( + ParticleRenderMode mode, + RBlockPos min, + RBlockPos max, + int remaining, + int configuredStep + ) { + if (remaining <= 0) return Integer.MAX_VALUE; + + int minX = Math.min(min.x(), max.x()); + int maxX = Math.max(min.x(), max.x()) + 1; + int minY = Math.min(min.y(), max.y()); + int maxY = Math.max(min.y(), max.y()) + 1; + int minZ = Math.min(min.z(), max.z()); + int maxZ = Math.max(min.z(), max.z()) + 1; + + long xLen = (long) (maxX - minX) + 1; + long yLen = (long) (maxY - minY) + 1; + long zLen = (long) (maxZ - minZ) + 1; + + long approx; + switch (mode) { + case PERIMETER_AT_VIEWER_Y -> approx = 2L * xLen + 2L * zLen; + case WIRE_EDGES -> approx = 4L * (xLen + yLen + zLen); + default -> { + return configuredStep; + } + } + + if (approx <= remaining) return configuredStep; + int stepFromBudget = (int) Math.ceil((double) approx / (double) remaining); + return Math.max(configuredStep, Math.max(1, stepFromBudget)); + } + + private record Overlay( + Box box, + ParticleRenderMode mode, + ParticlePalette palette, + int priority, + int step, + long expiresAtNanos + ) { + private Overlay { + if (mode == null) mode = ParticleRenderMode.WIRE_EDGES; + if (palette == null) palette = ParticlePalette.PRIMARY_CORNERS; + step = Math.max(1, step); + } + } + + private static long encode(int priority, boolean corner, ZonesParticleRole role) { + long p = (long) priority; + long c = corner ? 1L : 0L; + long r = (long) (role == null ? 0 : role.ordinal()) & 0xFFFFL; + return (p << 32) | (c << 16) | r; + } + + private static boolean shouldReplace(long existing, long incoming) { + int existingPriority = (int) (existing >> 32); + int incomingPriority = (int) (incoming >> 32); + if (incomingPriority != existingPriority) return incomingPriority > existingPriority; + + boolean existingCorner = ((existing >> 16) & 1L) == 1L; + boolean incomingCorner = ((incoming >> 16) & 1L) == 1L; + return incomingCorner && !existingCorner; + } + + private static ZonesParticleRole decodeRole(long encoded) { + int ordinal = (int) (encoded & 0xFFFFL); + ZonesParticleRole[] values = ZonesParticleRole.values(); + if (ordinal < 0 || ordinal >= values.length) return ZonesParticleRole.PRIMARY; + return values[ordinal]; + } + + private static boolean sameWorld(RWorldRef a, RWorldRef b) { + if (a == null || b == null) return false; + return Objects.equals(a.identifier(), b.identifier()); + } + + private static long distanceSquaredToAabb(RBlockPos point, RBlockPos min, RBlockPos max) { + int minX = Math.min(min.x(), max.x()); + int maxX = Math.max(min.x(), max.x()); + int minY = Math.min(min.y(), max.y()); + int maxY = Math.max(min.y(), max.y()); + int minZ = Math.min(min.z(), max.z()); + int maxZ = Math.max(min.z(), max.z()); + + long dx = axisDistance(point.x(), minX, maxX); + long dy = axisDistance(point.y(), minY, maxY); + long dz = axisDistance(point.z(), minZ, maxZ); + return dx * dx + dy * dy + dz * dz; + } + + private static long axisDistance(int value, int min, int max) { + if (value < min) return (long) min - value; + if (value > max) return (long) value - max; + return 0L; + } +} diff --git a/api/src/main/resources/config.yml b/api/src/main/resources/config.yml index 1ac917c..a98a4b6 100644 --- a/api/src/main/resources/config.yml +++ b/api/src/main/resources/config.yml @@ -38,10 +38,37 @@ visuals: # Defines the particle type used for the corners of the selection # Defaults to WAX_ON secondary: WAX_ON + # Defines the particle type used for previews (e.g. expand preview) + # Defaults to COMPOSTER + preview: COMPOSTER # Maximum distance from the player to spawn particles # Defaults to 15 # Don't set this too high, can easily cause players to time out range: 15 + # How often particle visuals are rendered (in ms) + # Defaults to 200 + period-ms: 200 + # Maximum unique particle points per player per tick (hard cap) + # Defaults to 2500 + budget-per-player: 2500 + + selection: + # Render mode for the player's current selection + # Valid values are: FACES, WIRE_EDGES, PERIMETER_AT_VIEWER_Y + # Defaults to FACES + mode: FACES + # Sampling step (1 = every block) + step: 1 + + nearby: + # Shows the boundaries of all nearby regions + # Render mode for nearby region boundaries + # Valid values are: PERIMETER_AT_VIEWER_Y, WIRE_EDGES, FACES + mode: PERIMETER_AT_VIEWER_Y + # Sampling step for nearby region boundaries + step: 1 + # Highlights the effective region at the player's current position + highlight-effective: true zone-saving: # Valid values are SHUTDOWN, MODIFIED, PERIODIC diff --git a/api/src/main/resources/messages.properties b/api/src/main/resources/messages.properties deleted file mode 100644 index 9a40029..0000000 --- a/api/src/main/resources/messages.properties +++ /dev/null @@ -1,61 +0,0 @@ -commands.cancel.success=Cancelled selection. -commands.invalid=Invalid subcommand. Use /zone <...> -commands.invalid-player=Invalid player. -commands.invalid-permission=Invalid permission. -commands.no-permission=You don't have permission to execute this command! -commands.invalid-region=Invalid region. -commands.only-player=This command can only be executed by a player. -commands.create.overlap=Error: Region overlaps existing region. -commands.create.click-corners=Click two corners to select the region. -commands.create.success=Region '' created successfully! -commands.delete.success=Region '' deleted successfully! -commands.expand.success=Region '' expanded successfully! -commands.expand.fail=Error: Region '' could not be expanded. -commands.expand.invalid-usage=Invalid usage. Usage: /zone expand [overlap] -commands.save=Saved regions to file -commands.select.selected=Selected region '' -commands.select.deselected=Deselected region. -commands.perm.allow=Rule set: allow in region -commands.perm.deny=Rule set: deny in region -commands.perm.unset=Rule removed: in region -commands.perm.clear=Cleared all rules for in region -commands.perm.invalid-target=Invalid target. Use +universal, group:, player:, or a player name/UUID. -commands.perm.invalid-flag-kind=Permission can't be managed with /zone perm. -commands.perm.nothing-to-remove=No matching rule found. -commands.subcreate.no-parent=Error: No parent region found. -commands.subcreate.outside-parent=Error: Selection is outside parent region. -commands.subcreate.success=Sub-region created successfully! -commands.load=Loaded regions! -commands.rename.provide-name=Provide a new name for the region! -commands.rename.success=Region '' renamed to ''! -commands.import.not-loaded=Error: Plugin must be loaded for this command! -commands.import.no-plugin=Provide a plugin name to import regions from! -commands.import.success=Imported regions! -commands.mode.set=Mode set to - -messages.default=Message not found! Contact a Server Administrator -region.none-found=No regions found! -region.no-interact-permission=You are lacking the permission for type in region(s) -region.info.name= -region.info.min= -region.info.max= -region.info.key=Region key: # -region.info.parent=Parent region: -region.info.members.name= -region.info.members.permission= : -region.info.members.values.allowed= -region.info.members.values.denied= -create.primary=Set first location to -create.secondary=Set second location to -flags.universal=Universal - -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.role=Defines a member role -flags.group=Add a group to the player diff --git a/api/src/main/resources/messages.yml b/api/src/main/resources/messages.yml index 263fb8c..8beafda 100644 --- a/api/src/main/resources/messages.yml +++ b/api/src/main/resources/messages.yml @@ -50,6 +50,10 @@ commands: worldguard-tooltip: "Imports regions from WorldGuard" mode: set: "Mode set to " + find: + nearby: + enabled: "Nearby region boundaries enabled." + disabled: "Nearby region boundaries disabled." region: none-found: "No regions found!" diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPlatform.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPlatform.java index f7cff6c..6a1a530 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPlatform.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/BukkitPlatform.java @@ -5,6 +5,7 @@ import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Zones; +import de.t14d3.zones.ZonesParticleRole; import de.t14d3.zones.ZonesPlatform; import de.t14d3.zones.permissions.PermissionManager; import de.t14d3.zones.utils.Types; @@ -22,11 +23,13 @@ public class BukkitPlatform implements ZonesPlatform { private final ZonesBukkit plugin; private final Particle primary; private final Particle secondary; + private final Particle preview; public BukkitPlatform(ZonesBukkit plugin) { this.plugin = plugin; this.primary = Particle.valueOf(plugin.getConfig().getString("visuals.particles.primary", "WAX_OFF")); this.secondary = Particle.valueOf(plugin.getConfig().getString("visuals.particles.secondary", "WAX_ON")); + this.preview = Particle.valueOf(plugin.getConfig().getString("visuals.particles.preview", "COMPOSTER")); } @Override @@ -49,12 +52,17 @@ public ZonesBukkit getPlugin() { } @Override - public void spawnParticle(int type, RBlockPos particleLocation, RPlayer player) { + public void spawnParticle(ZonesParticleRole role, RBlockPos particleLocation, RPlayer player) { org.bukkit.entity.Player bukkitPlayer = player.tryHandle(org.bukkit.entity.Player.class).orElse(null); if (bukkitPlayer == null) return; org.bukkit.Location bukkitLocation = new Location(bukkitPlayer.getWorld(), particleLocation.x(), particleLocation.y(), particleLocation.z()); - ParticleBuilder particleBuilder = new ParticleBuilder(type == 1 ? primary : secondary); + Particle particle = switch (role) { + case PRIMARY -> primary; + case SECONDARY -> secondary; + case PREVIEW -> preview; + }; + ParticleBuilder particleBuilder = new ParticleBuilder(particle); particleBuilder.location(bukkitLocation); particleBuilder.receivers(bukkitPlayer); particleBuilder.count(1); diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CancelCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CancelCommand.java index 2ced00b..34ba083 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CancelCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/CancelCommand.java @@ -3,22 +3,18 @@ import de.t14d3.rapunzellib.message.MessageFormatService; import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.rapunzellib.objects.RWorldRef; -import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; import de.t14d3.zones.objects.Box; import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import dev.jorel.commandapi.CommandAPICommand; import org.bukkit.entity.Player; - public class CancelCommand { - private final RegionManager regionManager; private MessageFormatService messages; private ZonesBukkit plugin; public CancelCommand(ZonesBukkit plugin) { this.plugin = plugin; - this.regionManager = plugin.getRegionManager(); this.messages = plugin.getMessages(); } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/DeleteCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/DeleteCommand.java index 20543f1..017db28 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/DeleteCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/DeleteCommand.java @@ -13,10 +13,8 @@ public class DeleteCommand { private RegionManager regionManager; private MessageFormatService messages; - private final ZonesBukkit plugin; public DeleteCommand(ZonesBukkit plugin) { - this.plugin = plugin; this.regionManager = plugin.getRegionManager(); this.messages = plugin.getMessages(); } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java index 19b944c..d37279b 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java @@ -7,89 +7,242 @@ import de.t14d3.zones.RegionManager; import de.t14d3.zones.bukkit.ZonesBukkit; import de.t14d3.zones.bukkit.commands.utils.CustomArgument; +import de.t14d3.zones.objects.Box; import de.t14d3.zones.objects.Direction; +import de.t14d3.zones.visuals.particles.ParticlePalette; +import de.t14d3.zones.visuals.particles.ParticleRenderMode; import dev.jorel.commandapi.BukkitTooltip; import dev.jorel.commandapi.CommandAPICommand; import dev.jorel.commandapi.StringTooltip; +import dev.jorel.commandapi.SuggestionInfo; import dev.jorel.commandapi.arguments.ArgumentSuggestions; import dev.jorel.commandapi.arguments.BooleanArgument; import dev.jorel.commandapi.arguments.IntegerArgument; import dev.jorel.commandapi.arguments.StringArgument; +import dev.jorel.commandapi.executors.CommandArguments; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; public class ExpandCommand { + private static final String PREVIEW_ID = "expand-preview"; + private static final long PREVIEW_THROTTLE_NANOS = Duration.ofMillis(100).toNanos(); + private static final Duration PREVIEW_TTL = Duration.ofSeconds(10); + private final ZonesBukkit plugin; - private RegionManager regionManager; - private MessageFormatService messages; + private final RegionManager regionManager; + private final MessageFormatService messages; + private final ConcurrentHashMap lastPreviewNanos = new ConcurrentHashMap<>(); + public final CommandAPICommand expand; public ExpandCommand(ZonesBukkit plugin) { this.plugin = plugin; this.regionManager = plugin.getRegionManager(); this.messages = plugin.getMessages(); - } - public CommandAPICommand expand = new CommandAPICommand("expand") - .withPermission("zones.expand") - .withArguments(CustomArgument.region("key", "zones.expand.other", CustomArgument.MemberType.ADMIN), - new IntegerArgument("amount"), - new StringArgument("direction") - .setOptional(true) - .replaceSuggestions(ArgumentSuggestions.stringsWithTooltipsAsync(info -> { - return CompletableFuture.supplyAsync(() -> { - List directions = new ArrayList<>(); - for (Direction direction : Direction.values()) { - directions.add(direction.name()); - } - StringTooltip[] suggestions = new StringTooltip[directions.size()]; - int i = 0; - for (String direction : directions) { - suggestions[i++] = StringTooltip.ofMessage(direction, - BukkitTooltip.messageFromAdventureComponent( - messages.component("commands.expand.direction"))); - } - return suggestions; - }); - })), - new BooleanArgument("overlap") - .setOptional(true) - .withPermission("zones.expand.overlap") - ) - .executes((sender, args) -> { - RegionKey regionKey = RegionKey.fromString(args.getRaw("key")); - Direction direction; - Region region = regionManager.regions().get(regionKey.getValue()); - if (args.getRaw("direction") == null) { + this.expand = new CommandAPICommand("expand") + .withPermission("zones.expand") + .withArguments(CustomArgument.region("key", "zones.expand.other", CustomArgument.MemberType.ADMIN), + new IntegerArgument("amount") + .replaceSuggestions(ArgumentSuggestions.stringsAsync(info -> { + maybePreview(info, PreviewStage.AMOUNT); + return CompletableFuture.completedFuture(new String[0]); + })), + new StringArgument("direction") + .setOptional(true) + .replaceSuggestions(ArgumentSuggestions.stringsWithTooltipsAsync(info -> { + return CompletableFuture.supplyAsync(() -> { + maybePreview(info, PreviewStage.DIRECTION); + List directions = new ArrayList<>(); + for (Direction direction : Direction.values()) { + directions.add(direction.name()); + } + StringTooltip[] suggestions = new StringTooltip[directions.size()]; + int i = 0; + for (String direction : directions) { + suggestions[i++] = StringTooltip.ofMessage(direction, + BukkitTooltip.messageFromAdventureComponent( + messages.component("commands.expand.direction"))); + } + return suggestions; + }); + })), + new BooleanArgument("overlap") + .setOptional(true) + .withPermission("zones.expand.overlap") + ) + .executes((sender, args) -> { if (sender instanceof Player player) { - direction = Direction.fromYaw(player.getLocation().getYaw()); - if (region == null - || !regionManager.withWorldReadLock(region.getWorld(), - () -> region.isAdmin(player.getUniqueId()))) { - sender.sendMessage(messages.component("commands.invalid-region")); + plugin.getZones().getParticleVisualManager().removeOverlay(player.getUniqueId(), PREVIEW_ID); + } + RegionKey regionKey = RegionKey.fromString(args.getRaw("key")); + Direction direction; + Region region = regionManager.regions().get(regionKey.getValue()); + String directionArg = args.getRaw("direction"); + if (directionArg == null || directionArg.isBlank()) { + if (sender instanceof Player player) { + direction = Direction.fromYaw(player.getLocation().getYaw()); + if (region == null + || !regionManager.withWorldReadLock(region.getWorld(), + () -> region.isAdmin(player.getUniqueId()))) { + sender.sendMessage(messages.component("commands.invalid-region")); + return; + } + } else { + sender.sendMessage(messages.component("commands.invalid")); return; } } else { - sender.sendMessage(messages.component("commands.invalid")); - return; + direction = Direction.valueOf(directionArg.toUpperCase()); + } + int amount = args.getByClassOrDefault("amount", Integer.class, 0); + boolean overlapArg = args.getByClassOrDefault("overlap", Boolean.class, false); + boolean allowOverlap = overlapArg && sender.hasPermission("zones.expand.overlap"); + if (regionManager.expandBounds(region, direction, amount, allowOverlap)) { + sender.sendMessage( + messages.component("commands.expand.success", + Placeholders.builder().string("region", regionKey.toString()).build())); + } else { + sender.sendMessage( + messages.component("commands.expand.fail", + Placeholders.builder().string("region", regionKey.toString()).build())); } - } else { - direction = Direction.valueOf(args.getRaw("direction").toUpperCase()); - } - int amount = Integer.parseInt(args.getRaw("amount")); - Boolean overlapArg = (Boolean) args.get("overlap"); - boolean allowOverlap = Boolean.TRUE.equals(overlapArg) && sender.hasPermission("zones.expand.overlap"); - if (regionManager.expandBounds(region, direction, amount, allowOverlap)) { - sender.sendMessage( - messages.component("commands.expand.success", - Placeholders.builder().string("region", regionKey.toString()).build())); - } else { - sender.sendMessage( - messages.component("commands.expand.fail", - Placeholders.builder().string("region", regionKey.toString()).build())); } - } + ); + } + + private enum PreviewStage { + AMOUNT, + DIRECTION + } + + private void maybePreview(SuggestionInfo info, PreviewStage stage) { + if (info == null || stage == null) return; + if (!(info.sender() instanceof Player player)) return; + + long now = System.nanoTime(); + Long last = lastPreviewNanos.put(player.getUniqueId(), now); + if (last != null && (now - last) < PREVIEW_THROTTLE_NANOS) return; + + CommandArguments prev = info.previousArgs(); + if (prev == null) return; + + String rawKey = prev.getRaw("key"); + if (rawKey == null || rawKey.isBlank()) { + plugin.getZones().getParticleVisualManager().removeOverlay(player.getUniqueId(), PREVIEW_ID); + return; + } + + String rawAmount = stage == PreviewStage.AMOUNT ? info.currentArg() : prev.getRaw("amount"); + Integer amount = tryParseInt(rawAmount); + if (amount == null || amount <= 0) { + plugin.getZones().getParticleVisualManager().removeOverlay(player.getUniqueId(), PREVIEW_ID); + return; + } + + String rawDirection = stage == PreviewStage.DIRECTION ? info.currentArg() : null; + Direction direction = resolveDirection(rawDirection, player); + + RegionKey key = RegionKey.fromString(rawKey); + Region region = regionManager.regions().get(key.getValue()); + if (region == null) { + plugin.getZones().getParticleVisualManager().removeOverlay(player.getUniqueId(), PREVIEW_ID); + return; + } + + regionManager.withWorldReadLock(region.getWorld(), () -> { + var min = region.getMin(); + var max = region.getMax(); + if (min == null || max == null) { + plugin.getZones().getParticleVisualManager().removeOverlay(player.getUniqueId(), PREVIEW_ID); + return; + } + + Box preview = computeExpandedPreview(region, direction, amount); + if (preview == null || preview.getMin() == null || preview.getMax() == null) { + plugin.getZones().getParticleVisualManager().removeOverlay(player.getUniqueId(), PREVIEW_ID); + return; + } + + plugin.getZones().getParticleVisualManager().showOverlay( + player.getUniqueId(), + PREVIEW_ID, + preview, + ParticleRenderMode.WIRE_EDGES, + ParticlePalette.PREVIEW, + 50, + 1, + PREVIEW_TTL ); + }); + } + + private static Direction resolveDirection(String rawDirection, Player player) { + if (player == null) return Direction.NORTH; + if (rawDirection == null || rawDirection.isBlank()) { + return Direction.fromYaw(player.getLocation().getYaw()); + } + + String prefix = rawDirection.trim().toUpperCase(); + Direction exact = null; + for (Direction d : Direction.values()) { + if (d.name().equals(prefix)) return d; + if (d.name().startsWith(prefix)) { + if (exact != null) return Direction.fromYaw(player.getLocation().getYaw()); + exact = d; + } + } + return exact != null ? exact : Direction.fromYaw(player.getLocation().getYaw()); + } + + private static Integer tryParseInt(String raw) { + if (raw == null) return null; + String s = raw.trim(); + if (s.isEmpty()) return null; + + int end = 0; + boolean signed = s.charAt(0) == '-' || s.charAt(0) == '+'; + if (signed) end = 1; + while (end < s.length() && Character.isDigit(s.charAt(end))) end++; + if (end == 0 || (end == 1 && (signed))) return null; + + try { + return Integer.parseInt(s.substring(0, end)); + } catch (Exception ignored) { + return null; + } + } + + private static Box computeExpandedPreview(Region region, Direction direction, int amount) { + if (region == null || direction == null) return null; + var min = region.getMin(); + var max = region.getMax(); + var world = region.getWorld(); + if (min == null || max == null || world == null) return null; + + var newMin = min; + var newMax = max; + + switch (direction) { + case NORTH -> + newMin = new de.t14d3.rapunzellib.objects.RBlockPos(newMin.x(), newMin.y(), newMin.z() - amount); + case SOUTH -> + newMax = new de.t14d3.rapunzellib.objects.RBlockPos(newMax.x(), newMax.y(), newMax.z() + amount); + case WEST -> + newMin = new de.t14d3.rapunzellib.objects.RBlockPos(newMin.x() - amount, newMin.y(), newMin.z()); + case EAST -> + newMax = new de.t14d3.rapunzellib.objects.RBlockPos(newMax.x() + amount, newMax.y(), newMax.z()); + case DOWN -> + newMin = new de.t14d3.rapunzellib.objects.RBlockPos(newMin.x(), newMin.y() - amount, newMin.z()); + case UP -> newMax = new de.t14d3.rapunzellib.objects.RBlockPos(newMax.x(), newMax.y() + amount, newMax.z()); + } + + return new Box(newMin, newMax, world, true); + } } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/FindCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/FindCommand.java index 8ca4a3b..203c27c 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/FindCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/FindCommand.java @@ -1,40 +1,10 @@ package de.t14d3.zones.bukkit.commands; -/* -import de.t14d3.zones.bukkit.ZonesBukkit; -import de.t14d3.zones.objects.Player; -import dev.jorel.commandapi.CommandAPICommand; - -public class FindCommand { - private ZonesBukkit plugin; - - public FindCommand(ZonesBukkit plugin) { - this.plugin = plugin; - } - - public CommandAPICommand find = new CommandAPICommand("find") - .withPermission("zones.find") - .executes((sender, args) -> { - if (sender instanceof org.bukkit.entity.Player nativePlayer) { - Player player = plugin.getPlatform().getPlayer(nativePlayer.getUniqueId()); - if (plugin.getZones().getFindBossbar().players.containsKey(player)) { - plugin.getPlatform().getAudience(player) - .hideBossBar(plugin.getZones().getFindBossbar().players.get(player)); - plugin.getZones().getFindBossbar().players.remove(player); - } else { - plugin.getZones().getFindBossbar().players.put(player, - null); // null to let the bossbar handler do the creation - } - } else { - sender.sendMessage(plugin.getMessages().getCmp("commands.only-player")); - } - }); -} -*/ - import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.bukkit.ZonesBukkit; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import dev.jorel.commandapi.CommandAPICommand; +import dev.jorel.commandapi.arguments.BooleanArgument; public class FindCommand { private ZonesBukkit plugin; @@ -45,9 +15,29 @@ public FindCommand(ZonesBukkit plugin) { public CommandAPICommand find = new CommandAPICommand("find") .withPermission("zones.find") + .withSubcommand(new CommandAPICommand("nearby") + .withOptionalArguments(new BooleanArgument("enabled")) + .executes((sender, args) -> { + if (sender instanceof org.bukkit.entity.Player nativePlayer) { + RPlayer player = RPlayer.wrap(nativePlayer).orElse(null); + if (player == null) return; + + Boolean requested = (Boolean) args.get("enabled"); + boolean current = player.extras().get(ZonesExtraKeys.NEARBY_VISUALS).orElse( + plugin.getZones().getConfig().getBoolean("visuals.particles.nearby.enabled", false) + ); + boolean next = requested != null ? requested : !current; + player.extras().put(ZonesExtraKeys.NEARBY_VISUALS, next); + sender.sendMessage(plugin.getMessages().component( + next ? "commands.find.nearby.enabled" : "commands.find.nearby.disabled" + )); + } else { + sender.sendMessage(plugin.getMessages().component("commands.only-player")); + } + })) .executes((sender, args) -> { if (sender instanceof org.bukkit.entity.Player nativePlayer) { - RPlayer player = RPlayer.wrap(nativePlayer).orElse(null); + RPlayer player = RPlayer.wrap(nativePlayer).orElse(null); if (player == null) return; if (plugin.getZones().getFindBossbar().players.containsKey(player)) { diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java b/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java index c3737fe..b8a70ee 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java @@ -5,6 +5,7 @@ import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.rapunzellib.objects.RWorldRef; import de.t14d3.zones.Zones; +import de.t14d3.zones.ZonesParticleRole; import de.t14d3.zones.ZonesPlatform; import de.t14d3.zones.permissions.PermissionManager; import de.t14d3.zones.utils.Types; @@ -28,6 +29,7 @@ public class FabricPlatform implements ZonesPlatform { public SimpleParticleType primary; public ParticleOptions secondary; + public ParticleOptions preview; public FabricPlatform(ZonesFabric mod) { this.mod = mod; @@ -49,13 +51,19 @@ public Types getTypes() { } @Override - public void spawnParticle(int type, RBlockPos particleLocation, RPlayer player) { + public void spawnParticle(ZonesParticleRole role, RBlockPos particleLocation, RPlayer player) { ServerPlayer serverPlayer = player.tryHandle(ServerPlayer.class).orElse(null); if (serverPlayer == null) return; + ParticleOptions particle = switch (role) { + case PRIMARY -> primary; + case SECONDARY -> secondary; + case PREVIEW -> preview != null ? preview : secondary; + }; + serverPlayer.serverLevel().sendParticles( serverPlayer, - (type == 1 ? primary : secondary), + particle, false, false, particleLocation.x(), @@ -127,4 +135,3 @@ private ServerLevel resolveWorld(RWorldRef world, ServerPlayer fallbackPlayer) { return fallbackPlayer.serverLevel(); } } - diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java b/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java index a4196c4..7b77d80 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java @@ -75,16 +75,22 @@ void onStarted(MinecraftServer server) { String primaryType = zones.getConfig().getString("visuals.particles.primary", "WAX_OFF").toLowerCase(); String secondaryType = zones.getConfig().getString("visuals.particles.secondary", "WAX_ON").toLowerCase(); + String previewType = zones.getConfig().getString("visuals.particles.preview", "COMPOSTER").toLowerCase(); ParticleType primary = BuiltInRegistries.PARTICLE_TYPE - .get(ResourceLocation.withDefaultNamespace(primaryType)) + .get(ResourceLocation.withDefaultNamespace(primaryType)) .map(ref -> ref.value()) .orElseThrow(); ParticleType secondary = BuiltInRegistries.PARTICLE_TYPE .get(ResourceLocation.withDefaultNamespace(secondaryType)) .map(ref -> ref.value()) .orElseThrow(); + ParticleType preview = BuiltInRegistries.PARTICLE_TYPE + .get(ResourceLocation.withDefaultNamespace(previewType)) + .map(ref -> ref.value()) + .orElseThrow(); ((FabricPlatform) platform).primary = (SimpleParticleType) primary; ((FabricPlatform) platform).secondary = (SimpleParticleType) secondary; + ((FabricPlatform) platform).preview = (SimpleParticleType) preview; } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java index cf4482b..29a5103 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java @@ -5,28 +5,62 @@ import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import de.t14d3.rapunzellib.message.MessageFormatService; import de.t14d3.rapunzellib.message.Placeholders; +import de.t14d3.rapunzellib.objects.RBlockPos; import de.t14d3.zones.Region; import de.t14d3.zones.RegionKey; import de.t14d3.zones.RegionManager; import de.t14d3.zones.fabric.ZonesFabric; +import de.t14d3.zones.objects.Box; import de.t14d3.zones.objects.Direction; +import de.t14d3.zones.visuals.particles.ParticlePalette; +import de.t14d3.zones.visuals.particles.ParticleRenderMode; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; +import net.minecraft.server.level.ServerPlayer; + +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; public class ExpandCommand { + private static final String PREVIEW_ID = "expand-preview"; + private static final long PREVIEW_THROTTLE_NANOS = Duration.ofMillis(100).toNanos(); + private static final Duration PREVIEW_TTL = Duration.ofSeconds(10); + + private enum PreviewStage { + AMOUNT, + DIRECTION + } + + private final ZonesFabric mod; private final RegionManager regionManager; private final MessageFormatService messages; + private final ConcurrentHashMap lastPreviewNanos = new ConcurrentHashMap<>(); public ExpandCommand(ZonesFabric mod) { + this.mod = mod; this.regionManager = mod.getRegionManager(); this.messages = mod.getMessages(); } int execute(CommandContext context) { - Region region = regionManager.regions() - .get(RegionKey.fromString(context.getArgument("key", String.class)).getValue()); + ServerPlayer nativePlayer = context.getSource().getPlayer(); + if (nativePlayer != null) { + mod.getZones().getParticleVisualManager().removeOverlay(nativePlayer.getUUID(), PREVIEW_ID); + } + + RegionKey key; + try { + key = RegionKey.fromString(context.getArgument("key", String.class)); + } catch (Exception ignored) { + context.getSource().sendMessage(messages.component("commands.invalid-region")); + return 1; + } + + Region region = regionManager.regions().get(key.getValue()); if (region == null) { context.getSource().sendMessage(messages.component("commands.invalid-region")); return 1; @@ -69,14 +103,171 @@ int execute(CommandContext context) { return 1; } + private void maybePreview(CommandContext context, SuggestionsBuilder builder, PreviewStage stage) { + if (context == null || builder == null || stage == null) return; + ServerPlayer player = context.getSource().getPlayer(); + if (player == null) return; + + long now = System.nanoTime(); + Long last = lastPreviewNanos.put(player.getUUID(), now); + if (last != null && (now - last) < PREVIEW_THROTTLE_NANOS) return; + + String rawKey; + try { + rawKey = context.getArgument("key", String.class); + } catch (Exception ignored) { + mod.getZones().getParticleVisualManager().removeOverlay(player.getUUID(), PREVIEW_ID); + return; + } + + if (rawKey == null || rawKey.isBlank()) { + mod.getZones().getParticleVisualManager().removeOverlay(player.getUUID(), PREVIEW_ID); + return; + } + rawKey = rawKey.trim(); + if (rawKey.length() >= 2 && rawKey.startsWith("\"") && rawKey.endsWith("\"")) { + rawKey = rawKey.substring(1, rawKey.length() - 1).trim(); + } + + Integer parsedAmount; + if (stage == PreviewStage.DIRECTION) { + try { + parsedAmount = context.getArgument("amount", Integer.class); + } catch (Exception ignored) { + parsedAmount = null; + } + } else { + parsedAmount = tryParseInt(builder.getRemaining()); + } + + if (parsedAmount == null || parsedAmount <= 0) { + mod.getZones().getParticleVisualManager().removeOverlay(player.getUUID(), PREVIEW_ID); + return; + } + final int amount = parsedAmount; + + String rawDirection = stage == PreviewStage.DIRECTION ? builder.getRemaining() : null; + Direction direction = resolveDirection(rawDirection, player); + + RegionKey key; + try { + key = RegionKey.fromString(rawKey); + } catch (Exception ignored) { + mod.getZones().getParticleVisualManager().removeOverlay(player.getUUID(), PREVIEW_ID); + return; + } + + Region region = regionManager.regions().get(key.getValue()); + if (region == null) { + mod.getZones().getParticleVisualManager().removeOverlay(player.getUUID(), PREVIEW_ID); + return; + } + + boolean expandOther = CommandPermissions.check(context.getSource(), "zones.expand.other"); + regionManager.withWorldReadLock(region.getWorld(), () -> { + if (!expandOther && !region.isOwner(player.getUUID())) { + mod.getZones().getParticleVisualManager().removeOverlay(player.getUUID(), PREVIEW_ID); + return; + } + + Box preview = computeExpandedPreview(region, direction, amount); + if (preview == null || preview.getMin() == null || preview.getMax() == null) { + mod.getZones().getParticleVisualManager().removeOverlay(player.getUUID(), PREVIEW_ID); + return; + } + + mod.getZones().getParticleVisualManager().showOverlay( + player.getUUID(), + PREVIEW_ID, + preview, + ParticleRenderMode.WIRE_EDGES, + ParticlePalette.PREVIEW, + 50, + 1, + PREVIEW_TTL + ); + }); + } + + private static Direction resolveDirection(String rawDirection, ServerPlayer player) { + if (player == null) return Direction.NORTH; + if (rawDirection == null || rawDirection.isBlank()) { + return Direction.fromYaw(player.getViewYRot(1.0f)); + } + + String prefix = rawDirection.trim(); + if (prefix.length() >= 2 && prefix.startsWith("\"") && !prefix.endsWith("\"")) { + prefix = prefix.substring(1); + } + if (prefix.length() >= 2 && prefix.startsWith("\"") && prefix.endsWith("\"")) { + prefix = prefix.substring(1, prefix.length() - 1); + } + prefix = prefix.trim().toUpperCase(); + + Direction match = null; + for (Direction d : Direction.values()) { + if (d.name().equals(prefix)) return d; + if (d.name().startsWith(prefix)) { + if (match != null) return Direction.fromYaw(player.getViewYRot(1.0f)); + match = d; + } + } + return match != null ? match : Direction.fromYaw(player.getViewYRot(1.0f)); + } + + private static Integer tryParseInt(String raw) { + if (raw == null) return null; + String s = raw.trim(); + if (s.isEmpty()) return null; + + int end = 0; + boolean signed = s.charAt(0) == '-' || s.charAt(0) == '+'; + if (signed) end = 1; + while (end < s.length() && Character.isDigit(s.charAt(end))) end++; + if (end == 0 || (end == 1 && signed)) return null; + + try { + return Integer.parseInt(s.substring(0, end)); + } catch (Exception ignored) { + return null; + } + } + + private static Box computeExpandedPreview(Region region, Direction direction, int amount) { + if (region == null || direction == null) return null; + var min = region.getMin(); + var max = region.getMax(); + var world = region.getWorld(); + if (min == null || max == null || world == null) return null; + + RBlockPos newMin = min; + RBlockPos newMax = max; + + switch (direction) { + case NORTH -> newMin = new RBlockPos(newMin.x(), newMin.y(), newMin.z() - amount); + case SOUTH -> newMax = new RBlockPos(newMax.x(), newMax.y(), newMax.z() + amount); + case WEST -> newMin = new RBlockPos(newMin.x() - amount, newMin.y(), newMin.z()); + case EAST -> newMax = new RBlockPos(newMax.x() + amount, newMax.y(), newMax.z()); + case DOWN -> newMin = new RBlockPos(newMin.x(), newMin.y() - amount, newMin.z()); + case UP -> newMax = new RBlockPos(newMax.x(), newMax.y() + amount, newMax.z()); + } + + return new Box(newMin, newMax, world, true); + } + LiteralArgumentBuilder command() { return Commands.literal("expand") .requires(source -> CommandPermissions.check(source, "zones.expand")) - .then(Commands.argument("key", StringArgumentType.string()) + .then(Commands.argument("key", StringArgumentType.string()) .suggests(RootCommand::regionKeySuggestion) .then(Commands.argument("amount", IntegerArgumentType.integer()) + .suggests((context, builder) -> { + maybePreview(context, builder, PreviewStage.AMOUNT); + return builder.buildFuture(); + }) .then(Commands.argument("direction", StringArgumentType.string()) .suggests((context, builder) -> { + maybePreview(context, builder, PreviewStage.DIRECTION); builder.suggest("north"); builder.suggest("east"); builder.suggest("south"); diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/FindCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/FindCommand.java index d725375..c77e1f3 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/FindCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/FindCommand.java @@ -4,6 +4,7 @@ import com.mojang.brigadier.context.CommandContext; import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.fabric.ZonesFabric; +import de.t14d3.zones.rapunzellib.ZonesExtraKeys; import net.minecraft.commands.CommandSourceStack; public class FindCommand { @@ -33,5 +34,29 @@ int execute(CommandContext context) { } return 1; } -} + int toggleNearby(CommandContext context) { + return setNearby(context, null); + } + + int setNearby(CommandContext context, Boolean enabled) { + if (context.getSource().getPlayer() == null) { + context.getSource().sendMessage(mod.getMessages().component("commands.only-player")); + return 0; + } + + RPlayer player = RPlayer.wrap(context.getSource().getPlayer()).orElse(null); + if (player == null) return 0; + + boolean current = player.extras().get(ZonesExtraKeys.NEARBY_VISUALS).orElse( + mod.getZones().getConfig().getBoolean("visuals.particles.nearby.enabled", false) + ); + boolean next = enabled != null ? enabled : !current; + player.extras().put(ZonesExtraKeys.NEARBY_VISUALS, next); + + context.getSource().sendMessage(mod.getMessages().component( + next ? "commands.find.nearby.enabled" : "commands.find.nearby.disabled" + )); + return 1; + } +} diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java index 4c0ac95..d5d9c27 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java @@ -1,6 +1,7 @@ package de.t14d3.zones.fabric.commands; +import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; @@ -135,7 +136,14 @@ public void register() { .executes(modeCommand::execute))) .then(Commands.literal("find") .requires(source -> CommandPermissions.check(source, "zones.find")) - .executes(findCommand::execute)) + .executes(findCommand::execute) + .then(Commands.literal("nearby") + .executes(findCommand::toggleNearby) + .then(Commands.argument("enabled", BoolArgumentType.bool()) + .executes(context -> findCommand.setNearby( + context, + BoolArgumentType.getBool(context, "enabled") + ))))) .then(Commands.literal("select") .requires(source -> CommandPermissions.check(source, "zones.select")) .executes(selectCommand::execute) @@ -170,4 +178,3 @@ public void register() { )); } } - From 10d1b80d16e4d7ef43610f962ec81e70ba1f61e4 Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:58:33 +0100 Subject: [PATCH 3/9] Fix fabric, publishing stuff Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- .github/workflows/gradle-publish.yml | 47 ---- README.md | 17 ++ api/build.gradle.kts | 4 - .../t14d3/zones/visuals/ParticleHandler.java | 10 - .../particles/ParticleVisualManager.java | 7 +- build.gradle.kts | 240 +++++++++++++++--- bukkit/build.gradle.kts | 77 ++---- .../de/t14d3/zones/bukkit/ZonesBukkit.java | 5 - fabric/build.gradle.kts | 29 ++- .../de/t14d3/zones/fabric/FabricPlatform.java | 7 +- .../de/t14d3/zones/fabric/ZonesFabric.java | 27 +- .../zones/fabric/commands/PermCommand.java | 102 +++----- .../zones/fabric/commands/RootCommand.java | 10 +- gradle.properties | 7 +- gradle/libs.versions.toml | 8 +- jitpack.yml | 5 - 16 files changed, 338 insertions(+), 264 deletions(-) delete mode 100644 .github/workflows/gradle-publish.yml delete mode 100644 jitpack.yml diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml deleted file mode 100644 index 18b281e..0000000 --- a/.github/workflows/gradle-publish.yml +++ /dev/null @@ -1,47 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created -# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle - -name: Gradle Package - -on: - release: - types: [ released ] - tags-ignore: - - 'latest-dev' - workflow_dispatch: - -jobs: - build: - - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - server-id: github # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 - - - name: Build with Gradle - run: ./gradlew build - - # The USERNAME and TOKEN need to correspond to the credentials environment variables used in - # the publishing section of your build.gradle - - name: Publish to GitHub Packages - run: ./gradlew publish - env: - USERNAME: ${{ github.actor }} - TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 9dc1bf8..2050f56 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,23 @@ region the player is standing in. It currently supports the following placeholde responsive. * **Caching:** The plugin uses a caching system to improve performance when checking permissions. +## Developers + +Zones exposes a lot of internals for plugin developers to use (e.g. for custom flags, integrations, etc.). +Artifacts are available at https://maven.t14d3.de/#/snapshots/de/t14d3/zones/ + +Example: + +```kotlin +repositories { + maven("https://maven.t14d3.de/snapshots") +} + +dependencies { + compileOnly("de.t14d3.zones:api:") +} + +``` ## Contributing Contributions to the plugin are welcome! If you have any ideas for new features, bug fixes, or improvements, please feel diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 27a6dba..fdef70a 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -4,10 +4,6 @@ plugins { repositories { mavenCentral() - maven { - name = "JitPack" - url = uri("https://jitpack.io") - } } dependencies { diff --git a/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java b/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java index 0675253..90f745d 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java +++ b/api/src/main/java/de/t14d3/zones/visuals/ParticleHandler.java @@ -1,19 +1,9 @@ package de.t14d3.zones.visuals; import de.t14d3.rapunzellib.objects.RBlockPos; -import de.t14d3.zones.Zones; import de.t14d3.zones.visuals.particles.ParticleRenderMode; public class ParticleHandler { - private final Zones zones; - - public ParticleHandler(Zones zones) { - this.zones = zones; - } - - public void particleScheduler() { - zones.getParticleVisualManager().start(); - } @FunctionalInterface public interface ParticlePointConsumer { diff --git a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java index 3bfc960..41c8745 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java +++ b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java @@ -31,15 +31,10 @@ public final class ParticleVisualManager { public ParticleVisualManager(Zones zones) { this.zones = zones; this.platform = zones.getPlatform(); - } - - public void start() { - if (started) return; - started = true; long periodMs = zones.getConfig().getLong("visuals.particles.period-ms", 200); Duration period = Duration.ofMillis(Math.max(50, periodMs)); - Rapunzel.context().scheduler().runRepeatingAsync(period, period, this::tickSafely); + Rapunzel.context().scheduler().runRepeatingAsync(Duration.ofSeconds(1), period, this::tickSafely); } private void tickSafely() { diff --git a/build.gradle.kts b/build.gradle.kts index 642d11e..2ae78b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,6 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import net.fabricmc.loom.task.RemapJarTask + plugins { id("maven-publish") alias(libs.plugins.shadow) @@ -7,7 +10,74 @@ plugins { group = "de.t14d3" -version = "0.2.2" +val buildVersion = System.getenv("VERSION")?.takeIf { it.isNotBlank() } ?: "0.3.0" +version = buildVersion + +abstract class CheckReposiliteConfig : DefaultTask() { + @get:Input + @get:Optional + abstract val reposiliteBaseUrl: Property + @get:Input + @get:Optional + abstract val reposiliteUsername: Property + @get:Input + @get:Optional + abstract val reposilitePassword: Property + + @TaskAction + fun run() { + val missingKeys = mutableListOf() + + fun require(name: String, value: String?) { + if (value.isNullOrBlank()) missingKeys += name + } + + require("reposiliteUsername/REPOSILITE_USERNAME", reposiliteUsername.orNull) + require("reposilitePassword/REPOSILITE_PASSWORD", reposilitePassword.orNull) + + reposiliteBaseUrl.orNull?.let { baseUrl -> + val url = baseUrl.trim() + if (url.isNotBlank() && !url.startsWith("http://") && !url.startsWith("https://")) { + missingKeys += "reposiliteBaseUrl/REPOSILITE_BASE_URL must start with http:// or https://" + } + } + + if (missingKeys.isNotEmpty()) { + throw GradleException( + "Missing Reposilite publishing configuration:\n" + + missingKeys.joinToString(separator = "\n") { "- $it" } + + "\n\nConfigure these as Gradle properties (recommended: ~/.gradle/gradle.properties) or environment variables." + ) + } + } +} + +val reposiliteBaseUrl = + (findProperty("reposiliteBaseUrl") as String?) + ?: System.getenv("REPOSILITE_BASE_URL") + ?: "https://maven.t14d3.de" +val reposiliteUsername: String? = + (findProperty("reposiliteUsername") as String?) ?: System.getenv("REPOSILITE_USERNAME") +val reposilitePassword: String? = + (findProperty("reposilitePassword") as String?) ?: System.getenv("REPOSILITE_PASSWORD") + +val reposiliteRepo = if (buildVersion.endsWith("SNAPSHOT")) "snapshots" else "releases" +val reposiliteRepoUrl = "${reposiliteBaseUrl.trimEnd('/')}/$reposiliteRepo" + +val checkReposiliteConfig = tasks.register("checkReposiliteConfig") { + group = "publishing" + description = "Validates required configuration for publishing to Reposilite." + + reposiliteBaseUrl.set( + providers.gradleProperty("reposiliteBaseUrl").orElse(providers.environmentVariable("REPOSILITE_BASE_URL")) + ) + reposiliteUsername.set( + providers.gradleProperty("reposiliteUsername").orElse(providers.environmentVariable("REPOSILITE_USERNAME")) + ) + reposilitePassword.set( + providers.gradleProperty("reposilitePassword").orElse(providers.environmentVariable("REPOSILITE_PASSWORD")) + ) +} repositories { mavenLocal() @@ -16,29 +86,37 @@ repositories { name = "sonatype" url = uri("https://oss.sonatype.org/content/groups/public/") } + maven { + name = "t14d3-releases" + url = uri("${reposiliteBaseUrl.trimEnd('/')}/releases") + } maven { name = "t14d3-snapshots" - url = uri("https://maven.t14d3.de/snapshots") + url = uri("${reposiliteBaseUrl.trimEnd('/')}/snapshots") } maven { name = "papermc-repo" url = uri("https://repo.papermc.io/repository/maven-public/") } - maven { - name = "JitPack" - url = uri("https://jitpack.io") - } } allprojects { + group = rootProject.group + version = rootProject.version + plugins.apply("java") + plugins.apply("maven-publish") repositories { mavenLocal() mavenCentral() + maven { + name = "t14d3-releases" + url = uri("${reposiliteBaseUrl.trimEnd('/')}/releases") + } maven { name = "t14d3-snapshots" - url = uri("https://maven.t14d3.de/snapshots") + url = uri("${reposiliteBaseUrl.trimEnd('/')}/snapshots") } maven { name = "papermc-repo" @@ -46,11 +124,7 @@ allprojects { } maven { name = "sonatype" - url = uri("https://oss.sonatype.org/content/groups/public/") - } - maven { - name = "JitPack" - url = uri("https://jitpack.io") + url = uri("https://oss.sonatype.org/content/groups/public/") } } dependencies { @@ -62,6 +136,16 @@ allprojects { } } +subprojects { + extensions.configure { + archivesName.set("${rootProject.name.lowercase()}-${project.name}") + } +} + +extensions.configure { + archivesName.set(rootProject.name.lowercase()) +} + dependencies { implementation(project(":api")) implementation(project(":bukkit")) @@ -89,19 +173,12 @@ tasks.withType().configureEach { tasks { + jar { + enabled = false + } shadowJar { archiveClassifier.set("") mergeServiceFiles() - relocate("dev.jorel.commandapi", "de.t14d3.zones.dependencies.commandapi") - - relocate("org.simpleyaml", "de.t14d3.zones.dependencies.simpleyaml") - relocate("org.yaml.snakeyaml", "de.t14d3.zones.dependencies.snakeyaml") - - - dependencies { - exclude(dependency("org.checkerframework:checker-qual")) - exclude(dependency("io.leangen.geantyref:geantyref")) - } manifest { attributes["paperweight-mappings-namespace"] = "mojang" } @@ -122,23 +199,116 @@ tasks { } } -publishing { - repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/t14d3/zones") - credentials { - username = project.findProperty("gpr.user").toString() ?: System.getenv("USERNAME") - password = project.findProperty("gpr.key").toString() ?: System.getenv("TOKEN") +val syncCentralJars = tasks.register("syncCentralJars") { + group = "build" + description = "Copies API/Bukkit/Fabric jars into the root build/libs directory." + + val apiJar = project(":api").tasks.named("jar") + val bukkitJar = project(":bukkit").tasks.named("shadowJar") + val fabricJar = project(":fabric").tasks.named("remapJar") + + dependsOn(apiJar, bukkitJar, fabricJar) + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + includeEmptyDirs = false + into(layout.buildDirectory.dir("libs")) + + from(apiJar.flatMap { it.archiveFile }) + from(bukkitJar.flatMap { it.archiveFile }) + from(fabricJar.flatMap { it.archiveFile }) +} + +tasks.named("build") { + dependsOn(syncCentralJars) +} + + +allprojects { + + tasks.withType { + relocate("org.yaml.snakeyaml", "de.t14d3.zones.dependencies.snakeyaml") + relocate("dev.jorel.commandapi", "de.t14d3.zones.dependencies.commandapi") + + + dependencies { + exclude(dependency("org.checkerframework:checker-qual")) + exclude(dependency("io.leangen.geantyref:geantyref")) + } + } + + extensions.configure { + repositories { + maven { + name = "reposilite" + url = uri(reposiliteRepoUrl) + + credentials { + username = reposiliteUsername + password = reposilitePassword + } } } } + + tasks.withType().configureEach { + if (name.contains("ToReposiliteRepository")) { + dependsOn(rootProject.tasks.named("checkReposiliteConfig")) + } + } +} + +extensions.configure { publications { - create("gpr") { - artifactId = project.name.lowercase() - groupId = group.toString().lowercase() - version = version.toString() - from(components["java"]) + create("mavenJava") { + artifactId = rootProject.name.lowercase() + groupId = rootProject.group.toString() + version = buildVersion + artifact(tasks.named("shadowJar")) + } + } +} + +project(":api") { + plugins.withId("java") { + extensions.configure { + publications { + create("mavenJava") { + artifactId = "${rootProject.name.lowercase()}-api" + groupId = rootProject.group.toString() + version = buildVersion + from(components["java"]) + } + } + } + } +} + +project(":bukkit") { + plugins.withId("com.gradleup.shadow") { + extensions.configure { + publications { + create("mavenJava") { + artifactId = "${rootProject.name.lowercase()}-bukkit" + groupId = rootProject.group.toString() + version = buildVersion + artifact(tasks.named("shadowJar")) + } + } + } + } +} + +project(":fabric") { + plugins.withId("fabric-loom") { + extensions.configure { + publications { + create("mavenJava") { + artifactId = "${rootProject.name.lowercase()}-fabric" + groupId = rootProject.group.toString() + version = buildVersion + artifact(tasks.named("remapJar")) + } + } } } } diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index 48c8368..75d8ad7 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("java") alias(libs.plugins.run.paper) alias(libs.plugins.paperweight.userdev) + alias(libs.plugins.shadow) } repositories { @@ -34,64 +35,16 @@ dependencies { compileOnly(rootProject.libs.placeholderapi) compileOnly(rootProject.libs.worldedit.bukkit) compileOnly(rootProject.libs.worldguard.bukkit) - implementation(platform(rootProject.libs.intellectualsites.bom)) + compileOnly(platform(rootProject.libs.intellectualsites.bom)) compileOnly(rootProject.libs.fawe.core) compileOnly(rootProject.libs.fawe.bukkit) implementation(rootProject.libs.commandapi.paper.shade) - implementation(rootProject.libs.adventure.platform.bukkit) + compileOnly(rootProject.libs.adventure.platform.bukkit) - val localRapunzelLibsDir = rootProject.file("../RapunzelLib/build/libs") - - val localRapunzelPlatformPaper = localRapunzelLibsDir - .takeIf { it.isDirectory } - ?.listFiles() - ?.filter { it.isFile && it.name.startsWith("rapunzellib-platform-paper-") && it.name.endsWith("-shaded.jar") } - ?.maxByOrNull { it.lastModified() } - - val localRapunzelEventsPaper = localRapunzelLibsDir - .takeIf { it.isDirectory } - ?.listFiles() - ?.filter { - it.isFile && - it.name.startsWith("rapunzellib-events-paper-") && - it.name.endsWith(".jar") && - !it.name.endsWith("-sources.jar") && - !it.name.endsWith("-javadoc.jar") - } - ?.maxByOrNull { it.lastModified() } - - val localRapunzelEvents = localRapunzelLibsDir - .takeIf { it.isDirectory } - ?.listFiles() - ?.filter { - it.isFile && - it.name.startsWith("rapunzellib-events-") && - it.name.endsWith(".jar") && - !it.name.contains("-paper-") && - !it.name.contains("-fabric-") && - !it.name.endsWith("-sources.jar") && - !it.name.endsWith("-javadoc.jar") - } - ?.maxByOrNull { it.lastModified() } - - if (localRapunzelPlatformPaper != null) { - implementation(files(localRapunzelPlatformPaper)) - } else { - implementation("de.t14d3.rapunzellib:platform-paper:${libs.versions.rapunzellib.get()}:shaded") - } - - if (localRapunzelEvents != null) { - implementation(files(localRapunzelEvents)) - } else { - implementation(rootProject.libs.rapunzellib.events) - } - - if (localRapunzelEventsPaper != null) { - implementation(files(localRapunzelEventsPaper)) - } else { - implementation(rootProject.libs.rapunzellib.events.paper) - } + implementation(rootProject.libs.rapunzellib.api) + implementation(rootProject.libs.rapunzellib.platform.paper) + implementation(rootProject.libs.rapunzellib.events.paper) paperweight.paperDevBundle(rootProject.libs.versions.paper.dev.bundle.get()) } @@ -126,3 +79,21 @@ tasks.runServer { tasks.test { useJUnitPlatform() } + +tasks { + jar { + archiveClassifier.set("dev") + } + + shadowJar { + archiveClassifier.set("") + mergeServiceFiles() + + exclude("LICENSE*") + exclude("net/kyori/**") + } + + build { + dependsOn(shadowJar) + } +} diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java index 6ab49f1..36cf6cc 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java @@ -18,7 +18,6 @@ import de.t14d3.zones.utils.DebugLoggerManager; import de.t14d3.zones.utils.Types; import de.t14d3.zones.utils.Utils; -import de.t14d3.zones.visuals.ParticleHandler; import dev.jorel.commandapi.CommandAPI; import dev.jorel.commandapi.CommandAPIPaperConfig; import org.bukkit.permissions.Permission; @@ -86,10 +85,6 @@ public void onEnable() { this.saveDefaultConfig(); - // Create particle handler/scheduler - var particleHandler = new ParticleHandler(zones); - particleHandler.particleScheduler(); - // Register mode permissions for (Utils.SelectionMode mode : Utils.SelectionMode.values()) { getServer().getPluginManager().addPermission( diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 5aa6ddb..d4014b4 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -1,7 +1,8 @@ +import net.fabricmc.loom.task.RemapJarTask + plugins { id("java") id("fabric-loom") - alias(libs.plugins.shadow) } repositories { @@ -49,9 +50,15 @@ dependencies { modImplementation(include("net.kyori:adventure-platform-fabric:${libs.versions.adventure.platform.fabric.get()}")!!) implementation(rootProject.libs.adventure.minimessage) + include(rootProject.libs.adventure.minimessage) + + // Keep the mod dependency on the Loom-remapped classpath (dev runs use Mojang-mapped Minecraft), + // but still include the jars for jar-in-jar distribution. + modImplementation("de.t14d3.rapunzellib:platform-fabric:${libs.versions.rapunzellib.get()}") + include("de.t14d3.rapunzellib:platform-fabric:${libs.versions.rapunzellib.get()}") - modImplementation(include("de.t14d3.rapunzellib:platform-fabric:${libs.versions.rapunzellib.get()}")!!) - modImplementation(include("de.t14d3.rapunzellib:events-fabric:${libs.versions.rapunzellib.get()}")!!) + modImplementation("de.t14d3.rapunzellib:events-fabric:${libs.versions.rapunzellib.get()}") + include("de.t14d3.rapunzellib:events-fabric:${libs.versions.rapunzellib.get()}") } tasks.processResources { @@ -68,14 +75,12 @@ tasks.test { } tasks { - shadowJar { - exclude("*mixins.json") - exclude("*refmap.json") - exclude("*.accesswidener") - exclude("fabric-installer*") - exclude("LICENSE*") - exclude("/assets/") - exclude("/net/") - exclude("/ui/") + jar { + archiveClassifier.set("dev") + from(project(":api").the()["main"].output) } } + +tasks.named("remapJar") { + archiveClassifier.set("") +} diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java b/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java index b8a70ee..71fcd40 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/FabricPlatform.java @@ -60,8 +60,7 @@ public void spawnParticle(ZonesParticleRole role, RBlockPos particleLocation, RP case SECONDARY -> secondary; case PREVIEW -> preview != null ? preview : secondary; }; - - serverPlayer.serverLevel().sendParticles( + serverPlayer.level().sendParticles( serverPlayer, particle, false, @@ -125,13 +124,13 @@ private void removeBeacon(ServerPlayer player, ServerLevel world, RBlockPos loca } private ServerLevel resolveWorld(RWorldRef world, ServerPlayer fallbackPlayer) { - if (world == null) return fallbackPlayer.serverLevel(); + if (world == null) return fallbackPlayer.level(); String id = world.identifier(); for (ServerLevel level : mod.getServer().getAllLevels()) { if (level.dimension().location().toString().equals(id)) { return level; } } - return fallbackPlayer.serverLevel(); + return fallbackPlayer.level(); } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java b/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java index 7b77d80..22f3a81 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java @@ -47,6 +47,13 @@ public void onInitializeServer() { ServerLifecycleEvents.SERVER_STOPPING.register(this::onDisable); this.dataFolder = new File(FabricLoader.getInstance().getConfigDir().toFile(), "Zones"); + } + + void onEnable(@NotNull MinecraftServer server) { + this.server = server; + + FabricRapunzelBootstrap.bootstrap(MOD_ID, server, ZonesFabric.class); + this.platform = new FabricPlatform(this); this.types = new FabricTypes(this); @@ -54,22 +61,18 @@ public void onInitializeServer() { this.zones = new Zones(platform); this.regionManager = zones.getRegionManager(); - this.permissionManager = (FabricPermissionManager) zones.getPermissionManager(); - - Zones.getInstance().getLogger().info("Zones Fabric mod initialized!"); - } - - void onEnable(@NotNull MinecraftServer server) { - this.server = server; - - FabricRapunzelBootstrap.bootstrap(MOD_ID, server, ZonesFabric.class); GameEvents.install(this); ZonesRapunzelHooks.install(zones); + + this.permissionManager = (FabricPermissionManager) zones.getPermissionManager(); + this.rootCommand = new RootCommand(this); - // Fabric callbacks are bridged into RapunzelLib GameEvents; Zones subscribes via ZonesRapunzelHooks. + + Zones.getInstance().getLogger().info("Zones Fabric mod initialized!"); } void onStarted(MinecraftServer server) { + if (zones == null || regionManager == null) return; regionManager.loadRegions(); zones.getLogger().info("Mod enabled, loaded {} regions.", zones.getRegionManager().regions().size()); @@ -95,7 +98,9 @@ void onStarted(MinecraftServer server) { } void onDisable(MinecraftServer server) { - regionManager.saveRegions(); + if (regionManager != null) { + regionManager.saveRegions(); + } Rapunzel.shutdown(MOD_ID); } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java index 563e18c..f8cd6f5 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java @@ -28,6 +28,7 @@ import net.kyori.adventure.text.Component; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; +import net.minecraft.server.players.ProfileResolver; import java.util.Locale; import java.util.UUID; @@ -219,7 +220,7 @@ int executePattern(CommandContext context, boolean allow, bo if (flag.valueKind() == FlagValueKind.STRING_SET && (flag == Flags.ROLE || flag == Flags.GROUP)) { didChange = remove ? applyMembershipUnset(membership, subject, flag, rawPattern) - : applyMembership(membership, subject, flag, rawPattern, allow, false); + : applyMembership(membership, subject, flag, rawPattern, allow); } else { if (!remove) { didChange = applyAdd(perms, subject, flag, rawPattern, allow); @@ -286,66 +287,54 @@ boolean applyUnset(RegionPermissions perms, SubjectRef subject, Flag flag, Strin RegionPermissions.SubjectPermissions subjectPerms = perms.subjects().get(subject); if (subjectPerms == null) return false; RegionPermissions.PermissionValue pv = subjectPerms.get(flag.id()); - if (pv == null) return false; - - if (pv instanceof RegionPermissions.TargetDecisionValue targeted) { - String pattern = normalizeTargetDecisionValue(rawPattern); - return targeted.removeAllow(pattern) || targeted.removeDeny(pattern); - } - - if (pv instanceof RegionPermissions.StringSetValue set) { - String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); - if (value.isEmpty()) return false; - return set.allowValues().remove(value) || set.denyValues().remove(value); + switch (pv) { + case null -> { + return false; + } + case RegionPermissions.TargetDecisionValue targeted -> { + String pattern = normalizeTargetDecisionValue(rawPattern); + return targeted.removeAllow(pattern) || targeted.removeDeny(pattern); + } + case RegionPermissions.StringSetValue set -> { + String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); + if (value.isEmpty()) return false; + return set.allowValues().remove(value) || set.denyValues().remove(value); + } + default -> { + } } return false; } - private static boolean applyMembership(RegionMembership membership, SubjectRef subject, Flag flag, String rawPattern, boolean allow, boolean remove) { + private static boolean applyMembership(RegionMembership membership, SubjectRef subject, Flag flag, String rawPattern, boolean allow) { if (membership == null || subject == null || flag == null) return false; String value = rawPattern == null ? "" : rawPattern.trim().toLowerCase(Locale.ROOT); if (value.isEmpty()) return false; - boolean changed; if (flag == Flags.ROLE) { if (!(subject instanceof PlayerSubject p)) return false; RegionMembership.PlayerEntry entry = membership.player(p.uuid()); - if (remove) { - changed = allow ? entry.roles().removeAllow(value) : entry.roles().removeDeny(value); - } else { - if (allow) entry.roles().allow(value); - else entry.roles().deny(value); - changed = true; - } + if (allow) entry.roles().allow(value); + else entry.roles().deny(value); cleanupEmptyMembership(membership, p.uuid()); - return changed; + return true; } if (flag == Flags.GROUP) { if (subject instanceof PlayerSubject p) { RegionMembership.PlayerEntry entry = membership.player(p.uuid()); - if (remove) { - changed = allow ? entry.groups().removeAllow(value) : entry.groups().removeDeny(value); - } else { - if (allow) entry.groups().allow(value); - else entry.groups().deny(value); - changed = true; - } + if (allow) entry.groups().allow(value); + else entry.groups().deny(value); cleanupEmptyMembership(membership, p.uuid()); - return changed; + return true; } if (subject instanceof GroupSubject g) { RegionMembership.GroupEntry entry = membership.group(g.name()); - if (remove) { - changed = allow ? entry.includes().removeAllow(value) : entry.includes().removeDeny(value); - } else { - if (allow) entry.includes().allow(value); - else entry.includes().deny(value); - changed = true; - } + if (allow) entry.includes().allow(value); + else entry.includes().deny(value); cleanupEmptyGroupDefinition(membership, g.name()); - return changed; + return true; } return false; } @@ -524,8 +513,7 @@ private static SubjectRef parseTargetSubject(CommandSourceStack source, String r if ("+universal".equalsIgnoreCase(raw) || "@all".equalsIgnoreCase(raw) - || "@*".equalsIgnoreCase(raw) - || "+universal".equalsIgnoreCase(raw)) { + || "@*".equalsIgnoreCase(raw)) { return Subjects.universal(); } @@ -539,16 +527,13 @@ private static SubjectRef parseTargetSubject(CommandSourceStack source, String r if (source != null) { var server = source.getServer(); - if (server != null) { - var online = server.getPlayerList().getPlayerByName(nameOrUuid); - if (online != null) return new PlayerSubject(online.getUUID()); - try { - GameProfile profile = server.getProfileCache() != null - ? server.getProfileCache().get(nameOrUuid).orElse(null) - : null; - if (profile != null) return new PlayerSubject(profile.getId()); - } catch (Exception ignored) { - } + var online = server.getPlayerList().getPlayerByName(nameOrUuid); + if (online != null) return new PlayerSubject(online.getUUID()); + try { + ProfileResolver resolver = server.services().profileResolver(); + GameProfile profile = resolver.fetchByName(raw).orElse(null); + if (profile != null) return new PlayerSubject(profile.id()); + } catch (Exception ignored) { } } return null; @@ -571,16 +556,13 @@ private static SubjectRef parseTargetSubject(CommandSourceStack source, String r if (source != null) { var server = source.getServer(); - if (server != null) { - var online = server.getPlayerList().getPlayerByName(raw); - if (online != null) return new PlayerSubject(online.getUUID()); - try { - GameProfile profile = server.getProfileCache() != null - ? server.getProfileCache().get(raw).orElse(null) - : null; - if (profile != null) return new PlayerSubject(profile.getId()); - } catch (Exception ignored) { - } + var online = server.getPlayerList().getPlayerByName(raw); + if (online != null) return new PlayerSubject(online.getUUID()); + try { + ProfileResolver resolver = server.services().profileResolver(); + GameProfile profile = resolver.fetchByName(raw).orElse(null); + if (profile != null) return new PlayerSubject(profile.id()); + } catch (Exception ignored) { } } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java index d5d9c27..b5409e0 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/RootCommand.java @@ -1,6 +1,7 @@ package de.t14d3.zones.fabric.commands; +import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; @@ -12,7 +13,6 @@ import de.t14d3.zones.Zones; import de.t14d3.zones.fabric.ZonesFabric; import de.t14d3.zones.utils.Messages; -import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.kyori.adventure.platform.modcommon.impl.NonWrappingComponentSerializer; import net.kyori.adventure.platform.modcommon.impl.WrappedComponent; import net.minecraft.commands.CommandSourceStack; @@ -24,6 +24,7 @@ import java.util.concurrent.CompletableFuture; public class RootCommand { + private final ZonesFabric mod; private final CancelCommand cancelCommand; private final CreateCommand createCommand; private final DeleteCommand deleteCommand; @@ -41,6 +42,7 @@ public class RootCommand { private final MigrateCommand migrateCommand; public RootCommand(ZonesFabric mod) { + this.mod = mod; this.cancelCommand = new CancelCommand(mod); this.createCommand = new CreateCommand(mod); this.deleteCommand = new DeleteCommand(mod); @@ -92,7 +94,9 @@ protected static CompletableFuture regionKeySuggestion(CommandConte } public void register() { - CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register( + Zones.getInstance().getLogger().info("Registering zones command tree."); + CommandDispatcher dispatcher = mod.getServer().getCommands().getDispatcher(); + dispatcher.register( Commands.literal("zone") .then(Commands.literal("cancel") .requires(source -> CommandPermissions.check(source, "zones.cancel")) @@ -175,6 +179,6 @@ public void register() { return builder.buildFuture(); }) .executes(migrateCommand::execute))) - )); + ); } } diff --git a/gradle.properties b/gradle.properties index 3bc4570..bf86fb7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1 @@ -org.gradle.jvmargs=-Xmx2G -# Fabric Properties -minecraft_version=1.21.4 -yarn_mappings=1.21.4+build.8 -loader_version=0.16.10 -fabric_version=0.117.0+1.21.4 \ No newline at end of file +org.gradle.jvmargs=-Xmx2G \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4a178e..d6a8a96 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ annotations = "23.0.0" adventure = "4.19.0" adventure-platform-bukkit = "4.3.4" -adventure-platform-fabric = "6.2.0" +adventure-platform-fabric = "6.7.0" # Zones: api log4j-core = "2.25.3" @@ -31,9 +31,10 @@ commandapi-paper-shade = "11.0.1-SNAPSHOT" rapunzellib = "0.1.9-SNAPSHOT" # Zones: fabric -minecraft = "1.21.4" +minecraft = "1.21.10" +yarn-mappings = "1.21.10+build.3" fabric-loader = "0.16.10" -fabric-api = "0.117.0+1.21.4" +fabric-api = "0.138.4+1.21.10" fabric-resource-loader-v1 = "1.0.4+14959b836f" [plugins] @@ -66,6 +67,7 @@ commandapi-paper-shade = { module = "dev.jorel:commandapi-paper-shade", version. rapunzellib-api = { module = "de.t14d3.rapunzellib:api", version.ref = "rapunzellib" } rapunzellib-events = { module = "de.t14d3.rapunzellib:events", version.ref = "rapunzellib" } +rapunzellib-platform-paper = { module = "de.t14d3.rapunzellib:platform-paper", version.ref = "rapunzellib" } rapunzellib-events-paper = { module = "de.t14d3.rapunzellib:events-paper", version.ref = "rapunzellib" } rapunzellib-platform-fabric = { module = "de.t14d3.rapunzellib:platform-fabric", version.ref = "rapunzellib" } rapunzellib-events-fabric = { module = "de.t14d3.rapunzellib:events-fabric", version.ref = "rapunzellib" } diff --git a/jitpack.yml b/jitpack.yml deleted file mode 100644 index b4da404..0000000 --- a/jitpack.yml +++ /dev/null @@ -1,5 +0,0 @@ -before_install: - - source "$HOME/.sdkman/bin/sdkman-init.sh" - - sdk update - - sdk install java 21.0.3-zulu - - sdk use java 21.0.3-zulu From 80423b4d20d8141abeb6b77dec4083e1aae27fde Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:58:33 +0100 Subject: [PATCH 4/9] Fix fabric, publishing stuff Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- .github/workflows/gradle.yml | 14 ++++++++++++-- .../visuals/particles/ParticleVisualManager.java | 1 - 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index cf7c6e1..5271010 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,8 +1,18 @@ name: Java CI with Gradle on: - - push - - pull_request + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build: diff --git a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java index 41c8745..7cfb07e 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java +++ b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java @@ -26,7 +26,6 @@ public final class ParticleVisualManager { private final Zones zones; private final ZonesPlatform platform; private final ConcurrentHashMap> overlays = new ConcurrentHashMap<>(); - private volatile boolean started; public ParticleVisualManager(Zones zones) { this.zones = zones; From 7b80c885f21d9adde53ee8cda1d42024237dd7e7 Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:20:30 +0100 Subject: [PATCH 5/9] Goddammit Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 5271010..494218c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -46,7 +46,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: Package - path: build/libs/Zones*.jar + path: build/libs/zones*.jar - name: Release dev build uses: marvinpinto/action-automatic-releases@master From 6124246788ce94fd569ec76fac152e3c126fe4a6 Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:23:56 +0100 Subject: [PATCH 6/9] AAAAAAAAAAAAAAAA Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 494218c..46bc1fb 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -55,7 +55,7 @@ jobs: title: Development Build automatic_release_tag: latest-dev prerelease: true - files: build/libs/Zones*.jar + files: build/libs/zones*.jar repo_token: ${{ secrets.GITHUB_TOKEN }} # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html). From e7c2d25463bece6c4b3bf35394327088bd4992cd Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:10:29 +0100 Subject: [PATCH 7/9] I am a professional yesyes Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- README.md | 2 +- build.gradle.kts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2050f56..9ccc5a0 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ repositories { } dependencies { - compileOnly("de.t14d3.zones:api:") + compileOnly("de.t14d3.zones:zones-api:") } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 2ae78b1..4b49dbf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } -group = "de.t14d3" +group = "de.t14d3.zones" val buildVersion = System.getenv("VERSION")?.takeIf { it.isNotBlank() } ?: "0.3.0" version = buildVersion @@ -61,8 +61,7 @@ val reposiliteUsername: String? = val reposilitePassword: String? = (findProperty("reposilitePassword") as String?) ?: System.getenv("REPOSILITE_PASSWORD") -val reposiliteRepo = if (buildVersion.endsWith("SNAPSHOT")) "snapshots" else "releases" -val reposiliteRepoUrl = "${reposiliteBaseUrl.trimEnd('/')}/$reposiliteRepo" +val reposiliteRepoUrl = "${reposiliteBaseUrl.trimEnd('/')}/snapshots" val checkReposiliteConfig = tasks.register("checkReposiliteConfig") { group = "publishing" From 2cbf005ee0db567c158f6c7a8add422c0fcf4a01 Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Sun, 11 Jan 2026 09:12:04 +0100 Subject: [PATCH 8/9] Cleanup Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- .../java/de/t14d3/zones/RegionManager.java | 69 ++++++++++++++++++- .../de/t14d3/zones/WorldRegionManager.java | 20 +++++- api/src/main/java/de/t14d3/zones/Zones.java | 20 +----- .../zones/datasource/YamlDataSource.java | 5 +- .../de/t14d3/zones/visuals/FindBossbar.java | 6 +- .../particles/ParticleVisualManager.java | 9 ++- api/src/main/resources/config.yml | 8 ++- api/src/main/resources/messages.yml | 1 + .../de/t14d3/zones/bukkit/ZonesBukkit.java | 9 --- .../zones/bukkit/commands/ExpandCommand.java | 29 +++++++- .../bukkit/commands/SubCreateCommand.java | 8 ++- .../de/t14d3/zones/fabric/ZonesFabric.java | 9 +-- .../zones/fabric/commands/ExpandCommand.java | 27 +++++++- .../zones/fabric/commands/PermCommand.java | 8 +-- .../fabric/commands/SubCreateCommand.java | 7 +- gradle/libs.versions.toml | 2 +- 16 files changed, 178 insertions(+), 59 deletions(-) diff --git a/api/src/main/java/de/t14d3/zones/RegionManager.java b/api/src/main/java/de/t14d3/zones/RegionManager.java index 8f26130..09083e8 100644 --- a/api/src/main/java/de/t14d3/zones/RegionManager.java +++ b/api/src/main/java/de/t14d3/zones/RegionManager.java @@ -190,7 +190,19 @@ public Region createNewRegion(RegionKey key, String name, RBlockPos min, RBlockP * @param playerUUID The UUID of the player who will own the new region. * @param parentRegion The parent region of the new region. */ - public Region createSubRegion(String name, RBlockPos min, RBlockPos max, RWorldRef world, UUID playerUUID, Region parentRegion) { + public @Nullable Region createSubRegion(String name, RBlockPos min, RBlockPos max, RWorldRef world, UUID playerUUID, Region parentRegion) { + if (parentRegion == null || min == null || max == null || world == null) return null; + if (!sameWorld(world, parentRegion.getWorld())) return null; + + WorldRegionManager manager = getOrCreateWorldManager(world); + boolean insideParent = manager.withReadLock(() -> parentRegion.contains(min) && parentRegion.contains(max)); + if (!insideParent) return null; + + boolean overlaps = manager.withReadLock(() -> + manager.overlapsExistingRegionLocked(min, max, world, other -> isLineageRelated(parentRegion, other)) + ); + if (overlaps) return null; + RegionKey regionKey = RegionKey.generate(); RegionPermissions permissions = new RegionPermissions(); @@ -315,7 +327,18 @@ public boolean expandBounds(Region region, Direction direction, int amount, bool case DOWN -> newMin = new RBlockPos(newMin.x(), newMin.y() - amount, newMin.z()); } - if (!allowOverlap && manager.overlapsExistingRegionLocked(newMin, newMax, world, region.getKey())) { + if (newMin.x() > newMax.x() || newMin.y() > newMax.y() || newMin.z() > newMax.z()) { + return false; + } + + Region parent = region.getParentRegion(this); + if (parent != null) { + if (!parent.contains(newMin) || !parent.contains(newMax)) { + return false; + } + } + + if (!allowOverlap && overlapsOutsideLineageInternal(region, newMin, newMax)) { return false; } @@ -333,6 +356,20 @@ public void expandBounds(Region region, Direction direction, int amount) { expandBounds(region, direction, amount, true); } + private boolean overlapsOutsideLineageInternal(@Nullable Region region, RBlockPos min, RBlockPos max) { + if (region == null) return false; + RWorldRef world = region.getWorld(); + if (world == null) return false; + WorldRegionManager manager = getOrCreateWorldManager(world); + return manager.withReadLock(() -> + manager.overlapsExistingRegionLocked(min, max, world, other -> isLineageRelated(region, other)) + ); + } + + public boolean overlapsOutsideLineage(@Nullable Region region, RBlockPos min, RBlockPos max) { + return overlapsOutsideLineageInternal(region, min, max); + } + /** * Adds a region to the loaded regions map. * Requires an existing region object @@ -383,9 +420,35 @@ public void withWorldWriteLock(@Nullable RWorldRef world, Runnable action) { }); } + private boolean isLineageRelated(@Nullable Region a, @Nullable Region b) { + if (a == null || b == null) return false; + if (!sameWorld(a.getWorld(), b.getWorld())) return false; + if (a.getKey().equals(b.getKey())) return true; + return isAncestorOf(a, b) || isAncestorOf(b, a); + } + + private boolean isAncestorOf(@Nullable Region candidate, @Nullable Region possibleDescendant) { + if (candidate == null || possibleDescendant == null) return false; + if (!sameWorld(candidate.getWorld(), possibleDescendant.getWorld())) return false; + + Set visited = null; + Region current = possibleDescendant.getParentRegion(this); + while (current != null) { + if (current.getKey().equals(candidate.getKey())) return true; + if (visited == null) visited = new HashSet<>(); + if (!visited.add(current.getKey().getValue())) break; + current = current.getParentRegion(this); + } + return false; + } + + private static boolean sameWorld(@Nullable RWorldRef a, @Nullable RWorldRef b) { + if (a == null || b == null) return false; + return Objects.equals(a.identifier(), b.identifier()); + } + private WorldRegionManager getOrCreateWorldManager(RWorldRef world) { String worldId = world == null ? "" : world.identifier(); - if (worldId == null) worldId = ""; WorldRegionManager existing = worldManagers.get(worldId); if (existing != null) return existing; diff --git a/api/src/main/java/de/t14d3/zones/WorldRegionManager.java b/api/src/main/java/de/t14d3/zones/WorldRegionManager.java index b71a963..7f824e3 100644 --- a/api/src/main/java/de/t14d3/zones/WorldRegionManager.java +++ b/api/src/main/java/de/t14d3/zones/WorldRegionManager.java @@ -131,10 +131,26 @@ void updateCuboidLocked(Region region, RBlockPos oldMin, RBlockPos oldMax) { addRegionToChunksLocked(region, min, max); } - boolean overlapsExistingRegionLocked(RBlockPos min, RBlockPos max, RWorldRef world, @Nullable RegionKey keyToIgnore) { + boolean overlapsExistingRegionLocked( + RBlockPos min, + RBlockPos max, + RWorldRef world, + @Nullable RegionKey keyToIgnore + ) { + return overlapsExistingRegionLocked(min, max, world, + region -> region != null && region.getKey().equals(keyToIgnore)); + } + + boolean overlapsExistingRegionLocked( + RBlockPos min, + RBlockPos max, + RWorldRef world, + @Nullable java.util.function.Predicate ignore + ) { if (min == null || max == null || world == null) return false; for (Region region : regions.values()) { - if (region.intersects(min, max, world) && !region.getKey().equals(keyToIgnore)) { + if (region.intersects(min, max, world)) { + if (ignore != null && ignore.test(region)) continue; return true; } } diff --git a/api/src/main/java/de/t14d3/zones/Zones.java b/api/src/main/java/de/t14d3/zones/Zones.java index bff21b4..ab7d7bd 100644 --- a/api/src/main/java/de/t14d3/zones/Zones.java +++ b/api/src/main/java/de/t14d3/zones/Zones.java @@ -15,17 +15,14 @@ import java.io.File; import java.util.Objects; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -@SuppressWarnings("ThisEscapedInObjectConstruction") /** * Core Zones service container. * *

This class wires up managers (regions, permissions, config, visuals) and exposes * them to the platform implementations (Bukkit/Fabric) and integrations.

*/ +@SuppressWarnings("ThisEscapedInObjectConstruction") public class Zones { private static Zones instance; @@ -35,7 +32,6 @@ public class Zones { private final DebugLoggerManager debugLogger; private final Logger logger = LoggerFactory.getLogger(getClass()); private final YamlConfig config; - private final ThreadPoolExecutor executor; private final FindBossbar findBossbar; private final ParticleVisualManager particleVisualManager; @@ -63,13 +59,6 @@ public Zones(ZonesPlatform platform) { this.regionManager = new RegionManager(this); this.permissionManager = platform.createPermissionManager(this); this.particleVisualManager = new ParticleVisualManager(this); - this.executor = new ThreadPoolExecutor( - 0, - config.getInt("advanced.thread-pool.max-size", Runtime.getRuntime().availableProcessors() * 2), - config.getInt("advanced.thread-pool.keepalive", 60), - TimeUnit.SECONDS, - new SynchronousQueue<>() - ); this.findBossbar = new FindBossbar(this); } @@ -137,13 +126,6 @@ public File getDataFolder() { return platform.getDataFolder(); } - /** - * Returns the thread pool used for asynchronous/auxiliary work. - */ - public ThreadPoolExecutor getThreadPool() { - return executor; - } - /** * Returns the configured region saving mode. */ diff --git a/api/src/main/java/de/t14d3/zones/datasource/YamlDataSource.java b/api/src/main/java/de/t14d3/zones/datasource/YamlDataSource.java index 93ff360..3f9f410 100644 --- a/api/src/main/java/de/t14d3/zones/datasource/YamlDataSource.java +++ b/api/src/main/java/de/t14d3/zones/datasource/YamlDataSource.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.CompletableFuture; public class YamlDataSource extends AbstractDataSource { private final YamlConfig regionsFile; @@ -231,9 +230,9 @@ private static void readMembership(ConfigurationSection membershipSection, Regio */ @Override public void saveRegions(List regions) { - CompletableFuture.runAsync(() -> { + Rapunzel.context().scheduler().runAsync(() -> { try { - regionsFile.set("regions", null); // Clear existing regions + regionsFile.set("regions", null); // Clear existing regions for (Region region : regions) { saveRegion(region.getKey().toString(), region); diff --git a/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java b/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java index 8d81d5a..fa19054 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java +++ b/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java @@ -7,10 +7,10 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Executors; public class FindBossbar { private final Zones zones; @@ -27,8 +27,8 @@ public FindBossbar(Zones zones) { this.textColor = NamedTextColor.NAMES.value(zones.getConfig().getString("visuals.bossbar.text-color", "WHITE")); this.progress = zones.getConfig().getFloat("visuals.bossbar.progress", 1.0f); - Executors.newSingleThreadScheduledExecutor() - .scheduleAtFixedRate(new FindBossbarRunnable(), 0, 1, java.util.concurrent.TimeUnit.SECONDS); + Rapunzel.context().scheduler() + .runRepeatingAsync(Duration.ZERO, Duration.ofSeconds(1), new FindBossbarRunnable()); } private class FindBossbarRunnable implements Runnable { diff --git a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java index 7cfb07e..3ecc6c5 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java +++ b/api/src/main/java/de/t14d3/zones/visuals/particles/ParticleVisualManager.java @@ -31,8 +31,13 @@ public ParticleVisualManager(Zones zones) { this.zones = zones; this.platform = zones.getPlatform(); - long periodMs = zones.getConfig().getLong("visuals.particles.period-ms", 200); - Duration period = Duration.ofMillis(Math.max(50, periodMs)); + Duration period = zones.getConfig().getDuration("visuals.particles.period", null); + if (period == null) { + long periodMs = zones.getConfig().getLong("visuals.particles.period-ms", 200); + period = Duration.ofMillis(Math.max(50, periodMs)); + } else if (period.toMillis() < 50) { + period = Duration.ofMillis(50); + } Rapunzel.context().scheduler().runRepeatingAsync(Duration.ofSeconds(1), period, this::tickSafely); } diff --git a/api/src/main/resources/config.yml b/api/src/main/resources/config.yml index a98a4b6..09cef4f 100644 --- a/api/src/main/resources/config.yml +++ b/api/src/main/resources/config.yml @@ -45,8 +45,9 @@ visuals: # Defaults to 15 # Don't set this too high, can easily cause players to time out range: 15 - # How often particle visuals are rendered (in ms) - # Defaults to 200 + # How often particle visuals are rendered + # Accepts duration strings (e.g. 200ms, 1s). Legacy milliseconds key is kept for compatibility. + period: 200ms period-ms: 200 # Maximum unique particle points per player per tick (hard cap) # Defaults to 2500 @@ -77,7 +78,8 @@ zone-saving: # MODIFIED: Saves the regions.yml file when the plugin is disabled or when a region is modified # PERIODIC: Saves the regions.yml file periodically mode: MODIFIED - period: 60 # Default: 60, only used when mode is PERIODIC + # Duration between periodic saves (only used when mode is PERIODIC) + period: 60s # Controls whether the plugin should listen for specific events diff --git a/api/src/main/resources/messages.yml b/api/src/main/resources/messages.yml index 8beafda..3598d9e 100644 --- a/api/src/main/resources/messages.yml +++ b/api/src/main/resources/messages.yml @@ -22,6 +22,7 @@ commands: direction: "Direction to expand" success: "Region '' expanded successfully!" fail: "Error: Region '' could not be expanded." + outside-parent: "Error: Expansion would leave the parent region." invalid-usage: "Invalid usage. Usage: /zone expand [overlap]" save: "Saved regions to file" select: diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java index 36cf6cc..80aa207 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/ZonesBukkit.java @@ -101,15 +101,6 @@ public void onEnable() { }); - // Register saving task - if (getSavingMode() == Utils.SavingModes.PERIODIC) { - getServer().getScheduler().runTaskTimerAsynchronously(this, () -> { - regionManager.saveRegions(); - getLogger().info("Zones have been saved."); - }, 20L, getConfig().getInt("zone-saving.period", 60) * 20L); - } - - // PlaceholderAPI integration if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) { new PlaceholderAPI(this).register(); diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java index d37279b..193998d 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/ExpandCommand.java @@ -104,6 +104,29 @@ public ExpandCommand(ZonesBukkit plugin) { int amount = args.getByClassOrDefault("amount", Integer.class, 0); boolean overlapArg = args.getByClassOrDefault("overlap", Boolean.class, false); boolean allowOverlap = overlapArg && sender.hasPermission("zones.expand.overlap"); + Box preview = computeExpandedPreview(region, direction, amount); + if (preview == null || preview.getMin() == null || preview.getMax() == null) { + sender.sendMessage(messages.component("commands.expand.fail", + Placeholders.builder().string("region", regionKey.toString()).build())); + return; + } + + Region parent = region.getParentRegion(regionManager); + if (parent != null) { + boolean insideParent = regionManager.withWorldReadLock(parent.getWorld(), + () -> parent.contains(preview.getMin()) && parent.contains(preview.getMax())); + if (!insideParent) { + sender.sendMessage(messages.component("commands.expand.outside-parent")); + return; + } + } + + if (!allowOverlap && regionManager.overlapsOutsideLineage(region, preview.getMin(), + preview.getMax())) { + sender.sendMessage(messages.component("commands.create.overlap")); + return; + } + if (regionManager.expandBounds(region, direction, amount, allowOverlap)) { sender.sendMessage( messages.component("commands.expand.success", @@ -141,7 +164,7 @@ private void maybePreview(SuggestionInfo info, PreviewStage stage String rawAmount = stage == PreviewStage.AMOUNT ? info.currentArg() : prev.getRaw("amount"); Integer amount = tryParseInt(rawAmount); - if (amount == null || amount <= 0) { + if (amount == null || amount == 0) { plugin.getZones().getParticleVisualManager().removeOverlay(player.getUniqueId(), PREVIEW_ID); return; } @@ -243,6 +266,10 @@ private static Box computeExpandedPreview(Region region, Direction direction, in case UP -> newMax = new de.t14d3.rapunzellib.objects.RBlockPos(newMax.x(), newMax.y() + amount, newMax.z()); } + if (newMin.x() > newMax.x() || newMin.y() > newMax.y() || newMin.z() > newMax.z()) { + return null; + } + return new Box(newMin, newMax, world, true); } } diff --git a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SubCreateCommand.java b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SubCreateCommand.java index a217980..2011b6b 100644 --- a/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SubCreateCommand.java +++ b/bukkit/src/main/java/de/t14d3/zones/bukkit/commands/SubCreateCommand.java @@ -173,14 +173,18 @@ public SubCreateCommand(ZonesBukkit plugin) { return; } - regionManager.createSubRegion( - finalParentRegion.getName() + "_subb", + Region created = regionManager.createSubRegion( + finalParentRegion.getName() + "_sub", selection.getMin(), selection.getMax(), selectionWorld, player.getUniqueId(), finalParentRegion ); + if (created == null) { + player.sendMessage(messages.component("commands.create.overlap")); + return; + } plugin.getPlatform().removeBeacon(rPlayer, selectionWorld, selection.getMin()); plugin.getPlatform().removeBeacon(rPlayer, selectionWorld, selection.getMax()); diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java b/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java index 22f3a81..49f6dcb 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/ZonesFabric.java @@ -13,6 +13,7 @@ import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.Holder; import net.minecraft.core.particles.ParticleType; import net.minecraft.core.particles.SimpleParticleType; import net.minecraft.core.registries.BuiltInRegistries; @@ -80,16 +81,16 @@ void onStarted(MinecraftServer server) { String secondaryType = zones.getConfig().getString("visuals.particles.secondary", "WAX_ON").toLowerCase(); String previewType = zones.getConfig().getString("visuals.particles.preview", "COMPOSTER").toLowerCase(); ParticleType primary = BuiltInRegistries.PARTICLE_TYPE - .get(ResourceLocation.withDefaultNamespace(primaryType)) - .map(ref -> ref.value()) + .get(ResourceLocation.withDefaultNamespace(primaryType)) + .map(Holder.Reference::value) .orElseThrow(); ParticleType secondary = BuiltInRegistries.PARTICLE_TYPE .get(ResourceLocation.withDefaultNamespace(secondaryType)) - .map(ref -> ref.value()) + .map(Holder.Reference::value) .orElseThrow(); ParticleType preview = BuiltInRegistries.PARTICLE_TYPE .get(ResourceLocation.withDefaultNamespace(previewType)) - .map(ref -> ref.value()) + .map(Holder.Reference::value) .orElseThrow(); ((FabricPlatform) platform).primary = (SimpleParticleType) primary; ((FabricPlatform) platform).secondary = (SimpleParticleType) secondary; diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java index 29a5103..bc8184e 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/ExpandCommand.java @@ -93,6 +93,27 @@ int execute(CommandContext context) { return 1; } } + Box preview = computeExpandedPreview(region, direction, amount); + if (preview == null || preview.getMin() == null || preview.getMax() == null) { + context.getSource().sendMessage(messages.component("commands.expand.fail", + Placeholders.builder().string("region", region.getKey().toString()).build())); + return 1; + } + + Region parent = region.getParentRegion(regionManager); + if (parent != null) { + boolean insideParent = regionManager.withWorldReadLock(parent.getWorld(), + () -> parent.contains(preview.getMin()) && parent.contains(preview.getMax())); + if (!insideParent) { + context.getSource().sendMessage(messages.component("commands.expand.outside-parent")); + return 1; + } + } + + if (!allowOverlap && regionManager.overlapsOutsideLineage(region, preview.getMin(), preview.getMax())) { + context.getSource().sendMessage(messages.component("commands.create.overlap")); + return 1; + } if (regionManager.expandBounds(region, direction, amount, allowOverlap)) { context.getSource().sendMessage(messages.component("commands.expand.success", Placeholders.builder().string("region", region.getKey().toString()).build())); @@ -140,7 +161,7 @@ private void maybePreview(CommandContext context, Suggestion parsedAmount = tryParseInt(builder.getRemaining()); } - if (parsedAmount == null || parsedAmount <= 0) { + if (parsedAmount == null || parsedAmount == 0) { mod.getZones().getParticleVisualManager().removeOverlay(player.getUUID(), PREVIEW_ID); return; } @@ -252,6 +273,10 @@ private static Box computeExpandedPreview(Region region, Direction direction, in case UP -> newMax = new RBlockPos(newMax.x(), newMax.y() + amount, newMax.z()); } + if (newMin.x() > newMax.x() || newMin.y() > newMax.y() || newMin.z() > newMax.z()) { + return null; + } + return new Box(newMin, newMax, world, true); } diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java index f8cd6f5..873732d 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/PermCommand.java @@ -102,7 +102,7 @@ private LiteralArgumentBuilder patternCommand(String name, b .then(Commands.argument("flag", StringArgumentType.string()) .suggests(this::suggestFlags) .then(Commands.argument("pattern", StringArgumentType.string()) - .suggests(this::suggestPatterns) + .suggests(PermCommand::suggestPatterns) .executes(context -> executePattern(context, allow, remove)))))); } @@ -136,7 +136,7 @@ private CompletableFuture suggestTargets(CommandContext suggestPatterns(CommandContext context, SuggestionsBuilder builder) { + static CompletableFuture suggestPatterns(CommandContext context, SuggestionsBuilder builder) { builder.suggest("*"); builder.suggest("all"); @@ -254,7 +254,7 @@ int executePattern(CommandContext context, boolean allow, bo return 1; } - boolean applyAdd(RegionPermissions perms, SubjectRef subject, Flag flag, String rawPattern, boolean allow) { + static boolean applyAdd(RegionPermissions perms, SubjectRef subject, Flag flag, String rawPattern, boolean allow) { FlagValueKind kind = flag.valueKind(); if (kind == FlagValueKind.TARGET_DECISION) { String pattern = normalizeTargetDecisionValue(rawPattern); @@ -283,7 +283,7 @@ boolean applyAdd(RegionPermissions perms, SubjectRef subject, Flag flag, String return false; } - boolean applyUnset(RegionPermissions perms, SubjectRef subject, Flag flag, String rawPattern) { + static boolean applyUnset(RegionPermissions perms, SubjectRef subject, Flag flag, String rawPattern) { RegionPermissions.SubjectPermissions subjectPerms = perms.subjects().get(subject); if (subjectPerms == null) return false; RegionPermissions.PermissionValue pv = subjectPerms.get(flag.id()); diff --git a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SubCreateCommand.java b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SubCreateCommand.java index 02a7762..de00a49 100644 --- a/fabric/src/main/java/de/t14d3/zones/fabric/commands/SubCreateCommand.java +++ b/fabric/src/main/java/de/t14d3/zones/fabric/commands/SubCreateCommand.java @@ -84,7 +84,7 @@ int execute(CommandContext context) { return 1; } - regionManager.createSubRegion( + Region created = regionManager.createSubRegion( finalParentRegion.getName() + "_sub", selection.getMin(), selection.getMax(), @@ -92,6 +92,10 @@ int execute(CommandContext context) { player.uuid(), finalParentRegion ); + if (created == null) { + player.sendMessage(messages.component("commands.create.overlap")); + return 1; + } mod.getPlatform().removeBeacon(player, selectionWorld, selection.getMin()); mod.getPlatform().removeBeacon(player, selectionWorld, selection.getMax()); @@ -102,4 +106,3 @@ int execute(CommandContext context) { return 1; } } - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d6a8a96..4d0e4cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ intellectualsites-bom = "1.52" commandapi-paper-shade = "11.0.1-SNAPSHOT" # Zones: shared -rapunzellib = "0.1.9-SNAPSHOT" +rapunzellib = "0.2.0-SNAPSHOT" # Zones: fabric minecraft = "1.21.10" From 592feeb02896c0277a27f7aa3c73f93aaaf6b9c8 Mon Sep 17 00:00:00 2001 From: T14D3 <73843330+T14D3@users.noreply.github.com> Date: Sun, 11 Jan 2026 09:16:01 +0100 Subject: [PATCH 9/9] I am the stupid Signed-off-by: T14D3 <73843330+T14D3@users.noreply.github.com> --- api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java b/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java index fa19054..05a4d16 100644 --- a/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java +++ b/api/src/main/java/de/t14d3/zones/visuals/FindBossbar.java @@ -1,5 +1,6 @@ package de.t14d3.zones.visuals; +import de.t14d3.rapunzellib.Rapunzel; import de.t14d3.rapunzellib.objects.RPlayer; import de.t14d3.zones.Region; import de.t14d3.zones.Zones;