Removing Boiler Plate Code

Currently adding a new butterfly to the Butterfly Mod requires writing too much boiler plate code. This week I looked into simplifying things to the point where we can generate a java file and have the rest of the code automatically register any new butterflies.

Adding a butterfly to the mod takes a long time. Too long. One of the major problems I identified was the huge amount of boiler plate code you needed to write just to add a new butterfly. So I set out to rewrite the code to be more generic so that it would be much quicker and simpler to add a butterfly to the mod.

Registry IDs


A lot of items, entities, and blocks would have a list of IDs to use when registering. So each item would have a group of static strings looking like this:

    public static final String ADMIRAL_NAME = "bottled_butterfly_admiral";
    public static final String BUCKEYE_NAME = "bottled_butterfly_buckeye";
    public static final String CABBAGE_NAME = "bottled_butterfly_cabbage";
    public static final String CHALKHILL_NAME = "bottled_butterfly_chalkhill";
    public static final String CLIPPER_NAME = "bottled_butterfly_clipper";
    public static final String COMMON_NAME = "bottled_butterfly_common";
    public static final String EMPEROR_NAME = "bottled_butterfly_emperor";
    public static final String FORESTER_NAME = "bottled_butterfly_forester";
    public static final String GLASSWING_NAME = "bottled_butterfly_glasswing";
    public static final String HAIRSTREAK_NAME = "bottled_butterfly_hairstreak";
    public static final String HEATH_NAME = "bottled_butterfly_heath";
    public static final String LONGWING_NAME = "bottled_butterfly_longwing";
    public static final String MONARCH_NAME = "bottled_butterfly_monarch";
    public static final String MORPHO_NAME = "bottled_butterfly_morpho";
    public static final String RAINBOW_NAME = "bottled_butterfly_rainbow";
    public static final String SWALLOWTAIL_NAME = "bottled_butterfly_swallowtail";
    public static final String PEACOCK_NAME = "bottled_butterfly_peacock";

There are two blocks, four entities, and six items that have constants like this. This means that every time we add a butterfly we need to add constants in 12 different places. Not only is this inefficient, it can lead to potential bugs if you happened to forget one of the places to add them.

To fix this, I replaced all of these constants with a static method that would generate the ID based on the butterfly species.

    public static String getRegistryId(int butterflyIndex) {
        return "bottled_butterfly_" + ButterflySpeciesList.SPECIES[butterflyIndex];
    }

I’ll explain ButterflySpeciesList further down, but for now all you need to know is that it is a list of all butterfly species in the mod.

Using these methods we can now register the entities more efficiently. So naturally, we need to change how we register things as well.

Registration


Registration of these objects was just as inefficient as maintaining the registry IDs. We would register each new species manually, leading us to add 12 new registrations for each species. For example, this is the code to register the butterfly net item:

    public static final RegistryObject<Item> BUTTERFLY_NET = INSTANCE.register(ButterflyNetItem.EMPTY_NAME,
            () -> new ButterflyNetItem(-1));
    public static final RegistryObject<Item> BUTTERFLY_NET_ADMIRAL = INSTANCE.register(ButterflyNetItem.ADMIRAL_NAME,
            () -> new ButterflyNetItem(0));
    public static final RegistryObject<Item> BUTTERFLY_NET_BUCKEYE = INSTANCE.register(ButterflyNetItem.BUCKEYE_NAME,
            () -> new ButterflyNetItem(1));
    public static final RegistryObject<Item> BUTTERFLY_NET_CABBAGE = INSTANCE.register(ButterflyNetItem.CABBAGE_NAME,
            () -> new ButterflyNetItem(2));
    public static final RegistryObject<Item> BUTTERFLY_NET_CHALKHILL = INSTANCE.register(ButterflyNetItem.CHALKHILL_NAME,
            () -> new ButterflyNetItem(3));
    public static final RegistryObject<Item> BUTTERFLY_NET_CLIPPER = INSTANCE.register(ButterflyNetItem.CLIPPER_NAME,
            () -> new ButterflyNetItem(4));
    public static final RegistryObject<Item> BUTTERFLY_NET_COMMON = INSTANCE.register(ButterflyNetItem.COMMON_NAME,
            () -> new ButterflyNetItem(5));
    public static final RegistryObject<Item> BUTTERFLY_NET_EMPEROR = INSTANCE.register(ButterflyNetItem.EMPEROR_NAME,
            () -> new ButterflyNetItem(6));
    public static final RegistryObject<Item> BUTTERFLY_NET_FORESTER = INSTANCE.register(ButterflyNetItem.FORESTER_NAME,
            () -> new ButterflyNetItem(7));
    public static final RegistryObject<Item> BUTTERFLY_NET_GLASSWING = INSTANCE.register(ButterflyNetItem.GLASSWING_NAME,
            () -> new ButterflyNetItem(8));
    public static final RegistryObject<Item> BUTTERFLY_NET_HAIRSTREAK = INSTANCE.register(ButterflyNetItem.HAIRSTREAK_NAME,
            () -> new ButterflyNetItem(9));
    public static final RegistryObject<Item> BUTTERFLY_NET_HEATH = INSTANCE.register(ButterflyNetItem.HEATH_NAME,
            () -> new ButterflyNetItem(10));
    public static final RegistryObject<Item> BUTTERFLY_NET_LONGWING = INSTANCE.register(ButterflyNetItem.LONGWING_NAME,
            () -> new ButterflyNetItem(11));
    public static final RegistryObject<Item> BUTTERFLY_NET_MONARCH = INSTANCE.register(ButterflyNetItem.MONARCH_NAME,
            () -> new ButterflyNetItem(12));
    public static final RegistryObject<Item> BUTTERFLY_NET_MORPHO = INSTANCE.register(ButterflyNetItem.MORPHO_NAME,
            () -> new ButterflyNetItem(13));
    public static final RegistryObject<Item> BUTTERFLY_NET_RAINBOW = INSTANCE.register(ButterflyNetItem.RAINBOW_NAME,
            () -> new ButterflyNetItem(14));
    public static final RegistryObject<Item> BUTTERFLY_NET_SWALLOWTAIL = INSTANCE.register(ButterflyNetItem.SWALLOWTAIL_NAME,
            () -> new ButterflyNetItem(15));
    public static final RegistryObject<Item> BUTTERFLY_NET_PEACOCK = INSTANCE.register(ButterflyNetItem.PEACOCK_NAME,
            () -> new ButterflyNetItem(16));

