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

Add some internal documentation #170

Closed
wants to merge 13 commits into from
141 changes: 141 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,144 @@ While contributing to the project, we encourage all contributors to follow good
<br>

If you have any questions or feedback, please reach out to us on Discord (link in the README).

# Developer Guide: How to make a feature
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda wanna refactor most of the code for features so I think its easiest if we leave the changes to CONTRIBUTING.md out and add them back after we're sure what the code is gonna look like. The other changes all seem fine to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok


### Model
The mod is divided into the core and features.

The core is the part of the mod which provides systems for
the features to use, like the configuration system, or
houses general services, like telemetry data or macros.

The features are responsible for implementing the content.

### Setting up a base feature
Each feature has to extend [`AbstractFeature`](src/main/java/com/domain/redstonetools/features/AbstractFeature.java) in some way,
as well as provide basic information through the `@Feature` annotation.

```java
@Feature(name = "My Feature", description = "Test feature", command = "" /* this option is useless unless you extend CommandFeature */)
public class MyFeature extends AbstractFeature {

/* Automatically called when commands should be registered to
* the server dispatcher, no need to subscribe to an event. */
@Override
protected void registerCommands(CommandDispatcher<ServerCommandSource> dispatcher, boolean dedicated) {

}

}
```

This feature will not do much, as it does not provide any additional logic
by extending built in classes. All it provides is a registration callback
in the form of the `void register()` method which you can override.

### Making a command feature
To make a feature which provides a (server)command to the user,
you will need to extend [`CommandFeature`](src/main/java/com/domain/redstonetools/features/commands/CommandFeature.java).

It provides standardized logic for building a command with (optional) arguments.
It will read the command name from the descriptive `@Feature` annotation and read the arguments
from all [`Argument<T>`](src/main/java/com/domain/redstonetools/features/arguments/Argument.java) fields in your feature class.
> **NOTE:** This architecture does not allow for any subcommands.
> Only one argument chain can be bound to the main command.

They take [`TypeSerializer`](src/main/java/com/domain/redstonetools/features/arguments/TypeSerializer.java)s
as the type for parsing and suggesting values.
The argument fields are read in order, so to create a command `/myfeature <a string> <an optional integer>` we can do:
```java
@Feature(name = "My Feature", description = "Test feature", command = "mycommand")
public class MyFeature extends CommandFeature {

/* Each argument has to be declared as public static final
* and is automatically named using the field name if no name is explicitly set. */
public static final Argument<String> aString = Argument
.ofType(StringSerializer.string());

public static final Argument<Integer> anOptionalInteger = Argument
.ofType(IntegerSerializer.integer())
/* withDefault automatically marks it as optional */
.withDefault(1);

@Override
protected Feedback execute(ServerCommandSource source) throws CommandSyntaxException {
/* put your handler code here */

return Feedback.none();
}

}
```

To make your command do something, you have to override and implement the
`Feedback execute(ServerCommandSource source)` method declared by the `CommandFeature` class.

Before we do that, you need to understand the `Feedback` system. It is a standardized way of
sending formatted feedback to the user and handler code. There are 5 types:
- `Feedback.none()` - No explicit feedback in the form of a message, treated as a success.
- `Feedback.success(String message)` - Signals successful completion, with a message attached.
- `Feedback.warning(String message)` - Signals that it was completed, but with a warning attached.
- `Feedback.error(String message)` - Signals that the operation failed severely.
- `Feedback.invalidUsage(String message)` - Signals invalid usage/invocation of the operation.

To write a simple command which repeats the given string a number of times, with 1 being the default
we can do:
```java
@Feature(name = "My Feature", description = "Test feature", command = "mycommand")
public class MyFeature extends CommandFeature {

/* Each argument has to be declared as public static final
* and is automatically named using the field name if no name is explicitly set. */
public static final Argument<String> repeatString = Argument
.ofType(StringSerializer.string());

public static final Argument<Integer> count = Argument
.ofType(IntegerSerializer.integer())
/* withDefault automatically marks it as optional */
.withDefault(1);

@Override
protected Feedback execute(ServerCommandSource source) throws CommandSyntaxException {
String str = repeatString.getValue();
Integer count = count.getValue();

// error if count is below 1
if (count < 1) {
return Feedback.invalidUsage("Count can not be below 1");
}

return Feedback.success("Here is the repeated string: " + str.repeat(count));
}

}
```

Not a very useful feature or command, but it shows the different systems
associated with creating a command.

### Making a toggleable feature
There is a standardized API for creating toggleable features.
To create one, replicate the `CommandFeature` example but extend `ToggleableFeature` instead.
Features are toggled through commands, which are automatically created, so you don't have to build the commands manually.
Due to this, we don't need an `execute` method for the command either.

