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

[commands] Compound Parser #216

Merged
merged 23 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
- [commands] Add support for named command arguments
- [common] Add `CollectionHelpers#putAllIfAbsent(Map<K, V>, Map<K, V>)` to add all elements from one map to another if key isn't present
- [commands] Improve code structure for registering a command
- [commands] Add Compound parser
- [commands] Add `ParserAssertions#assertParserTabCompletion` for better unit testing

## 0.0.36
- [common] Improve MinecraftVersion added known version (1.8 to 1.21.1) with protocol number and some helper methods
Expand Down Expand Up @@ -40,6 +42,7 @@
- [spigot-commands] `MaterialParser` using namespaced key for tab competition
ikfir marked this conversation as resolved.
Show resolved Hide resolved
- [spigot] Fix visibility API to work on 1.8
- [commands] Add `ValueRequirement`
- [docs] Add docs about region box, transform, region and rotation.

## v0.0.29
- [commands] Improve location parser
Expand Down
1 change: 1 addition & 0 deletions Writerside/cb.tree
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<toc-element topic="Dynamic-annotation-parser.md"/>
<toc-element topic="value-requirement.md"/>
<toc-element topic="parameter-name.md"/>
<toc-element topic="compound-parser.md"/>
</toc-element>
</toc-element>
<toc-element topic="spigot.md">
Expand Down
3 changes: 2 additions & 1 deletion Writerside/topics/Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ You can find some examples of using the commands system in this page
* [`Write information about command`](Command-Information.md)
* [`Dynamic annotation parser`](Dynamic-annotation-parser.md)
* [`Value Requirement`](value-requirement.md)
* [`Named command arguments`](parameter-name.md)
* [`Named command arguments`](parameter-name.md)
* [`Compound parser`](compound-parser.md)
142 changes: 142 additions & 0 deletions Writerside/topics/compound-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# πŸ”¬ Compound parser

<sup>
Available Since 0.0.37
</sup>