Not only is this inefficient, it is full of magic numbers that make it even more prone to mistakes. What if I forget to increment the number when I add a new species? Or if the numbers don’t match other registrations?

Thankfully we can fix this quite easily using the new methods we just wrote. Instead of having a group of static butterflies, we can generate these registrations:

    private static RegistryObject<Item> registerButterflyNet(int butterflyIndex) {
        return INSTANCE.register(ButterflyNetItem.getRegistryId(butterflyIndex),
                () -> new ButterflyNetItem(butterflyIndex));
    }

    public static final List<RegistryObject<Item>> BUTTERFLY_NET_ITEMS = new ArrayList<>() {
        {
            for (int i = 0; i < ButterflySpeciesList.SPECIES.length; ++i) {
                add(registerButterflyNet(i));
            }
        }
    };

Now all our butterfly net items are held in this array, and since all registry IDs spawn from the same list of butterflies they are guaranteed to be in the correct order and use the correct Butterfly Index.

This also has the side effect of making it easier to register the extra things these items need (e.g. creative tab location, entity attributes, spawn rules, etc.). For example, our creative tab registry event handler now looks like this:

    @SubscribeEvent
    public static void registerCreativeTabContents(BuildCreativeModeTabContentsEvent event) {

        if (event.getTabKey() == CreativeModeTabs.SPAWN_EGGS) {
            for (RegistryObject<Item> i : BUTTERFLY_SPAWN_EGGS) {
                event.accept(i);
            }

            for (RegistryObject<Item> i : CATERPILLAR_SPAWN_EGGS) {
                event.accept(i);
            }
        }

        if (event.getTabKey() == CreativeModeTabs.NATURAL_BLOCKS) {

            for (RegistryObject<Item> i : BUTTERFLY_EGG_ITEMS) {
                event.accept(i);
            }

            for (RegistryObject<Item> i : CATERPILLAR_ITEMS) {
                event.accept(i);
            }
        }

        if (event.getTabKey() == CreativeModeTabs.TOOLS_AND_UTILITIES) {

            event.accept(BUTTERFLY_NET);
            for (RegistryObject<Item> i : BUTTERFLY_NET_ITEMS) {
                event.accept(i);
            }

            for (RegistryObject<Item> i : BOTTLED_BUTTERFLY_ITEMS) {
                event.accept(i);
            }

            for (RegistryObject<Item> i : BOTTLED_CATERPILLAR_ITEMS) {
                event.accept(i);
            }

            for (RegistryObject<Item> i : BUTTERFLY_SCROLL_ITEMS) {
                event.accept(i);
            }

            event.accept(BUTTERFLY_BOOK);
            event.accept(BUTTERFLY_ZHUANGZI);
        }
    }

