The Copycat of Butterflies

This week was supposed to be a simple implementation of a new butterfly. However, I ended up going off on a couple of tangents, fixing a few issues with the code here and there. Ultimately though, the Common Mime Butterfly is the latest addition to Bok’s Banging Butterflies.

The Mimic Concept


I spotted a Common Mimic Butterfly near the rice fields of Vang Vieng. It’s known as the Mimic, because it tries to mimic other butterflies. This particular butterfly is mimicking the Crow Butterfly, the Asian cousin of the Monarch Butterfly. You can tell it’s a Mimic by looking at the markings at the end of the wings.

Of course, I decided to add this butterfly to Bok’s Banging Butterflies, only I wanted to add a bit of a twist. What if the Mimic could actually mimic other butterflies in the mod? So, the concept I had for this butterfly was to have it alter its appearance whenever it tried to flee from any other creature.

It seemed simple enough. But I kept getting distracted by the details.#

How Big is the Common Mime?


My first distraction came when I started designing the butterfly’s stats, and couldn’t figure out what size I wanted it to be. Up until now, I had really just been using a gut feeling based on research, rather than the actual sizes of each butterfly.

I decided this wasn’t good enough, and started collecting the wingspans of all the (real) butterflies in the mod. By comparing them to the sizes I had already set on them, I came up with this chart:

SizeAvg. Wingspan (mm)
Tiny<20
Small21 – 40
Medium41 – 75
Large76 – 120
Huge121+

Now I have a definitive way of setting the size of each butterfly. Most butterflies already fit into these sizes, however there were a few moths I had to change. A lot of the clothes moth variants were reduced to Tiny in size, while others were increased in size. The worst offender was the Oak Silk moth, which instead of being Small was increased to Huge size.

Okay, now that’s done I can get back to working on the Common Mime.

Habitat Problems


Except, something didn’t sit right with me when I was looking at the ButterflyData. Here’s the enumeration I use to set the Habitat for each butterfly. Can you see the problem?

    public enum Habitat {
        NONE,
        FORESTS,
        FORESTS_AND_PLAINS,
        ICE,
        JUNGLES,
        PLAINS,
        NETHER,
        FORESTS_AND_WETLANDS,
        PLAINS_AND_SAVANNAS,
        PLAINS_AND_WETLANDS,
        HILLS_AND_PLATEAUS,
        FORESTS_PLAINS_WETLANDS,
        VILLAGES,
        WETLANDS
    }

FORESTS_AND_WETLANDS. FOREST_PLAINS_WETLANDS. I was manually creating enumerated values for each possible combination of Habitats. While it’s not really an issue now, this could lead to the size of this enumeration growing exponentially.

The solution was obvious. Butterflies should have an array of Habitats. This way butterflies can have any combination of preset habitats without having to add new enumerated values:

  "habitats": [
    "hills",
    "plateaus"
  ],

Supporting this in Python was easy. I could just use the same code to look for values in an array as I do for strings:

    if "forests" in habitat:

So I didn’t need to make any changes to this script, but I still needed to support it in Java. I changed the type for habitats to use a List of Habitats, and wrote some code to extract the data from an array rather than a single string value:

                // Extract Habitats
                JsonArray habitatData = object.get("habitats").getAsJsonArray();
                List<Habitat> habitats = new ArrayList<>();
                for (int i = 0; i < habitatData.size(); ++i) {
                    try {
                        Habitat habitat = EnumExtensions.searchEnum(Habitat.class, habitatData.get(i).getAsString());
                        habitats.add(habitat);
                    } catch (IllegalArgumentException e) {

                        // The value specified is invalid, so make sure it's written to the log.
                        LogUtils.getLogger().error("Invalid habitat([{}]) specified on [{}]",
                                habitatData.get(i).getAsString(),
                                object.get("entityId") != null ? object.get("entityId").getAsString() : "unknown");
                    }
                }

Now that this data is available, I needed to rewrite the code that displays the list of Habitats. Instead of looking up a specific string, this code generates a comma-separated list of all Habitats where you can find the butterfly.

            // If there are no habitats we still need a string.
            if (entry.habitats().isEmpty()) {
                component.append(Component.translatable("gui.butterflies.habitat.none"));
            }

            // When this flag is true we add commas
            boolean comma = false;
            for (Habitat habitat : entry.habitats()) {
                if (comma) {
                    component.append(Component.translatable("gui.butterflies.habitat.comma"));
                }

                switch (habitat) {
                    case FORESTS -> component.append(Component.translatable("gui.butterflies.habitat.forests"));
                    case HILLS -> component.append(Component.translatable("gui.butterflies.habitat.hills"));
                    case JUNGLES -> component.append(Component.translatable("gui.butterflies.habitat.jungles"));
                    case PLAINS -> component.append(Component.translatable("gui.butterflies.habitat.plains"));
                    case ICE -> component.append(Component.translatable("gui.butterflies.habitat.ice"));
                    case NETHER -> component.append(Component.translatable("gui.butterflies.habitat.nether"));
                    case PLATEAUS -> component.append(Component.translatable("gui.butterflies.habitat.plateaus"));
                    case SAVANNAS -> component.append(Component.translatable("gui.butterflies.habitat.savannas"));
                    case WETLANDS -> component.append(Component.translatable("gui.butterflies.habitat.wetlands"));
                    case VILLAGES -> component.append(Component.translatable("gui.butterflies.habitat.villages"));
                    default -> {
                    }
                }

                // If there is more than one habitat, create a comma-separated list.
                comma = true;
            }

