Porting Things to the Past

A while ago someone asked me to port Bok’s Banging Butterflies to 1.18.2. I made an attempt and failed due to the numerous changes in the code between Minecraft versions. This week I decided to take another look at it, determined to actually create a build that would work.

Small Stuff


When you port to a new version you can start by just updating your gradle.properties file to use the correct version. In my case I wanted Minecraft version 1.18.2 and Forge version 40.3.0. You also need to make sure that you use the correct pack_format in the pack.mcmeta file. Minecraft 1.18.2 uses pack format 8, so I updated the file to use that version.

After this you reload the gradle project, build the code, and you will see hundreds of errors. This can be quite daunting at first, but you’ll find a lot of these to be simple name changes, such as getWorld() being renamed to getLevel(), or Component.translatable(...) being replaced with new TranslatableComponent(...).

Once you get through all of these errors you’ll be left with a few more complex errors, where a system or two have been rewritten. Often as new versions of Minecraft are released, the code for newer features will be refined so that it is better optimised, or more robust. As a mod developer this means you may need to do some small rewrites if you want to support multiple versions of the game.

Build


After fixing the easy stuff, you need to get the mod to build. I was porting from 1.19.2 to 1.18.2, and the two main changes that affected my mod were the way that loot modifiers and banner patterns were registered.

Banner Pattern

I tackled the banners first. The registry for banner patterns wasn’t added until 1.19. In 1.18 you just created the banner pattern in the mod’s constructor. So to add the butterfly pattern to the game I first needed to delete my banner pattern registry, then call BannerPattern.create() in the mod’s constructor:

/**
 * The main entry point for the mod.
 */
@Mod(ButterfliesMod.MOD_ID)
public class ButterfliesMod
{
    // Define mod id in a common place for everything to reference
    public static final String MOD_ID = "butterflies";

    /**
     * Constructor.
     */
    public ButterfliesMod() {
        final IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
        final IEventBus forgeEventBus = MinecraftForge.EVENT_BUS;

        // Create a banner pattern
        BannerPattern.create("BUTTERFLY", "banner_pattern_butterfly", "banner_pattern_butterfly", true);

        // Etc.

}

Then, when I register the banner pattern item, I just create a new instance of BannerPatternItem:

