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.playeris the player. Readplayer.name,player.uuid,player.pos(.x .y .z),player.healthandplayer.level(alsoplayer.world) like fields.player:give("namespace:item", count)gives items (count defaults to 1).send_messageandsend_action_bartake 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) andmod(your mod:mod.id,mod.version,mod.config,mod.persist). Your top-levellocals are private to the file. - Modules:
require "ruby_lua.util"loadsscripts/util.lua. Only your own scripts can be required. There is noio, nopackage, no bytecodeload;oshas onlytime,clock,dateanddifftime, anddebugonlytraceback. - 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.