Skip to content

Common Patterns

This page covers battle-tested patterns extracted from real production scripts. Each pattern solves a specific problem you will encounter repeatedly when scripting with Ceremony. Copy them, adapt them, and combine them to build your own scripts faster.


Problem: Your script calls world.destroyBlock() inside a blockBreak event listener, but destroying a block fires blockBreak again, creating infinite recursion.

Solution: Track which positions are currently being processed and skip re-entrant calls:

const processing = new Set();
Events.on('blockBreak', (player, pos, state, world) => {
// Create a unique string key for this position
const key = pos.getX() + ',' + pos.getY() + ',' + pos.getZ();
// If we are already processing this position, skip it
if (processing.has(key)) return;
// Mark this position as being processed
processing.add(key);
// Now it is safe to destroy blocks -- the re-triggered event will be skipped
world.destroyBlock(pos, true, player);
// Clean up after we are done
processing.delete(key);
});

This pattern is used in both the Treecapitator and Vein Miner example scripts, which destroy many blocks in a chain from a single break event.


Problem: You need to find all connected blocks of the same type (a tree, an ore vein, a structure) starting from a broken block.

Solution: Use breadth-first search (BFS) with a queue and a visited set. Limit the maximum number of blocks to prevent server lag:

const MAX_BLOCKS = 128;
function findConnectedBlocks(world, startPos, matchFn) {
const BlockPos = Java.type('net.minecraft.core.BlockPos');
const queue = [startPos];
const visited = new Set();
const result = [];
visited.add(startPos.getX() + ',' + startPos.getY() + ',' + startPos.getZ());
// Six cardinal directions: up, down, north, south, east, west
const directions = [
[0, 1, 0], [0, -1, 0],
[1, 0, 0], [-1, 0, 0],
[0, 0, 1], [0, 0, -1]
];
while (queue.length > 0 && result.length < MAX_BLOCKS) {
const current = queue.shift();
const state = world.getBlockState(current);
// Check if this block matches what we are looking for
if (!matchFn(state)) continue;
result.push(current);
// Check all six neighbors
for (const [dx, dy, dz] of directions) {
const neighbor = new BlockPos(
current.getX() + dx,
current.getY() + dy,
current.getZ() + dz
);
const neighborKey = neighbor.getX() + ',' + neighbor.getY() + ',' + neighbor.getZ();
if (!visited.has(neighborKey)) {
visited.add(neighborKey);
queue.push(neighbor);
}
}
}
return result;
}

Usage example (finding all logs in a tree):

const logs = findConnectedBlocks(world, brokenPos, (state) => {
const id = getBlockId(state);
return id.endsWith('_log') || id.endsWith('_wood');
});

Problem: Event callbacks give you Java block state and item stack objects, but you need the string ID (like minecraft:oak_log) to make decisions in your script.

Solution: Use BuiltInRegistries to look up the registry key for any block, item, or entity type:

const BuiltInRegistries = Java.type('net.minecraft.core.registries.BuiltInRegistries');
function getBlockId(state) {
const key = BuiltInRegistries.BLOCK.getKey(state.getBlock());
return key ? key.toString() : '';
}
function getItemId(itemStack) {
const key = BuiltInRegistries.ITEM.getKey(itemStack.getItem());
return key ? key.toString() : '';
}
function getEntityTypeId(entity) {
const key = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType());
return key ? key.toString() : '';
}

Common registry lookups:

RegistryAccessReturns
BlocksBuiltInRegistries.BLOCKminecraft:oak_log, minecraft:diamond_ore, etc.
ItemsBuiltInRegistries.ITEMminecraft:diamond_sword, cobblemon:poke_ball, etc.
Entity TypesBuiltInRegistries.ENTITY_TYPEminecraft:zombie, cobblemon:pokemon, etc.
Sound EventsBuiltInRegistries.SOUND_EVENTminecraft:entity.player.levelup, etc.
Mob EffectsBuiltInRegistries.MOB_EFFECTminecraft:slowness, minecraft:strength, etc.

Problem: You want a script ability to only activate when a player has reached a certain Journey skill level (for example, Mining level 5 or Taming level 10).

