Keeping Their Numbers Down

Butterflies have been taking over. There have been reports that thousands have been spawning, overwhelming servers and lagging out game worlds. So I have spent a large portion of this week working to reduce the butterfly population and ensure that Minecraft servers remain safe.

I tried to make sure that this wouldn’t happen. Unfortunately butterflies tend to grow exponentially, and given the right conditions they can grow to huge numbers very quickly. So I went back to the drawing board and came up with several ways to ensure Minecraft servers didn’t get overwhelmed by butterfly plagues.

Mating

The first solution I came up with was to require the act of <redacted> before butterflies could lay eggs. Obviously simulating the exact intricacies of insect lovemaking in Minecraft is problematic for several reasons, so I kept it simple. Basically if a butterfly is near another butterfly it will become fertilised and can lay an egg. Once it lays the egg it needs to become fertilised again before it can lay another.

In this way, butterflies will be less likely to lay eggs, but players will also be able to breed them by building areas where they need to stay together.

To implement this, we need a new property on the butterflies to represent their fertility.

    // Serializers for data stored in the save data.
    protected static final EntityDataAccessor<Boolean> DATA_IS_FERTILE =
            SynchedEntityData.defineId(Butterfly.class, EntityDataSerializers.BOOLEAN);

    // Names of the attributes stored in the save data.
    protected static final String IS_FERTILE = "is_fertile";

    /**
     * Check if the butterfly can lay an egg.
     * @return TRUE if the butterfly is fertile.
     */
    protected boolean getIsFertile() {
        return entityData.get(DATA_IS_FERTILE);
    }

    /**
     * Set whether the butterfly can lay an egg.
     * @param isFertile Whether the butterfly is fertile.
     */
    private void setIsFertile(boolean isFertile) {
        entityData.set(DATA_IS_FERTILE, isFertile);
    }

We do this so that these values can be saved to the world when the game is shut down.

    /**
     * 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_FERTILE, this.entityData.get(DATA_IS_FERTILE));
    }

    /**
     * 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_FERTILE)) {
            this.entityData.set(DATA_IS_FERTILE, tag.getBoolean(IS_FERTILE));
        }
    }

And we also make sure that this value is synchronised with the clients.

    /**
     * Override to define extra data to be synced between server and client.
     */
    @Override
    protected void defineSynchedData() {
        super.defineSynchedData();
        this.entityData.define(DATA_IS_FERTILE, false);
    }

For the actual behaviour, we modify the customServerAiStep() method so that it checks this value before attempting to lay an egg. If it isn’t fertilised, it instead searches for a nearby butterfly of the same species and becomes fertilised if it finds one.

        if (this.getIsFertile() && this.random.nextInt(320) == 1) {

            // Attempt to lay an egg.

            // <code snipped>

            setIsFertile(false);
        } else {
            // Attempt to mate
            List<Butterfly> nearbyButterflies = this.level().getNearbyEntities(
                    Butterfly.class,
                    TargetingConditions.forNonCombat(),
                    this,
                    this.getBoundingBox().inflate(2.0D));

            for(Butterfly i : nearbyButterflies) {
                if (i.getType() == this.getType()) {
                    setIsFertile(true);
                    break;
                }
            }
        }

Since butterflies spawn in groups, most should become fertilised on spawn. After that it’s up to the whims of pathfinding.

Movement

One thing I noticed about butterflies in my test world is that they tend to stay in the same small area. They don’t move around a lot. This is likely the root cause of butterflies growing exponentially – if they spawn near leaf blocks they won’t move away from them and will keep laying eggs over and over. As a result, you end up with small clusters in the world that will end up with hundreds, maybe even thousands of butterflies spawning.

To fix this I took a look at the movement code I currently had for the butterflies. The code was originally copied from Minecraft’s bats, which is probably why they stay in the same general area.

            this.targetPosition = new BlockPos(
                    (int) this.getX()
                            + this.random.nextInt(7)
                            - this.random.nextInt(7),
                    (int) this.getY()
                            + this.random.nextInt(6)
                            - 2,
                    (int) this.getZ()
                            + this.random.nextInt(7)
                            - this.random.nextInt(7));

The x and z positions here will average out to be zero (mean, medina, and mode), meaning that it is unlikely a butterfly will move very far from its original position. This code is the reason you end up with butterfly clusters.

To fix this I modified this code slightly.

            this.targetPosition = new BlockPos(
                    (int) this.getX() + this.random.nextInt(8) - 4,
                    (int) this.getY() + this.random.nextInt(6) - 2,
                    (int) this.getZ() + this.random.nextInt(8) - 4);

The average movement along the x/z plane will still be zero, but only for the mean. Since it only uses a single random number, its movement will be more evenly distributed, allowing for more freedom of movement. Butterflies can still hang out in the general area where they spawn, but have more of a chance to go out and explore.

Predators

Another way of keeping butterfly numbers down is with natural predators. I already implemented cats as butterfly killers, but it was time to add more.

