Skip to main content
Version: Minecraft 26.2

Events

You register events with mod.events().register(event key, listener). Write listeners as lambdas.

mod.events().register(PlayerEvents.JOINED, player -> player.sendMessage("Welcome"));
  • You can register at any time, from any thread. Usually you do it in the entrypoint. A listener registered while the event is being fired runs from the next firing on.
  • If a listener throws an exception, Poly Mod Loader disables that listener and logs POLY-EVENT-001.
  • A view that a listener receives is valid only during that call.

Priorities​

mod.events().register(BlockEvents.BREAK, EventPriority.HIGH, (world, pos, block, player) ->
claimed(pos) ? EventResult.DENY : EventResult.PASS); // decides first
mod.events().register(BlockEvents.BROKEN, (world, pos, block, player) -> { /* … */ }); // no priority: NORMAL
  • Listeners run by EventPriority, in the order HIGHEST, HIGH, NORMAL, LOW, LOWEST. Listeners with the same priority run in registration order, so listeners registered from entrypoints run in mod load order.
  • register(key, listener) without a priority uses NORMAL, so mods written before priorities existed behave as before.
  • Use a higher priority to decide first in a decision event (for example, a protection mod that refuses breaking blocks in claimed areas). Use a lower one to see what the others left alone.
  • The levels have the same names and order as NeoForge's EventPriority. Fabric event phases map onto them (the default phase is NORMAL).
  • Priorities apply to every event, including EntityEvents.ALLOW_DAMAGE, MODIFY_DAMAGE, and PlayerEvents.LOGIN_CHECK.

Decision events and results​

A decision event is an event whose listeners may cancel or force what the game is about to do. Its listeners return an EventResult (dev.polymodloader.api.event), or an InteractionResult for the use events.

EventResultMeaning
PASSNo opinion. The next listener runs, and if nobody decides, the game goes on as usual
ALLOWLets the action happen and skips the remaining listeners. The game's own checks (tools, permissions, and so on) still apply
DENYCancels the action and skips the remaining listeners
  • The rule is the same for every decision event: listeners run in priority order, the first listener that does not return PASS decides, and the listeners after it are not called.
  • A decision listener that throws counts as PASS and is disabled (POLY-EVENT-001). A listener that returns null is treated the same way (POLY-EVENT-003).
  • The older events keep their own rules: EntityEvents.ALLOW_DAMAGE runs every listener and cancels if any returns false, MODIFY_DAMAGE hands each listener the previous result, and PlayerEvents.LOGIN_CHECK uses the first refusal reason.

Server​

ServerEvents fire on dedicated, integrated (single player), and game test servers, on the server thread, in this order: STARTING → STARTED → TICK (repeatedly) → STOPPING → STOPPED.

EventListenerTiming
ServerEvents.STARTINGserver -> … (ServerView)Registries and data packs are loaded, and the worlds are about to be created
ServerEvents.STARTEDserver -> …Once per server, just before the first tick
ServerEvents.TICKserver -> …At the start of every tick
ServerEvents.STOPPINGserver -> …When shutdown begins (before the world is saved)
ServerEvents.STOPPEDserver -> …Players and worlds were saved and the worlds closed

ServerView: tickCount(), playerCount(), motd(), dedicated()

mod.events().register(ServerEvents.STARTING, server -> LOGGER.info("Server starting (dedicated={})", server.dedicated()));
mod.events().register(ServerEvents.STOPPED, server -> LOGGER.info("Server stopped after {} ticks", server.tickCount()));

Player​

EventListenerTiming
PlayerEvents.JOINEDplayer -> …The player has finished connecting and entered the world
PlayerEvents.LEAVINGplayer -> …Just before the player is removed from the server (disconnect or kick)
PlayerEvents.LOGIN_CHECK(uuid, name) -> Optional<String>During login. Returning a reason refuses the player
PlayerEvents.COPY(oldPlayer, newPlayer, returnedFromEnd) -> …The game is building the new player object for a respawn and copying data from the old one
PlayerEvents.RESPAWNED(player, returnedFromEnd) -> …The new player object is in its world

