Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow required arguments after optional arguments (Linking Edition) #395

Merged
merged 18 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d143de1
allowing required arguments to come after optional arguments
DerEchtePilz Jan 5, 2023
09d1bd2
change AbstractArgument#linkArguments to accept an actual argument in…
DerEchtePilz Jan 5, 2023
5567697
Remove imports from CommandAPIMain. Rename AbstractArgument#linkArgum…
DerEchtePilz Jan 6, 2023
8aecdfe
make AbstractArgument#combineWith() copy requirements and permissions…
DerEchtePilz Jan 6, 2023
e206166
add documentation for the combineWith method
DerEchtePilz Jan 6, 2023
5ad5d9e
Merge branch 'JorelAli:dev/dev' into dev/dev
DerEchtePilz Jan 6, 2023
d340cd3
fixing argument generation in AbstractCommandAPICommand#getArgumentsT…
DerEchtePilz Jan 6, 2023
9b5b663
fix AbstractCommandAPICommand#getArgumentsToRegister() for real this …
DerEchtePilz Jan 6, 2023
a598955
handle chained calls of AbstractArgument#combineWith() correctly
DerEchtePilz Jan 7, 2023
b5f0ee9
make requirements and permissions copy values correctly
DerEchtePilz Jan 7, 2023
7e54622
removes an unused import in AbstractCommandAPICommand
DerEchtePilz Jan 7, 2023
162fd76
improve a message for the combined arguments examples
DerEchtePilz Jan 7, 2023
858097f
improve JavaDocs for AbstractArgument#combineWith()
DerEchtePilz Jan 7, 2023
6f0878e
optimize AbstractCommandAPICommand#getArgumentsToRegister(), modify O…
DerEchtePilz Jan 7, 2023
7a90e77
remove an unnecessary check in AbstractCommandAPICommand#register
DerEchtePilz Jan 7, 2023
af681d7
improve JavaDocs of AbstractArgument#combineWith()
DerEchtePilz Jan 7, 2023
2577d57
Merge JorelAli:dev/dev into dev/dev
DerEchtePilz Feb 3, 2023
b287a86
Merge 'DerEchtePilz:dev/dev' into 'dev/dev'
DerEchtePilz Feb 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ private List<Argument[]> getArgumentsToRegister(Argument[] argumentsArray) {

// Check optional argument constraints
// They can only be at the end, no required argument can follow an optional argument
// This method also ignores linked arguments
DerEchtePilz marked this conversation as resolved.
Show resolved Hide resolved
int firstOptionalArgumentIndex = -1;
for (int i = 0, optionalArgumentIndex = -1; i < argumentsArray.length; i++) {
if (argumentsArray[i].isOptional()) {
Expand All @@ -324,12 +325,29 @@ private List<Argument[]> getArgumentsToRegister(Argument[] argumentsArray) {
if (firstOptionalArgumentIndex != -1) {
DerEchtePilz marked this conversation as resolved.
Show resolved Hide resolved
for (int i = 0; i <= argumentsArray.length; i++) {
if (i >= firstOptionalArgumentIndex) {
DerEchtePilz marked this conversation as resolved.
Show resolved Hide resolved
Argument[] arguments = (Argument[]) new AbstractArgument[i];
System.arraycopy(argumentsArray, 0, arguments, 0, i);
argumentsToRegister.add(arguments);
List<Argument> arguments = new ArrayList<>();
Argument[] argumentsWithoutCombined = (Argument[]) new AbstractArgument[i];
System.arraycopy(argumentsArray, 0, argumentsWithoutCombined, 0, i);
for (Argument argument : argumentsWithoutCombined) {
arguments.addAll(unpackCombinedArguments(argument));
}
DerEchtePilz marked this conversation as resolved.
Show resolved Hide resolved
argumentsToRegister.add(arguments.toArray((Argument[]) new AbstractArgument[0]));
}
}
}
return argumentsToRegister;
}

private List<Argument> unpackCombinedArguments(Argument argument) {
if (!argument.hasCombinedArguments()) {
return List.of(argument);
}
List<Argument> combinedArguments = new ArrayList<>();
combinedArguments.add(argument);
for (Argument subArgument : argument.getCombinedArguments()) {
subArgument.copyPermissionsAndRequirements(argument);
combinedArguments.addAll(unpackCombinedArguments(subArgument));
}
return combinedArguments;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import dev.jorel.commandapi.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -224,6 +225,13 @@ public final Impl withRequirement(Predicate<CommandSender> requirement) {
return instance();
}

/**
* Resets the requirements for this command
*/
public final void resetRequirements() {
this.requirements = s -> true;
}

DerEchtePilz marked this conversation as resolved.
Show resolved Hide resolved
/////////////////
// Listability //
/////////////////
Expand Down Expand Up @@ -255,6 +263,7 @@ public Impl setListed(boolean listed) {
/////////////////

private boolean isOptional = false;
private final List<Argument> combinedArguments = new ArrayList<>();

/**
* Returns true if this argument will be optional when executing the command this argument is included in
Expand All @@ -276,6 +285,39 @@ public Impl setOptional(boolean optional) {
return instance();
}

/**
* Returns a list of arguments linked to this argument.
*
* @return A list of arguments linked to this argument
*/
public List<Argument> getCombinedArguments() {
return combinedArguments;
}

/**
* Returns true if this argument has linked arguments.
*
* @return true if this argument has linked arguments
*/
public boolean hasCombinedArguments() {
return !combinedArguments.isEmpty();
}

/**
* Adds combined arguments to this argument. Combined arguments are used to have required arguments after optional arguments
* by ignoring they exist until they are added to the arguments array for registration
*
* @param combinedArguments The arguments to combine to this argument
* @return this current argument
*/
@SafeVarargs
public final Impl combineWith(Argument... combinedArguments) {
for (Argument argument : combinedArguments) {
this.combinedArguments.add(argument);
}
return instance();
}

///////////
// Other //
///////////
Expand All @@ -294,6 +336,18 @@ public List<String> getEntityNames(Object argument) {
return Arrays.asList(new String[]{null});
}

/**
* Copies permissions and requirements from the provided argument to this argument
* This also resets additional permissions and requirements.
*
* @param argument The argument to copy permissions and requirements from
*/
public void copyPermissionsAndRequirements(Argument argument) {
this.resetRequirements();
this.withRequirement(argument.getRequirements());
this.withPermission(argument.getArgumentPermission());
}

@Override
public String toString() {
return this.getNodeName() + "<" + this.getClass().getSimpleName() + ">";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,34 @@ void resultingcommandexecutor3(){
/* ANCHOR_END: argumentsayhicmd2 */
}

{
/* ANCHOR: argumentrate */
new CommandAPICommand("rate")
.withOptionalArguments(new StringArgument("topic").combineWith(new IntegerArgument("rating", 0, 10)))
.withOptionalArguments(new PlayerArgument("target"))
.executes((sender, args) -> {
String topic = (String) args.get("topic");
if(topic == null) {
sender.sendMessage(
"Usage: /rate <topic> <rating> <player>(optional)",
"Select a topic to rate, then give a rating between 0 and 10",
"You can optionally add a player at the end to give the rating to"
);
return;
}

// We know this is not null because rating is required if topic is given
int rating = (int) args.get("rating");

// The target player is optional, so give it a default here
CommandSender target = (CommandSender) args.getOrDefault("target", sender);

target.sendMessage("Your " + topic + " was rated: " + rating + "/10");
})
.register();
/* ANCHOR_END: argumentrate */
}

@SuppressWarnings("unused")
public void argumentCasting() {
/* ANCHOR: argumentcasting */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,34 @@ CommandAPICommand("sayhi")
/* ANCHOR_END: argumentsayhicmd2 */
}

fun ratecommand() {
/* ANCHOR: argumentrate */
CommandAPICommand("rate")
.withOptionalArguments(StringArgument("topic").combineWith(IntegerArgument("rating", 0, 10)))
.withOptionalArguments(PlayerArgument("target"))
.executes(CommandExecutor { sender, args ->
val topic: String? = args["topic"] as String?
if (topic == null) {
sender.sendMessage(
"Usage: /rate <topic> <rating> <player>(optional)",
"Select a topic to rate, then give a rating between 0 and 10",
"You can optionally add a player at the end to give the rating to"
)
return@CommandExecutor
}

// We know this is not null because rating is required if topic is given
val rating = args["rating"] as Int

// The target player is optional, so give it a default here
val target: CommandSender = args.getOrDefault("target", sender) as CommandSender

target.sendMessage("Your $topic was rated: $rating/10")
})
.register()
/* ANCHOR_END: argumentrate */
}

@Suppress("unused")
public fun argumentCasting() {
/* ANCHOR: argumentcasting */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,34 @@ commandAPICommand("sayhi") {
/* ANCHOR_END: argumentsayhicmd2 */
}

fun ratecommand() {
/* ANCHOR: argumentrate */
commandAPICommand("rate") {
argument(StringArgument("topic").setOptional(true).combineWith(IntegerArgument("rating", 0, 10)))
playerArgument("target", optional = true)
anyExecutor { sender, args ->
val topic: String? = args["topic"] as String?
if (topic == null) {
sender.sendMessage(
"Usage: /rate <topic> <rating> <player>(optional)",
"Select a topic to rate, then give a rating between 0 and 10",
"You can optionally add a player at the end to give the rating to"
)
return@anyExecutor
}

// We know this is not null because rating is required if topic is given
val rating = args["rating"] as Int

// The target player is optional, so give it a default here
val target: CommandSender = args.getOrDefault("target", sender) as CommandSender

target.sendMessage("Your $topic was rated: $rating/10")
}
}
/* ANCHOR_END: argumentrate */
}

@Suppress("unused")
fun argumentCasting() {
/* ANCHOR: argumentcasting */
Expand Down
78 changes: 78 additions & 0 deletions docssrc/src/optional_arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,81 @@ This is how the `getOrDefault` method is being implemented:
</div>

</div>

## Implementing required arguments after optional arguments

We've now talked about how to implement optional arguments and how to avoid null values returned by optional arguments when they aren't provided when executing the command.

Now we also want to talk about how to implement required arguments after optional arguments. For this, the CommandAPI implements a `combineWith` method for arguments:

```java
AbstractArgument combineWith(Argument<?>... combinedArguments);
```

You will need to use this method if you want to have a required argument after an optional argument. In general, this is which pattern the CommandAPI follows while dealing with optional arguments:

1. You have a `CommandAPICommand` and you add arguments to it.
2. After your required arguments, you can provide optional arguments.

At this point your command is basically done. Any attempt to add a required argument will result in an `OptionalArgumentException`. However, this is where the `combineWith` method comes in.
This method allows you to combine arguments. Let's say you have an optional `StringArgument` (here simplified to `A`) and you want a required `PlayerArgument` (here simplified to `B`).
Argument `B` should only be required if argument `A` is given. To implement that logic, we are going to use the `combineWith` method so that we have this syntax:

```java
A.combineWith(B)
```

This does two things:

1. When checking optional argument constraints the argument `B` will be ignored so the `OptionalArgumentException` will not be thrown
2. It allows you to define additional optional arguments afterwards which can only be entered if argument `B` has been entered

This is how you would add another optional `PlayerArgument` (here simplified to `C`):

```java
new CommandAPICommand("mycommand")
.withOptionalArguments(A.combineWith(B))
.withOptionalArguments(C)
```

Let's say you declare your arguments like this:

```java
new CommandAPICommand("mycommand")
.withOptionalArguments(A.combineWith(B))
.withArguments(C)
```

This would result in an `OptionalArgumentException` because you are declaring a required argument after an optional argument without creating that exception for argument `C` like you do for argument `B`.

<div class="example">

### Example - Required arguments after optional arguments

We want to register a command `/rate` with the following syntax:

```mccmd
/rate - Sends an information message
/rate <topic> <rating> - Rates a topic with a rating and sends a message to the command sender
/rate <topic> <rating> <target> - Rates a topic with a rating and sends a message to the target
```

To implement that structure we make use of the `combineWith` method to make the argument after the optional argument \<topic> required:

<div class="multi-pre">

```java,Java
{{#include ../../commandapi-documentation-code/src/main/java/dev/jorel/commandapi/examples/java/Examples.java:argumentrate}}
```

```kotlin,Kotlin
{{#include ../../commandapi-documentation-code/src/main/kotlin/dev/jorel/commandapi/examples/kotlin/Examples.kt:argumentrate}}
```

```kotlin,Kotlin_DSL
{{#include ../../commandapi-documentation-code/src/main/kotlin/dev/jorel/commandapi/examples/kotlin/ExamplesKotlinDSL.kt:argumentrate}}
```

</div>

</div>