Skip to content

Machine and Recipe-Type Events ​

Current API Minecraft 1.21.1 / MBD2 21.1.1

MBD2 event subscriptions are targeted. The first argument is the exact machine-definition or recipe-type ID; the callback receives a KubeJS wrapper whose Java event is wrapper.event.

js
// kubejs/server_scripts/mbd2_events.js
MBDMachineEvents.onAfterRecipeWorking('example:crusher', wrapper => {
  const event = wrapper.event
  console.info(`Recipe ${event.recipe.id} left its active run at ${event.machine.pos}`)
})

Do not target a block ID or a recipe ID. A machine handler targets an ID in MBDRegistries.MACHINE_DEFINITIONS; a recipe-type handler targets an ID in MBDRegistries.RECIPE_TYPES.

Blueprints cover the same events

Every event on this page is also a blueprint entry node, so the same reaction can be drawn in the editor instead of scripted. Choose KubeJS when the pack already scripts; choose a blueprint when the machine should ship self-contained.

Server machine events ​

All of these live in kubejs/server_scripts. wrapper.event.machine is always present.

HandlerExtra fieldsCancellableWhen it runs
onLoad—noOne tick after the block entity becomes valid
onRemoved—noThe machine is being removed
onPlacedplayer, itemStacknoPlaced by an entity
onNeighborChangedblock, fromPosnoA neighbour update reached the machine
onDropsentity, mutable dropsnoDrop list assembled, before it is spawned
onOpenUIplayeryesBefore the machine UI opens
onUseWithoutItemplayer, hit, mutable interactionResultnoEmpty-hand right click
onUseCatalystcatalyst, player, handyesMultiblock catalyst use
onUImutable ui, mutable playernoThe server built the machine UI
onStateChangedoldState, newStateyesA machine-state transition
onStructureFormed / onStructureInvalid—noMultiblock formation and loss
onTick—yesBefore recipe logic and every trait tick

drops is the live Java List<ItemStack>; add to it rather than replacing it. Cancelling onTick skips recipe logic and every trait's serverTick.

Recipe lifecycle events ​

HandlerExtra fieldsCancellableExact point
onBeforeRecipeModifymutable recipeyesBefore configured modifiers and parallel calculation
onAfterRecipeModifymutable recipenoAfter modifiers produced the effective recipe
onBeforeRecipeWorkingrecipeyesAfter fuel is taken, before inputs are committed
onRecipeWorkingrecipe, progressyesAfter this tick's per-tick IO committed, before progress increments
onRecipeWaitingrecipenoStatus became WAITING
onAfterRecipeWorkingrecipenoCompletion or interruption, before outputs
onConsumeInputsAfterWorkingrecipenoDeferred inputs were committed at completion
onRecipeFinishrecipenoAfter the outputs exist
onRecipeStatusChangedoldStatus, newStatusnoA recipe logic's status changed
onFuelRecipeModifymutable recipeyesA fuel candidate matched, before its inputs are taken
onFuelBurningFinishnullable recipenoThe fuel ran out, at the end of a server tick. Not delivered up to 21.1.2 — see below

Each of these events also carries recipeLogic, the recipe logic it is about — which matters once a machine runs several.

onAfterRecipeWorking fires before outputs are produced; onRecipeFinish fires after. Use the latter for "a craft completed" bonuses, or a bonus item lands in the slot the recipe's own output still needs.

onConsumeInputsAfterWorking only fires when the machine's Consume inputs after working setting is on — as a definition value, or as a per-machine runtime value override of recipe_logic.consume_inputs_after_working.

onFuelBurningFinish up to 21.1.2

Up to 21.1.2, MBDMachine#onFuelBurningFinish posts its event to the NeoForge bus without postCustomEvent(), so neither the KubeJS handler nor the blueprint entry node receives it; Java NeoForge.EVENT_BUS listeners do. Later versions deliver it like every other event. On 21.1.2 and earlier, track fuel exhaustion through onRecipeWaiting or machine.recipeLogic.fuelTime instead.

An event with no KubeJS handler

MachineUseItemOnEvent (right click with an item) exists and has a blueprint entry node, but MBDServerEvents registers no KubeJS name for it. Use a blueprint, or a Java listener on NeoForge.EVENT_BUS.

js
MBDMachineEvents.onRecipeWorking('example:crusher', wrapper => {
  const { recipe, progress } = wrapper.event
  if (progress % 20 === 0) {
    console.debug(`${recipe.id}: ${progress}/${recipe.duration}`)
  }
})

