diff --git a/build.gradle.kts b/build.gradle.kts index fdbfe03..a4199c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,10 +7,10 @@ plugins { java id("de.undercouch.download") version "5.6.0" - id("io.freefair.lombok") version "8.7.1" + id("io.freefair.lombok") version "8.10.2" // Including dependencies in final jar id("io.github.goooler.shadow") version "8.1.8" - id("com.vanniktech.maven.publish") version "0.29.0" + id("com.vanniktech.maven.publish") version "0.30.0" } object DepData { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..2c35211 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0..e0fd020 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.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java b/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java index f118876..41d2107 100644 --- a/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java +++ b/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java @@ -305,10 +305,6 @@ public void onEnable() { setupNewCommands(); Utils.debug("Commands set up."); - // Register the event handlers we'll use - setupEvents(); - Utils.debug("Events set up."); - // Load the stored data try { dataHandler.load(); @@ -353,6 +349,10 @@ public void onEnable() { .scheduleSyncRepeatingTask(this, this::handleAutoUnclaim, check, check); Utils.debug("Scheduled unclaimed chunk checker."); + // Register the event handlers we'll use + setupEvents(); + Utils.debug("Events set up."); + // Done! Utils.log("Initialization complete."); } diff --git a/src/main/java/com/cjburkey/claimchunk/event/WorldProfileEventHandler.java b/src/main/java/com/cjburkey/claimchunk/event/WorldProfileEventHandler.java index 8f08526..43d6603 100644 --- a/src/main/java/com/cjburkey/claimchunk/event/WorldProfileEventHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/event/WorldProfileEventHandler.java @@ -24,6 +24,7 @@ import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.entity.Projectile; +import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -38,11 +39,108 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Pattern; @SuppressWarnings("unused") -public record WorldProfileEventHandler(ClaimChunk claimChunk) implements Listener { +public class WorldProfileEventHandler implements Listener { + + private final ClaimChunk claimChunk; + + public WorldProfileEventHandler(ClaimChunk claimChunk) { + this.claimChunk = claimChunk; + } + + private void fmtAndSendErrToPly(@NotNull Player player, @Nullable String flagName) { + + } + + private void handleCancelEntity( + @NotNull Consumer setCancelled, + @NotNull ChunkPos chunkPos, + @Nullable Player accessor, + @NotNull EntityType entity, + @NotNull EntityAccess.EntityAccessType entityAccessType, + @NotNull CCFlags.EntityFlagType interactionType, + @NotNull Function allowEntityAccess) { + var worldProfile = claimChunk.getProfileHandler().getProfile(chunkPos.world()); + if (!worldProfile.enabled) { + // Don't handle anything in this world + return; + } + + UUID accessorUuid = accessor == null ? null : accessor.getUniqueId(); + UUID chunkOwner = claimChunk.getChunkHandler().getOwner(chunkPos); + if (chunkOwner != null) { + // Make sure chunks should be protected when the player is online/offline + if (!shouldProtectOwnerChunks(chunkOwner, claimChunk.getServer(), worldProfile)) { + // Unprotected + return; + } + + // Check if the player has enabled/disabled this event with flags + FlagHandler.FlagProtectInfo protection = + claimChunk + .getFlagHandler() + .queryEntityProtection( + chunkOwner, accessorUuid, chunkPos, entity, interactionType); + if (protection.result().doesProtect(false)) { + setCancelled.accept(true); + if (accessor != null) { + fmtAndSendErrToPly(accessor, protection.flagName().orElse(null)); + } + return; + } + } + + // Fallback to the world profile protection + var entityAccess = + worldProfile.getEntityAccess(chunkOwner != null, chunkPos.world(), entity); + if (!allowEntityAccess.apply(entityAccess)) { + setCancelled.accept(true); + if (accessor != null) {fmtAndSendErrToPly(accessor, null);} + } + } + + private void shouldCancelBlock( + @NotNull Consumer setCancelled, + @NotNull ChunkPos chunkPos, + @Nullable Player accessor, + @NotNull Material block, + @NotNull BlockAccess.BlockAccessType blockAccessType, + @NotNull CCFlags.BlockFlagType interactionType, + @NotNull Function allowBlockAccess) { + var worldProfile = claimChunk.getProfileHandler().getProfile(chunkPos.world()); + + UUID accessorUuid = accessor == null ? null : accessor.getUniqueId(); + UUID chunkOwner = claimChunk.getChunkHandler().getOwner(chunkPos); + if (chunkOwner != null) { + // Make sure chunks should be protected when the player is online/offline + if (!shouldProtectOwnerChunks(chunkOwner, claimChunk.getServer(), worldProfile)) { + return; + } + + // Check if the player has enabled/disabled this event with flags + FlagHandler.FlagProtectInfo protection = + claimChunk + .getFlagHandler() + .queryBlockProtection( + chunkOwner, accessorUuid, chunkPos, block, interactionType); + if (protection.result().doesProtect(false)) { + setCancelled.accept(true); + if (accessor != null) {fmtAndSendErrToPly(accessor, protection.flagName().orElse(null));} + return; + } + } + + // Fallback to the world profile protection + var blockAccess = worldProfile.getBlockAccess(chunkOwner != null, chunkPos.world(), block); + if (!allowBlockAccess.apply(blockAccess)) { + setCancelled.accept(true); + if (accessor != null) {fmtAndSendErrToPly(accessor, null);} + } + } // -- EVENTS -- // diff --git a/src/main/java/com/cjburkey/claimchunk/flag/CCFlags.java b/src/main/java/com/cjburkey/claimchunk/flag/CCFlags.java index 0f13a55..9d69936 100644 --- a/src/main/java/com/cjburkey/claimchunk/flag/CCFlags.java +++ b/src/main/java/com/cjburkey/claimchunk/flag/CCFlags.java @@ -1,6 +1,7 @@ package com.cjburkey.claimchunk.flag; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Set; diff --git a/src/main/java/com/cjburkey/claimchunk/flag/CCPermFlags.java b/src/main/java/com/cjburkey/claimchunk/flag/CCPermFlags.java index c0dcf21..cff96c8 100644 --- a/src/main/java/com/cjburkey/claimchunk/flag/CCPermFlags.java +++ b/src/main/java/com/cjburkey/claimchunk/flag/CCPermFlags.java @@ -29,6 +29,7 @@ public class CCPermFlags { public final HashMap blockControls = new HashMap<>(); public final HashMap entityControls = new HashMap<>(); + public final HashMap flagDenyMessages = new HashMap<>(); public @Nullable CCFlags.SimpleFlag pvpFlag = null; public @Nullable CCFlags.SimpleFlag pearlFlag = null; private final HashSet allFlags = new HashSet<>(); @@ -212,8 +213,7 @@ public void loadFromConfig(@NotNull YamlConfiguration config) { } // Read each flag name - for (String flagName : flagSection.getKeys(false)) { - flagName = flagName.trim(); + for (final String flagName : flagSection.getKeys(false)) { if (!flagName.matches("[a-zA-Z0-9_-]+")) { Utils.err( "Flag name \"%s\" isn't alphanumeric! Must be a string of A-Z, a-z, 0-9," @@ -229,6 +229,23 @@ public void loadFromConfig(@NotNull YamlConfiguration config) { continue; } + // Check if the deny message is set for this flag + List msgs = flagEntries.stream().filter(map -> map.containsKey("denyMessage")).map(map -> { + if (map.size() > 1) { + Utils.warn("The deny message map entry for flag %s has other values specified, but they will be ignored! \"for\" entries cannot be combined with their own cancel messages!", flagName); + } + return (String) map.get("denyMessage"); + }).toList(); + @Nullable String protectionMessage = null; + if (!msgs.isEmpty()) { + if (msgs.size() > 1) { + Utils.warn("Protection message set multiple times for flag %s", flagName); + } + protectionMessage = msgs.getFirst(); + } + + flagDenyMessages.put(flagName, protectionMessage); + // Loop through each map for (Map flagMap : flagEntries) { String forType = (String) flagMap.get("for"); @@ -309,14 +326,11 @@ public void loadFromConfig(@NotNull YamlConfiguration config) { forType, flagName); } } + } - // Player property CJ-made-error safety check :) - if (blockControls.isEmpty() && entityControls.isEmpty()) { - throw new RuntimeException( - "ClaimChunk failed to load any block/entity protection flags, make sure the" - + " /plugins/ClaimChunk/flags.yml file is set up correctly (or allow it" - + " to regenerate)"); - } + // Player property CJ-made-error safety check :) + if (allFlags.isEmpty()) { + Utils.log("No flags loaded! If this is intentional, no worries!"); } } diff --git a/src/main/java/com/cjburkey/claimchunk/flag/FlagHandler.java b/src/main/java/com/cjburkey/claimchunk/flag/FlagHandler.java index f518d58..064b009 100644 --- a/src/main/java/com/cjburkey/claimchunk/flag/FlagHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/flag/FlagHandler.java @@ -10,7 +10,9 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; +import java.util.function.Function; public class FlagHandler { @@ -44,7 +46,7 @@ public void clearPermissionFlags( return dataHandler.getPlyFlags(owner, accessor, chunk); } - public boolean queryProtectionSimple( + public @NotNull FlagProtectInfo queryProtectionSimple( @NotNull UUID chunkOwner, @Nullable UUID accessor, @NotNull ChunkPos chunkPos, @@ -53,10 +55,10 @@ public boolean queryProtectionSimple( ApplicableFlags applicableFlags = getApplicableFlags(chunkOwner, accessor, chunkPos); return doesProtect(applicableFlags, simpleFlag.name(), simpleFlag.protectWhen()); } - return false; + return FlagProtectInfo.unspecified(); } - public boolean queryBlockProtection( + public @NotNull FlagProtectInfo queryBlockProtection( @NotNull UUID chunkOwner, @Nullable UUID accessor, @NotNull ChunkPos chunkPos, @@ -71,10 +73,10 @@ public boolean queryBlockProtection( protectingFlag.name(), protectingFlag.flagData().protectWhen()); } - return false; + return FlagProtectInfo.unspecified(); } - public boolean queryEntityProtection( + public @NotNull FlagProtectInfo queryEntityProtection( @NotNull UUID chunkOwner, @Nullable UUID accessor, @NotNull ChunkPos chunkPos, @@ -89,8 +91,7 @@ public boolean queryEntityProtection( protectingFlag.name(), protectingFlag.flagData().protectWhen()); } - - return false; + return FlagProtectInfo.unspecified(); } private @NotNull FlagHandler.FlagProtectResult checkFlag( @@ -106,7 +107,7 @@ public boolean queryEntityProtection( return FlagProtectResult.Unspecified; } - private ApplicableFlags getApplicableFlags( + private @NotNull ApplicableFlags getApplicableFlags( @NotNull UUID chunkOwner, @Nullable UUID accessor, @NotNull ChunkPos chunkPos) { Map chunkPlayerFlags = accessor == null ? null : getPlyFlags(chunkOwner, accessor, chunkPos); @@ -118,37 +119,44 @@ private ApplicableFlags getApplicableFlags( return new ApplicableFlags(chunkPlayerFlags, chunkFlags, playerFlags, globalFlags); } - // TODO: RETURN FlagProtectResult.Unspecified TO ALLOW WORLD PROFILE - // FALLBACK. - private boolean doesProtect( + private @NotNull FlagProtectInfo doesProtect( ApplicableFlags applicableFlags, String flagName, CCFlags.ProtectWhen protectWhen) { FlagProtectResult result; + final Optional flagNameOptional = Optional.of(flagName); + Function makeOutput = r -> new FlagProtectInfo(r, flagNameOptional); + if (applicableFlags.chunkPlayerFlags() != null) { result = checkFlag(applicableFlags.chunkPlayerFlags(), flagName, protectWhen); if (result.isSpecified()) { - return result.doesProtect(); + return makeOutput.apply(result); } } result = checkFlag(applicableFlags.chunkFlags(), flagName, protectWhen); if (result.isSpecified()) { - return result.doesProtect(); + return makeOutput.apply(result); } if (applicableFlags.playerFlags() != null) { result = checkFlag(applicableFlags.playerFlags(), flagName, protectWhen); if (result.isSpecified()) { - return result.doesProtect(); + return makeOutput.apply(result); } } result = checkFlag(applicableFlags.globalFlags(), flagName, protectWhen); if (result.isSpecified()) { - return result.doesProtect(); + return makeOutput.apply(result); } - return false; + return new FlagProtectInfo(FlagProtectResult.Unspecified, Optional.empty()); + } + + public record FlagProtectInfo(@NotNull FlagProtectResult result, @NotNull Optional flagName) { + public static FlagProtectInfo unspecified() { + return new FlagProtectInfo(FlagProtectResult.Unspecified, Optional.empty()); + } } public enum FlagProtectResult { diff --git a/src/main/java/com/cjburkey/claimchunk/i18n/V2JsonMessages.java b/src/main/java/com/cjburkey/claimchunk/i18n/V2JsonMessages.java index 1d07a29..efe53a0 100644 --- a/src/main/java/com/cjburkey/claimchunk/i18n/V2JsonMessages.java +++ b/src/main/java/com/cjburkey/claimchunk/i18n/V2JsonMessages.java @@ -156,9 +156,12 @@ public final class V2JsonMessages { public String chunkLeaveUnknown = "&6Entering unclaimed territory"; public String chunkLeaveSelf = "&6Exiting your territory"; + // Permission flag default messages + public String permFlagDenyDefault = "&c&e%%ACTION%%&c is disabled for &e%%TARGET%%&c in &e%%OWNER%%&c's chunk"; + // Protection localization public String chunkCancelAdjacentPlace = - "&cYou can't place &e%%BLOCK%%&c next to &e%%BLOCK%%&c in %%OWNER%%&c's chunk"; + "&cYou can't place &e%%BLOCK%%&c next to &e%%BLOCK%%&c in &e%%OWNER%%&c's chunk"; public String chunkCancelClaimedEntityInteract = "&cYou can't interact with &e%%ENTITY%%&c in &e%%OWNER%%&c's chunk"; public String chunkCancelUnclaimedEntityInteract = diff --git a/src/main/resources/flags.yml b/src/main/resources/flags.yml index 75e5e13..3891475 100644 --- a/src/main/resources/flags.yml +++ b/src/main/resources/flags.yml @@ -27,7 +27,12 @@ permissionFlags: breakBlocks: - # Blocks + # The deny message can be specified for each flag, but it must be separate + # from each "for" entry for the flag. + # If you don't provide a deny message here, the one from your + # "messages.json" will be shown instead. + - denyMessage: '&cYou cannot break &e%%TARGET%%&c in &e%%OWNER%%&c''s chunks' + # Protection map (for example with multiple, see the "containers" flag): - for: BLOCKS # For blocks, type can be BREAK, PLACE, INTERACT, or EXPLODE # If no `include` is present, the default is to include all @@ -37,53 +42,64 @@ permissionFlags: # Similarly, `exclude` includes all default blocks/items and # excludes the provided ones. # If both are provided, exclusions are processed FIRST and then - # inclusions + # inclusions. type: BREAK placeBlocks: + - denyMessage: '&cYou cannot place &e%%TARGET%%&c in &e%%OWNER%%&c''s chunks' - for: BLOCKS type: PLACE interactBlocks: + - denyMessage: '&cYou cannot interact with &e%%TARGET%%&c in &e%%OWNER%%&c''s chunks' - for: BLOCKS type: INTERACT # Handle these separately. - # Note: @CONTAINER blocks are covered by the `containers` flag below + # Note: @CONTAINER blocks are covered by the `containers` flag towards + # the bottom. exclude: ['@REDSTONE', '@DOORS', '@CONTAINER'] redstone: + - denyMessage: '&cYou cannot interact with redstone blocks in &e%%OWNER%%&c''s chunks' - for: BLOCKS type: INTERACT include: ['@REDSTONE'] doors: + - denyMessage: '&cYou cannot interact with doors in &e%%OWNER%%&c''s chunks' - for: BLOCKS type: INTERACT include: ['@DOORS'] # Entities damageEntities: + - denyMessage: '&cYou cannot damage &e%%TARGET%%&c in &e%%OWNER%%&c''s chunks' - for: ENTITIES # Entities can have DAMAGE, INTERACT, and EXPLODE protection types type: DAMAGE interactEntities: + - denyMessage: '&cYou cannot interact with &e%%TARGET%%&c in &e%%OWNER%%&c''s chunks' - for: ENTITIES type: INTERACT # Handle container entities with container blocks in the `containers` flag exclude: ['@VEHICLES', '@CONTAINER_ENTITIES'] vehicles: + - denyMessage: '&cYou cannot use vehicles in &e%%OWNER%%&c''s chunks' - for: ENTITIES type: INTERACT include: ['@VEHICLES'] # In this example, when `disablePvp` is enabled, protection is enabled, # unlike the other flags. disablePvp: + - denyMessage: '&cPVP is disabled in &e%%OWNER%%&c''s chunks' # PVP is unique, set the `for` to `PLAYERS`: - for: PLAYERS protectWhen: ENABLED # Ender pearls are another unique target disablePearl: + - denyMessage: '&cEnder pearls are disabled in &e%%OWNER%%&c''s chunks' - for: PEARLS protectWhen: ENABLED # Can also handle multiple types with one flag containers: + - denyMessage: '&cYou cannot use containers in &e%%OWNER%%&c''s chunks' - for: BLOCKS type: INTERACT include: ['@CONTAINER']