Screwing Up the Life Cycle

Well, I screwed up. It turns out a major selling point of Bok’s Butterflies wasn’t working at all. The life cycle of the butterflies wasn’t working at all. It all boils down to a misunderstanding on my part. So this week I have a new release that fixes this feature.

The Mistake


When I switched butterflies from being Animals instead of AmbientCreatures, I assumed that the age of the animals worked exactly the same way that I implemented. Namely that the age starts at zero, and increases by one every tick.

It turns out that this is not how aging works, at all. The age of both caterpillar and chrysalis mobs remained at zero, which meant that they would never move on to the next stage of their life cycle. Butterflies would still lay eggs, the eggs would hatch into caterpillars, but then the cycle would break.

As soon as I realised what had happened, I worked hard on figuring out how aging actually works for Animals, and worked on a fix for a new release.

How Aging Actually Works in Minecraft


AgeableMobs in Minecraft are either babies, or adults. If they are an adult, their age is zero. If they are a baby, then their age is a negative number, representing the number of ticks until they become an adult. For example, an age of -600 would mean the mob will become an adult in 600 ticks (or around 30 seconds of real time). Usually a baby will be spawned with an age of -24000 ticks, which is around 20 minutes, or a single day/night cycle.

In addition, babies are generally smaller, but have larger heads in proportion to their bodies. This isn’t true of all mobs, but the takeaway is that the model, and thus the collision box, is smaller for babies.

When a baby reaches an age of zero, it becomes an adult, using the larger model, collision box, and now able to be bred with other animals.

The Fix


After spending a lot of time debugging and understanding how aging works, the fix was simple. I just needed to set the age of caterpillars and chrysalises when they spawn.

                caterpillar.setAge(-24000);

                caterpillar.finalizeSpawn(level,
                        level.getCurrentDifficultyAt(position),
                        MobSpawnType.NATURAL,
                        null,
                        null);

Then when we check for the next stage of the life cycle, we check for an age of zero, not 24,000.

            // Spawn Chrysalis.
            if (this.getAge() >= 0 && this.random.nextInt(0, 15) == 0) {
                // Etc...
            }

This almost worked. To test it, I set the age to -600 so I wouldn’t have to wait as long and jumped into the game. No caterpillars spawned.

I spent ages debugging, breaking, stressing, trying to figure out what was going on. The game said the caterpillars were in the level. They were aging properly. And weirdly, butterflies were eventually spawning. Everything seemed fine in the code.

Then I accidentally managed to see one. I had to be really close, and I noticed by using F3+B that its bounding box was really small. I understood what was happening now.

Remember how I said babies have smaller models and bounding boxes? Well, the bounding boxes of baby caterpillars and chrysalises were so small that they would be culled unless you got extremely close to them (less than a single block away).

It took me a while to figure out how to fix this, but the solution I came up with was to override the setAge() method. In the base class this will recalculate the bounding box if the entity changes from adult to baby (or vice versa). I just needed an override that removed this behaviour.

    /**
     * Override so that the bounding box isn't recalculated for "babies".
     * @param age The age of the entity.
     */
    @Override
    public void setAge(int age) {
        this.age = age;
    }

The Good News


The good news is that thanks to the use of feature and development branches, the main branch is still untouched since my last release. This means I was able to implement a fix into main, and create a new release quickly and without having to worry about any changes I’ve made since then.

If you look at the current development branch you will see it is already very different. I’ve moved folders around, implemented new features, and created new bugs. I don’t want to have any of this code in a release until I am happy with it, so it’s good that I still have a main branch without all these changes.

So even if you are working solo on a small project, I think this proves that taking advantage of feature and development branches is an essential skill.

A Second Screw Up


After I fixed the bug with the life cycle, I discovered there was another bug. A couple of people left comments on the CurseForge page saying that there was a crash when they hovered over a full butterfly net in the inventory.

I knew right away that it must be the appendButterflyNameToHoverText() method that was causing the crash, but I had no idea why. And I couldn’t reproduce it when I ran the game locally. So I did something new.

I created a custom mod pack with only my mod and nothing else in the CurseForge app. I loaded into the game, gave myself a butterfly net and went hunting. When I caught a butterfly I opened the inventory and as soon as the cursor went over the full net…

A crash.

Which was good, because I now had a crash log I could look at. And it showed me exactly what the problem was.

java.lang.NoClassDefFoundError: org/codehaus/plexus/util/StringUtils

I was using a method that didn’t exist. That library looked weird though. It didn’t feel like a common include – how did it make it into my project? Is there no standard StringUtils class that I can use?

import org.codehaus.plexus.util.StringUtils;

It turns out that I had included the wrong one. There is another class which does exactly the same thing. All I needed to do was change the import.

import org.apache.commons.lang3.StringUtils;

I tested again with a build using this version of the import and no crash. After another quick release I finally had a mod that people could use properly. Unless someone finds another bug…

But there is also a lesson here. I need to make sure I actually play the mod in a release environment. The reason I didn’t notice this one is because I didn’t test new features via Curseforge. It worked locally because the version of Java my devenv uses has access to libraries that Minecraft doesn’t use.

Curseforge allows you to upload alpha versions of your mods. I’m going to start taking advantage of that feature.

Anyway, check out the new release that actually works.