NeoForge 21.9 for Minecraft 1.21.9

The first beta release of NeoForge 21.9 for Minecraft 1.21.9 is now available: 21.9.0-beta. NeoForge 21.9 comes with two large refactors, concerning FML and the Transfer APIs. Additionally, Minecraft 1.21.9 itself comes with many changes.

FML changes

FML is the part of a NeoForge installation, both for users and for modders, that handles the startup of Minecraft, locates the mods and libraries from various sources, handles mod dependency resolution, and so on…

This release comes with a large update and refactor of FML, completely rewriting how the game is started and the classloader is setup, with the goal of making FML more straightforward and also slightly faster.

In the coming weeks, we will also look into reworking how transformations via coremods and ILaunchPluginService operate. Note that this will not affect mixins.

Access to FML state

Most of FML’s state must now be accessed via FMLLoader.getCurrent(). For the common case of checking the current distribution and whether the environment is production, static getters are available in FMLEnvironment.

Migration should be quite mechanical:

  // Check for client vs dedicated service distribution:
- FMLEnvironment.dist
+ FMLEnvironment.getDist()

  // Check for development vs production:
- FMLEnvironment.production
+ FMLEnvironment.isProduction()

  // Access the current game directory:
- FMLLoader.getGamePath()
+ FMLLoader.getCurrent().getGameDir()

Additional runtime classpath / Runtime dependencies

Previous versions of FML would only load mods from the classpath, but not libraries. This meant that adding external libraries required special handling, such as using additionalRuntimeClasspath with ModDevGradle (MDG) or adding runtime dependencies for the runs with NeoGradle (NG).

FML will now load anything that is available on the classpath in development, including libraries. Minecraft, mods, and game libraries1 will be loaded in a transformable module layer, whereas other libraries will remain available through the normal classloader. As a result, library dependencies can now be added through standard Gradle means without needing to add them to a special classpath:

  dependencies {
      // This is still required to add the library in your jar and at compile+run time.
      jarJar(implementation("org.commonmark:commonmark:0.21.0"))
-     // This can be removed since implementation dependencies are already made available at runtime:
-     additionalRuntimeClasspath "org.commonmark:commonmark:0.21.0"
}

Adding dependencies to additionalRuntimeClasspath has no effect anymore. Future versions of MDG will likely disallow using it entirely in Minecraft 1.21.9 and newer.

Note that the latest versions of the Gradle plugins are 2.0.110 for MDG and 7.0.192 for NG. Remember to update your Gradle plugin before or after the migration to Minecraft 1.21.9.

Other FML usage

Modders making use of other advanced FML features, such as language loaders or other sorts of FML plugin developers, will be affected to a larger extent. Don’t hesitate to come and ask for support in the NeoForge Discord server if you need help ugrading.

Transfer rework

A large rework of the Transfer APIs was merged in NeoForge 21.9.1-beta. You can either update directly to the latest NeoForge version, or target 21.9.0-beta first so that you can adapt to the transfer rework separately.

This change affects the IItemHandler, IFluidHandler, IFluidHandlerItem and IEnergyStorage interfaces, the corresponding capabilities, and all the supporting classes. See the corresponding blog post for a detailed explanation, including porting recommendations.

Level rendering changes

Vanilla separated level rendering into an extraction phase of render state from the level, followed by a render phase that receives the render state and submits things to render.

Except for debug renderers, in 1.21.9 all level rendering is thus migrated to a render state.

Entities

EntityRenderers were already migrated to a render state in 1.21.6. They now submit to a SubmitNodeCollector instead of a MultiBufferSource. The actual rendering will be handled later for improved batching.

Block entity renderers

BlockEntityRenderers have been migrated to the render state system as well. They now have an additional type parameter – the associated render state – and the old render method is replaced by 3 new methods:

  • createRenderState: creates a new instance of the render state.
  • extractRenderState: writes the current state of the block entity to an instance of the render state.
  • submit: reads an instance of the render state and submits it to a SubmitNodeCollector.

For example a simple block entity renderer that rendered an item stack with a specifing facing might have looked like this in the old system:

public class MyBlockEntityRenderer implements BlockEntityRenderer<MyBlockEntity> {
    /* other methods omitted */

