Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Registering cauldron interactions

Use Hooks::register_cauldron_interaction with a RegistrationContext and RegisterCauldronInteractionEvent. Retain typed BlockKey and ItemKey values from registration, as described in the registry-key chapter. The hook runs on both sides when preparing the resource snapshot; publication happens only after the complete candidate validates. Failed preparation retains the previous snapshot.

The names correspond to NeoForge 26.1's documented RegisterCauldronInteractionEvent, Dispatcher, and Interaction. They remain unverified for NeoForge 26.3. See the public cauldron documentation.

Register an exact item on a block's dispatcher:

fn register_cauldron_interaction(
    &self,
    _: &RegistrationContext,
    event: &mut RegisterCauldronInteractionEvent<'_>,
) {
    let handler: Arc<dyn Interaction> = Arc::new(Syrup { empty_bottle: self.glass_bottle });
    event.interactions.register(self.empty_cauldron, self.syrup_bottle, handler.clone()).unwrap();
    event.interactions.register(self.syrup_cauldron, self.syrup_bottle, handler).unwrap();
}

A custom block opts into dispatch by registration; its name need not end in cauldron. Register the filled target too when a plan replaces the empty block with a custom cauldron. Its declared state schema must admit the proposed state, for example level=1 in a 1..3 level property. register_to_all(item, handler) queues registration over the complete prepared dispatcher set, including custom blocks registered later in the hook sequence. Conflicts surface when preparation finishes; they fail the whole preparation and name both owners. register_to_all does not return Duplicate directly. Immediate admission errors from either registration method are also stored, so ignoring an error cannot publish a partial snapshot.

Vanilla entries are prebound in the same table. With register, replacing one, or registering a second entry for the same dispatcher and item, returns CauldronRegistrationError::Duplicate { owners }, naming the existing owner and the attempted owner, and refuses publication. There is no override flag. Unknown block or item keys refuse registration. On water cauldrons, vanilla's dye tag always precedes exact-item entries: even washing that returns Pass does not fall through to the mod's entry.

Implement Interaction::decide(&dyn Host, &CauldronUse) for eligibility. It reads the complete captured held stack, target state, actor, hand, dimension, creative flag and underwater flag. Return Pass to leave normal item use available, Consume to stop it without a plan, or Success(CauldronPlan) to propose a block replacement, one held-item exchange and deferred effects. The exchange uses the existing survival, creative, inventory insertion and overflow-drop rules.

For example, a handler can propose a syrup-bottle exchange without changing the world during the decision:

struct Syrup { empty_bottle: ItemKey }

impl Interaction for Syrup {
    fn decide(&self, host: &dyn Host, _: &CauldronUse) -> CauldronDecision {
        let Some(filled) = host.block_state_id("example:syrup_cauldron[level=1]") else {
            return CauldronDecision::Pass;
        };
        CauldronDecision::Success(CauldronPlan {
            block: Some(filled),
            exchange: Some(ItemStack::new(self.empty_bottle, 1).unwrap()),
            sound: Some(Identifier::parse("minecraft:item.bottle.empty").unwrap()),
            fluid_event: Some(Identifier::parse("minecraft:fluid_place").unwrap()),
            statistics: vec![
                Identifier::parse("minecraft:use_cauldron").unwrap(),
                Identifier::parse("minecraft:used").unwrap(),
            ],
        })
    }

    fn fire(&self, _: &mut dyn ServerHost, _: &CauldronUse, _: &CauldronPlan) -> CauldronFire {
        // No extra writes: the engine applies the validated plan.
        CauldronFire::Applied
    }
}

A transfer can carry two distinct statistics: for example minecraft:use_cauldron and minecraft:used. The latter awards the used-item statistic for the original held item's key. Each is awarded once; duplicate statistics and plans with more than two statistics refuse the action. A plan may also carry one sound and one fluid event (minecraft:fluid_place or minecraft:fluid_pickup). State and returned stacks are validated against the current resources before mutation.

Interaction::fire(&mut dyn ServerHost, &CauldronUse, &CauldronPlan) runs only on the server, inside the shared action transaction. Return CauldronFire::Applied to permit the plan and supported callback writes to commit. Refused, a panic, a guest trap, stale preconditions or an enclosing abort discards the action. Sounds, statistics, events and overflow drops wait for root commit. Uses the shared transaction and runtime limits; no cauldron-specific budget.

Client prediction selects the same entry, but skips custom handlers entirely: it does not call decide or predict writes and consumes the local action while waiting for the server result. Vanilla handlers continue to predict normally. The server owns inventories, statistics, drops and game events. Matching custom binary builds and synchronized resource generations are not established by the current handshake, so custom prediction is suppressed. Native and wasm handlers use the same generated registration and callback route; CauldronFire defaults to Refused when a generated callback cannot run.