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 self.server.level. From the level you read its name and seed, and you enumerate the dimensions inside it:

src/endstone_my_plugin/my_plugin.py
from endstone.level import Dimension
from endstone.plugin import Plugin

class MyPlugin(Plugin):
    api_version = "0.11"

    def on_enable(self) -> None:
        level = self.server.level
        self.logger.info(f"World: {level.name}, seed {level.seed}")

        for dimension in level.dimensions:
            self.logger.info(f"  dimension {dimension.id}")

        overworld = level.get_dimension(Dimension.OVERWORLD)
  • Level is the world as a whole - its name, seed, the in-game time, and the list of actors across every dimension.
  • Dimension is one space within that level. Get them all from level.dimensions, or fetch one by id with level.get_dimension(Dimension.OVERWORLD). The constants Dimension.OVERWORLD, Dimension.NETHER, and Dimension.THE_END are Identifiers, and get_dimension also accepts the raw string ("minecraft:overworld"). Each dimension's id is that same Identifier.

The in-game time lives on the level, not the dimension: read and write level.time as an integer of ticks. Endstone's level API does not currently expose weather or game rules - drive those with the vanilla /weather and /gamerule commands through server.dispatch_command 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.

from endstone.level import Location

loc = Location(overworld, 100.0, 64.0, -200.0)

loc.x, loc.y, loc.z              # the float coordinates
loc.block_x, loc.block_y, loc.block_z  # floored to the block they sit in
loc.dimension                    # the dimension this location belongs to

The coordinates are floats, so a location can sit anywhere within a block. The block_x / block_y / block_z properties 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 live Block whose type and data reflect the world right now:

block = overworld.get_block_at(100, 64, -200)
# or, from a location:
block = overworld.get_block_at(loc)

block.type            # "minecraft:stone" - the block's identifier
block.data            # a BlockData snapshot, including its block states
block.location        # where it sits, as a Location
block.get_relative(0, 1, 0)  # the block one above
  • block.type is the identifier string, like "minecraft:grass_block".
  • block.data returns a BlockData describing the block fully. Its block_states is a dict of the per-block properties - direction, waterlogging, growth stage, and so on - keyed by name with bool, int, or str values.
  • get_relative steps to a neighbouring block, either by an offset or by a BlockFace (BlockFace.UP, DOWN, NORTH, SOUTH, EAST, WEST) and an optional distance.
data = block.data
self.logger.info(f"{data.type} states={data.block_states}")

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.set_type("minecraft:stone")

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

data = self.server.create_block_data(
    "minecraft:oak_stairs",
    {"weirdo_direction": 2, "upside_down_bit": False},
)
block.set_data(data)

create_block_data fills every property with its default, then overrides only the keys you pass, so you set just the states you care about. Both set_type and set_data take an optional apply_physics flag (default True); 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 block_states).

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:

from endstone.event import event_handler, BlockBreakEvent

class MyPlugin(Plugin):
    api_version = "0.11"

    def on_enable(self) -> None:
        self.register_events(self)

    @event_handler
    def on_block_break(self, event: BlockBreakEvent) -> None:
        self.logger.info(f"{event.player.name} broke {event.block.type}")

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:

src/endstone_my_plugin/my_plugin.py
from endstone.command import Command, CommandSender
from endstone.level import Location
from endstone.player import Player
from endstone.plugin import Plugin

class MyPlugin(Plugin):
    api_version = "0.11"

    commands = {
        "platform": {
            "description": "Place a glass block under your feet",
            "usages": ["/platform"],
        }
    }

    def on_command(self, sender: CommandSender, command: Command, args: list[str]) -> bool:
        if not isinstance(sender, Player):
            sender.send_error_message("Run this in game.")
            return True

        loc = sender.location
        block = loc.dimension.get_block_at(loc.block_x, loc.block_y - 1, loc.block_z)

        data = self.server.create_block_data("minecraft:stained_glass", {"color": "light_blue"})
        block.set_data(data)

        sender.send_message(f"Placed {block.type} at {block.x}, {block.y}, {block.z}")
        return True

See commands for how the command is declared and dispatched, and events for reacting to blocks players change themselves.

On this page