The last piece of the puzzle was network synchronisation. I needed to ensure that the data was sent correctly, and to do this I can use writeCollection and readList:

// Writes any collection into a network buffer. This can be used for anything
// that derives from `Collection`, including lists.
collectionBuffer.writeCollection(i.habitats(), FriendlyByteBuf::writeEnum);

// Reads a list from a network buffer.
buffer.readList((x) -> x.readEnum(ButterflyData.Habitat.class));

Now the enumeration for Habitats looks a lot more sensible:

public enum Habitat {
        NONE,
        FORESTS,
        FORESTS_AND_PLAINS,
        ICE,
        JUNGLES,
        PLAINS,
        NETHER,
        SAVANNAS,
        HILLS,
        PLATEAUS,
        VILLAGES,
        WETLANDS
    }

Now I can move onto implementing the Common Mime Butterfly.

No, You Need to Fix Navigation First


The way I wanted to implement the mimicry was to create a new goal based on AvoidEntityGoal. Only I noticed that butterflies actually never use this goal. I tried everything to get the goal to work as intended and kept hitting walls. Changing priorities didn’t work. Removing all other goals didn’t work. If I had any hair left I would have been tearing it out at this stage.

I decided to do some debugging and dig into the code. I found that, as part of the checks for whether or not AvoidEntityGoal could be used, there was a call to isStableDestination on the path navigation. This checks to see if the target is a solid block, and returns false if it isn’t. Surprisingly, this check is also in FlyingPathNavigation.

Since butterflies are designed to target random blocks in the air most of the time, this meant that AvoidEntityGoal could never be used by them. to solve this, I overrode this method in the path navigation for butterflies:

        FlyingPathNavigation navigation = new FlyingPathNavigation(this, level) {

            /**
             * All destinations are stable for butterflies.
             * @param blockPos The block position of the destination.
             * @return Always TRUE.
             */
            @Override
            public boolean isStableDestination(@NotNull BlockPos blockPos) {
                return true;
            }

        };

Now, every destination is stable for a butterfly, and I can finally start working on the Common Mimic. For real this time.

Butterfly Mimic Goal


The concept for the Mimic Goal was to simply have the Common Mimic use a different texture when is in the AvoidEntityGoal state. I struggled to get this working for too long a time, when I realised what the problem was. Goals are set on the server, and textures are set on the client. To do this, I would need to rely on some synchronised data in the Butterfly class.

    protected static final EntityDataAccessor<String> DATA_MIMIC_TEXTURE =
            SynchedEntityData.defineId(Butterfly.class, EntityDataSerializers.STRING);

I created a method to set this texture based on a Butterfly Index that would eventually be used by my custom goal.

    /**
     * Set the texture index for mimics.
     * @param i The texture index.
     */
    public void setMimicTextureIndex(int i) {

        if (i >= 0) {
            ButterflyData data = ButterflyData.getEntry(i);
            if (data != null) {

                String species = data.entityId();
                entityData.set(DATA_MIMIC_TEXTURE, "textures/entity/butterfly/butterfly_" + species + ".png");
            }
        } else {
            entityData.set(DATA_MIMIC_TEXTURE, "");
        }
    }

Then I updated the getTexture method so that it would return the Mimic texture if it was valid:

    /**
     * Get the texture to use for rendering.
     * @return The resource location of the texture.
     */
    public ResourceLocation getTexture() {
        String mimicTexture = entityData.get(DATA_MIMIC_TEXTURE);
        if (!mimicTexture.isEmpty()) {
            return ResourceLocation.fromNamespaceAndPath(ButterfliesMod.MOD_ID, mimicTexture);
        }

        return this.texture;
    }

Now this is in place, I can create a custom goal for the butterfly to use instead of AvoidEntityGoal. This goal would inherit from AvoidEntityGoal, and simply set a random texture when the goal starts, and invalidate the texture when the goal stops.

/**
 * Goal used by the common mimic butterfly.
 */
public class ButterflyMimicGoal extends AvoidEntityGoal<LivingEntity> {

    //  The butterfly mob.
    private final Butterfly butterflyMob;

