Finish up Brigadier

This commit is contained in:
2026-05-19 21:07:41 -04:00
parent d58365f93f
commit cf15f33496
54 changed files with 1429 additions and 900 deletions
@@ -4,6 +4,12 @@ import dev.plex.command.PlexCommand;
/**
* Registers and unregisters Plex commands with the running platform.
*
* <p>Commands are installed through Paper's Brigadier command lifecycle. A command
* registered before that lifecycle event is active in the current server command
* tree. A command registered or unregistered after that lifecycle event is staged
* in Plex's registry and takes effect the next time Paper rebuilds lifecycle
* commands, such as on a full server restart.</p>
*/
public interface CommandApi
{
@@ -17,7 +23,20 @@ public interface CommandApi
/**
* Unregisters a command from Plex.
*
* <p>If Paper's Brigadier lifecycle has already registered commands for this
* server run, the command may remain in the active dispatcher until Paper
* rebuilds lifecycle commands.</p>
*
* @param command command to unregister
*/
void unregister(PlexCommand command);
/**
* Returns whether command changes are staged for the next Paper command
* lifecycle rebuild.
*
* @return {@code true} when command registration or unregistration changed
* after the active command lifecycle was built
*/
boolean requiresLifecycleReload();
}
@@ -0,0 +1,154 @@
package dev.plex.command;
import dev.plex.command.source.RequiredCommandSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Explicit metadata for a Plex command.
*
* @param name primary command name
* @param description short command description
* @param usage command usage text; {@code <command>} is replaced with the command name
* @param aliases alternate root labels for the command
* @param permission permission node required to use the command
* @param requiredSource source restriction for command execution
*/
public record CommandSpec(
String name,
String description,
String usage,
List<String> aliases,
String permission,
RequiredCommandSource requiredSource)
{
/**
* Creates a command spec builder for the given primary name.
*
* @param name primary command name
* @return command spec builder
*/
public static Builder builder(String name)
{
return new Builder(name);
}
/**
* Returns usage text with the command placeholder expanded.
*
* @return command usage text for this command
*/
public String resolvedUsage()
{
return usage.replace("<command>", name);
}
/**
* Builder for command specs.
*/
public static final class Builder
{
private final String name;
private String description = "";
private String usage = "/<command>";
private List<String> aliases = List.of();
private String permission = "";
private RequiredCommandSource requiredSource = RequiredCommandSource.ANY;
private Builder(String name)
{
this.name = name;
}
/**
* Sets the command description.
*
* @param description command description
* @return this builder
*/
public Builder description(String description)
{
this.description = description;
return this;
}
/**
* Sets the command usage text.
*
* @param usage command usage text
* @return this builder
*/
public Builder usage(String usage)
{
this.usage = usage;
return this;
}
/**
* Sets comma-separated command aliases.
*
* @param aliases comma-separated command aliases
* @return this builder
*/
public Builder aliases(String aliases)
{
if (aliases == null || aliases.isBlank())
{
this.aliases = List.of();
return this;
}
this.aliases = Arrays.stream(aliases.split(","))
.map(String::trim)
.filter(alias -> !alias.isBlank())
.toList();
return this;
}
/**
* Sets command aliases.
*
* @param aliases command aliases
* @return this builder
*/
public Builder aliases(List<String> aliases)
{
this.aliases = aliases == null ? List.of() : new ArrayList<>(aliases);
return this;
}
/**
* Sets the required permission node.
*
* @param permission permission node
* @return this builder
*/
public Builder permission(String permission)
{
this.permission = permission == null ? "" : permission;
return this;
}
/**
* Sets the required command source.
*
* @param requiredSource required command source
* @return this builder
*/
public Builder source(RequiredCommandSource requiredSource)
{
this.requiredSource = requiredSource == null ? RequiredCommandSource.ANY : requiredSource;
return this;
}
/**
* Builds the command spec.
*
* @return command spec
*/
public CommandSpec build()
{
return new CommandSpec(name, description, usage, List.copyOf(aliases), permission, requiredSource);
}
}
}
@@ -1,11 +1,8 @@
package dev.plex.command;
import com.mojang.brigadier.tree.LiteralCommandNode;
import dev.plex.command.annotation.CommandParameters;
import dev.plex.command.annotation.CommandPermissions;
import dev.plex.command.source.RequiredCommandSource;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import java.util.Arrays;
import java.util.List;
/**
@@ -13,6 +10,13 @@ import java.util.List;
*/
public interface PlexCommand
{
/**
* Returns explicit command metadata.
*
* @return command metadata
*/
CommandSpec commandSpec();
/**
* Builds the Brigadier command tree for this command.
*
@@ -20,38 +24,6 @@ public interface PlexCommand
*/
LiteralCommandNode<CommandSourceStack> buildCommand();
/**
* Reads command parameter metadata from {@link CommandParameters}.
*
* @return command parameter metadata
* @throws IllegalStateException if the command class is missing {@link CommandParameters}
*/
default CommandParameters parameters()
{
CommandParameters parameters = getClass().getAnnotation(CommandParameters.class);
if (parameters == null)
{
throw new IllegalStateException(getClass().getName() + " requires a CommandParameters annotation");
}
return parameters;
}
/**
* Reads command permission metadata from {@link CommandPermissions}.
*
* @return command permission metadata
* @throws IllegalStateException if the command class is missing {@link CommandPermissions}
*/
default CommandPermissions permissions()
{
CommandPermissions permissions = getClass().getAnnotation(CommandPermissions.class);
if (permissions == null)
{
throw new IllegalStateException(getClass().getName() + " requires a CommandPermissions annotation");
}
return permissions;
}
/**
* Returns the primary command name.
*
@@ -59,7 +31,7 @@ public interface PlexCommand
*/
default String getName()
{
return parameters().name();
return commandSpec().name();
}
/**
@@ -69,7 +41,7 @@ public interface PlexCommand
*/
default String getDescription()
{
return parameters().description();
return commandSpec().description();
}
/**
@@ -79,7 +51,7 @@ public interface PlexCommand
*/
default String getUsage()
{
return parameters().usage().replace("<command>", getName());
return commandSpec().resolvedUsage();
}
/**
@@ -89,7 +61,7 @@ public interface PlexCommand
*/
default String getPermission()
{
return permissions().permission();
return commandSpec().permission();
}
/**
@@ -99,24 +71,16 @@ public interface PlexCommand
*/
default RequiredCommandSource getRequiredSource()
{
return permissions().source();
return commandSpec().requiredSource();
}
/**
* Returns command aliases as a trimmed list.
*
* @return comma-separated aliases from {@link CommandParameters#aliases()} as a trimmed list
* @return command aliases
*/
default List<String> getAliases()
{
String aliases = parameters().aliases();
if (aliases.isBlank())
{
return List.of();
}
return Arrays.stream(aliases.split(","))
.map(String::trim)
.filter(alias -> !alias.isBlank())
.toList();
return commandSpec().aliases();
}
}
@@ -1,39 +0,0 @@
package dev.plex.command.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Declares display and invocation metadata for a Plex command.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface CommandParameters
{
/**
* Returns the primary command name.
*
* @return primary command name
*/
String name();
/**
* Returns the short command description.
*
* @return short command description
*/
String description() default "";
/**
* Returns the command usage text.
*
* @return command usage text; {@code <command>} is replaced with the command name
*/
String usage() default "/<command>";
/**
* Returns comma-separated command aliases.
*
* @return comma-separated command aliases
*/
String aliases() default "";
}
@@ -1,26 +0,0 @@
package dev.plex.command.annotation;
import dev.plex.command.source.RequiredCommandSource;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Declares permission and command-source requirements for a Plex command.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface CommandPermissions
{
/**
* Returns the permission node required to use the command.
*
* @return permission node required to use the command
*/
String permission() default "";
/**
* Returns the command source required to run the command.
*
* @return command source required to run the command
*/
RequiredCommandSource source() default RequiredCommandSource.ANY;
}
@@ -106,6 +106,13 @@ public abstract class PlexModule
/**
* Registers and tracks a command owned by this module.
*
* <p>Paper Brigadier commands are lifecycle-registered. Commands registered
* during module load before Plex's command handler initializes are active for
* the current startup. Commands registered after the Paper command lifecycle
* has already run are tracked by Plex but are not guaranteed to appear in the
* live dispatcher until Paper rebuilds lifecycle commands, normally on a full
* server restart.</p>
*
* @param command command to register
*/
public void registerCommand(PlexCommand command)
@@ -120,6 +127,10 @@ public abstract class PlexModule
/**
* Unregisters and stops tracking a command owned by this module.
*
* <p>Unregistration removes the command from this module and Plex's registry.
* If Paper has already built the active Brigadier dispatcher, the command may
* remain callable until Paper rebuilds lifecycle commands.</p>
*
* @param command command to unregister
*/
public void unregisterCommand(PlexCommand command)