Instead of adding each item manually, we can just iterate over the arrays and add them all in a single line. Not only is this easier to read, it is yet more code we don’t need to modify when we add a new species.

We are almost there. Unfortunately there is another complication with our entities that we need to fix before we can finish.

Entity Construction


When I originally implemented butterflies, I wanted to be able to pass in a custom species to their constructor. Unfortunately, when you register an entity, it needs a method that matches the default entity constructor. To get around this, I used static methods to wrap the constructor like so:

    /**
     * Create an Admiral butterfly
     * @param entityType The type of the entity.
     * @param level The current level.
     * @return A newly constructed butterfly.
     */
    @NotNull
    public static Butterfly createAdmiralButterfly(
            EntityType<? extends Butterfly> entityType,
            Level level) {
        return new Butterfly(
                "admiral",
                entityType, level);
    }

The problem here is twofold. First, it’s yet more custom code we need to write every time we add a new species. Second, we need to use the same method or constructor if we want to register entities using the new method.

Thankfully, experience has proved useful here. Instead of passing in the species, I’ve since learned there are a couple of places where you can get the Entity ID during construction. We can then use this to pull out the species of the butterfly.

    public Butterfly(EntityType<? extends Butterfly> entityType,
                     Level level) {
        super(entityType, level);

        String species = "undiscovered";
        String encodeId = this.getEncodeId();
        if (encodeId != null) {
            String[] split = encodeId.split(":");
            if (split.length >= 2) {
                species = split[1];
            }
        }

Now we remove all the static methods we were originally using, and can use ::new in our registrations:

    private static RegistryObject<EntityType<Butterfly>> registerButterfly(int butterflyIndex) {
        return INSTANCE.register(Butterfly.getRegistryId(butterflyIndex),
                () -> EntityType.Builder.of(Butterfly::new, MobCategory.CREATURE)
                .sized(0.3f, 0.4f)
                .build(Butterfly.getRegistryId(butterflyIndex)));
    }

    public static final List<RegistryObject<EntityType<Butterfly>>> BUTTERFLY_ENTITIES = new ArrayList<>() {
        {
            for (int i = 0; i < ButterflySpeciesList.SPECIES.length; ++i) {
                add(registerButterfly(i));
            }
        }
    };

For the DirectionalCreatures we had to make a few extra changes to get this to work, but the principal is the same.

Code Generation


All of the code we have written is based around a single array that holds a list of the butterfly species. The file that holds this list is simple:

package com.bokmcdok.butterflies.world;

/**
 * Generated code - do not modify
 */
public class ButterflySpeciesList {
    public static final String[] SPECIES = {
            "admiral",
            "buckeye",
            "cabbage",
            "chalkhill",
            "clipper",
            "common",
            "emperor",
            "forester",
            "glasswing",
            "hairstreak",
            "heath",
            "longwing",
            "monarch",
            "morpho",
            "rainbow",
            "swallowtail",
            "peacock",
    };
}

The problem here is that we need to keep this list consistent with the data files created for the butterflies. The solution is obvious if you read the comments in the above code snippet.

We use our data generation script to also generate this file. This will keep everything consistent across the entire project – our code and data will be in sync. To generate this file I added a new method to out data generation script.

CODE_GENERATION = "java/com/bokmcdok/butterflies/world/ButterflySpeciesList.java"

def generate_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 + """\",
""")

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

This is as simple as it can be. Now when we add a new species we just need to run this script to generate all the code and data that we need.

Speaking of which…

How to Add a New Butterfly


Before we started cleaning our code to keep things simple, these were the steps we had to do to add a new butterfly:

  1. Add references to GUI textures
  2. Register all blocks, entities, and items
    • Including accessor helpers
    • And Creative Tab contents
    • And Spawn Rules, and renderers, etc.
  3. Add 12 new entity IDs
  4. Add 4 new creation methods for each entity
  5. Update the number of butterflies needed to complete the Butterfly Book.
  6. Generate the data
  7. Add localisation strings
  8. Add new textures
  9. Update the advancements
  10. Update the Butterfly specific data (for the Butterfly Book)
  11. Add biome modifiers
  12. Add the butterfly to frog food

With all the recent changes, these are the steps that are left:

  1. Generate the data
  2. Add new textures
  3. Update the Butterfly specific data (for the Butterfly Book)
  4. Add biome modifiers

There’s still some work to do, but it’s a lot simpler now. No new content has been added, but there are some features I plan on adding in the future that seemed absolutely daunting. Now I’m more confident I can work on these features without burning myself out.