Merge pull request #166 from bergerhealer/cloud-v2

Migrate to Cloud v2
This commit is contained in:
Irmo van den Berge authored and GitHub committed 2024-05-25 18:08:56 +02:00
commit 6fa045cccb
14 files changed
+601 -459

No files matched your search

+75 -19
View File
@@ -1,55 +1,72 @@
# Using Cloud Command Framework
BKCommonLib shades in a custom fork of the [Cloud Command Framework](https://github.com/bergerhealer/cloud "Cloud Command Framework") library so that depending plugins can use it. If you are new to Cloud, look through their [documentation](https://incendo.github.io/cloud/ "documentation") first.
BKCommonLib shades in the [Cloud Command Framework](https://github.com/Incendo/cloud "Cloud Command Framework") library so that depending plugins can use it. If you are new to Cloud, look through their [documentation](https://cloud.incendo.org/ "documentation") first.
### Technical
Cloud is shaded at a different package path (to avoid conflicts), so using the integrated cloud requires remapping it using the maven shade or gradle shadow plugin. BKCommonLib has a BOM (Bill of materials) hosted on the same repository BKCommonLib is, that includes information about the cloud dependency and its version. The below maven and gradle configurations make use of this.
Incendo Cloud jars can be found on the maven central repository, so they should be available automatically.
### Version
Currently the 1.8.4 release of Cloud is included.
Currently the 2.0.0 beta/release-candidate of Cloud is included.
### Maven
The bergerhealer cloud fork is hosted on the same repository as BKCommonLib:
BKCommonLib has a BOM (Bill of materials) hosted on the same repository BKCommonLib is, that includes information about the cloud dependency and its version. The below snippets are for your project's `pom.xml`.
Begin by adding the repository where BKCommonLib is hosted:
```xml
<repository>
<id>MG-Dev Jenkins CI Maven Repository</id>
<url>https://ci.mg-dev.eu/plugin/repository/everything</url>
</repository>
<repositories>
<repository>
<id>MG-Dev Jenkins CI Maven Repository</id>
<url>https://ci.mg-dev.eu/plugin/repository/everything</url>
</repository>
</repositories>
```
Depend on BKCommonLib and the (fork) cloud command framework libraries:
Next, add BKCommonLib-bom under **dependencyManagement** (!). This automatically loads the information about what cloud version and its dependencies are used for a particular version of BKCommonLib. Next just add all the dependencies without specifying the version. The version is taken from the bom.
```xml
<properties>
<project.bkcommonlib.version>1.20.2-v1-SNAPSHOT</project.bkcommonlib.version>
<project.cloud.version>1.8.4</project.cloud.version>
<project.bkcommonlib.version>1.20.6-v1-SNAPSHOT</project.bkcommonlib.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.bergerkiller.bukkit</groupId>
<artifactId>BKCommonLib-bom</artifactId>
<version>${project.bkcommonlib.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.bergerkiller.bukkit</groupId>
<artifactId>BKCommonLib</artifactId>
<version>${project.bkcommonlib.version}</version>
<scope>provided</scope>
</dependency>
<!-- Cloud Command Framework -->
<dependency>
<groupId>org.bergerhealer.cloud.commandframework</groupId>
<groupId>org.incendo</groupId>
<artifactId>cloud-paper</artifactId>
<version>${project.cloud.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bergerhealer.cloud.commandframework</groupId>
<groupId>org.incendo</groupId>
<artifactId>cloud-annotations</artifactId>
<version>${project.cloud.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bergerhealer.cloud.commandframework</groupId>
<groupId>org.incendo</groupId>
<artifactId>cloud-minecraft-extras</artifactId>
<version>${project.cloud.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
```
However, the library and its dependencies are shaded in at a different package than normal. So, you will have to include the maven shade plugin in your project to relocate them. Not doing so will cause a failure at runtime.
```xml
<build>
<plugins>
<!-- Relocates references to the Cloud command framework to where they are in BKCommonLib -->
<plugin>
@@ -69,7 +86,7 @@ However, the library and its dependencies are shaded in at a different package t
<relocations>
<!-- BKCommonLib relocations of Cloud command framework -->
<relocation>
<pattern>cloud.commandframework</pattern>
<pattern>org.incendo.cloud</pattern>
<shadedPattern>com.bergerkiller.bukkit.common.dep.cloud</shadedPattern>
</relocation>
<relocation>
@@ -87,4 +104,43 @@ However, the library and its dependencies are shaded in at a different package t
</relocations>
</configuration>
</plugin>
</plugins>```
</plugins>
</build>
```
### Gradle
Depending on BKCommonLib will automatically tell Gradle what versions of Cloud and its dependencies to use. The below configuration assumes kotlin gradle syntax, and are for your project's `build.gradle.kts` file.
Begin by adding the repository where BKCommonLib is hosted:
```kotlin
repositories {
maven("https://ci.mg-dev.eu/plugin/repository/everything/")
}
```
Next add the BKCommonLib dependency and the cloud dependencies. For cloud, no version has to be specified, which will be provided by BKCommonLib. It is recommended to make use of [version catalogs](https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format) to make this look cleaner, but this is optional.
```kotlin
dependencies {
compileOnlyApi("com.bergerkiller.bukkit:BKCommonLib:1.20.6-v1-SNAPSHOT")
// Cloud integrated in BKCommonLib
compileOnly("org.incendo:cloud-bom")
compileOnly("org.incendo:cloud-minecraft-bom")
}
```
Finally, make sure the cloud dependency is shaded correctly when compiling your plugin:
```kotlin
plugins {
id("com.github.johnrengelman.shadow") version "8.1.1"
}
tasks {
shadowJar {
val commonPrefix = "com.bergerkiller.bukkit.common.dep"
relocate("org.incendo.cloud", "$commonPrefix.cloud")
relocate("io.leangen.geantyref", "$commonPrefix.typetoken")
relocate("me.lucko.commodore", "$commonPrefix.me.lucko.commodore")
relocate("net.kyori", "$commonPrefix.net.kyori")
}
}
```
+4 -4
View File
@@ -68,8 +68,8 @@ This means you don't have to compile different classes for every different permu
<summary>Includes Cloud Command Framework</summary>
#####
BKCommonLib includes the [**Cloud Command Framework**](https://github.com/bergerhealer/cloud). Write clean commands with annotations or builder pattern, complete with suggestions, permission handling and localization.
BKCommonLib adds a few default utilities to get set up for Paper/Spigot servers even faster.
BKCommonLib includes the [**Cloud Command Framework**](https://cloud.incendo.org/). Write clean commands with annotations or builder pattern, complete with suggestions, permission handling and localization.
BKCommonLib adds a few default utilities to get set up for Paper/Spigot servers even faster. [Dependency information is here](https://github.com/bergerhealer/BKCommonLib/blob/master/CLOUD_HOWTO.md).
******
</details>
<details>
@@ -398,7 +398,7 @@ There are many more features hidden inside utility classes. Look around.
## Dependency Information
Want to use BKCommonLib in your own projects? Use the following repository and dependency information. Remember to update the version as necessary.
Want to use the included [Cloud Command Framework](https://github.com/bergerhealer/cloud)? Follow [these instructions](https://github.com/bergerhealer/BKCommonLib/blob/master/CLOUD_HOWTO.md) to properly relocate your imports.
Want to use the included [Cloud Command Framework](https://cloud.incendo.org/)? Follow [these instructions](https://github.com/bergerhealer/BKCommonLib/blob/master/CLOUD_HOWTO.md) to properly relocate your imports.
<details open>
<summary>Maven</summary>
@@ -448,7 +448,7 @@ Bergerkiller, the project owner, is generally fine with people copy-pasting code
If you're unsure, ask in our Discord server.
BKCommonLib shades in/uses the following libraries, further License conditions may apply:
- [Cloud Command Framework](https://github.com/incendo/cloud) (custom fork)
- [Cloud Command Framework](https://github.com/incendo/cloud)
- [ASM](https://github.com/llbit/ow2-asm)
- [Javassist](https://github.com/jboss-javassist/javassist)
- [Objenesis](http://objenesis.org/)
+18 -17
View File
@@ -8,15 +8,6 @@ plugins {
val buildNumber = System.getenv("BUILD_NUMBER") ?: "NO-CI"
repositories {
mavenLocal {
// Used to access a server JAR for testing
// TODO Use Paperclip instead
content {
includeGroup("org.spigotmc")
includeGroup("com.mojang")
includeGroup("com.bergerkiller.mountiplex")
}
}
mavenCentral()
maven("https://hub.spigotmc.org/nexus/content/groups/public/")
@@ -26,6 +17,16 @@ repositories {
// - Aikar minecraft-timings
// - Myles ViaVersion
maven("https://ci.mg-dev.eu/plugin/repository/everything/")
// Used to access a server JAR for testing
// TODO Use Paperclip instead
mavenLocal {
content {
includeGroup("org.spigotmc")
includeGroup("com.mojang")
includeGroup("com.bergerkiller.mountiplex")
}
}
}
// Configuration for shaded dependencies which should not be added to the published Maven .pom
@@ -97,9 +98,9 @@ dependencies {
// Versions are part of BKCommonLib-bom and are made available automatically
//
internal("org.bergerhealer.cloud.commandframework:cloud-paper")
internal("org.bergerhealer.cloud.commandframework:cloud-annotations")
internal("org.bergerhealer.cloud.commandframework:cloud-minecraft-extras")
internal("org.incendo:cloud-paper")
internal("org.incendo:cloud-annotations")
internal("org.incendo:cloud-minecraft-extras")
internal(libs.commodore) {
isTransitive = false
}
@@ -159,9 +160,9 @@ tasks {
targetCompatibility = "1.8"
}
withType<Test> {
this.testLogging {
this.showStandardStreams = true
withType<Test>().configureEach {
testLogging {
showStandardStreams = true
}
// Uncomment to enable SIMD optimizations, to test performance of color conversion
@@ -194,7 +195,7 @@ tasks {
relocate("com.google.gson", "$prefix.gson")
// Cloud command framework and its dependencies
relocate("cloud.commandframework", "$prefix.cloud")
relocate("org.incendo.cloud", "$prefix.cloud")
relocate("io.leangen.geantyref", "$prefix.typetoken")
relocate("me.lucko.commodore", "$prefix.me.lucko.commodore")
relocate("net.kyori", "$prefix.net.kyori")
@@ -212,7 +213,7 @@ tasks {
exclude(dependency("org.checkerframework:checker-qual"))
}
destinationDirectory.set(buildDir)
destinationDirectory.set(layout.buildDirectory)
archiveFileName.set("${project.name}-${project.version}-$buildNumber.jar")
isPreserveFileTimestamps = false
+4 -4
View File
@@ -13,8 +13,8 @@ viaversion = "3.2.1"
vault = "1.7"
luckperms = "5.4"
protocollib = "4.4.0"
cloud_core = "1.8.4"
cloud_minecraft = "1.8.4"
cloud_core = "2.0.0-rc.1"
cloud_minecraft = "2.0.0-beta.7"
commodore = "1.13"
adventure = "4.14.0"
adventure-platform-bukkit = "4.3.2-PR144-fix"
@@ -29,8 +29,8 @@ netty-all = { module = "io.netty:netty-all", version.ref = "netty" }
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
mountiplex = { module = "com.bergerkiller.mountiplex:Mountiplex", version.ref = "mountiplex" }
cloud-core-bom = { module = "org.bergerhealer.cloud.commandframework:cloud-bom", version.ref = "cloud_core" }
cloud-minecraft-bom = { module = "org.bergerhealer.cloud.commandframework:cloud-bom", version.ref = "cloud_minecraft" }
cloud-core-bom = { module = "org.incendo:cloud-bom", version.ref = "cloud_core" }
cloud-minecraft-bom = { module = "org.incendo:cloud-minecraft-bom", version.ref = "cloud_minecraft" }
regionchangetracker = { module = "com.bergerkiller.bukkit.regionchangetracker:BKCommonLib-RegionChangeTracker-Core", version.ref = "regionchangetracker" }
colorconversionhelper = { module = "com.bergerkiller.bukkit.colorconversionhelper:BKCommonLib-ColorConversionHelper", version.ref = "colorconversionhelper" }
softdependency = { module = "com.bergerkiller.bukkit.softdependency:SoftDependency", version.ref = "softdependencylib" }
@@ -1,125 +0,0 @@
package com.bergerkiller.bukkit.common.cloud;
import java.util.LinkedList;
import org.bukkit.command.CommandSender;
import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext;
import cloud.commandframework.execution.preprocessor.CommandPreprocessor;
/**
* Preprocesses the commands so that quotes can be used to specify words
* with spaces in them as a single argument.
*/
public class CloudCommandPreprocessor implements CommandPreprocessor<CommandSender> {
@Override
public void accept(CommandPreprocessingContext<CommandSender> context) {
// Tokenize "text" -> text with quote escaping rules
{
StringArgTokenizer tokenizer = new StringArgTokenizer();
for (String arg : context.getInputQueue()) {
tokenizer.next(arg);
}
context.getInputQueue().clear();
context.getInputQueue().addAll(tokenizer.complete());
}
}
// Used by convertArgsList
// From BKCommonLib StringUtil, can remove when dependency changes
private static final class StringArgTokenizer {
private final LinkedList<String> result = new LinkedList<String>();
private final StringBuilder buffer = new StringBuilder();
private boolean isEscaped = false;
public void next(String arg) {
// If currently escaped, append space character to buffer first
if (isEscaped) {
buffer.append(' ');
}
int numEscapedAtStart = 0;
for (;numEscapedAtStart < arg.length() && arg.charAt(numEscapedAtStart)=='\"'; numEscapedAtStart++);
int remEscapedAtStart = (numEscapedAtStart % 3);
// Only "-characters are specified. This requires special handling.
// Replace """ with "-characters, and decide based on remainder what to do.
if (numEscapedAtStart == arg.length()) {
if (isEscaped) {
if (remEscapedAtStart == 0) {
// Doesn't change escape parity
appendEscapedQuotes(numEscapedAtStart / 3);
} else {
// ["some | "] -> [some ]
// ["some | ""] -> [some "]
// ["some | """"] -> [some "]
// ["some | """""] -> [some ""]
appendEscapedQuotes((numEscapedAtStart / 3) + remEscapedAtStart - 1);
commitBuffer();
}
} else {
appendEscapedQuotes(numEscapedAtStart / 3);
if (remEscapedAtStart == 1) {
isEscaped = true; // Start escape section
} else {
// When remainder isn't a single quote, commit the buffer
commitBuffer();
}
}
return;
}
// Handle start of an escaped section
if (!isEscaped && remEscapedAtStart > 0) {
isEscaped = true;
remEscapedAtStart--;
}
// Add all remaining quotes at the start
appendEscapedQuotes(numEscapedAtStart / 3 + remEscapedAtStart);
// Process remaining characters of argument in sequence
int numTrailingQuotes = 0;
for (int i = numEscapedAtStart; i < arg.length(); i++) {
char c = arg.charAt(i);
if (c != '\"') {
appendEscapedQuotes(numTrailingQuotes);
numTrailingQuotes = 0;
buffer.append(c);
} else if (++numTrailingQuotes == 3) {
numTrailingQuotes = 0;
buffer.append('\"');
}
}
// Close escaped section
if (!isEscaped) {
appendEscapedQuotes(numTrailingQuotes);
commitBuffer();
} else if (numTrailingQuotes > 0) {
appendEscapedQuotes(numTrailingQuotes-1);
commitBuffer();
}
}
public LinkedList<String> complete() {
if (isEscaped) {
commitBuffer();
}
return result;
}
private void appendEscapedQuotes(int num_quotes) {
for (int n = 0; n < num_quotes; n++) {
buffer.append('\"');
}
}
private void commitBuffer() {
result.add(buffer.toString());
buffer.setLength(0);
isEscaped = false;
}
}
}
@@ -1,8 +1,7 @@
package com.bergerkiller.bukkit.common.cloud;
import com.bergerkiller.bukkit.common.localization.ILocalizationEnum;
import cloud.commandframework.context.CommandContext;
import org.incendo.cloud.context.CommandContext;
/**
* An exception that, when thrown, will result in a localized message
@@ -1,24 +1,24 @@
package com.bergerkiller.bukkit.common.cloud;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import cloud.commandframework.bukkit.BukkitCaptionRegistryFactory;
import com.bergerkiller.bukkit.common.cloud.parsers.SoundEffectArgument;
import com.bergerkiller.bukkit.common.cloud.captions.BKCommonLibCaptionRegistry;
import com.bergerkiller.bukkit.common.cloud.parsers.SoundEffectParser;
import com.bergerkiller.bukkit.common.internal.CommonBootstrap;
import com.bergerkiller.bukkit.common.resources.ResourceKey;
import com.bergerkiller.bukkit.common.resources.SoundEffect;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -29,33 +29,37 @@ import com.bergerkiller.bukkit.common.internal.CommonPlugin;
import com.bergerkiller.bukkit.common.localization.LocalizationEnum;
import com.bergerkiller.bukkit.common.utils.CommonUtil;
import cloud.commandframework.Command;
import cloud.commandframework.CommandManager;
import cloud.commandframework.annotations.AnnotationParser;
import cloud.commandframework.annotations.injection.ParameterInjector;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.arguments.parser.ParserParameter;
import cloud.commandframework.arguments.parser.ParserParameters;
import cloud.commandframework.arguments.parser.StandardParameters;
import cloud.commandframework.arguments.standard.StringArgument;
import cloud.commandframework.brigadier.CloudBrigadierManager;
import cloud.commandframework.bukkit.BukkitCommandManager;
import cloud.commandframework.bukkit.BukkitCommandManager.BrigadierFailureException;
import cloud.commandframework.bukkit.CloudBukkitCapabilities;
import cloud.commandframework.captions.Caption;
import cloud.commandframework.captions.SimpleCaptionRegistry;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.exceptions.CommandExecutionException;
import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.execution.postprocessor.CommandPostprocessor;
import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.minecraft.extras.MinecraftHelp;
import cloud.commandframework.paper.PaperCommandManager;
import cloud.commandframework.services.PipelineException;
import io.leangen.geantyref.TypeToken;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.SenderMapper;
import org.incendo.cloud.annotations.AnnotationParser;
import org.incendo.cloud.annotations.PreprocessorMapper;
import org.incendo.cloud.brigadier.CloudBrigadierManager;
import org.incendo.cloud.bukkit.BukkitCommandManager;
import org.incendo.cloud.bukkit.BukkitCommandManager.BrigadierInitializationException;
import org.incendo.cloud.bukkit.CloudBukkitCapabilities;
import org.incendo.cloud.caption.Caption;
import org.incendo.cloud.caption.CaptionProvider;
import org.incendo.cloud.component.CommandComponent;
import org.incendo.cloud.component.preprocessor.ComponentPreprocessor;
import org.incendo.cloud.description.Description;
import org.incendo.cloud.exception.CommandExecutionException;
import org.incendo.cloud.exception.InjectionException;
import org.incendo.cloud.execution.ExecutionCoordinator;
import org.incendo.cloud.execution.postprocessor.CommandPostprocessor;
import org.incendo.cloud.injection.ParameterInjector;
import org.incendo.cloud.meta.CommandMeta;
import org.incendo.cloud.minecraft.extras.MinecraftHelp;
import org.incendo.cloud.paper.PaperCommandManager;
import org.incendo.cloud.parser.ArgumentParser;
import org.incendo.cloud.parser.ParserDescriptor;
import org.incendo.cloud.parser.ParserParameter;
import org.incendo.cloud.parser.ParserParameters;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.services.PipelineException;
import org.incendo.cloud.suggestion.BlockingSuggestionProvider;
/**
* Configures the Cloud Command Framework for basic use inside a Bukkit Paper or
@@ -70,6 +74,7 @@ public class CloudSimpleHandler {
private BukkitCommandManager<CommandSender> manager;
private AnnotationParser<CommandSender> annotationParser;
private BukkitAudiences bukkitAudiences;
private final Set<Class<?>> exceptionTypes = new HashSet<>();
/**
* Whether this handler for the Cloud Command Framework has been enabled
@@ -91,20 +96,19 @@ public class CloudSimpleHandler {
try {
this.manager = new PaperCommandManager<>(
/* Owning plugin */ plugin,
/* Coordinator function */ CommandExecutionCoordinator.simpleCoordinator(),
/* Command Sender -> C */ Function.identity(),
/* C -> Command Sender */ Function.identity()
/* Coordinator function */ ExecutionCoordinator.simpleCoordinator(),
/* Command Sender <-> C */ SenderMapper.identity()
);
} catch (final Exception e) {
throw new IllegalStateException("Failed to initialize the command manager", e);
}
// BKCommonLib captions
this.manager.captionRegistry(new BukkitCaptionRegistryFactory<CommandSender>().create());
this.manager.captionRegistry().registerProvider(BKCommonLibCaptionRegistry.provider());
// BKCommonLib argument types
this.manager.parserRegistry().registerParserSupplier(new TypeToken<ResourceKey<SoundEffect>>() {},
parserParameters -> new SoundEffectArgument.SoundEffectParser<>());
parserParameters -> new SoundEffectParser<>());
// Register Brigadier mappings
// Only do this on PaperSpigot. On base Spigot, this breaks command blocks
@@ -132,18 +136,33 @@ public class CloudSimpleHandler {
brig.setNativeNumberSuggestions(false);
// I rather hide the proxy class so we use reflection to set it all up
// Kinda ugly...
try {
java.lang.reflect.Method registerMethod = Class.forName("com.bergerkiller.bukkit.common.cloud.parsers.QuotedArgumentParserProxy")
.getMethod("registerBrigadier", CloudBrigadierManager.class);
registerMethod.setAccessible(true);
try {
registerMethod.invoke(null, brig);
} catch (InvocationTargetException ex) {
throw ex.getCause();
}
} catch (Throwable t) {
plugin.getLogger().log(Level.WARNING, "Failed to register the quoted argument parser with brigadier", t);
}
// Register a 'sound effect name' auto-completion provider for brigadier
// This is automatically used for ResourceKey<SoundEffect> types so clients see their client-side sound names.
// As this involves a bunch of at-runtime field and class types, do this stuff in a
// function generated at runtime. Messy :(
if (CommonBootstrap.evaluateMCVersion(">=", "1.13")) {
try {
SoundEffectArgument.registerBrigadier(brig);
SoundEffectParser.registerBrigadier(brig);
} catch (Throwable t) {
plugin.getLogger().log(Level.WARNING, "Failed to register sound effect auto-completion for brigadier", t);
}
}
} catch (BrigadierFailureException ex) {
} catch (BrigadierInitializationException ex) {
plugin.getLogger().log(Level.WARNING, "Failed to register commands using brigadier, " +
"using fallback instead. Error:", ex);
}
@@ -151,41 +170,40 @@ public class CloudSimpleHandler {
}
// Registers a custom command preprocessor that handles quote-escaping
this.manager.registerCommandPreProcessor(new CloudCommandPreprocessor());
// TODO reimplement
// Create the annotation parser. This allows you to define commands using methods annotated with
// @CommandMethod
final Function<ParserParameters, CommandMeta> commandMetaFunction = p ->
CommandMeta.simple()
.with(CommandMeta.DESCRIPTION, p.get(StandardParameters.DESCRIPTION, "No description"))
.build();
// Create the annotation parser. This allows you to define commands using methods annotated with @Command
this.annotationParser = new AnnotationParser<>(
/* Manager */ this.manager,
/* Command sender type */ CommandSender.class,
/* Mapper for command meta instances */ commandMetaFunction
/* Command sender type */ CommandSender.class
);
// Shows the argname as <argname> as a suggestion
// Fix for numeric arguments on the broken brigadier system
suggest("argname", (context,b) -> Collections.singletonList("<" + context.getCurrentArgument().getName() + ">"));
// TODO: This no longer works!
//suggest("argname", (context,b) -> Collections.singletonList("<" + context.getCurrentArgument().getName() + ">"));
suggest("argname", (context,b) -> Collections.singletonList("<argument>"));
// Fixes incorrect exception handling in Cloud, so that user-specified
// exception types can be used instead.
handle(CommandExecutionException.class, this::handleException);
handle(PipelineException.class, this::handleException);
handle(InjectionException.class, this::handleException);
// Makes LocalizedParserException functional
handle(CloudLocalizedException.class, (sender, ex) -> {
sender.sendMessage(ex.getMessage());
// Makes CloudLocalizedException functional
handle(CloudLocalizedException.class, (sender, exception) -> {
sender.sendMessage(exception.getMessage());
});
// Suggests a player name, either of a player that is online right now,
// or of a player that was online in the past.
suggest("playername", (context, input) -> {
String prefix = input.readString();
// Try online players first
List<String> names = Bukkit.getOnlinePlayers().stream()
.map(Player::getName)
.filter(p -> p.startsWith(input))
.filter(p -> p.startsWith(prefix))
.collect(Collectors.toList());
if (!names.isEmpty()) {
@@ -196,7 +214,8 @@ public class CloudSimpleHandler {
// TODO: Doesnt work? Weird.
return Stream.of(Bukkit.getOfflinePlayers())
.map(OfflinePlayer::getName)
.filter(p -> p.startsWith(input))
.filter(Objects::nonNull)
.filter(p -> p.startsWith(prefix))
.collect(Collectors.toList());
});
@@ -207,31 +226,19 @@ public class CloudSimpleHandler {
this.bukkitAudiences = BukkitAudiences.create(plugin);
}
private void handleException(CommandSender sender, Throwable exception) {
private void handleException(CommandSender sender, Throwable exception) throws Throwable {
Throwable cause = exception.getCause();
// Find handler for this exception, if registered, execute that handler
// If the handler throws, handle it as an internal error
@SuppressWarnings({"unchecked", "rawtypes"})
BiConsumer<CommandSender, Throwable> handler = manager.getExceptionHandler((Class) cause.getClass());
if (handler != null) {
try {
handler.accept(sender, exception.getCause());
return;
} catch (Throwable t2) {
cause = t2;
}
if (exceptionTypes.contains(cause.getClass())) {
throw cause;
} else {
// Rethrow to pass to the next handler
throw exception;
}
// Default fallback
this.manager.getOwningPlugin().getLogger().log(Level.SEVERE,
"Exception executing command handler", cause);
sender.sendMessage(ChatColor.RED + "An internal error occurred while attempting to perform this command.");
}
/**
* Gets the parser instance used to parse annotated commands
*
*
* @return parser
*/
public AnnotationParser<CommandSender> getParser() {
@@ -240,7 +247,7 @@ public class CloudSimpleHandler {
/**
* Gets the command manager
*
*
* @return manager
*/
public CommandManager<CommandSender> getManager() {
@@ -257,12 +264,22 @@ public class CloudSimpleHandler {
this.manager.registerCommandPostProcessor(processor);
}
/**
* Register a parser
*
* @param descriptor The descriptor of the parser
* @param <T> Generic type specifying what is produced by the parser
*/
public <T> void parse(ParserDescriptor<CommandSender, T> descriptor) {
this.manager.parserRegistry().registerParser(descriptor);
}
/**
* Register a parser supplier
*
* @param type The type that is parsed by the parser
* @param supplier The function that generates the parser. The map supplied may contain parameters used
* to configure the parser, many of which are documented in {@link StandardParameters}
* to configure the parser, many of which are documented in {@link org.incendo.cloud.parser.StandardParameters}
* @param <T> Generic type specifying what is produced by the parser
*/
public <T> void parse(
@@ -277,7 +294,7 @@ public class CloudSimpleHandler {
*
* @param type The type that is parsed by the parser
* @param supplier The function that generates the parser. The map supplied my contain parameters used
* to configure the parser, many of which are documented in {@link StandardParameters}
* to configure the parser, many of which are documented in {@link org.incendo.cloud.parser.StandardParameters}
* @param <T> Generic type specifying what is produced by the parser
*/
public <T> void parse(
@@ -292,7 +309,7 @@ public class CloudSimpleHandler {
*
* @param name Parser name
* @param supplier The function that generates the parser. The map supplied my contain parameters used
* to configure the parser, many of which are documented in {@link StandardParameters}
* to configure the parser, many of which are documented in {@link org.incendo.cloud.parser.StandardParameters}
*/
public void parse(
String name,
@@ -303,7 +320,7 @@ public class CloudSimpleHandler {
/**
* Register a new named suggestion provider with a constant list of suggestions.
*
*
* @param name Name of the suggestions provider. The name is case independent.
* @param suggestions List of suggestions
*/
@@ -313,7 +330,7 @@ public class CloudSimpleHandler {
/**
* Register a new named suggestion provider with a no-input supplier for a list of suggestions.
*
*
* @param name Name of the suggestions provider. The name is case independent.
* @param suggestionsProvider The suggestions provider
*/
@@ -331,7 +348,7 @@ public class CloudSimpleHandler {
*/
public void suggest(
String name,
BiFunction<CommandContext<CommandSender>, String, List<String>> suggestionsProvider
BlockingSuggestionProvider.Strings<CommandSender> suggestionsProvider
) {
manager.parserRegistry().registerSuggestionProvider(name, suggestionsProvider);
}
@@ -417,8 +434,7 @@ public class CloudSimpleHandler {
*/
public <A extends Annotation> void preprocessAnnotation(
final Class<A> annotation,
final Function<A, BiFunction<CommandContext<CommandSender>, Queue<String>,
ArgumentParseResult<Boolean>>> preprocessorMapper
final PreprocessorMapper<A, CommandSender> preprocessorMapper
) {
this.annotationParser.registerPreprocessorMapper(annotation, preprocessorMapper);
}
@@ -434,8 +450,7 @@ public class CloudSimpleHandler {
*/
public <A extends Annotation> void preprocessAnnotation(
final Class<A> annotation,
final BiFunction<CommandContext<CommandSender>, Queue<String>,
ArgumentParseResult<Boolean>> preprocessorMapper
final ComponentPreprocessor<CommandSender> preprocessorMapper
) {
preprocessAnnotation(annotation, a -> preprocessorMapper);
}
@@ -443,21 +458,22 @@ public class CloudSimpleHandler {
/**
* Registers an exception handler for a given exception class type. This handler will be called
* when this type of exception is thrown during command handling.
*
*
* @param <T> Exception class type
* @param exceptionType Type of exception to handle
* @param handler Handler for the exception type
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public <T extends Throwable> void handle(Class<T> exceptionType, BiConsumer<CommandSender, T> handler) {
this.manager.registerExceptionHandler((Class) exceptionType, (BiConsumer) handler);
public <T extends Throwable> void handle(Class<T> exceptionType, ThrowingBiConsumer<CommandSender, T> handler) {
this.exceptionTypes.add(exceptionType);
this.manager.exceptionController().registerHandler(exceptionType, ctx -> handler.accept(ctx.context().sender(), ctx.exception()));
}
/**
* Registers a message-sending exception handler for a given exception class type. When the exception
* is handled, the message as specified is sent to the player. If the message matches a caption
* regex, then the message is first translated.
*
*
* @param <T>
* @param exceptionType
* @param message
@@ -465,14 +481,14 @@ public class CloudSimpleHandler {
public <T extends Throwable> void handleMessage(Class<T> exceptionType, String message) {
final Caption caption = Caption.of(message);
handle(exceptionType, (sender, exception) -> {
String translated = manager.captionRegistry().getCaption(caption, sender);
String translated = manager.captionRegistry().caption(caption, sender);
sender.sendMessage(translated);
});
}
/**
* Registers a class instance containing command annotations
*
*
* @param <T> Type of annotation holding Object
* @param annotationsClassInstance Object with command annotations
*/
@@ -482,7 +498,7 @@ public class CloudSimpleHandler {
/**
* Registers all the Localization enum constants declared in a Class as captions
*
*
* @param localizationDefaults Enum or Class with static LocalizationEnum constants
*/
public void captionFromLocalization(Class<? extends LocalizationEnum> localizationDefaults) {
@@ -493,32 +509,29 @@ public class CloudSimpleHandler {
/**
* Registers a caption with a factory for producing the value for matched captions
*
*
* @param regex The regex to match
* @param messageFactory Factory for producing the desired value for a caption
*/
public void caption(String regex, BiFunction<Caption, CommandSender, String> messageFactory) {
if (manager.captionRegistry() instanceof SimpleCaptionRegistry) {
final Caption caption = Caption.of(regex);
((SimpleCaptionRegistry<CommandSender>) manager.captionRegistry()).registerMessageFactory(
caption, messageFactory
);
}
public void caption(String regex, Function<CommandSender, String> messageFactory) {
final Caption caption = Caption.of(regex);
final CaptionProvider<CommandSender> provider = CaptionProvider.forCaption(caption, messageFactory);
manager.captionRegistry().registerProvider(provider);
}
/**
* Registers a caption with a fixed value String
*
*
* @param regex The regex to match
* @param value The String value to use when the regex is matched
*/
public void caption(String regex, String value) {
caption(regex, (caption, sender) -> value);
caption(regex, sender -> value);
}
/**
* Registers a new help command for all the commands under a filter prefix
*
*
* @param filterPrefix Command filter prefix, for commands shown in the menu
* @param helpDescription Description of the help command
* @return minecraft help command
@@ -532,7 +545,7 @@ public class CloudSimpleHandler {
/**
* Registers a new help command for all the commands under a filter prefix
*
*
* @param filterPrefix Command filter prefix, for commands shown in the menu
* @param helpDescription Description of the help command
* @param modifier Modifier for the command applied before registering
@@ -549,21 +562,18 @@ public class CloudSimpleHandler {
// Start a builder
Command.Builder<CommandSender> command = Command.newBuilder(
filterPrefix.get(0),
CommandMeta.simple()
.with(CommandMeta.DESCRIPTION, helpDescription)
.build());
CommandMeta.empty());
command = command.commandDescription(Description.of(helpDescription));
// Add literals, then 'help'
for (int i = 1; i < filterPrefix.size(); i++) {
command = command.literal(filterPrefix.get(i));
}
command = command.literal("help");
command = command.argument(StringArgument.<CommandSender>newBuilder("query")
.greedy()
.asOptional());
command = command.optional("query", StringParser.greedyStringParser());
command = command.handler(context -> {
String query = context.getOrDefault("query", "");
help.queryCommands(query, context.getSender());
help.queryCommands(query, context.sender());
});
command = modifier.apply(command);
@@ -575,30 +585,29 @@ public class CloudSimpleHandler {
/**
* Creates a help menu
*
*
* @param commandPrefix Help command prefix
* @param filterPrefix Command filter prefix, for commands shown in the menu
* @return minecraft help
*/
public MinecraftHelp<CommandSender> help(String commandPrefix, final List<String> filterPrefix) {
MinecraftHelp<CommandSender> help = new MinecraftHelp<>(
commandPrefix, /* Help Prefix */
this.bukkitAudiences::sender, /* Audience mapper */
this.manager /* Manager */
);
help.commandFilter(command -> {
List<CommandArgument<CommandSender, ?>> args = command.getArguments();
if (args.size() < filterPrefix.size()) {
return false;
}
for (int i = 0; i < filterPrefix.size(); i++) {
if (!args.get(i).getName().equals(filterPrefix.get(i))) {
return false;
}
}
return true;
});
MinecraftHelp<CommandSender> help = MinecraftHelp.<CommandSender>builder()
.commandManager(this.manager)
.audienceProvider(this.bukkitAudiences::sender)
.commandPrefix(commandPrefix)
.commandFilter(command -> {
List<CommandComponent<CommandSender>> args = command.components();
if (args.size() < filterPrefix.size()) {
return false;
}
for (int i = 0; i < filterPrefix.size(); i++) {
if (!args.get(i).name().equals(filterPrefix.get(i))) {
return false;
}
}
return true;
})
.build();
return help;
}
@@ -0,0 +1,6 @@
package com.bergerkiller.bukkit.common.cloud;
@FunctionalInterface
public interface ThrowingBiConsumer<T, U> {
void accept(T t, U u) throws Throwable;
}
@@ -1,6 +1,6 @@
package com.bergerkiller.bukkit.common.cloud.captions;
import cloud.commandframework.captions.Caption;
import org.incendo.cloud.caption.Caption;
import java.util.Collection;
import java.util.Collections;
@@ -1,25 +1,17 @@
package com.bergerkiller.bukkit.common.cloud.captions;
import cloud.commandframework.captions.SimpleCaptionRegistry;
import org.incendo.cloud.caption.CaptionProvider;
/**
* Caption registry that uses bi-functions to produce messages
*
* @param <C> Command sender type
*/
public class BKCommonLibCaptionRegistry<C> extends SimpleCaptionRegistry<C> {
public class BKCommonLibCaptionRegistry {
public static <C> CaptionProvider<C> provider() {
return CaptionProvider.forCaption(BKCommonLibCaptionKeys.ARGUMENT_PARSE_FAILURE_SOUNDEFFECT, r -> ARGUMENT_PARSE_FAILURE_SOUNDEFFECT);
}
/**
* Default caption for {@link BKCommonLibCaptionKeys#ARGUMENT_PARSE_FAILURE_SOUNDEFFECT}
*/
public static final String ARGUMENT_PARSE_FAILURE_SOUNDEFFECT = "'{input}' is not a valid sound effect name";
@SuppressWarnings("deprecation")
protected BKCommonLibCaptionRegistry() {
super();
this.registerMessageFactory(
BKCommonLibCaptionKeys.ARGUMENT_PARSE_FAILURE_SOUNDEFFECT,
(caption, sender) -> ARGUMENT_PARSE_FAILURE_SOUNDEFFECT
);
}
}
@@ -1,18 +0,0 @@
package com.bergerkiller.bukkit.common.cloud.captions;
/**
* Factory creating {@link BKCommonLibCaptionRegistry} instances
*
* @param <C> Command sender type
*/
public final class BKCommonLibCaptionRegistryFactory<C> {
/**
* Create a new BKCommonLib caption registry instance
*
* @return Created instance
*/
public BKCommonLibCaptionRegistry<C> create() {
return new BKCommonLibCaptionRegistry<>();
}
}
@@ -0,0 +1,84 @@
package com.bergerkiller.bukkit.common.cloud.parsers;
import io.leangen.geantyref.TypeToken;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.execution.CommandExecutor;
import org.incendo.cloud.parser.ArgumentParseResult;
import org.incendo.cloud.parser.ArgumentParser;
import org.incendo.cloud.parser.ParserDescriptor;
import org.incendo.cloud.suggestion.SuggestionFactory;
import org.incendo.cloud.suggestion.SuggestionProvider;
import org.incendo.cloud.suggestion.SuggestionProviderHolder;
/**
* Helper interface for an Argument Parser that processes the input with the quoted string parser
* first. This allows quoted name arguments to be further parsed. Makes use of the argument parser
* mapper feature.
*
* @param <C> Command sender type
* @param <T> Parser result value type
*/
public interface QuotedArgumentParser<C, T> extends SuggestionProviderHolder<C> {
/**
* Creates the ArgumentParser that parses command input as a quoted string, and then
* calls into this parser.
*
* @return ArgumentParser
*/
default ArgumentParser<C, T> createParser() {
return new QuotedArgumentParserProxy<>(this);
}
/**
* Creates a Parser Descriptor that uses this parser to parse the quoted string
*
* @param outputType Output TypeToken type of this parser
* @return Parser Descriptor
*/
default ParserDescriptor<C, T> createDescriptor(TypeToken<T> outputType) {
return ParserDescriptor.of(createParser(), outputType);
}
/**
* Creates a Parser Descriptor that uses this parser to parse the quoted string
*
* @param outputType Output Class type of this parser
* @return Parser Descriptor
*/
default ParserDescriptor<C, T> createDescriptor(Class<T> outputType) {
return ParserDescriptor.of(createParser(), outputType);
}
/**
* Attempts to parse the quoted {@code input} string into an object of type {@link T}.
*
* <p>This method may be called when a command chain is being parsed for execution
* (using {@link CommandExecutor#executeCommand(Object, String)})
* or when a command is being parsed to provide context for suggestions
* (using {@link SuggestionFactory#suggest(Object, String)}).
* It is possible to use {@link CommandContext#isSuggestions()}} to see what the purpose of the
* parsing is. Particular care should be taken when parsing for suggestions, as the parsing
* method is then likely to be called once for every character written by the command sender.</p>
*
* <p>This method should never throw any exceptions under normal circumstances. Instead, if the
* parsing for some reason cannot be done successfully {@link ArgumentParseResult#failure(Throwable)}
* should be returned. This then wraps any exception that should be forwarded to the command sender.</p>
*
* <p>The parser is assumed to be completely stateless and should not store any information about
* the command sender or the command context. Instead, information should be stored in the
* {@link CommandContext}.</p>
*
* @param commandContext Command context
* @param inputString Command Input Quoted String
* @return Parsed command result
*/
ArgumentParseResult<T> parseQuotedString(CommandContext<C> commandContext, String inputString);
@Override
@SuppressWarnings("unchecked")
default @NonNull SuggestionProvider<C> suggestionProvider() {
return this instanceof SuggestionProvider ? (SuggestionProvider<C>) this : SuggestionProvider.noSuggestions();
}
}
@@ -0,0 +1,234 @@
package com.bergerkiller.bukkit.common.cloud.parsers;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.brigadier.CloudBrigadierManager;
import org.incendo.cloud.brigadier.argument.BrigadierMapping;
import org.incendo.cloud.brigadier.argument.BrigadierMappingBuilder;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.ArgumentParseResult;
import org.incendo.cloud.parser.ArgumentParser;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
/**
* This argument parser wraps the {@link QuotedArgumentParser} in such a way that brigadier can pick
* it up and correctly register the argument with the quoted string() type.
*
* @param <C> Command sender type
* @param <T> Output value type
*/
final class QuotedArgumentParserProxy<C, T> implements ArgumentParser<C, T>, SuggestionProvider<C> {
private final ArgumentParser<C, String> baseParser = new StringParser<>(StringParser.StringMode.QUOTED);
private final QuotedArgumentParser<C, T> parser;
public QuotedArgumentParserProxy(QuotedArgumentParser<C, T> parser) {
this.parser = parser;
}
@Override
@SuppressWarnings("unchecked")
public @NonNull ArgumentParseResult<@NonNull T> parse(@NonNull CommandContext<@NonNull C> commandContext, @NonNull CommandInput commandInput) {
ArgumentParseResult<String> baseResult = baseParser.parse(commandContext, commandInput);
Optional<String> baseParsed = baseResult.parsedValue();
if (baseParsed.isPresent()) {
return parser.parseQuotedString(commandContext, baseParsed.get());
} else {
// No value so I guess this is fine?
return (ArgumentParseResult<T>) baseResult;
}
}
@NonNull
public SuggestionProvider<C> suggestionProvider() {
return this;
}
@Override
public CompletableFuture<? extends Iterable<? extends Suggestion>> suggestionsFuture(
final CommandContext<C> context,
final CommandInput commandInput
) {
String input = commandInput.lastRemainingToken();
if (input.startsWith("\"")) {
// Rewrite the input as if it was typed without quotes
final CommandInput unescapedInput = CommandInput.of(unescapeString(input));
// Rewrite all underlying suggestions as "-escaped
return createSuggestions(context, unescapedInput, (suggestions, newSuggestions) -> {
for (Suggestion suggestion : suggestions) {
newSuggestions.add(suggestion.withSuggestion(escapeString(suggestion.suggestion())));
}
});
} else if (input.isEmpty()) {
// Simply show all suggestions, quote-escape those suggestions that require it
return createSuggestions(context, commandInput, (suggestions, newSuggestions) -> {
for (Suggestion suggestion : suggestions) {
newSuggestions.add(suggestion.withSuggestion(escapeStringIfNeeded(suggestion.suggestion())));
}
});
} else {
// Show all suggestions that do not require quote-escaping
return createSuggestions(context, commandInput, (suggestions, newSuggestions) -> {
for (Suggestion suggestion : suggestions) {
if (isAllowedUnquoted(suggestion.suggestion())) {
newSuggestions.add(suggestion);
}
}
});
}
}
private CompletableFuture<? extends Iterable<? extends Suggestion>> createSuggestions(
final CommandContext<C> commandContext,
final CommandInput commandInput,
final BiConsumer<Iterable<? extends Suggestion>, List<Suggestion>> mapperAndFilter
) {
return parser.suggestionProvider().suggestionsFuture(commandContext, commandInput)
.thenApplyAsync(suggestions -> {
List<Suggestion> newSuggestions;
if (suggestions instanceof Collection) {
newSuggestions = new ArrayList<>(((Collection<?>) suggestions).size());
} else {
newSuggestions = new ArrayList<>();
}
mapperAndFilter.accept(suggestions, newSuggestions);
return newSuggestions;
});
}
/*
* These functions were taken over from brigadier and is how quoted string rules work there
*/
public static boolean isAllowedInUnquotedString(final char c) {
//TODO: Flags seem to allow all characters
// So for the moment, we just only disallow spaces
return c != ' ' && c != '"';
/*
return c >= '0' && c <= '9'
|| c >= 'A' && c <= 'Z'
|| c >= 'a' && c <= 'z'
|| c == '_' || c == '-'
|| c == '.' || c == '+';
*/
}
public static boolean isAllowedUnquoted(String str) {
int len = str.length();
for (int i = 0; i < len; i++) {
if (!isAllowedInUnquotedString(str.charAt(i))) {
return false;
}
}
return true;
}
public static String unescapeString(String str) {
// First character must be a " or its not escaped at all. Probably an error.
int len = str.length();
if (len == 0 || str.charAt(0) != '"') {
return str;
}
StringBuilder newStr = new StringBuilder(len - 1);
boolean escaped = false;
for (int i = 1; i < len; i++) {
char c = str.charAt(i);
if (escaped) {
escaped = false;
newStr.append(c);
} else if (c == '\\') {
escaped = true;
} else if (c == '"') {
break;
} else {
newStr.append(c);
}
}
return newStr.toString();
}
public static String escapeStringIfNeeded(String str) {
int len = str.length();
for (int i = 0; i < len; i++ ) {
char c = str.charAt(i);
if (!isAllowedInUnquotedString(c)) {
StringBuilder newStr = new StringBuilder(len + 10);
newStr.append('"');
newStr.append(str, 0, i);
appendEscapedCharToBuilder(newStr, c);
appendEscapedStringToBuilder(newStr, str, i + 1, len);
newStr.append('"');
return newStr.toString();
}
}
return str;
}
public static String escapeString(String str) {
StringBuilder newStr = new StringBuilder(str.length() + 10);
newStr.append('"');
appendEscapedStringToBuilder(newStr, str, 0, str.length());
newStr.append('"');
return newStr.toString();
}
private static void appendEscapedStringToBuilder(StringBuilder builder, String str, int startIndex, int endIndex) {
for (int i = startIndex; i < endIndex; i++) {
appendEscapedCharToBuilder(builder, str.charAt(i));
}
}
private static void appendEscapedCharToBuilder(StringBuilder builder, char c) {
if (c == '\\' || c == '"') {
builder.append('\\');
}
builder.append(c);
}
/**
* Registers the quoted argument parser into brigadier.
*
* @param brig CloudBrigadierManager
* @throws Exception Anything can go wrong
*/
@SuppressWarnings({"rawtypes", "unchecked", "JavaReflectionInvocation", "JavaReflectionMemberAccess"})
public static void registerBrigadier(CloudBrigadierManager<?, ?> brig) throws Exception {
// This horrible fucking code makes the "QuotedArgumentParser" work correctly.
// Cloud does not support registering a mapper for "all classes that are an instance of", so
// we use a proxy final class that is always the same class type to make this work.
// In brigadier we register this to use the StringArgumentType quoted string().
// This makes it all work properly.
// Brig isn't on our class path so there is this mess of a code. Oh well.
//
// Hopefully someday there is a nicer API in Cloud to get this job done, like a mapper
// that allows setting a suggestion provider on an existing parser.
//
// There is no other way currently. The only type allowing this to work must be the Mapping argument parser.
// That one cannot be inherited.
// See:
// https://github.com/Incendo/cloud-minecraft/blob/master/cloud-brigadier/src/main/java/org/incendo/cloud/brigadier/node/LiteralBrigadierNodeFactory.java#L251
Class<?> stringArgumentTypeClass = Class.forName("com.mojang.brigadier.arguments.StringArgumentType");
Class<?> argumentTypeClass = Class.forName("com.mojang.brigadier.arguments.ArgumentType");
Object quotedStringArgumentType = stringArgumentTypeClass.getMethod("string").invoke(null);
BrigadierMappingBuilder builder = BrigadierMapping.builder();
builder = builder.cloudSuggestions();
builder = (BrigadierMappingBuilder) BrigadierMappingBuilder.class.getMethod("toConstant", argumentTypeClass)
.invoke(builder, quotedStringArgumentType);
brig.mappings().registerMapping(QuotedArgumentParserProxy.class, builder.build());
}
}
@@ -1,15 +1,5 @@
package com.bergerkiller.bukkit.common.cloud.parsers;
import cloud.commandframework.ArgumentDescription;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.brigadier.BrigadierMappingBuilder;
import cloud.commandframework.brigadier.CloudBrigadierManager;
import cloud.commandframework.captions.CaptionVariable;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.exceptions.parsing.NoInputProvidedException;
import cloud.commandframework.exceptions.parsing.ParserException;
import com.bergerkiller.bukkit.common.Common;
import com.bergerkiller.bukkit.common.cloud.captions.BKCommonLibCaptionKeys;
import com.bergerkiller.bukkit.common.resources.ResourceKey;
@@ -25,13 +15,20 @@ import com.bergerkiller.mountiplex.reflection.util.LambdaBuilder;
import io.leangen.geantyref.TypeToken;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.brigadier.CloudBrigadierManager;
import org.incendo.cloud.brigadier.argument.BrigadierMappingBuilder;
import org.incendo.cloud.caption.CaptionVariable;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.exception.parsing.ParserException;
import org.incendo.cloud.parser.ArgumentParseResult;
import org.incendo.cloud.parser.ArgumentParser;
import org.incendo.cloud.parser.ParserDescriptor;
import org.incendo.cloud.suggestion.BlockingSuggestionProvider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -40,126 +37,32 @@ import java.util.function.Function;
*
* @param <C> Command sender type
*/
public class SoundEffectArgument<C> extends CommandArgument<C, ResourceKey<SoundEffect>> {
protected SoundEffectArgument(
final boolean required,
final String name,
final String defaultValue,
final BiFunction<CommandContext<C>, String,
List<String>> suggestionsProvider,
final ArgumentDescription defaultDescription
) {
super(required, name,
new SoundEffectParser<>(),
defaultValue,
new TypeToken<ResourceKey<SoundEffect>>() {},
suggestionsProvider,
defaultDescription);
public class SoundEffectParser<C> implements ArgumentParser<C, ResourceKey<SoundEffect>>, BlockingSuggestionProvider.Strings<C> {
public static <C> @NonNull ParserDescriptor<C, ResourceKey<SoundEffect>> soundEffectParser() {
return ParserDescriptor.of(new SoundEffectParser<>(), new TypeToken<ResourceKey<SoundEffect>>() {
});
}
/**
* Create a new {@link Builder}.
*
* @param name argument name
* @param <C> sender type
* @return new {@link Builder}
* @since 1.8.0
*/
public static <C> Builder<C> builder(final String name) {
return new Builder<>(name);
}
/**
* Create a new required argument
*
* @param name Argument name
* @param <C> Command sender type
* @return Created argument
*/
public static <C> CommandArgument<C, ResourceKey<SoundEffect>> of(final String name) {
return SoundEffectArgument.<C>builder(name).asRequired().build();
}
/**
* Create a new optional argument
*
* @param name Argument name
* @param <C> Command sender type
* @return Created argument
*/
public static <C> CommandArgument<C, ResourceKey<SoundEffect>> optional(final String name) {
return SoundEffectArgument.<C>builder(name).asOptional().build();
}
/**
* Create a new optional argument with a default value
*
* @param name Argument name
* @param soundEffect Default value
* @param <C> Command sender type
* @return Created argument
*/
public static <C> CommandArgument<C, ResourceKey<SoundEffect>> optional(
final String name,
final ResourceKey<SoundEffect> soundEffect
) {
return SoundEffectArgument.<C>builder(name).asOptionalWithDefault(soundEffect.getPath()).build();
}
public static final class Builder<C> extends CommandArgument.Builder<C, ResourceKey<SoundEffect>> {
private Builder(final String name) {
super(new TypeToken<ResourceKey<SoundEffect>>() {}, name);
@Override
public @NonNull ArgumentParseResult<@NonNull ResourceKey<SoundEffect>> parse(@NonNull CommandContext<@NonNull C> commandContext, @NonNull CommandInput commandInput) {
String input = commandInput.peekString();
ResourceKey<SoundEffect> result = SoundEffect.fromName(input);
if (result == null) {
return ArgumentParseResult.failure(new SoundEffectParseException(input, commandContext));
}
@Override
public CommandArgument<C, ResourceKey<SoundEffect>> build() {
return new SoundEffectArgument<>(
this.isRequired(),
this.getName(),
this.getDefaultValue(),
this.getSuggestionsProvider(),
this.getDefaultDescription()
);
}
commandInput.readString();
return ArgumentParseResult.success(result);
}
public static class SoundEffectParser<C> implements ArgumentParser<C, ResourceKey<SoundEffect>> {
@Override
public ArgumentParseResult<ResourceKey<SoundEffect>> parse(
final CommandContext<C> commandContext,
final Queue<String> inputQueue
) {
if (inputQueue.isEmpty()) {
return ArgumentParseResult.failure(new NoInputProvidedException(
this.getClass(),
commandContext
));
}
String input = inputQueue.peek();
ResourceKey<SoundEffect> result = SoundEffect.fromName(input);
if (result == null) {
return ArgumentParseResult.failure(new SoundEffectParseException(input, commandContext));
}
inputQueue.remove();
return ArgumentParseResult.success(result);
}
@Override
public List<String> suggestions(
final CommandContext<C> commandContext,
final String input
) {
Collection<MinecraftKeyHandle> keys = SoundEffectHandle.getSoundNames();
List<String> suggestions = new ArrayList<>(keys.size());
for (MinecraftKeyHandle key : keys) {
suggestions.add(key.toString());
}
return suggestions;
@Override
public @NonNull Iterable<@NonNull String> stringSuggestions(@NonNull CommandContext<C> commandContext, @NonNull CommandInput input) {
Collection<MinecraftKeyHandle> keys = SoundEffectHandle.getSoundNames();
List<String> suggestions = new ArrayList<>(keys.size());
for (MinecraftKeyHandle key : keys) {
suggestions.add(key.toString());
}
return suggestions;
}
public static final class SoundEffectParseException extends ParserException {
@@ -244,7 +147,7 @@ public class SoundEffectArgument<C> extends CommandArgument<C, ResourceKey<Sound
final BrigadierMappingBuilder.SuggestionProviderSupplier<?, ?> suggestionProvider =
LambdaBuilder.of(BrigadierMappingBuilder.SuggestionProviderSupplier.class)
.createConstant(soundSuggestionProvider);
.createConstant(soundSuggestionProvider);
final FastMethod<Object> registerMapping = new FastMethod<>(CloudBrigadierManager.class.getMethod("registerMapping", TypeToken.class, Consumer.class));
final FastMethod<Object> builderTo = new FastMethod<>(BrigadierMappingBuilder.class.getMethod("to", Function.class));
@@ -253,7 +156,8 @@ public class SoundEffectArgument<C> extends CommandArgument<C, ResourceKey<Sound
builderTo.forceInitialization();
registerMapping.invoke(brig,
new TypeToken<SoundEffectArgument.SoundEffectParser<CommandSender>>() {},
new TypeToken<com.bergerkiller.bukkit.common.cloud.parsers.SoundEffectParser<CommandSender>>() {
},
(Consumer<BrigadierMappingBuilder<?, ?>>) builder -> {
builderTo.invoke(builder, (Function<Object, Object>) o -> createResourceLocationArgument.invoke(null));
builder.suggestedBy(CommonUtil.unsafeCast(suggestionProvider));