Skip to content

Latest commit

 

History

History
142 lines (114 loc) · 7.76 KB

architecture.md

File metadata and controls

142 lines (114 loc) · 7.76 KB

Architecture

Overview

Compile-time annotation processing generates platform-specific command wrappers. No runtime reflection is used for command execution, and the core runtime has no external dependencies.

The processor is split into two independently usable halves:

@Command class
   │  CommandParserFactory  (parser/)        reads annotations → immutable models
   ▼
CommandModel tree
   │  CommandGeneratorFactory (generator/)   models → JavaPoet → source file
   ▼
*$StandaloneCommand / *$BukkitCommand / *$PaperCommand

BaseCommandProcessor only wires the two halves: it parses annotated classes and passes each model to the platform's generator. It contains no generation logic.

Module Layout

craftcommand/
├── annotations/        @Command, @Default, @Resolve, @Greedy, @Suggest, @Name        (no dependencies)
├── runtime/            CommandManager, ArgumentResolver, CommandInfo                 (JDK only)
├── bukkit/
│   ├── annotations/    @Permission
│   ├── runtime/        BukkitCommandManager
│   └── processor/      BukkitCommandProcessor → BukkitCommandGenerator
├── paper/
│   ├── runtime/        PaperCommandManager, PaperSuggestions
│   └── processor/      PaperCommandProcessor → PaperCommandGenerator, PaperExecutionSource
├── standalone/
│   ├── runtime/        StandaloneCommandManager
│   └── processor/      StandaloneCommandProcessor → StandaloneCommandGenerator
├── validation/
│   ├── annotations/    @Min, @Max, @ValidateWith
│   └── processor/      MinHandler, MaxHandler, ValidateWithHandler (SPI)
├── processor/
│   ├── BaseCommandProcessor
│   ├── parser/         CommandParserFactory, CommandParser, ResolverLookup
│   ├── generator/      CommandGenerator(Factor), BaseCommandGenerator, Execution/SuggestionGenerator,
│   │                   TypeSupport, SenderTypeRegistry, Naming, ...
│   ├── model/          CommandModel, MethodModel, ParameterModel
│   └── extension/      ParameterAnnotationHandler, MethodAnnotationHandler
└── docs/               This documentation

Processing Flow

  1. BaseCommandProcessor.process finds classes annotated with @Command (nested command classes are handled by their enclosing tree).
  2. CommandParserFactory.parse produces a CommandModel. This is the only phase that reads the core command annotations; it links each parameter's @Resolve/@Suggest targets, including on nested resolver parameters.
  3. CommandGenerator.generate writes one wrapper class per root command. Generators consume models; the only remaining annotation reads are SPI handler dispatch (third-party annotations) and platform annotations such as @Permission. Elements are still used for types and names.
  4. Platform processor registration is generated by AutoService (@AutoService(Processor.class)), so a platform processor only declares its supported annotation and its generator factory.

Core and platform annotations are read through the standard Element#getAnnotation(Class) API, so no prism generation step or extra annotation-processing dependency is required.

Code Generation Pipeline

BaseCommandGenerator owns the shared six-step template. A platform generator overrides the hooks it needs:

Step Hook Purpose
1 configureClass Superclass / interfaces / extra fields
2 addConstructorStatements Constructor setup
3 generateEntryMethods execute / tabComplete / getCommandNode
4 generateHelpers Routing, suggestions, sender casts, platform helpers (overridable)
5 generatePlatformHelpers Platform helper methods, called from step 4
6 (inherited) Command info, routing, parameter resolution

onBeforeExecute and getSenderExpression are optional hooks used by the Bukkit/Paper generators.

Message Flow

Generated wrappers never send text directly. They call CommandManager.sendMessage, which formats the key/fallback and delegates delivery to the platform's sendMessage implementation; handleException uses the same path for caught exceptions. Override formatMessage to translate keys.

Platform Delivery
Standalone System.out.println
Bukkit CommandSender#sendMessage
Paper Adventure Component

Keys include permission, unknown-subcommand, usage, missing-argument, validation.*, and the literals passed to @Permission(message = ...) (or their i18n: suffix).

Runtime Flow

manager.register(new MyCommand())
  → Class.forName(wrapper name, commandClass.getClassLoader())
  → MethodHandle constructor lookup
  → Platform registration (CommandMap / LifecycleEvents / HashMap)

The wrapper is resolved with the command class's own class loader, so addon classes and their generated wrappers stay in the same class-loader scope. The runtime modules depend on the JDK only. bukkit-runtime and paper-runtime also need the server API, which is provided; there are no third-party runtime libraries.

Extension Points

Interface Module Purpose
ParameterAnnotationHandler processor SPI Custom parameter annotations
MethodAnnotationHandler processor SPI Custom method annotations
ArgumentResolver runtime Custom type resolution & suggestions
CommandGeneratorFactory processor New platform generator

Libraries

Library Used for Decision
Palantir JavaPoet 0.22 Code generation Kept: maintained fork, source-model output. Requires JDK 17
Brigadier (via paper-api) Paper command tree and suggestions Kept: Paper's native API, no replacement
AutoService 1.1.1 META-INF/services processor entries Kept: removes three hand-written service files
Avaje Prisms Annotation access Removed: standard Element#getAnnotation is less build setup
JUnit, Mockito, MockBukkit Tests only Kept: no production footprint

No annotation-processing library is required beyond JavaPoet and AutoService. The generated wrappers use the JDK only.

Design Principles

  1. Parse once, generate from models — core annotations are read in parser/ only
  2. Generate, not reflect — wrappers are plain Java
  3. Classloader-aware wrapper lookup — addon class loaders work; wrapper construction uses a MethodHandle
  4. Java 8 generated code, JDK 17 processor — generated wrappers avoid records and var; the processor modules compile at Java 17 because Palantir JavaPoet 0.22 is compiled for Java 17
  5. Minimal generated code — named locals, no unused helpers, no generated narration
  6. SPI for extensions — third-party annotations and suggestions
  7. Standard annotation reads — Element#getAnnotation(Class) for core and platform annotations; no generated prism classes or prism processor dependency