Skip to Content
Hytale logoCommunity-built docsOpen source and updated by the community.
Plugin DevelopmentEvent System

Event System

Hytale’s event system allows plugins to respond to game events. The system supports both synchronous and asynchronous events, as well as special ECS-based events.

Event Types

Based on the decompiled source code, Hytale has several categories of events:

Regular Events (IBaseEvent/ICancellable)

Standard events that can be registered with simple listeners:

Player Events:

  • PlayerConnectEvent - Player joins server (getHolder(), getPlayerRef(), getWorld())
  • PlayerDisconnectEvent - Player leaves server
  • PlayerReadyEvent - Player finished loading (recommended for welcome messages)
  • PlayerChatEvent - Chat messages (async, cancellable) - getSender(), getContent(), getTargets()
  • PlayerInteractEvent - Player interaction with entities/blocks
  • PlayerMouseButtonEvent - Mouse button clicks (known issues)
  • PlayerMouseMotionEvent - Mouse movement
  • AddPlayerToWorldEvent - Player added to world
  • DrainPlayerFromWorldEvent - Player removed from world
  • ChangeGameModeEvent - Game mode changed

Permission Events:

  • PlayerPermissionChangeEvent - Player permission modified
  • PlayerGroupEvent - Player group changed
  • GroupPermissionChangeEvent - Group permission modified

Inventory Events:

  • LivingEntityInventoryChangeEvent - Inventory changed
  • CraftRecipeEvent - Recipe crafted
  • InteractivelyPickupItemEvent - Item picked up
  • DropItemEvent - Item dropped

World Events:

  • DiscoverZoneEvent - Zone discovered
  • StartWorldEvent - World initialization
  • WorldPathChangedEvent - World waypoints changed

ECS Events (CancellableEcsEvent)

Events that require the ECS system pattern with EntityEventSystem:

  • BreakBlockEvent - Block breaking (cancellable) - getTargetBlock(), getBlockType(), getItemInHand()
  • PlaceBlockEvent - Block placement (cancellable)
  • UseBlockEvent - Block interaction (cancellable)
  • DamageBlockEvent - Block damage
  • DamageEvent - Damage events (requires DamageModule)
  • DeathEvent - Player/entity death (requires DamageModule)
  • EntityRemoveEvent - Entity removed from world

Event Priorities

The EventPriority enum controls listener execution order:

