Skip to content

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:

  1. Join a new world: you get a ruby and the welcome message, and the HUD shows x1.
  2. Get nine rubies in creative and craft a block. The HUD still says 9.
  3. /rubies and /rubies top answer.
  4. Break the block: it chimes and drops itself.
  5. Set "gift.rubies": 0 in config/ruby_tools.json, restart, join a new world: no gift.
  6. 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 = 1 and a recipe.
  • A shop menu with cl.menu.open that 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_lua folder 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.