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.

Sable exposes a set of Java interfaces that your blocks and block entities can implement to participate in the sub-level physics system. Each interface targets a specific capability — from per-physics-tick callbacks on block entities to aerodynamic lift contributions and custom collision geometry. Implement only the interfaces relevant to your block’s behavior; unused interfaces carry no overhead.

BlockEntitySubLevelActor

Implement BlockEntitySubLevelActor on your BlockEntity subclass to receive tick callbacks while that block entity is mounted on a sub-level. All methods have default no-op implementations, so you only need to override what you use.
MethodCalled when
sable$tick(ServerSubLevel)Once per server game tick while on a sub-level
sable$physicsTick(ServerSubLevel, RigidBodyHandle, double)Once per physics substep (may be multiple per game tick)
sable$getLoadingDependencies()When the system resolves which sub-levels load/unload together
sable$getConnectionDependencies()When the system resolves connected sub-levels; used as the default for loading dependencies
Physics logic that must influence the simulation — such as applying forces — should go in sable$physicsTick, not sable$tick. Multiple physics substeps run per game tick, so sable$tick fires less frequently and too late to affect physics state for that substep.
sable$getLoadingDependencies() delegates to sable$getConnectionDependencies() by default. Override sable$getConnectionDependencies() to declare which other sub-levels are “connected” to this one; both loading and connection logic will use it automatically unless you override sable$getLoadingDependencies() separately.
Both dependency methods may be called after chunks have been unloaded. Do not perform direct level block access inside them — store any required sub-level references on the block entity instead.
public class MyBlockEntity extends BlockEntity implements BlockEntitySubLevelActor {

    public MyBlockEntity(BlockPos pos, BlockState state) {
        super(MY_BLOCK_ENTITY_TYPE, pos, state);
    }

    @Override
    public void sable$physicsTick(ServerSubLevel subLevel, RigidBodyHandle handle, double timeStep) {
        // Apply a constant upward force every physics substep
        handle.applyLinearImpulse(new Vector3d(0, 100 * timeStep, 0));
    }
}

BlockSubLevelAssemblyListener

Implement BlockSubLevelAssemblyListener on your Block subclass to be notified when SubLevelAssemblyHelper moves a block of this type during sub-level assembly. This is useful for transferring state, updating references, or triggering side effects at assembly time.
MethodCalled when
beforeMove(originLevel, resultingLevel, newState, oldPos, newPos)Before the block is placed in the destination level
afterMove(originLevel, resultingLevel, newState, oldPos, newPos)After the block is placed; the original block has not yet been removed
beforeMove has a default no-op implementation. afterMove is abstract and must be implemented.
public class MyBlock extends Block implements BlockSubLevelAssemblyListener {

    @Override
    public void afterMove(ServerLevel originLevel, ServerLevel resultingLevel,
                          BlockState newState, BlockPos oldPos, BlockPos newPos) {
        // Copy a block entity's data from the origin level to the resulting level
        BlockEntity oldEntity = originLevel.getBlockEntity(oldPos);
        BlockEntity newEntity = resultingLevel.getBlockEntity(newPos);
        if (oldEntity instanceof MyBlockEntity old && newEntity instanceof MyBlockEntity next) {
            next.copyDataFrom(old);
        }
    }
}

BlockSubLevelCollisionShape

Implement BlockSubLevelCollisionShape on your Block subclass to provide a collision shape that differs from the block’s standard getCollisionShape. The shape returned here is used when baking the rigid body geometry for the sub-level, not for normal world collision.
public class MyBlock extends Block implements BlockSubLevelCollisionShape {

    @Override
    public VoxelShape getSubLevelCollisionShape(BlockGetter blockGetter, BlockState state) {
        // Return a simplified box instead of the full voxel shape
        return Shapes.box(0.1, 0.0, 0.1, 0.9, 1.0, 0.9);
    }
}

BlockSubLevelLiftProvider

Implement BlockSubLevelLiftProvider on your Block subclass to contribute aerodynamic lift and drag forces to the sub-level during each physics substep. The interface includes a default implementation of sable$contributeLiftAndDrag that handles the full lift and drag calculation; in most cases you only need to provide the three scalar values and the surface normal.
MethodPurpose
sable$getNormal(BlockState)The facing direction of this lift surface (required)
sable$getParallelDragScalar()Drag coefficient along the normal (default 0.75)
sable$getDirectionlessDragScalar()Omnidirectional drag coefficient (default ~0.0689)
sable$getLiftScalar()Lift force coefficient (default 0.475)
sable$contributeLiftAndDrag(...)Full physics callback; override to replace the built-in calculation
The default values for sable$getDirectionlessDragScalar are tuned to prevent exponential velocity gain relative to the default sable$getParallelDragScalar and sable$getLiftScalar. If you change either scalar, recalculate the directionless drag minimum as (-k1 + sqrt(k1² + k2²)) / 2.
public class MyWingBlock extends Block implements BlockSubLevelLiftProvider {

