Moths, Silk, and Startling

This week I add silk to the butterfly mod and finally add my first moth. This moth has its own unique behaviour, something I will start adding to several other butterflies. Along the way I fixed a bug or two, and discovered a couple more that need to be fixed.

Silk


In the next version of the mod I plan to allow players to harvest silk from specific moth larvae. To support this I added a new silk item. This is a simple item that can be used to craft string and paper. It provides players with a new way to obtain these items without being overpowered.

Adding these items was pretty simple as I’ve done all of this before. So I won’t go into details, but you can see the changes in the pull request. With the addition of a few simple recipes, and an achievement for collecting silk this was a fairly simple feature to implement.

Moth Data Generation


Before adding a moth, I needed to change the data generation code a little bit. The first thing I did was to add clothes to the list of butterflies and generated the data. The problem here is that the generated data would add the moth to butterfly-specific achievements, which is something I didn’t want.

Moth Data

So, after reverting the generated changes to the achievements, I set about creating moth-specific data generation. To start with I created a separate array for moths and called the generate_data() method with this new array.

MOTHS = [
    'clothes'
]

#...

# Python's main entry point
if __name__ == "__main__":
    generate_data_files(BUTTERFLIES)
    generate_data_files(MOTHS)

Now, when new moths are added, their data will be based on the new Clothes Moth that I will be adding later.

Frog Food

Updating the frog_food.json is as easy as just appending the array to the data.

# Generate list of entities to add to frog food.
def generate_frog_food():
    print("Generating frog food...")
    values = []
    for i in BUTTERFLIES:
        values.append("butterflies:" + i)

    for i in MOTHS:
        values.append("butterflies:" + i)

    frog_food = FrogFood(values)

    with open(FROG_FOOD, 'w') as file:
        file.write(frog_food.to_json())

Localisation

For the localisation strings, I added a new loop that generates names for moths that are different to butterflies. I also added a small helper function to make the code easier to read. As before, these names can be overridden and the generator won’t revert them.

# Add a value to the specified JSON data, but only if the key doesn't already
# exist.
def try_add_localisation_string(json_data, key, value):
    if key not in json_data:
        json_data[key] = value


# Generates localisation strings if they don't already exist.
def generate_localisation_strings():
    print("Generating localisation strings...")

    with open(LOCALISATION, 'r', encoding="utf8") as input_file:
        json_data = json.load(input_file)

    for i in BUTTERFLIES:
        name = i.capitalize
        try_add_localisation_string(json_data, "entity.butterflies." + i, name + "Butterfly")
        try_add_localisation_string(json_data, "entity.butterflies." + i + "_caterpillar", name + "Caterpillar")
        try_add_localisation_string(json_data, "entity.butterflies." + i + "_chrysalis", name + "Chrysalis")
        try_add_localisation_string(json_data, "item.butterflies." + i, name + "Butterfly")
        try_add_localisation_string(json_data, "item.butterflies." + i + "_egg", name + "Butterfly Egg")
        try_add_localisation_string(json_data, "item.butterflies." + i + "_caterpillar", name + "Caterpillar")

    for i in MOTHS:
        name = i.capitalize
        try_add_localisation_string(json_data, "entity.butterflies." + i, name + "Moth")
        try_add_localisation_string(json_data, "entity.butterflies." + i + "_caterpillar", name + "Larva")
        try_add_localisation_string(json_data, "entity.butterflies." + i + "_chrysalis", name + "Cocoon")
        try_add_localisation_string(json_data, "item.butterflies." + i, name + "Moth")
        try_add_localisation_string(json_data, "item.butterflies." + i + "_egg", name + "Moth Egg")
        try_add_localisation_string(json_data, "item.butterflies." + i + "_caterpillar", name + "Larva")

    for i in BUTTERFLIES + MOTHS:
        try_add_localisation_string(json_data, "gui.butterflies.fact." + i, "")

    with open(LOCALISATION, 'w', encoding="utf8") as file:
        file.write(json.dumps(json_data,
                              default=lambda o: o.__dict__,
                              sort_keys=True,
                              indent=2))

Advancements

Advancements turned out to be fairly easy. All I needed to do was to modify the function to use parameters (similar to generate_data_files()), and then I could just call the method multiple times.

BUTTERFLY_ACHIEVEMENT_TEMPLATES = "resources/data/butterflies/advancement_templates/butterfly/"
MOTH_ACHIEVEMENT_TEMPLATES = "resources/data/butterflies/advancement_templates/moth/"
BOTH_ACHIEVEMENT_TEMPLATES = "resources/data/butterflies/advancement_templates/both/"

#...

# Generates advancements based on templates that can be found in the specified#
# location.
def generate_advancements(entities, templates):
    #...

# Python's main entry point
if __name__ == "__main__":
    generate_data_files(BUTTERFLIES)
    generate_data_files(MOTHS)
    # generate_data_files(FLOWERS) # Disabled for now due to tulip problem
    generate_frog_food()
    generate_localisation_strings()
    generate_advancements(BUTTERFLIES, BUTTERFLY_ACHIEVEMENT_TEMPLATES)
    generate_advancements(MOTHS, MOTH_ACHIEVEMENT_TEMPLATES)
    generate_advancements(BUTTERFLIES + MOTHS, BOTH_ACHIEVEMENT_TEMPLATES)

Now it is possible to create achievement templates for just butterflies, just moths, or for both butterflies and moths.

Code Generation

Finally we can update the code generation by adding one extra call to the function.

