MC Forge Mod Dev Blog: Migrating to Minecraft 1.16

Updated: 13 minutes to read

It is no longer news that Minecraft Forge has stable Minecraft 1.16 support, as the first recommended build for 1.16.x, which is Forge 34.1.0, was released in September 2020, very soon after I published the previous blog post for this serires about an update to my mod project. Development of Forge for Minecraft 1.16.x already had significant progress when the update for my mod was worked on, and I contemplated adding support for 1.16 along with that update. However, after a hard attempt to port my mod to Minecraft 1.16, I decided that because many method names in the Minecraft API source code decompiled by MCP were still not fully deobfuscated, Forge on 1.16 was still immature, and my mod’s update would not ship with 1.16 support.

When I saw the first recommended build for 1.16.x, I downloaded it to find out whether the deobfuscation was complete. I was disappointed: a build that still consisted of lots of meaningless obfuscated method names should never qualify as a stable build in my opinion, but Forge developers decided to say that “this build is production-ready and recommended”, dropping official support for Minecraft 1.14.x at the same time. An implication of Forge ending support for a Minecraft version is that if you ask a question about it on the official forum, you will see a moderator coldly replying “your version is no longer supported, please update to receive support”, and ruthlessly closing the thread, preventing other community members from offering a helping hand. So you release an incomplete version and deprecate an older stable version, forcing people who don’t want to be on the bleeding edge to use 1.15.x? I almost wanted to curse Forge developers for this stupid decision and Forge forum moderators’ frigid treatment of forum threads about unsupported Minecraft versions.

My wait for a Forge build that I think is truly stable continued, and it went a little longer than it should have been, as I got busy with other things in my life. Deobfuscation was satisfactory by October 2020, which helped me complete the majority of the migration to Minecraft 1.16. The only thing that stopped me from finalizing the port was a redesign of text components API in Minecraft itself. My mod needs to read chat messages delivered to the client in their original form, i.e. with the formatting codes, for detecting the start of a Bed Wars game and registering purchases of team upgrades. Unfortunately, Minecraft’s internal method for getting a chat message in formatted form was removed in 1.16 without replacement, so I had to figure out a way to reconstruct a formatted message myself. This was actually not as hard as I had imagined, but I chose to save it for later and did not revisit it until recently.

What Is Deobfuscation?

To answer this question, let us first look at what obfuscation is. Obfuscation is a technique used by software developers who are so eager to protect their software from reverse engineering that they are afraid of other people getting even a tiny clue about how their software is implemented from mere peeks through the function/method names in the program. More specifically, obfuscation entails scrambling function/method names into meaningless symbols, preventing other people from guessing what a function/method does from what it is called.

Mojang has been obfuscating Minecraft for a very long time, and this can definitely be a barrier for people who want to write modifications to the game. The Mod Coder Pack (MCP) was created to remap obfuscated method names in decompiled Minecraft source code to better names that actually tell those methods’ purposes. This process is called deobfuscation.

The result of a complete deobfuscation is MCP mappings, which consist of reasonably meaningful method names (MCP names) enough to give us a rough idea of what each method does, like getMainWindow for example. But those method names are not all generated by magic: they are available thanks to developers' efforts. When an MCP name is not available for a method, it will be assigned a Searge name from Searge mappings, which can be something like func_228018_at_. I would call Searge mappings partially deobfuscated because at least a method’s Searge name is different from its original name in the Minecraft binary, but it still does not tell the method’s purpose, so I would not call it a complete deobfuscation.

It would not be easy to work with a Minecraft Forge release bundled with a version of MCP that still contains significant amount of Searge names. Look at this commit for my mod, which switched to a newer version of Forge with updated MCP mappings, and allowed me to replace the remaining Searge names with MCP names. Would anyone prefer the scrambled Searge names to more expressive MCP names? This is why I was unhappy about Forge releasing a recommended build with incomplete deobfuscation. If it were up to me, I would not stabilize Forge for Minecraft 1.16 until all MCP mappings are done, because counting on mod developers themselves to figure out what each method does is not a responsible act.

