Skip to content

The world

Lesson 7.4

Mods often look at the world or change it: is there water here? Put a block there. This lesson covers worlds, positions, and reading and placing blocks.

Worlds

The game calls a world a level: a cl_level handle. The overworld, the Nether and the End are three levels. You get one from:

  • an event: e->level on block, interaction and entity events,
  • an entity: cl_entity_level(player),
  • a command: cl_cmd_ctx_level(ctx),
  • the server: cl_server_overworld(),
  • on the client: cl_client_level(), the world you see.

A level can tell you a few things about itself:

char dim[64];
size_t len = 0;
cl_level_dimension(level, dim, sizeof dim, &len);   // "minecraft:overworld"
int64_t day = cl_level_day_time(level) % 24000;     // 6000 is noon
int64_t ticks = cl_level_game_time(level);          // ticks since the world was created
CL_LOG("%s, time of day %lld, game time %lld", dim, (long long)day, (long long)ticks);

day_time keeps counting up from day to day, so % 24000 gives the time within the current day. game_time never goes backwards, which makes it a good clock for cooldowns.

Positions

A block position is a cl_block_pos: three whole numbers.

cl_block_pos p = {10, 64, -5};
cl_block_pos above = {p.x, p.y + 1, p.z};

Directions in Minecraft: north is towards negative z, south positive z, west negative x, east positive x, and up is positive y.

An entity position is a cl_vec3 with decimals, because entities stand anywhere inside a block. Rounding down gives the block:

static cl_block_pos block_at(cl_vec3 v) {
    cl_block_pos p = {(int32_t)floor(v.x), (int32_t)floor(v.y), (int32_t)floor(v.z)};
    return p;
}

static cl_vec3 center_of(cl_block_pos p) {
    cl_vec3 v = {p.x + 0.5, p.y + 0.5, p.z + 0.5};
    return v;
}

center_of gives the middle of a block, which is what sounds and teleports want. Small helpers like these two are worth keeping in their own file. floor is from <math.h>.

Reading blocks

cl_level_get_block(level, pos, &state) gives you the block state at a position. A state is a number that stands for one exact block, including details like which way stairs face. To ask what it is:

cl_block_state state = 0;
if (cl_level_get_block(level, below, &state) == CL_OK) {
    if (cl_block_state_is(state, CL_STR("minecraft:grass_block"))) {
        cl_easy_send_message(player, "You are standing on grass.");
    }

    // the block's id as a string
    char id[64];
    size_t len = 0;
    cl_block_name(cl_block_state_block(state), id, sizeof id, &len);
    CL_LOG("below: %s", id);   // minecraft:grass_block
}

Reading a block in a part of the world that isn’t loaded fails with CL_E_NOT_LOADED. Stick to blocks near players.

Placing blocks

The easy way:

cl_easy_set_block(level, pos, "minecraft:gold_block");

It looks the block up, takes its default state, and places it with CL_SET_ALL. The full way does the same in three steps, which lets you look the block up only once:

static cl_block_state g_gold = 0;

static void place_gold(cl_level level, cl_block_pos pos) {
    if (g_gold == 0) {
        cl_block_id id = 0;
        if (cl_block_lookup(CL_STR("minecraft:gold_block"), &id) != CL_OK) return;
        g_gold = cl_block_default_state(id);
    }
    cl_level_set_block(level, pos, g_gold, CL_SET_ALL);
}

The last argument says who should hear about the change. Always pass CL_SET_ALL: it updates the neighbours (so water flows and redstone reacts) and sends the change to players. Without CL_SET_UPDATE_CLIENTS, the block changes on the server but players don’t see it until they reload the chunk.

Flags Means
CL_SET_ALL Update neighbours and send to players. Use this.
CL_SET_UPDATE_CLIENTS Only send to players.
CL_SET_UPDATE_NEIGHBORS Only update neighbours.

To remove a block, place air: cl_easy_set_block(level, pos, "minecraft:air"). Your own blocks work too: "my_mod:ruby_block".

Blocks can only be placed on the server side, in tick-time code: server events like block.break or player.join, commands, or server timers. On the client, cl_level_set_block returns CL_E_WRONG_SIDE.

Building something

Loops and positions together make little builders. A 5 x 5 glass floor under the player:

static void glass_floor(cl_entity player) {
    cl_vec3 v;
    if (cl_entity_position(player, &v) != CL_OK) return;
    cl_level level = cl_entity_level(player);
    cl_block_pos c = block_at(v);
    for (int dx = -2; dx <= 2; ++dx) {
        for (int dz = -2; dz <= 2; ++dz) {
            cl_block_pos p = {c.x + dx, c.y - 1, c.z + dz};
            cl_easy_set_block(level, p, "minecraft:glass");
        }
    }
}

Each block is quick, but thousands in one tick are not. For big builds, spread the work over several ticks with the timers from Timers and tasks.

Try it

When a player breaks a block of gold ore, replace the block below it with a gold block, and tell the player. Leave everything else alone.

Show a solution
#include <clight_easy.h>

static void on_block_break(void* payload, void* user) {
    (void)user;
    cl_ev_block_break* e = CL_EVENT(cl_ev_block_break, payload);
    if (!cl_block_state_is(e->state, CL_STR("minecraft:gold_ore"))) return;

    cl_block_pos below = {e->pos.x, e->pos.y - 1, e->pos.z};
    cl_easy_set_block(e->level, below, "minecraft:gold_block");
    if (!CL_IS_NULL(e->player)) cl_easy_send_action_bar(e->player, "Lucky! There is gold below.");
}

static cl_result mod_main(void) {
    return CL_ON(CL_EV_BLOCK_BREAK, on_block_break);
}

CLIGHT_MOD_ENTRY(mod_main)

e->state is the block that is being broken, so it can be checked straight away without reading the world.

Recap: get a level from an event, an entity or the server, positions are cl_block_pos (round entity positions down with floor), read blocks with cl_level_get_block and cl_block_state_is, and place them with cl_easy_set_block or cl_level_set_block(..., CL_SET_ALL).