Skip to main content

Introduction

NeoForge decided to rewrite a few systems so porting a mod from Forge to NeoForge is not always trivial. This tutorial will help you with that. Note that this is far from complete and only touches a few areas that have changed.

Build Setup

There are a few things you need to change in your build files.

settings.gradle

Let's start with settings.gradle. Use the following template:

pluginManagement {
    repositories {
        mavenLocal()
        gradlePluginPortal()
        maven { url = 'https://maven.neoforged.net/releases' }
    }
}

plugins {
    id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0'
}

gradle.properties

Then in gradle.properties you can have this. Of course change the versions to the versions you want to use. This property file also enables parchment which makes it so that you get better names for parameters in Minecraft code:

org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false

minecraft_version=1.20.4
minecraft_version_range=[1.20.4,1.21)
neo_version=20.4.151-beta
neo_version_range=[20.4,)
loader_version_range=[2,)

neogradle.subsystems.parchment.minecraftVersion=1.20.3
neogradle.subsystems.parchment.mappingsVersion=2023.12.31


## Mod Properties

mod_id=tut2block
mod_name=Tutorial 2 Block
mod_license=MIT
mod_version=1.0.0
mod_group_id=com.mcjty.tut2block
mod_authors=McJty
mod_description=Tutorial V4, Episode 2\nA more complex block

top_version=1.20.4_neo-11.0.1-2
jei_version=17.3.0.48

build.gradle

Here is an example of a build.gradle file:

plugins {
    id 'java-library'
    id 'eclipse'
    id 'idea'
    id 'maven-publish'
    id 'net.neoforged.gradle.userdev' version '7.0.80'
}

version = mod_version
group = mod_group_id

repositories {
    mavenLocal()
}

base {
    archivesName = mod_id
}

java.toolchain.languageVersion = JavaLanguageVersion.of(17)

runs {
    configureEach {
        systemProperty 'forge.logging.markers', 'REGISTRIES'
        systemProperty 'forge.logging.console.level', 'debug'
        modSource project.sourceSets.main
    }

    client {
        systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
    }

    server {
        systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
        programArgument '--nogui'
    }

    gameTestServer {
        systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
    }

    data {
        programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath()
    }
}

sourceSets.main.resources { srcDir 'src/generated/resources' }

repositories {
    maven { // JEI
        url "https://maven.blamejared.com"
    }
    maven { // TOP
        url "https://maven.k-4u.nl"
    }
}

dependencies {
    implementation "net.neoforged:neoforge:${neo_version}"

    compileOnly "mezz.jei:jei-${minecraft_version}-common-api:${jei_version}"
    compileOnly "mezz.jei:jei-${minecraft_version}-neoforge-api:${jei_version}"
    runtimeOnly "mezz.jei:jei-${minecraft_version}-neoforge:${jei_version}"

    implementation "mcjty.theoneprobe:theoneprobe:${top_version}"
}

tasks.withType(ProcessResources).configureEach {
    var replaceProperties = [
            minecraft_version   : minecraft_version, minecraft_version_range: minecraft_version_range,
            neo_version         : neo_version, neo_version_range: neo_version_range,
            loader_version_range: loader_version_range,
            mod_id              : mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version,
            mod_authors         : mod_authors, mod_description: mod_description,
    ]
    inputs.properties replaceProperties

    filesMatching(['META-INF/mods.toml']) {
        expand replaceProperties + [project: project]
    }
}

jar {
    manifest {
        attributes([
                "Specification-Title"     : mod_id,
                "Specification-Vendor"    : mod_authors,
                "Specification-Version"   : "1", // We are version 1 of ourselves
                "Implementation-Title"    : project.name,
                "Implementation-Version"  : project.jar.archiveVersion,
                "Implementation-Vendor"   : mod_authors,
                "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
        ])
    }
}

jar.finalizedBy('reobfJar')

publishing {
    publications {
        mavenJava(MavenPublication) {
            artifact jar
        }
    }
    repositories {
        maven {
            url "file://${project.projectDir}/mcmodsrepo"
        }
    }
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
}

The main mod file

A few things have changed in the main mod. First the mod constructor can now have optional parameters:

  • The mod bus can now be obtained through a parameter in the constructor
  • The side at which the mod is running can be obtained through a parameter in the constructor

