Skip to content

Blocks

Lesson 7.2

A block is like an item that lives in the world. You register it the same way, and it needs a few more asset files: one for how it looks as a block and one for how it looks in your hand.

Registering a block

void on_register() override {
    cl_block_desc b = CL_BLOCK_DESC_INIT;
    b.id = CL_STR("my_mod:ruby_block");
    b.copy_properties_from = CL_STR("minecraft:emerald_block");
    b.vanilla_fallback = CL_STR("minecraft:emerald_block");
    b.item = CL_STR("my_mod:ruby_block");
    if (!clight::registry::block_register(&b)) {
        clight::log::error("ruby_block failed to register");
    }
}
  • copy_properties_from takes hardness, blast resistance, sounds and the right tool from a vanilla block. It is the easiest way to get a block that feels right.
  • vanilla_fallback is what players without your mod see.
  • item also registers an item for the block, so players can hold and place it. Use the block’s own id.

Like items, blocks are registered once at startup, in on_register.

How a mod block behaves

Under the hood, a mod block stands on a vanilla block. That base block is vanilla_fallback, or copy_properties_from if there is no fallback, or stone if you set neither. The base decides how the block behaves in the world:

  • how long it takes to break, and which tool it needs,
  • its sounds and how it reacts to explosions,
  • light, friction, walking speed and jump height, unless you set your own (below).

So pick the base that feels closest to what you want. A glowing crystal could stand on minecraft:glowstone, a slippery block on minecraft:ice, a bouncy one on minecraft:slime_block.

Your own block properties

These cl_block_desc fields change how the block feels, on top of the base:

Field Means Minecraft examples
destroy_time How long it takes to break (hardness) dirt 0.5, stone 1.5, obsidian 50
friction How slippery it is to walk on most blocks 0.6, slime 0.8, ice 0.98
speed_factor Walking speed on it most blocks 1.0, soul sand and honey 0.4
jump_factor Jump height from it most blocks 1.0, honey 0.5
light_emission The light it gives, 0 to 15 torch 14, glowstone 15
sound_type Its break, place and step sounds "glass", "wood", "amethyst_block", or a block id like "minecraft:glass"
b.destroy_time = 0.8f;
b.friction = 0.98f;                 // slides like ice
b.light_emission = 7;
b.sound_type = CL_STR("amethyst_block");

A field left at its default (CL_BLOCK_DESC_INIT) keeps the value of copy_properties_from, or of the base block. The server uses these values for what it simulates (mobs and items sliding, mining time, light, sounds), and players with your mod predict the same, so walking and mining feel smooth. Players without it walk on the base block on their screen and get corrected by the server now and then.

Blocks are simple cubes for now: one look, no properties like “facing” or “lit”, and a full-cube model.

Assets for a block

For my_mod:ruby_block:

assets/my_mod/
  blockstates/ruby_block.json       which model the block uses
  models/block/ruby_block.json      the cube model
  textures/block/ruby_block.png     the texture, 16 x 16
  items/ruby_block.json             the model in hand and in inventories
  lang/en_us.json                   "block.my_mod.ruby_block": "Block of Ruby"

blockstates/ruby_block.json. The block has a single state, written as "":

{
  "variants": {
    "": { "model": "my_mod:block/ruby_block" }
  }
}

models/block/ruby_block.json, a cube with the same texture on all six sides:

{
  "parent": "minecraft:block/cube_all",
  "textures": {
    "all": "my_mod:block/ruby_block"
  }
}

items/ruby_block.json reuses the block model for the item:

{
  "model": {
    "type": "minecraft:model",
    "model": "my_mod:block/ruby_block"
  }
}

Block names use the key block.<mod id>.<name>.

When a player mines the block with the right tool, it drops its item. In creative mode it drops nothing, like vanilla blocks.

Behaviour

Blocks can have code too, in a cl_block_behavior. Three kinds exist:

Function Runs when Gets
use_without_item A player right-clicks the block ctx, player, hand, out
attack A player left-clicks (starts hitting) it ctx, player
random_tick The block gets a random tick ctx

ctx is a cl_block_ctx with ctx->level, ctx->pos, ctx->state and ctx->user (the descriptor’s user pointer).

cl_override doorbell_use(const cl_block_ctx* ctx, cl_entity, cl_hand, cl_interaction_result* out) {
    const cl_vec3 at{ctx->pos.x + 0.5, ctx->pos.y + 0.5, ctx->pos.z + 0.5};
    clight::Level(ctx->level).sound_play(at, "minecraft:block.note_block.bell", CL_SOUND_BLOCKS, 1.0f, 1.0f);
    *out = CL_RESULT_SUCCESS;
    return CL_OVERRIDE_HANDLED;
}

