Skip to content

Block Ticking

Block ticking lets blocks do things over time — growing crops, decaying leaves, spreading moss, that kind of thing. Each block type can have a TickProcedure attached in its JSON definition, and the server will call it periodically for every instance of that block.

com.hypixel.hytale.server.core.asset.type.blocktick.config.TickProcedure

The base class all tick procedures extend. It provides a thread-local random number generator and one abstract method you need to implement:

TickProcedure.java
public abstract class TickProcedure {
public static final CodecMapCodec<TickProcedure> CODEC = new CodecMapCodec<>("Type");
public static final BuilderCodec<TickProcedure> BASE_CODEC =
BuilderCodec.abstractBuilder(TickProcedure.class).build();
protected static final SplittableRandom BASE_RANDOM = new SplittableRandom();
protected static final ThreadLocal<SplittableRandom> RANDOM =
ThreadLocal.withInitial(BASE_RANDOM::split);
protected SplittableRandom getRandom() {
return RANDOM.get();
}
public abstract BlockTickStrategy onTick(
World world, WorldChunk chunk,
int x, int y, int z, int blockId
);
}

The CODEC field is a CodecMapCodec — meaning the JSON "Type" key determines which procedure subclass gets deserialized. Procedures register themselves against this codec during plugin setup.

The return value from onTick tells the tick system what to do next with that block:

StrategyDescription
CONTINUEKeep ticking this block on future ticks
IGNOREDTick was ignored (no procedure found, or ticking disabled)
SLEEPStop ticking this block until something wakes it up
WAIT_FOR_ADJACENT_CHUNK_LOADPause ticking until neighboring chunks are loaded

The BlockTickPlugin registers two built-in procedures:

BlockTickPlugin.java (registration)
TickProcedure.CODEC.register("BasicChance",
BasicChanceBlockGrowthProcedure.class,
BasicChanceBlockGrowthProcedure.CODEC);
TickProcedure.CODEC.register("SplitChance",
SplitChanceBlockGrowthProcedure.class,
SplitChanceBlockGrowthProcedure.CODEC);

So in JSON, you use "Type": "BasicChance" or "Type": "SplitChance" — not the full class names.

Rolls a random chance each tick. If it succeeds, the block transforms into a different block.

BasicChance procedure
{
"TickProcedure": {
"Type": "BasicChance",
"NextId": "MyPlugin_MaturePlant",
"ChanceMin": 1,
"Chance": 10,
"NextTicking": false
}
}
PropertyTypeDescription
NextIdstringBlock ID to transform into
ChanceMinintMinimum threshold for the random roll to succeed
ChanceintUpper bound of the random range (rolls 0 to Chance - 1)
NextTickingbooleanIf true, the new block continues ticking. If false, it sleeps after transforming

The chance logic works like this: a random int in [0, Chance) is generated, and if it’s less than ChanceMin, the growth triggers. So ChanceMin: 1, Chance: 10 gives a 1-in-10 chance per tick.

Extends BasicChance but instead of a single target block, it picks randomly from a weighted map of possible outcomes.

SplitChance procedure
{
"TickProcedure": {
"Type": "SplitChance",
"NextIds": {
"MyPlugin_Crop_Mature_A": 3,
"MyPlugin_Crop_Mature_B": 1
},
"ChanceMin": 1,
"Data": 10,
"NextTicking": false
}
}
PropertyTypeDescription
NextIdsobjectMap of block IDs to their relative weights
ChanceMinintMinimum threshold for the random roll
DataintUpper bound of the random range (same role as Chance in BasicChance)
NextTickingbooleanWhether the resulting block continues ticking

In the example above, when the growth triggers, there’s a 3/4 chance of becoming Mature_A and a 1/4 chance of Mature_B.

The TickProcedure field goes directly in your block type JSON:

MyPlugin_GrowingPlant.json
{
"Id": "MyPlugin_GrowingPlant",
"DrawType": "Model",
"CustomModel": "models/plant_stage1.blockymodel",
"TickProcedure": {
"Type": "BasicChance",
"NextId": "MyPlugin_MaturePlant",
"ChanceMin": 1,
"Chance": 10,
"NextTicking": false
}
}

The tick system runs as part of the chunk processing pipeline:

  1. Discovery — When a chunk is first generated, BlockTickPlugin scans every block in the chunk. Any block with a TickProcedure on its BlockType gets flagged as ticking in its BlockSection.

  2. PreTick — The ChunkBlockTickSystem.PreTick system runs first, preparing the chunk’s timing state.

  3. Ticking — The ChunkBlockTickSystem.Ticking system iterates all flagged blocks, looks up their TickProcedure via BlockTickPlugin.getTickProcedure(blockId), and calls onTick. The returned BlockTickStrategy determines what happens next.

  4. Error handling — If a tick procedure throws, the system logs a warning and returns SLEEP for that block, preventing it from crashing the tick loop.

Ticking can be enabled or disabled per-world via WorldConfig.isBlockTicking().

You can create your own by extending TickProcedure and registering it:

SpreadingMossProcedure.java
public class SpreadingMossProcedure extends TickProcedure {
public static final BuilderCodec<SpreadingMossProcedure> CODEC =
BuilderCodec.builder(SpreadingMossProcedure.class,
SpreadingMossProcedure::new, TickProcedure.BASE_CODEC)
.addField(
new KeyedCodec<>("SpreadChance", Codec.INTEGER),
(proc, v) -> proc.spreadChance = v,
proc -> proc.spreadChance
)
.build();
private int spreadChance = 10;
@Override
public BlockTickStrategy onTick(World world, WorldChunk chunk,
int x, int y, int z, int blockId) {
if (getRandom().nextInt(spreadChance) != 0) {
return BlockTickStrategy.CONTINUE;
}
BlockType stone = BlockType.getAssetMap().get("Rock_Stone");
BlockType mossy = BlockType.getAssetMap().get("MossyStone");
int nx = x + getRandom().nextInt(3) - 1;
int nz = z + getRandom().nextInt(3) - 1;
if (world.getBlockType(nx, y, nz) == stone) {
world.setBlock(nx, y, nz, mossy);
}
return BlockTickStrategy.CONTINUE;
}
}

Then register it in your plugin’s setup():

Registration
TickProcedure.CODEC.register("SpreadingMoss",
SpreadingMossProcedure.class,
SpreadingMossProcedure.CODEC);

And use it in JSON:

MyPlugin_MossyBlock.json
{
"Id": "MyPlugin_MossyBlock",
"TickProcedure": {
"Type": "SpreadingMoss",
"SpreadChance": 15
}
}

The RandomTickPlugin registers a RandomTickSystem that processes random blocks per chunk section, checking BlockType.getRandomTickProcedure() to execute procedures. It introduces two built-in procedures:

ProcedureDescription
ChangeIntoBlockChanges a block into a specified target block type
SpreadToSpreads blocks to neighbors (e.g., grass spreading to dirt), with configurable directions, Y range, light level requirements, and revert behavior

The RandomTick resource controls tick rates per chunk:

FieldDefaultDescription
blocksPerSectionPerTickStable1Blocks ticked per section for stable ticks (deterministic hash)
blocksPerSectionPerTickUnstable3Blocks ticked per section for unstable ticks (random)

The SpreadTo procedure supports spreading blocks like grass or mycelium:

FieldDefaultDescription
SpreadDirectionsDirections the block can spread
MinY / MaxYY range constraints for spreading
AllowedTagTag the target block must have to accept spread
RequireEmptyAboveTargetWhether the block above the target must be empty
RequiredLightLevel6Minimum light level for spreading
RevertBlockBlock to revert to when covered