    @Override
    public void render(MyBlockEntity blockEntity, float partialTicks, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int overlayCoord, Vec3 cameraPos) {
        poseStack.pushPose();
        poseStack.translate(0.5, 0.5, 0.5);
        poseStack.mulPose(blockEntity.getFacing().getRotation());
        poseStack.translate(-0.5, -0.5, -0.5);

        ItemRenderer itemRenderer = Minecraft.getInstance().getItemRenderer();
        itemRenderer.renderStatic(blockEntity.getItem(), /* ... */);
    }
}

Now, a render state class is needed:

class MyBlockEntityRenderState extends BlockEntityRenderState {
    // Mutable field to hold the facing:
    Direction facing;
    // Field to hold the mutable item stack render state:
    final ItemStackRenderState item = new ItemStackRenderState();
}

Note that the item stack has its own render state that should be stored.

The block entity renderer now looks like the following:

// Note the extra type parameter:
public class MyBlockEntityRenderer implements BlockEntityRenderer<MyBlockEntity, MyBlockEntityRenderState> {
    // We'll need a field for the item model resolver
    private final ItemModelResolver itemModelResolver;

    public MyBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
        this.itemModelResolver = context.itemModelResolver();
    }

    /* other methods omitted */

    // createRenderState creates a new instance of the render state:
    @Override
    public MyBlockEntityRenderState createRenderState() {
        return new MyBlockEntityRenderState();
    }

    // extractRenderState updates the render state from the data in the block entity:
    @Override
    public void extractRenderState(MyBlockEntity blockEntity, MyBlockEntityRenderState renderState, float partialTick, Vec3 cameraPos, @Nullable ModelFeatureRenderer.CrumblingOverlay crumblingOverlay) {
        // Update facing:
        renderState.facing = blockEntity.getFacing();
        // Update item stack render state:
        this.itemModelResolver.updateForTopItem(renderState.item, blockEntity.getItem(), /* ... */);
    }

    // submit reads the contents of the render state and submits them for rendering:
    @Override
    public void submit(MyBlockEntityRenderState renderState, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, CameraRenderState camera) {
        poseStack.pushPose();
        poseStack.translate(0.5, 0.5, 0.5);
        // Read facing from render state:
        poseStack.mulPose(renderState.facing.getRotation());
        poseStack.translate(-0.5, -0.5, -0.5);

        // Read item state from render state:
        renderState.item.submit(poseStack, /* ... */);
    }
}

Event changes

NeoForge events related to level rendering have also been migrated to separate state extraction, or are in the process of being migrated to it.

For example, custom block outline now requires subscribing to the ExtractBlockOutlineRenderStateEvent. If additional outlines should be added, then a CustomBlockOutlineRenderer must be registered to the event. The old RenderHighlightEvent was removed.

Our RenderLevelStageEvent and IDimensionSpecialEffectsExtension hooks will be migrated to separate state extraction shortly. See the corresponding PR.

Key mapping categories

Key mapping categories, previously a String, are now a KeyMapping.Category record.

  public class MyModKeyMappings {
-     public static final String MAIN_CATEGORY = "mymod.key.categories.main";
+     public static final KeyMapping.Category MAIN_CATEGORY = new KeyMapping.Category(ResourceLocation.fromNamespaceAndPath("mymod", "main"));
  }

Additionally, the category must be registered using RegisterKeyMappingsEvent.registerCategory.

Finding out what needs to be migrated

A good strategy to migrate your mod is to have a working Minecraft 1.21.8 workspace alongside your in-progress Minecraft 1.21.9 workspace. This allows you to quickly find out how to update specific pieces of code. The workflow is as follows:

  1. Find a function call that doesn’t compile anymore in 1.21.9.
  2. Find the same function call in the 1.21.8 workspace that still compiles.
  3. Right click the function call, and go to a vanilla usage of the method. Note: In IntelliJ, this requires the Minecraft sources to be attached, and the scope for the search needs to be set to All Places!
  4. Go to the same vanilla usage in 1.21.9, and compare to see how vanilla updated their code.
  5. In many cases it is enough to make the same change to your code.

This can also be applied on a larger scale than a function call, for example to learn how block entity rendering changed between 1.21.8 and 1.21.9.

Porting Primer

A porting primer covering a lot more of Minecraft’s changes is available here (courtesy of @ChampionAsh5357).

Happy porting!


  1. Game libraries are libraries that have FMLModType: GAMELIBRARY in their manifest. They can access Minecraft and mod classes, and are targetable by mixins and other bytecode transformations. ↩︎