Worlds and blocks

Reach the level and its dimensions, read blocks at coordinates, and place blocks with custom block states.

A Bedrock world is a level, and a level holds one or more dimensions - the overworld, the nether, and the end. Blocks live inside a dimension, addressed by their x, y, z coordinates. To touch a block you first reach its dimension, then ask the dimension for the block at a position.

Reach the level and its dimensions

The server exposes the running world at getServer().getLevel(). From the level you read its name and seed, and you enumerate the dimensions inside it:

include/my_plugin.h
#include <endstone/endstone.hpp>

namespace es = endstone;

class MyPlugin : public es::Plugin {
public:
    void onEnable() override
    {
        es::Level *level = getServer().getLevel();
        getLogger().info("World: {}, seed {}", level->getName(), level->getSeed());

        for (es::Dimension *dimension : level->getDimensions()) {
            getLogger().info("  dimension {}", dimension->getId());
        }

        es::Dimension *overworld = level->getDimension(es::Dimension::Overworld);
    }
};
  • Level is the world as a whole - its getName(), getSeed(), the in-game getTime() / setTime(), and getActors() across every dimension.
  • Dimension is one space within that level. Get them all from getDimensions(), or fetch one by id with getDimension(es::Dimension::Overworld). The constants es::Dimension::Overworld, Nether, and TheEnd are DimensionIds (an alias for es::Identifier<Dimension>), and getDimension also accepts the raw string ("minecraft:overworld"). Each dimension's getId() returns that same DimensionId.

The in-game time lives on the level, not the dimension: read and write it with level->getTime() / level->setTime(ticks). Endstone's level API does not currently expose weather or game rules - drive those with the vanilla /weather and /gamerule commands through getServer().dispatchCommand if you need them.

Locations and positions

A Location is a point inside a dimension: a dimension plus x, y, z, and an optional pitch and yaw facing. It's what blocks report for their position and what teleports and spawns consume.

es::Location loc{*overworld, 100.0, 64.0, -200.0};

loc.getX();  loc.getY();  loc.getZ();             // the coordinates
loc.getBlockX();  loc.getBlockY();  loc.getBlockZ();  // floored to the block they sit in
loc.getDimension();                               // the dimension this location belongs to

The constructor takes the dimension by reference, then the coordinates. The getBlockX() / getBlockY() / getBlockZ() accessors floor each axis to the integer coordinate of the block that contains the point - exactly what you pass when looking a block up by coordinate.

Read a block

Ask a dimension for the block at integer coordinates, or hand it a Location. Either way you get a std::unique_ptr<Block> whose getType() and getData() reflect the world right now:

std::unique_ptr<es::Block> block = overworld->getBlockAt(100, 64, -200);
// or, from a location:
block = overworld->getBlockAt(loc);

block->getType();          // "minecraft:stone" - the block's identifier
block->getData();          // a std::unique_ptr<BlockData>, including its block states
block->getLocation();      // where it sits, as a Location
block->getRelative(0, 1, 0);  // the block one above
  • getType() is the identifier string, like "minecraft:grass_block".
  • getData() returns a std::unique_ptr<BlockData> describing the block fully. Its getBlockStates() is a BlockStates map - std::unordered_map<std::string, std::variant<bool, std::string, int>> - of the per-block properties such as direction or waterlogging.
  • getRelative steps to a neighbouring block, either by an offset or by a BlockFace (BlockFace::Up, Down, North, South, East, West) and an optional distance.
auto data = block->getData();
getLogger().info("{} states={}", data->getType(), data->getBlockStates());

Change a block

Set a block's type by identifier, or set its full data to carry block states. Both write straight to the world:

block->setType("minecraft:stone");

To place a block with specific states - a stair facing a direction, a waterlogged fence, a particular wood - build a BlockData with getServer().createBlockData(type, states) and apply it. The BlockStates map is the same shape you read back from getBlockStates():

auto data = getServer().createBlockData(
    "minecraft:oak_stairs",
    {{"weirdo_direction", 2}, {"upside_down_bit", false}});
block->setData(*data);

createBlockData fills every property with its default, then overrides only the keys you pass, so you set just the states you care about. Both setType and setData have an overload taking a bool apply_physics; pass false to skip the neighbour updates when you're placing many blocks and will settle physics yourself.

Block-state names and values are vanilla Bedrock data - they vary per block. An unknown state key or an out-of-range value is silently dropped or clamped, so verify the names for the block you're placing (for example with /setblock and tab-completion in game, or by reading an existing block's getBlockStates()).

React to block changes

When you care about blocks players break or place rather than ones you set yourself, handle the block events. They live on the events page - BlockBreakEvent and BlockPlaceEvent both carry the block and the player responsible:

class MyPlugin : public es::Plugin {
public:
    void onEnable() override
    {
        registerEvent(&MyPlugin::onBlockBreak, *this);
    }

    void onBlockBreak(es::BlockBreakEvent &event)
    {
        getLogger().info("{} broke {}", event.getPlayer().getName(), event.getBlock().getType());
    }
};

Putting it together

A command that places a configured block where the player is standing. It reaches the player's dimension, floors their location to a block, builds the block data once, and writes it:

include/my_plugin.h
#include <endstone/endstone.hpp>

namespace es = endstone;

class MyPlugin : public es::Plugin {
public:
    bool onCommand(es::CommandSender &sender, const es::Command &command,
                   const std::vector<std::string> &args) override
    {
        auto *player = sender.asPlayer();
        if (player == nullptr) {
            sender.sendErrorMessage("Run this in game.");
            return true;
        }

        es::Location loc = player->getLocation();
        auto block = loc.getDimension().getBlockAt(loc.getBlockX(), loc.getBlockY() - 1, loc.getBlockZ());

        auto data = getServer().createBlockData("minecraft:stained_glass", {{"color", std::string("light_blue")}});
        block->setData(*data);

        sender.sendMessage("Placed {} at {}, {}, {}", block->getType(), block->getX(), block->getY(), block->getZ());
        return true;
    }
};

Declare the command in your plugin.toml and see commands for how it's wired and dispatched, and events for reacting to blocks players change themselves.

On this page