Skip to content

Slash Commands ​

Mods declare command trees against a loader type — never against Brigadier or a version command class. declareCommands runs once, right after onInitialize, and the tree it leaves behind is plain data; the per-version adapter translates it into the game's dispatcher every time the game builds one (server start and every datapack reload). That split is what keeps a mod's commands working across rebuilds of the dispatcher: a mod that registered into the dispatcher itself would lose its commands on the first reload.

declareCommands ​

Make the @Mod class implement OMLCommandProvider:

kotlin
@Mod(id = "my_mod", name = "My Mod", version = "1.0.0")
class MyMod : OMLModInitializer, OMLCommandProvider {

    override fun declareCommands(commands: OMLCommandRegistry) {
        commands.register("mycmd") {
            permissionLevel = 2
            executes { source ->
                source.reply("hello from my_mod, executed by ${source.name}")
            }
            argument("count", OMLArgumentType.INTEGER) {
                executes { source -> source.reply("count=${source.getInt("count")}") }
                subcommand("loud") {
                    executes { source -> source.reply("COUNT=${source.getInt("count")}") }
                }
            }
        }
    }
}

register declares the root literal (/mycmd). subcommand adds a literal child, argument a typed one; executes may sit on any node, including one that has children — /mycmd, /mycmd 5 and /mycmd 5 loud above are three separate handlers. Argument values are read inside the handler by the name given to argument, and only when that argument is on the executed path.

Declaration members ​

MemberMeaning
subcommand(name) { }a literal child: /root <name> …
argument(name, type) { }a typed child: /root <name> <value> …
executes { source -> }runs when this node terminates the command
permissionLevelminimum level for this node and everything below it

permissionLevel defaults to 0 (everyone). Nodes at 0 get no check attached at all, so an unprotected subcommand under a protected parent stays protected by the parent.

Argument types ​

OMLArgumentTypeGame parserValue typeRead with
WORDsingle unquoted wordStringgetString
STRINGquoted or single wordStringgetString
GREEDY_STRINGthe rest of the lineStringgetString
INTEGERwhole numberIntgetInt
FLOATnumber with fractionDoublegetDouble
BOOLEANtrue / falseBooleangetBoolean

Each type has exactly one getter. The getters are unchecked casts, so reaching for the wrong one throws ClassCastException inside your handler — the command bridge catches it and answers the invoker with a red [my_mod] command failed: … while logging the stack trace. Reading a name that is not part of the executed path is the same story (… is not part of the executed path).

The command source ​

Handlers receive one OMLCommandSource per invocation:

MemberMeaning
namethe invoker's display name — Server for the console
hasPlayertrue only when a player ran it (console, function, command block: false)
reply(message)success channel: chat for a player, plain stdout for the console
replyError(message)failure channel: rendered red for a player
getString/getInt/getDouble/getBooleanargument values, by declared name
platformthe raw version source object (CommandSourceStack on 26.x)

platform is the usual escape hatch: reach the player, the level or the permission state the game itself uses when the surface above is not enough.

Permission levels ​

permissionLevel maps onto the game's operator levels, not onto a number OML invents:

ValueMeaning
0everyone
1moderators
2game masters
3admins
4+the server owner

When a declaration stops being a declaration ​

Translation happens at dispatcher-construction time, in two hook points (the client's and the dedicated server's), and every rebuilt dispatcher is registered into exactly once. Two consequences worth knowing:

  • Editing a command tree means a restart. declareCommands ran once, at mod init, and the game keeps using what it was given — a datapack reload rebuilds the dispatcher from the same trees.
  • A malformed declaration is contained. Brigadier rejects an empty name, an illegal character or a duplicate root literal; the bridge logs registering /<name> from mod [<id>] failed and skips that one command, so the other mods' commands still land. Two mods claiming the same root literal: the one declared first wins and the other loses with that log line — pick namespaced roots (my_mod_cool rather than cool) if you expect collisions.

Not covered yet ​

No argument suggestions or tab completion beyond the game's own literal completion, no command aliases, no client-side-only commands. The loader itself registers /oml mods (loaded mods with versions, failed inits in red) and /oml version through this same API, as a working reference beyond the sample mod's /techmod_ping.

Still settling

The command DSL is in the provided, still settling tier: during 0.x its shape may change, with the changes recorded in the release notes. ::

Released under the AGPL-3.0 license.