Generating More Butterfly Data

This week I focused on improving data generation for the mod. This doesn’t add any new features to the mod, however it does mean that adding new features to the mod will be much easier in the future. And being able to iterate new features faster is always a bonus.

When I added the peacock butterfly last week, I made a note of all the steps needed, and the potential ways I could reduce some of the work before adding new butterflies in the future. This week I started working on the easier problems. In the end the goal is to reduce the amount of work to add a new butterfly to adding some textures and some spawn rules, allowing for the rest of the data to be generated automatically.

Butterfly Book Challenge


When I originally implemented the Butterfly Book challenge I hardcoded the total number of butterflies needed for it to complete. This works, as long as the total number of butterflies doesn’t change. Of course, last week I added a new butterfly, and this means that the total number has changed and the hardcoded value is now wrong.

To fix this, I added a new method to the ButterflyData class that returns the number of butterflies. I’m doing it here because I have plans to add other creatures that use this class in the future that may not actually be butterflies.

    /**
     * Returns the total number of butterfly species in the mod.
     * @return The total number of butterflies.
     */
    public static int getNumButterflySpecies() {
        return BUTTERFLY_ENTRIES.size();
    }

Now we just modify ButterflyBookItem to use this new method instead of a magic number.

        if (newPages.size()  >= ButterflyData.getNumButterflySpecies()) {
            newTag.putInt(CompoundTagId.CUSTOM_MODEL_DATA, 1);
        }

That’s already one less thing we need to do when we add a new butterfly!

Frog Food


A while ago we made it so that butterflies have a few extra predators. One of those predators is the frog, but this means manually adding every species to the frog_food.json file. Since this file is just a list of all the butterfly species, it should be easy enough to generate.

I modified the generate_butterfly_files.py script to include a new function that can generate the frog_food.json.

# Frog food class used to generate JSON.
class FrogFood(object):
    replace = False
    values = []

    # Initialise with a set of values.
    def __init__(self, values):
        self.values = values
        self.replace = False

    # Convert the class to a JSON string.
    def toJSON(self):
        return json.dumps(
            self,
            default=lambda o: o.__dict__,
            sort_keys=True,
            indent=4)

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

    print(values)

    frog_food = FrogFood(values)

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

The class is designed to hold the structure and generate the JSON data. Then all we need to do is generate the list of butterflies and write out the result to the file.

Now when we run the script, it will automatically add any new butterflies to this list, and swamps will become dangerous places for butterflies to live.s

Butterfly Scroll Textures


The textures that are used by butterfly scrolls and the butterfly book are currently hardcoded in a global array. This means that we need to add a new entry to the array when we add a new butterfly. But they can easily be generated just like so much other data in this mod.

First we remove the SCROLLS array from ButterflyTextures.java. Then, in our ButterflyData we add a method that can generate a texture’s ResourceLocation.

    /**
     * Gets the texture to use for a specific butterfly
     * @param butterflyIndex The butterfly index.
     * @return The resource location of the texture to use.
     */
    public static ResourceLocation indexToButterflyScrollTexture(int butterflyIndex) {
        String entityId = indexToEntityId(butterflyIndex);
        return new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/" + entityId + ".png");
    }

Then we can use this method in code alongside the butterfly index instead of indexing into an array. For example, we can update our butterfly book screen to use the following.

    public void render(@NotNull GuiGraphics guiGraphics, int x, int y, float unknown) {
        super.render(guiGraphics, x, y, unknown);
        int i = (this.width - 192) / 2;
        guiGraphics.blit(ButterflyData.indexToButterflyScrollTexture(butterflyIndex), i, 2, 0, 0, 192, 192);
    }

Now we only need to create a new texture when we add a new butterfly. The code will handle the rest for us automatically

Localisation


Our localisation has two problems. The first is that there are many repeated strings. The second is that many of the strings we do use, could be generated via a a script.

Removing Duplicates

