Custom Mechanism

This page will use sugarcane as an example to roughly implement a mechanism similar to the vanilla sugarcane.

Firstly create two classes for both item and block

package net.momirealms.customcrops.api.example;

import net.momirealms.customcrops.api.core.item.AbstractCustomCropsItem;
import net.momirealms.customcrops.common.util.Key;

public class SugarCaneItem extends AbstractCustomCropsItem {
    
    private static SugarCaneItem instance;

    public SugarCaneItem() {
        super(Key.key("customcrops", "sugarcane_item"));
    }

    public static SugarCaneItem instance() {
        if (instance == null) {
            instance = new SugarCaneItem();
        }
        return instance;
    }
}
package net.momirealms.customcrops.api.example;

import net.momirealms.customcrops.api.core.block.AbstractCustomCropsBlock;
import net.momirealms.customcrops.common.util.Key;

public class SugarCaneBlock extends AbstractCustomCropsBlock {

    private static SugarCaneBlock instance;

    private SugarCaneBlock() {
        super(Key.key("customcrops", "sugarcane_block"));
    }

    // This method is used to verify whether a block belongs to this mechanism
    // The id would be the item ID from the custom item plugin for instance Oraxen
    @Override
    public boolean isBlockInstance(String id) {
        return id.equals("customcrops:sugarcane_block_id");
    }
    
    public static SugarCaneBlock instance() {
        if (instance == null) {
            instance = new SugarCaneBlock();
        }
        return instance;
    }
    
    @Override
    public NamedTextColor insightColor() {
        return NamedTextColor.YELLOW;
    }
}

Then register it in onEnable() method

package net.momirealms.customcrops.api.example;

import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.Registries;
import net.momirealms.customcrops.api.core.RegistryAccess;
import net.momirealms.customcrops.api.event.CustomCropsReloadEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.List;

public class YourPlugin extends JavaPlugin implements Listener {

    @Override
    public void onEnable() {
        RegistryAccess access = BukkitCustomCropsPlugin.getInstance().getRegistryAccess();
        access.registerBlockMechanic(SugarCaneBlock.instance());
        access.registerItemMechanic(SugarCaneItem.instance());
        getServer().getPluginManager().registerEvents(this, this);
    }

    @EventHandler
    public void onPluginReload(CustomCropsReloadEvent event) {
        // ITEMS and BLOCKS are temporary mappings to associate item id with mechanism type
        // For example, if you have multiple blocks with the same mechanism, such as red wool, green wool, and gray wool, you need to register them in this registry.
        // This registry is cleared when the plugin is reloaded to apply changes to the configuration file
        // NOTE: NEVER USE THE REGISTRIES ANNOTATED WITH @DONOTUSE
        for (String sugarcaneItem : List.of("customcrops:sugarcane_item_id")) {
            Registries.ITEMS.register(sugarcaneItem, SugarCaneItem.instance());
        }
        for (String sugarcaneBlock : List.of("customcrops:sugarcane_block_id")) {
            Registries.BLOCKS.register(sugarcaneBlock, SugarCaneBlock.instance());
        }
    }
}

Then we can create some basic logics for instance placing the sugarcane block

package net.momirealms.customcrops.api.example;

import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.ExistenceForm;
import net.momirealms.customcrops.api.core.InteractionResult;
import net.momirealms.customcrops.api.core.item.AbstractCustomCropsItem;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.util.LocationUtils;
import net.momirealms.customcrops.common.util.Key;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;

import java.util.Collection;
import java.util.List;

public class SugarCaneItem extends AbstractCustomCropsItem {

    private static SugarCaneItem instance;

    public SugarCaneItem() {
        super(Key.key("customcrops", "sugarcane_item"));
    }

    public static SugarCaneItem instance() {
        if (instance == null) {
            instance = new SugarCaneItem();
        }
        return instance;
    }

    @Override
    public InteractionResult interactAt(WrappedInteractEvent wrapped) {
        // Sugarcane should be planted on blocks
        if (wrapped.existenceForm() != ExistenceForm.BLOCK)
            return InteractionResult.PASS;
        // Sugarcane should be planted on the upper face
        if (wrapped.clickedBlockFace() != BlockFace.UP)
            return InteractionResult.PASS;
        // Get the location of the clicked block
        Location location = wrapped.location();

        Block block = location.getBlock();
        // should be planted on sand or dirt
        if (block.getType() != Material.SAND && block.getType() != Material.RED_SAND && block.getType() != Material.DIRT) {
            return InteractionResult.PASS;
        }

        // should be planted near water
        outer: {
            for (BlockFace face : List.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST)) {
                if (block.getRelative(face).getType() == Material.WATER) {
                    break outer;
                }
            }
            return InteractionResult.PASS;
        }

        // check if the place is suitable for planting
        Location sugarcaneLocation = location.clone().add(0,1,0);
        if (!suitableForPlant(sugarcaneLocation)) {
            return InteractionResult.PASS;
        }