I thought it would be cool to have frogs able to eat butterflies, but it turns out that frogs use a different method of determining what to eat. Thankfully, it’s easy to add more frog food using tags. We just need to add frog_food.json in the /resources/data/minecraft/tags/entity_types/ folder.

{
  "replace": false,
  "values": [
    "butterflies:admiral",
    "butterflies:buckeye",
    "butterflies:cabbage",
    "butterflies:chalkhill",
    "butterflies:clipper",
    "butterflies:common",
    "butterflies:emperor",
    "butterflies:forester",
    "butterflies:glasswing",
    "butterflies:hairstreak",
    "butterflies:heath",
    "butterflies:longwing",
    "butterflies:monarch",
    "butterflies:morpho",
    "butterflies:rainbow",
    "butterflies:swallowtail"
  ]
}

With these values added to the tag, frogs will now launch their tongues at butterflies as often as they would with slimes.

For other predators we can implement them similar to how we did with cats. I made a list of the mobs I wanted as predators and added each one to the EntityJoinLevelEvent event handler. Due to the way each mob works, we need different priorities and goals, so this method takes that into account.

    /**
     * On joining the world modify entities' goals so butterflies have predators.
     * @param event Information for the event.
     */
    @SubscribeEvent
    public static void onJoinWorld(EntityJoinLevelEvent event) {

        //  Cat
        if (event.getEntity() instanceof Cat cat) {
            cat.targetSelector.addGoal(1, new NonTameRandomTargetGoal<>(
                    cat, Butterfly.class, false, null));
        }

        //  Foxes
        if (event.getEntity() instanceof Fox fox) {
            fox.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(
                    fox, Butterfly.class, false));
        }

        //  Ocelots and Parrots
        if (event.getEntity() instanceof Ocelot ||
            event.getEntity() instanceof Parrot) {

            Mob mob = (Mob)event.getEntity();
            mob.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(
                    mob, Butterfly.class, false));
        }

        //  Spiders, Cave Spiders, Witches, and Zombies of all kinds
        if (event.getEntity() instanceof Spider ||
            event.getEntity() instanceof Witch ||
            event.getEntity() instanceof Zombie) {

            Mob mob = (Mob)event.getEntity();
            mob.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(
                    mob, Butterfly.class, false));
        }

        //  Wolf
        if (event.getEntity() instanceof Wolf wolf) {
            wolf.targetSelector.addGoal(5, new NonTameRandomTargetGoal<>(
                    wolf, Butterfly.class, false, null));
        }
    }
}

I wanted to add bats as a predator as well, however the code for bats is extremely simple and it would take a lot of effort to change their behaviour so they can do this. Assuming it’s even possible. So I decided to drop them from the list of predators.

It also turns out that parrots can’t attack, which makes sense since they are peaceful mobs. Adding the ability to attack is actually pretty easy, unfortunately this would have a knock-on effect to other parrot behaviour which would alter the gameplay too much. I decided not to add an attack to them, but having butterflies as a target means they will look at them and follow them around. I thought this was cute even if they don’t actually attack them, so I kept it in.

Not Many Eggs

Another obvious solution was to limit the number of eggs each butterfly can lay. If each butterfly lays 10 eggs, it won’t be long before they kill a server. But if each one only lays 1 or 2 eggs, then server death is less likely.

To implement this I added a new property, similar to IS_FERTILE above. The properties are also added to the save data and synced to clients. I use Math.max() to ensure the number of eggs never goes into the negatives.

    // Serializers for data stored in the save data.
    protected static final EntityDataAccessor<Integer> DATA_NUM_EGGS =
            SynchedEntityData.defineId(Butterfly.class, EntityDataSerializers.INT);

    // Names of the attributes stored in the save data.
    protected static final String NUM_EGGS = "num_eggs";

    /**
     * Get the number of eggs this butterfly can lay.
     * @return The number of eggs left for this butterfly.
     */
    protected int getNumEggs() {
        return entityData.get(DATA_NUM_EGGS);
    }

    /**
     * Set the number of eggs this butterfly can lay.
     * @param numEggs The number of eggs remaining.
     */
    private void setNumEggs(int numEggs) {
        entityData.set(DATA_NUM_EGGS, Math.max(0, numEggs));
    }

    /**
     * Reduce the number of eggs the butterfly can lay by 1.
     */
    private void useEgg() {
        setNumEggs(getNumEggs() - 1);
    }

Now all we need is to add a check before we run any egg-laying code in the customServerAiStep(). We also call useEgg() after an egg is laid to reduce the number available.

        if (getNumEggs() > 0) {
            if (getIsFertile() && this.random.nextInt(320) == 1) {

                // Attempt to lay an egg.

                // <snip>

                if (ButterflyLeavesBlock.swapLeavesBlock(
                        level,
                        position,
                        EntityType.getKey(this.getType()))) {
                    setIsFertile(false);
                    useEgg();
                }
            } else {
                // Attempt to mate

                // <snip>
            }
        }

