diff --git a/build.gradle.kts b/build.gradle.kts index bf9db0cd..c47c4ba4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ plugins { } group = "io.github.revxrsal" -version = "4.0.0-beta.19" +version = "4.0.0-beta.25" java { toolchain { diff --git a/bukkit/src/main/java/revxrsal/commands/bukkit/BukkitVisitors.java b/bukkit/src/main/java/revxrsal/commands/bukkit/BukkitVisitors.java index d5b5719b..b5b55180 100644 --- a/bukkit/src/main/java/revxrsal/commands/bukkit/BukkitVisitors.java +++ b/bukkit/src/main/java/revxrsal/commands/bukkit/BukkitVisitors.java @@ -24,6 +24,7 @@ package revxrsal.commands.bukkit; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.World; import org.bukkit.command.CommandSender; @@ -135,6 +136,7 @@ public final class BukkitVisitors { .addParameterTypeLast(Player.class, new PlayerParameterType(brigadierEnabled)) .addParameterTypeLast(OfflinePlayer.class, new OfflinePlayerParameterType(brigadierEnabled)) .addParameterTypeLast(World.class, new WorldParameterType()) + .addParameterTypeLast(Location.class, new LocationParameterType()) .addParameterTypeFactoryLast(new EntitySelectorParameterTypeFactory()); if (BukkitVersion.isBrigadierSupported()) builder.parameterTypes() diff --git a/bukkit/src/main/java/revxrsal/commands/bukkit/brigadier/BukkitArgumentTypes.java b/bukkit/src/main/java/revxrsal/commands/bukkit/brigadier/BukkitArgumentTypes.java index 1dcea80d..89443ea3 100644 --- a/bukkit/src/main/java/revxrsal/commands/bukkit/brigadier/BukkitArgumentTypes.java +++ b/bukkit/src/main/java/revxrsal/commands/bukkit/brigadier/BukkitArgumentTypes.java @@ -24,6 +24,7 @@ package revxrsal.commands.bukkit.brigadier; import com.mojang.brigadier.arguments.ArgumentType; +import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -63,6 +64,7 @@ public final class BukkitArgumentTypes { .addTypeLast(OfflinePlayer.class, SINGLE_PLAYER) .addTypeLast(Player.class, SINGLE_PLAYER) .addTypeLast(Entity.class, SINGLE_ENTITY) + .addTypeLast(Location.class, MinecraftArgumentType.BLOCK_POS.get()) .addTypeFactoryLast((parameter) -> { if (parameter.type() != EntitySelector.class) return null; diff --git a/bukkit/src/main/java/revxrsal/commands/bukkit/brigadier/MinecraftArgumentType.java b/bukkit/src/main/java/revxrsal/commands/bukkit/brigadier/MinecraftArgumentType.java index 5ffc8765..cc4447e8 100644 --- a/bukkit/src/main/java/revxrsal/commands/bukkit/brigadier/MinecraftArgumentType.java +++ b/bukkit/src/main/java/revxrsal/commands/bukkit/brigadier/MinecraftArgumentType.java @@ -31,6 +31,7 @@ import java.lang.reflect.Constructor; import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -49,95 +50,95 @@ public enum MinecraftArgumentType { * - boolean single * - boolean playerOnly */ - ENTITY("ArgumentEntity", boolean.class, boolean.class), + ENTITY(new String[]{"ArgumentEntity", "EntityArgument"}, boolean.class, boolean.class), /** * A player, online or not. Can also use a selector, which may match one or more * players (but not entities). */ - GAME_PROFILE("ArgumentProfile"), + GAME_PROFILE("ArgumentProfile", "GameProfileArgument"), /** * A chat color. One of the names from colors, or {@code reset}. * Case-insensitive. */ - COLOR("ArgumentChatFormat"), + COLOR("ArgumentChatFormat", "ColorArgument"), /** * A JSON Chat component. */ - COMPONENT("ArgumentChatComponent"), + COMPONENT("ArgumentChatComponent", "ComponentArgument"), /** * A regular message, potentially including selectors. */ - MESSAGE("ArgumentChat"), + MESSAGE("ArgumentChat", "MessageArgument"), /** * An NBT value, parsed using JSON-NBT rules. This represents a full NBT tag. */ - NBT("ArgumentNBTTag"), + NBT("ArgumentNBTTag", "CompoundTagArgument"), /** * Represents a partial NBT tag, usable in data modify command. */ - NBT_TAG("ArgumentNBTBase"), + NBT_TAG("ArgumentNBTBase", "NbtTagArgument"), /** * A path within an NBT value, allowing for array and member accesses. */ - NBT_PATH("ArgumentNBTKey"), + NBT_PATH("ArgumentNBTKey", "NbtPathArgument"), /** * A scoreboard objective. */ - SCOREBOARD_OBJECTIVE("ArgumentScoreboardObjective"), + SCOREBOARD_OBJECTIVE("ArgumentScoreboardObjective", "ObjectiveArgument"), /** * A single score criterion. */ - OBJECTIVE_CRITERIA("ArgumentScoreboardCriteria"), + OBJECTIVE_CRITERIA("ArgumentScoreboardCriteria", "ObjectiveCriteriaArgument"), /** * A scoreboard operator. */ - SCOREBOARD_SLOT("ArgumentScoreboardSlot"), + SCOREBOARD_SLOT("ArgumentScoreboardSlot", "SlotArgument"), /** * Something that can join a team. Allows selectors and *. */ - SCORE_HOLDER("ArgumentScoreholder"), + SCORE_HOLDER("ArgumentScoreholder", "ScoreHolderArgument"), /** * The name of a team. Parsed as an unquoted string. */ - TEAM("ArgumentScoreboardTeam"), + TEAM("ArgumentScoreboardTeam", "TeamArgument"), /** * A scoreboard operator. */ - OPERATION("ArgumentMathOperation"), + OPERATION("ArgumentMathOperation", "OperationArgument"), /** * A particle effect (an identifier with extra information following it for * specific particles, mirroring the Particle packet) */ - PARTICLE("ArgumentParticle"), + PARTICLE("ArgumentParticle", "ParticleArgument"), /** * Represents an angle. */ - ANGLE("ArgumentAngle"), + ANGLE("ArgumentAngle", "AngleArgument"), /** * A name for an inventory slot. */ - ITEM_SLOT("ArgumentInventorySlot"), + ITEM_SLOT("ArgumentInventorySlot", "SlotArgument"), /** * An Identifier. */ - RESOURCE_LOCATION("ArgumentMinecraftKeyRegistered"), + RESOURCE_LOCATION("ArgumentMinecraftKeyRegistered", "ResourceLocationArgument"), /** * A potion effect. @@ -157,95 +158,95 @@ public enum MinecraftArgumentType { /** * Represents a dimension. */ - DIMENSION("ArgumentDimension"), + DIMENSION("ArgumentDimension", "DimensionArgument"), /** * Represents a time duration. */ - TIME("ArgumentTime"), + TIME("ArgumentTime", "TimeArgument"), /** * Represents a UUID value. * * @since Minecraft 1.16 */ - UUID("ArgumentUUID"), + UUID("ArgumentUUID", "UuidArgument"), /** * A location, represented as 3 numbers (which must be integers). May use relative locations * with ~ */ - BLOCK_POS("coordinates.ArgumentPosition"), + BLOCK_POS("coordinates.ArgumentPosition", "coordinates.BlockPosArgument"), /** - * A column location, represented as 3 numbers (which must be integers). May use relative locations + * A column location, represented as 2 numbers (which must be integers). May use relative locations * with ~. */ - COLUMN_POS("coordinates.ArgumentVec2I"), + COLUMN_POS("coordinates.ArgumentVec2I", "coordinates.ColumnPosArgument"), /** * A location, represented as 3 numbers (which may have a decimal point, but will be moved to the * center of a block if none is specified). May use relative locations with ~. */ - VECTOR_3("coordinates.ArgumentVec3"), + VECTOR_3("coordinates.ArgumentVec3", "coordinates.Vec3Argument"), /** * A location, represented as 2 numbers (which may have a decimal point, but will be moved to the center * of a block if none is specified). May use relative locations with ~. */ - VECTOR_2("coordinates.ArgumentVec2"), + VECTOR_2("coordinates.ArgumentVec2", "coordinates.Vec2Argument"), /** * An angle, represented as 2 numbers (which may have a decimal point, but will be moved to the * center of a block if none is specified). May use relative locations with ~. */ - ROTATION("coordinates.ArgumentRotation"), + ROTATION("coordinates.ArgumentRotation", "coordinates.RotationArgument"), /** * A collection of up to 3 axes. */ - SWIZZLE("coordinates.ArgumentRotationAxis"), + SWIZZLE("coordinates.ArgumentRotationAxis", "coordinates.SwizzleArgument"), /** * A block state, optionally including NBT and state information. */ - BLOCK_STATE("blocks.ArgumentTile"), + BLOCK_STATE("blocks.ArgumentTile", "blocks.BlockStateArgument"), /** * A block, or a block tag. */ - BLOCK_PREDICATE("blocks.ArgumentBlockPredicate"), + BLOCK_PREDICATE("blocks.ArgumentBlockPredicate", "blocks.BlockPredicateArgument"), /** * An item, optionally including NBT. */ - ITEM_STACK("item.ArgumentItemStack"), + ITEM_STACK("item.ArgumentItemStack", "item.ItemArgument"), /** * An item, or an item tag. */ - ITEM_PREDICATE("item.ArgumentItemPredicate"), + ITEM_PREDICATE("item.ArgumentItemPredicate", "item.ItemPredicateArgument"), /** * A function. */ - FUNCTION("item.ArgumentTag"), + FUNCTION("item.ArgumentTag", "item.FunctionArgument"), /** * The entity anchor related to the facing argument in the teleport command, * is feet or eyes. */ - ENTITY_ANCHOR("ArgumentAnchor"), + ENTITY_ANCHOR("ArgumentAnchor", "EntityAnchorArgument"), /** * An integer range of values with a min and a max. */ - INT_RANGE("ArgumentCriterionValue$b"), + INT_RANGE("ArgumentCriterionValue$b", "RangeArgument$Ints"), /** * A floating-point range of values with a min and a max. */ - FLOAT_RANGE("ArgumentCriterionValue$a"), + FLOAT_RANGE("ArgumentCriterionValue$a", "RangeArgument$Floats"), /** * Template mirror @@ -265,8 +266,17 @@ public enum MinecraftArgumentType { private @Nullable ArgumentType argumentType; private @Nullable Constructor argumentConstructor; - MinecraftArgumentType(String name, Class... parameters) { - Class argumentClass = resolveArgumentClass(name); + MinecraftArgumentType(String... names) { + this(names, new Class[0]); + } + + MinecraftArgumentType(String[] names, Class... parameters) { + Class argumentClass = null; + for (String name : names) { + argumentClass = resolveArgumentClass(name); + if (argumentClass != null) + break; + } this.parameters = parameters; if (argumentClass == null) { argumentType = null; @@ -288,25 +298,32 @@ public enum MinecraftArgumentType { } } + static class Data { + private static final List POSSIBLE_CLASS_NAMES = Arrays.asList( + "net.minecraft.server.{name}", + "net.minecraft.server.{version}.{name}", + "net.minecraft.commands.arguments.{name}", + "net.minecraft.server.{version}.{stripped_name}" + ); + } + private static @Nullable Class resolveArgumentClass(String name) { - try { - if (BukkitVersion.supports(1, 16)) { - try { - return BukkitVersion.findNmsClass("commands.arguments." + name); - } catch (Throwable ignored) { - return Class.forName("net.minecraft.commands.arguments." + name); - } - } else { - String stripped; - if (name.lastIndexOf('.') != -1) - stripped = name.substring(name.lastIndexOf('.') + 1); - else - stripped = name; - return BukkitVersion.findNmsClass(stripped); + String strippedName; + if (name.lastIndexOf('.') != -1) + strippedName = name.substring(name.lastIndexOf('.') + 1); + else + strippedName = name; + for (String s : Data.POSSIBLE_CLASS_NAMES) { + String className = s + .replace("{version}", BukkitVersion.version()) + .replace("{name}", name) + .replace("{stripped_name}", strippedName); + try { + return Class.forName(className); + } catch (ClassNotFoundException ignored) { } - } catch (Throwable t) { - return null; } + return null; } /** diff --git a/bukkit/src/main/java/revxrsal/commands/bukkit/exception/BukkitExceptionHandler.java b/bukkit/src/main/java/revxrsal/commands/bukkit/exception/BukkitExceptionHandler.java index 686034a8..cf2bfc84 100644 --- a/bukkit/src/main/java/revxrsal/commands/bukkit/exception/BukkitExceptionHandler.java +++ b/bukkit/src/main/java/revxrsal/commands/bukkit/exception/BukkitExceptionHandler.java @@ -19,6 +19,11 @@ public void onInvalidWorld(InvalidWorldException e, BukkitCommandActor actor) { actor.error(legacyColorize("&cInvalid world: &e" + e.input() + "&c.")); } + @HandleException + public void onInvalidWorld(MissingLocationParameterException e, BukkitCommandActor actor) { + actor.error(legacyColorize("&cExpected &e" + e.axis().name().toLowerCase() + "&c.")); + } + @HandleException public void onSenderNotConsole(SenderNotConsoleException e, BukkitCommandActor actor) { actor.error(legacyColorize("&cYou must be the console to execute this command!")); diff --git a/bukkit/src/main/java/revxrsal/commands/bukkit/exception/MissingLocationParameterException.java b/bukkit/src/main/java/revxrsal/commands/bukkit/exception/MissingLocationParameterException.java new file mode 100644 index 00000000..a34a368a --- /dev/null +++ b/bukkit/src/main/java/revxrsal/commands/bukkit/exception/MissingLocationParameterException.java @@ -0,0 +1,26 @@ +package revxrsal.commands.bukkit.exception; + +import org.jetbrains.annotations.NotNull; +import revxrsal.commands.exception.InvalidValueException; + +/** + * Thrown when a parameter of {@link org.bukkit.Location} is not specified, such + * as {@code x} or {@code y} or {@code z}. + */ +public class MissingLocationParameterException extends InvalidValueException { + + public enum MissingAxis { + X, Y, Z + } + + private final @NotNull MissingAxis missingAxis; + + public MissingLocationParameterException(@NotNull String input, @NotNull MissingAxis missingAxis) { + super(input); + this.missingAxis = missingAxis; + } + + public @NotNull MissingAxis axis() { + return missingAxis; + } +} diff --git a/bukkit/src/main/java/revxrsal/commands/bukkit/parameters/LocationParameterType.java b/bukkit/src/main/java/revxrsal/commands/bukkit/parameters/LocationParameterType.java new file mode 100644 index 00000000..acf446cf --- /dev/null +++ b/bukkit/src/main/java/revxrsal/commands/bukkit/parameters/LocationParameterType.java @@ -0,0 +1,142 @@ +package revxrsal.commands.bukkit.parameters; + +import com.google.common.base.Preconditions; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; +import revxrsal.commands.bukkit.actor.BukkitCommandActor; +import revxrsal.commands.bukkit.exception.MissingLocationParameterException; +import revxrsal.commands.bukkit.exception.MissingLocationParameterException.MissingAxis; +import revxrsal.commands.exception.CommandErrorException; +import revxrsal.commands.node.ExecutionContext; +import revxrsal.commands.parameter.ParameterType; +import revxrsal.commands.stream.MutableStringStream; +import revxrsal.commands.util.Lazy; + +import java.util.function.DoubleSupplier; +import java.util.function.Supplier; + +/** + * A {@link ParameterType} that parses {@link Location} types + *

+ * Credits to @SkytAsul + */ +public final class LocationParameterType implements ParameterType { + + @Override + public Location parse(@NotNull MutableStringStream input, @NotNull ExecutionContext context) { + if (input.peek() == '^') + return parseLocal(input, context.actor()); + else + return parseWorld(input, context.actor()); + } + + private void consumeSpace(@NotNull MutableStringStream input, @NotNull MissingAxis missingAxis) { + if (input.hasFinished()) + throw new MissingLocationParameterException(input.peekString(), missingAxis); + if (input.peek() == ' ') + input.moveForward(); + } + + // absolute or tilde notation ~ + private Location parseWorld(@NotNull MutableStringStream input, @NotNull BukkitCommandActor actor) { + Supplier actorLocation = Lazy.of(() -> actor.requirePlayer().getLocation()); + double x; + double y; + double z; + + x = readWorldCoordinate(input, () -> actorLocation.get().getX()); + + consumeSpace(input, MissingAxis.Y); + y = readWorldCoordinate(input, () -> actorLocation.get().getY()); + + consumeSpace(input, MissingAxis.Z); + z = readWorldCoordinate(input, () -> actorLocation.get().getZ()); + + World world = actor.isPlayer() ? actorLocation.get().getWorld() : Bukkit.getWorld("world"); + return new Location(world, x, y, z); + } + + private double readWorldCoordinate(@NotNull MutableStringStream input, DoubleSupplier relativeToSupplier) { + if (input.peek() == '~') { + double relativeTo = relativeToSupplier.getAsDouble(); + input.moveForward(); + if (!input.hasFinished() && !Character.isWhitespace(input.peek())) + relativeTo += input.readDouble(); + return relativeTo; + } else + return input.readDouble(); + } + + // caret notation ^ + private Location parseLocal(@NotNull MutableStringStream input, @NotNull BukkitCommandActor actor) { + Location actorLocation = actor.requirePlayer().getLocation(); + double x = readLocalCoordinate(input); + + consumeSpace(input, MissingAxis.Y); + double y = readLocalCoordinate(input); + + consumeSpace(input, MissingAxis.Z); + double z = readLocalCoordinate(input); + + Vector vector = getLocal(actorLocation, new Vector(x, y, z)); + return new Location(actorLocation.getWorld(), vector.getX(), vector.getY(), vector.getZ()); + } + + private double readLocalCoordinate(@NotNull MutableStringStream input) { + if (input.read() != '^') + throw new CommandErrorException("Expected '^'."); + if (input.hasFinished() || Character.isWhitespace(input.peek())) + return 0; + return input.readDouble(); + } + + // math from https://www.spigotmc.org/threads/local-coordinates.529011/#post-4280379 + private Vector getLocal(Location reference, Vector local) { + // Firstly a vector facing YAW = 0, on the XZ plane as start base + Vector axisBase = new Vector(0, 0, 1); + // This one pointing YAW + 90° should be the relative "left" of the field of view, isn't it (since + // ROLL always is 0°)? + + Vector axisLeft = rotateAroundY(axisBase.clone(), Math.toRadians(-reference.getYaw() + 90.0f)); + // Left axis should be the rotation axis for going up, too, since it's perpendicular... + Vector axisUp = rotateAroundNonUnitAxis(reference.getDirection().clone(), axisLeft, Math.toRadians(-90f)); + + // Based on these directions, we got all we need + Vector sway = axisLeft.clone().normalize().multiply(local.getX()); + Vector heave = axisUp.clone().normalize().multiply(local.getY()); + Vector surge = reference.getDirection().clone().multiply(local.getZ()); + + // Add up the global reference based result + return new Vector(reference.getX(), reference.getY(), reference.getZ()).add(sway).add(heave).add(surge); + } + + @NotNull + private Vector rotateAroundY(Vector vector, double angle) { + double angleCos = Math.cos(angle); + double angleSin = Math.sin(angle); + double x = angleCos * vector.getX() + angleSin * vector.getZ(); + double z = -angleSin * vector.getX() + angleCos * vector.getZ(); + return vector.setX(x).setZ(z); + } + + @NotNull + private Vector rotateAroundNonUnitAxis(@NotNull Vector vector, @NotNull Vector axis, double angle) throws IllegalArgumentException { + Preconditions.checkArgument(axis != null, "The provided axis vector was null"); + double x = vector.getX(); + double y = vector.getY(); + double z = vector.getZ(); + double x2 = axis.getX(); + double y2 = axis.getY(); + double z2 = axis.getZ(); + double cosTheta = Math.cos(angle); + double sinTheta = Math.sin(angle); + double dotProduct = vector.dot(axis); + double xPrime = x2 * dotProduct * ((double) 1.0F - cosTheta) + x * cosTheta + (-z2 * y + y2 * z) * sinTheta; + double yPrime = y2 * dotProduct * ((double) 1.0F - cosTheta) + y * cosTheta + (z2 * x - x2 * z) * sinTheta; + double zPrime = z2 * dotProduct * ((double) 1.0F - cosTheta) + z * cosTheta + (-y2 * x + x2 * y) * sinTheta; + return vector.setX(xPrime).setY(yPrime).setZ(zPrime); + } +} diff --git a/bukkit/src/main/java/revxrsal/commands/bukkit/util/BukkitVersion.java b/bukkit/src/main/java/revxrsal/commands/bukkit/util/BukkitVersion.java index d57623b4..dc88b571 100644 --- a/bukkit/src/main/java/revxrsal/commands/bukkit/util/BukkitVersion.java +++ b/bukkit/src/main/java/revxrsal/commands/bukkit/util/BukkitVersion.java @@ -22,7 +22,7 @@ public final class BukkitVersion { private static final int MAJOR_VERSION, MINOR_VERSION, PATCH_NUMBER; /** - * The current version string, for example 1_17_R1 + * The current version string, for example v1_17_R1 */ private static final String VERSION = fetchVersion(); @@ -120,6 +120,15 @@ public static int patchNumber() { return PATCH_NUMBER; } + /** + * Returns the current version string, for example v1_17_R1 + * + * @return The current version string + */ + public static @NotNull String version() { + return VERSION; + } + /** * Returns the NMS class with the given name. The name must not contain * the net.minecraft.server prefix. diff --git a/common/src/main/java/revxrsal/commands/Lamp.java b/common/src/main/java/revxrsal/commands/Lamp.java index 8c3d58e8..689d0b1b 100644 --- a/common/src/main/java/revxrsal/commands/Lamp.java +++ b/common/src/main/java/revxrsal/commands/Lamp.java @@ -303,9 +303,9 @@ public SuggestionProvider findNextSuggestionProvider(Type type, AnnotationLis commandClass = registry.handler().getClass(); instance = registry.handler(); registered.addAll(tree.register(commandClass, instance, registry.paths())); + } else { + registered.addAll(tree.register(commandClass, instance)); } - - registered.addAll(tree.register(commandClass, instance)); } return registered; } @@ -612,6 +612,8 @@ public Builder() { responseHandler(CompletionStageResponseHandler.INSTANCE); responseHandler(OptionalResponseHandler.INSTANCE); commandCondition(PermissionConditionChecker.INSTANCE); + commandCondition(CooldownCondition.INSTANCE); + hooks().onPostCommandExecuted(CooldownCondition.INSTANCE); accept(KotlinFeatureRegistry.INSTANCE); } diff --git a/common/src/main/java/revxrsal/commands/annotation/CommandPlaceholder.java b/common/src/main/java/revxrsal/commands/annotation/CommandPlaceholder.java index da5c11cf..52289307 100644 --- a/common/src/main/java/revxrsal/commands/annotation/CommandPlaceholder.java +++ b/common/src/main/java/revxrsal/commands/annotation/CommandPlaceholder.java @@ -31,7 +31,7 @@ import java.lang.annotation.Target; /** - * An annotation that takes the place of a {@link @Command} of a {@link OrphanCommand}. + * An annotation that takes the place of a {@link Command @Command} of a {@link OrphanCommand}. *

* If a method has {@link Subcommand @Subcommand} on it, this annotation is unnecessary. * It simply exists in cases where the orphan command is the function itself, and diff --git a/common/src/main/java/revxrsal/commands/annotation/Cooldown.java b/common/src/main/java/revxrsal/commands/annotation/Cooldown.java new file mode 100644 index 00000000..cf2fb19a --- /dev/null +++ b/common/src/main/java/revxrsal/commands/annotation/Cooldown.java @@ -0,0 +1,54 @@ +/* + * This file is part of sweeper, licensed under the MIT License. + * + * Copyright (c) Revxrsal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package revxrsal.commands.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * Adds a cooldown to the command. + */ +@DistributeOnMethods +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface Cooldown { + + /** + * The cooldown value + * + * @return The command cooldown value + */ + long value(); + + /** + * The time unit in which the cooldown goes for. + * + * @return The time unit for the cooldown + */ + TimeUnit unit() default TimeUnit.SECONDS; + +} diff --git a/common/src/main/java/revxrsal/commands/autocomplete/SingleCommandCompleter.java b/common/src/main/java/revxrsal/commands/autocomplete/SingleCommandCompleter.java new file mode 100644 index 00000000..517658e9 --- /dev/null +++ b/common/src/main/java/revxrsal/commands/autocomplete/SingleCommandCompleter.java @@ -0,0 +1,283 @@ +package revxrsal.commands.autocomplete; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import revxrsal.commands.command.CommandActor; +import revxrsal.commands.command.ExecutableCommand; +import revxrsal.commands.node.*; +import revxrsal.commands.stream.MutableStringStream; + +import java.util.*; + +import static revxrsal.commands.node.DispatcherSettings.LONG_FORMAT_PREFIX; +import static revxrsal.commands.node.DispatcherSettings.SHORT_FORMAT_PREFIX; + +/** + * Auto-completer for individual {@link ExecutableCommand ExecutableCommands} + * + * @param Actor type + */ +final class SingleCommandCompleter { + + private final ExecutableCommand command; + private final MutableStringStream input; + private final MutableExecutionContext context; + + private final List suggestions = new ArrayList<>(); + + private int positionBeforeParsing = -1; + + public SingleCommandCompleter(A actor, ExecutableCommand command, MutableStringStream input) { + this.command = command; + this.input = input; + this.context = ExecutionContext.createMutable(command, actor, input.toImmutableView()); + } + + private void rememberPosition() { + if (positionBeforeParsing != -1) + throw new IllegalArgumentException("You already have a position remembered that you did not consume."); + positionBeforeParsing = input.position(); + } + + private String restorePosition() { + if (positionBeforeParsing == -1) + throw new IllegalArgumentException("You forgot to call rememberPosition() when trying to restore position."); + int positionAfterParsing = input.position(); + input.setPosition(positionBeforeParsing); + positionBeforeParsing = -1; + return input.peek(positionAfterParsing - positionBeforeParsing); + } + + public void complete() { + Map> remainingFlags = null; + for (CommandNode node : command.nodes()) { + if (node.isLiteral()) { + CompletionResult result = completeLiteral(node.requireLiteralNode()); + if (result == CompletionResult.HALT) + break; + } else { + ParameterNode parameter = node.requireParameterNode(); + if (parameter.isFlag() || parameter.isSwitch()) { + (remainingFlags == null ? remainingFlags = new HashMap<>() : remainingFlags) + .put(universalFlagName(parameter), parameter); + continue; + } + CompletionResult result = completeParameter(parameter); + if (result == CompletionResult.HALT) + break; + } + } + if (!command.containsFlags() || remainingFlags == null) + return; + completeFlags(remainingFlags); + } + + private CompletionResult completeParameter(@NotNull ParameterNode parameter) { + rememberPosition(); + if (parameter.isSwitch()) { + context.addResolvedArgument(parameter.name(), true); + return CompletionResult.CONTINUE; + } + try { + Object value = parameter.parse(input, context); + context.addResolvedArgument(parameter.name(), value); + int positionAfterParsing = input.position(); + String consumed = restorePosition(); + Collection parameterSuggestions = parameter.complete(context); + input.setPosition(positionAfterParsing); // restore so that we can move forward + + if (input.hasFinished()) { + filterSuggestions(consumed, parameterSuggestions); + return CompletionResult.HALT; + } + if (input.peek() == ' ') + input.skipWhitespace(); + return CompletionResult.CONTINUE; + } catch (Throwable t) { + String consumed = restorePosition(); + filterSuggestions(consumed, parameter.complete(context)); + return CompletionResult.HALT; + } + } + + @Contract(mutates = "param1") + private void completeFlags(@NotNull Map> remainingFlags) { + boolean lastWasShort = false; + while (input.hasRemaining()) { + if (input.peek() == ' ') + input.skipWhitespace(); + String next = input.peekUnquotedString(); + if (next.startsWith("--")) { + lastWasShort = false; + String flagName = next.substring(LONG_FORMAT_PREFIX.length()); + ParameterNode targetFlag = remainingFlags.remove(flagName); + if (targetFlag == null) { + for (ParameterNode value : remainingFlags.values()) { + if (universalFlagName(value).startsWith(flagName)) + suggestions.add(LONG_FORMAT_PREFIX + universalFlagName(value)); + } + return; + } + input.readUnquotedString(); // consumes the flag name + if (input.hasFinished()) + return; + if (input.remaining() == 1 && input.peek() == ' ') { + Collection parameterSuggestions = targetFlag.complete(context); + suggestions.addAll(parameterSuggestions); + return; + } + input.skipWhitespace(); + CompletionResult result = completeParameter(targetFlag); + if (result == CompletionResult.HALT) { + return; + } else if (input.hasRemaining() && input.peek() == ' ') { + input.skipWhitespace(); + } + } else if (next.startsWith("-")) { + lastWasShort = true; + String shortenedString = next.substring(SHORT_FORMAT_PREFIX.length()); + char[] spec = shortenedString.toCharArray(); + input.moveForward(SHORT_FORMAT_PREFIX.length()); + for (char flag : spec) { + input.moveForward(); + @Nullable ParameterNode targetFlag = removeParameterWithShorthand(remainingFlags, flag); + if (targetFlag == null) + continue; + if (targetFlag.isSwitch()) { + context.addResolvedArgument(targetFlag.name(), true); + } + if (input.hasFinished()) { + if (targetFlag.isFlag()) + return; + for (ParameterNode remFlag : remainingFlags.values()) { + if (remFlag.shorthand() != null) { + String flagCompletion = SHORT_FORMAT_PREFIX + shortenedString + remFlag.shorthand(); + suggestions.add(remFlag.isFlag() ? flagCompletion + ' ' : flagCompletion); + } + } + return; + } + if (targetFlag.isSwitch()) + continue; + if (input.remaining() == 1 && input.peek() == ' ') { + Collection parameterSuggestions = targetFlag.complete(context); + suggestions.addAll(parameterSuggestions); + return; + } + if (input.hasRemaining() && input.peek() == ' ') + input.skipWhitespace(); + CompletionResult result = completeParameter(targetFlag); + if (result == CompletionResult.HALT) { + return; + } + + } + } + } + for (ParameterNode c : remainingFlags.values()) { + if (lastWasShort) + suggestions.add(SHORT_FORMAT_PREFIX + c.shorthand()); + else + suggestions.add(LONG_FORMAT_PREFIX + (c.isSwitch() ? c.switchName() : c.flagName())); + } + } + + private @Nullable ParameterNode removeParameterWithShorthand( + Map> parametersLeft, + char c + ) { + for (Iterator>> iterator = parametersLeft.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry> entry = iterator.next(); + Character shorthand = entry.getValue().shorthand(); + if (shorthand != null && shorthand == c) { + iterator.remove(); + return entry.getValue(); + } + } + return null; + } + + private CompletionResult completeLiteral(@NotNull LiteralNode node) { + String nextWord = input.readUnquotedString(); + if (input.hasFinished()) { + if (node.name().startsWith(nextWord)) { + // complete it for the user :) + suggestions.add(node.name()); + } + return CompletionResult.HALT; + } + if (!node.name().equalsIgnoreCase(nextWord)) { + // the user inputted a command that isn't ours. dismiss the operation + return CompletionResult.HALT; + } + if (input.hasRemaining() && input.peek() == ' ') { + // our literal is just fine. move to the next node + input.skipWhitespace(); + return CompletionResult.CONTINUE; + } + return CompletionResult.HALT; + } + + private void filterSuggestions(String consumed, @NotNull Collection parameterSuggestions) { + for (String parameterSuggestion : parameterSuggestions) { + if (parameterSuggestion.toLowerCase().startsWith(consumed.toLowerCase())) { + suggestions.add(getRemainingContent(parameterSuggestion, consumed)); + } + } + } + + private String universalFlagName(@NotNull ParameterNode parameter) { + if (parameter.isSwitch()) + return parameter.switchName(); + if (parameter.isFlag()) + return parameter.flagName(); + return parameter.name(); + } + + public @NotNull List suggestions() { + return suggestions; + } + + /** + * Represents the result of the completion of a {@link CommandNode} + */ + private enum CompletionResult { + + /** + * Halt the completion and don't return anything. This is sent when: + *

+ */ + HALT, + + /** + * Continue moving through the command nodes. This is sent when + * all previous nodes have been successfully parsed, and the input + * has been valid until now. + */ + CONTINUE + } + + private static String getRemainingContent(String suggestion, String consumed) { + // Find the index where they match until + int matchIndex = consumed.length(); + + // Find the first space after the matching part + int spaceIndex = suggestion.lastIndexOf(' ', matchIndex - 1); + + // Return the content after the first space + return suggestion.substring(spaceIndex + 1); + } + +} diff --git a/common/src/main/java/revxrsal/commands/autocomplete/StandardAutoCompleter.java b/common/src/main/java/revxrsal/commands/autocomplete/StandardAutoCompleter.java index 1cd5c076..b28725fd 100644 --- a/common/src/main/java/revxrsal/commands/autocomplete/StandardAutoCompleter.java +++ b/common/src/main/java/revxrsal/commands/autocomplete/StandardAutoCompleter.java @@ -24,20 +24,13 @@ package revxrsal.commands.autocomplete; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import revxrsal.commands.Lamp; import revxrsal.commands.command.CommandActor; import revxrsal.commands.command.ExecutableCommand; -import revxrsal.commands.node.*; import revxrsal.commands.stream.MutableStringStream; import revxrsal.commands.stream.StringStream; import java.util.*; -import java.util.stream.Collectors; - -import static revxrsal.commands.node.DispatcherSettings.LONG_FORMAT_PREFIX; -import static revxrsal.commands.node.DispatcherSettings.SHORT_FORMAT_PREFIX; -import static revxrsal.commands.util.Collections.*; /** * A basic implementation of {@link AutoCompleter} that respects secret @@ -55,29 +48,6 @@ public StandardAutoCompleter(Lamp lamp) { this.lamp = lamp; } - private static @NotNull List filterWithSpaces(Collection suggestions, String consumed) { - return suggestions - .stream() - .filter(suggestion -> startsWithIgnoreCase(suggestion, consumed)) - .map(s -> getRemainingContent(s, consumed)) - .collect(Collectors.toList()); - } - - private static boolean startsWithIgnoreCase(String a, String b) { - return a.toLowerCase().startsWith(b.toLowerCase()); - } - - public static String getRemainingContent(String suggestion, String consumed) { - // Find the index where they match until - int matchIndex = consumed.length(); - - // Find the first space after the matching part - int spaceIndex = suggestion.lastIndexOf(' ', matchIndex - 1); - - // Return the content after the first space - return suggestion.substring(spaceIndex + 1); - } - @Override public @NotNull List complete(@NotNull A actor, @NotNull String input) { return complete(actor, StringStream.create(input)); @@ -104,193 +74,8 @@ public static String getRemainingContent(String suggestion, String consumed) { } private List complete(ExecutableCommand possible, MutableStringStream input, A actor) { - MutableExecutionContext context = ExecutionContext.createMutable(possible, actor, input.toImmutableCopy()); - for (CommandNode child : possible.nodes()) { - if (child instanceof ParameterNode) { - ParameterNode parameter = (ParameterNode) child; - if (parameter.isFlag() || parameter.isSwitch()) - break; - } - if (input.remaining() == 1 && input.peek() == ' ') { - input.skipWhitespace(); - return promptWith(child, actor, context, input); - } - - if (child instanceof LiteralNode) { - LiteralNode l = (LiteralNode) child; - String nextWord = input.readUnquotedString(); - if (input.hasFinished()) { - if (l.name().startsWith(nextWord)) { - // complete it for the user :) - return Arrays.asList(l.name()); - } else { - // the user inputted a command that isn't ours. dismiss the operation - return Arrays.asList(); - } - } else { - if (!l.name().equalsIgnoreCase(nextWord)) { - // the user inputted a command that isn't ours. dismiss the operation - return Arrays.asList(); - } - if (input.canRead(1) && input.peek() == ' ') { - // our literal is just fine. move to the next node - input.skipWhitespace(); - continue; - } - } - } else if (child instanceof ParameterNode) { - ParameterNode parameter = (ParameterNode) child; - int posBeforeParsing = input.position(); - if (!parameter.permission().isExecutableBy(actor)) - return Arrays.asList(); - try { - Object value = parameter.parse(input, context); - context.addResolvedArgument(parameter.name(), value); - if (input.hasFinished()) { - input.setPosition(posBeforeParsing); - String consumed = input.peekRemaining(); - // user inputted something valid, but we still have some - // suggestions. throw it at them - if (consumed.contains(" ")) { - return filterWithSpaces(parameter.complete(actor, input, context), consumed); - } - return filter(parameter.complete(actor, input, context), s -> startsWithIgnoreCase(s, consumed)); - } else if (input.peek() == ' ') { - input.skipWhitespace(); - } - } catch (Throwable e) { - // user inputted invalid input. what do we do here? - // 1. restore the stream to its previous state - // 2. see what we consumed - // 2.1. if suggestion does not contain spaces, we're cool. just - // give the same suggestions - // 2.2. if suggestion does contain spaces, pick the part after - // the space - int finishedAt = input.position(); - input.setPosition(posBeforeParsing); - String consumed = input.peek(finishedAt - posBeforeParsing); - if (consumed.contains(" ")) { - return filterWithSpaces(parameter.complete(actor, input, context), consumed); - } - if (input.canRead(consumed.length() + 1) && input.peekOffset(consumed.length()) == ' ') { - input.read(consumed.length()); - continue; - } - return filter(parameter.complete(actor, input, context), s -> startsWithIgnoreCase(s, consumed)); - } - } - } - List> flags = filter(possible.parameters().values(), c -> c.isFlag() || c.isSwitch()); - while (input.hasRemaining()) { - if (input.peek() == ' ') - input.skipWhitespace(); - String next = input.peekUnquotedString(); - if (next.startsWith(LONG_FORMAT_PREFIX)) { - String flagName = next.substring(LONG_FORMAT_PREFIX.length()); - @Nullable ParameterNode parameter = removeParameterNamed(flags, flagName); - input.readUnquotedString(); - if (input.hasFinished()) - return Arrays.asList(); - if (input.hasRemaining() && input.peek() == ' ') { - input.skipWhitespace(); - } - if (input.hasFinished() && parameter != null) { - return copyList(parameter.suggestions().getSuggestions(context)); - } else { - if (parameter != null) { - tryParseFlag(parameter, input, context); - if (input.hasFinished() || input.peek() != ' ') { - return Arrays.asList(); - } - } - continue; - } - } else if (next.startsWith(SHORT_FORMAT_PREFIX)) { - String shortenedString = next.substring(SHORT_FORMAT_PREFIX.length()); - char[] spec = shortenedString.toCharArray(); - input.readUnquotedString(); - for (char flag : spec) { - @Nullable ParameterNode parameter = removeParameterWithShorthand(flags, flag); - if (parameter == null) - continue; - tryParseFlag(parameter, input, context); - if (input.hasRemaining() && input.peek() == ' ') { - input.skipWhitespace(); - return copyList(parameter.suggestions().getSuggestions(context)); - } else if (input.hasFinished()) { - return flags.stream().map(f -> { - if (f.shorthand() != null) { - String result = SHORT_FORMAT_PREFIX + shortenedString + f.shorthand(); - return f.isFlag() ? result + ' ' : result; - } - return null; - }).filter(Objects::nonNull) - .collect(Collectors.toList()); - } - } - } else { - break; - } - } - return map(flags, c -> LONG_FORMAT_PREFIX + (c.isSwitch() ? c.switchName() : c.flagName())); + SingleCommandCompleter commandCompleter = new SingleCommandCompleter<>(actor, possible, input); + commandCompleter.complete(); + return commandCompleter.suggestions(); } - - private void tryParseFlag(@NotNull ParameterNode parameter, MutableStringStream input, MutableExecutionContext context) { - if (parameter.isSwitch()) { - context.addResolvedArgument(parameter.name(), true); - } else { - try { - input.skipWhitespace(); - if (parameter.isSwitch()) { - context.addResolvedArgument(parameter.name(), true); - return; - } - Object value = parameter.parse(input, context); - context.addResolvedArgument(parameter.name(), value); - } catch (Throwable ignored) { - input.readUnquotedString(); - } - } - } - - private @NotNull List promptWith(CommandNode child, A - actor, ExecutionContext context, StringStream input) { - if (child instanceof LiteralNode) { - LiteralNode l = (LiteralNode) child; - return Arrays.asList(l.name()); - } else if (child instanceof ParameterNode) { - ParameterNode p = (ParameterNode) child; - return copyList(p.complete(actor, input, context)); - } else - return Arrays.asList(); - } - - private @Nullable ParameterNode removeParameterWithShorthand(List> parametersLeft, char c) { - for (Iterator> iterator = parametersLeft.iterator(); iterator.hasNext(); ) { - ParameterNode value = iterator.next(); - Character shorthand = value.shorthand(); - if (shorthand != null && shorthand == c) { - iterator.remove(); - return value; - } - } - return null; - } - - private @Nullable ParameterNode removeParameterNamed - (List> parametersLeft, String name) { - for (Iterator> iterator = parametersLeft.iterator(); iterator.hasNext(); ) { - ParameterNode value = iterator.next(); - if (value.isFlag() && Objects.equals(value.flagName(), name)) { - iterator.remove(); - return value; - } - if (value.isSwitch() && Objects.equals(value.switchName(), name)) { - iterator.remove(); - return value; - } - } - return null; - } - } diff --git a/common/src/main/java/revxrsal/commands/command/CooldownCondition.java b/common/src/main/java/revxrsal/commands/command/CooldownCondition.java new file mode 100644 index 00000000..25b49a03 --- /dev/null +++ b/common/src/main/java/revxrsal/commands/command/CooldownCondition.java @@ -0,0 +1,70 @@ +/* + * This file is part of lamp, licensed under the MIT License. + * + * Copysecond (c) Revxrsal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the seconds + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copysecond notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package revxrsal.commands.command; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import revxrsal.commands.annotation.Cooldown; +import revxrsal.commands.exception.CooldownException; +import revxrsal.commands.hook.PostCommandExecutedHook; +import revxrsal.commands.node.ExecutionContext; +import revxrsal.commands.process.CommandCondition; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@ApiStatus.Internal +public enum CooldownCondition implements CommandCondition, PostCommandExecutedHook { + + INSTANCE; + + private static final ScheduledExecutorService COOLDOWN_POOL = Executors.newSingleThreadScheduledExecutor(); + private final Map> cooldowns = new ConcurrentHashMap<>(); + + @Override + public void onPostExecuted(@NotNull ExecutableCommand command, @NotNull ExecutionContext context) { + Cooldown cooldown = command.annotations().get(Cooldown.class); + if (cooldown == null || cooldown.value() == 0) return; + Map spans = cooldowns.computeIfAbsent(context.actor().uniqueId(), u -> new ConcurrentHashMap<>()); + spans.put(command.hashCode(), System.currentTimeMillis()); + COOLDOWN_POOL.schedule(() -> spans.remove(command.hashCode()), cooldown.value(), cooldown.unit()); + } + + @Override public void test(@NotNull ExecutionContext context) { + Cooldown cooldown = context.command().annotations().get(Cooldown.class); + if (cooldown == null || cooldown.value() == 0) return; + UUID uuid = context.actor().uniqueId(); + Map spans = cooldowns.get(uuid); + if (spans == null) return; + long created = spans.get(context.command().hashCode()); + long passed = System.currentTimeMillis() - created; + long left = cooldown.unit().toMillis(cooldown.value()) - passed; + if (left > 0 && left < 1000) + left = 1000L; // for formatting + throw new CooldownException(left); + } +} diff --git a/common/src/main/java/revxrsal/commands/exception/CooldownException.java b/common/src/main/java/revxrsal/commands/exception/CooldownException.java new file mode 100644 index 00000000..b3470837 --- /dev/null +++ b/common/src/main/java/revxrsal/commands/exception/CooldownException.java @@ -0,0 +1,86 @@ +/* + * This file is part of lamp, licensed under the MIT License. + * + * Copysecond (c) Revxrsal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the seconds + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copysecond notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package revxrsal.commands.exception; + +import org.jetbrains.annotations.NotNull; +import revxrsal.commands.annotation.Cooldown; +import revxrsal.commands.command.CommandActor; + +import java.util.concurrent.TimeUnit; + +import static revxrsal.commands.util.Preconditions.notNull; + +/** + * Thrown when the {@link CommandActor} has to wait before executing a + * command again. This is set by {@link Cooldown}. + */ +@ThrowableFromCommand +public class CooldownException extends RuntimeException { + + /** + * The time left (in milliseconds) + */ + private final long timeLeft; + + /** + * Creates a new {@link CooldownException} with the given timestamp + * in milliseconds + * + * @param timeLeft The time left in milliseconds + */ + public CooldownException(long timeLeft) { + this.timeLeft = timeLeft; + } + + /** + * Creates a new {@link CooldownException} with the given timestamp + * in any unit + * + * @param unit The time unit in which the time left is given + * @param timeLeft The time left in the given unit + */ + public CooldownException(TimeUnit unit, long timeLeft) { + this.timeLeft = unit.toMillis(timeLeft); + } + + /** + * Returns the time left before being able to execute again + * + * @return Time left in milliseconds + */ + public long getTimeLeftMillis() { + return timeLeft; + } + + /** + * Returns the time left in the given unit + * + * @param unit Unit to convert to + * @return The time left + */ + public long getTimeLeft(@NotNull TimeUnit unit) { + notNull(unit, "unit"); + return unit.convert(timeLeft, TimeUnit.MILLISECONDS); + } +} diff --git a/common/src/main/java/revxrsal/commands/exception/DefaultExceptionHandler.java b/common/src/main/java/revxrsal/commands/exception/DefaultExceptionHandler.java index 12cc3b73..3d52c140 100644 --- a/common/src/main/java/revxrsal/commands/exception/DefaultExceptionHandler.java +++ b/common/src/main/java/revxrsal/commands/exception/DefaultExceptionHandler.java @@ -27,6 +27,11 @@ import revxrsal.commands.command.CommandActor; import revxrsal.commands.node.ParameterNode; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + public class DefaultExceptionHandler extends RuntimeExceptionAdapter { @HandleException @@ -135,9 +140,49 @@ public void onUnknownParameter(@NotNull UnknownParameterException e, @NotNull A actor.error("Unknown flag: " + e.name()); } + @HandleException + public void onCooldown(@NotNull CooldownException e, @NotNull A actor) { + actor.error("You must wait " + formatTimeFancy(e.getTimeLeftMillis()) + " before using this command again."); + } + @HandleException public void onSendable(@NotNull SendableException e, @NotNull A actor) { e.sendTo(actor); } + public static String formatTimeFancy(long time) { + Duration d = Duration.ofMillis(time); + long hours = d.toHours(); + long minutes = d.minusHours(hours).getSeconds() / 60; + long seconds = d.minusMinutes(minutes).minusHours(hours).getSeconds(); + List words = new ArrayList<>(); + if (hours != 0) + words.add(hours + plural(hours, " hour")); + if (minutes != 0) + words.add(minutes + plural(minutes, " minute")); + if (seconds != 0) + words.add(seconds + plural(seconds, " second")); + return toFancyString(words); + } + + public static String toFancyString(List list) { + StringJoiner builder = new StringJoiner(", "); + if (list.isEmpty()) return ""; + if (list.size() == 1) return list.get(0).toString(); + for (int i = 0; i < list.size(); i++) { + T el = list.get(i); + if (i + 1 == list.size()) + return builder + " and " + el.toString(); + else + builder.add(el.toString()); + } + return builder.toString(); + } + + public static String plural(Number count, String thing) { + if (count.intValue() == 1) return thing; + if (thing.endsWith("y")) + return thing.substring(0, thing.length() - 1) + "ies"; + return thing + "s"; + } } diff --git a/common/src/main/java/revxrsal/commands/hook/CommandExecutedHook.java b/common/src/main/java/revxrsal/commands/hook/CommandExecutedHook.java index 069c5a17..aff9f87b 100644 --- a/common/src/main/java/revxrsal/commands/hook/CommandExecutedHook.java +++ b/common/src/main/java/revxrsal/commands/hook/CommandExecutedHook.java @@ -29,7 +29,7 @@ import revxrsal.commands.node.ExecutionContext; /** - * A hook that gets called when a command is executed + * A hook that gets called when a command is about to be executed */ @FunctionalInterface public interface CommandExecutedHook extends Hook { diff --git a/common/src/main/java/revxrsal/commands/hook/Hooks.java b/common/src/main/java/revxrsal/commands/hook/Hooks.java index 4d4b42af..5306f040 100644 --- a/common/src/main/java/revxrsal/commands/hook/Hooks.java +++ b/common/src/main/java/revxrsal/commands/hook/Hooks.java @@ -137,6 +137,23 @@ public boolean onCommandExecuted(@NotNull ExecutableCommand command, @NotNull return !cancelHandle.wasCancelled(); } + /** + * Calls all {@link PostCommandExecutedHook post-execution hooks}. + * + * @param command The command that was executed + * @param context The execution context + */ + @ApiStatus.Internal + @SuppressWarnings({"rawtypes", "unchecked"}) + public void onPostCommandExecuted(@NotNull ExecutableCommand command, @NotNull ExecutionContext context) { + for (Hook hook : hooks) { + if (hook instanceof PostCommandExecutedHook) { + PostCommandExecutedHook executedHook = (PostCommandExecutedHook) hook; + executedHook.onPostExecuted(command, context); + } + } + } + /** * A builder for {@link Hooks} * @@ -156,6 +173,16 @@ public static class Builder { return hook(hook); } + /** + * Adds a hook that runs after a command is executed + * + * @param hook Hook to register + * @return this builder + */ + public @NotNull Builder onPostCommandExecuted(@NotNull PostCommandExecutedHook hook) { + return hook(hook); + } + /** * Adds a hook that runs after a command is registered * diff --git a/common/src/main/java/revxrsal/commands/hook/PostCommandExecutedHook.java b/common/src/main/java/revxrsal/commands/hook/PostCommandExecutedHook.java new file mode 100644 index 00000000..54b58825 --- /dev/null +++ b/common/src/main/java/revxrsal/commands/hook/PostCommandExecutedHook.java @@ -0,0 +1,48 @@ +/* + * This file is part of sweeper, licensed under the MIT License. + * + * Copyright (c) Revxrsal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package revxrsal.commands.hook; + +import org.jetbrains.annotations.NotNull; +import revxrsal.commands.command.CommandActor; +import revxrsal.commands.command.ExecutableCommand; +import revxrsal.commands.node.ExecutionContext; + +/** + * A hook that gets called when a command is executed. + *

+ * This hook is only fired for commands that have been successfully executed. + * Any command that errors in the process will not be invoked here. + */ +@FunctionalInterface +public interface PostCommandExecutedHook extends Hook { + + /** + * Invokes the hook for a command whose action has been successfully executed + * + * @param command The command that will be executed + * @param context The execution context + */ + void onPostExecuted(@NotNull ExecutableCommand command, @NotNull ExecutionContext context); + +} diff --git a/common/src/main/java/revxrsal/commands/node/ParameterNode.java b/common/src/main/java/revxrsal/commands/node/ParameterNode.java index 31514089..3e56679e 100644 --- a/common/src/main/java/revxrsal/commands/node/ParameterNode.java +++ b/common/src/main/java/revxrsal/commands/node/ParameterNode.java @@ -140,13 +140,11 @@ default boolean isRequired() { /** * Provides suggestions for the given user input. * - * @param actor The actor to generate for - * @param input The command input * @param context The execution context. * @return The */ @Contract(pure = true) - @NotNull Collection complete(A actor, @NotNull StringStream input, @NotNull ExecutionContext context); + @NotNull Collection complete(@NotNull ExecutionContext context); /** * Returns the parameter Java type diff --git a/common/src/main/java/revxrsal/commands/node/parser/ParameterNodeImpl.java b/common/src/main/java/revxrsal/commands/node/parser/ParameterNodeImpl.java index d4cee1df..36d878ea 100644 --- a/common/src/main/java/revxrsal/commands/node/parser/ParameterNodeImpl.java +++ b/common/src/main/java/revxrsal/commands/node/parser/ParameterNodeImpl.java @@ -182,7 +182,7 @@ public boolean isOptional() { } @Override - public @NotNull Collection complete(@NotNull A actor, @NotNull StringStream input, @NotNull ExecutionContext context) { + public @NotNull Collection complete(@NotNull ExecutionContext context) { return suggestions.getSuggestions(context); } diff --git a/common/src/main/java/revxrsal/commands/node/parser/ReflectionAction.java b/common/src/main/java/revxrsal/commands/node/parser/ReflectionAction.java index 0dc26669..746c80a6 100644 --- a/common/src/main/java/revxrsal/commands/node/parser/ReflectionAction.java +++ b/common/src/main/java/revxrsal/commands/node/parser/ReflectionAction.java @@ -67,6 +67,7 @@ public void execute(ExecutionContext context) { //noinspection rawtypes function.responseHandler().handleResponse(result, (ExecutionContext) context); } + context.lamp().hooks().onPostCommandExecuted(context.command(), context); } catch (Throwable t) { context.lamp().handleException(t, ErrorContext.executingFunction(context)); } diff --git a/common/src/main/java/revxrsal/commands/parameter/ParameterTypes.java b/common/src/main/java/revxrsal/commands/parameter/ParameterTypes.java index b845fd26..666ba90b 100644 --- a/common/src/main/java/revxrsal/commands/parameter/ParameterTypes.java +++ b/common/src/main/java/revxrsal/commands/parameter/ParameterTypes.java @@ -34,15 +34,13 @@ import revxrsal.commands.help.Help.ChildrenCommands; import revxrsal.commands.help.Help.RelatedCommands; import revxrsal.commands.help.Help.SiblingCommands; +import revxrsal.commands.node.ExecutionContext; import revxrsal.commands.parameter.builtins.*; import revxrsal.commands.parameter.primitives.*; import revxrsal.commands.stream.StringStream; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +import java.util.*; /** * An immutable registry of {@link ParameterType ParameterTypes} and @@ -61,7 +59,7 @@ public final class ParameterTypes { * Highest priority factories. These come even before the user's * parameter types, but work in very specific conditions only. */ - private static final List HIGHEST_PRIORITY_FACTORIES = Arrays.asList( + private static final List HIGHEST_PRIORITY_FACTORIES = Collections.singletonList( ParseWithParameterTypeFactory.INSTANCE ); @@ -90,6 +88,7 @@ public final class ParameterTypes { ContextParameter.Factory.forType(StringStream.class, (parameter, context) -> context.input()), ContextParameter.Factory.forType(ExecutableCommand.class, (parameter, context) -> context.command()), ContextParameter.Factory.forType(Lamp.class, (parameter, context) -> context.lamp()), + ContextParameter.Factory.forType(ExecutionContext.class, (parameter, context) -> context), ContextParameter.Factory.forTypeAndSubclasses(CommandActor.class, (parameter, context) -> context.actor()), ContextParameter.Factory.forType(RelatedCommands.class, (parameter, context) -> context.command().relatedCommands(context.actor())), ContextParameter.Factory.forType(SiblingCommands.class, (parameter, context) -> context.command().siblingCommands(context.actor())), diff --git a/common/src/main/java/revxrsal/commands/stream/BaseStringStream.java b/common/src/main/java/revxrsal/commands/stream/BaseStringStream.java index b4e6b493..16d0d399 100644 --- a/common/src/main/java/revxrsal/commands/stream/BaseStringStream.java +++ b/common/src/main/java/revxrsal/commands/stream/BaseStringStream.java @@ -143,6 +143,10 @@ public char read() { } public @NotNull String readUntil(char delimiter) { + return readUntil(delimiter, false); + } + + public @NotNull String readUntil(char delimiter, boolean allowUnclosed) { StringBuilder result = new StringBuilder(); boolean escaped = false; while (hasRemaining()) { @@ -163,14 +167,24 @@ public char read() { result.append(c); } } - throw new InputParseException(InputParseException.Cause.UNCLOSED_QUOTE); + if (allowUnclosed) + return result.toString(); + else + throw new InputParseException(InputParseException.Cause.UNCLOSED_QUOTE); } @Override public @NotNull String peekString() { + if (!hasRemaining()) + return ""; int cursor = pos; - String value = readString(); - pos = cursor; - return value; + char next = peek(); + if (next == DOUBLE_QUOTE) { + pos += 1; + String result = readUntil(DOUBLE_QUOTE, true); + pos = cursor; + return result; + } + return peekUnquotedString(); } public @NotNull String peekUnquotedString() { diff --git a/common/src/main/java/revxrsal/commands/stream/MutableStringStream.java b/common/src/main/java/revxrsal/commands/stream/MutableStringStream.java index b82e012e..2fcb9fb5 100644 --- a/common/src/main/java/revxrsal/commands/stream/MutableStringStream.java +++ b/common/src/main/java/revxrsal/commands/stream/MutableStringStream.java @@ -204,4 +204,16 @@ public interface MutableStringStream extends StringStream { @Contract(pure = true, value = "-> new") MutableStringStream toMutableCopy(); + /** + * Returns an immutable view of this {@link MutableStringStream}. A view is different + * from a copy, as it will reflect changes to the original string stream, but cannot + * be used to it. + * + * @return An immutable string stream view + */ + @NotNull + @Contract(pure = true) + @Unmodifiable + StringStream toImmutableView(); + } diff --git a/common/src/main/java/revxrsal/commands/stream/MutableStringStreamImpl.java b/common/src/main/java/revxrsal/commands/stream/MutableStringStreamImpl.java index 4fc8bd46..e4642d35 100644 --- a/common/src/main/java/revxrsal/commands/stream/MutableStringStreamImpl.java +++ b/common/src/main/java/revxrsal/commands/stream/MutableStringStreamImpl.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; import revxrsal.commands.exception.InputParseException; import revxrsal.commands.exception.InvalidBooleanException; import revxrsal.commands.exception.InvalidDecimalException; @@ -34,6 +35,8 @@ public final class MutableStringStreamImpl extends BaseStringStream implements MutableStringStream { + private StringStreamView immutableView = null; + /** * Creates a new {@link MutableStringStream} with its position at zero. * @@ -217,4 +220,81 @@ public boolean isMutable() { return true; } + @Override public @NotNull @Unmodifiable StringStream toImmutableView() { + if (immutableView == null) + immutableView = new StringStreamView(); + return immutableView; + } + + private class StringStreamView implements StringStream { + + @Override public @NotNull String source() { + return MutableStringStreamImpl.this.source(); + } + + @Override public int totalSize() { + return MutableStringStreamImpl.this.totalSize(); + } + + @Override public int remaining() { + return MutableStringStreamImpl.this.remaining(); + } + + @Override public char peek() { + return MutableStringStreamImpl.this.peek(); + } + + @Override public String peek(int characters) { + return MutableStringStreamImpl.this.peek(characters); + } + + @Override public char peekOffset(int offset) { + return MutableStringStreamImpl.this.peekOffset(offset); + } + + @Override public boolean hasRemaining() { + return MutableStringStreamImpl.this.hasRemaining(); + } + + @Override public boolean hasFinished() { + return MutableStringStreamImpl.this.hasFinished(); + } + + @Override public boolean canRead(int characters) { + return MutableStringStreamImpl.this.canRead(characters); + } + + @Override public int position() { + return MutableStringStreamImpl.this.position(); + } + + @Override public @NotNull String peekUnquotedString() { + return MutableStringStreamImpl.this.peekUnquotedString(); + } + + @Override public @NotNull String peekString() { + return MutableStringStreamImpl.this.peekString(); + } + + @Override public @NotNull String peekRemaining() { + return MutableStringStreamImpl.this.peekRemaining(); + } + + @Override public @NotNull @Unmodifiable StringStream toImmutableCopy() { + return MutableStringStreamImpl.this.toImmutableCopy(); + } + + @Override public @NotNull MutableStringStream toMutableCopy() { + return MutableStringStreamImpl.this.toMutableCopy(); + } + + @Override public boolean isMutable() { + return false; + } + + @Override public boolean isEmpty() { + return MutableStringStreamImpl.this.isEmpty(); + } + } + } diff --git a/common/src/main/java/revxrsal/commands/stream/StringStream.java b/common/src/main/java/revxrsal/commands/stream/StringStream.java index e79b1162..412c4d0e 100644 --- a/common/src/main/java/revxrsal/commands/stream/StringStream.java +++ b/common/src/main/java/revxrsal/commands/stream/StringStream.java @@ -106,7 +106,8 @@ public interface StringStream { /** * Peeks the next number of character, without moving the cursor forward. * - * @return The number of characters to peek + * @param characters The number of characters to peek + * @return The peeked string */ String peek(int characters); @@ -165,6 +166,9 @@ public interface StringStream { * peek the next string until a whitespace character is encountered. *

* This will not move the cursor forward. + *

+ * Note that, if the next string is an unclosed-quoted string, it will return + * the content inside the unclosed quote. * * @return The next string. */ diff --git a/minestom/src/main/java/revxrsal/commands/minestom/hooks/MinestomCommandHooks.java b/minestom/src/main/java/revxrsal/commands/minestom/hooks/MinestomCommandHooks.java index 7085da66..873bd4b9 100644 --- a/minestom/src/main/java/revxrsal/commands/minestom/hooks/MinestomCommandHooks.java +++ b/minestom/src/main/java/revxrsal/commands/minestom/hooks/MinestomCommandHooks.java @@ -110,8 +110,7 @@ private void addCommand(@NotNull ExecutableCommand command, @NotNull Command if (node.isLiteral()) arguments.add(toArgument(node)); - else if (node instanceof ParameterNode) { - ParameterNode parameter = (ParameterNode) node; + else if (node instanceof ParameterNode parameter) { if (parameter.isSwitch()) { // add the required minestomCommand.addSyntax(generateAction(command), arguments.toArray(Argument[]::new)); @@ -224,11 +223,9 @@ private ArgumentColl ofFlag(ParameterNode parameter) { A actor = actorFactory.create(sender, node.lamp()); StringStream input = StringStream.create(exception.getInput()); ExecutionContext context = ExecutionContext.create(node.command(), actor, input); - if (node instanceof LiteralNode) { - LiteralNode literal = (LiteralNode) node; + if (node instanceof LiteralNode literal) { node.lamp().handleException(exception, ErrorContext.parsingLiteral(context, literal)); - } else if (node instanceof ParameterNode) { - ParameterNode parameter = (ParameterNode) node; + } else if (node instanceof ParameterNode parameter) { node.lamp().handleException(exception, ErrorContext.parsingParameter(context, parameter, input)); } }; diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts deleted file mode 100644 index 5a8362fd..00000000 --- a/paper/build.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id("java") -} - -repositories { - maven(url = "https://oss.sonatype.org/content/repositories/snapshots") - maven(url = "https://hub.spigotmc.org/nexus/content/groups/public/") - maven(url = "https://libraries.minecraft.net") - maven(url = "https://repo.papermc.io/repository/maven-public/") -} - -dependencies { - implementation(project(":common")) - implementation(project(":bukkit")) - implementation(project(":brigadier")) - compileOnly("com.mojang:brigadier:1.0.18") - compileOnly("io.papermc.paper:paper-api:1.21-R0.1-SNAPSHOT") -} - -java.toolchain.languageVersion.set(JavaLanguageVersion.of(21)) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 94aec48a..ce789f49 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,7 +13,6 @@ rootProject.name = "lamp" include("common") include("bukkit") -include("paper") include("bungee") include("brigadier") include("velocity") diff --git a/velocity/src/main/java/revxrsal/commands/velocity/VelocityVisitors.java b/velocity/src/main/java/revxrsal/commands/velocity/VelocityVisitors.java index a95e8145..dfecbbac 100644 --- a/velocity/src/main/java/revxrsal/commands/velocity/VelocityVisitors.java +++ b/velocity/src/main/java/revxrsal/commands/velocity/VelocityVisitors.java @@ -31,9 +31,11 @@ import revxrsal.commands.Lamp; import revxrsal.commands.LampBuilderVisitor; import revxrsal.commands.LampVisitor; +import revxrsal.commands.brigadier.types.ArgumentTypes; import revxrsal.commands.command.CommandActor; import revxrsal.commands.exception.CommandExceptionHandler; import revxrsal.commands.parameter.ContextParameter; +import revxrsal.commands.velocity.actor.ActorFactory; import revxrsal.commands.velocity.actor.VelocityCommandActor; import revxrsal.commands.velocity.annotation.CommandPermission; import revxrsal.commands.velocity.exception.VelocityExceptionHandler; @@ -137,4 +139,48 @@ public final class VelocityVisitors { public static @NotNull LampVisitor brigadier(@NotNull VelocityLampConfig config) { return new VelocityBrigadier<>(config); } + + /** + * Registers the commands into Velocity as {@link BrigadierCommand brigadier commands}. + * + * @param server The server instance + * @param actorFactory The actor factory + * @param argumentTypes The argument type registry + * @param The actor type + * @return The visitor + */ + public static @NotNull LampVisitor brigadier( + @NotNull ProxyServer server, + @NotNull ActorFactory actorFactory, + @NotNull ArgumentTypes argumentTypes + ) { + return new VelocityBrigadier<>(server, actorFactory, argumentTypes); + } + + /** + * Registers the commands into Velocity as {@link BrigadierCommand brigadier commands}. + * + * @param server The server instance + * @param actorFactory The actor factory + * @param The actor type + * @return The visitor + */ + public static @NotNull LampVisitor brigadier( + @NotNull ProxyServer server, + @NotNull ActorFactory actorFactory + ) { + return new VelocityBrigadier<>(server, actorFactory, ArgumentTypes.builder().build()); + } + + /** + * Registers the commands into Velocity as {@link BrigadierCommand brigadier commands}. + * + * @param server The server instance + * @return The visitor + */ + public static @NotNull LampVisitor brigadier( + @NotNull ProxyServer server + ) { + return new VelocityBrigadier<>(server, ActorFactory.defaultFactory(), ArgumentTypes.builder().build()); + } } diff --git a/velocity/src/main/java/revxrsal/commands/velocity/hooks/VelocityBrigadier.java b/velocity/src/main/java/revxrsal/commands/velocity/hooks/VelocityBrigadier.java index 16a52ae4..e24e55a2 100644 --- a/velocity/src/main/java/revxrsal/commands/velocity/hooks/VelocityBrigadier.java +++ b/velocity/src/main/java/revxrsal/commands/velocity/hooks/VelocityBrigadier.java @@ -30,14 +30,17 @@ import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandMeta; import com.velocitypowered.api.command.CommandSource; + import com.velocitypowered.api.proxy.ProxyServer; import org.jetbrains.annotations.NotNull; import revxrsal.commands.Lamp; import revxrsal.commands.LampVisitor; import revxrsal.commands.brigadier.BrigadierConverter; import revxrsal.commands.brigadier.BrigadierParser; +import revxrsal.commands.brigadier.types.ArgumentTypes; import revxrsal.commands.command.ExecutableCommand; import revxrsal.commands.node.ParameterNode; import revxrsal.commands.velocity.VelocityLampConfig; +import revxrsal.commands.velocity.actor.ActorFactory; import revxrsal.commands.velocity.actor.VelocityCommandActor; /** @@ -47,21 +50,31 @@ */ public final class VelocityBrigadier implements LampVisitor, BrigadierConverter { - private final VelocityLampConfig config; + private final ProxyServer server; + private final ActorFactory actorFactory; + private final ArgumentTypes argumentTypes; private final BrigadierParser parser = new BrigadierParser<>(this); - public VelocityBrigadier(VelocityLampConfig config) { - this.config = config; + public VelocityBrigadier(@NotNull VelocityLampConfig config) { + this.server = config.server(); + this.actorFactory = config.actorFactory(); + this.argumentTypes = config.argumentTypes(); + } + + public VelocityBrigadier(@NotNull ProxyServer server, @NotNull ActorFactory actorFactory, @NotNull ArgumentTypes argumentTypes) { + this.server = server; + this.actorFactory = actorFactory; + this.argumentTypes = argumentTypes; } @Override public @NotNull ArgumentType getArgumentType(@NotNull ParameterNode parameter) { - return config.argumentTypes().type(parameter); + return argumentTypes.type(parameter); } @Override public @NotNull A createActor(@NotNull CommandSource sender, @NotNull Lamp lamp) { - return config.actorFactory().create(sender, lamp); + return actorFactory.create(sender, lamp); } @Override @@ -73,10 +86,8 @@ public void visit(@NotNull Lamp lamp) { } for (CommandNode node : root.getChildren()) { BrigadierCommand brigadierCommand = new BrigadierCommand((LiteralCommandNode) node); - CommandMeta meta = config.server().getCommandManager().metaBuilder(node.getName()) -// .plugin(config.plugin()) - .build(); - config.server().getCommandManager().register(meta, brigadierCommand); + CommandMeta meta = server.getCommandManager().metaBuilder(node.getName()).build(); + server.getCommandManager().register(meta, brigadierCommand); } } }