```java
@Feature(name = "My Second Feature", description = "Test toggleable feature", command = "mytoggleable")
public class MySecondFeature extends ToggleableFeature {

}
```

To query the state of the feature anywhere in the code, first use `INJECTOR.getInstance(YourFeature.class)` to get
the feature's instance, then follow up with `.isEnabled()`.

```java
// cache the instance
private final MyToggleableFeature feature = RedstoneToolsClient.INJECTOR.getInstance(MyToggleableFeature.class);

/* some code */ {
if (!feature.isEnabled()) /* ... */
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public abstract class AbstractFeature {
}
}

/* Getters */

public String getName() {
return feature.name();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Describes basic properties about a feature.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Feature {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
import tools.redstone.redstonetools.features.arguments.serializers.TypeSerializer;
import com.mojang.brigadier.context.CommandContext;

/**
* An argument to a command or other process
* as a configuration option.
*
* @param <T> The value type.
*/
public class Argument<T> {

private String name;
private final TypeSerializer<T, ?> type;
private boolean optional = false;
private T value;
private T defaultValue;
// TODO: maybe add an isSet flag and unset
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for null values

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The values shouldnt be used outside of the execute method anyway

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the serializer accepts null or if the default is null then null is a valid value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes i know but how do we know if the value is set or not when null is a valid value

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you dont need to? if the value is null its either explicitly set to null or defaulted to null, whether its set by the user or if it has that value by default shouldnt matter

// the value after executing the command

private Argument(TypeSerializer<T, ?> type) {
this.type = type;
Expand All @@ -18,6 +27,13 @@ public static <T> Argument<T> ofType(TypeSerializer<T, ?> type) {
return new Argument<>(type);
}

/**
* Set the default value on this argument.
* This forces it to be optional.
*
* @param defaultValue The value.
* @return This.
*/
public Argument<T> withDefault(T defaultValue) {
optional = true;
this.defaultValue = defaultValue;
Expand All @@ -31,14 +47,23 @@ public Argument<T> named(String name) {
return this;
}

public Argument<T> ensureNamed(String fieldName) {
if (name == null) {
name = fieldName;
/**
* Set the name of this argument to the given
* value if unset.
*
* @param name The name to set.
* @return This.
*/
public Argument<T> ensureNamed(String name) {
if (this.name == null) {
this.name = name;
}

return this;
}

/* Getters */

public String getName() {
return name;
}
Expand All @@ -51,8 +76,14 @@ public boolean isOptional() {
return optional;
}

/**
* Update the value of this argument using
* the given command context.
*
* @param context The command context.
*/
@SuppressWarnings("unchecked")
public void setValue(CommandContext<?> context) {
public void updateValue(CommandContext<?> context) {
try {
value = (T) context.getArgument(name, Object.class);
} catch (IllegalArgumentException e) {
Expand All @@ -64,6 +95,11 @@ public void setValue(CommandContext<?> context) {
}
}

/**
* Get the current value set.
*
* @return The value or null if unset.
*/
public T getValue() {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
import java.util.Collection;
import java.util.concurrent.CompletableFuture;

/**
* A type serializer which wraps a Brigadier argument
* type for string deserialization and command features.
*
* @see TypeSerializer
*/
public abstract class BrigadierSerializer<T, S> extends TypeSerializer<T, S> {

// the wrapped brigadier argument type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;

/**
* A Brigadier-based serializer which serializes to
* and deserializes from string.
*
* @see BrigadierSerializer
*/
public abstract class StringBrigadierSerializer<T> extends BrigadierSerializer<T, String> {

public StringBrigadierSerializer(Class<T> clazz, ArgumentType<T> argumentType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
import java.util.concurrent.CompletableFuture;

/**
* Base class for the 'wrapped' argument type.
* Describes how to serialize and deserialize a value of type
* {@code T} to and from {@code S}. Additionally provides
* the ability to read the value from a string.
*
* Implements the Brigadier {@link ArgumentType}, so it can
* be directly used in commands.
*
* @param <T> The value type.
* @param <S> The serialized type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected void registerCommands(CommandDispatcher<ServerCommandSource> dispatche
arguments,
context -> {
for (var argument : arguments) {
argument.setValue(context);
argument.updateValue(context);
}

var feedback = execute(context.getSource());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
import net.minecraft.server.command.ServerCommandSource;

public abstract class AbstractFeedbackSender {

public abstract void sendFeedback(ServerCommandSource source, Feedback feedback);

}
Loading