All are called on the server thread. LOGIN_CHECK is described in more detail in Operations.

Respawning​

The game creates a new player object when a player respawns, after death or when leaving the End.

  • COPY fires while that object is being built, after the game copied what it keeps (the inventory with keepInventory, everything when leaving the End) and after Poly Mod Loader copied EntityDataKey data. Copy anything else your mod keeps per player object here. The new player is not in a world yet.
  • RESPAWNED fires after the new player object was placed in its world.
  • returnedFromEnd is true when the player left the End through the exit portal (alive), and false after death.
mod.events().register(PlayerEvents.COPY, (oldPlayer, newPlayer, returnedFromEnd) -> {
// Entity data was copied already; count the respawn on the new player object.
StoredData data = newPlayer.data(respawns);
data.data().putInt("count", data.data().getInt("count", 0) + 1);
data.markChanged();
});
mod.events().register(PlayerEvents.RESPAWNED, (player, returnedFromEnd) -> { /* … */ });

Interaction​

InteractionEvents (dev.polymodloader.api.event.interaction) fire on the server thread when the server handles a player's click, for connected and mock players alike. They are decision events.

EventListenerTiming
InteractionEvents.USE_BLOCK(player, hand, stack, pos, face) -> InteractionResultA player right-clicks a block, before the held item or the block reacts. Fired once per hand the game tries: the main hand first, then the off hand if the main hand passed
InteractionEvents.USE_ITEM(player, hand, stack) -> InteractionResultA player right-clicks with an item in the air (not at a block or entity), before the item reacts. Also fired for an empty hand
InteractionEvents.USE_ENTITY(player, hand, stack, target) -> InteractionResultA player right-clicks an entity, before the entity or the held item reacts
InteractionEvents.ATTACK_ENTITY(player, target) -> EventResultA player attacks (left-clicks) an entity, before damage and knockback
  • hand is a Hand (MAIN_HAND, OFF_HAND, in dev.polymodloader.api.entity). stack is the live stack in that hand, possibly empty.
  • Use events: PASS lets the next listener and then the game handle the click. SUCCESS or CONSUME ends the click as handled, so the game's own behavior (opening a chest, placing a block, eating, trading, mounting) does not happen. FAIL ends it as refused. Once a result is decided, the server sends the inventory again to correct the client's prediction.
  • Attack: DENY cancels the attack: no damage, knockback, or durability loss.
  • The client's own prediction of the click (what the client draws first) is not an event yet.
mod.events().register(InteractionEvents.USE_BLOCK, (player, hand, stack, pos, face) ->
onWard(player.world(), pos) ? InteractionResult.FAIL : InteractionResult.PASS);
mod.events().register(InteractionEvents.USE_ITEM, (player, hand, stack) ->
stack.customName().filter("Sealed"::equals).isPresent() ? InteractionResult.FAIL : InteractionResult.PASS);
mod.events().register(InteractionEvents.USE_ENTITY, (player, hand, stack, target) ->
onWard(target.world(), target.blockPos()) ? InteractionResult.FAIL : InteractionResult.PASS);
mod.events().register(InteractionEvents.ATTACK_ENTITY, (player, target) ->
onWard(target.world(), target.blockPos()) ? EventResult.DENY : EventResult.PASS);

onWard(world, pos) is a helper of the example mod (InteractionContent in examples/native-showcase-mod). It checks whether the block below pos is in a block tag, using world.isBlockIn from Tags.

Block​

EventListenerTiming
BlockEvents.BREAK(world, pos, block, player) -> EventResultA player is about to break a block (survival or creative), before it is removed. DENY keeps the block
BlockEvents.BROKEN(world, pos, block, player) -> …After a player breaks a block (survival or creative)
BlockEvents.PLACE(world, pos, block, player) -> EventResultA player is about to place a block from a block item (beds and doors included), after the game checked that the block fits. DENY refuses it
BlockEvents.PLACED(world, pos, block, player) -> …After the block was placed, its block entity data applied, and the item used up