Several recipe logics ​

Newer than 21.1.2, a machine can run several recipe logics side by side. Every recipe lifecycle event above then fires for each of them, and wrapper.event.recipeLogic says which. The machine gives the rest:

MemberGives
machine.recipeLogicThe primary logic, as before
machine.recipeLogicsEvery logic in the order they tick: the primary and its threads, then the additional logics
machine.getRecipeLogic(address)The logic at an address — a name, or name#thread — or null
machine.aggregatedRecipeStatusThe status the machine shows for all its logics together

A logic tells its name, address, threadIndex, whether it is primary, and its recipeType, beside the usual status, progress, duration and lastRecipe.

js
// kubejs/server_scripts/mbd2_recipe_logics.js
MBDMachineEvents.onRecipeFinish('example:alloy_smelter', wrapper => {
  const { recipe, recipeLogic, machine } = wrapper.event
  console.info(`${recipeLogic.address} finished ${recipe.id}`)
  machine.recipeLogics.forEach(logic => {
    console.info(`  ${logic.address}: ${logic.status} ${logic.progress}/${logic.duration}`)
  })
})

MBDMachineEvents.onRecipeStatusChanged('example:alloy_smelter', wrapper => {
  const { recipeLogic, newStatus, machine } = wrapper.event
  if (!recipeLogic.primary) return
  // the side line pauses while the main line works
  const smelt = machine.getRecipeLogic('smelt')
  if (smelt != null) {
    smelt.setWorkingEnabled(`${newStatus}` !== 'WORKING')
  }
})

onRecipeStatusChanged follows one logic. The machine's working and waiting states follow aggregatedRecipeStatus instead: working if any logic works, else waiting if any waits, else suspended if all are, else idle.

Cancelling ​

Two equivalent forms, for the handlers marked cancellable above:

js
MBDMachineEvents.onOpenUI('example:crusher', wrapper => {
  wrapper.event.setCanceled(true)          // set it on the Java event
})

MBDMachineEvents.onOpenUI('example:crusher', wrapper => {
  return false                             // KubeJS interrupt-false, mapped to setCanceled
})

Cancelling is a control-flow decision, not a substitute for recipe conditions — a cancelled onBeforeRecipeWorking runs after fuel was already consumed, and a cancelled onRecipeWorking runs after that tick's per-tick IO committed. Put eligibility in a RecipeCondition.

Never move resources in an event that can run during simulation or on the client. Resource accounting belongs in a Java IRecipeHandlerTrait.

Client events ​

These belong in kubejs/client_scripts.

HandlerGroupFields
onClientTick(machineId, cb)MBDMachineEventsmachine
onCustomDataUpdate(machineId, cb)MBDMachineEventsoldValue, newValue; cancellable
onCustomKeyframe(machineId, cb)MBDMachineEventsinstruction, controllerName, animationTick; GeckoLib only
onRecipeUI(recipeTypeId, cb)MBDRecipeTypeEventsmutable recipe, mutable ui; cancellable
registerCustomRenderers(cb)MBDClientEventsnot targeted — see script renderers
js
// kubejs/client_scripts/mbd2_visuals.js
MBDMachineEvents.onCustomDataUpdate('example:crusher', wrapper => {
  const { oldValue, newValue } = wrapper.event
  console.debug(`Client machine data changed: ${oldValue} -> ${newValue}`)
})

Keep these visual-only. Holding a machine object on the client does not transfer server authority.

Proxy-recipe transfer ​

js
// kubejs/server_scripts/mbd2_proxy.js
MBDRecipeTypeEvents.onTransferProxyRecipe('example:electric_furnace', wrapper => {
  const event = wrapper.event
  // event.recipeType       target MBDRecipeType
  // event.proxyTypeId      source vanilla/mod recipe type ID
  // event.proxyType        source RecipeType object
  // event.proxyRecipeId    source recipe ID
  // event.proxyRecipe      source recipe object
  // event.mbdRecipe        nullable, mutable converted result
  if (`${event.proxyTypeId}` !== 'minecraft:smelting') {
    event.setCanceled(true)
  }
})

This filters recipes produced by a proxy the recipe type already declares; it does not enable one. See proxy recipe types.

Tick cost

onTick, onClientTick, and onRecipeWorking run every tick per machine. Do not scan the world, parse IDs, rebuild collections, or allocate UI objects in them.

Released under the MIT License.