diff --git a/cloud-minecraft/cloud-brigadier/build.gradle.kts b/cloud-minecraft/cloud-brigadier/build.gradle.kts index 982977e3..dff3dc31 100644 --- a/cloud-minecraft/cloud-brigadier/build.gradle.kts +++ b/cloud-minecraft/cloud-brigadier/build.gradle.kts @@ -2,4 +2,5 @@ dependencies { implementation(project(":cloud-core")) /* Needs to be provided by the platform */ compileOnly("com.mojang", "brigadier", Versions.brigadier) + testImplementation("com.mojang", "brigadier", Versions.brigadier) } diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java index 804b5550..276ec7dc 100644 --- a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java @@ -39,6 +39,7 @@ import cloud.commandframework.arguments.standard.IntegerArgument; import cloud.commandframework.arguments.standard.ShortArgument; import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.arguments.standard.StringArrayArgument; +import cloud.commandframework.brigadier.argument.WrappedBrigadierParser; import cloud.commandframework.context.CommandContext; import cloud.commandframework.permission.CommandPermission; import cloud.commandframework.permission.Permission; @@ -92,6 +93,7 @@ public final class CloudBrigadierManager { private final Supplier> dummyContextProvider; private final CommandManager commandManager; private Function brigadierCommandSenderMapper; + private Function backwardsBrigadierCommandSenderMapper; /** * Create a new cloud brigadier manager @@ -108,6 +110,14 @@ public final class CloudBrigadierManager { this.commandManager = commandManager; this.dummyContextProvider = dummyContextProvider; this.registerInternalMappings(); + commandManager.registerCommandPreProcessor(ctx -> { + if (this.backwardsBrigadierCommandSenderMapper != null) { + ctx.getCommandContext().store( + WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER, + this.backwardsBrigadierCommandSenderMapper.apply(ctx.getCommandContext().getSender()) + ); + } + }); } private void registerInternalMappings() { @@ -195,6 +205,14 @@ public final class CloudBrigadierManager { /* Map String[] to a greedy string */ this.registerMapping(new TypeToken>() { }, false, argument -> StringArgumentType.greedyString()); + /* Map wrapped parsers to their native types */ + this.registerWrapperMapping(); + } + + private void registerWrapperMapping() { + /* a small hack to make type inference work properly... O doesn't behave as a wildcard */ + this.registerMapping(new TypeToken>() { + }, true, WrappedBrigadierParser::getNativeArgument); } /** @@ -219,6 +237,18 @@ public final class CloudBrigadierManager { return this.brigadierCommandSenderMapper; } + /** + * Set the backwards mapper from Cloud to Brigadier command senders. + * + *

This is passed to completion requests for mapped argument types.

+ * + * @param mapper the reverse brigadier sender mapper + * @since 1.4.0 + */ + public void backwardsBrigadierSenderMapper(final @NonNull Function<@NonNull C, @Nullable S> mapper) { + this.backwardsBrigadierCommandSenderMapper = mapper; + } + /** * Set whether to use Brigadier's native suggestions for number argument types. *

diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/QueueAsStringReader.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/QueueAsStringReader.java new file mode 100644 index 00000000..4ac9c260 --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/QueueAsStringReader.java @@ -0,0 +1,71 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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 cloud.commandframework.brigadier.argument; + +import com.mojang.brigadier.StringReader; + +import java.util.Deque; +import java.util.Queue; + +final class QueueAsStringReader extends StringReader { + private boolean closed; + private final Queue input; + + QueueAsStringReader(final Queue input) { + super(String.join(" ", input)); + this.input = input; + } + + /** + * Update the underlying queue based on the reader state. + * + *

Can only be run once.

+ */ + void updateQueue() { + if (this.closed) { + throw new IllegalStateException("double-closed"); + } + this.closed = true; + + /* Update elements in the queue to align it with the Brigadier cursor position */ + int idx = this.getCursor(); + + while (idx > 0) { + final String next = this.input.element(); + this.input.remove(); + if (idx >= next.length()) { + idx -= next.length() + 1 /* whitespace */; + } else { + /* we've gotten to a partial word consumed by brigadier... let's try and modify the underlying queue */ + if (!(this.input instanceof Deque)) { + throw new IllegalArgumentException(); + } + ((Deque) this.input).addFirst(next.substring(idx)); + break; + } + } + } + +} diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/StringReaderAsQueue.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/StringReaderAsQueue.java new file mode 100644 index 00000000..a5f0f2a0 --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/StringReaderAsQueue.java @@ -0,0 +1,209 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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 cloud.commandframework.brigadier.argument; + +import com.mojang.brigadier.StringReader; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Queue; + +/** + * An interface combining {@link Queue} behaviour with a Brigadier {@link StringReader}. + * + *

This can be implemented either by wrapping an existing {@link StringReader} instance, or extending {@link StringReader} + * at its creation time to implement this interface.

+ */ +public interface StringReaderAsQueue extends Queue { + + /** + * Given an existing Brigadier {@code StringReader}, get a view of it as a {@link Queue} + * @param reader the input reader + * @return a view of the contents of the reader as a {@link Queue} split by word. + */ + static StringReaderAsQueue from(final StringReader reader) { + if (reader instanceof StringReaderAsQueue) { + return (StringReaderAsQueue) reader; + } else { + return new StringReaderAsQueueImpl.Wrapping(reader); + } + } + + /** + * Get the backing {@link StringReader} used to source data. + * + * @return the original reader + */ + StringReader getOriginal(); + + @Override + default boolean isEmpty() { + return !this.getOriginal().canRead(); + } + + @Override + default boolean contains(final Object element) { + if (element == null) { + return false; + } + + // check if the string is in the collection, and + final int cursor = this.getOriginal().getCursor(); + final String contents = this.getOriginal().getString(); + final int idx = contents.indexOf((String) element, cursor); + if (idx == -1) { + return false; + } + final int length = this.getOriginal().getTotalLength(); + final int end = idx + contents.length(); + + return (idx == cursor || Character.isWhitespace(contents.charAt(idx - 1))) + && (end == length || Character.isWhitespace(contents.charAt(end))); + } + + @Override + default @NonNull Iterator iterator() { + // lazily break into words -- doesn't consume though! + return new Iterator() { + private final String contents = StringReaderAsQueue.this.getOriginal().getString(); + private int rangeStart = StringReaderAsQueue.this.getOriginal().getCursor(); + private int rangeEnd = this.calculateNextEnd(); + + private int calculateNextEnd() { + if (this.rangeStart >= this.contents.length()) { + return -1; + } + final int nextSpace = StringReaderAsQueueImpl.nextWhitespace(this.contents, this.rangeStart); + return nextSpace == -1 ? this.contents.length() : nextSpace; + } + + private void computeNext() { + this.rangeStart = this.rangeEnd + 1; + this.rangeEnd = this.calculateNextEnd(); + } + + @Override + public boolean hasNext() { + return this.rangeEnd > 0; + } + + @Override + public String next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + final String next = this.contents.substring(this.rangeStart, this.rangeEnd); + this.computeNext(); + return next; + } + }; + } + + @Override + default Object[] toArray() { + if (this.isEmpty()) { + return new Object[0]; + } + final ArrayList out = new ArrayList<>(5); + for (final String element : this) { + /* addAll calls toArray on us... which would create a stack overflow */ + //noinspection UseBulkOperation + out.add(element); + } + return out.toArray(); + } + + @Override + default T[] toArray(final T[] a) { + if (this.isEmpty()) { + return Arrays.copyOf(a, 0); + } + final ArrayList out = new ArrayList<>(5); + for (final String element : this) { + /* addAll calls toArray on us... which would create a stack overflow */ + //noinspection UseBulkOperation + out.add(element); + } + return out.toArray(a); + } + + @Override + default boolean add(final String element) { + throw new IllegalStateException("StringReaders cannot have elements appended"); + } + + @Override + default boolean offer(final String element) { + return false; + } + + @Override + default String remove() { + final String result = this.poll(); + if (result == null) { + throw new NoSuchElementException(); + } + return result; + } + + @Override + default String element() { + final String result = this.peek(); + if (result == null) { + throw new NoSuchElementException(); + } + return result; + } + + @Override + default boolean containsAll(final @NonNull Collection elements) { + throw new UnsupportedOperationException("Complex Queue operations are not yet implemented in Cloud"); + } + + @Override + default boolean addAll(final @NonNull Collection elements) { + throw new UnsupportedOperationException("Complex Queue operations are not yet implemented in Cloud"); + } + + @Override + default boolean removeAll(final @NonNull Collection elements) { + throw new UnsupportedOperationException("Complex Queue operations are not yet implemented in Cloud"); + } + + @Override + default boolean retainAll(final @NonNull Collection elements) { + throw new UnsupportedOperationException("Complex Queue operations are not yet implemented in Cloud"); + } + + @Override + default void clear() { // consume all + this.getOriginal().setCursor(this.getOriginal().getTotalLength()); + } + +} diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/StringReaderAsQueueImpl.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/StringReaderAsQueueImpl.java new file mode 100644 index 00000000..658df060 --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/StringReaderAsQueueImpl.java @@ -0,0 +1,136 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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 cloud.commandframework.brigadier.argument; + +import com.mojang.brigadier.StringReader; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; +import java.util.Queue; + +/** + * A wrapper around Mojang's {@link StringReader} that implements the {@link Queue} interface. + * + *

This allows passing the full Brigadier state around through Cloud's parsing chain.

+ */ +final class StringReaderAsQueueImpl { + + private StringReaderAsQueueImpl() { + } + + /* Next whitespace index starting at startIdx, or -1 if none is found */ + static int nextWhitespace(final String input, final int startIdx) { + for (int i = startIdx, length = input.length(); i < length; ++i) { + if (Character.isWhitespace(input.charAt(i))) { + return i; + } + } + return -1; + } + + /* Wrapping variant is implemented here + * Extending variant is only implementable with Mixin, because of clashing return types on the two interfaces (on `peek`). */ + + static final class Wrapping implements StringReaderAsQueue { + private final StringReader original; + private int nextSpaceIdx; /* the character before the start of a new word */ + private @Nullable String nextWord; + + Wrapping(final StringReader original) { + this.original = original; + this.nextSpaceIdx = original.getCursor() - 1; + this.advance(); + } + + @Override + public StringReader getOriginal() { + return this.original; + } + + /* Brigadier doesn't automatically consume whitespace... in order to get the matched behaviour, we consume whitespace + * after every popped string. + */ + private void advance() { + final int startOfNextWord = this.nextSpaceIdx + 1; + this.nextSpaceIdx = nextWhitespace(this.original.getString(), startOfNextWord); + if (this.nextSpaceIdx != -1) { + this.nextWord = this.original.getString().substring(startOfNextWord, this.nextSpaceIdx); + } else if (startOfNextWord < this.original.getTotalLength()) { + this.nextWord = this.original.getString().substring(startOfNextWord); + this.nextSpaceIdx = this.original.getTotalLength() + 1; + } else { + this.nextWord = null; + } + this.original.setCursor(startOfNextWord); + } + + @Override + public String poll() { + /* peek and then advance */ + final String next = this.peek(); + if (next != null) { + this.advance(); + } + return next; + } + + @Override + public String peek() { + return this.nextWord; + } + + @Override + public int size() { + if (this.nextWord == null) { + return 0; + } + int counter = 1; + for (int i = this.nextSpaceIdx; + i != -1 && i < this.original.getTotalLength(); + i = nextWhitespace(this.original.getString(), i + 1)) { + counter++; + } + return counter; + } + + @Override + public boolean remove(final Object o) { + if (Objects.equals(o, this.nextWord)) { + this.advance(); + return true; + } + return false; + } + + @Override + public void clear() { + StringReaderAsQueue.super.clear(); + this.nextWord = null; + this.nextSpaceIdx = -1; + } + + } + +} diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/WrappedBrigadierParser.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/WrappedBrigadierParser.java new file mode 100644 index 00000000..4fadaa6c --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/WrappedBrigadierParser.java @@ -0,0 +1,164 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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 cloud.commandframework.brigadier.argument; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.context.CommandContext; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; + +import static java.util.Objects.requireNonNull; + +/** + * A wrapped argument parser that can expose Brigadier argument types to the Cloud world. + * + * @param the sender type + * @param the value type of the argument + */ +public final class WrappedBrigadierParser implements ArgumentParser { + public static final String COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER = "_cloud_brigadier_native_sender"; + + private final ArgumentType nativeType; + private final int expectedArgumentCount; + + /** + * Create an argument parser based on a brigadier command. + * + * @param nativeType the native command type + */ + public WrappedBrigadierParser(final ArgumentType nativeType) { + this(nativeType, DEFAULT_ARGUMENT_COUNT); + } + + /** + * Create an argument parser based on a brigadier command. + * + * @param nativeType the native command type + * @param expectedArgumentCount the number of arguments the brigadier type is expected to consume + */ + public WrappedBrigadierParser( + final ArgumentType nativeType, + final int expectedArgumentCount + ) { + this.nativeType = requireNonNull(nativeType, "brigadierType"); + this.expectedArgumentCount = expectedArgumentCount; + } + + /** + * Get the backing Brigadier {@link ArgumentType} for this parser. + * + * @return the argument type + */ + public ArgumentType getNativeArgument() { + return this.nativeType; + } + + @Override + public @NonNull ArgumentParseResult<@NonNull T> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final Queue<@NonNull String> inputQueue + ) { + // Convert to a brig reader + final StringReader reader; + + if (inputQueue instanceof StringReader) { + reader = (StringReader) inputQueue; + } else if (inputQueue instanceof StringReaderAsQueue) { + reader = ((StringReaderAsQueue) inputQueue).getOriginal(); + } else { + reader = new QueueAsStringReader(inputQueue); + } + + // Then try to parse + try { + return ArgumentParseResult.success(this.nativeType.parse(reader)); + } catch (final CommandSyntaxException ex) { + return ArgumentParseResult.failure(ex); + } finally { + if (reader instanceof QueueAsStringReader) { + ((QueueAsStringReader) reader).updateQueue(); + } + } + } + + @Override + public @NonNull List<@NonNull String> suggestions( + final @NonNull CommandContext commandContext, + final @NonNull String input + ) { + /* + * Strictly, this is incorrect. + * However, it seems that all Mojang really does with the context passed here + * is use it to query data on the native sender. Hopefully this hack holds up. + */ + final com.mojang.brigadier.context.CommandContext reverseMappedContext = new com.mojang.brigadier.context.CommandContext<>( + commandContext.getOrDefault(COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER, commandContext.getSender()), + commandContext.getRawInputJoined(), + Collections.emptyMap(), + null, + null, + Collections.emptyList(), + StringRange.at(0), + null, + null, + false + ); + + final CompletableFuture result = this.nativeType.listSuggestions(reverseMappedContext, + new SuggestionsBuilder(input, 0)); + + /* again, avert your eyes */ + final List suggestions = result.join().getList(); + final List out = new ArrayList<>(suggestions.size()); + for (final Suggestion suggestion : suggestions) { + out.add(suggestion.getText()); + } + return out; + } + + @Override + public boolean isContextFree() { + return true; + } + + @Override + public int getRequestedArgumentCount() { + return this.expectedArgumentCount; + } + +} diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/package-info.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/package-info.java new file mode 100644 index 00000000..553e90f6 --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/package-info.java @@ -0,0 +1,29 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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. +// + +/** + * Support for wrapping brigadier {@link com.mojang.brigadier.arguments.ArgumentType ArgumentTypes} + * as Cloud {@link cloud.commandframework.arguments.parser.ArgumentParser}. + */ +package cloud.commandframework.brigadier.argument; diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/package-info.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/package-info.java index 17163a7a..56a9131e 100644 --- a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/package-info.java +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/package-info.java @@ -23,6 +23,14 @@ // /** - * Brigadier mappings + * Brigadier mappings. + * + *

For platform implementations using Brigadier, {@link cloud.commandframework.brigadier.CloudBrigadierManager} can map + * Cloud {@link cloud.commandframework.CommandTree command trees} to Brigadier nodes.

+ * + *

To bridge Brigadier and Cloud argument types, an argument parser that wraps Brigadier argument types is available in + * {@link cloud.commandframework.brigadier.argument.WrappedBrigadierParser}. Other classes in that package allow constructing + * Brigadier {@link com.mojang.brigadier.StringReader} instances that can be used for efficient interoperability with + * Brigadier.

*/ package cloud.commandframework.brigadier; diff --git a/cloud-minecraft/cloud-brigadier/src/test/java/cloud/commandframework/brigadier/argument/QueueAsStringReaderTest.java b/cloud-minecraft/cloud-brigadier/src/test/java/cloud/commandframework/brigadier/argument/QueueAsStringReaderTest.java new file mode 100644 index 00000000..586cdc9d --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/test/java/cloud/commandframework/brigadier/argument/QueueAsStringReaderTest.java @@ -0,0 +1,76 @@ +package cloud.commandframework.brigadier.argument; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QueueAsStringReaderTest { + + private static Queue words(final String... elements) { + return new LinkedList<>(Arrays.asList(elements)); + } + + @Test + void testUnchanged() { + final Queue contents = words("hello", "world"); + final QueueAsStringReader reader = new QueueAsStringReader(contents); + reader.updateQueue(); + + assertEquals(words("hello", "world"), contents); + } + + @Test + void testSingleWordRemoved() throws CommandSyntaxException { + final Queue contents = words("hello", "some", "worlds"); + final QueueAsStringReader reader = new QueueAsStringReader(contents); + assertEquals("hello", reader.readString()); + reader.updateQueue(); + + assertEquals(words("some", "worlds"), contents); + } + + @Test + void testBeginningAndMiddleRemoved() throws CommandSyntaxException { + final Queue contents = words("hello", "some", "worlds"); + final QueueAsStringReader reader = new QueueAsStringReader(contents); + assertEquals("hello", reader.readString()); + reader.skipWhitespace(); + assertEquals("some", reader.readString()); + reader.updateQueue(); + + assertEquals(words("worlds"), contents); + } + + @Test + void testAllThreeWordsRead() throws CommandSyntaxException { + final Queue contents = words("hello", "some", "worlds"); + final QueueAsStringReader reader = new QueueAsStringReader(contents); + assertEquals("hello", reader.readString()); + reader.skipWhitespace(); + assertEquals("some", reader.readString()); + reader.skipWhitespace(); + assertEquals("worlds", reader.readString()); + reader.updateQueue(); + + assertTrue(contents.isEmpty()); + } + + @Test + void testPartialWordRead() throws CommandSyntaxException { + final Queue contents = words("hi", "minecraft:pig"); + final QueueAsStringReader reader = new QueueAsStringReader(contents); + assertEquals("hi", reader.readString()); + reader.skipWhitespace(); + assertEquals("minecraft", reader.readStringUntil(':')); + reader.updateQueue(); + + assertEquals(words("pig"), contents); + } + +} diff --git a/cloud-minecraft/cloud-brigadier/src/test/java/cloud/commandframework/brigadier/argument/StringReaderAsQueueTest.java b/cloud-minecraft/cloud-brigadier/src/test/java/cloud/commandframework/brigadier/argument/StringReaderAsQueueTest.java new file mode 100644 index 00000000..ec22d24a --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/test/java/cloud/commandframework/brigadier/argument/StringReaderAsQueueTest.java @@ -0,0 +1,159 @@ +package cloud.commandframework.brigadier.argument; + +import cloud.commandframework.types.tuples.Pair; +import com.mojang.brigadier.StringReader; +import org.junit.jupiter.api.Test; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for {@link StringReaderAsQueue} + * + * Most operations have 4 cases: 0 arguments, 1 argument, 2 arguments, and 3 or more arguments. + * At that point every whitespace handling path should be exercised. + */ +class StringReaderAsQueueTest { + + @Test + void testIsNotEmpty() { + final StringReader reader = new StringReader("hello there"); + final Queue queue = StringReaderAsQueue.from(reader); + assertFalse(queue.isEmpty()); + } + + @Test + void testCreateEmpty() { + final Queue queue = StringReaderAsQueue.from(new StringReader("")); + assertTrue(queue.isEmpty()); + assertNull(queue.peek()); + assertNull(queue.poll()); + } + + @Test + void testReadWord() { + final StringReader original = new StringReader("meow purr"); + final Queue queue = StringReaderAsQueue.from(original); + assertEquals("meow", queue.poll()); + assertEquals("purr", original.getRemaining()); + } + + @Test + void testReadSingleWordContents() { + final StringReader reader = new StringReader("hello"); + final Queue queue = StringReaderAsQueue.from(reader); + + assertFalse(queue.isEmpty()); + assertEquals("hello", queue.peek()); + assertEquals(1, queue.size()); + } + + @Test + void testReadTwoWords() { + final StringReader original = new StringReader("meow purr"); + final Queue queue = StringReaderAsQueue.from(original); + assertEquals("meow", queue.poll()); + assertEquals("purr", queue.poll()); + assertTrue(queue.isEmpty()); + } + + @Test + void testReadThreeWords() { + final Queue queue = StringReaderAsQueue.from(new StringReader("we enjoy commands")); + assertEquals("we", queue.poll()); + assertEquals("enjoy", queue.poll()); + assertEquals("commands", queue.poll()); + assertNull(queue.poll()); + } + + @Test + void testPeekRepeatedly() { + final Queue queue = StringReaderAsQueue.from(new StringReader("commands are fun")); + for (int i = 0; i < 3; ++i) { + assertEquals("commands", queue.peek()); + } + } + + @Test + void testMultiElementIterator() { + final StringReader reader = new StringReader("tell @a :3"); + final Iterator elements = StringReaderAsQueue.from(reader).iterator(); + assertTrue(elements.hasNext()); + assertEquals("tell", elements.next()); + assertTrue(elements.hasNext()); + assertEquals("@a", elements.next()); + assertTrue(elements.hasNext()); + assertEquals(":3", elements.next()); + assertFalse(elements.hasNext()); + + assertThrows(NoSuchElementException.class, elements::next); + } + + @Test + void testDoubleElementIterator() { + final Iterator elements = StringReaderAsQueue.from(new StringReader("cloud good")).iterator(); + assertTrue(elements.hasNext()); + assertEquals("cloud", elements.next()); + assertTrue(elements.hasNext()); + assertEquals("good", elements.next()); + assertFalse(elements.hasNext()); + + assertThrows(NoSuchElementException.class, elements::next); + } + + @Test + void testSingleElementIterator() { + final Iterator elements = StringReaderAsQueue.from(new StringReader("word")).iterator(); + assertTrue(elements.hasNext()); + assertEquals("word", elements.next()); + assertFalse(elements.hasNext()); + + assertThrows(NoSuchElementException.class, elements::next); + } + + @Test + void testEmptyIterator() { + final Iterator empty = StringReaderAsQueue.from(new StringReader("")).iterator(); + + assertFalse(empty.hasNext()); + assertThrows(NoSuchElementException.class, empty::next); + } + + @Test + void testPartlyStartedIteration() { + final Queue queue = StringReaderAsQueue.from(new StringReader("let's go for a walk")); + assertEquals("let's", queue.poll()); + + final Iterator it = queue.iterator(); + assertEquals("go", it.next()); + } + + @Test + void testToArrayMultiple() { + final Queue elements = StringReaderAsQueue.from(new StringReader("one two three four")); + assertArrayEquals(new String[] { "one", "two", "three", "four" }, elements.toArray()); + } + + @Test + void testSizes() { + Stream.of( + Pair.of("", 0), + Pair.of("hi", 1), + Pair.of("the second!", 2), + Pair.of("a third entry", 3), + Pair.of("one two three, four", 4) + ).forEach(pair -> { + assertEquals(pair.getSecond(), StringReaderAsQueue.from(new StringReader(pair.getFirst())).size()); + }); + } + +} diff --git a/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityCommandManager.java b/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityCommandManager.java index fb9b30dc..d837d20a 100644 --- a/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityCommandManager.java +++ b/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityCommandManager.java @@ -175,4 +175,8 @@ public class VelocityCommandManager extends CommandManager implements Brig return this.commandSenderMapper; } + final @NonNull Function<@NonNull C, @NonNull CommandSource> getBackwardsCommandSenderMapper() { + return this.backwardsCommandSenderMapper; + } + } diff --git a/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityPluginRegistrationHandler.java b/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityPluginRegistrationHandler.java index 4e732789..083bacf7 100644 --- a/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityPluginRegistrationHandler.java +++ b/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityPluginRegistrationHandler.java @@ -55,6 +55,7 @@ final class VelocityPluginRegistrationHandler implements CommandRegistrationH this.brigadierManager.brigadierSenderMapper( sender -> this.manager.getCommandSenderMapper().apply(sender) ); + this.brigadierManager.backwardsBrigadierSenderMapper(this.manager.getBackwardsCommandSenderMapper()); } @Override