Skip to content

Fantamomo/brigadier-kt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

brigadier-kt

A Kotlin-first DSL and structured execution framework built on top of Mojang's Brigadier command library.

brigadier-kt does not replace Brigadier — it enhances it by preserving Brigadier's dispatcher, parsing engine, node system, and execution model while transforming the development experience into a structured, middleware-enabled, Kotlin-native command framework.


Table of Contents


Introduction

What is brigadier-kt?

Brigadier is a powerful, performant, tree-based command parsing library. However, its API is low-level and verbose when building complex command trees. brigadier-kt addresses this by providing a clean, idiomatic Kotlin interface while maintaining 100% compatibility with Brigadier's core functionality.

You still use Brigadier's dispatcher and nodes — brigadier-kt simply makes defining them clean, structured, and maintainable.

Why Use brigadier-kt?

Problems with Pure Brigadier:

Working directly with Brigadier often leads to:

  • Deep .then() nesting that obscures command structure
  • Repetitive generic type declarations
  • Manual ctx.getArgument(...) calls scattered throughout code
  • Duplicated validation logic across command handlers
  • No middleware pipeline for shared validation
  • No structured approach to argument transformation

Example in Pure Brigadier:

dispatcher.register(
        literal("group")
            .then(argument("name", StringArgumentType.word())
                .then(literal("info")
                    .executes(ctx -> {
                        String name = ctx.getArgument("name", String.class);
                        // resolve group...
                        return 1;
                }))));

This becomes increasingly difficult to read and maintain as command trees grow.

Core Features

brigadier-kt provides:

  • Clean Kotlin DSL — Visually structured command definitions
  • Middleware Guards — Centralized validation and transformation pipeline
  • Type-Safe Arguments — Reified generics remove boilerplate
  • Argument References — Strongly typed, reusable argument bindings
  • Transformation Pipeline — Convert parsed strings to domain objects
  • Mutable Context — Safe argument injection and override system
  • Redirect / Fork / Forward — Full support for Brigadier's node routing system
  • Suggestions DSL — Simplified suggestion handling
  • Full Brigadier Compatibility — Drop-in enhancement, not a replacement

Getting Started

Compiler Requirements

Argument references (argRef() and createArgRef<T>()) rely on Kotlin's context parameters feature. You must enable the following compiler option in your build configuration:

-Xcontext-parameters

Without this flag, any code using argRef() or createArgRef<T>() will fail to compile.

Basic Command

val node = command<CommandSource>("hello") {
    execute {
        println("Hello world!")
        SINGLE_SUCCESS
    }
}
dispatcher.register(node)

This registers the /hello command.

Command Tree Structure

The DSL mirrors your command structure visually, making the hierarchy immediately clear:

command<CommandSource>("group") {

    literal("info") {
        execute {
            println("Showing group info")
            SINGLE_SUCCESS
        }
    }

    literal("list") {
        execute {
            println("Listing groups")
            SINGLE_SUCCESS
        }
    }

    literal("create") {
        execute {
            println("Creating group")
            SINGLE_SUCCESS
        }
    }
}

Benefits:

  • Indentation reflects tree hierarchy
  • No .then() chains
  • Clear parent-child relationships
  • Easy to navigate and modify

Arguments

command<CommandSource>("user") {

    argument("name", StringArgumentType.word()) {
        execute {
            val name = arg<String>("name")
            println("User: $name")
            SINGLE_SUCCESS
        }
    }
}

What This Does:

  • Registers /user <name> command
  • Parses name argument
  • Provides type-safe access in execution block

Type-Safe Argument Handling

Basic Argument Access

Instead of Brigadier's verbose approach:

ctx.getArgument("name", String.class)

Use brigadier-kt's type-safe methods:

arg<String>("name")         // Required argument
optionalArg<String>("name") // Optional argument

Benefits:

  • No explicit .class parameter
  • Leverages Kotlin's reified generics
  • Cleaner, more maintainable code
  • Compile-time type safety

Argument References

The Problem

String-based argument access becomes fragile and error-prone in large command trees:

// Repeated throughout multiple handlers
arg<String>("group")
arg<String>("group")
arg<String>("group")

Issues:

  • Typos cause runtime errors
  • No IDE refactoring support
  • Hard to track usage
  • Difficult to maintain

The Solution: KtArgumentRef

Bind once, reuse everywhere with strong typing. brigadier-kt provides two ways to create argument references:


argRef() — Reference to the Current Argument

argRef() creates a reference bound to the argument at the current DSL scope. The name and type are inferred automatically from context, so no parameters are required.

argument("group", StringArgumentType.word()) {
    val groupRawRef = argRef() // Bound to "group", type String

    literal("info") {
        execute {
            val name = groupRawRef.get() // Returns String
            println("Group: $name")
            SINGLE_SUCCESS
        }
    }
}

Use argRef() when you want a reference to the raw parsed value of the current argument node.


createArgRef<T>(name) — Named Reference with Explicit Type

