MC Forge Mod Dev Blog: Migrating to Minecraft 1.16
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 theScreen
class should complete any teardown tasks when the screen is being closed, and it must contain a call tonet.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 theonClose()
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 theScreen
class is now called instead. The default implementation ofcloseScreen()
only contains a call toMinecraft.displayGuiScreen(Screen)
. -
onClose()
should only contain code that needs to be executed when the screen is being closed. It must not callMinecraft.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 callMinecraft.displayGuiScreen(Screen)
or, as a better way, use thecloseScreen()
method. Both methods would eventually cause theonClose()
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.