MultiLoader 1.21+ · Part 15

Events and Listeners (MultiLoader 1.21+)

INTERMEDIATE MULTILOADER 1.21-1.21.1 18 min read · Aug 17, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

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.

NOTE
Complete the Getting Started tutorial before continuing. Event registration happens in each loader's mod entry point.

The Loader Abstraction Pattern

Create a class called CommonEvents in your common project. This class contains only static handler methods with no loader imports:

java
public class CommonEvents {
public static void onPlayerLogin(ServerPlayer player) {
player.sendSystemMessage(
Component.literal("Welcome, " + player.getName().getString() + "!"));
}
public static void onBlockBreak(Level level, BlockPos pos, BlockState state, Player player) {
if (state.is(BlockRegistry.NEW_DIRT.get())) {
Constants.LOG.info("{} broke a New Dirt block at {}", player.getName().getString(), pos);
}
}
}

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:

java
@EventBusSubscriber(modid = Constants.MOD_ID, bus = EventBusSubscriber.Bus.GAME)
public class ExampleEvents {
@SubscribeEvent
public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
if (event.getEntity() instanceof ServerPlayer player) {
CommonEvents.onPlayerLogin(player);
}
}
@SubscribeEvent
public static void onBlockBreak(BlockEvent.BreakEvent event) {
CommonEvents.onBlockBreak(
event.getLevel() instanceof Level level ? level : null,
event.getPos(),
event.getState(),
event.getPlayer()
);
}
}

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.

TIP
If you prefer explicit registration over the annotation, you can omit @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:

java
public class ExampleModFabric implements ModInitializer {
@Override
public void onInitialize() {
ExampleConfig.setInstance(FabricExampleConfig.load());
CommonClass.init();
registerEvents();
}
private static void registerEvents() {
// Player login
ServerEntityCombatEvents.AFTER_KILLED_OTHER_ENTITY.register((world, entity, killedEntity) -> {});
ServerLifecycleEvents.SERVER_STARTED.register(server -> {});
// Player login event
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
if (handler.getPlayer() instanceof ServerPlayer player) {
CommonEvents.onPlayerLogin(player);
}
});
// Block break event
PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, blockEntity) -> {
CommonEvents.onBlockBreak(world, pos, state, player);
});
}
}
TIP
Fabric's event callbacks are in the 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:

java
// In CommonEvents, using only base Minecraft classes
public static boolean onLivingHurt(LivingEntity entity, DamageSource source, float amount) {
// Return false to cancel, true to allow (adapt to whichever loader's boolean convention)
if (entity.level().isClientSide()) return true;
if (source.is(DamageTypes.FELL_OUT_OF_WORLD)) {
Constants.LOG.info("{} fell out of the world", entity.getName().getString());
}
return true;
}

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 GitHub
NEXT IN SERIES

Networking 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 →