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.

A sub-level is a self-contained Minecraft level region — containing chunks, block entities, and entities — that exists at a dynamic 3D pose (position and orientation) within a regular Minecraft level. Unlike normal world regions, a sub-level can move and rotate freely in 3D space. Internally, Sable stores sub-levels in a “plot grid” far out in world coordinates, invisible to normal gameplay, then renders and simulates them at whatever pose they currently hold.

The plot grid

SubLevelContainer manages a 2D grid of fixed-size plots. Each plot is a square region of chunks that can hold exactly one sub-level. The grid origin is placed at DEFAULT_ORIGIN = 10000 plots out from zero, putting every plot well beyond the 30-million-block boundary that separates it from player-reachable space. The side length of each plot is 2^logPlotSize chunks, and the side length of the entire grid is 2^logSideLength plots. When a sub-level is allocated, Sable finds the first empty slot in the occupancy BitSet and places the plot there. The occupancy data persists across world saves, so plots stay in the same grid position even after a server restart.
The DEFAULT_ORIGIN constant is defined on SubLevelContainer and is the same for every level in a given world. You should not rely on any specific world-space coordinates for the plot grid — always go through the container API.

Sub-level identity

Every sub-level is assigned a UUID at allocation time. allocateNewSubLevel generates a random UUID automatically; allocateSubLevel lets you supply a specific UUID for explicit placement (useful when restoring sub-levels from persistent storage). You can look up a loaded sub-level at any time by UUID:
SubLevel subLevel = container.getSubLevel(uuid);

Server vs. client containers

Each Level instance carries its own SubLevelContainer. Two typed overloads let you get the right subtype without an explicit cast:
MethodReturn type
SubLevelContainer.getContainer(ServerLevel)ServerSubLevelContainer
SubLevelContainer.getContainer(ClientLevel)ClientSubLevelContainer
ServerSubLevelContainer drives physics, loading tickets, and tracking. ClientSubLevelContainer handles interpolation and lighting scene IDs for rendering. Both extend the same SubLevelContainer base, so query methods like getAllSubLevels(), getSubLevel(UUID), and queryIntersecting(BoundingBox3dc) are available on either side.

Allocating sub-levels

ServerSubLevelContainer container = SubLevelContainer.getContainer(serverLevel);

// Automatic placement — finds the first empty plot
ServerSubLevel subLevel = container.allocateNewSubLevel(pose);

// Explicit placement — supply a UUID and grid coordinates
ServerSubLevel subLevel = container.allocateSubLevel(uuid, plotX, plotZ, pose);
allocateNewSubLevel(Pose3d) scans the occupancy bitset for the first free slot and places the sub-level there. allocateSubLevel(UUID, int, int, Pose3d) throws IllegalArgumentException if the target slot is already occupied or out of bounds.
The plot grid has a finite number of slots (2^logSideLength × 2^logSideLength). If all slots are occupied, allocateNewSubLevel throws IllegalStateException. Sub-levels that are no longer needed should be removed so their slots are freed.

Querying sub-levels

MethodDescription
getAllSubLevels()Returns all currently loaded sub-levels.
getSubLevel(UUID)Looks up a specific sub-level by its UUID; returns null if not loaded.
getSubLevel(int x, int z)Looks up by local plot grid coordinates.
queryIntersecting(BoundingBox3dc)Returns an Iterable of every sub-level whose bounding box intersects the given 3D AABB.

Loading tickets

Sub-levels are only kept in memory while they are loaded. To keep a sub-level loaded across chunk unloads and server restarts, add a SubLevelLoadingTicket via ServerSubLevelContainer.addForceLoadTicket:
container.addForceLoadTicket(subLevel, SubLevelLoadingTicketType.COMMAND_FORCED, ticketKey);
Tickets are typed — the generic parameter T is the key type that uniquely identifies the ticket within that sub-level. Removing a ticket with removeForceLoadTicket releases the force-load constraint. Ticket state is persisted to SubLevelTicketsSavedData so sub-levels survive server restarts as long as their tickets remain active.
Use your own SubLevelLoadingTicketType (registered via SubLevelLoadingTicketType.create) to distinguish tickets created by your mod from those created by others. This prevents accidental removal of another mod’s tickets.

Build docs developers (and LLMs) love