createArgRef<T>(name) creates a reference to any argument by name, with an explicitly specified type. This is particularly useful when a Guard will transform the argument into a domain object — the ref can be typed to the target type rather than the raw parsed type.

argument("group", StringArgumentType.word()) {
    val groupRef = createArgRef<Group>("group") // Typed to Group, not String

    guard {
        val group = groupService.find(arg<String>("group"))
        groupRef.set(group) // Inject the domain object
        continueCommand()
    }

    literal("info") {
        execute {
            val group = groupRef.get() // Returns Group directly
            println("Group: ${group.name}")
            SINGLE_SUCCESS
        }
    }
}

Use createArgRef<T>(name) when:

  • You need a reference to an argument that will be transformed by a Guard
  • The reference type differs from the originally parsed type
  • You want strongly-typed access to a named argument from any scope

Setting Values via References

A reference created with createArgRef is mutable — you can write to it using ref.set(value), which is equivalent to calling setArgument(name, value) directly on the context. This pairs naturally with Guards to inject transformed domain objects.

guard {
    val group = groupService.find(arg<String>("group"))
    groupRef.set(group)       // via reference
    // equivalent to:
    setArgument("group", group) // via context directly
    continueCommand()
}

Both approaches produce the same result. Prefer ref.set(...) when you have already declared a createArgRef for the argument, as it avoids repeating the string key and keeps the type contract explicit.


Comparison

Feature argRef() createArgRef<T>(name)
Bound to current argument node ✗ (explicit name)
Type inferred from context ✓ (raw parsed type) ✗ (you specify type)
Supports transformed types
Mutable (ref.set(...))
Use case Raw parsed value access Domain object after transform

Full Example

argument("group", StringArgumentType.word()) {
    val groupRawRef = argRef()              // String reference (raw parsed)
    val groupRef = createArgRef<Group>("group") // Group reference (post-transform)

    guard {
        val name = groupRawRef.get()
        val group = groupService.find(name)

        if (group == null) {
            println("Group not found: $name")
            return@guard abort(NO_SUCCESS)
        }

        groupRef.set(group) // Inject transformed domain object
        continueCommand()
    }

    literal("info") {
        execute {
            val group = groupRef.get() // Receives Group object
            println("Group info: ${group.name} (${group.members.size} members)")
            SINGLE_SUCCESS
        }
    }

    literal("delete") {
        execute {
            val group = groupRef.get()
            groupService.delete(group)
            println("Deleted group: ${group.name}")
            SINGLE_SUCCESS
        }
    }

    literal("members") {
        literal("list") {
            execute {
                val group = groupRef.get()
                println("Members: ${group.members.joinToString()}")
                SINGLE_SUCCESS
            }
        }
    }
}

Why This Matters:

  • Strong Typing — Compile-time safety for both raw and transformed types
  • No String Keys — Eliminate magic strings in execute blocks
  • Refactoring Support — IDE can track all usages of a reference
  • Guard IntegrationcreateArgRef and ref.set work naturally in the middleware pipeline

Guards: The Middleware System

Guards are one of the core architectural features of brigadier-kt. They introduce a structured middleware pipeline between parsing and execution, enabling centralized validation and transformation.

Understanding Guards

The Problem in Pure Brigadier

In vanilla Brigadier:

  • Validation logic must be duplicated in every execute block
  • Argument transformation must be repeated across handlers
  • No mechanism exists for shared subtree validation
  • Business logic and validation become tightly coupled
  • Domain object resolution happens repeatedly

What Guards Provide

Guards create a middleware layer that:

  • Executes After Parsing — Work with validated command structure
  • Executes Before Handlers — Intercept before business logic runs
  • Execute Hierarchically — Run from root → leaf in tree order
  • Can Abort Execution — Prevent handler execution with custom result codes
  • Can Transform Arguments — Convert parsed strings to domain objects
  • Can Inject Arguments — Add new values to context
  • Can Override Values — Replace parsed arguments with transformed ones

Basic Guard Usage

command<CommandSource>("admin") {

    guard {
        if (!source.isAdmin()) {
            println("Access denied")
            abort() // Returns NO_SUCCESS by default
        } else {
            continueCommand()
        }
    }

    literal("reload") {
        execute {
            println("Reloaded")
            SINGLE_SUCCESS
        }
    }

    literal("shutdown") {
        execute {
            println("Shutting down")
            SINGLE_SUCCESS
        }
    }
}

Flow:

  1. Command is parsed
  2. Guard checks if user is admin
  3. If not admin → abort() stops execution
  4. If admin → continueCommand() allows execution to proceed
  5. Execute block runs

Note: For simple permission checks, a requires predicate is often more appropriate as it affects node visibility. Use guards when you need more complex logic or custom error handling.


The runOnSameNode Parameter

Signature

fun <S> KtCommandBuilder<S, *>.guard(
    runOnSameNode: Boolean = true,
    guard: KtCommandGuard<S>
)

Default Behaviour (runOnSameNode = true)

By default, a Guard runs for every execute block in its subtree — including any execute block defined directly on the same node where the Guard is declared.

