Skip to content

Commands

Lesson 13.1

Commands like /rubies or /home set give players a direct way to use your mod. CLight builds them from small pieces, so you can describe the whole command in one expression.

A first command

cl.on("server.command_register", function()
  cl.cmd.register(
    cl.cmd.literal("hello"):executes(function(ctx)
      ctx:success("Hello there!")
      return 1
    end)
  )
end)

Now typing /hello in chat answers “Hello there!”.

  • Commands are registered in the server.command_register event. It runs when a world opens and again after /reload, and it is replayed when your script hot reloads.
  • cl.cmd.literal("hello") is the command’s name, without the /.
  • :executes(fn) is the code that runs. It gets a command context, ctx.
  • The return value is the command’s result: a positive number for success. return 1 is normal. If you return nothing, it counts as 1.

The command context

Method Does
ctx:player() The player who typed it, or nil for the server console and command blocks.
ctx:is_player() true if a player typed it.
ctx:source_name() The player’s name, or "Server".
ctx:success(text) Sends a reply.
ctx:failure(text) Sends an error reply, in red.
ctx:arg(name) The value of an argument (below).
ctx:entities(name) The entities of a player or entity argument, always as a list (below).
ctx:args() Everything typed after the command name, as one string.
ctx:permission_level() The permission level of whoever typed it: 0 for normal players, 2 to 4 for operators, 4 for the console.

Commands can come from the console, so check for a player before you use one:

cl.cmd.literal("heal"):executes(function(ctx)
  local p = ctx:player()
  if not p then
    ctx:failure("Only players can be healed.")
    return 0
  end
  p:set_health(20)
  ctx:success("Healed.")
end)

Returning 0 or a negative number tells the game the command failed.

Subcommands

:then_(child) adds a part after the name. (It is then_ with an underscore, because then is a Lua keyword.)

cl.cmd.register(
  cl.cmd.literal("home")
    :then_(cl.cmd.literal("set"):executes(function(ctx)
      ctx:success("Home set.")
    end))
    :then_(cl.cmd.literal("go"):executes(function(ctx)
      ctx:success("Going home.")
    end))
)

This makes /home set and /home go. Every method returns the node again, so the calls chain. /home on its own is incomplete here, because the home node has no executes; the player gets “Unknown or incomplete command”. Give it an executes too if /home alone should do something.

Arguments

cl.cmd.argument(name, type) is a part the player fills in:

cl.cmd.register(
  cl.cmd.literal("rubies")
    :then_(cl.cmd.argument("amount", "integer"):executes(function(ctx)
      local n = ctx:arg("amount")
      ctx:player():give("my_mod:ruby", n)
      ctx:success("Here are " .. n .. " rubies.")
    end))
)

/rubies 5 gives five. /rubies five doesn’t match an integer, so the player gets an error message instead.

Type Matches ctx:arg gives
"string" (the default) One word, or several words in quotes: "Ruby Hills" a string, without the quotes
"word" One word a string
"integer" A whole number, like 5 or -2 an integer
"number" A number, like 2.5 a number
"bool" true or false a boolean
"greedy" Everything up to the end of the line a string
"player" One player: a name or a selector like @p the player
"players" Players: a name, @a, @a[distance=..10] … a list of players
"entity" One entity: a name, a UUID or a selector like @e[type=cow,limit=1,sort=nearest] the entity
"entities" Entities: @e[type=zombie], a name … a list of entities

"greedy" must be the last part, because it takes the rest of the line:

cl.cmd.literal("shout"):then_(cl.cmd.argument("text", "greedy"):executes(function(ctx)
  cl.entity.server_broadcast(ctx:source_name() .. " shouts: " .. ctx:arg("text"):upper())
end))

The brigadier names that vanilla uses work as well: "brigadier:integer", "brigadier:double", "brigadier:bool", "brigadier:greedy_string".

Players and entities as arguments

The argument types "player", "players", "entity" and "entities" understand everything vanilla commands do: a player name, a UUID, and the selectors @p, @a, @r, @s, @e with all their options in square brackets. A selector may contain spaces inside its brackets: @e[type=zombie, distance=..10] is still one argument.

cl.cmd.register(
  cl.cmd.literal("rubygift")
    :then_(cl.cmd.argument("target", "player")
      :then_(cl.cmd.argument("amount", "integer"):executes(function(ctx)
        local target = ctx:arg("target")
        target:give("my_mod:ruby", ctx:arg("amount"))
        ctx:success("Gave rubies to " .. target.name)
      end)))
)

/rubygift Alex 3 and /rubygift @p 3 both work. If no player matches, the player gets Minecraft’s own error (“No player was found”) and your code doesn’t run. A "player" or "entity" argument also refuses a selector that finds more than one; "players" and "entities" give a list:

cl.cmd.literal("rubyrain"):then_(cl.cmd.argument("targets", "players"):executes(function(ctx)
  local targets = ctx:arg("targets")
  for _, p in ipairs(targets) do p:give("my_mod:ruby") end
  ctx:success("Rubies for " .. #targets .. " players")
  return #targets
end))

The selector is read from the place and view of whoever runs the command, so @p from a command block is the player nearest to the block.

Who may use it

:requires_permission(...) limits a node to operators. It works on any node: the command’s name or a subcommand.

cl.cmd.register(
  cl.cmd.literal("ruby")
    :executes(function(ctx) ctx:success("Rubies are red.") end)                -- everyone
    :then_(cl.cmd.literal("reset"):requires_permission("my_mod.admin")         -- operators only
      :executes(function(ctx) ctx:success("All rubies reset.") end))
)

Everyone can use /ruby; only operators can use /ruby reset. For everyone else the node doesn’t exist: they get “Unknown or incomplete command”, like for vanilla’s op commands. On the name node it hides the whole command.

You can pass a name like "my_mod.admin" or a vanilla permission level:

Value Who
"my_mod.admin" (any name) Operators (level 2). Name your permissions after your mod; a permissions plugin can map them later.
2 Operators, like /give and /gamemode
3 Operators of level 3, like /ban and /op
4 The server owner, like /stop

In singleplayer you are an operator when cheats are on. ctx:permission_level() tells you the level inside the command, for example to show more details to operators.

Running vanilla commands

cl.cmd.run(command) runs any command as the server, without the /:

cl.cmd.run("weather clear")
cl.cmd.run("time set day")

It returns the command’s result number. That is a handy way to reach game features the API doesn’t cover yet.

Try it

Make a /dice command. /dice rolls a number from 1 to 6. /dice 20 rolls from 1 to 20. Reply with the result, and refuse sides below 2.

Show a solution
local function roll(ctx, sides)
  if sides < 2 then
    ctx:failure("A die needs at least 2 sides.")
    return 0
  end
  local n = math.random(1, sides)
  ctx:success(ctx:source_name() .. " rolled " .. n .. " (1-" .. sides .. ")")
  return n
end

cl.on("server.command_register", function()
  cl.cmd.register(
    cl.cmd.literal("dice")
      :executes(function(ctx) return roll(ctx, 6) end)
      :then_(cl.cmd.argument("sides", "integer"):executes(function(ctx)
        return roll(ctx, ctx:arg("sides"))
      end))
  )
end)

The dice node has its own executes for the plain /dice, and the argument node has another for /dice 20. Both share one helper.

Recap: register commands in server.command_register with cl.cmd.literal, cl.cmd.argument, :then_ and :executes, and answer with ctx:success or ctx:failure. "player" and "entities" arguments take names and selectors, and :requires_permission on any node keeps it for operators.