Skip to content

Adding a Block Behavior

This guide walks through implementing interactive behavior for a block in Pumpkin.


When Is a Behavior Needed?

  • Non-Interactive Blocks (e.g., stone, dirt, terracotta): Do not require any custom Rust code. Their breaking speeds, hardness, and placement properties are automatically derived from pumpkin-data.
  • Interactive or Functional Blocks (e.g., doors, chests, levers, crops, campfires, TNT): Require an implementation of the BlockBehaviour trait to respond to player clicks, redstone signals, physics steps, or timer ticks.

Step 1: Create the Behavior Struct

Create a new file under crates/pumpkin/src/block/blocks/<category>/<block_name>.rs (or directly in crates/pumpkin/src/block/blocks/).

Use the #[pumpkin_block(...)] macro to associate your behavior struct with one or more vanilla block identifiers:

rust
use std::sync::Arc;
use pumpkin_data::Block;
use pumpkin_data::block_properties::{DoorLikeProperties, Facing};
use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos;
use crate::block::registry::BlockActionResult;
use crate::block::{
    BlockBehaviour, NormalUseArgs, OnNeighborUpdateArgs, PlacedArgs, UseWithItemArgs,
};
use crate::world::World;

/// Associates this behavior struct with one or multiple block IDs.
#[pumpkin_block([
    "minecraft:oak_door",
    "minecraft:iron_door",
    "minecraft:spruce_door",
    "minecraft:birch_door"
])]
pub struct DoorBlock;

impl BlockBehaviour for DoorBlock {
    fn normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResult {
        let state_id = args.world.get_block_state(args.location).await;

        // Iron doors require redstone power and cannot be opened manually by hand
        if args.block.id == pumpkin_data::Block::IRON_DOOR.id {
            return BlockActionResult::Pass;
        }

        // Toggle open state
        let is_open = DoorLikeProperties::open(state_id);
        let new_state = DoorLikeProperties::with_open(state_id, !is_open);
        args.world.set_block_state(args.location, new_state).await;

        BlockActionResult::Consume
    }

    fn on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>) {
        // Handle neighbor redstone signals or support block destruction
    }
}

Step 2: BlockBehaviour Hook Reference

The BlockBehaviour trait exposes lifecycle hooks modeled on vanilla mechanics:

Player & Item Interaction Hooks

HookSignatureDescription
normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResultTriggered when right-clicking the block with an empty hand.
use_with_item(&self, args: UseWithItemArgs<'_>) -> BlockActionResultTriggered when right-clicking the block while holding an item.
get_screen_handler_factory(&self, args: GetScreenHandlerFactoryArgs<'_>) -> Option<Box<dyn ScreenHandlerFactory>>Returns an inventory container factory to open a UI screen (e.g., Chests, Crafting Tables).

World Lifecycle & Physics Hooks

HookSignatureDescription
placed(&self, args: PlacedArgs<'_>)Triggered immediately after the block is placed in the world.
broken(&self, args: BrokenArgs<'_>)Triggered when broken by a player or game mechanics.
on_entity_collision(&self, args: OnEntityCollisionArgs<'_>)Triggered when an entity walks inside, steps on, or falls into the block.
on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>)Triggered when an adjacent block state updates or is destroyed.
on_projectile_hit(&self, args: OnProjectileHitArgs<'_>)Triggered when an arrow, snowball, or projectile strikes the block.

Redstone & Power Hooks

HookSignatureDescription
emits_redstone_power(&self, state_id: BlockStateId) -> boolDeclares whether the block emits redstone power to neighbors.
get_weak_redstone_power(&self, ...) -> u8Returns emitted weak power level (0–15).
get_strong_redstone_power(&self, ...) -> u8Returns emitted strong direct power level (0–15).

Foliage & Bonemeal Hooks

HookSignatureDescription
is_valid_bonemeal_target(&self, args: BonemealArgs<'_>) -> boolChecks if bone meal can be applied (e.g., crop not fully grown).
is_bonemeal_success(&self, args: BonemealArgs<'_>) -> boolEvaluates random chance roll for bone meal success.
perform_bonemeal(&self, args: BonemealArgs<'_>)Advances growth stages or spawns adjacent flora.

Step 3: Modifying Block States

All block properties are strongly typed in pumpkin_data::block_properties:

rust
use pumpkin_data::block_properties::{BedLikeProperties, Part};

// 1. Read typed properties from the numeric BlockStateId
let part = BedLikeProperties::part(state_id); // Part::Head or Part::Foot
let is_occupied = BedLikeProperties::occupied(state_id);

// 2. Generate a new BlockStateId with updated properties
let updated_state = BedLikeProperties::with_occupied(state_id, true);

// 3. Commit the updated state to the world
world.set_block_state(pos, updated_state).await;

Step 4: Adding a Block Entity (Tile Entity)

If your block needs to store persistent custom state (inventories, custom text, timers):

  1. Create a block entity struct in crates/pumpkin/src/block/entities/:

    rust
    use pumpkin_util::math::position::BlockPos;
    use crate::block::entities::BlockEntity;
    
    pub struct CustomBlockEntity {
        pub pos: BlockPos,
        pub custom_counter: u32,
    }
    
    impl BlockEntity for CustomBlockEntity {
        fn to_nbt(&self) -> pumpkin_nbt::compound::NbtCompound {
            let mut compound = pumpkin_nbt::compound::NbtCompound::new();
            compound.put_int("Counter", self.custom_counter as i32);
            compound
        }
    
        fn from_nbt(&mut self, tag: &pumpkin_nbt::compound::NbtCompound) {
            if let Some(val) = tag.get_int("Counter") {
                self.custom_counter = val as u32;
            }
        }
    
        fn tick(&mut self, world: &std::sync::Arc<crate::world::World>, pos: &BlockPos) {
            // Periodic logic per game tick
        }
    }
  2. Link the block entity in your BlockBehaviour:

    rust
    impl BlockBehaviour for CustomBlock {
        fn create_block_entity(&self, pos: BlockPos) -> Option<Arc<Mutex<dyn BlockEntity>>> {
            Some(Arc::new(Mutex::new(CustomBlockEntity::new(pos))))
        }
    }

Step 5: Registering the Block

  1. Export your module in crates/pumpkin/src/block/blocks/mod.rs:

    rust
    pub mod doors;
  2. Register the behavior struct in crates/pumpkin/src/block/registry.rs:

    rust
    manager.register(DoorBlock);

At startup, manager.register automatically reads the block IDs declared in #[pumpkin_block(...)] and routes interactions to your implementation.

Veröffentlicht unter der MIT-Lizenz.