const cl_block_behavior kDoorbellBehavior = {sizeof(cl_block_behavior), doorbell_use, nullptr, nullptr};

class DoorbellMod final : public clight::Mod {
public:
    void on_register() override {
        cl_block_desc b = CL_BLOCK_DESC_INIT;
        b.id = CL_STR("my_mod:doorbell");
        b.copy_properties_from = CL_STR("minecraft:oak_planks");
        b.vanilla_fallback = CL_STR("minecraft:oak_planks");
        b.item = CL_STR("my_mod:doorbell");
        b.behavior = &kDoorbellBehavior;
        clight::registry::block_register(&b);
    }
};

The three function fields are in the order of the table, so { size, use, attack, random_tick }. nullptr means “no code for this”. Parameters you don’t use can stay without a name, like cl_entity and cl_hand above, and the compiler won’t warn. CL_BLOCK_BEHAVIOR_INIT gives an empty behaviour you can fill field by field, which is easier to read when you only need random_tick:

cl_block_behavior make_moss_behavior() {
    cl_block_behavior bh = CL_BLOCK_BEHAVIOR_INIT;
    bh.random_tick = [](const cl_block_ctx* ctx, int32_t*) -> cl_override {
        clight::Level(ctx->level).set_block(ctx->pos, "minecraft:moss_block");
        return CL_OVERRIDE_HANDLED;
    };
    return bh;
}

A lambda without captures works as well as a named function here. Return CL_OVERRIDE_HANDLED when your code handled the click, or CL_OVERRIDE_CALL_ORIGINAL to let the game do what the base block would do. attack and random_tick have an extra int32_t* parameter that isn’t used yet; ignore it.

With lambdas, clight::registry::block(desc, behaviour) does the same. Its fields are .use_without_item, .attack and .random_tick, in that order, and each lambda returns true (handled) or false (do what the base block does):

cl_block_desc b = CL_BLOCK_DESC_INIT;
b.id = CL_STR("my_mod:doorbell");
b.copy_properties_from = CL_STR("minecraft:oak_planks");
b.vanilla_fallback = CL_STR("minecraft:oak_planks");
b.item = CL_STR("my_mod:doorbell");
clight::registry::block(b, {.use_without_item = [](const cl_block_ctx& ctx, clight::Entity, cl_hand, cl_interaction_result& out) {
    const cl_vec3 at{ctx.pos.x + 0.5, ctx.pos.y + 0.5, ctx.pos.z + 0.5};
    clight::Level(ctx.level).sound_play(at, "minecraft:block.note_block.bell", CL_SOUND_BLOCKS, 1.0f, 1.0f);
    out = CL_RESULT_SUCCESS;
    return true;
}});

The lambdas may capture state; as for items, the helper uses b.user itself.

random_tick runs now and then, at the same rate crops grow (on average about every 68 seconds per block, with the default randomTickSpeed). It is the right tool for slow changes, like a block that turns into another one over time.

Try it

Register a my_mod:bounce_pad block that bounces like a slime block and says “Boing!” in the action bar when right-clicked. Add the language entry.

Show a solution
#include <clight.hpp>

cl_override bounce_use(const cl_block_ctx*, cl_entity player, cl_hand, cl_interaction_result* out) {
    clight::Entity(player).send_action_bar("Boing!");
    *out = CL_RESULT_SUCCESS;
    return CL_OVERRIDE_HANDLED;
}

const cl_block_behavior kBounceBehavior = {sizeof(cl_block_behavior), bounce_use, nullptr, nullptr};

class BounceMod final : public clight::Mod {
public:
    void on_register() override {
        cl_block_desc b = CL_BLOCK_DESC_INIT;
        b.id = CL_STR("my_mod:bounce_pad");
        b.vanilla_fallback = CL_STR("minecraft:slime_block");
        b.item = CL_STR("my_mod:bounce_pad");
        b.behavior = &kBounceBehavior;
        if (!clight::registry::block_register(&b)) clight::log::error("bounce_pad failed");
    }
};

CLIGHT_MOD(BounceMod)
{
  "block.my_mod.bounce_pad": "Bounce Pad"
}

The slime block as base gives the bounce, the sounds and the quick breaking. Players without the mod see a slime block.

Recap: register blocks in on_register with a cl_block_desc from CL_BLOCK_DESC_INIT and clight::registry::block_register, pick a base block with vanilla_fallback, add item for the block item, give them a blockstate, model, texture and name, and use a cl_block_behavior or clight::registry::block(b, {...}) with lambdas for clicks and random ticks.