P.S. I hope all of us would never obfuscate the programs we write for the sole purpose of keeping the implementation details as a secret, no matter whether we are forced to do it by a manager or are doing it of our own volition. Such act is against the spirit of free software. Everyone should enjoy freedom to computing, which includes the freedom to study how the programs they use are designed and implemented, and the freedom to modify them as they wish.

Enough for rants, let us start looking at the patches to my mod that were necessary for migration to Minecraft 1.16.

Additional Matrix Stack Parameter of GUI Rendering Methods

Many methods pertinent to GUI rendering now requires an additional argument of type com.mojang.blaze3d.matrix.MatrixStack. If you are calling such a method while there is already a variable called matrixStack in the current namespace, then you can use that variable’s value as the argument for free. Otherwise, you may simply create a new matrix stack by calling its default constructor new MatrixStack().

For example, the following code snippet shows how a method that handles rendering of a screen may look like. Notice that it is calling some other GUI rendering methods too, including renderBackground and drawCenteredString.

@Override
public void render(int mouseX, int mouseY, float partialTicks) {
    // Draw the background of the screen
    this.renderBackground();
    // Draw the title
    this.drawCenteredString(this.font, title, width, height, color);
    // Call the super class' method to complete rendering
    super.render(mouseX, mouseY, partialTicks);
}

For Minecraft 1.16.x, the same method would be changed to the one shown below. Notice that both the method signature and every GUI rendering method called inside it now have an extra matrixStack argument:

@Override
public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) {
    this.renderBackground(matrixStack);
    drawCenteredString(matrixStack, this.font, title, width, height, color);
    super.render(matrixStack, mouseX, mouseY, partialTicks);
}

Removed Method for Getting a Settings Option’s Display String

If you have followed the previous blog in this series to create a configuration GUI for your mod, then you need to make changes to every usage of SliderPercentageOption and IteratableOption. The net.minecraft.client.settings.AbstractOption.getDisplayString() method, which returns the option’s translated name, was changed to a protected method getBaseMessageTranslation(), so there is no available public method left in the AbstractOption class that can return the option’s name. If you still want to display the same translated option name as before, you must generated it in your own code by passing the translation key to method I18n.format(String) and concatenating the result with a colon.

import net.minecraft.client.settings.SliderPercentageOption;
import net.minecraft.client.settings.IteratableOption;
import net.minecraft.util.text.StringTextComponent;
import net.minecraft.client.resources.I18n;

// For SliderPercentageOption
this.optionsRowList.addOption(new SliderPercentageOption(
        "hbwhelper.configGui.hudX.title",
        min, max, step,
        unused -> (double) ModSettings.getHudX(),
        (unused, newValue) -> ModSettings.setHudX(newValue.intValue()),
        // BiFunction that returns a string text component in format "<name>: <value>"
        (gs, option) -> new StringTextComponent(
                // Use I18n.format(String) to get a translation key's value
                I18n.format("hbwhelper.configGui.hudX.title")
                + ": "
                + (int) option.get(gs)
        )
));

// For IteratableOption
this.optionsRowList.addOption(new IteratableOption(
        "hbwhelper.configGui.dreamMode.title",
        (unused, newValue) ->
                ModSettings.setDreamMode(DreamMode.values()[
                        (ModSettings.getDreamMode().ordinal() + newValue)
                                % DreamMode.values().length
                ]),
        (unused, option) -> new StringTextComponent(
                I18n.format("hbwhelper.configGui.dreamMode.title")
                + ": "
                + I18n.format(ModSettings.getDreamMode().getTranslateKey())
        )
));

This inevitably leads to repetitions of translation keys in the source code, but it is the most straightforward solution I can find at this moment. After all, we are depending on the internal and undocumented API of Minecraft, so anything can change.

As an alternative, you may choose to extend the SliderPercentageOption and IteratableOption classes to gain access to the protected getBaseMessageTranslation() method, but this involves creating two new classes.

