Upgrading Butterflies to NeoForge

Changes are on the horizon for the Minecraft Modding community. While I don’t fully understand all the drama, Forge may be on its way out, making NeoForge the new way of making mods. In order to prepare for this new world, I’ve decided to create a 1.20.4 version of the mod that runs in NeoForge.

While I can’t speak to the drama that’s happening, the long and short is that it looks like Forge may become legacy soon, with most of the team working on the NeoForged project. I thought it would be a good idea to get a head start on upgrading before things start to shift.

Gradle


The first step in porting the mod is to update the Gradle project. To start with this, I simply downloaded the latest NeoForged MDK and extracted it into my project folder (in a new branch, of course). This overwrote the Forge Gradle project that was already in there.

It took a good amount of nudging, but once I had the project reloaded and ready to go, I attempted to compile the project. As expected, there were build errors. I knew this wasn’t going to be that easy. Time to dig in and figure out how to get this thing running.

The Little Things


So there were a lot of errors to get through. I tried to get through the simple stuff first, so I could work on the more advanced errors one by one. Most of the errors were due to a change in import locations. Basically everywhere you imported from net.minecraftforge.* you would instead use net.neoforged.*.

Next were a few things that had been renamed. For example FMLJavaModLoadingContext was now just ModLoadingContext. I found NeoForged’s Discord extremely useful for this part. Any replacements I couldn’t figure out could be found through a quick search – someone else was bound to have asked it before.

Next up were registries. These were a little bit more involved. In several places I had relied on the ForgeRegistries, which had been removed. Thankfully the vanilla code has the BuiltInRegistries that do the same thing.

The other small thing was that RegistryObjects had been replaced with DeferredHolders. These take two parameters: one being the registry you want to add to, the other being the type of object you want to add. For our purposes this was as simple as changing (e.g) RegistryObject<Block> to DeferredHolder<Block, Block>.

This fixed most of the compile errors that I was getting. There were a couple of others that were going to need a bit more work.

The Block Entity Solution


The BottledButterflyBlock has an Block Entity attached to it, which have changed slightly in NeoForge. They now require a codec() method which I didn’t know how to implement. I started to look for documentation, but before I got far it hit me.

The Block Entity was originally used to store the type of butterfly that was in the bottle. However, when I fixed the recipes, I also removed the reliance on the block entity. The only reason I kept it in was so that players upgrading from older version of the mod wouldn’t experience any errors. This would be the first version of the mod for 1.20.4, and wouldn’t even use the block entity.

So instead of figuring out how to fix it, I just removed the block entity.

Networking


The last error I was struggling with was that CustomPayloadEvent didn’t seem to be anywhere in NeoForge. I asked around on the Discord, and they pointed me to the documentation on the reworked network code.

To update the code I needed first to make sure that I registered a channel for my mod. This can be done when subscribing to a RegisterPayloadHandlerEvent.

        /**
         * Register a network payload namespace for our mod.
         *
         * @param event The event fired when payload handlers are being registered.
         */
        @SubscribeEvent
        public static void register(final RegisterPayloadHandlerEvent event) {
            final IPayloadRegistrar registrar = event.registrar(ButterfliesMod.MOD_ID);
            registrar.play(ClientboundButterflyDataPacket.ID, ClientboundButterflyDataPacket::new, handler -> handler
                    .client(ClientPayloadHandler.getInstance()::handleButterflyData));
        }

Reading the data from the buffer is now done by adding a constructor to the data packet, right next to the `write() method.

    /**
     * Construct from a byte buffer. Reads the data ready for use.
     * @param buffer The buffer to read the data from.
     */
    public ClientboundButterflyDataPacket(final FriendlyByteBuf buffer) {
        this((Collection<ButterflyData>) buffer.readCollection(ArrayList::new,
            (entry) -> new ButterflyData(entry.readInt(),
                    entry.readUtf(),
                    entry.readEnum(ButterflyData.Size.class),
                    entry.readEnum(ButterflyData.Speed.class),
                    entry.readEnum(ButterflyData.Rarity.class),
                    entry.readEnum(ButterflyData.Habitat.class),
                    entry.readInt(),
                    entry.readInt(),
                    entry.readInt(),
                    entry.readInt())));
    }

Finally we can handle the packet in a thread safe way by implementing a client payload handler.

/**
 * Handles payloads sent to the client.
 */
public class ClientPayloadHandler {

    //  The static instance of the handler.
    private static final ClientPayloadHandler INSTANCE = new ClientPayloadHandler();

    /**
     * Get the instance of the handler.
     * @return The singleton instance.
     */
    public static ClientPayloadHandler getInstance() {
        return INSTANCE;
    }

    /**
     * Handles the butterfly data.
     * @param data The inbound data record.
     * @param context The context in which the payload was received.
     */
    public void handleButterflyData(@NotNull final ClientboundButterflyDataPacket data,
                                    final PlayPayloadContext context) {

        // Do something with the data, on the main thread
        context.workHandler().submitAsync(() -> {
                    // Extract the data from the payload.
                    Collection<ButterflyData> butterflyData = data.data();

                    // Register the new data.
                    for (ButterflyData butterfly : butterflyData) {
                        ButterflyData.addButterfly(butterfly);
                    }
                })
                .exceptionally(e -> {
                    // Handle exception
                    context.packetHandler().disconnect(Component.translatable("networking.butterflies.data_sync_failed", e.getMessage()));
                    return null;
                });

    }
}

One thing I liked when writing this code is that Component.translatable reported a warning, since the string wasn’t in my en_US.json translation file. By right-clicking on the error, I was able to add the new string there and then, without having to find the json file and add the string manually. I love this feature – it’s going to prevent a lot of missing string errors, and help keep the file up to date with all the strings that should be there.

Rewriting the network code was probably the “hardest” part of the port, but I like the new network code. Having the buffer read/write in the same place makes perfect sense, and allowing a thread safe way to update game data is always a good thing to have.

Data


I could finally compile the game and run it. But I wasn’t done yet. More errors, of course. There were a few issues with event subscriptions not being registered properly. Some of these were actually errors in Forge as well, but NeoForge has better error detection/handling for this stuff.

Fixing these I was done. I could load into the game, create a world, and…

It seemed to work. I could use spawn eggs to create butterflies. Recipes worked. I could catch butterflies. It was all there. Except, no butterflies were spawning. I guessed the problem was with the biome modifiers. With some help from the Discord, I was able to figure out that I just needed to move them from the /forge/ folder to the /neoforge/ folder. I also needed to rename some elements the same way as well (e.g. forge:add_spawns becomes neoforge:add_spawns).

After that it was done. I created a world and saw butterflies and caterpillars spawning naturally in the world. As far as I could tell everything was working. There is now a NeoForge version of the mod for 1.20.4.

Conclusion


I managed to finish this port over the course of a weekend. It wasn’t as simple as just updating the Gradle project, but I never expected it to be. Overall, I think it’s been a worthwhile experience. Maybe I’ll finally port the mod to Fabric one day (don’t wait up, though).

I found the Discord community to be welcoming and willing to help. I tried to work out as much as I could myself, engaging them only when I got stuck. Pretty much every problem I had was solved within minutes.

Would I recommend others port to NeoForge? I’d say yes, and do it sooner than later. If the winds blow in NeoForge’s direction it will be useful to have a version ready to base updates on, and it’ll be easier to do it now before the project diverges further away from Forge.

I’m not 100% committed to NeoForge yet. I’ll carry on supporting the Forge versions of the mod as I create further updates. But it’s nice to have this ready when (if?) the time comes.

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.