    /**
     * Construction
     * @param mob The butterfly mob.x
     * @param avoidClass The class to avoid.
     * @param range The range to start avoiding the class.
     * @param walkSpeedModifier Modifier for the walk speed.
     * @param sprintSpeedModifier Modifier for the sprint speed.
     * @param predicateOnAvoidEntity The predicate to run on the avoid entity.
     */
    public ButterflyMimicGoal(Butterfly mob,
                              Class<LivingEntity> avoidClass,
                              float range,
                              double walkSpeedModifier,
                              double sprintSpeedModifier,
                              Predicate<LivingEntity> predicateOnAvoidEntity) {
        super(mob, avoidClass, range, walkSpeedModifier, sprintSpeedModifier, predicateOnAvoidEntity);

        this.butterflyMob = mob;

    }

    /**
     * When the goal starts, set a random texture on the butterfly.
     */
    @Override
    public void start() {
        super.start();

        // Set butterfly texture index to random number.
        int i = butterflyMob.getRandom().nextInt(ButterflyData.getNumButterflySpecies());
        butterflyMob.setMimicTextureIndex(i);
    }

    /**
     * When the goal ends, revert back to the default texture.
     */
    @Override
    public void stop() {
        super.stop();

        // Set butterfly texture index to -1.
        butterflyMob.setMimicTextureIndex(-1);
    }

    /**
     * Used for debug information.
     * @return The name of the goal.
     */
    @NotNull
    @Override
    public String toString() {
        return "Mimic / Texture = [" + this.butterflyMob.getTexture().toString() +
                "]";
    }
}

It’s almost all in place, but there was one last detour I needed before everything could be pieced together.

Traits


A few butterflies have very specific abilities or features, many of which use slightly hacky code in order to work. As an example, here’s how I ensure that Forester Butterflies aren’t afraid of cats:

        // Forester butterflies are not scared of cats.
        if (Objects.equals(getData().entityId(), "forester"))

It relies on the name of the butterfly containing a specific string. This means it can’t be applied to other butterflies easily, and that it may apply to a new butterfly accidentally (e.g. if I add a Green Forester Butterfly that’s supposed to be scared of cats). It also means that the butterfly’s behaviour will change if I rename it for whatever reason.

This isn’t a good solution, so I decided to add Traits to the butterflies to define these features. To start with I only have four, but I will be adding more as time goes on:

    // The various traits butterflies can have.
    public enum Trait {
        CATFRIEND,     // Is not scared of cats.
        CHRISTMASSY,   // Changes appearance on Christmas.
        MIMICRY,       // Mimics other butterflies when scared.
        MOTHWANDERER   // Is attracted to light like a moth to a flame.
    }

Since I want butterflies to be able to have more than one trait, I implemented this as an array in the ButterflyData. It turns out the work I did for Habitats came in handy here, as I was able to move the code to read an array from JSON data to its own method, getEnumCollection. This meant that I was able to use the same code to read both Habitats and Traits:

                List<Habitat> habitats = getEnumCollection(object, Habitat.class, "habitats");

                List<Trait> traits = getEnumCollection(object, Trait.class, "traits");

Now I can use these traits to activate new features on butterflies without having to rely on their names. As an example, this is how both CATFRIEND and MIMICRY are handled in the registerGoals method in the Butterfly class:

        // Some butterflies are not scared of cats.
        Predicate<LivingEntity> predicateOnAvoidEntity = Butterfly::isScaredOfEverything;
        if (getData().hasTrait(ButterflyData.Trait.CATFRIEND)) {
            predicateOnAvoidEntity = Butterfly::isNotScaredOfCats;
        }

        // Some butterflies can mimic others.
        if (getData().hasTrait(ButterflyData.Trait.MIMICRY)) {
            this.goalSelector.addGoal(1, new ButterflyMimicGoal(this,
                    LivingEntity.class,
                    10.0F,
                    2.2,
                    2.2,
                    predicateOnAvoidEntity));
        } else {
            this.goalSelector.addGoal(1, new AvoidEntityGoal<>(this,
                    LivingEntity.class,
                    10.0F,
                    2.2,
                    2.2,
                    predicateOnAvoidEntity));
        }

I tested the four butterfly’s already using these traits to make sure they work, and now we can find Common Mimic Butterflies that will change appearance when threatened!

Visual Design


I based the design of the Mimic‘s default texture on the picture I took in Vang Vieng. It came out looking a little like a differently shaped clipper with some white highlights.

It looks pretty good in-game as well, but the highlight for me is watching it change its appearance as it tries to avoid other entities.

A New Release


Since I’m not fixing bugs, but adding new features, I’ve release version 6.2.0 of Bok’s Banging Butterflies, so you can play with this new butterfly right now!

The version also fixes a couple of bugs. I had to update the Gradle projects for both 1.19.2 and 1.21.1. I’m not sure what exactly broke in the first place, but updating the projects seems to have worked. I also ported a fix for the Butterfly Microscope from 1.21.4 to 1.21.1, as it was also present in the earlier version.

So there’s a new butterfly for you to collect! Catching them all just became a little harder. But don’t rush too much, for next week I’m going to add another butterfly to the collection.