Removed Method for Getting Formatted Text

Many Minecraft players should be familiar with formatting codes starting with the section sign character §. They can be used to colorize text and control text other text styles like bold and italic. Text containing one or more formatting codes is called formatted text.

In Minecraft, there is an internal interface net.minecraft.util.text.ITextComponent, which is the interface for text component objects. Almost every piece of text you see in Minecraft has an underlying text component object that represents it. The ITextComponent interface had a getFormattedText() method that could return the text being represented by the text component with formatting codes but is removed in 1.16, and now there is not any convenient way to obtain the formatted text from a text component as before.

The ability to read chat messages in formatted text form is pivotal to my mod’s core features. It detects the beginning of a Bed Wars game by checking if the client receives the introductory message Hypixel generates when a game starts. It monitors the chat to see if the player’s team have purchased a team upgrade. Although it is still possible to get and parse chat messages without formatting codes in 1.16, this would make my mod vulnerable to attacks from other players who know how it works. They would be able to send a chat message like “Leo3418 has purchased Heal Pool” to trick my mod into registering the Heal Pool team upgrade when nobody in the team has actually bought it. The current implementation of my mod defends against such attacks by matching chat messages with formatting codes, because players cannot send formatted chat messages, and all text messages with formatting codes are sure to be originated from Hypixel.

To deal with this problem, I have written my own utility method that takes an ITextComponent instance and returns the formatted string for it, and then all I need to do is to replace usages of the removed getFormattedText() method with calls to my utility method. Minecraft 1.16’s API is still providing enough methods for recreate a string with formatting codes by hand, but such procedure is a little bit complicated, so I encapsulated it into a standalone method to reuse the code for different modules in my mod.

- String formattedMsg = event.getMessage().getFormattedText();
+ String formattedMsg = TextComponents.toFormattedText(event.getMessage());

If you are facing the same issue, you are free to use the file that contains my utility method in your own mod, because my mod is released as free software under GNU GPLv3 or later with additional permissions, as long as you follow the license terms.

Additional Parameter of Method for Sending Chat Messages

The net.minecraft.entity.Entity class contains a sendMessage method which allows you to send messages to the user via in-game chat:

import net.minecraft.client.Minecraft;
import net.minecraft.util.text.StringTextComponent;

// Pre-1.16 way to send a chat message from the mod to the player
Minecraft.getInstance().player.sendMessage(new StringTextComponent("hello, world"));

But in Minecraft 1.16, this method requires an additional java.util.UUID argument for the UUID of the player sending the chat message. This argument is only used if you are trying to send a message to another remote player connected to the current multiplayer game from your mod. If you are just sending a message to the local player represented by an instance of net.minecraft.client.entity.player.ClientPlayerEntity, this argument will be ignored, so you can specify whatever value for it, including null. If you do not like passing in null arguments, you can follow Minecraft’s practice of using this method of its own, which is supplying the NIL_UUID constant for the argument:

import net.minecraft.client.Minecraft;
import net.minecraft.util.Util;
import net.minecraft.util.text.StringTextComponent;

Minecraft.getInstance().player.sendMessage(
        new StringTextComponent("hello, world"),
        Util.NIL_UUID
);

Mandatory License Field in mods.toml

Starting from Minecraft Forge 34.1, the mods.toml file for your mod must contain a license field, whose value is intended to state the name of your mod’s license. You can look at the change to my mod’s mods.toml for an example.

New Method for Closing a Screen

Note: This section is only applicable to Minecraft Forge 35.1.x and earlier (which corresponds to Minecraft 1.16.4 and earlier). Please ignore the contents of this section if you are developing on Minecraft Forge 36.1.x (for Minecraft 1.16.5) or later versions.

