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 clight::Level. 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: player.level(),
  • a command: ctx.level(),
  • the server: clight::level::server_overworld(),
  • on the client: clight::level::client_level(), the world you see.

A level can tell you a few things about itself:

std::string dim = level.dimension().value_or("?");   // "minecraft:overworld"
int64_t day = level.day_time() % 24000;              // 6000 is noon
int64_t ticks = level.game_time();                   // ticks since the world was created
clight::log::info("{}, time of day {}, game time {}", dim, day, 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:

cl_block_pos block_at(cl_vec3 v) {
    return {static_cast<int32_t>(std::floor(v.x)), static_cast<int32_t>(std::floor(v.y)), static_cast<int32_t>(std::floor(v.z))};
}

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

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 header. std::floor is from <cmath>.

Reading blocks

level.get_block(pos) gives you the block state at a position, as an Expected<cl_block_state>. A state is a number that stands for one exact block, including details like which way stairs face. To ask what it is:

if (auto state = level.get_block(below)) {
    if (clight::registry::block_state_is(*state, "minecraft:grass_block")) {
        player.send_message("You are standing on grass.");
    }

    // the block's id as a string
    std::string id = clight::registry::block_name(clight::registry::block_state_block(*state)).value_or("?");
    clight::log::info("below: {}", 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:

level.set_block(pos, "minecraft:gold_block");

It looks the block up, takes its default state, places it so neighbours and players hear about it, and returns true when it worked. The full way does the same in steps, which lets you look the block up only once:

class GoldMod final : public clight::Mod {
public:
    void place_gold(clight::Level level, cl_block_pos pos) {
        if (gold_ == 0) {
            auto id = clight::registry::block_lookup("minecraft:gold_block");
            if (!id) return;
            gold_ = clight::registry::block_default_state(*id);
        }
        level.set_block(pos, gold_, CL_SET_ALL);
    }

private:
    cl_block_state gold_ = 0;
};

The last argument says who should hear about the change. It defaults to CL_SET_ALL, so you can leave it out: 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: level.set_block(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, set_block fails with CL_E_WRONG_SIDE.

Building something

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

void glass_floor(clight::Entity player) {
    auto v = player.position();
    if (!v) return;
    clight::Level level = player.level();
    cl_block_pos c = block_at(*v);
    for (int dx = -2; dx <= 2; ++dx) {
        for (int dz = -2; dz <= 2; ++dz) {
            level.set_block({c.x + dx, c.y - 1, c.z + dz}, "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.hpp>

class LuckyGoldMod final : public clight::Mod {
public:
    void on_construct() override {
        clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
            clight::Level(e.level).set_block({e.pos.x, e.pos.y - 1, e.pos.z}, "minecraft:gold_block");
            if (clight::Entity player = e.player) player.send_action_bar("Lucky! There is gold below.");
        }, {.filter = "minecraft:gold_ore"}).detach();
    }
};

CLIGHT_MOD(LuckyGoldMod)

The filter makes sure the handler only runs for gold ore, so it doesn’t need to check the block itself. if (clight::Entity player = e.player) declares the player and checks that there is one in the same line, because not every break comes from a player.

Recap: get a clight::Level from an event, an entity or the server, positions are cl_block_pos (round entity positions down with std::floor), read blocks with level.get_block and clight::registry::block_state_is, and place them with level.set_block(pos, "id").