All run on the server thread.

  • BREAK fires before the game's own checks for the held item, game master blocks, and adventure mode, so it also fires for breaks the game then refuses. With DENY, the player's client is told the block (and its block entity) is still there. BREAK fires before BROKEN.
  • With PLACE returning DENY, the item is not used up, the click result is FAIL, and the player's client is synced again.
  • PLACE and PLACED fire only for players. Placement without a player, such as by a dispenser, does not fire them.
// High priority: protection decides before other mods see the break.
mod.events().register(BlockEvents.BREAK, EventPriority.HIGH, (world, pos, block, player) -> {
if (block.equals(WARDED_BLOCK) && !player.mainHandItem().equals(SPARK)) {
return EventResult.DENY;
}
return EventResult.PASS;
});
mod.events().register(BlockEvents.PLACE, (world, pos, block, player) ->
onWard(world, pos) ? EventResult.DENY : EventResult.PASS);
mod.events().register(BlockEvents.PLACED, (world, pos, block, player) -> { /* … */ });

Entity​

EventListenerTiming
EntityEvents.JOINED_WORLD(entity, world) -> …An entity of any kind (players, mobs, items, projectiles) starts being tracked by a server world: spawned, loaded with its chunk, or arriving from another dimension
EntityEvents.ALLOW_DAMAGE(entity, damage) -> booleanJust before damage. If any listener returns false, the damage is canceled
EntityEvents.MODIFY_DAMAGE(entity, damage) -> floatChanges the amount of damage that was not canceled
EntityEvents.DIED(entity, cause) -> …A living entity died, server players included

All run on the server thread. See Damage and status effects.

  • JOINED_WORLD cannot be canceled. Do not remove or move the entity from the listener. Schedule such changes with WorldView.schedule.
  • Chunks full of entities load often, so keep JOINED_WORLD listeners light.

World​

Per dimension and server start, the order is LOADED → READY → SAVING (on every save) → UNLOADED.

EventListenerTiming
WorldEvents.LOADEDworld -> …The server created every dimension. Fired for each, before spawn chunks are prepared and before the first tick. Dimensions are not saved or ticking yet
WorldEvents.READYworld -> …The first tick after a dimension is loaded or created. Once per dimension on each server start. A good time to read storage
WorldEvents.SAVINGworld -> …A dimension is about to be saved (autosave, /save-all, or stopping), before its chunks and level data are written. Changes made now are saved with it
WorldEvents.UNLOADEDworld -> …The stopping server is about to close a dimension, after it was saved. The last moment to read it. Changes are not saved any more
WorldEvents.CHUNK_LOADED(world, chunkX, chunkZ) -> …After a chunk is fully loaded on the server (from disk or newly generated)
WorldEvents.CHUNK_UNLOADED(world, chunkX, chunkZ) -> …After a chunk is saved, just before it is unloaded

All run on the server thread. Chunk events are called often, so keep your handlers light.

Client​

Client events are called only on the client, on the client thread.

EventListenerTiming
ClientEvents.TICKview -> … (ClientView)A client tick ended (20 times per second, including on the title screen)
ClientEvents.FIELD_OF_VIEWfov -> floatEvery frame, after the game computes the field of view (sprinting, underwater)
ClientEvents.JOINED_WORLDview -> …The player entered a world (single player or server) and the player and world exist. Once per connection
ClientEvents.LEFT_WORLDview -> …Just before leaving the world. The player and world still exist
ClientEvents.ITEM_TOOLTIP(stack, lines, advanced) -> …Whenever the game builds an item's tooltip, after the game's own lines. See Items

ClientView is described in Client basics.

Buy me a coffee