command<Player>("test") {

    guard { // runOnSameNode = true (default)
        println("[Guard] Running")
        continueCommand()
    }

    literal("hello") {
        execute { /* Guard runs here */ SINGLE_SUCCESS }
    }

    execute { /* Guard also runs here */ SINGLE_SUCCESS }
}

Running /test triggers the Guard before the execute block, just like running /test hello does.

Opt-out: runOnSameNode = false

When runOnSameNode = false, the Guard is skipped when the execute block on the same node is invoked. It still runs normally for all child nodes.

command<Player>("test") {

    guard(false) { // will NOT run when /test itself is executed
        println("[Global Guard] Executing command as ${source.name}")
        GuardResult.Continue
    }

    literal("hello") {
        execute { /* Guard runs here */ SINGLE_SUCCESS }
    }

    argument("level", IntegerArgumentType.integer(0, 100)) {
        execute { /* Guard runs here */ SINGLE_SUCCESS }
    }

    execute {
        // Guard does NOT run here
        SINGLE_SUCCESS
    }
}

Execution behaviour:

Command Guard runs? Reason
/test No execute is on the same node; runOnSameNode = false
/test hello Yes Child node — runOnSameNode does not apply
/test 42 Yes Child node — runOnSameNode does not apply

When to Use runOnSameNode = false

This option is useful when a Guard represents logic that is only meaningful for subcommands — for example, logging the specific subcommand used, resolving a resource that only subcommands need, or enforcing a requirement that does not apply to the base command itself.

command<Player>("stats") {

    // Log which subcommand the player is using,
    // but don't interfere when /stats is called with no arguments.
    guard(false) {
        println("Player ${source.name} used a stats subcommand")
        continueCommand()
    }

    literal("kills") {
        execute { /* Guard runs */ SINGLE_SUCCESS }
    }

    literal("deaths") {
        execute { /* Guard runs */ SINGLE_SUCCESS }
    }

    execute {
        // Guard does NOT run - shows summary without logging
        println("Stats overview for ${source.name}")
        SINGLE_SUCCESS
    }
}

Argument Transformation

This is where Guards become particularly powerful.

Example Scenario: Group Resolution

Command Structure:

/group <group> info
/group <group> delete
/group <group> members add <user>
/group <group> members remove <user>

Requirements:

  • Validate that <group> exists
  • Convert string → Group domain object
  • Inject the resolved Group into execution context
  • Avoid duplicate resolution in every handler
  • Handle "not found" cases consistently

Without Guards (Repetitive)

Every execute block must duplicate the resolution logic:

argument("group", StringArgumentType.word()) {

    literal("info") {
        execute {
            val name = arg<String>("group")
            val group = groupService.find(name)
            if (group == null) {
                println("Group not found: $name")
                return@execute NO_SUCCESS
            }
            println("Group info: $group")
            SINGLE_SUCCESS
        }
    }

    literal("delete") {
        execute {
            val name = arg<String>("group")
            val group = groupService.find(name)
            if (group == null) {
                println("Group not found: $name")
                return@execute NO_SUCCESS
            }
            println("Deleting group: $group")
            SINGLE_SUCCESS
        }
    }

    // and so on...
}

Problems:

  • Resolution logic duplicated in every handler
  • Inconsistent error messages
  • Hard to modify validation rules
  • Performance overhead from repeated lookups
  • Business logic cluttered with validation

With Guards (Centralized)

argument("group", StringArgumentType.word()) {
    val groupRawRef = argRef()
    val groupRef = createArgRef<Group>("group")

    guard {
        val name = groupRawRef.get()
        val group = groupService.find(name)

        if (group == null) {
            println("Group not found: $name")
            return@guard abort(NO_SUCCESS)
        }

        // Transform string to domain object — both approaches are equivalent
        setArgument("group", group)
        // or: groupRef.set(group)
        continueCommand()
    }

    literal("info") {
        execute {
            val group = groupRef.get() // Receives Group object
            println("Group info: ${group.name} (${group.members.size} members)")
            SINGLE_SUCCESS
        }
    }

    literal("delete") {
        execute {
            val group = groupRef.get()
            groupService.delete(group)
            println("Deleted group: ${group.name}")
            SINGLE_SUCCESS
        }
    }

    literal("members") {
        // Nested commands also receive the transformed Group
        literal("list") {
            execute {
                val group = groupRef.get()
                println("Members: ${group.members.joinToString()}")
                SINGLE_SUCCESS
            }
        }
    }
}

Benefits:

  • ✅ Resolution happens exactly once
  • ✅ Validation is centralized and consistent
  • ✅ Execution logic works with domain objects
  • ✅ Business logic is clean and focused
  • ✅ Error handling is unified
  • ✅ Performance improved (single lookup)
  • ✅ Easy to modify validation rules

Disabling Guards

Now consider extending the existing structure with a new subcommand:

/group <group> create

This command should create a new group using the provided name.


The Problem

Because the <group> argument already has a Guard attached (which:

  • resolves the String to a Group
  • aborts if the group does not exist
  • injects the resolved Group object

), that Guard will also execute for create.

