The processor is two independent halves, each with its own factory:
| Phase | Factory | Input | Output | Reads annotations |
|---|---|---|---|---|
| Parse | CommandParserFactory |
TypeElement |
CommandModel |
yes (only here) |
| Generate | CommandGeneratorFactory |
CommandModel |
Java source file | only SPI + platform annotations |
BaseCommandProcessor calls the first and the second; it holds no generation code. A platform processor is
therefore a few lines. A platform generator is one class with a handful of hooks.
CommandParser reads @Command, @Default, @Resolve, @Name, @Greedy, @Suggest and builds a
CommandModel tree (root command, nested command classes, methods, parameters).
All annotation-derived facts are resolved at this point:
- a named
@Resolvelinks itsMethodModel, including on parameters of other resolver methods @Suggestlinks either a provider method or marks a provider field- diagnostics for unknown resolvers / suggest providers are reported here, not during generation
Generators therefore only read models. The remaining annotation reads are SPI handler dispatch (third-party
annotations) and platform annotations such as @Permission; elements are still used for types and names.
BaseCommandGenerator.generate runs the shared template:
Step 1: configureClass() → class declaration (superclass / interfaces)
Step 2: fields → instance, manager, subcommand instances
Step 3: constructor → platform setup + nested command construction
Step 4: generateEntryMethods() → execute / tabComplete / getCommandNode
Step 5: generateHelpers() → routing, suggestions, sender casts, platform helpers
Step 6: buildCommandInfo() → BaseCommand.getCommandInfo()
- Subcommand dispatch:
switch (args[0].toLowerCase()), one case per subcommand and alias, nested command classes routed toexecute<ClassName>helpers. - Minimum argument check is computed at compile time:
if (args.length < required) throw new CommandException(...). - Built-in types parse inline (
args[1],args[2], ...). Locals are named after the declared parameters (num1,num2), falling back toparam_Nonly when a name would shadow a generated local. - An argument cursor (
int argIdx/int[] argIdxHolder) is emitted only when a parameter can consume conditionally or a runtime resolver can consume a variable number of arguments. - Custom types call
manager.resolveParameter(...). - SPI handlers add validation or checks after each value is resolved.
getCommandNode()builds a native Brigadier tree of literals and arguments.- Paper argument factories are discovered from every no-argument
ArgumentTypesmethod at processing time, so new paper-api argument types are supported without changing this generator. Resolver-returning factories are unwrapped through theirArgumentResolvergeneric type. PaperExecutionSourceretrieves values from the Brigadier context (native arguments ormanager.resolveParameter(...)via aStringargument).- Resolver parameters are resolved from their linked
MethodModel, the same way as the array platforms.
A platform needs two small classes. Neither needs to know how parsing works, and the generator only touches the hooks it changes:
// 1. Generator: platform settings + entry points. Everything else is inherited.
public final class MyCommandGenerator extends BaseCommandGenerator {
public MyCommandGenerator(ProcessingEnvironment env) {
super(env);
// optional: senderTypeRegistry().registerSenderType("my.Sender");
// optional: typeSupport().register(TypeSupport.Entry.builder(...)...build());
}
@Override protected String getWrapperClassSuffix() { return "$MyCommand"; }
@Override public ClassName getSenderTypeName() { return ClassName.get("my", "Sender"); }
@Override protected TypeName getManagerType() { return ParameterizedTypeName.get(
ClassName.get("io.github.projectunified.craftcommand", "CommandManager"), getSenderTypeName()); }
@Override protected void configureClass(TypeSpec.Builder type, CommandModel model) {
type.addSuperinterface(ClassName.get("my", "MyCommand"));
}
@Override protected void generateEntryMethods(TypeSpec.Builder type, CommandModel model) {
MethodSpec.Builder execute = MethodSpec.methodBuilder("execute")
.addAnnotation(Override.class).addModifiers(Modifier.PUBLIC)
.returns(boolean.class)
.addParameter(getSenderTypeName(), "sender")
.addParameter(String[].class, "args");
generateExecuteMethodBody(execute, model, "return true");
type.addMethod(execute.build());
}
}// 2. Processor: registers the generator factory and its service entry.
@AutoService(Processor.class)
@SupportedAnnotationTypes("io.github.projectunified.craftcommand.annotation.Command")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyCommandProcessor extends BaseCommandProcessor {
@Override protected CommandGeneratorFactory getGeneratorFactory() {
return MyCommandGenerator::new;
}
}JavaPoet knowledge needed by a platform author:
generateEntryMethods:TypeSpec/MethodSpec/ClassName(the entry method signatures).configureClass:TypeSpec.Builderif the class needs a superclass or interface.- optional hooks:
addConstructorStatements(MethodSpec.Builder),onBeforeExecute(control flow and$T/$Splaceholders),generatePlatformHelpers(TypeSpec/MethodSpec),getSenderExpression(CodeBlock). TypeSupport.Entry:CodeBlocklambdas plus the named inputsTypeSupport.Resolution/TypeSupport.Suggestionfor platform type parsing and suggestions.
The shared generators are plain calls: generateExecuteMethodBody, buildSuggestionRouting,
buildAliasesExpression, buildSenderCastHelpers, buildTypeSuggestionHelpers, plus the sender/type queries.
Override generateHelpers only for a platform that does not use the array-based helper set (Paper does this and
calls buildSenderCastHelpers + buildTypeSuggestionHelpers). Override onBeforeExecute for checks that run
before any parameter is resolved.
Register the processor with the compiler like any annotation processor. @AutoService(Processor.class) generates
META-INF/services/javax.annotation.processing.Processor at compile time.
| Component | Purpose |
|---|---|
TypeSupport |
Built-in and platform type behaviors (width, literal, parse, suggestions) |
SenderTypeRegistry |
Sender types of a platform and how to recognize them |
SpiLoader |
Loads ParameterAnnotationHandler / MethodAnnotationHandler via Java SPI |
Naming |
Deterministic generated member names |
ArgCursor |
Emits constant indices until conditional consumption requires a cursor |
- Plain Java 8, no runtime helpers required for a normal command.
- Class-level Javadoc only; helper methods rely on names and signatures.
COMMAND_INFOisArrays.asList(...)(wrapped once inunmodifiableListfor the multi-entry case), orsingletonList/emptyList.- Execution and suggestion routing is a
switchthat reads like hand-written code.
- Execution and validation errors throw
CommandException; generated wrappers catch them and callCommandManager.handleException(sender, exception). - Generated messages always go through the manager:
CommandManager.sendMessage(sender, key, defaultFormat, args...)formats and delivers the message.handleExceptionsends the exception message through the same path.formatMessagesupplies the generated default; override it to translate keys.
- Default keys:
permission,unknown-subcommand,usage,missing-argument,invalid-sender,validation.min,validation.max,validation.custom,player-not-found,world-not-found,invalid-coordinate,no-resolver,error(exception messages, with the exception message as the%sargument). - Platform managers implement
sendMessage(sender, message)to deliver to their sender type (console print, Bukkit chat, Paper component).