NeoForge uses an annotation-based event bus, while Fabric uses functional callback registrations. Because these APIs are incompatible, the multiloader approach is to keep all game logic in the common module and invoke it from thin loader-specific glue classes. This tutorial demonstrates the pattern with two examples: a player login message and a block break listener.
The Loader Abstraction Pattern
Create a class called CommonEvents in your common project. This class contains only static handler methods with no loader imports:
Both loaders call into these static methods. The methods themselves import only common Minecraft classes, so they compile against either loader without modification.
NeoForge Events
In the NeoForge subproject, create an ExampleEvents class annotated with @EventBusSubscriber. All @SubscribeEvent methods in the class are registered automatically:
The bus = EventBusSubscriber.Bus.GAME parameter targets the game event bus, which handles in-game events such as player actions and block interactions. Use Bus.MOD for mod lifecycle events such as FMLCommonSetupEvent and GatherDataEvent.
@EventBusSubscriber and instead call NeoForge.EVENT_BUS.register(new ExampleEvents()) in your mod constructor. Instance methods work with the explicit approach; static methods work with both.Fabric Callbacks
Fabric uses static callback registrations in your mod initialiser. Each event is a named callback with its own functional interface:
net.fabricmc.fabric.api packages. Each feature area (player events, world events, block events) has its own API class. The Fabric API Javadoc lists all available event callbacks and their functional interface signatures.Common Event Handlers
For events that do not have a direct equivalent on both loaders, write the logic in common as a static method and guard with a loader check if needed:
The NeoForge side calls this from a LivingIncomingDamageEvent handler, and the Fabric side calls it from a EntityDamageCallback registration. The common method itself never changes regardless of which loader invokes it.
Testing
Launch both the Fabric and NeoForge clients separately and log in. You should see the welcome message appear in chat. Break a New Dirt block in either environment and confirm the log message appears in the console. This verifies that both loader glue classes are correctly forwarding to the common handlers.
You can find the source for this tutorial here:
View Source on GitHubNetworking and Custom Packets (MultiLoader 1.21+)
Define a CustomPacketPayload record with a StreamCodec in common, register it via NeoForge RegisterPayloadsEvent and Fabric PayloadTypeRegistry, and abstract the send API behind a common NetworkHelper interface.
Continue →