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.

BlockEntitySubLevelActor is the primary interface for block entities that need to interact with the Sable physics system. Implement it on your BlockEntity subclass to receive tick callbacks and declare dependencies between sub-levels. Sable calls the methods on this interface automatically whenever the block entity is mounted on an active ServerSubLevel.

Interface methods

sable$tick

default void sable$tick(ServerSubLevel subLevel)
Called once per server game tick (20 Hz) while this block entity is mounted on a sub-level. Use this for game-logic updates that do not require sub-tick precision.
subLevel
ServerSubLevel
required
The sub-level this block entity is currently mounted on.
The default implementation is a no-op.

sable$physicsTick

default void sable$physicsTick(ServerSubLevel subLevel, RigidBodyHandle handle, double timeStep)
Called once per physics substep while this block entity is mounted on a sub-level. There may be multiple physics substeps per game tick depending on the configured substep count.
subLevel
ServerSubLevel
required
The sub-level this block entity is mounted on.
handle
RigidBodyHandle
required
A handle to the sub-level’s rigid body. Use this to apply impulses and read velocity.
timeStep
double
required
Duration of this physics substep in seconds: 1.0 / 20.0 / substeps. Scale continuous forces by this value so that the total per-tick effect is independent of the substep count.
The default implementation is a no-op.
Use sable$physicsTick for continuous force application (engines, propellers, lift). Use sable$tick for discrete logic (inventory checks, signal updates) that doesn’t need sub-tick precision.

sable$getLoadingDependencies

@Nullable
default Iterable<@NotNull SubLevel> sable$getLoadingDependencies()
Returns sub-levels that should load and unload together with the sub-level this block entity is on. When any sub-level in the group would be unloaded, all of them are unloaded as a unit. Returns a collection of dependent SubLevel instances, or null for none. The default implementation delegates to sable$getConnectionDependencies().
This method may be called after chunks have been unloaded. Do not perform direct level block access inside this method — cache any required sub-level references on the block entity.

sable$getConnectionDependencies

@Nullable
default Iterable<@NotNull SubLevel> sable$getConnectionDependencies()
Returns sub-levels considered “connected” to this block entity’s sub-level. Connection dependencies are used for grouping sub-levels that move and interact as one logical unit (e.g. a vehicle chassis and its wheels). Returns a collection of connected SubLevel instances, or null for no connections. The default implementation returns null.
Same caveat as sable$getLoadingDependencies — do not perform direct level access here.

Complete implementation example

public class EngineBlockEntity extends BlockEntity implements BlockEntitySubLevelActor {

    private double throttle = 1.0;
    private ServerSubLevel connectedSublevel = null; // cached reference, not fetched during tick

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

    @Override
    public void sable$tick(final ServerSubLevel subLevel) {
        // Run game-logic updates at 20 Hz
        // e.g. consume fuel, read redstone, update throttle
    }

    @Override
    public void sable$physicsTick(
        final ServerSubLevel subLevel,
        final RigidBodyHandle handle,
        final double timeStep
    ) {
        // Apply forward thrust scaled by timeStep so total per-tick
        // impulse is 500 N regardless of substep count.
        Vector3d localForce = new Vector3d(0, 0, throttle * 500.0 * timeStep);

        handle.applyLinearImpulse(localForce);
    }

    @Override
    public @Nullable Iterable<SubLevel> sable$getConnectionDependencies() {
        // Return connected sub-levels so they load/unload together.
        // Only return a cached reference — do NOT call level.getBlockEntity() here.
        if (this.connectedSublevel != null) {
            return List.of(this.connectedSublevel);
        }
        return null;
    }
}

Using QueuedForceGroup for named forces

Rather than calling handle.applyLinearImpulse() directly, you can contribute forces to a named ForceGroup so they appear in the in-game force visualization overlay:
@Override
public void sable$physicsTick(
    final ServerSubLevel subLevel,
    final RigidBodyHandle handle,
    final double timeStep
) {
    Vector3d thrustPos = new Vector3d(
        this.getBlockPos().getX() + 0.5,
        this.getBlockPos().getY() + 0.5,
        this.getBlockPos().getZ() + 0.5
    );
    Vector3d thrustForce = new Vector3d(0, 0, throttle * 500.0 * timeStep);

    QueuedForceGroup propulsion = subLevel.getOrCreateQueuedForceGroup(
        ForceGroups.PROPULSION.get()
    );
    propulsion.applyAndRecordPointForce(thrustPos, thrustForce);
}
See Force groups and mass tracking for the full force API.

Relationship to BlockEntitySubLevelPropellerActor

If your block entity acts as a propeller, consider implementing BlockEntitySubLevelPropellerActor instead, which extends BlockEntitySubLevelActor and provides a default sable$physicsTick implementation that reads thrust from a BlockEntityPropeller. See Block interface reference for details.

Build docs developers (and LLMs) love