If we natively add:

literal("create") {
    execute {
        val name = arg<String>("group")
        groupService.create(name)
        println("Created group: $name")
        SINGLE_SUCCESS
    }
}

the Guard will run first.

Since the group does not exist yet, the Guard will:

println("Group not found: $name")
abort(NO_SUCCESS)

Execution never reaches the create block.


The Solution: Disable Guards for This Subtree

brigadier-kt allows you to explicitly disable the Guard pipeline for a specific execution block:

literal("create") {
    execute(false) {
        val name = arg<String>("group")
        groupService.create(name)
        println("Created group: $name")
        SINGLE_SUCCESS
    }
}

Passing false to execute(...) means:

  • Parent Guards will not run
  • No validation is performed
  • No argument transformation occurs
  • Execution starts immediately after parsing

This isolates the create command from the existing middleware logic.


Important Consequence

Because the Guard is skipped:

  • The String → Group transformation does not happen
  • No injected Group object is available
  • Only the originally parsed value exists

Therefore you must access the raw argument:

val name = arg<String>("group")
// or: val name = groupRawRef.get()

You cannot do:

groupRef.get()       // throws — no Group was injected
arg<Group>("group")  // throws — same reason

Because no Group was injected into the context.


When to Disable Guards

Disabling Guards is appropriate when:

  • A command intentionally contradicts parent validation
  • A creation/bootstrap command must bypass existence checks
  • You need raw parsed values
  • You want full manual control over execution

Mutable Command Context

Guards operate on KtCommandContext, which extends Brigadier's CommandContext with safe mutation capabilities.

Available Operations

// Override or inject an argument (by key)
setArgument(name: String, value: Any)

// Override or inject an argument via a typed reference
ref.set(value)

// Remove an argument completely (further accessing will throw an exception)
removeArgument(name: String)

// Reset argument to original parsed value
resetArgument(name: String)

setArgument vs ref.set

Both setArgument(name, value) and ref.set(value) write to the same underlying mutable context and produce identical results. The difference is stylistic:

Approach When to use
setArgument(name, value) Quick one-off injection, no prior createArgRef declared
ref.set(value) You already have a createArgRef — keeps the key out of strings

Prefer ref.set(...) in transformation-heavy Guards where a createArgRef is already in scope, as it ties the write directly to the typed reference and avoids repeating magic strings.

Precedence Rules

Overridden arguments always take precedence over parsed ones:

guard {
    val raw = arg<String>("count")  // "5"
    val parsed = raw.toInt()        // 5

    setArgument("count", parsed)    // Override with Integer
    continueCommand()
}

execute {
    val count = arg<Int>("count")   // Receives Integer, not String
}

This enables safe, predictable transformation pipelines. Of course, you should use IntegerArgumentType instead of StringArgumentType for numeric arguments.


Redirect, Fork and Forward

Brigadier's node system supports more than simple linear command trees. Using redirects and forks, a node can point execution to an entirely different part of the tree — enabling patterns like looping chains, context modifiers, and conditional branching. brigadier-kt exposes all three of Brigadier's routing mechanisms as clean DSL functions.

Understanding Redirects

In a standard command tree, parsing always moves forward: each node consumes a token and descends into a child. A redirect breaks this rule — after a node is parsed, instead of looking for children on that node, Brigadier jumps to the children of a completely different target node and continues parsing from there.

This is how Minecraft's /execute as <target> run <command> works: after resolving <target>, execution doesn't stop — it loops back to the execute node's children so more subcommands can follow.

There are three variants:

  • redirect — Routes to a target node, optionally transforming the source (1 source → 1 source)
  • fork — Routes to a target node, producing multiple sources (1 source → n sources)
  • forward — Low-level combined routing with explicit fork control

Node Merging: The Self-Reference Pattern

A redirect needs a reference to a CommandNode as its target. This is straightforward when redirecting to an unrelated node. However, a common and important pattern requires a node to redirect back to itself — for example, so that after processing one modifier, the full modifier chain can be used again.

The challenge is that a node does not exist yet while its DSL block is being defined. brigadier-kt solves this using Brigadier's own node merging behavior.

When dispatcher.register(node) is called, the following happens internally:

// CommandDispatcher.register:
public LiteralCommandNode<S> register(final LiteralArgumentBuilder<S> command) {
    final LiteralCommandNode<S> build = command.build();
    root.addChild(build);
    return build;
}

// CommandNode.addChild — merges if a node with the same name already exists:
final CommandNode<S> child = children.get(node.getName());
if (child != null) {
    // Merge: adopt all children from the new node onto the existing one
    for (final CommandNode<S> grandchild : node.getChildren()) {
        child.addChild(grandchild);
    }
} else {
    children.put(node.getName(), node);
}
// some code lines where removed

This means you can register the same command name twice. The first registration returns the node reference immediately; the second registration merges its children onto the already-existing node. The reference obtained from the first registration stays valid and can safely be used as a redirect target inside the second registration.