PriorityOrder
LOWESTFirst
LOWSecond
NORMALDefault
HIGHFourth
HIGHESTLast
getEventRegistry().register(EventPriority.HIGH, PlayerReadyEvent.class, event -> { // Runs after NORMAL priority listeners });

Event Registration

Three Registration Methods

1. register() - For Synchronous Events

getEventRegistry().register(PlayerReadyEvent.class, event -> { event.getPlayer().sendMessage(Message.raw("Welcome!")); });

2. registerAsync() / registerAsyncGlobal() - For Async Events

PlayerChatEvent is asynchronous and MUST use async registration:

getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> { future.thenAccept(event -> { PlayerRef sender = event.getSender(); String message = event.getMessage(); System.out.println("Chat: " + message); }); });

3. registerGlobal() - Global Event Registration

getEventRegistry().registerGlobal(PlayerReadyEvent.class, ExampleEvent::onPlayerReady); // Static method in ExampleEvent class public static void onPlayerReady(final PlayerReadyEvent event) { final Player player = event.getPlayer(); player.sendMessage(Message.raw("Welcome to the server!")); }

ECS Event System

ECS events like BreakBlockEvent cannot be registered like normal events. They require extending EntityEventSystem:

BlockBreakEvent Example

public class BlockBreakSystem extends EntityEventSystem\<EntityStore, BreakBlockEvent\> { public BlockBreakSystem() { super(BreakBlockEvent.class); } @Override public void handle(int i, @Nonnull ArchetypeChunk\<EntityStore\> archetypeChunk, @Nonnull Store\<EntityStore\> store, @Nonnull CommandBuffer\<EntityStore\> commandBuffer, @Nonnull BreakBlockEvent event) { // IMPORTANT: Skip air blocks if (event.getBlockType() == BlockType.EMPTY) return; // Get player from ECS Ref\<EntityStore\> ref = archetypeChunk.getReferenceTo(i); Player player = store.getComponent(ref, Player.getComponentType()); if (player == null) return; // Send message player.sendMessage(Message.raw("You broke: " + event.getBlockType().getId())); } @Nullable @Override public Query\<EntityStore\> getQuery() { return PlayerRef.getComponentType(); } } // Register in your plugin's setup() method: @Override protected void setup() { getEntityStoreRegistry().registerSystem(new BlockBreakSystem()); }

Event Examples

Player Join Event

@Override protected void setup() { getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> { Player player = event.getPlayer(); player.sendMessage(Message.raw("Welcome to the server!")); // Log to console getLogger().atInfo().log("Player joined: " + player.getName()); }); }

Chat Event (Async)

@Override protected void setup() { getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> { future.thenAccept(event -> { // Cancel event if message contains spam if (event.getMessage().contains("spam")) { event.cancel(); return; } // Modify message String modified = "[Player] " + event.getMessage(); // Process modified message }); }); }

Player Disconnect Event

getEventRegistry().register(PlayerDisconnectEvent.class, event -> { Player player = event.getPlayer(); getLogger().atInfo().log("Player left: " + player.getName()); // Save player data savePlayerData(player); });

Death Event (ECS)

public class DeathSystem extends EntityEventSystem\<EntityStore, DeathEvent\> { public DeathSystem() { super(DeathEvent.class); } @Override public void handle(int i, @Nonnull ArchetypeChunk\<EntityStore\> chunk, @Nonnull Store\<EntityStore\> store, @Nonnull CommandBuffer\<EntityStore\> buffer, @Nonnull DeathEvent event) { Ref\<EntityStore\> ref = chunk.getReferenceTo(i); Player player = store.getComponent(ref, Player.getComponentType()); if (player != null) { player.sendMessage(Message.raw("You died!")); } } @Nullable @Override public Query\<EntityStore\> getQuery() { return PlayerRef.getComponentType(); } } // Don't forget to add dependency in manifest.json: // "Dependencies": { "Hytale:DamageModule": "*" }

Getting Player Information from Events

A common challenge is accessing player data from events. Here are patterns discovered from the decompiled source:

Getting Player UUID

// Method 1: From PlayerRef component (recommended for ECS contexts) UUID uuid = event.getPlayerRef() .getStore() .getComponent(event.getPlayerRef(), PlayerRef.getComponentType()) .getUuid(); // Method 2: From Player object directly Player player = event.getPlayer(); UUID uuid = player.getUuid(); // Method 3: Using Universe (if you only have username/uuid) Player player = Universe.get().getPlayer(uuid);

Getting Player from ECS Events

For ECS events like BreakBlockEvent, you cannot use event.getPlayer() directly:

// In EntityEventSystem handle() method: Ref<EntityStore> ref = archetypeChunk.getReferenceTo(i); Player player = store.getComponent(ref, Player.getComponentType()); // Get player's transform (position) TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType());

Getting World and Store References

// From player reference Ref<EntityStore> playerRef = player.getReference(); World world = player.getWorld(); Store<EntityStore> store = world.getEntityStore().getStore(); // Get transform component TransformComponent transform = store.getComponent(playerRef, TransformComponent.getComponentType());

Important Notes from Community

PlayerChatEvent is Asynchronous

“PlayerChatEvent is asynchronous - you MUST use registerAsync or registerAsyncGlobal”

This is a common mistake. Using register() instead of registerAsync() will not work.

No PlayerJoinEvent

There is no PlayerJoinEvent. Use either:

  • PlayerConnectEvent - When player connects
  • PlayerReadyEvent - When player is ready (recommended)

BreakBlockEvent Fires for Air

// ALWAYS check for empty blocks! if (event.getBlockType() == BlockType.EMPTY) return;

“BreakBlockEvent fires even when you break air blocks” - Community testing

PlayerMouseButtonEvent Issues

“PlayerMouseButtonEvent has issues - multiple reports of it not firing properly”

Known bug in early versions. May be fixed in later releases.

ECS Events Cannot Use Normal Registration

// ❌ WRONG - This won't work for BreakBlockEvent getEventRegistry().register(BreakBlockEvent.class, event -> { // This will never fire }); // ✅ CORRECT - Use EntityEventSystem getEntityStoreRegistry().registerSystem(new BlockBreakSystem());

Event Cancellation

Cancellable events implement ICancellable:

getEventRegistry().registerAsync(PlayerChatEvent.class, future -> { future.thenAccept(event -> { if (shouldCancel(event)) { event.cancel(); // Prevent event from happening } }); });

Event Priorities

While not explicitly documented, event order can matter:

// Register multiple listeners for same event getEventRegistry().register(PlayerReadyEvent.class, this::firstHandler); getEventRegistry().register(PlayerReadyEvent.class, this::secondHandler); // Both will be called, order may not be guaranteed

Performance Considerations

Use Async for Heavy Operations

getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> { future.thenAcceptAsync(event -> { // Heavy database operations database.logChatMessage(event.getMessage()); }, executorService); });

Avoid Blocking in Event Handlers

// ❌ BAD - Blocking main thread getEventRegistry().register(PlayerReadyEvent.class, event -> { Thread.sleep(1000); // DON'T DO THIS }); // ✅ GOOD - Run async if needed getEventRegistry().register(PlayerReadyEvent.class, event -> { CompletableFuture.runAsync(() -> { // Long operation }); });

Module Dependencies

Some events require specific modules to be loaded:

manifest.json:

{ "dependencies": [ "Hytale:DamageModule" ] }

Required for:

  • DeathEvent
  • DamageEvent

Debugging Events

Check if Events are Firing

@Override protected void setup() { getEventRegistry().register(PlayerReadyEvent.class, event -> { getLogger().atInfo().log("PlayerReadyEvent fired!"); System.out.println("Event: " + event); }); }

Common Issues

IssueSolution
Chat event not firingUse registerAsync() not register()
Break block event not firingUse EntityEventSystem pattern
Events fire multiple timesCheck you’re not registering twice
Death event not workingAdd Hytale:DamageModule dependency

Best Practices

1. Register in setup()

@Override protected void setup() { // Register all events here getEventRegistry().registerGlobal(PlayerReadyEvent.class, this::onJoin); }

2. Clean Up in shutdown()

@Override protected void shutdown() { // Unregister events if needed // Most cleanup is automatic }

3. Use Method References

// Clean and readable getEventRegistry().registerGlobal(PlayerReadyEvent.class, this::handleJoin); private void handleJoin(PlayerReadyEvent event) { // Handler logic }

4. Separate Event Handlers

public class EventHandlers { public static void onPlayerReady(PlayerReadyEvent event) { // Handle join } public static void onPlayerLeave(PlayerDisconnectEvent event) { // Handle leave } } // Register getEventRegistry().registerGlobal(PlayerReadyEvent.class, EventHandlers::onPlayerReady);

Complete Event Handler Example

public class MyPlugin extends JavaPlugin { @Override protected void setup() { // Regular events getEventRegistry().registerGlobal(PlayerReadyEvent.class, this::onJoin); getEventRegistry().register(PlayerDisconnectEvent.class, this::onLeave); // Async events getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> { future.thenAccept(this::onChat); }); // ECS events getEntityStoreRegistry().registerSystem(new BlockBreakSystem()); getEntityStoreRegistry().registerSystem(new DeathSystem()); } private void onJoin(PlayerReadyEvent event) { Player player = event.getPlayer(); player.sendMessage(Message.raw("Welcome!")); } private void onLeave(PlayerDisconnectEvent event) { Player player = event.getPlayer(); getLogger().atInfo().log(player.getName() + " left"); } private void onChat(PlayerChatEvent event) { // Chat handling } }

Next Steps

Last updated on