To allow for butterflies to continue to grow, we also add a small chance that they will have double the number of eggs in finalizeSpawn().

        //  Small chance the butterfly has more eggs.
        if (this.random.nextInt(16) == 1) {
            this.setNumEggs(2);
        }

This will allow for slow growth in the right conditions, allowing players to continue breeding butterflies given the correct conditions.

Maximum Density

The nuclear option is to simply limit the number of butterflies that can spawn in a specific area. This is the easiest change to make, but also the most effective. We just add an extra check to customServerAiStep().

            // Don't mate if there are too many butterflies in the area already.
            List<Butterfly> numButterflies = this.level().getNearbyEntities(
                    Butterfly.class,
                    TargetingConditions.forNonCombat(),
                    this,
                    this.getBoundingBox().inflate(32.0D));

            if (numButterflies.size() < 32) {

                // <snip egg-laying code.

            }

Configuration

All of these changes will significantly reduce the amount of butterflies that will breed. However, it’s possible we may have gone too far in the other direction. To mitigate this, I’ve also added some server configuration options that can be used to tweak how often butterflies spawn on servers.

In order to add a configuration, all we need to do is create a class to configure our new settings.

public class ButterfliesConfig {
    public static final ForgeConfigSpec SERVER_CONFIG;

    public static ForgeConfigSpec.DoubleValue doubleEggChance;
    public static ForgeConfigSpec.IntValue eggLimit;
    public static ForgeConfigSpec.IntValue maxDensity;
    public static ForgeConfigSpec.BooleanValue enableLifespan;

    static {
        ForgeConfigSpec.Builder configBuilder = new ForgeConfigSpec.Builder();
        setupServerConfig(configBuilder);
        SERVER_CONFIG = configBuilder.build();
    }

    private static void setupServerConfig(ForgeConfigSpec.Builder builder) {
        builder.comment("This category holds configs for the butterflies mod.");
        builder.push("Butterfly Options");

        doubleEggChance = builder
                .comment("Defines the chance a butterfly has double the eggs.")
                .defineInRange("double_egg_chance", 0.0625, 0, 1);

        eggLimit = builder
                .comment("Defines how many eggs each butterfly can lay.")
                .defineInRange("egg_limit", 1, 0, 1024);

        maxDensity = builder
                .comment("Defines how many butterflies can be in a 32x32 region" +
                        " before breeding is disabled. If set to zero, this is disabled")
                .defineInRange("max_density", 16, 0, 1024);

        enableLifespan = builder
                .comment("If set to TRUE butterflies will die naturally.")
                .define("enable_lifespan", true);

        builder.pop();
    }
}

There are four configuration options I decided to add. I may add more later, and I may change the defaults based on further playtesting.

Double Egg Chance

There is a chance that every butterfly will have double the eggs of a normal butterfly. In the code above I set it to 1/16, so here I set it to 0.0625 (i.e. 1/16 in decimal). We can stop this from ever happening by setting this to zero. We can then use this by altering the spawn code to use this variable instead of a hard coded value.

        if (this.random.nextDouble() < ButterfliesConfig.doubleEggChance.get()) {
            this.setNumEggs(ButterfliesConfig.eggLimit.get() * 2);
        } else {
            this.setNumEggs(ButterfliesConfig.eggLimit.get());
        }

Egg Limit

By default we limit each butterfly to 1 egg. But this might not be enough. Or it might be too many. So we can change this option so that we create butterflies with more eggs. The code above uses eggLimit to set the number of eggs, possibly doubling it if the RNG tells it to.

Max Density

By default we disable breeding if there are more than 16 butterflies nearby. We may want to increase/decrease this limit, or even disable it entirely, which is why this option exists. Using this in our code is simple.

            int maxDensity = ButterfliesConfig.maxDensity.get();
            if (maxDensity == 0 || numButterflies.size() <= maxDensity) {

                // <snip egg-laying code.

            }

Enable Lifespan

Butterflies will die naturally after a number of days. Some people may be saddened by this, so I’ve added an option to disable this. By adding a check to the lifespan code we can make use of this configuration option.

        // If the caterpillar gets too old it will die. This won't happen if it
        // has been set to persistent (e.g. by using a name tag).'
        if (ButterfliesConfig.enableLifespan.get()) {
            if (!this.isPersistenceRequired() &&
                    this.getAge() >= 0 &&
                    this.random.nextInt(0, 15) == 0) {
                this.kill();
            }
        }

Progress

This week was a whole lot of working on stuff I never intended on doing. It’s interesting how much new work can come out of a project from things you never expected to happen. But it’s all worth it – you end up with a better, more stable mod that people can enjoy playing. Which, of course, is the end goal of all this.

Big shout out to Toxidious for calling this out and helping me to figure out how to improve this aspect of the mod. Working on this has actually given me a few new ideas for new features I may implement in the future.

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.