Solution: Access the Journey progression system through Java interop to read a player’s skill level:

const JourneyApi = Java.type('com.briar.journey.api.JourneyApi');
function getSkillLevel(serverPlayer, skillName) {
try {
const api = JourneyApi.getInstance();
const profile = api.getPlayerProfile(serverPlayer.getUUID());
if (!profile) return 0;
const skill = profile.getSkill(skillName);
return skill ? skill.getLevel() : 0;
} catch (e) {
Logger.warn('Failed to get skill level: ' + e.message);
return 0;
}
}

Usage:

Events.on('blockBreak', (player, pos, state, world) => {
const miningLevel = getSkillLevel(player, 'mining');
if (miningLevel < 5) return; // Requires Mining level 5
// Activate the ability
Logger.info(player.getName().getString() + ' activated vein miner at level ' + miningLevel);
});

Problem: You want to give players different quality rewards based on their skill level, each with different drop chances.

Solution: Define reward tiers as an array of objects, then roll against them in order from best to worst:

const TREASURE_TIERS = [
{ minLevel: 25, chance: 0.02, items: ['minecraft:diamond', 'minecraft:emerald'] },
{ minLevel: 15, chance: 0.05, items: ['minecraft:gold_ingot', 'minecraft:lapis_lazuli'] },
{ minLevel: 10, chance: 0.10, items: ['minecraft:iron_ingot', 'minecraft:redstone'] },
{ minLevel: 5, chance: 0.15, items: ['minecraft:coal', 'minecraft:flint'] },
];
function rollTreasure(playerLevel) {
// Check tiers from best to worst
for (const tier of TREASURE_TIERS) {
if (playerLevel >= tier.minLevel && Math.random() < tier.chance) {
// Pick a random item from this tier
const item = tier.items[Math.floor(Math.random() * tier.items.length)];
return item;
}
}
return null; // No treasure this time
}

Giving the reward via server command:

Events.on('blockBreak', (player, pos, state, world) => {
const level = getSkillLevel(player, 'excavation');
const treasure = rollTreasure(level);
if (treasure) {
const playerName = player.getName().getString();
const cmd = 'give ' + playerName + ' ' + treasure + ' 1';
Server.getCommands().performPrefixedCommand(
Server.createCommandSourceStack().withSuppressedOutput(),
cmd
);
msg(player, '<gold>You found a treasure: <yellow>' + treasure.split(':')[1] + '</yellow>!');
}
});

Problem: You need to delay an action, run something repeatedly, or build a sequence of timed events.

Solution: Use Game.scheduler for tick-based scheduling:

// Run after 40 ticks (2 seconds)
Game.scheduler.after(40, () => {
broadcast('<gold>The event is starting!');
});
// Run every 200 ticks (10 seconds) -- returns a handle to cancel later
const handle = Game.scheduler.every(200, () => {
const players = Game.entities.players();
for (const p of players) {
actionbar(p, '<aqua>Server TPS: <white>' + Server.getTickCount());
}
});
// Cancel it later when done
// handle.cancel();
// Build a countdown sequence
Game.scheduler.after(0, () => title(player, '<red>3', '', 0, 20, 5));
Game.scheduler.after(20, () => title(player, '<yellow>2', '', 0, 20, 5));
Game.scheduler.after(40, () => title(player, '<green>1', '', 0, 20, 5));
Game.scheduler.after(60, () => {
title(player, '<bold><gold>GO!', '', 0, 30, 10);
sound(player, 'entity.ender_dragon.growl', 1.0, 1.5);
});

Problem: You want to prevent an ability from being spammed — for example, a special mining ability that should only trigger once every 30 seconds.

Solution: Use the PlayerDataProxy cooldown system built into every wrapped player:

Events.on('blockBreak', (player, pos, state, world) => {
const wrapped = wrapPlayer(player);
// Check if the ability is on cooldown
if (wrapped.data.isOnCooldown('vein_miner')) {
msg(player, '<red>Vein Miner is on cooldown!');
return;
}
// Activate the ability
activateVeinMiner(player, pos, state, world);
// Set a 30-second cooldown
wrapped.data.cooldown('vein_miner', 30);
});

