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 orderHIGHEST,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 usesNORMAL, 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 isNORMAL). - Priorities apply to every event, including
EntityEvents.ALLOW_DAMAGE,MODIFY_DAMAGE, andPlayerEvents.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.
EventResult | Meaning |
|---|---|
PASS | No opinion. The next listener runs, and if nobody decides, the game goes on as usual |
ALLOW | Lets the action happen and skips the remaining listeners. The game's own checks (tools, permissions, and so on) still apply |
DENY | Cancels 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
PASSdecides, and the listeners after it are not called. - A decision listener that throws counts as
PASSand is disabled (POLY-EVENT-001). A listener that returnsnullis treated the same way (POLY-EVENT-003). - The older events keep their own rules:
EntityEvents.ALLOW_DAMAGEruns every listener and cancels if any returnsfalse,MODIFY_DAMAGEhands each listener the previous result, andPlayerEvents.LOGIN_CHECKuses 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.
| Event | Listener | Timing |
|---|---|---|
ServerEvents.STARTING | server -> … (ServerView) | Registries and data packs are loaded, and the worlds are about to be created |
ServerEvents.STARTED | server -> … | Once per server, just before the first tick |
ServerEvents.TICK | server -> … | At the start of every tick |
ServerEvents.STOPPING | server -> … | When shutdown begins (before the world is saved) |
ServerEvents.STOPPED | server -> … | 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
| Event | Listener | Timing |
|---|---|---|
PlayerEvents.JOINED | player -> … | The player has finished connecting and entered the world |
PlayerEvents.LEAVING | player -> … | 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.
COPYfires while that object is being built, after the game copied what it keeps (the inventory withkeepInventory, everything when leaving the End) and after Poly Mod Loader copiedEntityDataKeydata. Copy anything else your mod keeps per player object here. The new player is not in a world yet.RESPAWNEDfires after the new player object was placed in its world.returnedFromEndistruewhen the player left the End through the exit portal (alive), andfalseafter 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.
| Event | Listener | Timing |
|---|---|---|
InteractionEvents.USE_BLOCK | (player, hand, stack, pos, face) -> InteractionResult | A 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) -> InteractionResult | A 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) -> InteractionResult | A player right-clicks an entity, before the entity or the held item reacts |
InteractionEvents.ATTACK_ENTITY | (player, target) -> EventResult | A player attacks (left-clicks) an entity, before damage and knockback |
handis aHand(MAIN_HAND,OFF_HAND, indev.polymodloader.api.entity).stackis the live stack in that hand, possibly empty.- Use events:
PASSlets the next listener and then the game handle the click.SUCCESSorCONSUMEends the click as handled, so the game's own behavior (opening a chest, placing a block, eating, trading, mounting) does not happen.FAILends it as refused. Once a result is decided, the server sends the inventory again to correct the client's prediction. - Attack:
DENYcancels 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
| Event | Listener | Timing |
|---|---|---|
BlockEvents.BREAK | (world, pos, block, player) -> EventResult | A 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) -> EventResult | A 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.
BREAKfires 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. WithDENY, the player's client is told the block (and its block entity) is still there.BREAKfires beforeBROKEN.- With
PLACEreturningDENY, the item is not used up, the click result isFAIL, and the player's client is synced again. PLACEandPLACEDfire 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
| Event | Listener | Timing |
|---|---|---|
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) -> boolean | Just before damage. If any listener returns false, the damage is canceled |
EntityEvents.MODIFY_DAMAGE | (entity, damage) -> float | Changes 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_WORLDcannot be canceled. Do not remove or move the entity from the listener. Schedule such changes withWorldView.schedule.- Chunks full of entities load often, so keep
JOINED_WORLDlisteners light.
World
Per dimension and server start, the order is LOADED → READY → SAVING (on every save) → UNLOADED.
| Event | Listener | Timing |
|---|---|---|
WorldEvents.LOADED | world -> … | 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.READY | world -> … | The first tick after a dimension is loaded or created. Once per dimension on each server start. A good time to read storage |
WorldEvents.SAVING | world -> … | 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.UNLOADED | world -> … | 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.
| Event | Listener | Timing |
|---|---|---|
ClientEvents.TICK | view -> … (ClientView) | A client tick ended (20 times per second, including on the title screen) |
ClientEvents.FIELD_OF_VIEW | fov -> float | Every frame, after the game computes the field of view (sprinting, underwater) |
ClientEvents.JOINED_WORLD | view -> … | The player entered a world (single player or server) and the player and world exist. Once per connection |
ClientEvents.LEFT_WORLD | view -> … | 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.