Common Patterns
Common Patterns
Section titled “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.
Re-entrancy Guards
Section titled “Re-entrancy Guards”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.
BFS Block Traversal
Section titled “BFS Block Traversal”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');});Java Registry Lookups
Section titled “Java Registry Lookups”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:
| Registry | Access | Returns |
|---|---|---|
| Blocks | BuiltInRegistries.BLOCK | minecraft:oak_log, minecraft:diamond_ore, etc. |
| Items | BuiltInRegistries.ITEM | minecraft:diamond_sword, cobblemon:poke_ball, etc. |
| Entity Types | BuiltInRegistries.ENTITY_TYPE | minecraft:zombie, cobblemon:pokemon, etc. |
| Sound Events | BuiltInRegistries.SOUND_EVENT | minecraft:entity.player.levelup, etc. |
| Mob Effects | BuiltInRegistries.MOB_EFFECT | minecraft:slowness, minecraft:strength, etc. |
Skill Level Checks
Section titled “Skill Level Checks”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);});Tiered Reward Systems
Section titled “Tiered Reward Systems”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>!'); }});Scheduled Tasks
Section titled “Scheduled Tasks”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:
Delayed Action (run once after a delay)
Section titled “Delayed Action (run once after a delay)”// Run after 40 ticks (2 seconds)Game.scheduler.after(40, () => { broadcast('<gold>The event is starting!');});Repeating Task (run every N ticks)
Section titled “Repeating Task (run every N ticks)”// Run every 200 ticks (10 seconds) -- returns a handle to cancel laterconst 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();Timed Sequence (chain of delayed steps)
Section titled “Timed Sequence (chain of delayed steps)”// Build a countdown sequenceGame.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);});Cooldown Pattern
Section titled “Cooldown Pattern”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:
| Method | Parameters | Description |
|---|---|---|
player.data.cooldown(key, seconds) | key: string, seconds: number | Sets a cooldown for the given duration |
player.data.isOnCooldown(key) | key: string | Returns true if the cooldown has not expired |
Safe Java Method Calls
Section titled “Safe Java Method Calls”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; }}
// Usageconst blockId = safeCall(() => getBlockId(state), '');const level = safeCall(() => getSkillLevel(player, 'mining'), 0);Command Execution
Section titled “Command Execution”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 playerrunCommand('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 serverrunCommand('say The event has started!');
// Teleport a playerrunCommand('tp PlayerName 100 64 200');Combining Patterns
Section titled “Combining Patterns”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();}Next Steps
Section titled “Next Steps”- Example Scripts — See these patterns applied in complete, production-ready scripts
- Your First Script — If you have not done the tutorial yet, start there
- Events Reference — Full list of all 140+ events you can listen for