Pattern:

// Step 1: Register an empty shell to obtain the node reference
val executeNode = dispatcher.register(
    command<CommandSourceStack>("execute") {
        requires { hasPermission(LEVEL_GAMEMASTERS) }
        // if you want to use requires, 
        // you MUST put it here in the first registration.
        // If you put it in the second registration, 
        // it will not be merged and so no permission check will be performed.
    }
)

// Step 2: Register the full tree — children are merged onto the existing node.
// executeNode is now a valid redirect target.
dispatcher.register(
    command<CommandSourceStack>("execute") {
        requires { hasPermission(LEVEL_GAMEMASTERS) }

        literal("run") {
            redirect(dispatcher.root) // redirect to the dispatcher root
        }

        literal("as") {
            argument("targets", EntityArgument.entities()) {
                fork(executeNode) {          // redirect back to execute's children
                    val targets = ...
                    targets.map { context.source.withEntity(it) }
                }
            }
        }
    }
)

This is the same technique Minecraft itself uses — the first dispatcher.register(Commands.literal("execute")...) call in ExecuteCommand.register exists solely to capture the node reference before the full tree is built.


redirect

redirect routes execution to a target node after the current node is parsed. It accepts an optional SingleRedirectModifier — a lambda that receives the current context and returns a single transformed source. If no modifier is given, the source is forwarded unchanged.

fun <S> KtCommandBuilder<S, *>.redirect(target: CommandNode<S>, modifier: SingleRedirectModifier<S>? = null)

Simple Redirect — No Transformation

The most basic use: after this node is parsed, continue with the children of target using the same source.

literal("run") {
    redirect(dispatcher.root) // /execute run <any command>
}

Redirect with Source Transformation

The modifier receives the current CommandContext and returns a new source. Parsing then continues at target using that new source. This is how context modifiers like positioned, rotated, or in work.

literal("positioned") {
    argument("pos", Vec3Argument.vec3()) {
        redirect(executeNode) { context ->
            context.source
                .withPosition(/*new pos*/)
        }
    }
}

literal("in") {
    argument("dimension", DimensionArgument.dimension()) {
        redirect(executeNode) { context ->
            context.source.withLevel(/*new level*/)
        }
    }
}

After the argument is parsed, the source is replaced with a new source pointing to the new position or dimension — then parsing loops back to executeNode, allowing further modifiers or a final run to follow.


fork

fork routes execution to a target node, but unlike redirect it can produce multiple sources from a single incoming source. Brigadier runs the remaining command tree once for each source in the returned collection.

fun <S> KtCommandBuilder<S, *>.fork(target: CommandNode<S>, modifier: RedirectModifier<S>)

This is how /execute as <targets> works: one command execution fans out into one execution per matched entity.

Fork — One Source to Many

literal("as") {
    argument("targets", EntityArgument.entities()) {
        fork(executeNode) { context ->
            val targets = ...
            targets.map { context.source.withEntity(it) }
        }
    }
}

literal("at") {
    argument("targets", EntityArgument.entities()) {
        fork(executeNode) { context ->
            val targets = ...
            targets.map { context.source.withPosition(it.position()) }
        }
    }
}

If the modifier returns an empty list, execution is silently canceled — no children of the target node are visited. This is how fork-based conditionals work: returning the source continues, returning empty aborts.

Fork — Conditional Branching

Returning the source or an empty list makes fork act as a conditional gate:

literal("if") {
    literal("entity") {
        argument("entities", EntityArgument.entities()) {
            fork(executeNode) { context ->
                val target = ...
                val condition = ...
                if (condition.matches(target)) {
                    listOf(context.source.withEntity(target))
                } else {
                    emptyList()
                }
            }
        }
    }
}

forward

forward is the low-level primitive that both redirect and fork build on. It accepts a RedirectModifier and an explicit fork boolean that controls how Brigadier treats the result.

fun <S> KtCommandBuilder<S, *>.forward(target: CommandNode<S>, fork: Boolean, modifier: RedirectModifier<S>)
fork value Behaviour
false the returned int is the sum of all the commands that have been executed, also if an exception is thrown it will not be catched
true the returned int is the cound of how many commands have been executed successfully, exceptions will be ignored

In most cases redirect and fork are the right choice. Use forward only when you need precise control over the fork flag that the higher-level functions don't expose — for example when wrapping a custom RedirectModifier implementation.

argument("targets", EntityArgument.entities()) {
    forward(
        target = executeNode,
        fork = true
    ) { context ->
        listOf(...)
    }
}

Important Note on Redirect Execution Context

When using redirects that loop back to a parent node (such as the common self-reference pattern), it is important to understand where execution actually occurs.

For example, consider a command like:

game select @a

If select uses a redirect (or fork) that routes execution back to the game node after resolving @a, the execution context changes:

  • After @a is parsed, Brigadier continues execution from the redirected node (game) using the modified source(s).
  • However, the execution node is considered to be the last successfully parsed node before the redirect completes in this case, the @a argument node.

Key consequence:

If you run:

game select @a
  • The execute block defined under game will NOT be executed.
  • Instead, the execute block (if any) associated with the @a argument node is used.

This happens because Brigadier executes the command at the final node reached during parsing, and a redirect does not "rewind" execution to the parent node — it only changes where parsing continues.

In short:

  • Redirects affect parsing flow, not execution ownership.
  • The command is executed at the last parsed node, not necessarily the node you redirected to.

This distinction is crucial when designing command trees that rely on chaining, looping, or context modifiers.


Guards and Redirects

Guards and redirects operate at different phases of Brigadier's execution model, and their interaction has important consequences.

Guards do NOT run at the redirect node

When a node redirects to a target, Guards attached to that redirect node are not executed. Execution arrives at the redirect node during parsing and immediately jumps to the target — there is no execution phase at the redirect node itself where Guards could run.

Guards only run during the execution phase, which begins at the node where the final command handler lives.

val executeNode = dispatcher.register(
    command<CommandSourceStack>("execute") {
        requires { hasPermission(LEVEL_GAMEMASTERS) }
    }
)

dispatcher.register(
    command<CommandSourceStack>("execute") {
        requires { hasPermission(LEVEL_GAMEMASTERS) }

        literal("as") {
            argument("targets", EntityArgument.entities()) {

                // A guard placed here will NEVER run —
                // this node immediately redirects and has no execute handler.
                guard {
                    continueCommand()
                }

                fork(executeNode) { context ->
                    ...
                }
            }
        }

        literal("run") {
            redirect(dispatcher.root)
        }

        // Guards placed here DO run — this is the execution entry point
        // when the chain eventually reaches a concrete command after "run".
    }
)

Guards DO run at the redirect target

When execution reaches the target of a redirect through normal command dispatch (i.e. when a command is typed that resolves through that node), Guards on the target node and its ancestors run as usual.

In the pattern above, after /execute as @a run say hello is fully parsed and say hello is dispatched, the Guards on the say node run normally. The redirect only affected how the source was modified during parsing — not whether Guards on the final command run.

Summary:

Location Guards run?
Node that calls redirect(...) or fork(...) ✗ Never — no execution phase here
Target node of the redirect ✓ Yes — when reached via normal dispatch
Nodes after run / dispatcher root ✓ Yes — normal execution flow

This means Guards are the right place to put validation that applies to the final command being run, not to the modifier chain itself. Modifier validation (e.g. "does this entity exist?") belongs in the fork or redirect lambda directly.


Routing: Context-Aware Execution Without Modifying S

Routing is a brigadier-kt feature that allows you to change execution context without changing the command source (S).

In vanilla Brigadier (and Minecraft), context changes like /execute as, /execute at, or /execute in work because Minecraft fully controls the source type and can create modified copies of it.

In most real environments (Paper, Fabric, custom systems), this is not possible:

  • You do not own S
  • You cannot safely mutate or extend it
  • You cannot attach additional execution state to it

This creates a limitation:

You cannot carry custom context through redirects.

Routing solves exactly this problem.


The Core Idea

Routing allows you to:

  • redirect execution (like normal Brigadier)
  • attach additional typed context during that redirect

This context is stored separately from S and can be accessed later during execution.


Example: BedWars Command Context

The Problem

You have a command like:

/bedwars start
/bedwars invite <player>

These commands operate on:

the game the sender is currently in

So internally, you might do:

val game = GameService.getGameFor(source)

The Limitation

What if you want to execute a command for a different game?

Without routing, your only option would be:

/bedwars start <game>
/bedwars invite <player> <game>

Problems:

  • Argument duplication everywhere
  • Polluted command signatures
  • Repeated resolution logic
  • Worse UX

The Routing Solution

Instead of adding arguments everywhere, you introduce a routing entry point:

/bedwars game <code> start
/bedwars game <code> invite <player>

Here’s the important part:

  • game <code> redirects back to the base command
  • but injects a new execution context

How It Works

1. Define a routing key
val GAME_KEY = createDynamicRoutingKey<Player, String>("game") { ctx ->
    GameService.getGameFor(ctx.source)
}

This defines:

  • a key (GAME_KEY)
  • a type (String)
  • a fallback → “player's current game”

2. Use routing to override it
literal("game") {
    argument("code", StringArgumentType.word()) {

        routing(baseNode) {
            set(GAME_KEY, context.arg("code"))
        }
    }
}

This does two things:

  1. Redirects execution back to /bedwars
  2. Overrides GAME_KEY with the provided code

3. Use the context in commands
literal("start") {
    execute {
        val game = context(GAME_KEY)
        GameService.startGame(game)
        SINGLE_SUCCESS
    }
}

What Happens at Runtime

Without routing:
/bedwars start

context(GAME_KEY) → fallback → player's current game


With routing:
/bedwars game ABCD start

Flow:

  1. game ABCD is parsed

  2. Routing runs:

    GAME_KEY = "ABCD"
    
  3. Execution redirects to /bedwars

  4. start executes

  5. context(GAME_KEY) now returns "ABCD"


