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->levelon 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).