# Generates a Java file with an array containing every species. This array is
# then used to create all the entities and items related to each butterfly, 
# saving a ton of new code needing to be written every time we add a new
# butterfly or moth.
def generate_code():
    print("Generating code...")

    with open(CODE_GENERATION, 'w', encoding="utf8") as output_file:
        output_file.write("""package com.bokmcdok.butterflies.world;

/**
 * Generated code - do not modify
 */
public class ButterflySpeciesList {
    public static final String[] SPECIES = {
""")

        for butterfly in BUTTERFLIES:
            output_file.write("""            \"""" + butterfly + """\",
""")

        for moth in MOTHS:
            output_file.write("""            \"""" + moth + """\",
""")

        output_file.write("""    };
}
""")

Index Generation

This is almost everything. Almost. See, there is still a problem here. The code uses the Butterfly Index to index into the array generated here. Each butterfly and moth’s data contains the index used in code to access that array.

These indexes are entered manually, which leads to the first problem: user error. If an index is entered incorrectly, it will reference the wrong data, and the wrong butterfly will spawn. This is something that has happened before, but I’ve fixed it before any releases have been made.

The second problem is the way we are using two arrays here. I want to still be able to add more butterflies, but if I do this now then every moth’s index will be increased by one. This means that every time I add a butterfly, I will have to manually edit every index for every moth. This could end up being a daunting task down the line.

The solution is, of course, to generate these indexes automatically. To achieve this, I added some extra code to the end of generate_data_files().

# Initial index
BUTTERFLY_INDEX = 0


# Generates data files from the input array based on the first entry in the#
# array.
def generate_data_files(entries):
    print("Generating data files...")

    # Get list of files containing input[0]
    # <snip>

    # Loop Start
    for entry in entries:

        for file in files:

            # <snip>

            with open(new_file, 'r', encoding="utf8") as input_file:
                json_data = json.load(input_file)

            global BUTTERFLY_INDEX
            if "index" in json_data:
                json_data["index"] = BUTTERFLY_INDEX
                BUTTERFLY_INDEX = BUTTERFLY_INDEX + 1

            with open(new_file, 'w', encoding="utf8") as file:
                file.write(json.dumps(json_data,
                                      default=lambda o: o.__dict__,
                                      sort_keys=True,
                                      indent=2))

By using the global BUTTERFLY_INDEX, we can ensure the indexes in the json data and in the Java code matches. It doesn’t matter if these indexes change between versions, since Minecraft will save entities based on their ResourceLocations rather than these indexes.

It’s also one less small task I need to do every time I add a butterfly.

Clothes Moth


Now that the data has been generated for the clothes moth, all I need to do is create the textures for the new moth. After spending some time in Gimp, I had some nice textures ready to use.

I added some biome modifiers for the moth and updated their butterfly data with stats I wanted them to have. The clothes moth is supposed to spawn in villages, so I added them to any biome where villages can spawn.

One thing I want to start adding to the mod are some unique behaviours for several butterflies. Some of the descriptions for the butterflies imply there will be an effect in-game, but currently there isn’t. I created several issues to fill out these new behaviours, but for now I’ll start with the clothes butterflies.

In real life, clothes moths are the moths that lay eggs on your clothes so the larvae can feast on them. To represent this, clothes moths can land and lay eggs on cloth blocks as well as on leaf blocks. I added a new attribute to the ButterflyData called extraLandingBlocks. For now, it will only support NONE and WOOL, but more blocks can be added later.

To support this new attribute, I added a method to the butterfly data that can be used to check if the block is valid.

    /**
     * Check if the current block is a valid landing block.
     * @param blockState The block state to check.
     * @return TRUE if the butterfly can land on the block.
     */
    public boolean isValidLandingBlock(BlockState blockState) {
        if (blockState.is(BlockTags.LEAVES)) {
            return true;
        }

        return extraLandingBlocks == ExtraLandingBlocks.WOOL &&
                blockState.is(BlockTags.WOOL);
    }

I changed all the checks for leaf blocks in the butterfly goals and in the caterpillar entity so that they use this method instead. Now, if I want to add more landing blocks I only need to modify this function. I can also add the behaviour to any butterfly or moth in the future.

Scaredy Butterflies


While working on the mod this week I noticed that butterflies wouldn’t land. I was confused for a long time, but then I remembered the culprit. There was a problem with the AvoidEntity goal.

        this.goalSelector.addGoal(1, new AvoidEntityGoal<>(this, LivingEntity.class, 3, 0.8, 1.33, (x) -> !(x instanceof Butterfly)));

The above code works in preventing butterflies being afraid of each other, but I had forgotten to take something into account. In my test world butterflies had been laying eggs, and a few caterpillars had spawned. These are both also entities, and butterflies were afraid of them since I hadn’t also excluded these entities in the lambda!

So I wrote a helper method that can be used to determine if a butterfly should be afraid or not.

    /**
     * Check if the butterfly is scared of this entity.
     * @param entity The entity that is too close.
     * @return TRUE if butterflies are scared of the entity.
     */
    private static boolean isScaredOf(LivingEntity entity) {
        return !(entity instanceof Butterfly ||
                entity instanceof Caterpillar ||
                entity instanceof ButterflyEgg ||
                entity instanceof Chrysalis);
    }

Now I can update the goal to use this method instead, and butterflies will now be able to land on blocks even if they have caterpillars, eggs, or chrysalises on them.

        this.goalSelector.addGoal(1, new AvoidEntityGoal<>(this, LivingEntity.class, 3, 0.8, 1.33, Butterfly::isScaredOf));

Bugs and More Bugs


This isn’t the end of my buggy woes, however. I found several other bugs while testing out various features, and they obviously need to be fixed. My priority now is to fix these issues before I continue working on moths. However, I’ve done a lot this week so it’s time to take a break.

Next time I’ll be talking about how I fixed all these bugs. Hopefully.