Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ryanhcode/sable/llms.txt

Use this file to discover all available pages before exploring further.

Assembly is the process of taking a set of blocks from a ServerLevel and moving them into a newly created sub-level. Sable provides the SubLevelAssemblyHelper utility class to handle this end-to-end: it allocates the sub-level, moves blocks and their block entity data, relocates hanging entities and tracking points, sets the rotation point to the center of mass, and teleports the physics body into place. In most cases you will pair assembleBlocks with gatherConnectedBlocks, which discovers which blocks should be assembled together.

Gathering connected blocks

Before you can assemble, you need to know which blocks belong to the structure. gatherConnectedBlocks runs a BFS from an origin block, expanding outward through any connected non-air block. Two blocks are considered connected if they are within a 3×3×3 neighborhood of each other, excluding corner-only adjacencies (i.e., connections must share at least a face or an edge, not just a corner).
SubLevelAssemblyHelper.GatherResult result = SubLevelAssemblyHelper.gatherConnectedBlocks(
    origin, serverLevel, 10000, null
);
The third argument is the maximum number of blocks to gather. If the search exceeds this limit, gathering stops early and returns GatherResult.State.TOO_MANY_BLOCKS. Pass null as the fourth argument to accept all connections, or supply a FrontierPredicate to restrict which connections are valid.

GatherResult

The returned GatherResult record contains:
FieldTypeDescription
blocksSet<BlockPos>All gathered block positions. null on failure.
checkedBlocksintTotal blocks examined during the search.
boundingBoxBoundingBox3iAxis-aligned bounding box enclosing all gathered blocks. null on failure.
assemblyStateGatherResult.StateSUCCESS, TOO_MANY_BLOCKS, or NO_BLOCKS.

FrontierPredicate

FrontierPredicate is a @FunctionalInterface that controls which block-to-block connections are followed during the BFS. Implement it to exclude certain block types, enforce directionality, or respect custom connection rules:
FrontierPredicate predicate = (originPos, originState, pos, state, directionFrom) -> {
    // Return false to stop the BFS from crossing this connection
    return !state.is(Blocks.OBSIDIAN);
};
directionFrom is the cardinal direction from originPos to pos, or null when the connection is diagonal (edge-adjacent but not face-adjacent).

Assembling blocks

Once you have a GatherResult with state SUCCESS, pass it to assembleBlocks:
if (result.assemblyState() == GatherResult.State.SUCCESS) {
    ServerSubLevel subLevel = SubLevelAssemblyHelper.assembleBlocks(
        serverLevel, origin, result.blocks(), result.boundingBox()
    );
}
assembleBlocks performs the following steps in order:
  1. Allocates a new sub-level via SubLevelContainer.allocateNewSubLevel, initializing its pose at the anchor block’s world position.
  2. Creates an empty center chunk in the new plot.
  3. Constructs an AssemblyTransform mapping each source block position to its destination position in the sub-level’s plot.
  4. Moves hanging entities (item frames, paintings, leashes) whose support blocks are inside the assembled volume.
  5. Moves all blocks, preserving block entity NBT and invoking BlockSubLevelAssemblyListener callbacks.
  6. Sets the sub-level’s rotation point to its computed center of mass.
  7. Teleports the physics body to the new logical position.
  8. Moves any tracking points inside the bounding box into the sub-level’s coordinate space.
The anchor block position determines where the sub-level’s origin is placed in world space. It is typically the block the player interacted with to trigger assembly.

AssemblyTransform

AssemblyTransform is an internal helper used during block movement. It maps each source BlockPos to a destination position relative to the sub-level’s center chunk and applies a Rotation enum value to rotate block states. The rotation is in 90-degree counter-clockwise increments. In the default assembly path, rotation is always Rotation.NONE; the transform is exposed publicly so advanced use cases (such as assembling structures at a rotated orientation) can construct their own.

BlockSubLevelAssemblyListener

Blocks that need to react to being moved can implement BlockSubLevelAssemblyListener:
public class MyBlock extends Block implements BlockSubLevelAssemblyListener {

    @Override
    public void beforeMove(ServerLevel originLevel, ServerLevel resultingLevel,
                           BlockState newState, BlockPos oldPos, BlockPos newPos) {
        // Called before the block is placed at newPos
    }

    @Override
    public void afterMove(ServerLevel originLevel, ServerLevel resultingLevel,
                          BlockState newState, BlockPos oldPos, BlockPos newPos) {
        // Called after the block is placed at newPos; the old block has not yet been removed
    }
}
Both callbacks receive the source and destination levels, the block state, and the old and new positions. Use beforeMove for cleanup in the origin level and afterMove for initialization in the sub-level.
When afterMove is called, the original block at oldPos still exists in originLevel. Do not rely on it having been removed yet — removal happens in a separate pass after all blocks have been placed.

Center of mass

After all blocks are placed in the sub-level, Sable reads the accumulated MassTracker data to find the center of mass and sets the sub-level’s rotation point there. This means the physics body rotates around the geometric center of the assembled structure’s mass distribution rather than around the anchor block. If the mass tracker returns no center of mass (e.g. the sub-level contains only massless blocks), the rotation point falls back to the plot’s center block.

Build docs developers (and LLMs) love