NeoForge 20.5 for Minecraft 1.20.5

The first beta release of NeoForge for Minecraft 1.20.5, NeoForge 20.5.0-beta is now released! Please try it out, play with it, develop with it, and give us feedback! For players, you can grab the latest installer directly from https://neoforged.net/.

We are not stable yet, so expect breaking changes in the coming weeks. If you update your mod now, you will have to make a few adjustments soon. Nonetheless, we recommend that modders start to port already given the extent of the changes. If you are a contributor, now is a great time to start working on your Pull Requests.

There are a few important breaking changes in NeoForge, but most of the technical changes come from Minecraft 1.20.5 itself.


Java 21

Minecraft 1.20.5 now uses Java 21.

Modders need to update their Gradle scripts to compile and run with Java 21, for example:

- java.toolchain.languageVersion = JavaLanguageVersion.of(17)
+ java.toolchain.languageVersion = JavaLanguageVersion.of(21)

NeoGradle needs an update as well. The current latest version is 7.0.105.

Gradle itself will also need to be on version 8.6 at least.

The neoforge.mods.toml File

Starting from 20.5, the only recognized metadata file for mods is neoforge.mods.toml, still in the META-INF folder. All mods.toml files must be renamed to neoforge.mods.toml to be recognized by NeoForge. This change was made to make identifying NeoForge mods easier, considering that mods.toml is also in use by MinecraftForge.

Remember to replace mods.toml by neoforge.mods.toml in your Gradle scripts too, especially in the processResources block.

As of this blog post, jar files with no neoforge.mods.toml file will be skipped silently by NeoForge. We are actively working on a better error message, and will have it ready before the stable release.

Tag Convention Updates

Tags that are common across mods should now use the c namespace, standing for common or convention. Groups of related tags are encouraged to use a hierarchical structure, for example c:ores for all ores and c:ores/tin for all tin ores.

Modders can access definitions for these tags through the Tags class.

Datapack authors and multi-platform modders, please note that this new tag format has also been adopted by the Fabric mod loader, and should be safe to target by everyone in the modded community.

Item Data Components

ItemStacks do not have a CompoundTag anymore. Instead, a stack can now have any number of data components. Each data component is a plain Java object, identified by a DataComponentType.

Modders should generally create and register their own DataComponentTypes to store data on item stacks. The vanilla component types are available in the DataComponentTypes class.

Warning

Stack copies are shallow, which means that the components themselves are shared between stacks. Components are compared using equals for stack comparisons, and hashCode can also be called when hashing stacks. Therefore, data components must be immutable, and provide a correct implementation of equals and hashCode. NeoForge adds a sanity check to make sure that components override these methods.

For example, assuming that we want to store a single integer on an ItemStack, the code would have to change as follows:

+ DataComponentType<Integer> ENERGY = ...;

- int energy = stack.getOrCreateTag().getInt("energy");
+ int energy = stack.getOrDefault(ENERGY, 0);

- stack.getOrCreateTag().setInt("energy", 10);
+ stack.set(ENERGY, 10);

For reference, have a look at the following code:

  • All of DataComponentType and DataComponentType.Builder.
  • DataComponentHolder which is implemented by item stacks.
  • ItemStack.set(..., ...) and ItemStack.remove(...) to change the components on a stack.
  • ItemStack.update(...) for convenience when updating the value of a component.

Items can provide a default set of components.

  • See Item.Properties.component(...) to add default components to items.

Comparison with Data Attachments

Stack data attachments were removed since they are superseded by this new system. While attachments were mutable and unique to each stack, data components are shared between stacks and must therefore be immutable.

For now, data attachments are still fully functional on other types of objects (block entities, chunks, entities, and levels).

Networking Updates

This update introduces StreamCodecs, and comes with more changes to custom payloads.

Stream Codecs

Stream codecs are used to abstract over reading and writing data to network buffers, similarly to how codecs are used to abstract over reading and writing data to JSON or NBT but with a much simpler implementation. Stream codecs have two generic types: the buffer type and the data type. Common buffer types are:

  • ByteBuf for the most generic stream codecs.
  • FriendlyByteBuf for codecs that use the convenience methods in FriendlyByteBuf, or that rely on other codecs using FriendlyByteBuf.
  • RegistryFriendlyByteBuf for codecs that access registry information such as raw integer IDs.

For example, a stream codec that reads an integer will have the type StreamCodec<ByteBuf, Integer> whereas a stream codec that reads an item stack will have the type StreamCodec<RegistryFriendlyByteBuf, ItemStack>.

Stream codecs are designed to be composed together using combinators. Here are a few pointers to get you started:

  • ByteBufCodecs contains many useful stream codecs, such as ByteBufCodecs.INT and other primitive types.
  • StreamCodec.composite(...) is used to combine multiple stream codecs, for example for the fields of a class.
  • List stream codecs are created using streamCodec.apply(ByteBufCodecs.list()). Similar methods exist for other collections.
  • NeoForge provides additional helpers in its NeoForgeStreamCodecs class.

Custom Payload Changes

CustomPacketPayloads are now identified by a CustomPacketPayload.Type wrapper around a ResourceLocation. Additionally, the serialization and deserialization is handled by a StreamCodec. For the first generic parameter of the stream codec, please note that:

  • RegistryFriendlyByteBuf is only usable in play packets.
  • FriendlyByteBuf can be used for any packet.
  • ByteBuf can also be used for any packet in case the convenience FriendlyByteBuf methods are not needed.