Cooldown API reference:

MethodParametersDescription
player.data.cooldown(key, seconds)key: string, seconds: numberSets a cooldown for the given duration
player.data.isOnCooldown(key)key: stringReturns true if the cooldown has not expired

Problem: Java interop can throw unexpected exceptions (null pointers, class cast errors, missing methods) that crash your script if unhandled.

Solution: Always wrap Java interop calls in try/catch blocks, especially when accessing player data, registry lookups, or mod APIs:

function safeGetBlockId(state) {
try {
const BuiltInRegistries = Java.type('net.minecraft.core.registries.BuiltInRegistries');
const key = BuiltInRegistries.BLOCK.getKey(state.getBlock());
return key ? key.toString() : '';
} catch (e) {
Logger.warn('Failed to get block ID: ' + e.message);
return '';
}
}

Pattern for wrapping any Java call:

function safeCall(fn, fallback) {
try {
return fn();
} catch (e) {
Logger.warn('Safe call failed: ' + e.message);
return fallback;
}
}
// Usage
const blockId = safeCall(() => getBlockId(state), '');
const level = safeCall(() => getSkillLevel(player, 'mining'), 0);

Problem: You need to run a server command from a script — for example, to give items, grant permissions, trigger another plugin, or interact with an economy system.

Solution: Use the server’s command dispatcher with a suppressed output source to avoid chat spam:

function runCommand(command) {
try {
Server.getCommands().performPrefixedCommand(
Server.createCommandSourceStack().withSuppressedOutput(),
command
);
} catch (e) {
Logger.warn('Command failed: ' + command + ' - ' + e.message);
}
}

Common usage examples:

// Give an item to a player
runCommand('give PlayerName minecraft:diamond 1');
// Add currency (example with an economy mod)
runCommand('eco give PlayerName 500');
// Set a permission (example with LuckPerms)
runCommand('lp user PlayerName permission set some.permission true');
// Send a message as the server
runCommand('say The event has started!');
// Teleport a player
runCommand('tp PlayerName 100 64 200');

Real scripts combine multiple patterns together. Here is a skeleton that uses most of the patterns on this page:

/// <reference path="types/ceremony-api.d.ts" />
const BuiltInRegistries = Java.type('net.minecraft.core.registries.BuiltInRegistries');
const processing = new Set();
function getBlockId(state) {
try {
const key = BuiltInRegistries.BLOCK.getKey(state.getBlock());
return key ? key.toString() : '';
} catch (e) { return ''; }
}
function getSkillLevel(player, skill) {
try {
const api = Java.type('com.briar.journey.api.JourneyApi').getInstance();
const profile = api.getPlayerProfile(player.getUUID());
return profile ? (profile.getSkill(skill)?.getLevel() ?? 0) : 0;
} catch (e) { return 0; }
}
function onEnable() {
Logger.info('My ability script loaded!');
Events.on('blockBreak', (player, pos, state, world) => {
// Skill check
if (getSkillLevel(player, 'mining') < 5) return;
// Cooldown check
const wrapped = wrapPlayer(player);
if (wrapped.data.isOnCooldown('my_ability')) return;
// Re-entrancy guard
const key = pos.getX() + ',' + pos.getY() + ',' + pos.getZ();
if (processing.has(key)) return;
// Block type check
const blockId = getBlockId(state);
if (!blockId.endsWith('_ore')) return;
// Activate ability with cooldown
wrapped.data.cooldown('my_ability', 30);
// BFS to find connected ores, destroy them, give rewards
const blocks = findConnectedBlocks(world, pos, (s) => getBlockId(s) === blockId);
for (const blockPos of blocks) {
processing.add(blockPos.getX() + ',' + blockPos.getY() + ',' + blockPos.getZ());
world.destroyBlock(blockPos, true, player);
}
processing.clear();
msg(player, '<gold>Vein mined <yellow>' + blocks.length + '</yellow> blocks!');
sound(player, 'entity.experience_orb.pickup', 0.6, 1.0);
});
}
function onDisable() {
processing.clear();
}