Project: ruby tools
Lesson 16.3
Time to put it all together. In this lesson you build a small but complete mod from an empty folder: a ruby item and a ruby block with recipes, a gift for new players, a command, a HUD counter and a config file. Every step uses something from an earlier lesson, linked so you can look it up.
Step 1: the project
> clight new lua ruby_tools --name "Ruby Tools"
> cd ruby_tools
Edit clight.mod.json: a short description, your name, and "sync": "required", because the mod adds items that both sides must know (The manifest).
{
"schema": 1,
"id": "ruby_tools",
"version": "1.0.0",
"name": "Ruby Tools",
"description": "Rubies, ruby blocks, a ruby counter on the HUD and a /rubies command.",
"authors": ["Alex"],
"license": "MIT",
"api": 1,
"game": ">=26.3 <26.4",
"side": "both",
"sync": "required",
"runtime": { "type": "lua", "entry": "scripts/main.lua" },
"permissions": []
}
Step 2: assets
The ruby and the block need models, textures and names (Items, Blocks). Draw two 16 x 16 pictures, or take any red texture for now:
assets/ruby_tools/
items/ruby.json
items/ruby_block.json
models/item/ruby.json
models/block/ruby_block.json
blockstates/ruby_block.json
textures/item/ruby.png
textures/block/ruby_block.png
lang/en_us.json
The JSON files are the ones from the Items and Blocks lessons, with my_mod replaced by ruby_tools. The language file:
{
"item.ruby_tools.ruby": "Ruby",
"block.ruby_tools.ruby_block": "Block of Ruby",
"ruby_tools.welcome": "Welcome! Here is a ruby to start with."
}
Step 3: recipes
Two recipes in datapack/ruby_tools/recipe/ (Recipes and data packs). ruby_block.json, nine rubies into a block:
{
"type": "minecraft:crafting_shaped",
"category": "building",
"pattern": ["###", "###", "###"],
"key": { "#": "ruby_tools:ruby" },
"result": { "id": "ruby_tools:ruby_block", "count": 1 }
}
ruby_from_block.json, and back:
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"ingredients": ["ruby_tools:ruby_block"],
"result": { "id": "ruby_tools:ruby", "count": 9 }
}
Step 4: a helper module
Counting rubies is needed on the server (for the command) and on the client (for the HUD). Put it into its own file, scripts/util.lua (Modules):
-- scripts/util.lua
local util = {}
util.RUBY = "ruby_tools:ruby"
util.BLOCK = "ruby_tools:ruby_block"
-- Rubies in the whole inventory. A ruby block counts as nine.
function util.count_rubies(player)
local n = 0
for slot = 0, 40 do
local stack <close> = player:inventory_get(slot)
if stack then
if stack:is(util.RUBY) then
n = n + stack:count()
elseif stack:is(util.BLOCK) then
n = n + stack:count() * 9
end
end
end
return n
end
return util
Step 5: config and registration
Now scripts/main.lua. It starts with the config (Config) and the registrations:
-- scripts/main.lua
local util = require "ruby_tools.util"
local config = cl.config.open("common")
config:define_i32("gift.rubies", 1, 0, 64)
config:define_bool("hud.enabled", true)
cl.on("lifecycle.register", function()
cl.items.register{
id = "ruby",
creative_tab = "minecraft:ingredients",
vanilla_fallback = "minecraft:emerald",
}
cl.blocks.register{
id = "ruby_block",
vanilla_fallback = "minecraft:emerald_block",
item = util.BLOCK,
}
end)
Start the game once (Running and reloading), open a creative world and look for the ruby in the Ingredients tab. If it shows the checkerboard, check the paths in step 2.
Step 6: the welcome gift
New players get rubies, as many as the config says (Players):
cl.on("player.join", function(event)
local n = config:get_i32("gift.rubies")
if n > 0 then
event.player:give(util.RUBY, n)
event.player:send_message(cl.text.translatable("ruby_tools.welcome"))
end
end)
Step 7: the command
/rubies tells you how many you have. /rubies top lists everybody online, richest first (Commands):
local function rubies_top(ctx)
local list = {}
for _, p in ipairs(cl.entity.server_players()) do
list[#list + 1] = { name = p.name, n = util.count_rubies(p) }
end
table.sort(list, function(a, b) return a.n > b.n end)
for i, entry in ipairs(list) do
ctx:success(("%d. %s: %d"):format(i, entry.name, entry.n))
end
return #list
end
cl.on("server.command_register", function()
cl.cmd.register(
cl.cmd.literal("rubies")
:executes(function(ctx)
local p = ctx:player()
if not p then
ctx:failure("Only players have pockets.")
return 0
end
local n = util.count_rubies(p)
ctx:success(("You have %d %s."):format(n, n == 1 and "ruby" or "rubies"))
return math.max(n, 1)
end)
:then_(cl.cmd.literal("top"):executes(rubies_top))
)
end)
table.sort with a compare function, from Working with tables, puts the biggest count first.
Step 8: the HUD counter
The client counts once per tick and the HUD only draws (The HUD):
cl.on("lifecycle.client_setup", function()
local count = 0
cl.on("tick.client_post", function()
local me = cl.entity.client_player()
count = me and util.count_rubies(me) or 0
end)
cl.hud.register_layer{
id = "rubies",
visible = function()
return count > 0 and config:get_bool("hud.enabled")
end,
render = function(g)
g:item_by_name(util.RUBY, 4, 4)
g:text("x" .. count, 22, 8, 0xE0115F, true)
end,
}
end)
Step 9: a little polish
A chime when a ruby block breaks (Sound and music):
cl.on("block.break", function(event)
if event.block == util.BLOCK then
event.world:play(event.pos:center(), "minecraft:block.amethyst_block.chime", "blocks", 1.0, 1.2)
end
end)
cl.log("Ruby Tools", mod.version, "loaded")
Step 10: test and publish
Play through it once like a new player would:
- Join a new world: you get a ruby and the welcome message, and the HUD shows
x1. - Get nine rubies in creative and craft a block. The HUD still says 9.
/rubiesand/rubies topanswer.- Break the block: it chimes and drops itself.
- Set
"gift.rubies": 0inconfig/ruby_tools.json, restart, join a new world: no gift. - Set
"hud.enabled": false: the counter is gone.
Then add icon.png and LICENSE, run clight check --strict and clight build, and upload the package as in Publishing.
Try it
Add an operator command /rubygive <amount> that gives the player who types it that many rubies, between 1 and 64. Register it in the same server.command_register handler.
Show a solution
cl.cmd.register(
cl.cmd.literal("rubygive"):requires_permission("ruby_tools.give")
:then_(cl.cmd.argument("amount", "integer"):executes(function(ctx)
local p = ctx:player()
local n = ctx:arg("amount")
if not p then ctx:failure("Players only.") return 0 end
if n < 1 or n > 64 then ctx:failure("Pick 1 to 64.") return 0 end
p:give(util.RUBY, n)
ctx:success("Gave you " .. n .. " rubies.")
return n
end))
)
It is a separate command because the permission applies to a whole command. Players who aren’t operators get “Unknown command”.
More ideas:
- A ruby pickaxe: an item with
max_stack_size = 1and a recipe. - A shop menu with
cl.menu.openthat trades rubies for diamonds. - A keybind that shows your rubies in the action bar, using a payload to ask the server.
What’s next
You can now build complete Celestial mods in Lua. From here:
- The reference docs describe every function, event and file format in detail. Keep them open while you work.
- Mixins are hooks into the game’s own code, for changes that no event covers, such as “change this value inside vanilla’s code”:
cl.mixin.inject("entity.living.hurt", "head", function(ci) ci.args.amount = ci.args.amount / 2 end). The next chapter teaches them, starting with What is a mixin? - Read other people’s mods. The SDK’s
examples/ruby_luafolder is a complete mod much like this one, with a screen and a custom sound as extras.
Have fun, and share what you make.
Recap: a complete mod is a manifest, assets, data-pack files and a few small scripts; build it one step at a time and test each step in the game.