    @Override
    public Direction sable$getNormal(BlockState state) {
        return state.getValue(BlockStateProperties.FACING);
    }

    @Override
    public float sable$getLiftScalar() {
        return 0.6f; // more lift than the default
    }
}

BlockSubLevelDynamicCollider

Implement BlockSubLevelDynamicCollider on your Block subclass to provide a collider that can change as the block state changes. Dynamic colliders are rebuilt when the block state is updated on the sub-level, rather than being baked once at assembly time.
public class MyDoorBlock extends Block implements BlockSubLevelDynamicCollider {

    @Override
    public void buildBoxes(VoxelColliderData data) {
        // Add collision boxes based on the current block state
        BlockState state = data.getState();
        if (!state.getValue(BlockStateProperties.OPEN)) {
            data.addBox(0.0, 0.0, 0.0, 1.0, 1.0, 0.1875);
        }
    }
}

BlockSubLevelCustomCenterOfMass

Implement BlockSubLevelCustomCenterOfMass on your Block subclass to override the center-of-mass contribution this block makes to the sub-level’s aggregate center of mass. The returned vector is relative to the lower corner of the block.
public class MyHeavyBaseBlock extends Block implements BlockSubLevelCustomCenterOfMass {

    @Override
    public Vector3dc getCenterOfMass(BlockGetter blockGetter, BlockState state) {
        // Shift mass contribution to the bottom half of the block
        return new Vector3d(0.5, 0.2, 0.5);
    }
}

BlockWithSubLevelCollisionCallback

Implement BlockWithSubLevelCollisionCallback on your Block subclass to receive a callback when the sub-level this block is part of collides with something. The interface provides a static sable$getCallback(BlockState) helper that also returns a FragileBlockCallback for block states that carry the FRAGILE physics property.
public class MyFragileBlock extends Block implements BlockWithSubLevelCollisionCallback {

    @Override
    public BlockSubLevelCollisionCallback sable$getCallback() {
        return (subLevel, collisionData) -> {
            // Break the block on any collision above a velocity threshold
            if (collisionData.getImpulse() > 50.0) {
                subLevel.getLevel().destroyBlock(this.getBlockPos(), true);
            }
        };
    }
}

Propeller interfaces

Two interfaces in the dev.ryanhcode.sable.api.block.propeller package handle propeller-style thrust blocks.

BlockEntityPropeller

BlockEntityPropeller defines the physical properties of a propeller: its facing direction, airflow speed, raw thrust, and whether it is currently active. The interface includes default helpers for computing air-pressure-adjusted scaled thrust and airflow-efficiency scaling.
MethodReturns
getBlockDirection()The Direction the propeller faces
getAirflow()Airflow in m/s
getThrust()Raw thrust in pN
isActive()Whether to compute and apply thrust
getScaledThrust()Thrust adjusted for airflow efficiency and air pressure

BlockEntitySubLevelPropellerActor

BlockEntitySubLevelPropellerActor extends BlockEntitySubLevelActor and wires a BlockEntityPropeller into the physics tick. Implement both interfaces on the same block entity class to get propulsion out of the box.
public class MyPropellerBlockEntity extends BlockEntity
        implements BlockEntityPropeller, BlockEntitySubLevelPropellerActor {

    @Override
    public BlockEntityPropeller getPropeller() {
        return this;
    }

    @Override
    public Direction getBlockDirection() {
        return this.getBlockState().getValue(BlockStateProperties.FACING);
    }

    @Override
    public double getAirflow() { return 20.0; }

    @Override
    public double getThrust() { return 500.0; }

    @Override
    public boolean isActive() { return this.powered; }

    @Override
    public Level getLevel() { return super.getLevel(); }

    @Override
    public BlockPos getBlockPos() { return super.getBlockPos(); }
}
BlockEntitySubLevelPropellerActor automatically calls applyForces each physics substep when isActive() returns true, queuing the scaled thrust impulse into the sub-level’s PROPULSION force group.

Build docs developers (and LLMs) love