Naturally Spawning Caterpillars

Originally the butterfly mod was supposed to be a simple mod that adds butterflies to the world. It’s growing a little beyond that, with the butterflies actually having a complete life cycle. Unfortunately it’s actually pretty rare that a player will stumble upon a caterpillar right now.

To fix this, I implemented natural spawning for caterpillars and chrysalises into the mod. This was actually trickier than it seemed at first, since these entities can only exist on leaf blocks, and can also be attached to any side of these blocks.

To solve this problem, I worked backwards, starting from ensuring the entity is attached to the leaf block, up to finding a valid spawn with a leaf block nearby.

Attaching to the Leaf Block


Since I wanted caterpillars and chrysalises to essentially follow the same rules for spawning, I moved their finalizeSpawn() overrides into DirectionalCreature so that both would use the same code.

Within finalizeSpawn(), we now check every direction of the spawn position for a leaf block. If there is one, then we update the entity’s spawn direction and block position so it is attached to that block. By default, we set this to be below the entity, just to cover any corner cases.

    /**
     * Set persistence if we are spawning from a spawn egg.
     * Attach to leaf block if not already attached.
     * @param levelAccessor Access to the level.
     * @param difficulty The local difficulty.
     * @param spawnType The type of spawn.
     * @param groupData The group data.
     * @param compoundTag Tag data for the entity.
     * @return The updated group data.
     */
    @Override
    public SpawnGroupData finalizeSpawn(@NotNull ServerLevelAccessor levelAccessor,
                                        @NotNull DifficultyInstance difficulty,
                                        @NotNull MobSpawnType spawnType,
                                        @Nullable SpawnGroupData groupData,
                                        @Nullable CompoundTag compoundTag) {
        if (spawnType == MobSpawnType.SPAWN_EGG) {
            setPersistenceRequired();
        }

        if (!levelAccessor.hasChunkAt(getSurfaceBlockPos())) {
            this.setSurfaceBlockPos(this.blockPosition().below());
            this.setSurfaceDirection(Direction.DOWN);
        }

        if (levelAccessor.hasChunkAt(getSurfaceBlockPos()) &&
                !levelAccessor.getBlockState(getSurfaceBlockPos()).is(BlockTags.LEAVES)) {

            for (Direction direction : Direction.values()) {
                BlockPos surfacePosition = this.blockPosition().relative(direction);
                if (levelAccessor.hasChunkAt(surfacePosition) &&
                        levelAccessor.getBlockState(surfacePosition).is(BlockTags.LEAVES)) {

                    this.setSurfaceDirection(direction);
                    this.setSurfaceBlockPos(surfacePosition);

                    Vec3 position = this.position();
                    double x = position.x();
                    double y = position.y();
                    double z = position.z();

                    switch (direction) {
                        case DOWN -> y = Math.floor(position.y());
                        case UP -> y = Math.floor(position.y()) + 1.0d;
                        case NORTH -> z = Math.floor(position.z());
                        case SOUTH -> z = Math.floor(position.z()) + 1.0d;
                        case WEST -> x = Math.floor(position.x());
                        case EAST -> x = Math.floor(position.x()) + 1.0d;
                    }

                    this.moveTo(x, y, z, 0.0F, 0.0F);

                    break;
                }
            }
        }

        return super.finalizeSpawn(levelAccessor, difficulty, spawnType, groupData, compoundTag);
    }

You will notice some calls to hasChunkAt() before we get a block position. This solves a problem that happens infrequently where a caterpillar spawns at the edge of a chunk. If the block we try to access is in an unloaded chunk, then the server will crash. So, before we access a block state, we make sure that the chunk is loaded first, otherwise we skip to the next block.

Finding the Leaf Block


Before we spawn an entity, we need to make sure there is a leaf block to attach to. For this, we need a method to check a spawn position is valid. We add a static method to the DirectionalCreature class.

    /**
     * Check if a directional creature can spawn in this position.
     * @param entityType The type of the entity.
     * @param level The current level.
     * @param spawnType The type of spawn.
     * @param blockPos The position we want to spawn the entity.
     * @param random A random source.
     * @return TRUE if there is a leaf block nearby.
     */
    public static boolean checkDirectionalSpawnRules(EntityType<? extends Mob> entityType,
                                                     LevelAccessor level,
                                                     MobSpawnType spawnType,
                                                     BlockPos blockPos,
                                                     RandomSource random) {
        for (Direction direction : Direction.values()) {
            BlockPos surfacePosition = blockPos.relative(direction);
            if (level.hasChunkAt(surfacePosition)) {
                if (level.getBlockState(surfacePosition).is(BlockTags.LEAVES)) {
                    return true;
                }
            }
        }

        return false;
    }

