Skip to content

Your first mod in Lua

Status: Implemented. The complete mod from this page is clight-sdk/examples/ruby_lua.

The same Ruby mod as the C version, as a Lua 5.4 script. There is no build step: the .clmod contains your scripts, and the script runs in a sandboxed VM.

clight new lua ruby_lua --name "Ruby (Lua example)"

Start here: a whole mod in six lines

-- When a player joins, give them a ruby and say hello.
cl.on("player.join", function(event)
  local player = event.player

  player:give("my_mod:ruby")
  player:send_message("Welcome, " .. player.name .. "!")
end)
  • cl.on(name, function(event) ... end) runs your function every time the event happens.
  • event.player is the player. Read player.name, player.uuid, player.pos (.x .y .z), player.health and player.level (also player.world) like fields.
  • player:give("namespace:item", count) gives items (count defaults to 1). send_message and send_action_bar take a plain string.
  • Ids are plain strings everywhere: level:set_block(pos, "minecraft:stone"), event.block == "minecraft:stone".
  • cl.log("Hello", player.name) writes to the game log.

The rest of this page builds the full Ruby example: a block, an item, a sound, a command and a HUD text.

clight.mod.json

The same as the C version, with the id ruby_lua and the runtime:

"runtime": { "type": "lua", "entry": "scripts/main.lua" }

The assets and data (assets/ruby_lua/, datapack/ruby_lua/) are the same as in the C version.

scripts/main.lua

-- Ruby (Lua example): the same mod as the C and C++ examples, as a sandboxed Lua 5.4 script.
-- No build step: `clight run` packs and starts it, and saving this file hot-reloads the handlers.
--
--   - a block (ruby_lua:ruby_block) and an item (ruby_lua:ruby), with models, textures, lang, a loot table and a recipe
--   - an event listener: players get a ruby on join; breaking a ruby block plays the mod's own sound
--   - a command: /rubylua counts the rubies in your inventory
--   - a HUD text: "Rubies: N" in the corner (client)
--   - a custom sound: assets/ruby_lua/sounds.json + sounds/ruby_chime.ogg
--
-- `cl` is the API: every clight.idl function is cl.<module>.<verb>(...) and a method on its first handle
-- (cl_player_give -> player:give, cl_sound_play -> cl.sound.play); enums are lowercase strings. `mod` is this mod.

local RUBY, RUBY_BLOCK = "ruby_lua:ruby", "ruby_lua:ruby_block"

local function count_rubies(player)
  local n = 0
  for slot = 0, 40 do
    local s = player:inventory_get(slot)
    if s and s:is(RUBY) then n = n + s:count() end
  end
  return n
end

-- registration (lifecycle.register)
cl.on("lifecycle.register", function()
  cl.blocks.register{
    id = RUBY_BLOCK,
    copy_properties_from = "minecraft:emerald_block",   -- hardness, sounds, tool
    vanilla_fallback = "minecraft:emerald_block",       -- what vanilla clients see
    item = RUBY_BLOCK,                                  -- also register its block item
  }
  cl.items.register{ id = RUBY, vanilla_fallback = "minecraft:emerald", creative_tab = "minecraft:ingredients" }
end)

-- player.join (server): one ruby and a welcome message (translated: assets/ruby_lua/lang/en_us.json)
cl.on("player.join", function(event)
  local player = event.player
  player:give(RUBY)
  player:send_message(cl.text.translatable("ruby_lua.welcome"))
end)

-- block.break (server): a chime when a ruby block breaks; everyone nearby hears it
cl.on("block.break", function(event)
  if event.block ~= RUBY_BLOCK then return end
  cl.sound.play(event.world, event.pos:center(), "ruby_lua:ruby_chime", "blocks", 1.0, 1.0)
end)

-- /rubylua: "You have N rubies."
cl.on("server.command_register", function()
  cl.cmd.register(cl.cmd.literal("rubylua"):executes(function(c)
    local p = c:player()
    if not p then
      c:failure("Only players have pockets.")
      return -1
    end
    local n = count_rubies(p)
    c:success(string.format("You have %d %s.", n, n == 1 and "ruby" or "rubies"))
    return n
  end))
end)

-- HUD (client): recount once per client tick, draw every frame (no work per frame beyond one text)
local hud_text = nil
cl.on("tick.client_post", function()
  local p = cl.entity.client_player()
  hud_text = p and ("Rubies: " .. count_rubies(p)) or nil
end)
cl.hud.register_layer{
  id = "rubies",
  visible = function() return hud_text ~= nil end,
  render = function(g) g:text(hud_text, 4, 4, 0xFFE0115F, true) end,
}

cl.log("Ruby (Lua)", mod.version, "loaded")

Run

clight build      # validates and packs build/ruby_lua-1.0.0.clmod (no compiling)
clight run

Edit main.lua and save. CLight reloads the script in place within about a second, keeping the world running. Registrations from lifecycle.register can’t change without a restart, but handlers, commands and logic can (Hot reload).

Lua specifics to know now

  • Globals: cl (the API) and mod (your mod: mod.id, mod.version, mod.config, mod.persist). Your top-level locals are private to the file.
  • Modules: require "ruby_lua.util" loads scripts/util.lua. Only your own scripts can be required. There is no io, no package, no bytecode load; os has only time, clock, date and difftime, and debug only traceback.
  • Errors in handlers are caught, logged with a traceback, and that handler call is treated as “no change”. Ten errors within one minute turn the mod off for the session (the Mods screen shows why).
  • Indices: inventory and menu slots are 0-based, matching vanilla slot numbers. Everything else follows Lua convention.
  • Performance: handlers run on the thread of their event under a per-mod lock, and one callback may run for at most 50 ms (limits).

See Lua 5.4 for the full details.