        // reduce the amount of item
        wrapped.itemInHand().setAmount(wrapped.itemInHand().getAmount() - 1);


        // Get the CustomCrops world and add block state
        CustomCropsWorld<?> world = wrapped.world();
        CustomCropsBlockState newBlockState = SugarCaneBlock.instance().createBlockState();
        world.addBlockState(Pos3.from(sugarcaneLocation), newBlockState);

        // place the block
        BukkitCustomCropsPlugin.getInstance().getItemManager().placeBlock(sugarcaneLocation, "customcrops:sugarcane_block_id");

        return InteractionResult.COMPLETE;
    }

    private boolean suitableForPlant(Location location) {
        Block block = location.getBlock();
        if (block.getType() != Material.AIR) return false;
        Location center = LocationUtils.toBlockCenterLocation(location);
        Collection<Entity> entities = center.getWorld().getNearbyEntities(center, 0.5,0.5,0.5);
        entities.removeIf(entity -> (entity instanceof Player || entity instanceof Item));
        return entities.isEmpty();
    }
}

Now you should be able to plant the sugarcane (I used a pineapple as a substitute in the gif because I haven't prepared the model for sugarcane yet)

Then we can configure the grow/break logics for sugarcane

package net.momirealms.customcrops.api.example;

import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.block.AbstractCustomCropsBlock;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.misc.NamedTextColor;
import net.momirealms.customcrops.api.util.LocationUtils;
import net.momirealms.customcrops.common.util.Key;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;

import java.util.Collection;
import java.util.Optional;

public class SugarCaneBlock extends AbstractCustomCropsBlock {

    private static SugarCaneBlock instance;

    private SugarCaneBlock() {
        super(Key.key("customcrops", "sugarcane_block"));
    }

    @Override
    public boolean isBlockInstance(String id) {
        return id.equals("customcrops:sugarcane_block_id");
    }

    public static SugarCaneBlock instance() {
        if (instance == null) {
            instance = new SugarCaneBlock();
        }
        return instance;
    }

    @Override
    public void randomTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location, boolean offlineTick) {
        // firstly check if it's the top block
        Pos3 upper = location.add(0,1,0);
        Optional<CustomCropsBlockState> upperState = world.getBlockState(upper);
        if (upperState.isPresent() && upperState.get().type() instanceof SugarCaneBlock) {
            return;
        }

        // get the current height
        int height = 1;
        for (int i = 1; i < 4; i++) {
            Pos3 under = location.add(0,-i,0);
            Optional<CustomCropsBlockState> optionalState = world.getBlockState(under);
            if (optionalState.isPresent() && optionalState.get().type() instanceof SugarCaneBlock) {
                height++;
            } else {
                break;
            }
        }
        if (height >= 4) return;

        // check if there's enough space for growing
        Location upperBukkitLocation = upper.toLocation(world.bukkitWorld());
        // switch to the main thread
        BukkitCustomCropsPlugin.getInstance().getScheduler().sync().run(() -> {
            if (!suitableForGrow(upperBukkitLocation)) {
                return;
            }
            // place the upper block
            BukkitCustomCropsPlugin.getInstance().getItemManager().placeBlock(upperBukkitLocation, "customcrops:sugarcane_block_id");
            world.addBlockState(upper, createBlockState());
        }, upperBukkitLocation);
    }

    private boolean suitableForGrow(Location location) {
        Block block = location.getBlock();
        if (block.getType() != Material.AIR) return false;
        Location center = LocationUtils.toBlockCenterLocation(location);
        Collection<Entity> entities = center.getWorld().getNearbyEntities(center, 0.5,0.5,0.5);
        entities.removeIf(entity -> (entity instanceof Player || entity instanceof Item));
        return entities.isEmpty();
    }

    @Override
    public void onBreak(WrappedBreakEvent event) {
        Location location = event.location();
        Pos3 pos3 = Pos3.from(location);
        CustomCropsWorld<?> world = event.world();

        // build the item
        ItemStack itemToDrop = BukkitCustomCropsPlugin.getInstance().getItemManager().build(null, "customcrops:sugarcane_item_id");

        // break the upper blocks
        for (int i = 0; i < 4; i++) {
            Pos3 upper = pos3.add(0, i,0);
            Optional<CustomCropsBlockState> optionalState = world.getBlockState(upper);
            if (optionalState.isPresent() && optionalState.get().type() instanceof SugarCaneBlock) {
                world.removeBlockState(upper);
                Location temp = upper.toLocation(world.bukkitWorld());
                BukkitCustomCropsPlugin.getInstance().getItemManager().removeBlock(temp);
                temp.getWorld().dropItemNaturally(temp, itemToDrop);
            } else {
                break;
            }
        }
    }

    @Override
    public NamedTextColor insightColor() {
        return NamedTextColor.YELLOW;
    }
}

Last updated