        this.butterflyBannerPattern = deferredRegister.register("banner_pattern_butterfly", () ->
                new BannerPatternItem(BannerPattern.valueOf("BUTTERFLY"), new Item.Properties()
                        .rarity(Rarity.UNCOMMON)
                        .stacksTo(1)
                        .tab(CreativeModeTab.TAB_MISC)));

Now this version of the mod will still be able to support the Butterfly Banner pattern just like all the other versions.

Loot Modifiers

Next I took a look at the loot modifiers. From 1.19 onwards, loot modifiers would use Codecs to modify loot, but before that they used custom serializers. To do this you need to implement a `GlobalLootModifierSerializer for your loot modifier class manually. For example, the serializer I implemented for butterfly loot modifier is as follows:

    /**
     * The Loot Modifier Serializer.
     */
    public static class Serializer extends GlobalLootModifierSerializer<ButterflyLootModifier> {

        // The item registry.
        protected final ItemRegistry itemRegistry;

        /**
         * Construction
         */
        public Serializer(ItemRegistry itemRegistry)
        {
            this.itemRegistry = itemRegistry;
        }

        /**
         * Read Loot Modifier data.
         * @param name The resource name.
         * @param object The Json Object with any extra data.
         * @param conditionsIn The conditions for the loot.
         * @return A new loot modifier.
         */
        @Override
        public ButterflyLootModifier read(ResourceLocation name,
                                          JsonObject object,
                                          LootItemCondition[] conditionsIn) {
            return new ButterflyLootModifier(itemRegistry, conditionsIn);
        }

        /**
         * Write loot modifier data.
         * @param instance The loot modifier.
         * @return Modifier data in JSON format.
         */
        @Override
        public JsonObject write(ButterflyLootModifier instance) {
            return makeConditions(instance.conditions);
        }
    }

To register the loot modifiers, I can just pass in the constructors to the register() method. This is actually easier to do than it is in later versions of Minecraft:

    /**
     * Register the loot modifiers.
     * @param itemRegistry The item registry.
     */
    public void initialise(ItemRegistry itemRegistry) {
        deferredRegister.register("butterfly_loot", () -> new ButterflyLootModifier.Serializer(itemRegistry));
        deferredRegister.register("oak_leaves_loot", () -> new OakLeavesLootModifier.Serializer(itemRegistry));
        deferredRegister.register("trail_ruins_rare_loot", () -> new TrailRuinsRareLootModifier.Serializer(itemRegistry));
    }

After fixing these I was finally able to build the mod. However, this didn’t mean that I would be able to run the game…

Run


I ran the game and it loaded to the main menu. Everything good so far. I created my first test world for 1.18.2 and… the datapack has errors in it.

I took a look at the logs and I could instantly see that it was the butterfly grave structure that was breaking the game:

Caused by: java.lang.IllegalStateException: Unbound values in registry ResourceKey[minecraft:root / minecraft:worldgen/configured_structure_feature]: [butterflies:liangshanbo_grave]

This was a vague error message that didn’t make it obvious to me what the problem was. Obviously something had changed with the terrain generation between versions, but I didn’t know what. I scoured the internet and came across a tutorial from Modding by Kaupenjoe that explained how to do custom structures in 1.18.2.

Even better than that, their GitHub repository had implementations for multiple versions of Minecraft, which meant that I could compare the changes in their tutorial code, and apply the same changes to my own data.

As it turns out, the error message above was telling me that the /configured_structure_feature folder doesn’t exist, since it was renamed to structure in later versions of Minecraft. I renamed the folder, and I went through the JSON files in Kaupenjoe’s project making modifications to my own to match.

After making the changes I loaded into the game again and tried to create a world. This time it loaded the data pack and created a new world. I was now running Bok’s Banging Butterflies in version 1.18.2 of the game!

But it’s still not over. This is where I have to test the features of the mod to make sure that they all work.

Test


I ran in creative and tested as many features as I could. I spawned in butterflies and watched them fly around. I created caterpillars crawling on leaves. The Butterfly Feeder and Butterfly Microscope all worked correctly. Everything seemed to work as designed. But there were still a couple of issues.

Natural Spawns

But when I flew around the world I couldn’t find a single butterfly or caterpillar in the world. This, as it turns out, is because in 1.18.2 spawns aren’t data driven yet. Instead, you have to listen for a BiomeLoadingEvent and add the spawns in code.

So I wrote a quick function that added the spawns based on the butterfly data. I loaded back into the game, but it seemed like it still wasn’t working. Confused, I decided to debug the code to see what was happening. I found that when the method was called, the Butterfly Data was still empty.

In 1.18.2 spawns are added before the data pack is loaded.

Damn. This was a problem because the butterfly data is contained in the data pack. I tried to think of some solutions to this. I could write some code that loads the data manually before this step, but that would add extra overhead during the load, reading the same data an extra time. An easier solution would be to have butterflies spawn in all or some biomes, ignoring the data. It would work, but I wanted to maintain as much parity between versions as possible.

Then it hit me: data generation. My python script already generates an array of butterfly names in code, used to register the entities when the game starts. I could just generate an array of habitats as well. So I modified the generate_code method so that it would also add an array of habitats to a .java file:

# 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(all):
    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 all:
            output_file.write("""            \"""" + butterfly + """\",
""")

        output_file.write("""    };

""")

        output_file.write("""
    public static final ButterflyData.Habitat[] HABITATS = {
""")

        # Generate the butterfly habitat array
        for butterfly in all:
            folders = [BUTTERFLIES_FOLDER, MALE_BUTTERFLIES_FOLDER, MOTHS_FOLDER, MALE_MOTHS_FOLDER, SPECIAL_FOLDER]
            habitat = None
            i = 0

            while habitat is None and i < len(folders):
                folder = folders[i]
                try:
                    with open(BUTTERFLY_DATA + folder + butterfly + ".json", 'r', encoding="utf8") as input_file:
                            json_data = json.load(input_file)

                    habitat = json_data["habitat"]
                except FileNotFoundError:
                    # doesn't exist
                    pass
                else:
                    # exists
                    pass

                i = i + 1

            output_file.write("""            ButterflyData.Habitat.""" + habitat.upper() + """,
""")

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

Running the generator I would get an array in ButterflySpeciesList.java that looked something like this:

    public static final ButterflyData.Habitat[] HABITATS = {
            ButterflyData.Habitat.FORESTS_AND_WETLANDS,
            ButterflyData.Habitat.FORESTS,
            ButterflyData.Habitat.PLAINS_AND_SAVANNAS,
            ButterflyData.Habitat.PLAINS_AND_WETLANDS,
            ButterflyData.Habitat.HILLS_AND_PLATEAUS,
            ButterflyData.Habitat.FORESTS,
            ButterflyData.Habitat.JUNGLES,
            ButterflyData.Habitat.JUNGLES,
            ButterflyData.Habitat.JUNGLES,
            
            // <snip>

    }

Now that it is generated into code, the habitats are available when I respond to the BiomeLoadingEvent, so I was now able to add butterfly spawns for the correct biomes:

    /**
     * Add the spawns for each butterfly.
     * @param event The event we respond to in order to add the villages.
     */
    private void onBiomeLoading(BiomeLoadingEvent event)
    {
        List<RegistryObject<EntityType<? extends Butterfly>>> butterflies = entityTypeRegistry.getButterflies();
        for (int i = 0; i < butterflies.size(); ++i) {
            ButterflyData data = ButterflyData.getEntry(i);
            switch (ButterflySpeciesList.HABITATS[i]) {
                case FORESTS:
                    if (event.getCategory().equals(Biome.BiomeCategory.FOREST)){
                        event.getSpawns().addSpawn(MobCategory.CREATURE,
                                new MobSpawnSettings.SpawnerData(butterflies.get(i).get(), 5, 1, 7));
                    }

                    break;

                case FORESTS_AND_PLAINS:
                    if (event.getCategory().equals(Biome.BiomeCategory.FOREST) ||
                        event.getCategory().equals(Biome.BiomeCategory.PLAINS)) {
                        event.getSpawns().addSpawn(MobCategory.CREATURE,
                                new MobSpawnSettings.SpawnerData(butterflies.get(i).get(), 5, 1, 7));
                    }

                    break;

                // I've cut most of the habitats to keep this code snippet short.

                default:
                    break;
            }
        }

    }

Now I could load into the world, fly around, and I would see butterflies spawning naturally. I though the mod was ready for release until I happened upon one more problem.

Flower Bud Render

I happened to come across a flower bud that a butterfly had created. It was easy to spot, as you can tell from the screenshot:

For some reason it was being rendered as a black box rather than the cutout texture it was supposed to be using. With what I’d learned so far I guessed that it was because the render types for blocks hadn’t been data driven yet.

I guessed correct, as the way it was done in 1.18.2 was during the FMLClientSetupEvent handler. I just needed to make a call for each flower bud in order to set the render type to cutout.

    /**
     * Register the screens with their respective menus.
     * @param event The client setup event.
     */
    private void clientSetup(FMLClientSetupEvent event) {
        event.enqueueWork(
                () -> MenuScreens.register(this.menuTypeRegistry.getButterflyFeederMenu().get(), ButterflyFeederScreen::new)
        );

        event.enqueueWork(
                () -> MenuScreens.register(this.menuTypeRegistry.getButterflyMicroscopeMenu().get(), ButterflyMicroscopeScreen::new)
        );

        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getAlliumBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getAzureBluetBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getBlueOrchidBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getCornflowerBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getDandelionBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getLilyOfTheValleyBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getOrangeTulipBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getOxeyeDaisyBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getPinkTulipBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getPoppyBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getRedTulipBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getWhiteTulipBud().get(), RenderType.cutout());
        ItemBlockRenderTypes.setRenderLayer(blockRegistry.getWitherRoseBud().get(), RenderType.cutout());
    }

After this, I was ready to release the mod for 1.18.2! It’s now available here, on CurseForge, and on Modrinth. Now I’ll just wait until the first bug comes in to let me know what I missed during this process…

Fin


In all this port went a lot smoother than the port for 1.19.2. I was more prepared for what to expect, and open source tutorials like Kaupenjoe’s are excellent resources to help with the changes.

I now have a loose process in place for doing ports:

  1. Fix Simple Compile Errors
    Basically get the error count down as low as possible by fixing all the easy renames/simple changes.
  2. Get it to Compile
    Now tackle the more complex issues that are related to updated/rewritten systems.
  3. Create a World
    At this stage there may be issues with the data pack that need fixing.
  4. Test Everything
    Test as many features as possible, and especially focus on the features you had to rewrite in step 2.

Next week I’ll be working on a 1.21.1 Neoforge version of the mod as it’s the most requested version so far. Neoforge has good documentation for each update so it should be easier to understand what has changed and how to update specific features.

Once that is done, I’ll finally be able to move onto the first feature for the final version of Bok’s Banging Butterflies. And that version may just creep you out a little…