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 provides several interfaces your Block (and BlockEntity) classes can implement to customize how blocks behave when they are part of a sub-level — from assembly callbacks to custom collision shapes, aerodynamic lift, and collision events.

BlockSubLevelAssemblyListener

Implement on a Block to receive callbacks when SubLevelAssemblyHelper moves that block into or out of a sub-level.
public interface BlockSubLevelAssemblyListener {
    default void beforeMove(ServerLevel originLevel, ServerLevel resultingLevel,
                            BlockState newState, BlockPos oldPos, BlockPos newPos) {}

    void afterMove(ServerLevel originLevel, ServerLevel resultingLevel,
                   BlockState newState, BlockPos oldPos, BlockPos newPos);
}
beforeMove
method
Called before the block is moved. The old block is still present at oldPos in originLevel. Use this to read any world state you need before the block is relocated.
afterMove
method
Called after the block has been placed at newPos in resultingLevel. The old block has not yet been removed at the time this fires — use it to sync additional data after placement.

BlockSubLevelCollisionShape

Implement on a Block to specify a different VoxelShape for physics collider baking, separate from the regular Minecraft collision shape.
public interface BlockSubLevelCollisionShape {
    VoxelShape getSubLevelCollisionShape(BlockGetter blockGetter, BlockState state);
}
Sable bakes voxel colliders from block shapes when a chunk section is added to the physics pipeline. Implementing this interface lets you return a simplified shape for physics (e.g., a full cube) while keeping a complex visual shape in-game.

BlockSubLevelDynamicCollider

Implement on a Block whose physics collider needs to update dynamically based on block state changes (rather than being baked once on chunk load).
public interface BlockSubLevelDynamicCollider {
    void buildBoxes(VoxelColliderData data);
}
Dynamic colliders are more expensive than static baked colliders. Only implement this interface when the collider must change at runtime (e.g., a door that opens and closes on a moving sub-level).

BlockSubLevelCustomCenterOfMass

Implement on a Block to override the center-of-mass contribution for that block state, instead of using the default block-center.
public interface BlockSubLevelCustomCenterOfMass {
    Vector3dc getCenterOfMass(BlockGetter blockGetter, BlockState state);
}
The returned vector is relative to the lower corner of the block. This affects how mass is distributed when computing the sub-level’s aggregate center of mass.

BlockSubLevelLiftProvider

Implement on a Block to contribute aerodynamic lift and drag forces to the sub-level during each physics substep. Sable groups adjacent lift-provider blocks into LiftProviderGroup clusters for efficient force accumulation. Key methods to override:
public interface BlockSubLevelLiftProvider {
    // Required: which face this lift provider faces
    @NotNull Direction sable$getNormal(BlockState state);

    // Optional: tune lift/drag coefficients (all have defaults)
    default float sable$getParallelDragScalar()    { return 0.75f; }
    default float sable$getDirectionlessDragScalar() { return 0.06888202261f; }
    default float sable$getLiftScalar()            { return 0.475f; }
}
The relationship between sable$getDirectionlessDragScalar() (k1) and sable$getLiftScalar() (k2) must satisfy k1 >= (-k1 + sqrt(k1² + k2²)) / 2 to prevent exponential velocity gain. The defaults already satisfy this constraint.
The sable$contributeLiftAndDrag method has a default implementation that computes lift and drag from velocity, pressure, and the block’s normal — override it only if you need fully custom aerodynamics.

BlockWithSubLevelCollisionCallback

Implement on a Block to receive callbacks when the sub-level containing this block collides with another rigid body.
public interface BlockWithSubLevelCollisionCallback {
    BlockSubLevelCollisionCallback sable$getCallback();
}
sable$getCallback() returns the BlockSubLevelCollisionCallback instance for this block. Blocks with the sable:fragile physics property automatically use FragileBlockCallback without needing to implement this interface.

Propeller interfaces

BlockEntityPropeller

Implement on a BlockEntity that acts as a propeller. Provides thrust direction, airflow, and thrust magnitude used by the physics system.
public interface BlockEntityPropeller {
    Direction getBlockDirection();  // facing direction of thrust
    double getAirflow();            // effective airflow in m/s
    double getThrust();             // base thrust in pN
    boolean isActive();             // whether thrust is currently applied

    // Computes thrust scaled by airflow efficiency and air pressure
    default double getScaledThrust() { ... }

    Level getLevel();
    BlockPos getBlockPos();
}
getScaledThrust() multiplies base thrust by an airflow scaling factor (clamped 0–1 based on the sub-level’s velocity relative to the thrust direction) and the current air pressure at the block’s location.

BlockEntitySubLevelPropellerActor

Extends BlockEntitySubLevelActor. Implement alongside BlockEntityPropeller to have forces automatically applied each physics substep via the PROPULSION force group.
public interface BlockEntitySubLevelPropellerActor extends BlockEntitySubLevelActor {
    BlockEntityPropeller getPropeller();

    // Default implementation applies thrust via QueuedForceGroup each physics tick
    @Override
    default void sable$physicsTick(ServerSubLevel subLevel, RigidBodyHandle handle, double timeStep) {
        final BlockEntityPropeller prop = this.getPropeller();
        if (prop.isActive()) {
            this.applyForces(subLevel, Vec3.atLowerCornerOf(prop.getBlockDirection().getNormal()), timeStep);
        }
    }
}
Forces are applied through subLevel.getOrCreateQueuedForceGroup(ForceGroups.PROPULSION.get()), which accumulates point forces and submits them to the physics pipeline at the end of the tick.

Build docs developers (and LLMs) love