A lot of items in the Butterfly mod have the same name. This has led to multiple localisation strings, all of which are the same. As an example, the Butterfly Net and its variants uses the following localisation entries.

  "item.butterflies.butterfly_net": "Butterfly Net",
  "item.butterflies.butterfly_net_full": "Butterfly Net",
  "item.butterflies.butterfly_net_admiral": "Butterfly Net",
  "item.butterflies.butterfly_net_buckeye": "Butterfly Net",
  "item.butterflies.butterfly_net_cabbage": "Butterfly Net",
  "item.butterflies.butterfly_net_chalkhill": "Butterfly Net",
  "item.butterflies.butterfly_net_clipper": "Butterfly Net",
  "item.butterflies.butterfly_net_common": "Butterfly Net",
  "item.butterflies.butterfly_net_emperor": "Butterfly Net",
  "item.butterflies.butterfly_net_forester": "Butterfly Net",
  "item.butterflies.butterfly_net_glasswing": "Butterfly Net",
  "item.butterflies.butterfly_net_hairstreak": "Butterfly Net",
  "item.butterflies.butterfly_net_heath": "Butterfly Net",
  "item.butterflies.butterfly_net_longwing": "Butterfly Net",
  "item.butterflies.butterfly_net_monarch": "Butterfly Net",
  "item.butterflies.butterfly_net_morpho": "Butterfly Net",
  "item.butterflies.butterfly_net_rainbow": "Butterfly Net",
  "item.butterflies.butterfly_net_swallowtail": "Butterfly Net",
  "item.butterflies.butterfly_net_peacock": "Butterfly Net",

Obviously this is inefficient. We have 18 strings that are all the same, when we should only be using one. To fix this we can override getName() in our item classes to force them to use a specific localisation string.

    private static final String NAME = "block.butterflies.bottled_butterfly";

    /**
     * Overridden so we can use a single localisation string for all instances.
     * @param itemStack The stack to get the name for.
     * @return The description ID, which is a reference to the localisation
     *         string.
     */
    @NotNull
    @Override
    public Component getName(@NotNull ItemStack itemStack) {
        return Component.translatable(NAME);
    }

By doing this for all items, we not only reduce the number of localisation strings, it also means we don’t need to keep adding new ones every time we add a new butterfly.

Another problem with the strings is that we have multiple entries that are the same for different objects. For example, the butterflies have an item string and an entity string that are the same.

  "item.butterflies.morpho": "Morpho Butterfly",

  ...

  "entity.butterflies.morpho": "Morpho Butterfly",

For both consistency and efficiency we should only use one of these strings. I have decided to adopt the entity strings, as they will also work with mods that display entity names. This is as simple as altering ButterflyContainerItem to use the entity string instead.

        if (entity != null) {
            translatable = "entity." + entity.toString().replace(':', '.');
        }

We make a similar change in CaterpillarItem, which is modified a bit more to use a butterfly index instead of a cached resource location. This is something I’m going to keep iterating on – ensuring that the butterfly index is used as a standard in more places.

After making these fixes and deleting unnecessary strings, we have reduced the total number of localisation strings from around 300 to less than 150. There are no longer any repeated strings, which means that if we ever introduce any other localisations it will be less work to translate.

Generating Placeholders

With the number of strings optimised, we can now look into generating strings automatically. We can easily generate the English localisations of many strings by reading in the JSON data and modifying it to add any missing strings. If strings already exist, they will not be replaced. In this way we can override anything that is generated if we are unhappy with it.

I wrote a new function in my python generation script to do this.

# Generates localisation strings if they don't already exist.
def generate_localisation_strings():
    with open(LOCALISATION, 'r', encoding="utf8") as input_file:
        json_data = json.load(input_file)

    for i in BUTTERFLIES:
        test_key = "item.butterflies." + i + "_egg"
        if test_key not in json_data:
            json_data[test_key] = i.capitalize() + " Butterfly Egg"

        test_key = "entity.butterflies." + i
        if test_key not in json_data:
            json_data[test_key] = i.capitalize() + " Butterfly"

        test_key = "entity.butterflies." + i + "_caterpillar"
        if test_key not in json_data:
            json_data[test_key] = i.capitalize() + " Caterpillar"

        test_key = "entity.butterflies." + i + "_chrysalis"
        if test_key not in json_data:
            json_data[test_key] = i.capitalize() + " Chrysalis"

        test_key = "gui.butterflies.fact." + i
        if test_key not in json_data:
            json_data[test_key] = ""

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

This will add strings for Butterfly Eggs, Butterflies, Caterpillars, and Chrysalises if they are missing. It will also add an empty fact string that can be filled out.

Now we don’t have to worry about localisation when we add a new butterfly. We should add a new string for the butterfly fact, but if we don’t it will just appear empty in the game, rather than displaying a string ID.

Generate More


We’ve already reduced the amount of work we need to do in order to add a new butterfly. But we can do more. Generating achievement data is the next thing I will be looking at, then I will try to automate registering new butterflies. If all goes well, adding a new butterfly won’t take anywhere near as much work, and will be less prone to mistakes 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.