**Table of content:**
- [Introduction](#introduction)
- [Usage](#usage)
- [Advantages](#advantages)

## Introduction
Writing custom parsers for complicated arguments, such as locations, can be a tedious and error prone task.
For this reason we have added a way to define compound parsers in the same way its possible to define commands. Each parser could have multiple variants with different input types.

## Usage
We will create a simple compound parser called `LocationParser` that will parse a location from a string doubles,
We will also have a variant with yaw & pitch as float
<tabs>

<tab title="LocationParser.java">


```java
// We need to provide all of our parsers that we will want to use
@WithParser(DoubleParser.class)
@WithParser(FloatParser.class)
@WithParser(WorldParser.class)
public class LocationParser extends CompoundParser<Location> {

public static final String DEFAULT_KEYWORD = "location";

public LocationParser(int priority, String keyword) {
super(
keyword, // We need to provide a keyword
Location.class, // We need to provide a type of the result
priority, // We need to provide a priority of the parser
new SimpleArgumentMapper(), // argument mapper to map arguments
new SimpleCommandLexer()// command lexer to tokenize the variant
);
}

public LocationParser(int priority) {
this(priority, DEFAULT_KEYWORD);
}

// Simple variant
@ParserVariant("<world> <double> <double> <double>")
public Location parseWorldWithXyz(World world, double x, double y, double z) {
return new Location(world, x, y, z);
}
ikfir marked this conversation as resolved.
Show resolved Hide resolved

// another variant with yaw & pitch
@ParserVariant("<world> <double> <double> <double> <float> <float>")
public Location parseWorldWithXyzYawPitch(World world, double x, double y, double z, float yaw, float pitch) {
return new Location(world, x, y, z, yaw, pitch);
}

// We could also have a requirement
@SenderLimit(SenderType.PLAYER)
@ParserVariant("<double> <double> <double>")
public Location parseWithXyz(Player sender, double x, double y, double z) {
return new Location(
sender.getWorld(),
x,
y,
z
);
}

@SenderLimit(SenderType.PLAYER)
@ParserVariant("<double> <double> <double> <float> <float>")
public Location parseWithXyzYawPitch(Player sender, double x, double y, double z, float yaw, float pitch) {
return new Location(
sender.getWorld(),
x,
y,
z,
yaw,
pitch
);
}
}
```

</tab>

</tabs>

That is everything you need to do to create your own compound parser.
There are a couple of things to note:
- You need to provide all of your parsers that you will want to use
- You may want to create a wrapper class that will wrap your compound parser and will cache the variants so you don't have to create them every time someone want to change keyword/priority

#### How could we warp it?

We will change `LocationParser` to be called `LocationParserImpl` and put it as `/* package-private */` then we will create class `LocationParser` that will be public
```java
public class LocationParser extends ArgumentParser<Location> {

private static LocationParserImpl impl = new LocationParserImpl();

public static final String DEFAULT_KEYWORD = "location";

public LocationParser(int priority, String keyword) {
super(
keyword,
Location.class,
priority
);
}

public LocationParser(int priority) {
this(priority, DEFAULT_KEYWORD);
}

@Override
public Optional<ParseResult<T>> parse(CommandProcessingContext processingContext) {
return impl.parse(processingContext);
}

@Override
public OptionalInt tryParse(CommandProcessingContext processingContext) {
return impl.tryParse(processingContext);
}

@Override
public Optional<TabCompletionResult> tabCompletion(CommandProcessingContext processingContext) {
return impl.tabCompletion(processingContext);
}

}
```
ikfir marked this conversation as resolved.
Show resolved Hide resolved
ikfir marked this conversation as resolved.
Show resolved Hide resolved
That will save you memory and computation time but the parsers you use must support multithreading because there could be multiple threads running at the same time, and you don't want to create a new instance of the parser every time.
You could lock the parser but it's not recommended because it will slow down the parsing process.

## Advantages
- **Improved Readability** πŸ“–: The compound parser provides a clear and descriptive name for each variant, making the command structure more readable and self-explanatory.
- **Easier to Use** πŸ‘: The compound parser is easy to use and can be used to create complex parsers with multiple parameters and variants with different requirements.
- **Reduced Code** πŸ“¦: The compound parser is a reusable component that can be used in multiple commands to avoid code duplication and reduce the amount of code needed to write a command.
- **Increased Flexibility** 🎯: The compound parser allows you to create parsers that are more complex and can be used in multiple places in your command structure.
- **Reduced Error Prone** πŸ›: The compound parser is easy to use and can be used to create complex parsers with multiple parameters and variants with different requirements.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

package net.apartium.cocoabeans.commands.spigot.requirements.factory;

import net.apartium.cocoabeans.commands.CommandNode;
import net.apartium.cocoabeans.commands.GenericNode;
ikfir marked this conversation as resolved.
Show resolved Hide resolved
import net.apartium.cocoabeans.commands.Sender;
import net.apartium.cocoabeans.commands.requirements.*;
import net.apartium.cocoabeans.commands.spigot.exception.PermissionException;
Expand All @@ -22,7 +22,7 @@ public class PermissionFactory implements RequirementFactory {

@Nullable
@Override
public Requirement getRequirement(CommandNode node, Object obj) {
public Requirement getRequirement(GenericNode node, Object obj) {
if (!(obj instanceof Permission permission))
return null;

Expand All @@ -33,13 +33,18 @@ private record PermissionImpl(Permission permission, String permissionAsString,

@Override
public RequirementResult meetsRequirement(RequirementEvaluationContext context) {
if (invert)
return meetsInvertedRequirement(context);

Sender sender = context.sender();
if (sender == null || !(sender.getSender() instanceof CommandSender commandSender))

if ((sender.getSender() == null || !(sender.getSender() instanceof CommandSender commandSender))) {
return RequirementResult.error(new UnmetPermissionResponse(
this,
context,
"You don't have permission to execute this command"
"You must be a command sender"
));
}

if (!commandSender.hasPermission(permissionAsString))
return RequirementResult.error(new UnmetPermissionResponse(
Expand All @@ -51,6 +56,26 @@ public RequirementResult meetsRequirement(RequirementEvaluationContext context)
return RequirementResult.meet();
}

private RequirementResult meetsInvertedRequirement(RequirementEvaluationContext context) {
Sender sender = context.sender();

if (sender.getSender() == null || !(sender.getSender() instanceof CommandSender commandSender))
return RequirementResult.error(new UnmetPermissionResponse(
this,
context,
"You must be a command sender"
));

if (commandSender.hasPermission(permissionAsString))
return RequirementResult.error(new UnmetPermissionResponse(
this,
context,
"You should not have permission to execute this command"
));

return RequirementResult.meet();
}

private class UnmetPermissionResponse extends UnmetRequirementResponse {


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import net.apartium.cocoabeans.CollectionHelpers;
import net.apartium.cocoabeans.commands.CommandNode;
import net.apartium.cocoabeans.commands.GenericNode;
import net.apartium.cocoabeans.commands.requirements.*;
import net.apartium.cocoabeans.commands.spigot.SenderType;
import net.apartium.cocoabeans.commands.spigot.exception.SenderLimitException;
Expand All @@ -30,7 +31,7 @@ public class SenderLimitFactory implements RequirementFactory {

@Nullable
@Override
public Requirement getRequirement(CommandNode node, Object obj) {
public Requirement getRequirement(GenericNode node, Object obj) {
if (!(obj instanceof SenderLimit senderLimit))
return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package net.apartium.cocoabeans.commands.spigot.requirements.factory;

import net.apartium.cocoabeans.commands.CommandNode;
import net.apartium.cocoabeans.commands.GenericNode;
import net.apartium.cocoabeans.commands.requirements.*;
import net.apartium.cocoabeans.commands.spigot.exception.WhitelistException;
import net.apartium.cocoabeans.commands.spigot.requirements.Whitelist;
Expand All @@ -30,7 +31,7 @@ public class WhitelistRequirementFactory implements RequirementFactory {

@Nullable
@Override
public Requirement getRequirement(CommandNode node, Object obj) {
public Requirement getRequirement(GenericNode node, Object obj) {
if (!(obj instanceof Whitelist whitelist))
return null;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.apartium.cocoabeans.commands.spigot.game.commands.utils;

import net.apartium.cocoabeans.commands.CommandNode;
import net.apartium.cocoabeans.commands.GenericNode;
import net.apartium.cocoabeans.commands.requirements.*;
import net.apartium.cocoabeans.commands.spigot.game.Game;
import net.apartium.cocoabeans.commands.spigot.game.GamePlayer;
Expand All @@ -13,7 +14,7 @@
public class InGameFactory implements RequirementFactory {

@Override
public @Nullable Requirement getRequirement(CommandNode commandNode, Object obj) {
public @Nullable Requirement getRequirement(GenericNode node, Object obj) {
if (!(obj instanceof InGame inGame))
return null;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.apartium.cocoabeans.commands.spigot.parsers;

import net.apartium.cocoabeans.commands.Command;
import net.apartium.cocoabeans.commands.CommandNode;
import net.apartium.cocoabeans.commands.SubCommand;
import org.bukkit.command.CommandSender;

@Command("cat")
public class CatCommand implements CommandNode {

@SubCommand("set <string> <meow>")
public void setCat(CommandSender sender, String id, Meow cat) {
sender.sendMessage(id + " has been set to " + cat);
}
ikfir marked this conversation as resolved.
Show resolved Hide resolved

}
ikfir marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.apartium.cocoabeans.commands.spigot.parsers;

public record Meow(String cat, int age, Gender gender) {

public enum Gender {
MALE, FEMALE, OTHER
}

@Override
public String toString() {
return "cat: " + cat + ", age: " + age + ", gender: " + gender;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.apartium.cocoabeans.commands.spigot.parsers;

import be.seeseemelk.mockbukkit.entity.PlayerMock;
import net.apartium.cocoabeans.commands.spigot.CommandsSpigotTestBase;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class MeowCommandTest extends CommandsSpigotTestBase {

private PlayerMock player;

@Override
@BeforeEach
public void setup() {
super.setup();
commandManager.registerArgumentTypeHandler(new MeowParser(0));
commandManager.addCommand(new CatCommand());

player = server.addPlayer("ikfir");
}

@Test
void setCommand() {
execute(player, "cat", "set tom a_cat 13 male");
assertEquals("tom has been set to cat: a_cat, age: 13, gender: MALE", player.nextMessage());
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package net.apartium.cocoabeans.commands.spigot.parsers;

import net.apartium.cocoabeans.commands.SimpleArgumentMapper;
import net.apartium.cocoabeans.commands.lexer.SimpleCommandLexer;
import net.apartium.cocoabeans.commands.parsers.*;

import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

@WithParser(StringParser.class)
@WithParser(IntParser.class)
public class MeowParser extends CompoundParser<Meow> {


public MeowParser(int priority) {
super("meow", Meow.class, priority, new SimpleArgumentMapper(), new SimpleCommandLexer());
}

@ParserVariant("<string> <int> <gender>")
public Meow serialize(String cat, int age, Meow.Gender gender) {
return new Meow(cat, age, gender);
}
ikfir marked this conversation as resolved.
Show resolved Hide resolved


@SourceParser(
keyword = "gender",
clazz = Meow.Gender.class,
resultMaxAgeInMills = -1
)
public Map<String, Meow.Gender> getGenders() {
return Arrays.stream(Meow.Gender.values())
.collect(Collectors.toMap(
value -> value.name().toLowerCase(),
value -> value
));
}
ikfir marked this conversation as resolved.
Show resolved Hide resolved

}
Loading
Loading