These parameters are optional. If you don't need them you don't have to use them. But here is an example of a mod constructor that uses both parameters:

    public MyMod(IEventBus modEventBus, Dist dist) {
        modEventBus.addListener(...);
        if (dist.isClient()) {
            ...
        }
    }

If you needed to register events on the forge bus then you have to use the NeoForge.EVENTBUS now.

Registration of objects

The registration of blocks, items, block entities and other registry objects has also changed a bit. There are now a few possibilities:

For blocks and items there are basically three options. First option is to use a simple supplier:

    public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(BuiltInRegistries.BLOCK, MODID);
    public static final Supplier<Block> MY_BLOCK = BLOCKS.register("my_block", MyBlock::new);

Another option is to use the new DeferredHolder:

    public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(BuiltInRegistries.BLOCK, MODID);
    public static final DeferredHolder<Block, MyBlock> MY_BLOCK = BLOCKS.register("my_block", MyBlock::new);

Only for blocks and items there is also the option to use a more compact version:

    public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(Tutorial2Block.MODID);
    public static final DeferredBlock<MyBlock> COMPLEX_BLOCK = BLOCKS.register("my_block", MyBlock::new);

Networking

Networking has changed a lot. The SimpleChannel class is gone. More information about this can be found at the networking refactor documentation.

In short, the easiest way to port your old networking code is to convert every packet to a record like this:

public record MyPacket(... fields ...) implements CustomPacketPayload {

    public static final ResourceLocation ID = new ResourceLocation(MODID, "unique_name");

    public static PacketHitToServer create(FriendlyByteBuf buf) {
        return new PacketHitToServer(... fields ...);
    }

    @Override
    public void write(FriendlyByteBuf buf) {
        // Write fields to the buffer
    }

    @Override
    public ResourceLocation id() {
        return ID;
    }

    public void handle(PlayPayloadContext ctx) {
        ctx.workHandler().submitAsync(() -> {
            // Handle the packet
        });
    }
}

Then you need to register this packet in the RegisterPayloadHandlerEvent. In this example we register our packet as a packet that will be sent from client to server. But the other way is also possible of course:

    public static void onRegisterPayloadHandler(RegisterPayloadHandlerEvent event) {
        final IPayloadRegistrar registrar = event.registrar(MODID)
                .versioned("1.0")
                .optional();
        registrar.play(MyPacket.ID, MyPacket::create, handler -> handler
                .server(MyPacket::handle));
    }

This event has to be added to the mod event bus. You can do that in the constructor of the mod like this:

    public MyMod(IEventBus modEventBus, Dist dist) {
        modEventBus.addListener(this::onRegisterPayloadHandler);
    }

Capabilities

Another system that has changed a lot in NeoForge is the capability system. Read the capability rework documentation for more information. Important to note is that capabilities have now split in two separate and independent systems. One is for adding data to objects (called Data attachments) and the other is for adding behaviour (called Capabilities).

Getting a standard capability from a block entity

In Forge you would use blockentity.getCapability() to get a capability from a block entity. This is no longer the case. Block entity capabilities are now accessed from the level like this:

   IItemHandler handler = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, direction);

Similar for energy and fluid capabilities.

Adding a capability to a block entity

Adding a capability has also changed completely. Let's say you want to add an item handler to a block entity. Here is some example code:

public class MyBlockEntity extends BlockEntity {

    private final Lazy<IItemHandler> itemHandler = Lazy.of(() -> createItemHandler());

    @Nonnull
    private ItemStackHandler createItemHandler() {
        return new ItemStackHandler(SLOT_COUNT) {
            @Override
            protected void onContentsChanged(int slot) {
                setChanged();
                level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_ALL);
            }
        };
    }

    public IItemHandler getItemHandler() {
        return itemHandler.get();
    }
}

Then you need to register this in the RegisterCapabilitiesEvent:

    public static void onRegisterCapabilities(RegisterCapabilitiesEvent event) {
        event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, Registration.MY_BLOCK_ENTITY.get(), (o, direction) -> o.getItemHandler());
    }

This also has to be registered on the mod event bus:

    public MyMod(IEventBus modEventBus, Dist dist) {
        modEventBus.addListener(this::onRegisterCapabilities);
    }

Opening a gui

To open a gui server side you cannot use NetworkHooks.openScreen() anymore. Instead you do:

    player.openMenu(containerProvider, buf -> buf.writeBlockPos(pos));

And instead of IForgeMenuType.create you nave to use IMenuTypeExtension.create.