diff --git a/Core/src/main/java/com/plotsquared/core/commands/CommandAdd.java b/Core/src/main/java/com/plotsquared/core/commands/CommandAdd.java new file mode 100644 index 000000000..2f1723783 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/commands/CommandAdd.java @@ -0,0 +1,86 @@ +package com.plotsquared.core.commands; + +import cloud.commandframework.annotations.Argument; +import cloud.commandframework.annotations.CommandMethod; +import cloud.commandframework.annotations.CommandPermission; +import com.google.inject.Inject; +import com.plotsquared.core.commands.arguments.PlotMember; +import com.plotsquared.core.commands.requirements.Requirement; +import com.plotsquared.core.commands.requirements.RequirementType; +import com.plotsquared.core.configuration.Settings; +import com.plotsquared.core.configuration.caption.TranslatableCaption; +import com.plotsquared.core.permissions.Permission; +import com.plotsquared.core.player.PlotPlayer; +import com.plotsquared.core.plot.Plot; +import com.plotsquared.core.util.EventDispatcher; +import com.plotsquared.core.util.PlayerManager; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.checkerframework.checker.nullness.qual.NonNull; + +class CommandAdd implements PlotSquaredCommandContainer { + + private final EventDispatcher eventDispatcher; + + @Inject + CommandAdd(final @NonNull EventDispatcher eventDispatcher) { + this.eventDispatcher = eventDispatcher; + } + + @Requirement(RequirementType.PLAYER) + @Requirement(RequirementType.IS_OWNER) + @CommandPermission("plots.add") + @CommandMethod("plot add [target]") + public void commandAdd( + final @NonNull PlotPlayer sender, + @Argument("target") final PlotMember target, + final @NonNull Plot plot + ) { + if (target instanceof PlotMember.Everyone) { + if (!sender.hasPermission(Permission.PERMISSION_TRUST_EVERYONE) && !sender.hasPermission(Permission.PERMISSION_ADMIN_COMMAND_TRUST)) { + sender.sendMessage( + TranslatableCaption.of("errors.invalid_player"), + TagResolver.resolver("value", Tag.inserting( + PlayerManager.resolveName(target.uuid()).toComponent(sender) + )) + ); + return; + } + } else if (plot.isOwner(target.uuid())) { + sender.sendMessage( + TranslatableCaption.of("member.already_added"), + TagResolver.resolver("player", Tag.inserting( + PlayerManager.resolveName(target.uuid()).toComponent(sender) + )) + ); + return; + } else if (plot.getMembers().contains(target.uuid())) { + sender.sendMessage( + TranslatableCaption.of("member.already_added"), + TagResolver.resolver("player", Tag.inserting( + PlayerManager.resolveName(target.uuid()).toComponent(sender) + )) + ); + return; + } else if (plot.getMembers().size() >= sender.hasPermissionRange(Permission.PERMISSION_ADD, Settings.Limit.MAX_PLOTS)) { + sender.sendMessage( + TranslatableCaption.of("members.plot_max_members_added"), + TagResolver.resolver("amount", Tag.inserting(Component.text(plot.getMembers().size()))) + ); + return; + } + + if (target instanceof PlotMember.Player) { + if (!plot.removeTrusted(target.uuid())) { + if (plot.getDenied().contains(target.uuid())) { + plot.removeDenied(target.uuid()); + } + } + } + + plot.addMember(target.uuid()); + this.eventDispatcher.callMember(sender, plot, target.uuid(), true); + sender.sendMessage(TranslatableCaption.of("member.member_added")); + } +} diff --git a/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCaptionKeys.java b/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCaptionKeys.java new file mode 100644 index 000000000..57d732676 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCaptionKeys.java @@ -0,0 +1,8 @@ +package com.plotsquared.core.commands; + +import cloud.commandframework.captions.Caption; + +public final class PlotSquaredCaptionKeys { + + public static final Caption ARGUMENT_PARSE_FAILURE_TARGET = Caption.of("argument.parse.failure.target"); +} diff --git a/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCommandContainer.java b/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCommandContainer.java new file mode 100644 index 000000000..8476e5b6a --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCommandContainer.java @@ -0,0 +1,8 @@ +package com.plotsquared.core.commands; + +/** + * Indicates that a class contains commands. + */ +public interface PlotSquaredCommandContainer { + +} diff --git a/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCommandManager.java b/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCommandManager.java index 644ab0bf4..39218761c 100644 --- a/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCommandManager.java +++ b/Core/src/main/java/com/plotsquared/core/commands/PlotSquaredCommandManager.java @@ -23,6 +23,7 @@ import cloud.commandframework.annotations.AnnotationParser; import cloud.commandframework.meta.SimpleCommandMeta; import com.google.inject.Inject; import com.google.inject.Injector; +import com.plotsquared.core.commands.parsers.PlotMemberParser; import com.plotsquared.core.player.PlotPlayer; import io.leangen.geantyref.TypeToken; import org.checkerframework.checker.nullness.qual.NonNull; @@ -63,8 +64,13 @@ public class PlotSquaredCommandManager { * Initializes all the known commands. */ public void initializeCommands() { - final Stream> commandClasses = Stream.of( - ); - commandClasses.map(injector::getInstance).forEach(this::scanClass); + // We start by scanning the parsers. + Stream.of( + PlotMemberParser.class + ).map(this.injector::getInstance).forEach(this::scanClass); + // Then we scan the commands. + Stream.of( + CommandAdd.class + ).map(this.injector::getInstance).forEach(this::scanClass); } } diff --git a/Core/src/main/java/com/plotsquared/core/commands/arguments/PlotMember.java b/Core/src/main/java/com/plotsquared/core/commands/arguments/PlotMember.java new file mode 100644 index 000000000..0545b4a6c --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/commands/arguments/PlotMember.java @@ -0,0 +1,76 @@ +package com.plotsquared.core.commands.arguments; + +import cloud.commandframework.context.CommandContext; +import com.plotsquared.core.commands.parsers.PlotMemberParser; +import com.plotsquared.core.database.DBFunc; +import com.plotsquared.core.player.PlotPlayer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.UUID; + +public sealed interface PlotMember { + + PlotMember EVERYONE = new Everyone(); + + default @NonNull UUID uuid(@NonNull CommandContext> context) { + return this.uuid(); + } + + @NonNull UUID uuid(); + + sealed interface PlayerLike extends PlotMember { + } + + record Player(@NonNull UUID uuid) implements PlayerLike { + } + + final class LazyPlayer implements PlayerLike { + + private final String candidate; + private final UuidSupplier uuidSupplier; + private @MonotonicNonNull UUID cachedUuid = null; + + public LazyPlayer( + final @NonNull String candidate, + final @NonNull UuidSupplier uuidSupplier + ) { + this.candidate = candidate; + this.uuidSupplier = uuidSupplier; + } + + public @NonNull UUID uuid() { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized @NonNull UUID uuid(final @NonNull CommandContext> context) { + if (this.cachedUuid == null) { + try { + this.cachedUuid = this.uuidSupplier.uuid(); + } catch (Exception ignored) { + } + + // The player didn't exist :-( + if (this.cachedUuid == null) { + throw new PlotMemberParser.TargetParseException(this.candidate, context); + } + } + return this.cachedUuid; + } + + @FunctionalInterface + public interface UuidSupplier { + @Nullable UUID uuid() throws Exception; + } + } + + final class Everyone implements PlotMember { + + @Override + public @NonNull UUID uuid() { + return DBFunc.EVERYONE; + } + } +} diff --git a/Core/src/main/java/com/plotsquared/core/commands/parsers/PlotMemberParser.java b/Core/src/main/java/com/plotsquared/core/commands/parsers/PlotMemberParser.java new file mode 100644 index 000000000..5554aa139 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/commands/parsers/PlotMemberParser.java @@ -0,0 +1,106 @@ +package com.plotsquared.core.commands.parsers; + +import cloud.commandframework.annotations.parsers.Parser; +import cloud.commandframework.captions.CaptionVariable; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.exceptions.parsing.NoInputProvidedException; +import cloud.commandframework.exceptions.parsing.ParserException; +import com.google.inject.Inject; +import com.plotsquared.core.commands.PlotSquaredCaptionKeys; +import com.plotsquared.core.commands.arguments.PlotMember; +import com.plotsquared.core.configuration.Settings; +import com.plotsquared.core.inject.annotations.ImpromptuPipeline; +import com.plotsquared.core.player.PlotPlayer; +import com.plotsquared.core.uuid.UUIDMapping; +import com.plotsquared.core.uuid.UUIDPipeline; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.io.Serial; +import java.util.Queue; +import java.util.UUID; + +public class PlotMemberParser { + + private final UUIDPipeline uuidPipeline; + + @Inject + public PlotMemberParser(@ImpromptuPipeline final @NonNull UUIDPipeline uuidPipeline) { + this.uuidPipeline = uuidPipeline; + } + + @Parser + public @NonNull PlotMember parse( + final @NonNull CommandContext> context, + final @NonNull Queue<@NonNull String> input + ) { + final var candidate = input.peek(); + if (candidate == null) { + throw new NoInputProvidedException(this.getClass(), context); + } + + if ("*".equals(candidate)) { + return PlotMember.EVERYONE; + } else if (candidate.length() > 16) { + try { + return new PlotMember.Player(UUID.fromString(candidate)); + } catch (IllegalArgumentException ignored) { + throw new TargetParseException(candidate, context); + } + } + + if (Settings.Paper_Components.PAPER_LISTENERS) { + try { + return this.uuidPipeline.getUUID(candidate, Settings.UUID.NON_BLOCKING_TIMEOUT) + .get() + .map(UUIDMapping::getUuid) + .map(PlotMember.Player::new) + .orElseThrow(); + } catch (Exception e) { + throw new TargetParseException(candidate, context); + } + } else { + return new PlotMember.LazyPlayer( + candidate, + () -> this.uuidPipeline.getUUID(candidate, Settings.UUID.NON_BLOCKING_TIMEOUT) + .get() + .map(UUIDMapping::getUuid) + .orElse(null) + ); + } + } + + public static final class TargetParseException extends ParserException { + + @Serial + private static final long serialVersionUID = 927476591631527552L; + private final String input; + + /** + * Construct a new Player parse exception + * + * @param input String input + * @param context Command context + */ + public TargetParseException( + final @NonNull String input, + final @NonNull CommandContext context + ) { + super( + PlotMemberParser.class, + context, + PlotSquaredCaptionKeys.ARGUMENT_PARSE_FAILURE_TARGET, + CaptionVariable.of("input", input) + ); + this.input = input; + } + + /** + * Get the supplied input + * + * @return String value + */ + public @NonNull String getInput() { + return this.input; + } + } +} diff --git a/Core/src/main/java/com/plotsquared/core/commands/requirements/Requirement.java b/Core/src/main/java/com/plotsquared/core/commands/requirements/Requirement.java new file mode 100644 index 000000000..adb38d8b9 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/commands/requirements/Requirement.java @@ -0,0 +1,17 @@ +package com.plotsquared.core.commands.requirements; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Repeatable(Requirements.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Requirement { + + @NonNull RequirementType value(); +} diff --git a/Core/src/main/java/com/plotsquared/core/commands/requirements/RequirementType.java b/Core/src/main/java/com/plotsquared/core/commands/requirements/RequirementType.java new file mode 100644 index 000000000..0b4123735 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/commands/requirements/RequirementType.java @@ -0,0 +1,36 @@ +package com.plotsquared.core.commands.requirements; + +import com.plotsquared.core.configuration.caption.Caption; +import com.plotsquared.core.configuration.caption.TranslatableCaption; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +public enum RequirementType { + PLAYER(""), + IN_PLOT("errors.not_in_plot"), + PLOT_HAS_OWNER("info.plot_unowned", IN_PLOT), + IS_OWNER("permission.no_plot_perms", PLOT_HAS_OWNER); + + private final Caption caption; + private @NonNull Set<@NonNull RequirementType> inheritedRequirements; + + RequirementType( + final String caption, + final @NonNull RequirementType... inheritedRequirements + ) { + this.caption = TranslatableCaption.of(caption); + this.inheritedRequirements = EnumSet.copyOf(Arrays.asList(inheritedRequirements)); + } + + public @NonNull Set<@NonNull RequirementType> inheritedRequirements() { + return Collections.unmodifiableSet(this.inheritedRequirements); + } + + public @NonNull Caption caption() { + return this.caption; + } +} diff --git a/Core/src/main/java/com/plotsquared/core/commands/requirements/Requirements.java b/Core/src/main/java/com/plotsquared/core/commands/requirements/Requirements.java new file mode 100644 index 000000000..6a8d1fb95 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/commands/requirements/Requirements.java @@ -0,0 +1,15 @@ +package com.plotsquared.core.commands.requirements; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Requirements { + + @NonNull Requirement @NonNull[] value(); +} diff --git a/Core/src/main/java/com/plotsquared/core/uuid/UUIDPipeline.java b/Core/src/main/java/com/plotsquared/core/uuid/UUIDPipeline.java index 036eeda05..5901b4edd 100644 --- a/Core/src/main/java/com/plotsquared/core/uuid/UUIDPipeline.java +++ b/Core/src/main/java/com/plotsquared/core/uuid/UUIDPipeline.java @@ -36,6 +36,7 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -273,6 +274,25 @@ public class UUIDPipeline { return this.getUUIDs(requests).orTimeout(timeout, TimeUnit.MILLISECONDS); } + /** + * Asynchronously attempt to fetch the mapping from a name. + *

+ * This will timeout after the specified time and throws a {@link TimeoutException} + * if this happens + * + * @param username Name + * @param timeout Timeout in milliseconds + * @return Mapping + */ + public @NonNull CompletableFuture> getUUID( + final @NonNull String username, + final long timeout + ) { + return this.getUUIDs(List.of(username), timeout).thenApply( + results -> results.stream().findFirst() + ); + } + /** * Asynchronously attempt to fetch the mapping from a list of UUIDs *