I’ve been focusing a lot on caterpillars these last few weeks, and a thought occurred to me. Wouldn’t it be cool to have bottles with caterpillars in as well? I added the idea to the backlog, and I spent this week working on adding bottled caterpillars to the mod,
Way before the first release I implemented bottled butterflies. At the time I was less experienced and had less of an understanding of some of the nuances of the crafting implementation. The main issue with this implementation is the reliance on NBT tags. Setting these tags during crafting seemed easy at first, but as it turns out, causes a few issues with crafting.
So I approached the implementation of bottled butterflies a little differently. Instead of relying on tags, I would have 16 different items and blocks – one for each caterpillar species. It means we have to do a bit of extra work defining the models, localisation, recipes and so on, but it also means we don’t need to rely on potentially broken custom code for this feature to work.
I put together all of the resources first. If you look at the commit you will see there are 16 blockstates (thankfully we can reuse a single block model for all of these), 16 item models, 16 textures, 16 loot tables, 16 recipes (and their unlocks), two new advancements, and several new localisation strings. With these in place, we can move on to coding the new item.
The Block
I started with the BottledCaterpillarBlock
. Rather than reuse the butterfly version, I copied it and removed all references to the Block Entity. Since we will have one item for each caterpillar, we don’t need an entity to save the species – it’s already defined by the block’s type.
We add names for each of the caterpillar types, that we will use to register the new blocks. Since we have 16 different blocks, we can determine the drops with loot tables, meaning we remove the getDrops()
method from this version. Other than this, it’s the same as the BottledButterflyBlock
class.
public class BottledCaterpillarBlock extends Block { // The name this item is registered under. public static final String ADMIRAL_NAME = "bottled_caterpillar_admiral"; public static final String BUCKEYE_NAME = "bottled_caterpillar_buckeye"; public static final String CABBAGE_NAME = "bottled_caterpillar_cabbage"; public static final String CHALKHILL_NAME = "bottled_caterpillar_chalkhill"; public static final String CLIPPER_NAME = "bottled_caterpillar_clipper"; public static final String COMMON_NAME = "bottled_caterpillar_common"; public static final String EMPEROR_NAME = "bottled_caterpillar_emperor"; public static final String FORESTER_NAME = "bottled_caterpillar_forester"; public static final String GLASSWING_NAME = "bottled_caterpillar_glasswing"; public static final String HAIRSTREAK_NAME = "bottled_caterpillar_hairstreak"; public static final String HEATH_NAME = "bottled_caterpillar_heath"; public static final String LONGWING_NAME = "bottled_caterpillar_longwing"; public static final String MONARCH_NAME = "bottled_caterpillar_monarch"; public static final String MORPHO_NAME = "bottled_caterpillar_morpho"; public static final String RAINBOW_NAME = "bottled_caterpillar_rainbow"; public static final String SWALLOWTAIL_NAME = "bottled_caterpillar_swallowtail"; // The bottle's "model". private static final VoxelShape SHAPE = Shapes.or( Block.box(5.0, 0.0, 5.0, 10.0, 1.0, 10.0), Block.box(4.0, 1.0, 4.D, 11.0, 2.0, 11.0), Block.box(3.0, 2.0, 3.0, 12.0, 6.0, 12.0), Block.box(4.0, 6.0, 4.D, 11.0, 7.0, 11.0), Block.box(5.0, 7.0, 5.0, 10.0, 8.0, 10.0), Block.box(6.0, 8.0, 6.0, 9.0, 10.0, 9.0), Block.box(5.0, 10.0, 5.0, 10.0, 12.0, 10.0), Block.box(6.0, 12.0, 6.0, 9.0, 13.0, 9.0)); /** * Create a butterfly block * @param properties The properties of this block */ public BottledCaterpillarBlock(Properties properties) { super(properties); } /** * Ensure we remove the butterfly when the block is destroyed. * @param level Allow access to the current level. * @param position The position of the block. * @param state The current block state. */ @Override public void destroy(@NotNull LevelAccessor level, @NotNull BlockPos position, @NotNull BlockState state) { super.destroy(level, position, state); removeEntity(level, position, Entity.RemovalReason.DISCARDED); } /** * Get the shape of the block. * @param blockState The current block state. * @param blockGetter Access to the block. * @param position The block's position. * @param collisionContext The collision context we are fetching for. * @return The block's bounding box. */ @NotNull @Override @SuppressWarnings("deprecation") public VoxelShape getShape(@NotNull BlockState blockState, @NotNull BlockGetter blockGetter, @NotNull BlockPos position, @NotNull CollisionContext collisionContext) { return SHAPE; } /** * Tell this block to render as a normal block. * @param blockState The current block state. * @return Always MODEL. */ @Override @NotNull public RenderShape getRenderShape(@NotNull BlockState blockState) { return RenderShape.MODEL; } /** * Called when the block is replaced with another block * @param oldBlockState The original block state. * @param level The current level. * @param position The block's position. * @param newBlockState The new block state. * @param flag Unknown. */ @Override @SuppressWarnings("deprecation") public void onRemove(@NotNull BlockState oldBlockState, @NotNull Level level, @NotNull BlockPos position, @NotNull BlockState newBlockState, boolean flag) { super.onRemove(oldBlockState, level, position, newBlockState,flag); removeEntity(level, position, Entity.RemovalReason.KILLED); } /** * Removes a butterfly for the specified reason. * @param level The current level. * @param position The block's position. * @param reason The removal reason. */ private void removeEntity(LevelAccessor level, BlockPos position, Entity.RemovalReason reason) { AABB aabb = new AABB(position); List<Caterpillar> entities = level.getEntitiesOfClass(Caterpillar.class, aabb); for(Caterpillar i : entities) { i.remove(reason); } } }
I’m not removing/modifying the old version since I don’t want to change how bottled butterfly blocks work right now. In the future I will be updating old items to work more like this in order to make item crafting more reliable, but for now I want to maintain backwards compatibility. This change is really a test drive for the fixes I eventually want to implement.
The Item
The BottledCaterpillarItem
class is based on the bottled butterfly implementation, only with all references to NBTs removed. Now we just create a ResourceLocation
when an item is registered, and use that to determine the hover text, and caterpillar to spawn or release.
As with butterflies, caterpillars can be released from jars. However, in this case a caterpillar item will be added to the player’s inventory, rather than it being released on the ground.
/** * Represents a bottled butterfly held in a player's hand. */ public class BottledCaterpillarItem extends BlockItem { // The name this item is registered under. public static final String ADMIRAL_NAME = "bottled_caterpillar_admiral"; public static final String BUCKEYE_NAME = "bottled_caterpillar_buckeye"; public static final String CABBAGE_NAME = "bottled_caterpillar_cabbage"; public static final String CHALKHILL_NAME = "bottled_caterpillar_chalkhill"; public static final String CLIPPER_NAME = "bottled_caterpillar_clipper"; public static final String COMMON_NAME = "bottled_caterpillar_common"; public static final String EMPEROR_NAME = "bottled_caterpillar_emperor"; public static final String FORESTER_NAME = "bottled_caterpillar_forester"; public static final String GLASSWING_NAME = "bottled_caterpillar_glasswing"; public static final String HAIRSTREAK_NAME = "bottled_caterpillar_hairstreak"; public static final String HEATH_NAME = "bottled_caterpillar_heath"; public static final String LONGWING_NAME = "bottled_caterpillar_longwing"; public static final String MONARCH_NAME = "bottled_caterpillar_monarch"; public static final String MORPHO_NAME = "bottled_caterpillar_morpho"; public static final String RAINBOW_NAME = "bottled_caterpillar_rainbow"; public static final String SWALLOWTAIL_NAME = "bottled_caterpillar_swallowtail"; // The butterfly index for this species. private final ResourceLocation species; /** * Construction * @param block The block to place in the world. * @param species The species of caterpillar in the bottle. */ public BottledCaterpillarItem(RegistryObject<Block> block, String species) { super(block.get(), new Item.Properties().stacksTo(1)); this.species = new ResourceLocation(ButterfliesMod.MODID, species); } /** * Adds some helper text that tells us what butterfly is in the net (if any). * @param stack The item stack. * @param level The current level. * @param components The current text components. * @param tooltipFlag Is this a tooltip? */ @Override public void appendHoverText(@NotNull ItemStack stack, @Nullable Level level, @NotNull List<Component> components, @NotNull TooltipFlag tooltipFlag) { String translatable = "item." + species.toString().replace(':', '.'); MutableComponent newComponent = Component.translatable(translatable); Style style = newComponent.getStyle().withColor(TextColor.fromLegacyFormat(ChatFormatting.DARK_RED)) .withItalic(true); newComponent.setStyle(style); components.add(newComponent); super.appendHoverText(stack, level, components, tooltipFlag); } /** * Right-clicking with a full bottle will release the butterfly. * @param level The current level. * @param player The player holding the net. * @param hand The player's hand. * @return The result of the action, if any. */ @Override @NotNull public InteractionResultHolder<ItemStack> use(@NotNull Level level, @NotNull Player player, @NotNull InteractionHand hand) { ItemStack stack = player.getItemInHand(hand); int butterflyIndex = ButterflyData.getButterflyIndex(species); ResourceLocation location = ButterflyData.indexToCaterpillarItem(butterflyIndex); Item caterpillarItem = ForgeRegistries.ITEMS.getValue(location); if (caterpillarItem != null) { ItemStack caterpillarStack = new ItemStack(caterpillarItem, 1); player.addItem(caterpillarStack); } player.setItemInHand(hand, new ItemStack(Items.GLASS_BOTTLE)); return InteractionResultHolder.success(stack); } /** * Placing the item will create an in-world bottle with a butterfly inside. * @param context The context in which the block is being placed. * @return The interaction result. */ @Override @NotNull public InteractionResult place(@NotNull BlockPlaceContext context) { InteractionResult result = super.place(context); if (result == InteractionResult.CONSUME) { Caterpillar.spawn((ServerLevel)context.getLevel(), species, context.getClickedPos(), Direction.DOWN, true); } return result; } }
You can already see how much simpler the implementation is now that we aren’t using tags to hold the species. We don’t even need to add any custom code to the OnItemCrafted
event. Everything just works out of the box.
Registration
Now that we have these classes implemented, we can register them. For the blocks themselves I create a helper method to construct the block with its properties.
private static BottledCaterpillarBlock bottledCaterpillarBlock() { return new BottledCaterpillarBlock(BlockBehaviour.Properties.copy(Blocks.GLASS) .isRedstoneConductor(BlockRegistry::never) .isSuffocating(BlockRegistry::never) .isValidSpawn(BlockRegistry::never) .isViewBlocking(BlockRegistry::never) .noOcclusion() .sound(SoundType.GLASS) .strength(0.3F)); }
Now we can register all our blocks with just a few lines. The block states and loot tables will be automatically linked to these items via their names.
// Bottled Caterpillars public static final RegistryObject<Block> BOTTLED_CATERPILLAR_ADMIRAL = INSTANCE.register(BottledCaterpillarBlock.ADMIRAL_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_BUCKEYE = INSTANCE.register(BottledCaterpillarBlock.BUCKEYE_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_CABBAGE = INSTANCE.register(BottledCaterpillarBlock.CABBAGE_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_CHALKHILL = INSTANCE.register(BottledCaterpillarBlock.CHALKHILL_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_CLIPPER = INSTANCE.register(BottledCaterpillarBlock.CLIPPER_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_COMMON = INSTANCE.register(BottledCaterpillarBlock.COMMON_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_EMPEROR = INSTANCE.register(BottledCaterpillarBlock.EMPEROR_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_FORESTER = INSTANCE.register(BottledCaterpillarBlock.FORESTER_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_GLASSWING = INSTANCE.register(BottledCaterpillarBlock.GLASSWING_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_HAIRSTREAK = INSTANCE.register(BottledCaterpillarBlock.HAIRSTREAK_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_HEATH = INSTANCE.register(BottledCaterpillarBlock.HEATH_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_LONGWING = INSTANCE.register(BottledCaterpillarBlock.LONGWING_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_MONARCH = INSTANCE.register(BottledCaterpillarBlock.MONARCH_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_MORPHO = INSTANCE.register(BottledCaterpillarBlock.MORPHO_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_RAINBOW = INSTANCE.register(BottledCaterpillarBlock.RAINBOW_NAME, BlockRegistry::bottledCaterpillarBlock); public static final RegistryObject<Block> BOTTLED_CATERPILLAR_SWALLOWTAIL = INSTANCE.register(BottledCaterpillarBlock.SWALLOWTAIL_NAME, BlockRegistry::bottledCaterpillarBlock);
To register the items, we just need to reference the blocks above and the species they will contain. We also register them under the Tools and Utilities tab for Creative Mode.
// Bottled Caterpillars public static final RegistryObject<Item> BOTTLED_CATERPILLAR_ADMIRAL = INSTANCE.register(BottledCaterpillarItem.ADMIRAL_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_ADMIRAL, Caterpillar.ADMIRAL_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_BUCKEYE = INSTANCE.register(BottledCaterpillarItem.BUCKEYE_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_BUCKEYE, Caterpillar.BUCKEYE_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_CABBAGE = INSTANCE.register(BottledCaterpillarItem.CABBAGE_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_CABBAGE, Caterpillar.CABBAGE_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_CHALKHILL = INSTANCE.register(BottledCaterpillarItem.CHALKHILL_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_CHALKHILL, Caterpillar.CHALKHILL_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_CLIPPER = INSTANCE.register(BottledCaterpillarItem.CLIPPER_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_CLIPPER, Caterpillar.CLIPPER_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_COMMON = INSTANCE.register(BottledCaterpillarItem.COMMON_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_COMMON, Caterpillar.COMMON_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_EMPEROR = INSTANCE.register(BottledCaterpillarItem.EMPEROR_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_EMPEROR, Caterpillar.EMPEROR_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_FORESTER = INSTANCE.register(BottledCaterpillarItem.FORESTER_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_FORESTER, Caterpillar.FORESTER_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_GLASSWING = INSTANCE.register(BottledCaterpillarItem.GLASSWING_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_GLASSWING, Caterpillar.GLASSWING_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_HAIRSTREAK = INSTANCE.register(BottledCaterpillarItem.HAIRSTREAK_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_HAIRSTREAK, Caterpillar.HAIRSTREAK_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_HEATH = INSTANCE.register(BottledCaterpillarItem.HEATH_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_HEATH, Caterpillar.HEATH_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_LONGWING = INSTANCE.register(BottledCaterpillarItem.LONGWING_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_LONGWING, Caterpillar.LONGWING_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_MONARCH = INSTANCE.register(BottledCaterpillarItem.MONARCH_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_MONARCH, Caterpillar.MONARCH_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_MORPHO = INSTANCE.register(BottledCaterpillarItem.MORPHO_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_MORPHO, Caterpillar.MORPHO_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_RAINBOW = INSTANCE.register(BottledCaterpillarItem.RAINBOW_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_RAINBOW, Caterpillar.RAINBOW_NAME)); public static final RegistryObject<Item> BOTTLED_CATERPILLAR_SWALLOWTAIL = INSTANCE.register(BottledCaterpillarItem.SWALLOWTAIL_NAME, () -> new BottledCaterpillarItem(BlockRegistry.BOTTLED_CATERPILLAR_SWALLOWTAIL, Caterpillar.SWALLOWTAIL_NAME));
Behavior Modification
The last step is to modify the behaviour of caterpillars. We do this by adding data to the Caterpillar
class to specify if it is bottled or not. This data is both saved to the level and synchronised between server and client.
// Serializers for data stored in the save data. protected static final EntityDataAccessor<Boolean> DATA_IS_BOTTLED = SynchedEntityData.defineId(Caterpillar.class, EntityDataSerializers.BOOLEAN); // Names of the attributes stored in the save data. protected static final String IS_BOTTLED = "is_bottled"; /** * Used to add extra parameters to the entity's save data. * @param tag The tag containing the extra save data. */ @Override public void addAdditionalSaveData(@NotNull CompoundTag tag) { super.addAdditionalSaveData(tag); tag.putBoolean(IS_BOTTLED, this.entityData.get(DATA_IS_BOTTLED)); } /** * Override to read any additional save data. * @param tag The tag containing the entity's save data. */ @Override public void readAdditionalSaveData(@NotNull CompoundTag tag) { super.readAdditionalSaveData(tag); // Get the bottle state if (tag.contains(IS_BOTTLED)) { this.entityData.set(DATA_IS_BOTTLED, tag.getBoolean(IS_BOTTLED)); } } /** * Override to define extra data to be synced between server and client. */ @Override protected void defineSynchedData() { super.defineSynchedData(); this.entityData.define(DATA_IS_BOTTLED, false); } /** * Set whether or not the caterpillar is in a bottle. * @param isBottled TRUE if the caterpillar is bottled. */ private void setIsBottled(boolean isBottled) { entityData.set(DATA_IS_BOTTLED, isBottled); }
Next, we alter the spawn()
method so that it takes an extra parameter. We pass true
for isBottled
when a caterpillar is spawned inside a bottle. If it is bottled we set it to be persistent and invulnerable, and we ensure it is facing down. We set its y-position slightly higher so that it appears to be on the bottom of the glass bottle.
public static void spawn(ServerLevel level, ResourceLocation location, BlockPos position, Direction direction, boolean isBottled) { EntityType<?> entityType = ForgeRegistries.ENTITY_TYPES.getValue(location); if (entityType != null) { Entity entity = entityType.create(level); if (entity instanceof Caterpillar caterpillar) { caterpillar.setIsBottled(isBottled); double x = position.getX() + 0.45D; double y = position.getY() + 0.4D; double z = position.getZ() + 0.5D; BlockPos spawnPosition = position.above(); if (isBottled) { direction = Direction.DOWN; y = Math.floor(position.getY()) + 0.07d; spawnPosition = position.below(); caterpillar.setInvulnerable(true); caterpillar.setPersistenceRequired(); } else { // <snip> } } } }
We use this property to disable some behaviours of the caterpillar. First, we always ignore gravity.
if (!this.getIsBottled() && this.level().hasChunkAt(getSurfaceBlockPos()) && this.level().isEmptyBlock(getSurfaceBlockPos())) { setSurfaceDirection(Direction.DOWN); setSurfaceBlockPos(this.blockPosition().below()); this.targetPosition = null; isNoGravity = false; }
Next, caterpillars in jars will not grow into chrysalises.
// Spawn Chrysalis. if (!this.getIsBottled() && this.getAge() >= 0 && this.random.nextInt(0, 15) == 0) { // <snip chrysalis spawn code> }
Finally, we have one minor little quirk in that we ensure that we create a base method for getIsBottled()
in the DirectionalCreature
class that always returns false. This is so we can use it in finalizeSpawn()
to ignore the code that attempts to find a nearby leaf block, otherwise we have immortal caterpillars spawning outside of bottles that are placed near leaves.
Outro
A lot of the work I’ve done on caterpillars has actually laid a bit of groundwork for some upcoming features. Butterfly eggs will become entities that work similar to caterpillars and chrysalises. I’m planning to go back and fix butterfly nets, scrolls, and bottled butterflies based on the code from this article as well.
Other than that, I tidied a few things up here and there, but no real major changes. This is the last thing I have on my list that updates caterpillars for now. So I decided it was time for the Caterpillar Release!
Of course, as soon as I released version 3.0.0 there were some severe bugs found. As it turns out, I had not real understanding of how networking works in Minecraft and this meant the mod was completely broken on servers. Version 3.0.2 fixes these issues, and I’ll talk about networking in Minecraft next week.