Unless we are Mojang employees, we have access to only the decompiled version of Minecraft source code, which is not expected to have any documentation, so any knowledge we can have about Minecraft’s API can only come from interpreting and analyzing the decompiled source code, plus experience gained mainly in trial-and-errors. And the empirical knowledge I have about closing a screen implemented based on Minecraft’s net.minecraft.client.gui.screen.Screen class is:

  • The onClose() method in the Screen class should complete any teardown tasks when the screen is being closed, and it must contain a call to net.minecraft.client.Minecraft.displayGuiScreen(Screen) in order to switch back to the parent screen.

  • By default, when the user presses the Esc key in a screen, its onClose() method will be called. Therefore, calling the onClose() method is the way to let a screen close.

In Minecraft 1.16, the intent of the onClose() method has been changed:

  • When the Esc key is pressed, the new closeScreen() method in the Screen class is now called instead. The default implementation of closeScreen() only contains a call to Minecraft.displayGuiScreen(Screen).

  • onClose() should only contain code that needs to be executed when the screen is being closed. It must not call Minecraft.displayGuiScreen(Screen); otherwise, an error would occur.

  • Hence, if you would like to close the current screen, you should no longer call the onClose() method. Instead, you can either directly call Minecraft.displayGuiScreen(Screen) or, as a better way, use the closeScreen() method. Both methods would eventually cause the onClose() method to be called.

How does this actually look? Consider a class like this:

/*
 * OK for 1.14.4 and 1.15.x, but not 1.16.x
 */

import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.button.Button;
import net.minecraft.client.resources.I18n;

public final class ConfigScreen extends Screen {
    /** The parent screen of this screen */
    private final Screen parentScreen;

    public ConfigScreen(Screen parentScreen) {
        // Set screen title
        super(new TranslationTextComponent("hbwhelper.configGui.title",
                HbwHelper.NAME));
        this.parentScreen = parentScreen;
    }

    @Override
    protected void init() {
        ...
        // Add a "Done" button for leaving this screen
        this.addButton(new Button(
                horizontalPosition, verticalPosition, width, height,
                I18n.format("gui.done"),
                // Action performed when the button is pressed
                button -> this.onClose()
        ));
    }

    /** Executes tasks before this screen is closed, then closes this screen */
    @Override
    public void onClose() {
        // Save mod configuration
        ModSettings.save();
        // Display the parent screen to close this screen
        this.minecraft.displayGuiScreen(parentScreen);
    }
}

To port this class to Minecraft 1.16.x, it needs to be modified as this:

/*
 * OK for 1.16.x, but not 1.14.4 or 1.15.x
 */

import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.button.Button;
import net.minecraft.client.resources.I18n;

public final class ConfigScreen extends Screen {
    private final Screen parentScreen;

    public ConfigScreen(Screen parentScreen) {
        super(new TranslationTextComponent("hbwhelper.configGui.title",
                HbwHelper.NAME));
        this.parentScreen = parentScreen;
    }

    @Override
    protected void init() {
        ...
        this.addButton(new Button(
                horizontalPosition, verticalPosition, width, height,
                // By the way, the type for button text is also changed
                // from String to ITextComponent
                new TranslationTextComponent("gui.done"),
                // Note that another method is used to close the screen instead
                button -> this.closeScreen()
        ));
    }

    /** Only executes tasks before this screen is closed */
    @Override
    public void onClose() {
        ModSettings.save();
        // Must NOT call Minecraft.displayGuiScreen(Screen) here!
    }

    /** Closes this screen (New method added in 1.16) */
    @Override
    public void closeScreen() {
        // The method call to display the parent screen is moved to here
        this.minecraft.displayGuiScreen(parentScreen);
    }
}

More

There must be many other changes to Minecraft 1.16.x and Minecraft Forge that require mod developers to patch their mods if they want to migrate to the new game version. This blog surely cannot cover all of them; rather, its intention is to share my experience in resolving issue that can happen throughout the migration and present to you a solution if you run into an identical problem. I hope it is useful to you! If you need more resource, you can look at the source code of version 1.2.1 of my mod for Minecraft 1.16.x for some real examples.