Simply put, we check every block around the position, and if it is a leaf block, we return TRUE to indicate a valid spawn.

Now we need to register these new rules when we respond to a SpawnPlacementRegisterEvent. This is one example, but I add rules for all 16 caterpillars and chrysalises.

        event.register(CATERPILLAR_HEATH.get(),
                SpawnPlacements.Type.NO_RESTRICTIONS,
                Heightmap.Types.MOTION_BLOCKING,
                 DirectionalCreature::checkDirectionalSpawnRules,
                SpawnPlacementRegisterEvent.Operation.AND);

With these rules in place caterpillars should, in theory, always spawn on leaf blocks. It’s also possible to spawn on all sides of the block, not just on top of them.

The final thing to do is to add biome modifiers to the data, similar to the ones we already have for butterflies. I add one for each butterfly and chrysalis, copying their spawn rules from the butterfly species they belong to.

{
  "type": "forge:add_spawns",
  "biomes": [
    "minecraft:plains",
    "minecraft:sunflower_plains",
    "minecraft:savanna",
    "minecraft:savanna_plateau",
    "minecraft:windswept_hills",
    "minecraft:windswept_savanna",
    "minecraft:meadow",
    "minecraft:cherry_grove"
  ],
  "spawners":
  {
    "type": "butterflies:common_caterpillar",
    "weight": 10,
    "minCount": 3,
    "maxCount": 5
  }
}

With this all in place, we now have naturally spawning caterpillars and chrysalises. But there was a problem. Doing all this highlighted a bug in my caterpillar code.

The Gravity Problem


During my tests I would consistently run into a game-breaking bug. The game would run fine for a while, but at some point entities would just stop updating and I wouldn’t be able to interact with them. This is an indication of a server crash.

I couldn’t get any information from the debugger – there were no error logs, and no obvious place where it was crashing. So I ran the game in debug mode and paused the debugger after a crash. After checking the call stack of each thread, I managed to track the problem down to this piece of code.

    /**
     * Only apply gravity if the caterpillar isn't attached to a block.
     */
    @Override
    public boolean isNoGravity() {
        boolean isNoGravity = true;

        if (this.level().isEmptyBlock(getSurfaceBlock())) {
            setSurfaceDirection(Direction.DOWN);
            setSurfaceBlock(this.blockPosition().below());
            this.targetPosition = null;
            isNoGravity = false;
        }

        return isNoGravity;
    }

The problem may not be obvious, as it wasn’t to me either. The actual issue is that this is called from places in code where you shouldn’t be accessing block data. This method should return true/false, and that’s it.

The solution was to move the place where it figures this out into the customServerAiUpdate() method. Now, isNoGravity is a member variable that gets returned by this method, and we add this similar code to the update method instead:

            // Update gravity
            isNoGravity = true;

            if (this.level().hasChunkAt(getSurfaceBlockPos())) {
                if (this.level().isEmptyBlock(getSurfaceBlockPos())) {

                    setSurfaceDirection(Direction.DOWN);
                    setSurfaceBlockPos(this.blockPosition().below());
                    this.targetPosition = null;
                    isNoGravity = false;
                }
            }

This means that the original method only needs to return this value, and now we update it in a place where we can access block states.

This is a bug that would be in the base mod, however it seems that thanks to caterpillars not spawning too often that it hasn’t been a problem thus far.

Fun Stuff


Someone reported an issue with caterpillars disappearing last week. I had the same issue with the 1.19.2, which I fixed by expanding the bounding boxes used for culling the entities. I thought it wouldn’t be a problem in 1.20, but apparently with some optimization mods it can start happening again.

So I created a quick fix based on the 1.19.2 version and asked if they could test it for me, since I couldn’t reproduce it locally. They confirmed it was fixed and then sent me a screenshot of what they were trying to do.

Apparently there’s a mod called Pehkui that allows you to resize entities, and they wanted giant caterpillars! Considering how much time I’ve spent making sure they can be small, it’s funny that at least one person wanted the exact opposite.

I kind of want to see what a giant butterfly would look like now…

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.