Key Insight

Commands like start and invite do not know where the game came from.

They simply do:

context(GAME_KEY)

Routing decides whether that value comes from:

  • the fallback (player’s current game)
  • or an overridden value (via /game <code>)

Second Use Case: Flags

Routing is not limited to complex objects — it also works for simple flags.

Example: Command Flags

Instead of:

/delete <file> --force

You can model this as:

/delete force <file>

Implementation

Define a key:

val FORCE_KEY = createStaticRoutingKey<CommandSource, Boolean>("force", false)

Add a routing node:

literal("force") {
    routing(baseNode) {
        set(FORCE_KEY, true)
    }
}

Usage

execute {
    val force = context(FORCE_KEY)
    if (force) {
        println("Force enabled")
    }
}

Result

/delete file.txt        → force = false
/delete force file.txt  → force = true

No extra arguments needed. No parsing duplication.


Mental Model

Routing extends the execution context:

CommandContext
 ├── source (S)
 ├── parsed arguments
 └── routing values (KtRoutingKey → value)
  • S stays untouched
  • routing values carry additional state
  • values can be overridden during redirects

Limitations of Routing

Routing is designed to work with a stable execution context. Because routing values are stored separately from the command source (S), there is an important constraint:

After a routing step, the number of sources must remain unchanged.

This means:

  • Redirects are safe: they preserve the same number of sources
  • Operations that change the number of sources are not supported after routing

Examples of problematic operations:

  • forks (multiple execution paths with different sources)
  • any mechanism that expands or shrinks the source set

These can lead to undefined or unpredictable behavior, because routing assumes a single, consistent execution context.


What is supported

You can freely use forks and similar mechanics before the first routing node.

Example:

/execute as @a run bedwars game ABCD start

This works because:

  1. /execute as @a creates multiple sources
  2. Each source is then handled independently
  3. Routing is applied afterward within each stable execution context

This pattern is fully supported and safe.


Future Outlook

Support for forks and other source-altering operations after routing may be added in the future, but is currently not implemented.


Summary of Routing

Routing exists because:

In most environments, you cannot modify the command source — but you still need dynamic execution context.

It allows you to:

  • inject context during redirects
  • override fallback values dynamically
  • avoid argument duplication
  • build clean, composable command systems

Typical use cases:

  • game/session context (/bedwars game <code>)
  • flags (force, recursive, etc.)
  • execution modifiers (similar to /execute)

In short:

Redirect controls the flow. Routing controls the context.


Advanced Features

Suggestions DSL

Brigadier suggestions normally require manual CompletableFuture<Suggestions> handling. brigadier-kt simplifies this dramatically.

Simple Suggestions

argument("color", StringArgumentType.word()) {
    suggests {
        suggest("red")
        suggest("green")
        suggest("blue")
    }
}

Notes:

  • suggests is called every time the argument needs completion
  • You can use dynamic values based on current context
  • Access CommandContext via context property
  • Access native SuggestionsBuilder via builder property
  • Guards do not run during suggestion — only during execution

Suggestions with Tooltips

argument("permission", StringArgumentType.word()) {
    suggests {
        suggest("admin", LiteralMessage("Full administrative access"))
        suggest("moderator", LiteralMessage("Moderation permissions"))
        suggest("user", LiteralMessage("Standard user permissions"))
    }
}

Dynamic Suggestions

argument("player", StringArgumentType.word()) {
    suggests {
        val online = server.getOnlinePlayers()
        online.forEach { player ->
            suggest(player.name)
        }
    }
}

Full Control Override

For advanced cases where you need complete control:

argument("custom", StringArgumentType.word()) {
    suggestsOverride { builder ->
        // Full CompletableFuture access
        builder.suggest("dynamicValue")
        builder.buildFuture()
    }
}

Access Control: requires vs Guards

These two features operate at different architectural layers and serve complementary purposes.

requires (Brigadier Native)

Purpose: Control structural accessibility during parsing

Characteristics:

  • ✅ Runs during command parsing phase
  • ✅ Controls node visibility
  • ✅ Affects command tree structure
  • ✅ Fast (pre-execution)
  • ❌ Cannot access arguments
  • ❌ Cannot modify context
  • ❌ Cannot return custom result codes
  • ❌ Boolean only (allow/deny)

Example:

command<CommandSource>("admin") {
    requires { hasPermission("admin") }

    literal("reload") {
        execute {
            println("Reloading...")
            SINGLE_SUCCESS
        }
    }
}

When to Use:

  • Simple permission checks
  • Static access control
  • When node visibility matters
  • When you need Brigadier's built-in permission system

Guards (brigadier-kt Feature)

Purpose: Validate, transform, and prepare for execution

Characteristics:

  • ✅ Runs after successful parsing
  • ✅ Full argument access
  • ✅ Can mutate context
  • ✅ Can inject new arguments
  • ✅ Can transform arguments
  • ✅ Can abort with custom result codes
  • ✅ Shared subtree validation logic
  • ✅ Domain object transformation
  • ❌ Does not affect node visibility
  • ❌ Runs after parsing

