Minestom’s event system is built around a hierarchical graph structure using EventNode, providing powerful filtering, organization, and performance optimization capabilities.
Events in Minestom flow through a tree of EventNodes, each able to filter and handle events based on type, conditions, or handler properties.
Filters based on the event handler (player, entity, instance, etc.):
EventNode.java:130-135
// Only creative playersEventNode<PlayerEvent> creativeNode = EventNode.value("creative", EventFilter.PLAYER, player -> player.getGameMode() == GameMode.CREATIVE);// Only players with specific permissionEventNode<PlayerEvent> adminNode = EventNode.value("admins", EventFilter.PLAYER, player -> player.getPermissionLevel() >= 4);
// Players with a specific tagTag<Boolean> VIP_TAG = Tag.Boolean("vip");EventNode<PlayerEvent> vipNode = EventNode.tag("vip-players", EventFilter.PLAYER, VIP_TAG);// Players with a specific tag valueTag<String> TEAM_TAG = Tag.String("team");EventNode<PlayerEvent> redTeamNode = EventNode.tag("red-team", EventFilter.PLAYER, TEAM_TAG, team -> "red".equals(team));
EventListener<PlayerMoveEvent> listener = EventListener.builder(PlayerMoveEvent.class) // Only if player moved more than 1 block .filter(event -> event.getNewPosition().distance(event.getPlayer().getPosition()) > 1.0) // Don't call if event is cancelled .ignoreCancelled(true) // Expire after 100 calls .expireCount(100) // Or expire when condition is met .expireWhen(event -> event.getPlayer().getGameMode() == GameMode.SPECTATOR) // The handler .handler(event -> { System.out.println("Player moved significantly!"); }) .build();eventNode.addListener(listener);
// Find all nodes with a specific nameList<EventNode<PlayerEvent>> nodes = rootNode.findChildren("player-handler", PlayerEvent.class);// Find by name onlyList<EventNode<Event>> nodes = rootNode.findChildren("my-node");
// Replace all nodes named "old-handler" with a new oneEventNode<PlayerEvent> newNode = EventNode.type("new-handler", EventFilter.PLAYER);rootNode.replaceChildren("old-handler", PlayerEvent.class, newNode);
EventNode<InstanceEvent> instanceEvents = instance.eventNode();instanceEvents.addListener(PlayerSpawnEvent.class, event -> { event.getPlayer().sendMessage("Welcome to this world!");});instanceEvents.addListener(InstanceTickEvent.class, event -> { // Called every tick for this instance});
Instance event nodes are automatically mapped to the instance, so events only reach listeners registered on the correct instance.
gameNode.addListener(AsyncPlayerConfigurationEvent.class, event -> { final Player player = event.getPlayer(); // Select random instance var instances = MinecraftServer.getInstanceManager().getInstances(); Instance instance = instances.stream() .skip(new Random().nextInt(instances.size())) .findFirst() .orElse(null); event.setSpawningInstance(instance); // Set spawn point player.setRespawnPoint(new Pos(0, 40, 0));});gameNode.addListener(PlayerSpawnEvent.class, event -> { final Player player = event.getPlayer(); player.setGameMode(GameMode.CREATIVE); player.setPermissionLevel(4); if (event.isFirstSpawn()) { player.sendNotification(new Notification( Component.text("Welcome!"), FrameType.TASK, Material.IRON_SWORD )); }});
gameNode.addListener(PlayerBlockInteractEvent.class, event -> { var player = event.getPlayer(); var instance = event.getInstance(); var block = event.getBlock(); // Bed interaction if (block.key().asMinimalString().endsWith("_bed")) { var pos = event.getBlockPosition(); var isHead = "head".equals(block.getProperty("part")); var facing = BlockFace.valueOf(block.getProperty("facing").toUpperCase()); // Find other half of bed var other = isHead ? pos.add(facing.getOppositeFace().toDirection().vec().asPos()) : pos.add(facing.toDirection().vec().asPos()); player.enterBed(isHead ? pos : other); }});
// By default, listeners ignore cancelled eventseventNode.addListener(PlayerMoveEvent.class, event -> { // Won't run if event is cancelled});// Explicitly handle cancelled eventsEventListener.builder(PlayerMoveEvent.class) .ignoreCancelled(false) .handler(event -> { // Runs even if event is cancelled }) .build();
For advanced use cases, you can map specific objects to nodes:
EventNode.java:345
// Map a player to a specific nodeEventNode<PlayerEvent> playerNode = globalHandler.map( player, EventFilter.PLAYER);// Events for this player will reach this nodeplayerNode.addListener(PlayerMoveEvent.class, event -> { // Only for this specific player});// Unmap when doneglobalHandler.unmap(player);
Mapped nodes have significant performance overhead due to map lookups. Use sparingly and prefer filtered nodes when possible.
// Good - flat structurerootNode.addChild(playerNode);rootNode.addChild(entityNode);rootNode.addChild(instanceNode);// Less efficient - deep nestingrootNode.addChild(level1Node);level1Node.addChild(level2Node);level2Node.addChild(level3Node); // Slower event propagation