The packet type and stream codec must be registered using the RegisterPayloadHandlersEvent event.

Network API Rework

Based on feedback from the previous iteration, we have also reorganized the network API to be more consistent and easier to use. Here is a brief summary of the changes:

  • Payload handlers run on the main thread by default. This can be changed using registrar.executesOn(HandlerThread.NETWORK).
  • IPayloadContext has been flattened into a single interface, and received a few new methods as well.
  • The registration methods in PayloadRegistrar have been renamed.
  • RegisterPayloadHandlerEvent -> RegisterPayloadHandlersEvent: renamed.
  • OnGameConfigurationEvent -> RegisterConfigurationTasksEvent: renamed.
  • IPayloadRegistrar -> PayloadRegistrar: replaced by direct class reference.
  • isConnected -> hasChannel: renamed.

We have also simplified the PacketDistributor system used to send packets. Refer to the PacketDistributor class for more details.

Updated Example

To help you get started, here is an example of a custom play payload that contains two item stacks.

public record TwoStacksPayload(ItemStack first, ItemStack second) implements CustomPacketPayload {
    // We have a type that wraps the resource location.
    public static final Type<TwoStacksPayload> TYPE = new Type<>(new ResourceLocation("mymod", "two_stacks"));

    // And we have a stream codec, here using RFBB (RegistryFriendlyByteBuf) because item stacks require it.
    public static final StreamCodec<RegistryFriendlyByteBuf, TwoStacksPayload> STREAM_CODEC = StreamCodec.composite(
            ItemStack.OPTIONAL_STREAM_CODEC,
            TwoStacksPayload::first,
            ItemStack.OPTIONAL_STREAM_CODEC,
            TwoStacksPayload::second,
            TwoStacksPayload::new);

    @Override
    public Type<TwoStacksPayload> type() {
        return TYPE;
    }
}

And the registration code will look as follows:

    public static void onPayloadRegister(RegisterPayloadHandlersEvent event) { // note: Handlers is plural
        PayloadRegistrar registrar = event.registrar("version");

        // could also use playToClient or playToServer for unidirectional packets
        registrar.playBidirectional(TwoStacksPayload.TYPE, TwoStacksPayload.STREAM_CODEC, <packet handler>);
        // and so on...
    }

The packet can be sent using the PacketDistributor:

// On the client side, send a packet to the server:
PacketDistributor.sendToServer(new TwoStacksPayload(..., ...));
// On the server side, send a packet to a specific client:
PacketDistributor.sendToClient(player, new TwoStacksPayload(..., ...));
// And so on...

DFU Update

The DFU (DataFixerUpper) library was updated from major version 6 to 7. This update fixes many longstanding annoyances with DFU, at the cost of a few breaking changes.

Here are a few highlights:

  • Many methods in ExtraCodecs (a Minecraft class that contains additional Codecs) were removed, as they now have an equivalent in DFU itself.
    • ExtraCodecs.strictOptionalField(codec, ...) is replaced by codec.optionalFieldOf(...), which is now strict by default.
    • ExtraCodecs.validate(codec, ...) is replaced by codec.validate(...).
    • ExtraCodecs.lazyInitializedCodec(() -> ...) is replaced by Codec.lazilyInitialized(() -> ...).
    • And so on…
  • Dispatch codecs (typically, codecs whose serialization depends on a type field) now require a MapCodec for the dispatched type. For example, recipe serializers now return a MapCodec instead of a Codec.
    • When using RecordCodecBuilder, use RecordCodecBuilder.mapCodec instead of .create to produce a MapCodec.
    • Many combinators such as .xmap will work with MapCodecs too.
    • Turning a MapCodec to a Codec is done using mapCodec.codec().
  • Util.getOrThrow(dataResult, ...) is replaced by dataResult.getOrThrow(...).

Smaller Changes

Here are a few smaller changes that will impact many mods.

Event System

  • @Mod.EventBusSubscriber -> @EventBusSubscriber: moved to top-level.
  • Mod.EventBusSubscriber.Bus.FORGE -> EventBusSubscriber.Bus.GAME: renamed constant. The game bus itself is still at NeoForge.EVENT_BUS.

Game Object Serialization

  • Many serialization methods now take an additional HolderLookup.Provider registries parameter.
    • It can be used to serialize a codec to NBT: registries.buildSerializationContext(NbtOps.INSTANCE).
    • The registry context can be retrieved from a RegistryFriendlyByteBuf, or used to create one.
  • BlockEntity.save(CompoundTag tag) -> BlockEntity.saveAdditional(CompoundTag tag, HolderLookup.Provider registries).
  • Use ItemStack.save(registries) and ItemStack.parseOptional(registries, compoundTag) to save and load item stacks to/from NBT.
    • WARNING: ItemStack.save(registries, Tag) will not modify the passed tag. Make sure that you always use the returned tag, or preferable call the overload that only takes the registries.

GUI Layers

  • RegisterGuiOverlaysEvent -> RegisterGuiLayersEvent.
  • VanillaGuiOverlay -> VanillaGuiLayers.

Porting Primer

Many more changes are collected on the following Mod Migration Primer. Thanks to @ChampionAsh5357 for this write-up.

1.20.1 And 1.20.4 Plans

We will now focus on 1.20.5 going forward, but we do not abandon support for 1.20.1 and 1.20.4. As usual, any Pull Request targeting older versions will first have to be accepted and merged into the 1.20.5 branch.