Example:

argument("group", StringArgumentType.word()) {
    val groupRef = createArgRef<Group>("group")

    guard {
        val name = arg<String>("group")
        val group = groupService.find(name)

        if (group == null) {
            println("Group not found: $name")
            return@guard abort()
        }

        if (!group.hasPermission(source, "view")) {
            println("Access denied")
            return@guard abort(NO_SUCCESS)
        }

        groupRef.set(group)
        continueCommand()
    }

    // Handlers receive transformed Group object via groupRef.get()
}

When to Use:

  • Argument validation
  • Domain object resolution
  • Complex permission checks requiring context
  • Argument transformation
  • Shared validation across subtree
  • Custom error handling

Comparison Table

Feature requires Guard
Parsing phase
Post-parse validation
Access arguments
Modify context
Argument transformation
Argument injection
Custom result codes
Shared subtree logic
Affects node visibility
Domain object resolution

Using Both Together

command<CommandSource>("group-admin") {
    requires { hasPermission("admin.access") }  // Node-level access control

    argument("group", StringArgumentType.word()) {
        val groupRef = createArgRef<Group>("group")

        guard {
            val name = arg<String>("group")
            val group = groupService.find(name)

            if (group == null) {
                println("Group not found: $name")
                return@guard abort(NO_SUCCESS)
            }

            // Check if admin has permission for THIS specific group
            if (!group.allowsAdmin(source)) {
                println("Access denied")
                return@guard abort(NO_SUCCESS)
            }

            groupRef.set(group)
            continueCommand()
        }

        literal("delete") {
            requires { hasPermission("admin.group.delete") }  // More specific permission
            execute {
                val group = groupRef.get()
                groupService.delete(group)
                SINGLE_SUCCESS
            }
        }
    }
}

Summary: They complement each other, not replace each other. Use requires for static access control and node visibility, use Guards for dynamic validation and transformation.


Architecture

Execution Lifecycle

When a command is executed, the following happens in order:

1. User Input
   ↓
2. Brigadier Parsing
   ↓
3. requires Predicates (if any)
   ↓
4. Redirect / Fork resolution (if any)
   │  Guards do NOT run here
   ↓
5. Guards Execute (root → leaf order)
   │  runOnSameNode = false guards are skipped
   │  if the execute block is on their own node
   ↓
   ├─ Guard 1 at root
   ├─ Guard 2 at intermediate node
   └─ Guard 3 at leaf node
   ↓
6. All Guards returned GuardResult.Continue?
   ├─ YES → Execute Handler
   └─ NO  → Stop (return Guard's abort result)
   ↓
7. Return Result

Clear Separation of Concerns:

  • Parsing → Brigadier handles syntax and structure
  • Access Controlrequires handles node-level permissions
  • Routingredirect, fork, forward handle execution flow
  • Validation & Transformation → Guards handle business validation
  • Business Logicexecute handles core functionality

Design Principles

brigadier-kt is built on strict architectural decisions:

  1. Non-Invasive Architecture

    • Brigadier remains completely untouched
    • No modifications to core parsing logic
    • Full backward compatibility
  2. Middleware-First Execution Model

    • Guards create a structured pipeline
    • Clear separation between validation and execution
    • Predictable execution order
  3. Explicit Control Flow

    • All Guard results are explicit (abort() or continueCommand())
    • No implicit behavior or magic
    • Clear intent in code
  4. Domain-Driven Transformation

    • Support converting parsed primitives to domain objects
    • Centralized transformation logic
    • Type-safe argument access
  5. Kotlin-Native DSL

    • Idiomatic Kotlin syntax
    • Leverage language features (reified generics, lambdas, etc.)
    • Clean, readable command definitions
  6. No Hidden Magic

    • All behavior is explicit
    • No reflection-based surprises
    • What you see is what you get
  7. Full Brigadier Compatibility

    • Works with existing Brigadier code
    • Can be adopted incrementally
    • Integrates with Brigadier ecosystem

Summary

brigadier-kt transforms Brigadier from a low-level parsing API into a structured, middleware-enabled command framework that embraces Kotlin idioms while preserving Brigadier's power and performance.

Key Benefits

Clean DSL Structure — Command trees that match your mental model
Type-Safe Arguments — Reified generics eliminate boilerplate
Argument ReferencesargRef() for raw access, createArgRef<T>() for typed domain objects
Middleware Guards — Centralized validation and transformation
runOnSameNode Control — Fine-grained Guard scoping per node
Domain Transformation — Work with domain objects, not primitives
Mutable Context — Safe argument injection via setArgument or ref.set
Redirect / Fork / Forward — Full Brigadier routing support with clean DSL
Node Merging — Self-reference pattern for looping command chains
Suggestions DSL — Simplified completion handling
Full Compatibility — Drop-in enhancement for Brigadier


Author: Fantamomo
License: MIT

About

Kotlin-first DSL for Mojang Brigadier commands with type-safe arguments, middleware Guards, and structured execution.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages